347 lines
12 KiB
Python
347 lines
12 KiB
Python
# -*- coding: iso-8859-1 -*-
|
|
"""
|
|
MoinMoin Access Control Lists
|
|
|
|
@copyright: 2003 by Thomas Waldmann, http://linuxwiki.de/ThomasWaldmann
|
|
@copyright: 2003 by Gustavo Niemeyer, http://moin.conectiva.com.br/GustavoNiemeyer
|
|
@license: GNU GPL, see COPYING for details.
|
|
"""
|
|
|
|
import re
|
|
from MoinMoin import user
|
|
|
|
class AccessControlList:
|
|
''' Access Control List
|
|
|
|
Control who may do what on or with a wiki page.
|
|
|
|
Syntax of an ACL string:
|
|
|
|
[+|-]User[,User,...]:[right[,right,...]] [[+|-]SomeGroup:...] ...
|
|
... [[+|-]Known:...] [[+|-]All:...]
|
|
|
|
"User" is a user name and triggers only if the user matches. Up
|
|
to version 1.2 only WikiNames were supported, as of version 1.3,
|
|
any name can be used in acl lines, including name with spaces
|
|
using esoteric languages.
|
|
|
|
"SomeGroup" is a page name matching cfg.page_group_regex with
|
|
some lines in the form " * Member", defining the group members.
|
|
|
|
"Known" is a group containing all valid / known users.
|
|
|
|
"All" is a group containing all users (Known and Anonymous users).
|
|
|
|
"right" may be an arbitrary word like read, write, delete, admin.
|
|
Only words in cfg.acl_validrights are accepted, others are
|
|
ignored. It is allowed to specify no rights, which means that no
|
|
rights are given.
|
|
|
|
How ACL is processed
|
|
|
|
When some user is trying to access some ACL-protected resource,
|
|
the ACLs will be processed in the order they are found. The first
|
|
matching ACL will tell if the user has access to that resource
|
|
or not.
|
|
|
|
For example, the following ACL tells that SomeUser is able to
|
|
read and write the resources protected by that ACL, while any
|
|
member of SomeGroup (besides SomeUser, if part of that group)
|
|
may also admin that, and every other user is able to read it.
|
|
|
|
SomeUser:read,write SomeGroup:read,write,admin All:read
|
|
|
|
In this example, SomeUser can read and write but can not admin,
|
|
revert or delete pages. Rights that are NOT specified on the
|
|
right list are automatically set to NO.
|
|
|
|
Using Prefixes
|
|
|
|
To make the system more flexible, there are also two modifiers:
|
|
the prefixes "+" and "-".
|
|
|
|
+SomeUser:read -OtherUser:write
|
|
|
|
The acl line above will grant SomeUser read right, and OtherUser
|
|
write right, but will NOT block automatically all other rights
|
|
for these users. For example, if SomeUser ask to write, the
|
|
above acl line does not define if he can or can not write. He
|
|
will be able to write if acl_rights_before or acl_rights_after
|
|
allow this (see configuration options).
|
|
|
|
Using prefixes, this acl line:
|
|
|
|
SomeUser:read,write SomeGroup:read,write,admin All:read
|
|
|
|
Can be written as:
|
|
|
|
-SomeUser:admin SomeGroup:read,write,admin All:read
|
|
|
|
Or even:
|
|
|
|
+All:read -SomeUser:admin SomeGroup:read,write,admin
|
|
|
|
Notice that you probably will not want to use the second and
|
|
third examples in ACL entries of some page. They are very
|
|
useful on the moin configuration entries though.
|
|
|
|
Configuration options
|
|
|
|
cfg.acl_rights_default
|
|
It is is ONLY used when no other ACLs are given.
|
|
Default: "Known:read,write,delete All:read,write",
|
|
|
|
cfg.acl_rights_before
|
|
When the page has ACL entries, this will be inserted BEFORE
|
|
any page entries.
|
|
Default: ""
|
|
|
|
cfg.acl_rights_after
|
|
When the page has ACL entries, this will be inserted AFTER
|
|
any page entries.
|
|
Default: ""
|
|
|
|
cfg.acl_rights_valid
|
|
These are the acceptable (known) rights (and the place to
|
|
extend, if necessary).
|
|
Default: ["read", "write", "delete", "admin"]
|
|
'''
|
|
|
|
special_users = ["All", "Known", "Trusted"] # order is important
|
|
|
|
def __init__(self, request, lines=[]):
|
|
"""Initialize an ACL, starting from <nothing>.
|
|
"""
|
|
self.setLines(request.cfg, lines)
|
|
|
|
def setLines(self, cfg, lines=[]):
|
|
self.clean()
|
|
self.addBefore(cfg)
|
|
if not lines:
|
|
self.addDefault(cfg)
|
|
else:
|
|
for line in lines:
|
|
self.addLine(cfg, line)
|
|
self.addAfter(cfg)
|
|
|
|
def clean(self):
|
|
self.acl = [] # [ ('User', {"read": 0, ...}), ... ]
|
|
self.acl_lines = []
|
|
self._is_group = {}
|
|
|
|
def addBefore(self, cfg):
|
|
self.addLine(cfg, cfg.acl_rights_before, remember=0)
|
|
def addDefault(self, cfg):
|
|
self.addLine(cfg, cfg.acl_rights_default, remember=0)
|
|
def addAfter(self, cfg):
|
|
self.addLine(cfg, cfg.acl_rights_after, remember=0)
|
|
|
|
def addLine(self, cfg, aclstring, remember=1):
|
|
""" Add another ACL line
|
|
|
|
This can be used in multiple subsequent calls to process longer
|
|
lists.
|
|
|
|
@param cfg: current config
|
|
@param aclstring: acl string from page or cfg
|
|
@param remember: should add the line to self.acl_lines
|
|
"""
|
|
group_re = re.compile(cfg.page_group_regex)
|
|
|
|
# Remember lines
|
|
if remember:
|
|
self.acl_lines.append(aclstring)
|
|
|
|
# Iterate over entries and rights, parsed by acl string iterator
|
|
acliter = ACLStringIterator(cfg.acl_rights_valid, aclstring)
|
|
for modifier, entries, rights in acliter:
|
|
if entries == ['Default']:
|
|
self.addDefault(cfg)
|
|
continue
|
|
|
|
for entry in entries:
|
|
if group_re.search(entry):
|
|
self._is_group[entry] = 1
|
|
rightsdict = {}
|
|
if modifier:
|
|
# Only user rights are added to the right dict.
|
|
# + add rights with value of 1
|
|
# - add right with value of 0
|
|
for right in rights:
|
|
rightsdict[right] = (modifier == '+')
|
|
else:
|
|
# All rights from acl_rights_valid are added to the
|
|
# dict, user rights with value of 1, and other with
|
|
# value of 0
|
|
for right in cfg.acl_rights_valid:
|
|
rightsdict[right] = (right in rights)
|
|
self.acl.append((entry, rightsdict))
|
|
|
|
def may(self, request, name, dowhat):
|
|
"""May <name> <dowhat>?
|
|
Returns boolean answer.
|
|
"""
|
|
is_group_member = request.dicts.has_member
|
|
|
|
allowed = None
|
|
for entry, rightsdict in self.acl:
|
|
if entry in self.special_users:
|
|
handler = getattr(self, "_special_"+entry, None)
|
|
allowed = handler(request, name, dowhat, rightsdict)
|
|
elif self._is_group.get(entry):
|
|
if is_group_member(entry, name):
|
|
allowed = rightsdict.get(dowhat)
|
|
else:
|
|
for special in self.special_users:
|
|
if is_group_member(entry, special):
|
|
handler = getattr(self, "_special_"+ special, None)
|
|
allowed = handler(request, name,
|
|
dowhat, rightsdict)
|
|
break # order of self.special_users is important
|
|
elif entry == name:
|
|
allowed = rightsdict.get(dowhat)
|
|
if allowed is not None:
|
|
return allowed
|
|
return 0
|
|
|
|
def getString(self, b='#acl ', e='\n'):
|
|
"""print the acl strings we were fed with"""
|
|
return ''.join(["%s%s%s" % (b,l,e) for l in self.acl_lines])
|
|
|
|
def _special_All(self, request, name, dowhat, rightsdict):
|
|
return rightsdict.get(dowhat)
|
|
|
|
def _special_Known(self, request, name, dowhat, rightsdict):
|
|
""" check if user <name> is known to us,
|
|
that means that there is a valid user account present.
|
|
works for subscription emails.
|
|
"""
|
|
if user.getUserId(request, name): # is a user with this name known?
|
|
return rightsdict.get(dowhat)
|
|
return None
|
|
|
|
def _special_Trusted(self, request, name, dowhat, rightsdict):
|
|
""" check if user <name> is known AND even has logged in using a password.
|
|
does not work for subsription emails that should be sent to <user>,
|
|
as he is not logged in in that case.
|
|
"""
|
|
if request.user.trusted and name == request.user.name:
|
|
return rightsdict.get(dowhat)
|
|
return None
|
|
|
|
def __eq__(self, other):
|
|
return self.acl_lines == other.acl_lines
|
|
def __ne__(self, other):
|
|
return self.acl_lines != other.acl_lines
|
|
|
|
|
|
class ACLStringIterator:
|
|
""" Iterator for acl string
|
|
|
|
Parse acl string and return the next entry on each call to
|
|
next. Implement the Iterator protocol.
|
|
|
|
Usage:
|
|
iter = ACLStringIterator(cfg.acl_rights_valid, 'user name:right')
|
|
for modifier, entries, rights in iter:
|
|
# process data
|
|
"""
|
|
|
|
def __init__(self, rights, aclstring):
|
|
""" Initialize acl iterator
|
|
|
|
@param rights: the acl rights to consider when parsing
|
|
@param aclstring: string to parse
|
|
"""
|
|
self.rights = rights
|
|
self.rest = aclstring.strip()
|
|
self.finished = 0
|
|
|
|
def __iter__(self):
|
|
""" Required by the Iterator protocol """
|
|
return self
|
|
|
|
def next(self):
|
|
""" Return the next values from the acl string
|
|
|
|
When the iterator is finished and you try to call next, it
|
|
raises a StopIteration. The iterator finish as soon as the
|
|
string is fully parsed or can not be parsed any more.
|
|
|
|
@rtype: 3 tuple - (modifier, [entry, ...], [right, ...])
|
|
@return: values for one item in an acl string
|
|
"""
|
|
# Handle finished state, required by iterator protocol
|
|
if self.rest == '':
|
|
self.finished = 1
|
|
if self.finished:
|
|
raise StopIteration
|
|
|
|
# Get optional modifier [+|-]entries:rights
|
|
modifier = ''
|
|
if self.rest[0] in ('+', '-'):
|
|
modifier, self.rest = self.rest[0], self.rest[1:]
|
|
|
|
# Handle the Default meta acl
|
|
if self.rest.startswith('Default ') or self.rest == 'Default':
|
|
self.rest = self.rest[8:]
|
|
entries, rights = ['Default'], []
|
|
|
|
# Handle entries:rights pairs
|
|
else:
|
|
# Get entries
|
|
try:
|
|
entries, self.rest = self.rest.split(':', 1)
|
|
except ValueError:
|
|
self.finished = 1
|
|
raise StopIteration("Can't parse rest of string")
|
|
if entries == '':
|
|
entries = []
|
|
else:
|
|
# TODO strip each entry from blanks?
|
|
entries = entries.split(',')
|
|
|
|
# Get rights
|
|
try:
|
|
rights, self.rest = self.rest.split(' ', 1)
|
|
# Remove extra white space after rights fragment,
|
|
# allowing using multiple spaces between items.
|
|
self.rest = self.rest.lstrip()
|
|
except ValueError:
|
|
rights, self.rest = self.rest, ''
|
|
rights = [r for r in rights.split(',') if r in self.rights]
|
|
|
|
return modifier, entries, rights
|
|
|
|
|
|
def parseACL(request, body):
|
|
""" Parse acl lines on page and return ACL object
|
|
|
|
Use ACL object may(request, dowhat) to get acl rights.
|
|
"""
|
|
acl_lines = []
|
|
while body and body[0] == '#':
|
|
# extract first line
|
|
try:
|
|
line, body = body.split('\n', 1)
|
|
except ValueError:
|
|
line = body
|
|
body = ''
|
|
|
|
# end parsing on empty (invalid) PI
|
|
if line == "#":
|
|
break
|
|
|
|
# skip comments (lines with two hash marks)
|
|
if line[1] == '#':
|
|
continue
|
|
|
|
tokens = line.split(None, 1)
|
|
if tokens[0].lower() == "#acl":
|
|
if len(tokens) == 2:
|
|
args = tokens[1].rstrip()
|
|
else:
|
|
args = ""
|
|
acl_lines.append(args)
|
|
return AccessControlList(request, acl_lines)
|
|
|