Commits

Thomas Waldmann committed 70b9962

acl processing: deal with users having multiple names, add tests

for multiple user names: check them all, if one of them is allowed, allow.

for hierarchical ACL processing and multiple parents (grandparents, ...):
check them all, if one of the alternative ACLs allows, allow.

the not logged-in "user" now has 1 name: u"anonymous"
(yet, it had 0 names, but that causes some issues in the code)

some stuff is still TODO, left some markers

Comments (0)

Files changed (8)

MoinMoin/_tests/__init__.py

         easier to use become_trusted().
     """
     flaskg.user.profile[NAME] = [username, ]
-    flaskg.user.may.name = username # currently just one/first name, see security.Permissions class
+    flaskg.user.may.names = [username, ]  # see security.DefaultSecurityPolicy class
     flaskg.user.valid = 1
 
 
 
     # if we still have no user obj, create a dummy:
     if not userobj:
-        userobj = user.User(auth_method='invalid')
+        userobj = user.User(name=u'anonymous', auth_method='invalid')
     # if we have a valid user we store it in the session
     if userobj.valid:
         session['user.itemid'] = userobj.itemid

MoinMoin/security/__init__.py

             return super(MySecPol, self).read(itemname)
     """
     def __init__(self, user):
-        self.name = user.name0 # XXX currently we just use first name
+        self.names = user.name
 
     def read(self, itemname):
         """read permission is special as we have 2 kinds of read capabilities:
            * READ - gives permission to read, unconditionally
            * PUBREAD - gives permission to read, when published
         """
-        return (flaskg.storage.may(itemname, rights.READ, username=self.name)
+        return (flaskg.storage.may(itemname, rights.READ, usernames=self.names)
                 or
-                flaskg.storage.may(itemname, rights.PUBREAD, username=self.name))
+                flaskg.storage.may(itemname, rights.PUBREAD, usernames=self.names))
 
     def __getattr__(self, attr):
         """ Shortcut to handle all known ACL rights.
         :returns: checking function for that right
         """
         if attr in app.cfg.acl_rights_contents:
-            return lambda itemname: flaskg.storage.may(itemname, attr, username=self.name)
+            return lambda itemname: flaskg.storage.may(itemname, attr, usernames=self.names)
         if attr in app.cfg.acl_rights_functions:
-            may = app.cfg.cache.acl_functions.may
-            return lambda: may(self.name, attr)
+            def multiuser_may():
+                # TODO: if "may" would accept multiple names, we could get rid of this
+                may = app.cfg.cache.acl_functions.may
+                for name in self.names:
+                    if may(name, attr):
+                        return True
+                return False
+            return multiuser_may
         raise AttributeError(attr)
 
 
                             handler = getattr(self, "_special_" + special, None)
                             allowed = handler(name, dowhat, rightsdict)
                             break # order of self.special_users is important
-            elif entry == name:
+            elif entry == name:  # XXX TODO maybe change this to "entry in names" to check users with multiple names in one go
                 allowed = rightsdict.get(dowhat)
             if allowed is not None:
                 return allowed

MoinMoin/security/_tests/test_security.py

 from MoinMoin.security import AccessControlList, ACLStringIterator
 
 from MoinMoin.user import User
-from MoinMoin.config import ACL
+from MoinMoin.config import NAME, ACL
 from MoinMoin.datastruct import ConfigGroups
 
 from MoinMoin._tests import update_item
                 yield _not_have_right, u, right, itemname
 
 
+class TestItemHierachicalAclsMultiItemNames(object):
+    """ security: real-life access control list on items testing
+    """
+    # parent / child item names
+    p1 = [u'p1', ]
+    c1 = [u'p1/c1', ]
+    p2 = [u'p2', ]
+    c2 = [u'p2/c2', ]
+    c12 = [u'p1/c12', u'p2/c12', ]
+    items = [
+        # itemnames, acl, content
+        (p1, u'Editor:', p1),  # deny access (due to hierarchic acl mode also effective for children)
+        (c1, None, c1),  # no own acl -> inherit from parent
+        (p2, None, p2),  # default acl effective (also for children)
+        (c2, None, c2),  # no own acl -> inherit from parent
+        (c12, None, c12),  # no own acl -> inherit from parents
+        ]
+
+    from MoinMoin._tests import wikiconfig
+
+    class Config(wikiconfig.Config):
+        content_acl = dict(hierarchic=True, before=u"WikiAdmin:admin,read,write,create,destroy", default=u"Editor:read,write", after=u"All:read")
+
+    def setup_method(self, method):
+        become_trusted(username=u'WikiAdmin')
+        for item_names, item_acl, item_content in self.items:
+            meta = {NAME: item_names}
+            if item_acl is not None:
+                meta.update({ACL: item_acl})
+            update_item(item_names[0], meta, item_content)
+
+    def testItemACLs(self):
+        """ security: test item acls """
+        tests = [
+            # itemname, username, expected_rights
+            (self.p1, u'WikiAdmin', ['read', 'write', 'admin', 'create', 'destroy']),  # by before acl
+            (self.p2, u'WikiAdmin', ['read', 'write', 'admin', 'create', 'destroy']),  # by before acl
+            (self.c1, u'WikiAdmin', ['read', 'write', 'admin', 'create', 'destroy']),  # by before acl
+            (self.c2, u'WikiAdmin', ['read', 'write', 'admin', 'create', 'destroy']),  # by before acl
+            (self.c12, u'WikiAdmin', ['read', 'write', 'admin', 'create', 'destroy']),  # by before acl
+            (self.p1, u'Editor', []),  # by p1 acl
+            (self.c1, u'Editor', []),  # by p1 acl
+            (self.p1, u'SomeOne', ['read']),  # by after acl
+            (self.c1, u'SomeOne', ['read']),  # by after acl
+            (self.p2, u'Editor', ['read', 'write']),  # by default acl
+            (self.c2, u'Editor', ['read', 'write']),  # by default acl
+            (self.p2, u'SomeOne', ['read']),  # by after acl
+            (self.c2, u'SomeOne', ['read']),  # by after acl
+            (self.c12, u'SomeOne', ['read']),  # by after acl
+            # now check the rather special stuff:
+            (self.c12, u'Editor', ['read', 'write']),  # disallowed via p1, but allowed via p2 via default acl
+        ]
+
+        for itemnames, username, may in tests:
+            u = User(auth_username=username)
+            u.valid = True
+            itemname = itemnames[0]
+
+            def _have_right(u, right, itemname):
+                can_access = getattr(u.may, right)(itemname)
+                assert can_access, "{0!r} may {1} {2!r} (hierarchic)".format(u.name, right, itemname)
+
+            # User should have these rights...
+            for right in may:
+                yield _have_right, u, right, itemname
+
+            def _not_have_right(u, right, itemname):
+                can_access = getattr(u.may, right)(itemname)
+                assert not can_access, "{0!r} may not {1} {2!r} (hierarchic)".format(u.name, right, itemname)
+
+            # User should NOT have these rights:
+            mayNot = [right for right in app.cfg.acl_rights_contents
+                      if right not in may]
+            for right in mayNot:
+                yield _not_have_right, u, right, itemname
+
+
+# XXX TODO add tests for a user having multiple usernames (one resulting in more permissions than other)
+
 coverage_modules = ['MoinMoin.security']

MoinMoin/storage/middleware/_tests/test_indexing.py

         assert rev_u.meta[NAMESPACE] == u'userprofiles'
         assert rev_u.meta[NAME] == [item_name_u.split(':')[1]]
 
+    def test_parentnames(self):
+        item_name = u'child'
+        item = self.imw[item_name]
+        item.store_revision(dict(name=[u'child', u'p1/a', u'p2/b', u'p2/c', u'p3/p4/d', ],
+                                 contenttype=u'text/plain'),
+                            StringIO(''))
+        item = self.imw[item_name]
+        assert item.parentnames == [u'p1', u'p2', u'p3/p4', ]  # one p2 duplicate removed
+
 class TestProtectedIndexingMiddleware(object):
     reinit_storage = True # cleanup after each test method
 

MoinMoin/storage/middleware/indexing.py

         return get_names(self._current)
 
     @property
+    def parentnames(self):
+        """
+        compute list of parent names (same order as in names, but no dupes)
+
+        :return: parent names (list of unicode)
+        """
+        parent_names = []
+        for name in self.names:
+            parentname_tail = name.rsplit('/', 1)
+            if len(parentname_tail) == 2:
+                parent_name = parentname_tail[0]
+                if parent_name not in parent_names:
+                    parent_names.append(parent_name)
+        return parent_names
+
+    @property
+    def parentids(self):
+        """
+        compute list of parent itemids
+
+        :return: parent itemids (set)
+        """
+        parent_ids = set()
+        for parent_name in self.parentnames:
+            rev = self.indexer._document(idx_name=LATEST_REVS, name_exact=parent_name)
+            if rev:
+                parent_ids.add(rev[ITEMID])
+        return parent_ids
+
+    @property
     def mtime(self):
         dt = self._current.get(MTIME)
         if dt is not None:
         assert name is None or isinstance(name, unicode)
         return name
 
-    @property
-    def fqname(self):
+    def _fqname(self, name):
         """
         return the fully qualified name including the namespace: NS:NAME
         """
         ns = self.namespace
-        name = self.name or u''
+        name = name or u''
         if ns:
             fqn = ns + u':' + name
         else:
         assert isinstance(fqn, unicode)
         return fqn
 
+    @property
+    def fqname(self):
+        """
+        return the fully qualified name including the namespace: NS:NAME
+        """
+        return self._fqname(self.name)
+
+    @property
+    def fqnames(self):
+        """
+        return the fully qualified names including the namespace: NS:NAME
+        """
+        return [self._fqname(name) for name in self.names]
+
+    @property
+    def fqparentnames(self):
+        """
+        return the fully qualified parent names including the namespace: NS:NAME
+        """
+        return [self._fqname(name) for name in self.parentnames]
+
     @classmethod
     def create(cls, indexer, **query):
         """

MoinMoin/storage/middleware/protecting.py

         lru_cache_decorator = lru_cache(EVAL_CACHE)
         self.eval_acl = lru_cache_decorator(self._eval_acl)
         lru_cache_decorator = lru_cache(LOOKUP_CACHE)
-        self.get_acl = lru_cache_decorator(self._get_acl)
+        self.get_acls = lru_cache_decorator(self._get_acls)
 
     def _clear_acl_cache(self):
         # if we have modified the backend somehow so ACL lookup is influenced,
         # this functions need to get called, so it clears the ACL cache.
         # ACL lookups afterwards will fetch fresh info from the lower layers.
-        self.get_acl.cache_clear()
+        self.get_acls.cache_clear()
 
     def _get_configured_acls(self, itemname):
         """
         else:
             raise ValueError('No acl_mapping entry found for item {0!r}'.format(itemname))
 
-    def _get_acl(self, fqname):
+    def _get_acls(self, itemid=None, fqname=None):
         """
-        return the effective item_acl for item fqname (= its own acl, or,
-        if hierarchic acl mode is enabled, of some parent item) - without
-        before/default/after acls. return None if no acl was found.
+        return a list of (alternatively valid) effective acls for the item
+        identified via itemid or fqname.
+        this can be a list just containing the item's own acl (as only alternative),
+        or a list with None, indicating no acl was found (in non-hierarchic mode).
+        if hierarchic acl mode is enabled, a list of all valid parent acls will
+        be returned.
+        All lists are without considering before/default/after acls.
         """
-        item = self[fqname]
+
+        if itemid is not None:
+            item = self.get_item(itemid=itemid)
+        elif fqname is not None:
+            # itemid might be None for new, not yet stored items,
+            # but we have fqname then
+            item = self.get_item(name_exact=fqname)
+        else:
+            raise ValueError("need itemid or fqname")
         acl = item.acl
+        fqname = item.fqname
         if acl is not None:
-            return acl
+            return [acl, ]
         acl_cfg = self._get_configured_acls(fqname)
         if acl_cfg['hierarchic']:
             # check parent(s), recursively
-            parent_tail = fqname.rsplit('/', 1)
-            if len(parent_tail) == 2:
-                parent, _ = parent_tail
-                acl = self.get_acl(parent)
-                if acl is not None:
-                    return acl
+            parentids = item.parentids
+            if parentids:
+                acl_list = []
+                for parentid in parentids:
+                    pacls = self.get_acls(parentid, None)
+                    acl_list.extend(pacls)
+                return acl_list
+        return [None, ]
 
     def _parse_acl(self, acl, default=''):
         return AccessControlList([acl, ], default=default, valid=self.valid_rights)
         item = self.indexer.existing_item(**query)
         return ProtectedItem(self, item)
 
-    def may(self, itemname, capability, username=None):
+    def may(self, itemname, capability, usernames=None):
+        if usernames is not None and isinstance(usernames, (str, unicode)):
+            # we got a single username (maybe str), make a list of unicode:
+            if isinstance(usernames, str):
+                usernames = usernames.decode('utf-8')
+            usernames = [usernames, ]
         if isinstance(itemname, list):
             # if we get a list of names, just use first one to fetch item
             itemname = itemname[0]
         item = self[itemname]
-        allowed = item.allows(capability, user_name=username)
+        allowed = item.allows(capability, user_names=usernames)
         return allowed
 
 
         return self.item.itemid
 
     @property
+    def fqname(self):
+        return self.item.fqname
+
+    @property
+    def parentids(self):
+        return self.item.parentids
+
+    @property
+    def parentnames(self):
+        return self.item.parentnames
+
+    @property
     def name(self):
         return self.item.name
 
     def __nonzero__(self):
         return bool(self.item)
 
-    def full_acl(self):
+    def full_acls(self):
         """
-        return the full acl for this item, including before/default/after acl.
+        iterator over all alternatively possible full acls for this item,
+        including before/default/after acl.
         """
         fqname = self.item.fqname
+        itemid = self.item.itemid
         acl_cfg = self.protector._get_configured_acls(fqname)
         before_acl = acl_cfg['before']
-        item_acl = self.protector.get_acl(fqname)
-        if item_acl is None:
-            item_acl = acl_cfg['default']
         after_acl = acl_cfg['after']
-        acl = u' '.join([before_acl, item_acl, after_acl])
-        return acl
+        for item_acl in self.protector.get_acls(itemid, fqname):
+            if item_acl is None:
+                item_acl = acl_cfg['default']
+            yield u' '.join([before_acl, item_acl, after_acl])
 
-    def allows(self, right, user_name=None):
-        """ Check if username may have <right> access on this item.
+    def allows(self, right, user_names=None):
+        """ Check if usernames may have <right> access on this item.
 
         :param right: the right to check
-        :param user_name: user name to use for permissions check (default is to
-                          use the user name doing the current request)
+        :param user_names: user names to use for permissions check (default is to
+                          use the user names doing the current request)
         :rtype: bool
         :returns: True if you have permission or False
         """
-        if user_name is None:
-            user_name = self.protector.user.name0
-
+        if user_names is None:
+            user_names = self.protector.user.name
+        # must be a non-empty list of user names
+        assert isinstance(user_names, list)
+        assert user_names
         acl_cfg = self.protector._get_configured_acls(self.item.fqname)
-        full_acl = self.full_acl()
-
-        allowed = self.protector.eval_acl(full_acl, acl_cfg['default'], user_name, right)
-        if allowed is not None:
-            return pchecker(right, allowed, self.item)
-
+        for user_name in user_names:
+            for full_acl in self.full_acls():
+                allowed = self.protector.eval_acl(full_acl, acl_cfg['default'], user_name, right)
+                if allowed is True and pchecker(right, allowed, self.item):
+                    return True
         return False
 
     def require(self, *capabilities):
     def __repr__(self):
         # In rare cases we might not have these profile settings when the __repr__ is called.
         name = getattr(self, NAME, [])
-        name0 = name and name[0] or None
         itemid = getattr(self, ITEMID, None)
 
         return "<{0}.{1} at {2:#x} name:{3!r} itemid:{4!r} valid:{5!r} trusted:{6!r}>".format(
             self.__class__.__module__, self.__class__.__name__, id(self),
-            name0, itemid, self.valid, self.trusted)
+            name, itemid, self.valid, self.trusted)
 
     def __getattr__(self, name):
         """