comparison 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
comparison
equal deleted inserted replaced
329:98409ef42c94 330:82d1259b3e36
122 const.OPT_ACCESS_MODEL: 122 const.OPT_ACCESS_MODEL:
123 {"type": "list-single", 123 {"type": "list-single",
124 "label": "Who can subscribe to this node", 124 "label": "Who can subscribe to this node",
125 "options": { 125 "options": {
126 const.VAL_AMODEL_OPEN: "Public node", 126 const.VAL_AMODEL_OPEN: "Public node",
127 const.VAL_AMODEL_ROSTER: "Node restricted to some roster groups", 127 const.VAL_AMODEL_PUBLISHER_ROSTER: "Node restricted to some groups of publisher's roster",
128 const.VAL_AMODEL_JID: "Node restricted to some jids", 128 const.VAL_AMODEL_WHITELIST: "Node restricted to some jids",
129 } 129 }
130 }, 130 },
131 const.OPT_ROSTER_GROUPS_ALLOWED: 131 const.OPT_ROSTER_GROUPS_ALLOWED:
132 {"type": "list-multi", 132 {"type": "list-multi",
133 "label": "Groups of the roster allowed to access the node", 133 "label": "Groups of the roster allowed to access the node",
557 557
558 def getAffiliations(self, entity): 558 def getAffiliations(self, entity):
559 return self.storage.getAffiliations(entity) 559 return self.storage.getAffiliations(entity)
560 560
561 561
562 def getItems(self, nodeIdentifier, recipient, maxItems=None, 562 def getItems(self, nodeIdentifier, requestor, recipient, maxItems=None,
563 itemIdentifiers=None, ext_data=None): 563 itemIdentifiers=None, ext_data=None):
564 d = self.getItemsData(nodeIdentifier, recipient, maxItems, itemIdentifiers, ext_data) 564 d = self.getItemsData(nodeIdentifier, requestor, recipient, maxItems, itemIdentifiers, ext_data)
565 d.addCallback(lambda items_data: [item_data.item for item_data in items_data]) 565 d.addCallback(lambda items_data: [item_data.item for item_data in items_data])
566 return d 566 return d
567 567
568 def getItemsData(self, nodeIdentifier, recipient, maxItems=None, 568 @defer.inlineCallbacks
569 def getOwnerRoster(self, node, owners=None):
570 if owners is None:
571 owners = yield node.getOwners()
572
573 if len(owners) != 1:
574 log.msg('publisher-roster access is not allowed with more than 1 owner')
575 return
576
577 owner_jid = owners[0]
578
579 try:
580 roster = yield self.privilege.getRoster(owner_jid)
581 except Exception as e:
582 log.msg("Error while getting roster of {owner_jid}: {msg}".format(
583 owner_jid = owner_jid.full(),
584 msg = e))
585 return
586 defer.returnValue(roster)
587
588 @defer.inlineCallbacks
589 def getItemsData(self, nodeIdentifier, requestor, recipient, maxItems=None,
569 itemIdentifiers=None, ext_data=None): 590 itemIdentifiers=None, ext_data=None):
570 """like getItems but return the whole ItemData""" 591 """like getItems but return the whole ItemData"""
592 if maxItems == 0:
593 log.msg("WARNING: maxItems=0 on items retrieval")
594 defer.returnValue([])
595
571 if ext_data is None: 596 if ext_data is None:
572 ext_data = {} 597 ext_data = {}
573 d = self.storage.getNode(nodeIdentifier, ext_data.get('pep', False), recipient) 598 node = yield self.storage.getNode(nodeIdentifier, ext_data.get('pep', False), recipient)
574 d.addCallback(_getAffiliation, recipient) 599 node, affiliation = yield _getAffiliation(node, requestor)
575 d.addCallback(self._doGetItems, recipient, maxItems, itemIdentifiers, 600
576 ext_data) 601 if not iidavoll.ILeafNode.providedBy(node):
577 return d 602 defer.returnValue([])
578 603
579 def checkGroup(self, roster_groups, entity): 604 if affiliation == 'outcast':
580 """Check that entity is authorized and in roster 605 raise error.Forbidden()
581 @param roster_group: tuple which 2 items: 606
582 - roster: mapping of jid to RosterItem as given by self.privilege.getRoster 607
583 - groups: list of authorized groups 608 # node access check
584 @param entity: entity which must be in group 609 owner = affiliation == 'owner'
585 @return: (True, roster) if entity is in roster and authorized 610 access_model = node.getAccessModel()
586 (False, roster) if entity is in roster but not authorized 611 roster = None
587 @raise: error.NotInRoster if entity is not in roster""" 612
588 roster, authorized_groups = roster_groups 613 if access_model == const.VAL_AMODEL_OPEN or owner:
589 _entity = entity.userhostJID() 614 pass
590 615 elif access_model == const.VAL_AMODEL_PUBLISHER_ROSTER:
591 if not _entity in roster: 616 roster = yield self.getOwnerRoster(node)
592 raise error.NotInRoster 617
593 return (roster[_entity].groups.intersection(authorized_groups), roster) 618 if roster is None:
594 619 raise error.Forbidden()
595 def _getNodeGroups(self, roster, nodeIdentifier, pep): 620
596 d = self.storage.getNodeGroups(nodeIdentifier, pep) 621 if requestor not in roster:
597 d.addCallback(lambda groups: (roster, groups)) 622 raise error.Forbidden()
598 return d 623
599 624 authorized_groups = yield node.getAuthorizedGroups()
600 def _rosterEb(self, failure, owner_jid): 625
601 log.msg("Error while getting roster of {}: {}".format(unicode(owner_jid), failure.value)) 626 if not roster[requestor].groups.intersection(authorized_groups):
602 return {} 627 # requestor is in roster but not in one of the allowed groups
603 628 raise error.Forbidden()
604 def _doGetItems(self, result, requestor, maxItems, itemIdentifiers, 629 elif access_model == const.VAL_AMODEL_WHITELIST:
605 ext_data): 630 affiliations = yield node.getAffiliations()
606 node, affiliation = result 631 try:
607 if maxItems == 0: 632 affiliation = affiliations[requestor.userhostJID()]
608 log.msg("WARNING: maxItems=0 on items retrieval") 633 except KeyError:
609 return [] 634 raise error.Forbidden()
610 635 else:
611 def append_item_config(items_data): 636 if affiliation not in ('owner', 'publisher', 'member'):
612 """Add item config data form to items with roster access model""" 637 raise error.Forbidden()
638 else:
639 raise Exception(u"Unknown access_model")
640
641 # at this point node access is checked
642
643 if owner:
644 # requestor_groups is only used in restricted access
645 requestor_groups = None
646 else:
647 if roster is None:
648 roster = yield self.getOwnerRoster(node)
649 if roster is None:
650 roster = {}
651 roster_item = roster.get(requestor.userhostJID())
652 requestor_groups = tuple(roster_item.groups) if roster_item else tuple()
653
654 if itemIdentifiers:
655 items_data = yield node.getItemsById(requestor_groups, owner, itemIdentifiers)
656 else:
657 items_data = yield node.getItems(requestor_groups, owner, maxItems, ext_data)
658
659 if owner:
660 # Add item config data form to items with roster access model
613 for item_data in items_data: 661 for item_data in items_data:
614 if item_data.access_model == const.VAL_AMODEL_OPEN: 662 if item_data.access_model == const.VAL_AMODEL_OPEN:
615 pass 663 pass
616 elif item_data.access_model == const.VAL_AMODEL_ROSTER: 664 elif item_data.access_model == const.VAL_AMODEL_PUBLISHER_ROSTER:
617 form = data_form.Form('submit', formNamespace=const.NS_ITEM_CONFIG) 665 form = data_form.Form('submit', formNamespace=const.NS_ITEM_CONFIG)
618 access = data_form.Field(None, const.OPT_ACCESS_MODEL, value=const.VAL_AMODEL_ROSTER) 666 access = data_form.Field(None, const.OPT_ACCESS_MODEL, value=const.VAL_AMODEL_PUBLISHER_ROSTER)
619 allowed = data_form.Field(None, const.OPT_ROSTER_GROUPS_ALLOWED, values=item_data.config[const.OPT_ROSTER_GROUPS_ALLOWED]) 667 allowed = data_form.Field(None, const.OPT_ROSTER_GROUPS_ALLOWED, values=item_data.config[const.OPT_ROSTER_GROUPS_ALLOWED])
620 form.addField(access) 668 form.addField(access)
621 form.addField(allowed) 669 form.addField(allowed)
622 item_data.item.addChild(form.toElement()) 670 item_data.item.addChild(form.toElement())
623 elif access_model == const.VAL_AMODEL_JID: 671 elif access_model == const.VAL_AMODEL_WHITELIST:
624 #FIXME: manage jid 672 #FIXME
625 raise NotImplementedError 673 raise NotImplementedError
626 else: 674 else:
627 raise error.BadAccessTypeError(access_model) 675 raise error.BadAccessTypeError(access_model)
628 return items_data 676
629 677 yield self._items_rsm(items_data, node, requestor_groups, owner, itemIdentifiers, ext_data)
630 def access_checked(access_data): 678 defer.returnValue(items_data)
631 authorized, roster = access_data
632 if not authorized:
633 raise error.NotAuthorized()
634
635 roster_item = roster.get(requestor.userhostJID())
636 authorized_groups = tuple(roster_item.groups) if roster_item else tuple()
637 owner = affiliation == 'owner'
638
639 if itemIdentifiers:
640 d = node.getItemsById(authorized_groups, owner, itemIdentifiers)
641 else:
642 d = node.getItems(authorized_groups, owner, maxItems, ext_data)
643 if owner:
644 d.addCallback(append_item_config)
645
646 d.addCallback(self._items_rsm, node, authorized_groups,
647 owner, itemIdentifiers,
648 ext_data)
649 return d
650
651 if not iidavoll.ILeafNode.providedBy(node):
652 return []
653
654 if affiliation == 'outcast':
655 raise error.Forbidden()
656
657 access_model = node.getAccessModel()
658 d = node.getNodeOwner()
659
660 def gotOwner(owner_jid):
661 d_roster = self.privilege.getRoster(owner_jid)
662 d_roster.addErrback(self._rosterEb, owner_jid)
663 return d_roster
664
665 d.addCallback(gotOwner)
666
667 if access_model == const.VAL_AMODEL_OPEN or affiliation == 'owner':
668 d.addCallback(lambda roster: (True, roster))
669 d.addCallback(access_checked)
670 elif access_model == const.VAL_AMODEL_ROSTER:
671 d.addCallback(self._getNodeGroups, node.nodeIdentifier, ext_data.get('pep', False))
672 d.addCallback(self.checkGroup, requestor)
673 d.addCallback(access_checked)
674
675 return d
676 679
677 def _setCount(self, value, response): 680 def _setCount(self, value, response):
678 response.count = value 681 response.count = value
679 682
680 def _setIndex(self, value, response, adjust): 683 def _setIndex(self, value, response, adjust):
904 _errorMap = { 907 _errorMap = {
905 error.NodeNotFound: ('item-not-found', None, None), 908 error.NodeNotFound: ('item-not-found', None, None),
906 error.NodeExists: ('conflict', None, None), 909 error.NodeExists: ('conflict', None, None),
907 error.Forbidden: ('forbidden', None, None), 910 error.Forbidden: ('forbidden', None, None),
908 error.NotAuthorized: ('not-authorized', None, None), 911 error.NotAuthorized: ('not-authorized', None, None),
909 error.NotInRoster: ('not-authorized', 'not-in-roster-group', None),
910 error.ItemNotFound: ('item-not-found', None, None), 912 error.ItemNotFound: ('item-not-found', None, None),
911 error.ItemForbidden: ('bad-request', 'item-forbidden', None), 913 error.ItemForbidden: ('bad-request', 'item-forbidden', None),
912 error.ItemRequired: ('bad-request', 'item-required', None), 914 error.ItemRequired: ('bad-request', 'item-required', None),
913 error.NoInstantNodes: ('not-acceptable', 915 error.NoInstantNodes: ('not-acceptable',
914 'unsupported', 916 'unsupported',
969 node = data['node'] 971 node = data['node']
970 pep = data['pep'] 972 pep = data['pep']
971 recipient = data['recipient'] 973 recipient = data['recipient']
972 974
973 def afterPrepare(result): 975 def afterPrepare(result):
974 owner_jid, notifications_filtered = result 976 owners, notifications_filtered = result
975 #we notify the owner 977 #we notify the owners
976 #FIXME: check if this comply with XEP-0060 (option needed ?) 978 #FIXME: check if this comply with XEP-0060 (option needed ?)
977 #TODO: item's access model have to be sent back to owner 979 #TODO: item's access model have to be sent back to owner
978 #TODO: same thing for getItems 980 #TODO: same thing for getItems
979 981
980 def getFullItem(item_data): 982 def getFullItem(item_data):
987 new_item = deepcopy(item) 989 new_item = deepcopy(item)
988 if item_config: 990 if item_config:
989 new_item.addChild(item_config.toElement()) 991 new_item.addChild(item_config.toElement())
990 return new_item 992 return new_item
991 993
992 notifications_filtered.append((owner_jid, 994 for owner_jid in owners:
993 set([pubsub.Subscription(node.nodeIdentifier, 995 notifications_filtered.append(
994 owner_jid, 996 (owner_jid,
995 'subscribed')]), 997 set([pubsub.Subscription(node.nodeIdentifier,
996 [getFullItem(item_data) for item_data in items_data])) 998 owner_jid,
999 'subscribed')]),
1000 [getFullItem(item_data) for item_data in items_data]))
997 1001
998 if pep: 1002 if pep:
999 return self.backend.privilege.notifyPublish( 1003 return self.backend.privilege.notifyPublish(
1000 recipient, 1004 recipient,
1001 node.nodeIdentifier, 1005 node.nodeIdentifier,
1016 node = data['node'] 1020 node = data['node']
1017 pep = data['pep'] 1021 pep = data['pep']
1018 recipient = data['recipient'] 1022 recipient = data['recipient']
1019 1023
1020 def afterPrepare(result): 1024 def afterPrepare(result):
1021 owner_jid, notifications_filtered = result 1025 owners, notifications_filtered = result
1022 #we add the owner 1026 #we add the owners
1023 1027
1024 notifications_filtered.append((owner_jid, 1028 for owner_jid in owners:
1025 set([pubsub.Subscription(node.nodeIdentifier, 1029 notifications_filtered.append(
1026 owner_jid, 1030 (owner_jid,
1027 'subscribed')]), 1031 set([pubsub.Subscription(node.nodeIdentifier,
1028 [item_data.item for item_data in items_data])) 1032 owner_jid,
1033 'subscribed')]),
1034 [item_data.item for item_data in items_data]))
1029 1035
1030 if pep: 1036 if pep:
1031 return self.backend.privilege.notifyRetract( 1037 return self.backend.privilege.notifyRetract(
1032 recipient, 1038 recipient,
1033 node.nodeIdentifier, 1039 node.nodeIdentifier,
1042 d = self._prepareNotify(items_data, node, data.get('subscription')) 1048 d = self._prepareNotify(items_data, node, data.get('subscription'))
1043 d.addCallback(afterPrepare) 1049 d.addCallback(afterPrepare)
1044 return d 1050 return d
1045 1051
1046 1052
1053 @defer.inlineCallbacks
1047 def _prepareNotify(self, items_data, node, subscription=None): 1054 def _prepareNotify(self, items_data, node, subscription=None):
1048 """Do a bunch of permissions check and filter notifications 1055 """Do a bunch of permissions check and filter notifications
1049 1056
1050 The owner is not added to these notifications, 1057 The owner is not added to these notifications,
1051 it must be called by the calling method 1058 it must be called by the calling method
1060 - notifications_filtered 1067 - notifications_filtered
1061 - node_owner_jid 1068 - node_owner_jid
1062 - items_data 1069 - items_data
1063 """ 1070 """
1064 1071
1065 def filterNotifications(result): 1072
1066 """Check access of subscriber for each item, and keep only allowed ones""" 1073 if subscription is None:
1067 notifications, (owner_jid,roster) = result 1074 notifications = yield self.backend.getNotifications(node.nodeDbId, items_data)
1068 1075 else:
1069 #we filter items not allowed for the subscribers 1076 notifications = [(subscription.subscriber, [subscription], items_data)]
1070 notifications_filtered = [] 1077
1071 1078 owners = node.getOwners()
1072 for subscriber, subscriptions, items_data in notifications: 1079 owner_roster = None
1073 if subscriber == owner_jid: 1080
1074 # as notification is always sent to owner, 1081 # now we check access of subscriber for each item, and keep only allowed ones
1075 # we ignore owner if he is here 1082
1076 continue 1083 #we filter items not allowed for the subscribers
1077 allowed_items = [] #we keep only item which subscriber can access 1084 notifications_filtered = []
1078 1085
1079 for item_data in items_data: 1086 for subscriber, subscriptions, items_data in notifications:
1080 item, access_model = item_data.item, item_data.access_model 1087 subscriber_bare = subscriber.userhostJID()
1081 access_list = item_data.config 1088 if subscriber_bare in owners:
1082 if access_model == const.VAL_AMODEL_OPEN: 1089 # as notification is always sent to owner,
1090 # we ignore owner if he is here
1091 continue
1092 allowed_items = [] #we keep only item which subscriber can access
1093
1094 for item_data in items_data:
1095 item, access_model = item_data.item, item_data.access_model
1096 access_list = item_data.config
1097 if access_model == const.VAL_AMODEL_OPEN:
1098 allowed_items.append(item)
1099 elif access_model == const.VAL_AMODEL_PUBLISHER_ROSTER:
1100 if owner_roster is None:
1101 owner_roster= yield self.getOwnerRoster(node, owners)
1102 if owner_roster is None:
1103 owner_roster = {}
1104 if not subscriber_bare in owner_roster:
1105 continue
1106 #the subscriber is known, is he in the right group ?
1107 authorized_groups = access_list[const.OPT_ROSTER_GROUPS_ALLOWED]
1108 if owner_roster[subscriber_bare].groups.intersection(authorized_groups):
1083 allowed_items.append(item) 1109 allowed_items.append(item)
1084 elif access_model == const.VAL_AMODEL_ROSTER: 1110 else: #unknown access_model
1085 _subscriber = subscriber.userhostJID() 1111 # TODO: white list access
1086 if not _subscriber in roster: 1112 raise NotImplementedError
1087 continue 1113
1088 #the subscriber is known, is he in the right group ? 1114 if allowed_items:
1089 authorized_groups = access_list[const.OPT_ROSTER_GROUPS_ALLOWED] 1115 notifications_filtered.append((subscriber, subscriptions, allowed_items))
1090 if roster[_subscriber].groups.intersection(authorized_groups): 1116
1091 allowed_items.append(item) 1117 defer.returnValue((owners, notifications_filtered))
1092
1093 else: #unknown access_model
1094 raise NotImplementedError
1095
1096 if allowed_items:
1097 notifications_filtered.append((subscriber, subscriptions, allowed_items))
1098 return (owner_jid, notifications_filtered)
1099
1100
1101 if subscription is None:
1102 d1 = self.backend.getNotifications(node.nodeDbId, items_data)
1103 else:
1104 d1 = defer.succeed([(subscription.subscriber, [subscription],
1105 items_data)])
1106
1107 def _got_owner(owner_jid):
1108 #return a tuple with owner_jid and roster
1109 def rosterEb(failure):
1110 log.msg("Error while getting roster of {}: {}".format(unicode(owner_jid), failure.value))
1111 return (owner_jid, {})
1112
1113 d = self.backend.privilege.getRoster(owner_jid)
1114 d.addErrback(rosterEb)
1115 d.addCallback(lambda roster: (owner_jid,roster))
1116 return d
1117
1118 d2 = node.getNodeOwner()
1119 d2.addCallback(_got_owner)
1120 d = defer.gatherResults([d1, d2])
1121 d.addCallback(filterNotifications)
1122 return d
1123 1118
1124 def _preDelete(self, data, pep, recipient): 1119 def _preDelete(self, data, pep, recipient):
1125 nodeIdentifier = data['node'].nodeIdentifier 1120 nodeIdentifier = data['node'].nodeIdentifier
1126 redirectURI = data.get('redirectURI', None) 1121 redirectURI = data.get('redirectURI', None)
1127 d = self.backend.getSubscribers(nodeIdentifier, pep, recipient) 1122 d = self.backend.getSubscribers(nodeIdentifier, pep, recipient)
1129 self.serviceJID, 1124 self.serviceJID,
1130 nodeIdentifier, 1125 nodeIdentifier,
1131 subscribers, 1126 subscribers,
1132 redirectURI)) 1127 redirectURI))
1133 return d 1128 return d
1134
1135 1129
1136 def _mapErrors(self, failure): 1130 def _mapErrors(self, failure):
1137 e = failure.trap(*self._errorMap.keys()) 1131 e = failure.trap(*self._errorMap.keys())
1138 1132
1139 condition, pubsubCondition, feature = self._errorMap[e] 1133 condition, pubsubCondition, feature = self._errorMap[e]
1289 try: 1283 try:
1290 ext_data['pep'] = request.delegated 1284 ext_data['pep'] = request.delegated
1291 except AttributeError: 1285 except AttributeError:
1292 pass 1286 pass
1293 d = self.backend.getItems(request.nodeIdentifier, 1287 d = self.backend.getItems(request.nodeIdentifier,
1288 request.sender,
1294 request.recipient, 1289 request.recipient,
1295 request.maxItems, 1290 request.maxItems,
1296 request.itemIdentifiers, 1291 request.itemIdentifiers,
1297 ext_data) 1292 ext_data)
1298 return d.addErrback(self._mapErrors) 1293 return d.addErrback(self._mapErrors)