WiLiKiにアクセス制御機能を付けた
説明するより実物を見たほうが早いでしょう。
以下はdiffです。
(物好きな人は)ご自由にお使いください
diff --git a/src/wiliki.scm b/src/wiliki.scm
index e1f15c8..89ce9ce 100644
--- a/src/wiliki.scm
+++ b/src/wiliki.scm
@@ -27,6 +27,8 @@
;;;
(define-module wiliki
+ (use file.util)
+ (use rfc.cookie)
(use srfi-1)
(use srfi-11)
(use srfi-13)
@@ -43,6 +45,7 @@
(use gauche.version)
(use gauche.parameter)
(use gauche.sequence)
+ (use wiliki.auth)
(use wiliki.format)
(use wiliki.page)
(use wiliki.db)
@@ -53,6 +56,7 @@
wiliki:language-link wiliki:make-navi-button
wiliki:top-link wiliki:edit-link wiliki:history-link
wiliki:all-link wiliki:recent-link wiliki:search-box
+ wiliki:login-link
wiliki:menu-links wiliki:page-title wiliki:breadcrumb-links
wiliki:wikiname-anchor wiliki:wikiname-anchor-string
wiliki:get-formatted-page-content
@@ -131,7 +135,11 @@
(define-wiliki-action v :read (pagename)
;; NB: see the comment in format-wikiname about the order of
;; wiliki-db-get and virtual-page? check.
- (cond [(wiliki:db-get pagename) => html-page]
+ (cond [(wiliki:db-get pagename) =>
+ (lambda (page)
+ (if (wiliki:acl-viewable? (wiliki) page)
+ (html-page page)
+ (errorf "Can't view the page ~s" pagename)))]
[(virtual-page? pagename) (html-page (handle-virtual-page pagename))]
[(equal? pagename (top-page-of (wiliki)))
(let1 toppage (make
@@ -166,7 +174,9 @@
,(if page
`(,#`"mtime: ,(ref page 'mtime)\n"
"\n"
- ,(ref page 'content))
+ ,(if (wiliki:acl-viewable? (wiliki) page)
+ (ref page 'content)
+ (format #f "Can't view the page ~s" pagename)))
`(,#`"mtime: 0\n"
"\n")))))
@@ -181,7 +191,13 @@
:content `((ul
,@(map (lambda (k)
`(li ,(wiliki:wikiname-anchor k)))
- (sort (wiliki:db-map (lambda (k v) k)) string))))
+ (sort
+ (filter
+ string?
+ (wiliki:db-map
+ (lambda (k v)
+ (if (wiliki:acl-viewable? (wiliki) k v) k '()))))
+ string))))
)))
(define-wiliki-action r :read (_)
@@ -255,26 +271,76 @@
(content :convert cv-in)
(mtime :convert x->integer :default 0)
(logmsg :convert cv-in)
+ (acl :convert cv-in :default #f)
(donttouch :default #f))
((if commit cmd-commit-edit cmd-preview)
- pagename content mtime logmsg donttouch #f))
+ pagename content mtime logmsg donttouch #f acl))
;;
;; History
;;
(define-wiliki-action h :read (pagename
(s :convert x->integer :default 0))
- (cmd-history pagename s))
+ (if (wiliki:acl-viewable? (wiliki) (wiliki:db-get pagename))
+ (cmd-history pagename s)
+ (errorf (errorf "Can't view the page ~s" pagename))))
(define-wiliki-action hd :read (pagename
(t :convert x->integer :default 0)
(t1 :convert x->integer :default 0))
- (cmd-diff pagename t t1))
+ (if (wiliki:acl-viewable? (wiliki) (wiliki:db-get pagename))
+ (cmd-diff pagename t t1)
+ (errorf (errorf "Can't view the page ~s" pagename))))
(define-wiliki-action hv :read (pagename
(t :convert x->integer :default 0))
- (cmd-viewold pagename t))
-
+ (if (wiliki:acl-viewable? (wiliki) (wiliki:db-get pagename))
+ (cmd-viewold pagename t)
+ (errorf (errorf "Can't view the page ~s" pagename))))
+
+(define-wiliki-action login :write (_
+ (user :default #f)
+ (pass :default #f))
+ (parameterize ([auth-db-path (ref (wiliki) 'password-path)]
+ [temporary-directory (ref (wiliki) 'session-dir)])
+ (cond
+ [(and user pass (auth-valid-password? user pass))
+ `(,(cgi-header
+ :cookies (construct-cookie-string
+ `(("SCMSESSID" ,(auth-new-session user))))
+ :location (wiliki:url :full "~a" (ref (wiliki) 'top-page))
+ :status "302 Moved"))]
+ [else
+ (html-page
+ (make
+ :title (string-append (title-of (wiliki))": "($$ "Login"))
+ :command "c=login"
+ :content
+ `(,@(if (or user pass)
+ `((span (@ (class "wiliki-alert"))
+ "Invalid user name or password"))
+ '())
+ (form
+ (@ (method POST) (action ,(wiliki:url)))
+ (input (@ (type hidden) (name c) (value login)))
+ (table
+ (tr (td "User name")
+ (td (input (@ (type text) (name user)))))
+ (tr (td "Password")
+ (td (input (@ (type password) (name pass)))))
+ (tr (td "")
+ (td (input (@ (type submit) (value ,($$ "Login")))))))))
+ ))])))
+
+(define-wiliki-action logout :read (_)
+ (parameterize ([temporary-directory (ref (wiliki) 'session-dir)])
+ (auth-delete-session! (ref (wiliki) 'session-id))
+ `(,(cgi-header
+ :cookies (construct-cookie-string
+ `(("SCMSESSID" "" :max-age 0)))
+ :location (wiliki:url :full "~a" (ref (wiliki) 'top-page))
+ :status "302 Moved"))))
+
;;================================================================
;; WiLiKi-specific formatting routines
;;
@@ -304,7 +370,8 @@
(wiliki:make-navi-button '() ($$ "Top"))))
(define (wiliki:edit-link page)
- (and (eq? (ref (wiliki) 'editable?) #t)
+ (and (and (eq? (ref (wiliki) 'editable?) #t)
+ (wiliki:acl-editable? (wiliki) page))
(wiliki:persistent-page? page)
(wiliki:make-navi-button `((p ,(ref page 'key)) (c e)) ($$ "Edit"))))
@@ -331,6 +398,14 @@
(class "navi-button")))
)))
+(define (wiliki:login-link)
+ (if (and (ref (wiliki) 'session-dir) (ref (wiliki) 'password-path))
+ (if (ref (wiliki) 'session-id)
+ `(,(string-append (symbol->string (ref (wiliki) 'user-name)) " | ")
+ (a (@ (href ,(url "c=logout"))) ,($$ "Logout")))
+ `((a (@ (href ,(url "c=login"))) ,($$ "Login"))))
+ '()))
+
(define (wiliki:breadcrumb-links page delim)
(define (make-link-comp rcomps acc)
(if (null? acc)
@@ -365,7 +440,8 @@
(ref page 'title)))))
(define (wiliki:default-page-header page opts)
- `(,@(wiliki:page-title page)
+ `((div (@ (align "right")) ,@(wiliki:login-link))
+ ,@(wiliki:page-title page)
(div (@ (align "right")) ,@(wiliki:breadcrumb-links page ":"))
(div (@ (align "right")) ,@(wiliki:menu-links page))
(hr)))
diff --git a/src/wiliki/core.scm b/src/wiliki/core.scm
index 58fc5d0..fcf0da9 100644
--- a/src/wiliki/core.scm
+++ b/src/wiliki/core.scm
@@ -34,12 +34,14 @@
(define-module wiliki.core
(use srfi-1)
(use srfi-13)
+ (use srfi-27)
(use gauche.parameter)
(use gauche.charconv)
(use gauche.logger)
(use file.util)
(use rfc.uri)
(use www.cgi)
+ (use wiliki.auth)
(use wiliki.page)
(use util.list)
(use util.match)
@@ -84,6 +86,9 @@
wiliki:contains-spam?
wiliki:ip-blacklist wiliki:ip-blacklist-append!
wiliki:from-blacklisted-ip?
+ wiliki:acl-editable?
+ wiliki:acl-viewable?
+ wiliki:acl-changeable?
))
(select-module wiliki.core)
@@ -165,6 +170,21 @@
:init-value 40)
(textarea-cols :accessor textarea-cols-of :init-keyword :textarea-cols
:init-value 80)
+ ;; Access control list
+ (acl :accessor acl :init-keyword :acl
+ :init-value '((default . all) (admin . all)))
+ (user-name :accessor user-name :init-keyword :user-name
+ :init-value 'default)
+ ;; Session
+ (session-dir :accessor session-dir :init-keyword :session-dir
+ :init-value #f)
+ (session-id :accessor session-id :init-value #f)
+ (session-lifetime :accessor session-lifetime :init-keyword :session-lifetime
+ :init-value 3600)
+ (session-gc-divisor :accessor session-gc-divisor
+ :init-keyword :session-gc-divisor :init-value 100)
+ (password-path :accessor password-path :init-keyword :password-path
+ :init-value #f)
))
;;;==================================================================
@@ -184,7 +204,14 @@
(lambda (param)
(let ((pagename (get-page-name self param))
(command (cgi-get-parameter "c" param))
- (language (cgi-get-parameter "l" param :convert string->symbol)))
+ (language (cgi-get-parameter "l" param :convert string->symbol))
+ (key (cgi-get-parameter "SCMSESSID" param)))
+ (when (ref self 'session-dir)
+ (when (> (/ 1 (ref (wiliki) 'session-gc-divisor)) (random-real))
+ (parameterize ([temporary-directory (ref (wiliki) 'session-dir)])
+ (auth-clean-sessions! (ref self 'session-lifetime))))
+ (when key
+ (set-session-user! self key)))
(parameterize ((wiliki:lang (or language (ref self'language))))
(cgi-output-character-encoding (wiliki:output-charset))
(setup-textdomain self language)
@@ -785,7 +812,8 @@
(write (list :ctime (ref page 'ctime)
:cuser (ref page 'cuser)
:mtime (ref page 'mtime)
- :muser (ref page 'muser)))
+ :muser (ref page 'muser)
+ :acl (ref page 'acl)))
(display (ref page 'content)))))
;; Raw acessors
@@ -829,7 +857,13 @@
(write-recent-changes db r)))
(define (wiliki:db-recent-changes)
- (read-recent-changes (check-db)))
+ (fold-right
+ (lambda (p acc)
+ (if (wiliki:acl-viewable? (wiliki) (wiliki:db-get (car p)))
+ (cons p acc)
+ acc))
+ '()
+ (read-recent-changes (check-db))))
(define (wiliki:db-fold proc seed)
(dbm-fold (check-db)
@@ -849,7 +883,11 @@
(sort
(dbm-fold (check-db)
(lambda (k v r)
- (if (pred k v) (acons k (read-from-string v) r) r))
+ (if (and (not (string-prefix? " " k))
+ (wiliki:acl-viewable? (wiliki) k v)
+ (pred k v))
+ (acons k (read-from-string v) r)
+ r))
'())
(get-optional maybe-sorter
(lambda (a b)
@@ -865,6 +903,56 @@
w v (cut string-contains-ci <> key))))
maybe-sorter)))
+;;; Access control
+(define-method wiliki:parents-acl ((page ) delim)
+ (define (append-acl! rev-list acc)
+ (let* ((name (string-join (reverse rev-list) delim))
+ (page (wiliki:db-get name)))
+ (if page
+ (append! acc (ref page 'acl))
+ acc)))
+ (let ((combs (string-split (ref page 'title) delim)))
+ (pair-fold append-acl! '() (cdr (reverse combs)))))
+
+(define-method wiliki:page-acl ((self ) (page ))
+ (append (ref page 'acl)
+ (wiliki:parents-acl page ":")
+ (ref self 'acl)))
+
+(define-method wiliki:acl-editable? ((self ) (page ))
+ (let* ((acl (wiliki:page-acl self page))
+ (auth (assoc (ref self 'user-name) acl)))
+ (if (and auth (member (cdr auth)'(all write)))
+ #t
+ #f)))
+
+(define-method wiliki:acl-viewable? ((self ) (page ))
+ (let* ((acl (wiliki:page-acl self page))
+ (auth (assoc (ref self 'user-name) acl)))
+ (if (and auth (member (cdr auth) '(all write read)))
+ #t
+ #f)))
+
+(define-method wiliki:acl-viewable? ((self ) key value)
+ (let ((page (apply make :title key (read-from-string value))))
+ (wiliki:acl-viewable? self page)))
+
+(define-method wiliki:acl-changeable? ((self ) (page ))
+ (let* ((acl (wiliki:page-acl self page))
+ (auth (assoc (ref self 'user-name) acl)))
+ (if (and auth (member (cdr auth) '(all)))
+ #t
+ #f)))
+
+;;; Session
+(define-method set-session-user! ((self ) key)
+ (guard (e [else #f])
+ (parameterize ([temporary-directory (ref self 'session-dir)])
+ (let* ((user (string->symbol (auth-get-session key))))
+ (slot-set! self 'session-id key)
+ (slot-set! self 'user-name user)
+ user))))
+
;;;==================================================================
;;; Event log
;;;
diff --git a/src/wiliki/edit.scm b/src/wiliki/edit.scm
index 4414a31..eb8255f 100644
--- a/src/wiliki/edit.scm
+++ b/src/wiliki/edit.scm
@@ -41,7 +41,7 @@
(define $$ gettext)
-(define (edit-form preview pagename content mtime logmsg donttouch)
+(define (edit-form preview pagename content mtime logmsg donttouch acl)
(define (buttons)
(if preview
`((input (@ (type submit) (name preview) (value ,($$ "Preview again"))))
@@ -74,6 +74,11 @@
(rows 2)
(cols ,(ref (wiliki)'textarea-cols)))
,logmsg)
+ ,@(if acl
+ (list
+ `(p ,($$ "Access control list (please write like this: ((default . read) (admin . all))"))
+ `(input (@ (type text) (name acl) (value ,acl))))
+ '())
(br)
,@(buttons)
(br)
@@ -134,27 +139,32 @@
(errorf "Can't edit the page ~s: the database is read-only" pagename))
(let* ((page (wiliki:db-get pagename #t))
(content (or (get-old-content page) (ref page 'content)))
+ (acl (ref page 'acl))
)
+ (unless (wiliki:acl-editable? (wiliki) page)
+ (errorf "Can't edit the page ~s: the page is not editable" pagename))
(wiliki:std-page
(make
:title pagename
:content
- (edit-form #f pagename content (ref page 'mtime) "" #f)))))
+ (edit-form #f pagename content (ref page 'mtime) "" #f
+ (and (wiliki:acl-changeable? (wiliki) page) acl))))))
-(define (cmd-preview pagename content mtime logmsg donttouch restricted)
+(define (cmd-preview pagename content mtime logmsg donttouch restricted acl)
(let ((page (wiliki:db-get pagename #t)))
(wiliki:std-page
(make
:title (format #f ($$ "Preview of ~a") pagename)
:content
(edit-form (preview-box (wiliki:format-content content))
- pagename content mtime logmsg donttouch)))))
+ pagename content mtime logmsg donttouch
+ acl)))))
;; DONTTOUCH - If #t, don't update RecentChanges.
;; LIMITED - #t indicates this edit is generated procedurally, like comment
;; feature. It is allowed if EDITABLE? == limited.
-(define (cmd-commit-edit pagename content mtime logmsg donttouch limited)
+(define (cmd-commit-edit pagename content mtime logmsg donttouch limited acl)
(let ((p (wiliki:db-get pagename #t))
(now (sys-time)))
@@ -165,13 +175,18 @@
(wiliki:redirect-page (ref (wiliki)'top-page)))
(define (update-page content)
- (when (page-changed? content (ref p 'content))
- (let1 new-content
- (parameterize ([wiliki:page-stack (list p)])
- (expand-writer-macros content))
+ (when (or (page-changed? content (ref p 'content))
+ (and acl
+ (not (equal? (read-from-string acl) (ref p 'acl)))))
+ (let ((new-content
+ (parameterize ([wiliki:page-stack (list p)])
+ (expand-writer-macros content)))
+ (aclist (and acl (read-from-string acl))))
+ (unless (list? aclist) (set! aclist '()))
(write-log (wiliki) pagename (ref p 'content) new-content now logmsg)
(set! (ref p 'mtime) now)
(set! (ref p 'content) new-content)
+ (when acl (set! (ref p 'acl) aclist))
(wiliki:db-put! pagename p :donttouch donttouch)))
(wiliki:redirect-page pagename))
@@ -238,6 +253,8 @@
(when (or (not editable)
(and (not limited) (eq? editable 'limited)))
(errorf "Can't edit the page ~s: the database is read-only" pagename)))
+ (unless (wiliki:acl-editable? (wiliki) p)
+ (errorf "Can't edit the page ~s: the page is not editable" pagename))
(cond
[(suspicious?)
=> (lambda (reason)
@@ -269,7 +286,10 @@
,(wiliki:format-diff-pre diff)
(a (@ (name "edit")) (hr))
,($$ "The following shows what you are about to submit. Please re-edit the content and submit again.
")
- ,@(edit-form #f (ref page 'key) content (ref page 'mtime) logmsg donttouch)
+ ,@(edit-form #f (ref page 'key) content (ref page 'mtime) logmsg
+ donttouch
+ (and (wiliki:acl-changeable? (wiliki) page)
+ (ref page 'acl)))
))))
(define (preview-box content)
diff --git a/src/wiliki/page.scm b/src/wiliki/page.scm
index 02d04d1..c4db8ae 100644
--- a/src/wiliki/page.scm
+++ b/src/wiliki/page.scm
@@ -71,6 +71,8 @@
(cuser :init-value #f :init-keyword :cuser)
(mtime :init-value #f :init-keyword :mtime)
(muser :init-value #f :init-keyword :muser)
+ ;; acl - Access control list.
+ (acl :init-value '() :init-keyword :acl)
))
;;==================================================================