Back to index

moin  1.9.0~rc2
__init__.py
Go to the documentation of this file.
00001 # -*- coding: iso-8859-1 -*-
00002 """
00003     MoinMoin - Wiki Security Interface and Access Control Lists
00004 
00005 
00006     This implements the basic interface for user permissions and
00007     system policy. If you want to define your own policy, inherit
00008     from the base class 'Permissions', so that when new permissions
00009     are defined, you get the defaults.
00010 
00011     Then assign your new class to "SecurityPolicy" in wikiconfig;
00012     and I mean the class, not an instance of it!
00013 
00014     @copyright: 2000-2004 Juergen Hermann <jh@web.de>,
00015                 2003-2008 MoinMoin:ThomasWaldmann,
00016                 2003 Gustavo Niemeyer,
00017                 2005 Oliver Graf,
00018                 2007 Alexander Schremmer
00019     @license: GNU GPL, see COPYING for details.
00020 """
00021 
00022 import re
00023 
00024 from MoinMoin import wikiutil, user
00025 from MoinMoin.Page import Page
00026 
00027 #############################################################################
00028 ### Basic Permissions Interface -- most features enabled by default
00029 #############################################################################
00030 
00031 def _check(request, pagename, username, right):
00032     """ Check <right> access permission for user <username> on page <pagename>
00033 
00034     For cfg.acl_hierarchic=False we just check the page in question.
00035 
00036     For cfg.acl_hierarchic=True we, we check each page in the hierarchy. We
00037     start with the deepest page and recurse to the top of the tree.
00038     If one of those permits, True is returned.
00039 
00040     For both configurations, we check acl_rights_before before the page/default
00041     acl and acl_rights_after after the page/default acl, of course.
00042 
00043     This method should not be called by users, use __getattr__ instead.
00044 
00045     @param request: the current request object
00046     @param pagename: pagename to get permissions from
00047     @param username: the user name
00048     @param right: the right to check
00049 
00050     @rtype: bool
00051     @return: True if you have permission or False
00052     """
00053     cache = request.cfg.cache
00054     allowed = cache.acl_rights_before.may(request, username, right)
00055     if allowed is not None:
00056         return allowed
00057 
00058     if request.cfg.acl_hierarchic:
00059         pages = pagename.split('/') # create page hierarchy list
00060         some_acl = False
00061         for i in range(len(pages), 0, -1):
00062             # Create the next pagename in the hierarchy
00063             # starting at the leaf, going to the root
00064             name = '/'.join(pages[:i])
00065             # Get page acl and ask for permission
00066             acl = Page(request, name).getACL(request)
00067             if acl.acl:
00068                 some_acl = True
00069                 allowed = acl.may(request, username, right)
00070                 if allowed is not None:
00071                     return allowed
00072                 # If the item has an acl (even one that doesn't match) we *do not*
00073                 # check the parents. We only check the parents if there's no acl on
00074                 # the item at all.
00075                 break
00076         if not some_acl:
00077             allowed = cache.acl_rights_default.may(request, username, right)
00078             if allowed is not None:
00079                 return allowed
00080     else:
00081         if request.page is not None and pagename == request.page.page_name:
00082             p = request.page # reuse is good
00083         else:
00084             p = Page(request, pagename)
00085         acl = p.getACL(request) # this will be fast in a reused page obj
00086         allowed = acl.may(request, username, right)
00087         if allowed is not None:
00088             return allowed
00089 
00090     allowed = cache.acl_rights_after.may(request, username, right)
00091     if allowed is not None:
00092         return allowed
00093 
00094     return False
00095 
00096 
00097 class Permissions:
00098     """ Basic interface for user permissions and system policy.
00099 
00100     Note that you still need to allow some of the related actions, this
00101     just controls their behavior, not their activation.
00102 
00103     When sub classing this class, you must extend the class methods, not
00104     replace them, or you might break the acl in the wiki. Correct sub
00105     classing looks like this:
00106 
00107     def read(self, pagename):
00108         # Your special security rule
00109         if something:
00110             return false
00111 
00112         # Do not return True or you break acl!
00113         # This call will use the default acl rules
00114         return Permissions.read(pagename)
00115     """
00116 
00117     def __init__(self, user):
00118         self.name = user.name
00119         self.request = user._request
00120 
00121     def save(self, editor, newtext, rev, **kw):
00122         """ Check whether user may save a page.
00123 
00124         `editor` is the PageEditor instance, the other arguments are
00125         those of the `PageEditor.saveText` method.
00126 
00127         @param editor: PageEditor instance.
00128         @param newtext: new page text, you can enable of disable saving according
00129             to the content of the text, e.g. prevent link spam.
00130         @param rev: new revision number? XXX
00131         @param kw: XXX
00132         @rtype: bool
00133         @return: True if you can save or False
00134         """
00135         return self.write(editor.page_name)
00136 
00137     def __getattr__(self, attr):
00138         """ Shortcut to export getPermission function for all known ACL rights
00139 
00140         if attr is one of the rights in acl_rights_valid, then return a
00141         checking function for it. Else raise an AttributeError.
00142 
00143         @param attr: one of ACL rights as defined in acl_rights_valid
00144         @rtype: function
00145         @return: checking function for that right, accepting a pagename
00146         """
00147         request = self.request
00148         if attr not in request.cfg.acl_rights_valid:
00149             raise AttributeError, attr
00150         return lambda pagename: _check(self.request, pagename, self.name, attr)
00151 
00152 
00153 # make an alias for the default policy
00154 Default = Permissions
00155 
00156 
00157 class AccessControlList:
00158     ''' Access Control List
00159 
00160     Control who may do what on or with a wiki page.
00161 
00162     Syntax of an ACL string:
00163 
00164         [+|-]User[,User,...]:[right[,right,...]] [[+|-]SomeGroup:...] ...
00165         ... [[+|-]Known:...] [[+|-]All:...]
00166 
00167         "User" is a user name and triggers only if the user matches. Up
00168         to version 1.2 only WikiNames were supported, as of version 1.3,
00169         any name can be used in acl lines, including name with spaces
00170         using esoteric languages.
00171 
00172         "SomeGroup" is a page name matching cfg.page_group_regex with
00173          some lines in the form " * Member", defining the group members.
00174 
00175         "Known" is a group containing all valid / known users.
00176 
00177         "All" is a group containing all users (Known and Anonymous users).
00178 
00179         "right" may be an arbitrary word like read, write, delete, admin.
00180         Only words in cfg.acl_validrights are accepted, others are
00181         ignored. It is allowed to specify no rights, which means that no
00182         rights are given.
00183 
00184     How ACL is processed
00185 
00186         When some user is trying to access some ACL-protected resource,
00187         the ACLs will be processed in the order they are found. The first
00188         matching ACL will tell if the user has access to that resource
00189         or not.
00190 
00191         For example, the following ACL tells that SomeUser is able to
00192         read and write the resources protected by that ACL, while any
00193         member of SomeGroup (besides SomeUser, if part of that group)
00194         may also admin that, and every other user is able to read it.
00195 
00196             SomeUser:read,write SomeGroup:read,write,admin All:read
00197 
00198         In this example, SomeUser can read and write but can not admin,
00199         revert or delete pages. Rights that are NOT specified on the
00200         right list are automatically set to NO.
00201 
00202     Using Prefixes
00203 
00204         To make the system more flexible, there are also two modifiers:
00205         the prefixes "+" and "-".
00206 
00207             +SomeUser:read -OtherUser:write
00208 
00209         The acl line above will grant SomeUser read right, and OtherUser
00210         write right, but will NOT block automatically all other rights
00211         for these users. For example, if SomeUser ask to write, the
00212         above acl line does not define if he can or can not write. He
00213         will be able to write if acl_rights_before or acl_rights_after
00214         allow this (see configuration options).
00215 
00216         Using prefixes, this acl line:
00217 
00218             SomeUser:read,write SomeGroup:read,write,admin All:read
00219 
00220         Can be written as:
00221 
00222             -SomeUser:admin SomeGroup:read,write,admin All:read
00223 
00224         Or even:
00225 
00226             +All:read -SomeUser:admin SomeGroup:read,write,admin
00227 
00228         Notice that you probably will not want to use the second and
00229         third examples in ACL entries of some page. They are very
00230         useful on the moin configuration entries though.
00231 
00232    Configuration options
00233 
00234        cfg.acl_rights_default
00235            It is is ONLY used when no other ACLs are given.
00236            Default: "Known:read,write,delete All:read,write",
00237 
00238        cfg.acl_rights_before
00239            When the page has ACL entries, this will be inserted BEFORE
00240            any page entries.
00241            Default: ""
00242 
00243        cfg.acl_rights_after
00244            When the page has ACL entries, this will be inserted AFTER
00245            any page entries.
00246            Default: ""
00247 
00248        cfg.acl_rights_valid
00249            These are the acceptable (known) rights (and the place to
00250            extend, if necessary).
00251            Default: ["read", "write", "delete", "admin"]
00252     '''
00253 
00254     special_users = ["All", "Known", "Trusted"] # order is important
00255 
00256     def __init__(self, cfg, lines=[]):
00257         """Initialize an ACL, starting from <nothing>.
00258         """
00259         if lines:
00260             self.acl = [] # [ ('User', {"read": 0, ...}), ... ]
00261             self.acl_lines = []
00262             for line in lines:
00263                 self._addLine(cfg, line)
00264         else:
00265             self.acl = None
00266             self.acl_lines = None
00267 
00268     def _addLine(self, cfg, aclstring, remember=1):
00269         """ Add another ACL line
00270 
00271         This can be used in multiple subsequent calls to process longer lists.
00272 
00273         @param cfg: current config
00274         @param aclstring: acl string from page or cfg
00275         @param remember: should add the line to self.acl_lines
00276         """
00277 
00278         # Remember lines
00279         if remember:
00280             self.acl_lines.append(aclstring)
00281 
00282         # Iterate over entries and rights, parsed by acl string iterator
00283         acliter = ACLStringIterator(cfg.acl_rights_valid, aclstring)
00284         for modifier, entries, rights in acliter:
00285             if entries == ['Default']:
00286                 self._addLine(cfg, cfg.acl_rights_default, remember=0)
00287             else:
00288                 for entry in entries:
00289                     rightsdict = {}
00290                     if modifier:
00291                         # Only user rights are added to the right dict.
00292                         # + add rights with value of 1
00293                         # - add right with value of 0
00294                         for right in rights:
00295                             rightsdict[right] = (modifier == '+')
00296                     else:
00297                         # All rights from acl_rights_valid are added to the
00298                         # dict, user rights with value of 1, and other with
00299                         # value of 0
00300                         for right in cfg.acl_rights_valid:
00301                             rightsdict[right] = (right in rights)
00302                     self.acl.append((entry, rightsdict))
00303 
00304     def may(self, request, name, dowhat):
00305         """ May <name> <dowhat>? Returns boolean answer.
00306 
00307             Note: this check does NOT include the acl_rights_before / _after ACL,
00308                   but it WILL use acl_rights_default if there is no (page) ACL.
00309         """
00310         if self.acl is None: # no #acl used on Page
00311             acl = request.cfg.cache.acl_rights_default.acl
00312         else: # we have a #acl on the page (self.acl can be [] if #acl is empty!)
00313             acl = self.acl
00314 
00315         groups = request.groups
00316 
00317         allowed = None
00318         for entry, rightsdict in acl:
00319             if entry in self.special_users:
00320                 handler = getattr(self, "_special_"+entry, None)
00321                 allowed = handler(request, name, dowhat, rightsdict)
00322             elif entry in groups:
00323                 if name in groups[entry]:
00324                     allowed = rightsdict.get(dowhat)
00325                 else:
00326                     for special in self.special_users:
00327                         if special in entry:
00328                             handler = getattr(self, "_special_" + special, None)
00329                             allowed = handler(request, name, dowhat, rightsdict)
00330                             break # order of self.special_users is important
00331             elif entry == name:
00332                 allowed = rightsdict.get(dowhat)
00333             if allowed is not None:
00334                 return allowed
00335         return allowed # should be None
00336 
00337     def getString(self, b='#acl ', e='\n'):
00338         """print the acl strings we were fed with"""
00339         if self.acl_lines:
00340             acl_lines = ''.join(["%s%s%s" % (b, l, e) for l in self.acl_lines])
00341         else:
00342             acl_lines = ''
00343         return acl_lines
00344 
00345     def _special_All(self, request, name, dowhat, rightsdict):
00346         return rightsdict.get(dowhat)
00347 
00348     def _special_Known(self, request, name, dowhat, rightsdict):
00349         """ check if user <name> is known to us,
00350             that means that there is a valid user account present.
00351             works for subscription emails.
00352         """
00353         if user.getUserId(request, name): # is a user with this name known?
00354             return rightsdict.get(dowhat)
00355         return None
00356 
00357     def _special_Trusted(self, request, name, dowhat, rightsdict):
00358         """ check if user <name> is known AND has logged in using a trusted
00359             authentication method.
00360             Does not work for subsription emails that should be sent to <user>,
00361             as he is not logged in in that case.
00362         """
00363         if (request.user.name == name and
00364             request.user.auth_method in request.cfg.auth_methods_trusted):
00365             return rightsdict.get(dowhat)
00366         return None
00367 
00368     def __eq__(self, other):
00369         return self.acl_lines == other.acl_lines
00370 
00371     def __ne__(self, other):
00372         return self.acl_lines != other.acl_lines
00373 
00374 
00375 class ACLStringIterator:
00376     """ Iterator for acl string
00377 
00378     Parse acl string and return the next entry on each call to
00379     next. Implement the Iterator protocol.
00380 
00381     Usage:
00382         iter = ACLStringIterator(cfg.acl_rights_valid, 'user name:right')
00383         for modifier, entries, rights in iter:
00384             # process data
00385     """
00386 
00387     def __init__(self, rights, aclstring):
00388         """ Initialize acl iterator
00389 
00390         @param rights: the acl rights to consider when parsing
00391         @param aclstring: string to parse
00392         """
00393         self.rights = rights
00394         self.rest = aclstring.strip()
00395         self.finished = 0
00396 
00397     def __iter__(self):
00398         """ Required by the Iterator protocol """
00399         return self
00400 
00401     def next(self):
00402         """ Return the next values from the acl string
00403 
00404         When the iterator is finished and you try to call next, it
00405         raises a StopIteration. The iterator finish as soon as the
00406         string is fully parsed or can not be parsed any more.
00407 
00408         @rtype: 3 tuple - (modifier, [entry, ...], [right, ...])
00409         @return: values for one item in an acl string
00410         """
00411         # Handle finished state, required by iterator protocol
00412         if self.rest == '':
00413             self.finished = 1
00414         if self.finished:
00415             raise StopIteration
00416 
00417         # Get optional modifier [+|-]entries:rights
00418         modifier = ''
00419         if self.rest[0] in ('+', '-'):
00420             modifier, self.rest = self.rest[0], self.rest[1:]
00421 
00422         # Handle the Default meta acl
00423         if self.rest.startswith('Default ') or self.rest == 'Default':
00424             self.rest = self.rest[8:]
00425             entries, rights = ['Default'], []
00426 
00427         # Handle entries:rights pairs
00428         else:
00429             # Get entries
00430             try:
00431                 entries, self.rest = self.rest.split(':', 1)
00432             except ValueError:
00433                 self.finished = 1
00434                 raise StopIteration("Can't parse rest of string")
00435             if entries == '':
00436                 entries = []
00437             else:
00438                 # TODO strip each entry from blanks?
00439                 entries = entries.split(',')
00440 
00441             # Get rights
00442             try:
00443                 rights, self.rest = self.rest.split(' ', 1)
00444                 # Remove extra white space after rights fragment,
00445                 # allowing using multiple spaces between items.
00446                 self.rest = self.rest.lstrip()
00447             except ValueError:
00448                 rights, self.rest = self.rest, ''
00449             rights = [r for r in rights.split(',') if r in self.rights]
00450 
00451         return modifier, entries, rights
00452 
00453 
00454 def parseACL(request, text):
00455     """ Parse acl lines from text and return ACL object """
00456     pi, dummy = wikiutil.get_processing_instructions(text)
00457     acl_lines = [args for verb, args in pi if verb == 'acl']
00458     return AccessControlList(request.cfg, acl_lines)