diff --git a/wiki/wikiacl.py b/wiki/wikiacl.py new file mode 100644 index 00000000..a4620c97 --- /dev/null +++ b/wiki/wikiacl.py @@ -0,0 +1,359 @@ +# -*- 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_enabled + If true will enable ACL support. + Default: 0 + + 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"] + + def __init__(self, request, lines=[]): + """Initialize an ACL, starting from . + """ + 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 + """ + # FIXME: should compile this once and cache (in cfg?) + 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 ? + Returns boolean answer. + """ + if not request.cfg.acl_enabled: + # everybody may read and write: + if dowhat in ["read", "write",]: + return 1 + # only known users may do some more dangerous things: + if request.user.valid: + if dowhat in ["delete", "revert",]: + return 1 + # in any other case, we better disallow it: + return 0 + + 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) and is_group_member(entry, name): + allowed = rightsdict.get(dowhat) + 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 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 is known AND even has logged in using a password. + does not work for subsription emails that should be sent to , + 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 aclstirng: 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: + # XXX TODO disallow : and , in usernames + 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. + """ + if not request.cfg.acl_enabled: + return AccessControlList(request) + + 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) +