view MoinMoin/security/ @ 1770:c64d3d8d16cb

security fix: fix virtual group bug in ACL evaluation, add a test for it Note: same issue has been fixed in moin/1.9 repo also, see cs 7b9f39289e16. This changeset was just ported to moin/2.0. Issue description: We have code that checks whether a group has special members "All" or "Known" or "Trusted", but there was a bug that checked whether these are present in the group NAME (not, as intended, in the group MEMBERS). a) If you have group MEMBERS like "All" or "Known" or "Trusted", they did not work until now, but will start working with this changeset. E.g. SomeGroup: * JoeDoe * Trusted SomeGroup will now (correctly) include JoeDoe and also all trusted users. It (erroneously) contained only "JoeDoe" and "Trusted" (as a username, not as a virtual group) before. b) If you have group NAMES containing "All" or "Known" or "Trusted", they behaved wrong until now (they erroneously included All/Known/Trusted users even if you did not list them as members), but will start working correctly with this changeset. E.g. AllFriendsGroup: * JoeDoe AllFriendsGroup will now (correctly) include only JoeDoe. It (erroneously) contained all users (including JoeDoe) before. E.g. MyTrustedFriendsGroup: * JoeDoe MyTrustedFriendsGroup will now (correctly) include only JoeDoe. It (erroneously) contained all trusted users and JoeDoe before.
author Thomas Waldmann <tw AT waldmann-edv DOT de>
date Mon, 03 Sep 2012 20:05:30 +0200
parents 4ac437141bbe
children dbfad38a4cdc
line wrap: on
line source
# Copyright: 2000-2004 Juergen Hermann <>
# Copyright: 2003-2008,2011 MoinMoin:ThomasWaldmann
# Copyright: 2003 Gustavo Niemeyer
# Copyright: 2005 Oliver Graf
# Copyright: 2007 Alexander Schremmer
# License: GNU GPL v2 (or any later version), see LICENSE.txt for details.

MoinMoin - Wiki Security Interface and Access Control Lists

This implements the basic interface for user permissions and
system policy. If you want to define your own policy, inherit
from the base class 'Permissions', so that when new permissions
are defined, you get the defaults.

Then assign your new class to "SecurityPolicy" in wikiconfig;
and I mean the class, not an instance of it!

from functools import wraps

from flask import current_app as app
from flask import g as flaskg
from flask import abort

from MoinMoin import user
from MoinMoin.i18n import _, L_, N_

def require_permission(permission):
    view decorator to require a specific permission

    if the permission is not granted, abort with 403
    def wrap(f):
        def wrapped_f(*args, **kw):
            has_permission = getattr(flaskg.user.may, permission)
            if not has_permission():
            return f(*args, **kw)
        return wrapped_f
    return wrap

class Permissions(object):
    """ Basic interface for user permissions and system policy.

    Note that you still need to allow some of the related actions, this
    just controls their behavior, not their activation.

    When sub classing this class, you must extend the class methods, not
    replace them, or you might break the ACLs in the wiki.
    Correct sub classing looks like this::

        def read(self, itemname):
            # Your special security rule
            if something:
                return False

            # Do not just return True or you break (ignore) ACLs!
            # This call will return correct permissions by checking ACLs:
    def __init__(self, user): =

    def __getattr__(self, attr):
        """ Shortcut to handle all known ACL rights.

        if attr is a valid acl right, return a checking function for it.
        Else raise an AttributeError.

        :param attr: one of ACL rights as defined in acl_rights_(contents|functions)
        :rtype: function
        :returns: checking function for that right
        if attr in app.cfg.acl_rights_contents:
            return lambda itemname:, attr,
        if attr in app.cfg.acl_rights_functions:
            may = app.cfg.cache.acl_functions.may
            return lambda: may(, attr)
        raise AttributeError(attr)

# make an alias for the default policy
Default = Permissions

class AccessControlList(object):
    Access Control List - controls who may do what.

    Syntax of an ACL string:

        [+|-]User[,User,...]:[right[,right,...]] [[+|-]SomeGroup:...] ...
        ... [[+|-]Known:...] [[+|-]All:...]

        "User" is a user name and triggers only if the user matches.
        Any name can be used in acl lines, including names with spaces
        using exotic languages.

        "SomeGroup" is a group name. The group defines its members somehow,
        e.g. on a wiki page of this name as first level list with the group
        members' names.

        "Known" is a special group containing all valid / known users.

        "All" is a special group containing all users (Known and Anonymous users).

        "right" may be an arbitrary word like read, write or admin.
        Only valid words are accepted, others are ignored (see valid param).
        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
        items. 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 asks to write, the
        above acl line does not define if he can or can not write. He
        will be able to write if the acls checked before or afterwards
        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

        Note that you probably would not want to use the second and
        third examples in ACL entries of some item. They are very
        useful in the wiki configuration though.

    special_users = ["All", "Known", "Trusted"] # order is important

    def __init__(self, lines=[], default='', valid=None):
        """ Initialize an ACL, starting from <nothing>.
        assert valid is not None
        self.acl_rights_valid = valid
        self.default = default
        assert isinstance(lines, (list, tuple))
        if lines:
            self.acl = [] # [ ('User', {"read": 0, ...}), ... ]
            self.acl_lines = []
            for line in lines:
            self.acl = None
            self.acl_lines = None

    def has_acl(self):
        """ Checks whether we have a real acl here. """
        # self.acl == None means that there is NO acl.
        # self.acl == [] means that there is a empty acl.
        return self.acl is not None

    def _addLine(self, aclstring, remember=1):
        """ Add another ACL line

        This can be used in multiple subsequent calls to process longer lists.

        :param aclstring: acl string from item or configuration
        :param remember: should add the line to self.acl_lines
        # Remember lines
        if remember:

        # Iterate over entries and rights, parsed by acl string iterator
        acliter = ACLStringIterator(self.acl_rights_valid, aclstring)
        for modifier, entries, rights in acliter:
            if entries == ['Default']:
                self._addLine(self.default, remember=0)
                for entry in entries:
                    rightsdict = {}
                    if modifier:
                        # Only user rights are added to the right dict.
                        # + add right with value of 1
                        # - add right with value of 0
                        for right in rights:
                            rightsdict[right] = (modifier == '+')
                        # 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 self.acl_rights_valid:
                            rightsdict[right] = (right in rights)
                    self.acl.append((entry, rightsdict))

    def may(self, name, dowhat):
        """ May <name> <dowhat>? Returns boolean answer.

            Note: this just checks THIS ACL, the before/default/after ACL must
                  be handled elsewhere, if needed.
        groups = flaskg.groups
        allowed = None
        for entry, rightsdict in self.acl:
            if entry in self.special_users:
                handler = getattr(self, "_special_"+entry, None)
                allowed = handler(name, dowhat, rightsdict)
            elif entry in groups:
                this_group = groups[entry]
                if name in this_group:
                    allowed = rightsdict.get(dowhat)
                    for special in self.special_users:
                        if special in this_group:
                            handler = getattr(self, "_special_" + special, None)
                            allowed = handler(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 allowed # should be None

    def _special_All(self, name, dowhat, rightsdict):
        return rightsdict.get(dowhat)

    def _special_Known(self, 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.search_users(name_exact=name): # is a user with this name known?
            return rightsdict.get(dowhat)
        return None

    def _special_Trusted(self, name, dowhat, rightsdict):
        """ check if user <name> is the current user AND is has logged in using
            an authentication method that set the trusted attribute.
            Does not work for subsription emails that should be sent to <user>,
            as the user is not logged in in that case.
        if == name and flaskg.user.trusted:
            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(object):
    """ Iterator for acl string

    Parse acl string and return the next entry on each call to next.
    Implements the Iterator protocol.


        iter = ACLStringIterator(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 = 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 finishes as soon as the
        string is fully parsed or can not be parsed any more.

        :rtype: 3 tuple - (modifier, [entry, ...], [right, ...])
        :returns: values for one item in an acl string
        # Handle finished state, required by iterator protocol
        if == '':
            self.finished = 1
        if self.finished:
            raise StopIteration

        # Get optional modifier [+|-]entries:rights
        modifier = ''
        if[0] in ('+', '-'):
            modifier, =[0],[1:]

        # Handle the Default meta acl
        if'Default ') or == 'Default':
            entries, rights = ['Default'], []

        # Handle entries:rights pairs
            # Get entries
                entries, =':', 1)
            except ValueError:
                self.finished = 1
                raise StopIteration("Can't parse rest of string")
            if entries == '':
                entries = []
                # TODO strip each entry from blanks?
                entries = entries.split(',')

            # Get rights
                rights, =' ', 1)
                # Remove extra white space after rights fragment,
                # allowing using multiple spaces between items.
            except ValueError:
                rights, =, ''
            rights = [r for r in rights.split(',') if r in self.rights]

        return modifier, entries, rights