diff sat_pubsub/backend.py @ 349:20b82fb8de02

backend: check nodes/items permission on disco#items: - move node access check workflow from getItemsData to a new checkNodeAccess method - only accessible items are returned to an entity when doing a disco#items on a node - for PEP, nodes with presence access model are not returned if entity has not presence subscription from the node owner - all nodes are returned in normal pubsub service - new NotLeafNodeError exception when an action need to be done on Leaf node and it is not the case - /!\ access it not fully checked : items access models are not handled for items id in disco#items, and whitelist nodes are returned regardless if requestor is in the white list or not. Furthermore, publisher-roster access is not handled for nodes.
author Goffi <goffi@goffi.org>
date Sun, 27 Aug 2017 20:33:39 +0200
parents 3bbab2173ebc
children efbdca10f0fb
line wrap: on
line diff
--- a/sat_pubsub/backend.py	Sun Aug 27 20:26:38 2017 +0200
+++ b/sat_pubsub/backend.py	Sun Aug 27 20:33:39 2017 +0200
@@ -190,9 +190,21 @@
         d.addCallback(lambda node: node.getType())
         return d
 
+    def _getNodesIds(self, subscribed, pep, recipient):
+        # TODO: filter whitelist nodes
+        # TODO: handle publisher-roster (should probably be renamed to owner-roster for nodes)
+        if not subscribed:
+            allowed_accesses = {'open', 'whitelist'}
+        else:
+            allowed_accesses = {'open', 'presence', 'whitelist'}
+        return self.storage.getNodeIds(pep, recipient, allowed_accesses)
 
-    def getNodes(self, pep):
-        return self.storage.getNodeIds(pep)
+    def getNodes(self, requestor, pep, recipient):
+        if pep:
+            d = self.privilege.isSubscribedFrom(requestor, recipient)
+            d.addCallback(self._getNodesIds, pep, recipient)
+            return d
+        return self.storage.getNodeIds(pep, recipient)
 
 
     def getNodeMetaData(self, nodeIdentifier, pep, recipient=None):
@@ -661,15 +673,75 @@
         d.addErrback(self.unwrapFirstError)
         return d
 
+    @defer.inlineCallbacks
+    def checkNodeAccess(self, node, requestor):
+        """check if a requestor can access data of a node
 
-    def getItemsIds(self, nodeIdentifier, authorized_groups, unrestricted, maxItems=None, ext_data=None, pep=False, recipient=None):
-        d = self.storage.getNode(nodeIdentifier, pep, recipient)
-        d.addCallback(lambda node: node.getItemsIds(authorized_groups,
-                                                    unrestricted,
-                                                    maxItems,
-                                                    ext_data))
-        return d
+        @param node(Node): node to check
+        @param requestor(jid.JID): entity who want to access node
+        @return (tuple): permissions data with:
+            - owner(bool): True if requestor is owner of the node
+            - roster(None, ): roster of the requestor
+                None if not needed/available
+            - access_model(str): access model of the node
+        @raise error.Forbidden: access is not granted
+        @raise error.NotLeafNodeError: this node is not a leaf
+        """
+        node, affiliation = yield _getAffiliation(node, requestor)
+
+        if not iidavoll.ILeafNode.providedBy(node):
+            raise error.NotLeafNodeError()
+
+        if affiliation == 'outcast':
+            raise error.Forbidden()
+
+        # node access check
+        owner = affiliation == 'owner'
+        access_model = node.getAccessModel()
+        roster = None
+
+        if access_model == const.VAL_AMODEL_OPEN or owner:
+            pass
+        elif access_model == const.VAL_AMODEL_PUBLISHER_ROSTER:
+            # FIXME: publisher roster should be used, not owner
+            roster = yield self.getOwnerRoster(node)
+
+            if roster is None:
+                raise error.Forbidden()
 
+            if requestor not in roster:
+                raise error.Forbidden()
+
+            authorized_groups = yield node.getAuthorizedGroups()
+
+            if not roster[requestor].groups.intersection(authorized_groups):
+                # requestor is in roster but not in one of the allowed groups
+                raise error.Forbidden()
+        elif access_model == const.VAL_AMODEL_WHITELIST:
+            affiliations = yield node.getAffiliations()
+            try:
+                affiliation = affiliations[requestor.userhostJID()]
+            except KeyError:
+                raise error.Forbidden()
+            else:
+                if affiliation not in ('owner', 'publisher', 'member'):
+                    raise error.Forbidden()
+        else:
+            raise Exception(u"Unknown access_model")
+
+        defer.returnValue((affiliation, owner, roster, access_model))
+
+    @defer.inlineCallbacks
+    def getItemsIds(self, nodeIdentifier, requestor, authorized_groups, unrestricted, maxItems=None, ext_data=None, pep=False, recipient=None):
+        # FIXME: items access model are not checked
+        # TODO: check items access model
+        node = yield self.storage.getNode(nodeIdentifier, pep, recipient)
+        affiliation, owner, roster, access_model = yield self.checkNodeAccess(node, requestor)
+        ids = yield node.getItemsIds(authorized_groups,
+                                     unrestricted,
+                                     maxItems,
+                                     ext_data)
+        defer.returnValue(ids)
 
     def getItems(self, nodeIdentifier, requestor, recipient, maxItems=None,
                        itemIdentifiers=None, ext_data=None):
@@ -679,6 +751,7 @@
 
     @defer.inlineCallbacks
     def getOwnerRoster(self, node, owners=None):
+        # FIXME: roster of publisher, not owner, must be used
         if owners is None:
             owners = yield node.getOwners()
 
@@ -708,48 +781,11 @@
         if ext_data is None:
             ext_data = {}
         node = yield self.storage.getNode(nodeIdentifier, ext_data.get('pep', False), recipient)
-        node, affiliation = yield _getAffiliation(node, requestor)
-
-        if not iidavoll.ILeafNode.providedBy(node):
+        try:
+            affiliation, owner, roster, access_model = yield self.checkNodeAccess(node, requestor)
+        except error.NotLeafNodeError:
             defer.returnValue([])
 
-        if affiliation == 'outcast':
-            raise error.Forbidden()
-
-
-        # node access check
-        owner = affiliation == 'owner'
-        access_model = node.getAccessModel()
-        roster = None
-
-        if access_model == const.VAL_AMODEL_OPEN or owner:
-            pass
-        elif access_model == const.VAL_AMODEL_PUBLISHER_ROSTER:
-            roster = yield self.getOwnerRoster(node)
-
-            if roster is None:
-                raise error.Forbidden()
-
-            if requestor not in roster:
-                raise error.Forbidden()
-
-            authorized_groups = yield node.getAuthorizedGroups()
-
-            if not roster[requestor].groups.intersection(authorized_groups):
-                # requestor is in roster but not in one of the allowed groups
-                raise error.Forbidden()
-        elif access_model == const.VAL_AMODEL_WHITELIST:
-            affiliations = yield node.getAffiliations()
-            try:
-                affiliation = affiliations[requestor.userhostJID()]
-            except KeyError:
-                raise error.Forbidden()
-            else:
-                if affiliation not in ('owner', 'publisher', 'member'):
-                    raise error.Forbidden()
-        else:
-            raise Exception(u"Unknown access_model")
-
         # at this point node access is checked
 
         if owner:
@@ -757,6 +793,7 @@
             requestor_groups = None
         else:
             if roster is None:
+                # FIXME: publisher roster should be used, not owner
                 roster = yield self.getOwnerRoster(node)
                 if roster is None:
                     roster = {}
@@ -1215,6 +1252,7 @@
                     allowed_items.append(item)
                 elif access_model == const.VAL_AMODEL_PUBLISHER_ROSTER:
                     if owner_roster is None:
+                        # FIXME: publisher roster should be used, not owner
                         owner_roster= yield self.getOwnerRoster(node, owners)
                     if owner_roster is None:
                         owner_roster = {}
@@ -1286,6 +1324,12 @@
         return d
 
     def getNodes(self, requestor, service, nodeIdentifier):
+        """return nodes for disco#items
+
+        Pubsub/PEP nodes will be returned if disco node is not specified
+        else Pubsub/PEP items will be returned
+        (according to what requestor can access)
+        """
         try:
             pep = service.pep
         except AttributeError:
@@ -1296,15 +1340,20 @@
 
         if nodeIdentifier:
             d = self.backend.getItemsIds(nodeIdentifier,
+                                         requestor,
                                          [],
                                          requestor.userhostJID() == service,
                                          None,
                                          None,
                                          pep,
                                          service)
+            # items must be set as name, not node
+            d.addCallback(lambda items: [(None, item) for item in items])
 
         else:
-            d = self.backend.getNodes(pep)
+            d = self.backend.getNodes(requestor.userhostJID(),
+                                      pep,
+                                      service)
         return d.addErrback(self._mapErrors)