diff sat_pubsub/backend.py @ 330:82d1259b3e36

backend, pgsql storage: better items/notification handling, various fixes: - replaced const.VAL_AMODEL_ROSTER by const.VAL_AMODEL_PUBLISHER_ROSTER to follow change in pgsql schema - implemented whitelist access model - fixed bad access check during items retrieval (access was checked on recipient instead of requestor/sender) - getItemsData and notification filtering now use inline callbacks: this make these complexe workflows far mor easy to read, and clarity is imperative in these security critical sections. - publisher-roster access model now need to have only one owner, else it will fail. The idea is to use this model only when owner=publisher, else there is ambiguity on the roster to use to check access - replaced getNodeOwner by node.getOwners, as a node can have several owners - notifications filtering has been fixed in a similar way - psql: simplified withPEP method, pep_table argument is actually not needed - removed error.NotInRoster: error.Forbidden is used instead - notifications now notify all the owners, not only the first one
author Goffi <goffi@goffi.org>
date Sun, 26 Mar 2017 20:52:32 +0200
parents 29c2553ef863
children e93a9fd329d9
line wrap: on
line diff
--- a/sat_pubsub/backend.py	Sun Mar 26 20:33:18 2017 +0200
+++ b/sat_pubsub/backend.py	Sun Mar 26 20:52:32 2017 +0200
@@ -124,8 +124,8 @@
                  "label": "Who can subscribe to this node",
                  "options": {
                      const.VAL_AMODEL_OPEN: "Public node",
-                     const.VAL_AMODEL_ROSTER: "Node restricted to some roster groups",
-                     const.VAL_AMODEL_JID: "Node restricted to some jids",
+                     const.VAL_AMODEL_PUBLISHER_ROSTER: "Node restricted to some groups of publisher's roster",
+                     const.VAL_AMODEL_WHITELIST: "Node restricted to some jids",
                      }
                 },
             const.OPT_ROSTER_GROUPS_ALLOWED:
@@ -559,120 +559,123 @@
         return self.storage.getAffiliations(entity)
 
 
-    def getItems(self, nodeIdentifier, recipient, maxItems=None,
+    def getItems(self, nodeIdentifier, requestor, recipient, maxItems=None,
                        itemIdentifiers=None, ext_data=None):
-        d = self.getItemsData(nodeIdentifier, recipient, maxItems, itemIdentifiers, ext_data)
+        d = self.getItemsData(nodeIdentifier, requestor, recipient, maxItems, itemIdentifiers, ext_data)
         d.addCallback(lambda items_data: [item_data.item for item_data in items_data])
         return d
 
-    def getItemsData(self, nodeIdentifier, recipient, maxItems=None,
+    @defer.inlineCallbacks
+    def getOwnerRoster(self, node, owners=None):
+        if owners is None:
+            owners = yield node.getOwners()
+
+        if len(owners) != 1:
+            log.msg('publisher-roster access is not allowed with more than 1 owner')
+            return
+
+        owner_jid = owners[0]
+
+        try:
+            roster = yield self.privilege.getRoster(owner_jid)
+        except Exception as e:
+            log.msg("Error while getting roster of {owner_jid}: {msg}".format(
+                owner_jid = owner_jid.full(),
+                msg = e))
+            return
+        defer.returnValue(roster)
+
+    @defer.inlineCallbacks
+    def getItemsData(self, nodeIdentifier, requestor, recipient, maxItems=None,
                        itemIdentifiers=None, ext_data=None):
         """like getItems but return the whole ItemData"""
+        if maxItems == 0:
+            log.msg("WARNING: maxItems=0 on items retrieval")
+            defer.returnValue([])
+
         if ext_data is None:
             ext_data = {}
-        d = self.storage.getNode(nodeIdentifier, ext_data.get('pep', False), recipient)
-        d.addCallback(_getAffiliation, recipient)
-        d.addCallback(self._doGetItems, recipient, maxItems, itemIdentifiers,
-                      ext_data)
-        return d
+        node = yield self.storage.getNode(nodeIdentifier, ext_data.get('pep', False), recipient)
+        node, affiliation = yield _getAffiliation(node, requestor)
+
+        if not iidavoll.ILeafNode.providedBy(node):
+            defer.returnValue([])
+
+        if affiliation == 'outcast':
+            raise error.Forbidden()
+
 
-    def checkGroup(self, roster_groups, entity):
-        """Check that entity is authorized and in roster
-        @param roster_group: tuple which 2 items:
-                        - roster: mapping of jid to RosterItem as given by self.privilege.getRoster
-                        - groups: list of authorized groups
-        @param entity: entity which must be in group
-        @return: (True, roster) if entity is in roster and authorized
-                 (False, roster) if entity is in roster but not authorized
-        @raise: error.NotInRoster if entity is not in roster"""
-        roster, authorized_groups = roster_groups
-        _entity = entity.userhostJID()
+        # 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 _entity in roster:
-            raise error.NotInRoster
-        return (roster[_entity].groups.intersection(authorized_groups), roster)
+            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")
 
-    def _getNodeGroups(self, roster, nodeIdentifier, pep):
-        d = self.storage.getNodeGroups(nodeIdentifier, pep)
-        d.addCallback(lambda groups: (roster, groups))
-        return d
+        # at this point node access is checked
 
-    def _rosterEb(self, failure, owner_jid):
-        log.msg("Error while getting roster of {}: {}".format(unicode(owner_jid), failure.value))
-        return {}
+        if owner:
+            # requestor_groups is only used in restricted access
+            requestor_groups = None
+        else:
+            if roster is None:
+                roster = yield self.getOwnerRoster(node)
+                if roster is None:
+                    roster = {}
+            roster_item = roster.get(requestor.userhostJID())
+            requestor_groups = tuple(roster_item.groups) if roster_item else tuple()
 
-    def _doGetItems(self, result, requestor, maxItems, itemIdentifiers,
-                    ext_data):
-        node, affiliation = result
-        if maxItems == 0:
-            log.msg("WARNING: maxItems=0 on items retrieval")
-            return []
+        if itemIdentifiers:
+            items_data = yield node.getItemsById(requestor_groups, owner, itemIdentifiers)
+        else:
+            items_data = yield node.getItems(requestor_groups, owner, maxItems, ext_data)
 
-        def append_item_config(items_data):
-            """Add item config data form to items with roster access model"""
+        if owner:
+            # Add item config data form to items with roster access model
             for item_data in items_data:
                 if item_data.access_model == const.VAL_AMODEL_OPEN:
                     pass
-                elif item_data.access_model == const.VAL_AMODEL_ROSTER:
+                elif item_data.access_model == const.VAL_AMODEL_PUBLISHER_ROSTER:
                     form = data_form.Form('submit', formNamespace=const.NS_ITEM_CONFIG)
-                    access = data_form.Field(None, const.OPT_ACCESS_MODEL, value=const.VAL_AMODEL_ROSTER)
+                    access = data_form.Field(None, const.OPT_ACCESS_MODEL, value=const.VAL_AMODEL_PUBLISHER_ROSTER)
                     allowed = data_form.Field(None, const.OPT_ROSTER_GROUPS_ALLOWED, values=item_data.config[const.OPT_ROSTER_GROUPS_ALLOWED])
                     form.addField(access)
                     form.addField(allowed)
                     item_data.item.addChild(form.toElement())
-                elif access_model == const.VAL_AMODEL_JID:
-                    #FIXME: manage jid
+                elif access_model == const.VAL_AMODEL_WHITELIST:
+                    #FIXME
                     raise NotImplementedError
                 else:
                     raise error.BadAccessTypeError(access_model)
-            return items_data
 
-        def access_checked(access_data):
-            authorized, roster = access_data
-            if not authorized:
-                raise error.NotAuthorized()
-
-            roster_item = roster.get(requestor.userhostJID())
-            authorized_groups = tuple(roster_item.groups) if roster_item else tuple()
-            owner = affiliation == 'owner'
-
-            if itemIdentifiers:
-                d = node.getItemsById(authorized_groups, owner, itemIdentifiers)
-            else:
-                d = node.getItems(authorized_groups, owner, maxItems, ext_data)
-                if owner:
-                    d.addCallback(append_item_config)
-
-            d.addCallback(self._items_rsm, node, authorized_groups,
-                          owner, itemIdentifiers,
-                          ext_data)
-            return d
-
-        if not iidavoll.ILeafNode.providedBy(node):
-            return []
-
-        if affiliation == 'outcast':
-            raise error.Forbidden()
-
-        access_model = node.getAccessModel()
-        d = node.getNodeOwner()
-
-        def gotOwner(owner_jid):
-            d_roster = self.privilege.getRoster(owner_jid)
-            d_roster.addErrback(self._rosterEb, owner_jid)
-            return d_roster
-
-        d.addCallback(gotOwner)
-
-        if access_model == const.VAL_AMODEL_OPEN or affiliation == 'owner':
-            d.addCallback(lambda roster: (True, roster))
-            d.addCallback(access_checked)
-        elif access_model == const.VAL_AMODEL_ROSTER:
-            d.addCallback(self._getNodeGroups, node.nodeIdentifier, ext_data.get('pep', False))
-            d.addCallback(self.checkGroup, requestor)
-            d.addCallback(access_checked)
-
-        return d
+        yield self._items_rsm(items_data, node, requestor_groups, owner, itemIdentifiers, ext_data)
+        defer.returnValue(items_data)
 
     def _setCount(self, value, response):
         response.count = value
@@ -906,7 +909,6 @@
         error.NodeExists: ('conflict', None, None),
         error.Forbidden: ('forbidden', None, None),
         error.NotAuthorized: ('not-authorized', None, None),
-        error.NotInRoster: ('not-authorized', 'not-in-roster-group', None),
         error.ItemNotFound: ('item-not-found', None, None),
         error.ItemForbidden: ('bad-request', 'item-forbidden', None),
         error.ItemRequired: ('bad-request', 'item-required', None),
@@ -971,8 +973,8 @@
         recipient = data['recipient']
 
         def afterPrepare(result):
-            owner_jid, notifications_filtered = result
-            #we notify the owner
+            owners, notifications_filtered = result
+            #we notify the owners
             #FIXME: check if this comply with XEP-0060 (option needed ?)
             #TODO: item's access model have to be sent back to owner
             #TODO: same thing for getItems
@@ -989,11 +991,13 @@
                     new_item.addChild(item_config.toElement())
                 return new_item
 
-            notifications_filtered.append((owner_jid,
-                                           set([pubsub.Subscription(node.nodeIdentifier,
-                                                            owner_jid,
-                                                            'subscribed')]),
-                                           [getFullItem(item_data) for item_data in items_data]))
+            for owner_jid in owners:
+                notifications_filtered.append(
+                    (owner_jid,
+                     set([pubsub.Subscription(node.nodeIdentifier,
+                                              owner_jid,
+                                              'subscribed')]),
+                     [getFullItem(item_data) for item_data in items_data]))
 
             if pep:
                 return self.backend.privilege.notifyPublish(
@@ -1018,14 +1022,16 @@
         recipient = data['recipient']
 
         def afterPrepare(result):
-            owner_jid, notifications_filtered = result
-            #we add the owner
+            owners, notifications_filtered = result
+            #we add the owners
 
-            notifications_filtered.append((owner_jid,
-                                           set([pubsub.Subscription(node.nodeIdentifier,
-                                                            owner_jid,
-                                                            'subscribed')]),
-                                           [item_data.item for item_data in items_data]))
+            for owner_jid in owners:
+                notifications_filtered.append(
+                    (owner_jid,
+                     set([pubsub.Subscription(node.nodeIdentifier,
+                                              owner_jid,
+                                              'subscribed')]),
+                     [item_data.item for item_data in items_data]))
 
             if pep:
                 return self.backend.privilege.notifyRetract(
@@ -1044,6 +1050,7 @@
         return d
 
 
+    @defer.inlineCallbacks
     def _prepareNotify(self, items_data, node, subscription=None):
         """Do a bunch of permissions check and filter notifications
 
@@ -1062,64 +1069,52 @@
             - items_data
         """
 
-        def filterNotifications(result):
-            """Check access of subscriber for each item, and keep only allowed ones"""
-            notifications, (owner_jid,roster) = result
-
-            #we filter items not allowed for the subscribers
-            notifications_filtered = []
-
-            for subscriber, subscriptions, items_data in notifications:
-                if subscriber == owner_jid:
-                    # as notification is always sent to owner,
-                    # we ignore owner if he is here
-                    continue
-                allowed_items = [] #we keep only item which subscriber can access
-
-                for item_data in items_data:
-                    item, access_model = item_data.item, item_data.access_model
-                    access_list = item_data.config
-                    if access_model == const.VAL_AMODEL_OPEN:
-                        allowed_items.append(item)
-                    elif access_model == const.VAL_AMODEL_ROSTER:
-                        _subscriber = subscriber.userhostJID()
-                        if not _subscriber in roster:
-                            continue
-                        #the subscriber is known, is he in the right group ?
-                        authorized_groups = access_list[const.OPT_ROSTER_GROUPS_ALLOWED]
-                        if roster[_subscriber].groups.intersection(authorized_groups):
-                            allowed_items.append(item)
-
-                    else: #unknown access_model
-                        raise NotImplementedError
-
-                if allowed_items:
-                    notifications_filtered.append((subscriber, subscriptions, allowed_items))
-            return (owner_jid, notifications_filtered)
-
 
         if subscription is None:
-            d1 = self.backend.getNotifications(node.nodeDbId, items_data)
+            notifications = yield self.backend.getNotifications(node.nodeDbId, items_data)
         else:
-            d1 = defer.succeed([(subscription.subscriber, [subscription],
-                                items_data)])
+            notifications = [(subscription.subscriber, [subscription], items_data)]
+
+        owners = node.getOwners()
+        owner_roster = None
+
+        # now we check access of subscriber for each item, and keep only allowed ones
 
-        def _got_owner(owner_jid):
-            #return a tuple with owner_jid and roster
-            def rosterEb(failure):
-                log.msg("Error while getting roster of {}: {}".format(unicode(owner_jid), failure.value))
-                return (owner_jid, {})
+        #we filter items not allowed for the subscribers
+        notifications_filtered = []
+
+        for subscriber, subscriptions, items_data in notifications:
+            subscriber_bare = subscriber.userhostJID()
+            if subscriber_bare in owners:
+                # as notification is always sent to owner,
+                # we ignore owner if he is here
+                continue
+            allowed_items = [] #we keep only item which subscriber can access
 
-            d = self.backend.privilege.getRoster(owner_jid)
-            d.addErrback(rosterEb)
-            d.addCallback(lambda roster: (owner_jid,roster))
-            return d
+            for item_data in items_data:
+                item, access_model = item_data.item, item_data.access_model
+                access_list = item_data.config
+                if access_model == const.VAL_AMODEL_OPEN:
+                    allowed_items.append(item)
+                elif access_model == const.VAL_AMODEL_PUBLISHER_ROSTER:
+                    if owner_roster is None:
+                        owner_roster= yield self.getOwnerRoster(node, owners)
+                    if owner_roster is None:
+                        owner_roster = {}
+                    if not subscriber_bare in owner_roster:
+                        continue
+                    #the subscriber is known, is he in the right group ?
+                    authorized_groups = access_list[const.OPT_ROSTER_GROUPS_ALLOWED]
+                    if owner_roster[subscriber_bare].groups.intersection(authorized_groups):
+                        allowed_items.append(item)
+                else: #unknown access_model
+                    # TODO: white list access
+                    raise NotImplementedError
 
-        d2 = node.getNodeOwner()
-        d2.addCallback(_got_owner)
-        d = defer.gatherResults([d1, d2])
-        d.addCallback(filterNotifications)
-        return d
+            if allowed_items:
+                notifications_filtered.append((subscriber, subscriptions, allowed_items))
+
+        defer.returnValue((owners, notifications_filtered))
 
     def _preDelete(self, data, pep, recipient):
         nodeIdentifier = data['node'].nodeIdentifier
@@ -1132,7 +1127,6 @@
                                                 redirectURI))
         return d
 
-
     def _mapErrors(self, failure):
         e = failure.trap(*self._errorMap.keys())
 
@@ -1291,6 +1285,7 @@
         except AttributeError:
             pass
         d = self.backend.getItems(request.nodeIdentifier,
+                                  request.sender,
                                   request.recipient,
                                   request.maxItems,
                                   request.itemIdentifiers,