Mercurial > libervia-pubsub
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,