1
# -*- coding: iso-8859-1 -*-
3
MoinMoin - Wiki Security Interface and Access Control Lists
6
This implements the basic interface for user permissions and
7
system policy. If you want to define your own policy, inherit
8
from the base class 'Permissions', so that when new permissions
9
are defined, you get the defaults.
11
Then assign your new class to "SecurityPolicy" in wikiconfig;
12
and I mean the class, not an instance of it!
14
@copyright: 2000-2004 Juergen Hermann <jh@web.de>,
15
2003-2008 MoinMoin:ThomasWaldmann,
16
2003 Gustavo Niemeyer,
18
2007 Alexander Schremmer
19
@license: GNU GPL, see COPYING for details.
24
from MoinMoin import wikiutil, user
25
from MoinMoin.Page import Page
27
#############################################################################
28
### Basic Permissions Interface -- most features enabled by default
29
#############################################################################
31
def _check(request, pagename, username, right):
32
""" Check <right> access permission for user <username> on page <pagename>
34
For cfg.acl_hierarchic=False we just check the page in question.
36
For cfg.acl_hierarchic=True we, we check each page in the hierarchy. We
37
start with the deepest page and recurse to the top of the tree.
38
If one of those permits, True is returned.
40
For both configurations, we check acl_rights_before before the page/default
41
acl and acl_rights_after after the page/default acl, of course.
43
This method should not be called by users, use __getattr__ instead.
45
@param request: the current request object
46
@param pagename: pagename to get permissions from
47
@param username: the user name
48
@param right: the right to check
51
@return: True if you have permission or False
53
cache = request.cfg.cache
54
allowed = cache.acl_rights_before.may(request, username, right)
55
if allowed is not None:
58
if request.cfg.acl_hierarchic:
59
pages = pagename.split('/') # create page hierarchy list
61
for i in range(len(pages), 0, -1):
62
# Create the next pagename in the hierarchy
63
# starting at the leaf, going to the root
64
name = '/'.join(pages[:i])
65
# Get page acl and ask for permission
66
acl = Page(request, name).getACL(request)
69
allowed = acl.may(request, username, right)
70
if allowed is not None:
73
allowed = cache.acl_rights_default.may(request, username, right)
74
if allowed is not None:
77
if request.page is not None and pagename == request.page.page_name:
78
p = request.page # reuse is good
80
p = Page(request, pagename)
81
acl = p.getACL(request) # this will be fast in a reused page obj
82
allowed = acl.may(request, username, right)
83
if allowed is not None:
86
allowed = cache.acl_rights_after.may(request, username, right)
87
if allowed is not None:
94
""" Basic interface for user permissions and system policy.
96
Note that you still need to allow some of the related actions, this
97
just controls their behavior, not their activation.
99
When sub classing this class, you must extend the class methods, not
100
replace them, or you might break the acl in the wiki. Correct sub
101
classing looks like this:
103
def read(self, pagename):
104
# Your special security rule
108
# Do not return True or you break acl!
109
# This call will use the default acl rules
110
return Permissions.read(pagename)
113
def __init__(self, user):
114
self.name = user.name
115
self.request = user._request
117
def save(self, editor, newtext, rev, **kw):
118
""" Check whether user may save a page.
120
`editor` is the PageEditor instance, the other arguments are
121
those of the `PageEditor.saveText` method.
123
@param editor: PageEditor instance.
124
@param newtext: new page text, you can enable of disable saving according
125
to the content of the text, e.g. prevent link spam.
126
@param rev: new revision number? XXX
129
@return: True if you can save or False
131
return self.write(editor.page_name)
133
def __getattr__(self, attr):
134
""" Shortcut to export getPermission function for all known ACL rights
136
if attr is one of the rights in acl_rights_valid, then return a
137
checking function for it. Else raise an AttributeError.
139
@param attr: one of ACL rights as defined in acl_rights_valid
141
@return: checking function for that right, accepting a pagename
143
request = self.request
144
if attr not in request.cfg.acl_rights_valid:
145
raise AttributeError, attr
146
return lambda pagename: _check(self.request, pagename, self.name, attr)
149
# make an alias for the default policy
150
Default = Permissions
153
class AccessControlList:
154
''' Access Control List
156
Control who may do what on or with a wiki page.
158
Syntax of an ACL string:
160
[+|-]User[,User,...]:[right[,right,...]] [[+|-]SomeGroup:...] ...
161
... [[+|-]Known:...] [[+|-]All:...]
163
"User" is a user name and triggers only if the user matches. Up
164
to version 1.2 only WikiNames were supported, as of version 1.3,
165
any name can be used in acl lines, including name with spaces
166
using esoteric languages.
168
"SomeGroup" is a page name matching cfg.page_group_regex with
169
some lines in the form " * Member", defining the group members.
171
"Known" is a group containing all valid / known users.
173
"All" is a group containing all users (Known and Anonymous users).
175
"right" may be an arbitrary word like read, write, delete, admin.
176
Only words in cfg.acl_validrights are accepted, others are
177
ignored. It is allowed to specify no rights, which means that no
182
When some user is trying to access some ACL-protected resource,
183
the ACLs will be processed in the order they are found. The first
184
matching ACL will tell if the user has access to that resource
187
For example, the following ACL tells that SomeUser is able to
188
read and write the resources protected by that ACL, while any
189
member of SomeGroup (besides SomeUser, if part of that group)
190
may also admin that, and every other user is able to read it.
192
SomeUser:read,write SomeGroup:read,write,admin All:read
194
In this example, SomeUser can read and write but can not admin,
195
revert or delete pages. Rights that are NOT specified on the
196
right list are automatically set to NO.
200
To make the system more flexible, there are also two modifiers:
201
the prefixes "+" and "-".
203
+SomeUser:read -OtherUser:write
205
The acl line above will grant SomeUser read right, and OtherUser
206
write right, but will NOT block automatically all other rights
207
for these users. For example, if SomeUser ask to write, the
208
above acl line does not define if he can or can not write. He
209
will be able to write if acl_rights_before or acl_rights_after
210
allow this (see configuration options).
212
Using prefixes, this acl line:
214
SomeUser:read,write SomeGroup:read,write,admin All:read
218
-SomeUser:admin SomeGroup:read,write,admin All:read
222
+All:read -SomeUser:admin SomeGroup:read,write,admin
224
Notice that you probably will not want to use the second and
225
third examples in ACL entries of some page. They are very
226
useful on the moin configuration entries though.
228
Configuration options
230
cfg.acl_rights_default
231
It is is ONLY used when no other ACLs are given.
232
Default: "Known:read,write,delete All:read,write",
234
cfg.acl_rights_before
235
When the page has ACL entries, this will be inserted BEFORE
240
When the page has ACL entries, this will be inserted AFTER
245
These are the acceptable (known) rights (and the place to
246
extend, if necessary).
247
Default: ["read", "write", "delete", "admin"]
250
special_users = ["All", "Known", "Trusted"] # order is important
252
def __init__(self, cfg, lines=[]):
253
"""Initialize an ACL, starting from <nothing>.
256
self.acl = [] # [ ('User', {"read": 0, ...}), ... ]
259
self._addLine(cfg, line)
262
self.acl_lines = None
264
def _addLine(self, cfg, aclstring, remember=1):
265
""" Add another ACL line
267
This can be used in multiple subsequent calls to process longer lists.
269
@param cfg: current config
270
@param aclstring: acl string from page or cfg
271
@param remember: should add the line to self.acl_lines
276
self.acl_lines.append(aclstring)
278
# Iterate over entries and rights, parsed by acl string iterator
279
acliter = ACLStringIterator(cfg.acl_rights_valid, aclstring)
280
for modifier, entries, rights in acliter:
281
if entries == ['Default']:
282
self._addLine(cfg, cfg.acl_rights_default, remember=0)
284
for entry in entries:
287
# Only user rights are added to the right dict.
288
# + add rights with value of 1
289
# - add right with value of 0
291
rightsdict[right] = (modifier == '+')
293
# All rights from acl_rights_valid are added to the
294
# dict, user rights with value of 1, and other with
296
for right in cfg.acl_rights_valid:
297
rightsdict[right] = (right in rights)
298
self.acl.append((entry, rightsdict))
300
def may(self, request, name, dowhat):
301
""" May <name> <dowhat>? Returns boolean answer.
303
Note: this check does NOT include the acl_rights_before / _after ACL,
304
but it WILL use acl_rights_default if there is no (page) ACL.
306
if self.acl is None: # no #acl used on Page
307
acl = request.cfg.cache.acl_rights_default.acl
308
else: # we have a #acl on the page (self.acl can be [] if #acl is empty!)
310
is_group_member = request.dicts.has_member
311
group_re = request.cfg.cache.page_group_regexact
313
for entry, rightsdict in acl:
314
if entry in self.special_users:
315
handler = getattr(self, "_special_"+entry, None)
316
allowed = handler(request, name, dowhat, rightsdict)
317
elif group_re.search(entry):
318
if is_group_member(entry, name):
319
allowed = rightsdict.get(dowhat)
321
for special in self.special_users:
322
if is_group_member(entry, special):
323
handler = getattr(self, "_special_" + special, None)
324
allowed = handler(request, name, dowhat, rightsdict)
325
break # order of self.special_users is important
327
allowed = rightsdict.get(dowhat)
328
if allowed is not None:
330
return allowed # should be None
332
def getString(self, b='#acl ', e='\n'):
333
"""print the acl strings we were fed with"""
335
acl_lines = ''.join(["%s%s%s" % (b, l, e) for l in self.acl_lines])
340
def _special_All(self, request, name, dowhat, rightsdict):
341
return rightsdict.get(dowhat)
343
def _special_Known(self, request, name, dowhat, rightsdict):
344
""" check if user <name> is known to us,
345
that means that there is a valid user account present.
346
works for subscription emails.
348
if user.getUserId(request, name): # is a user with this name known?
349
return rightsdict.get(dowhat)
352
def _special_Trusted(self, request, name, dowhat, rightsdict):
353
""" check if user <name> is known AND has logged in using a trusted
354
authentication method.
355
Does not work for subsription emails that should be sent to <user>,
356
as he is not logged in in that case.
358
if (request.user.name == name and
359
request.user.auth_method in request.cfg.auth_methods_trusted):
360
return rightsdict.get(dowhat)
363
def __eq__(self, other):
364
return self.acl_lines == other.acl_lines
366
def __ne__(self, other):
367
return self.acl_lines != other.acl_lines
370
class ACLStringIterator:
371
""" Iterator for acl string
373
Parse acl string and return the next entry on each call to
374
next. Implement the Iterator protocol.
377
iter = ACLStringIterator(cfg.acl_rights_valid, 'user name:right')
378
for modifier, entries, rights in iter:
382
def __init__(self, rights, aclstring):
383
""" Initialize acl iterator
385
@param rights: the acl rights to consider when parsing
386
@param aclstring: string to parse
389
self.rest = aclstring.strip()
393
""" Required by the Iterator protocol """
397
""" Return the next values from the acl string
399
When the iterator is finished and you try to call next, it
400
raises a StopIteration. The iterator finish as soon as the
401
string is fully parsed or can not be parsed any more.
403
@rtype: 3 tuple - (modifier, [entry, ...], [right, ...])
404
@return: values for one item in an acl string
406
# Handle finished state, required by iterator protocol
412
# Get optional modifier [+|-]entries:rights
414
if self.rest[0] in ('+', '-'):
415
modifier, self.rest = self.rest[0], self.rest[1:]
417
# Handle the Default meta acl
418
if self.rest.startswith('Default ') or self.rest == 'Default':
419
self.rest = self.rest[8:]
420
entries, rights = ['Default'], []
422
# Handle entries:rights pairs
426
entries, self.rest = self.rest.split(':', 1)
429
raise StopIteration("Can't parse rest of string")
433
# TODO strip each entry from blanks?
434
entries = entries.split(',')
438
rights, self.rest = self.rest.split(' ', 1)
439
# Remove extra white space after rights fragment,
440
# allowing using multiple spaces between items.
441
self.rest = self.rest.lstrip()
443
rights, self.rest = self.rest, ''
444
rights = [r for r in rights.split(',') if r in self.rights]
446
return modifier, entries, rights
449
def parseACL(request, text):
450
""" Parse acl lines from text and return ACL object """
451
pi, dummy = wikiutil.get_processing_instructions(text)
452
acl_lines = [args for verb, args in pi if verb == 'acl']
453
return AccessControlList(request.cfg, acl_lines)