# HG changeset patch # User Goffi # Date 1427815916 -7200 # Node ID a87c155d0fd5d54c0a4f349d0c5664c1e0cd4052 # Parent dfc47748d8d8f95b4b79533dc825cf1bd6ec831e replaced former roster dirty hack by a XEP-0356 first draft implementation, only roster get is implemented so far diff -r dfc47748d8d8 -r a87c155d0fd5 sat_pubsub/backend.py --- a/sat_pubsub/backend.py Tue Mar 31 17:30:27 2015 +0200 +++ b/sat_pubsub/backend.py Tue Mar 31 17:31:56 2015 +0200 @@ -82,13 +82,13 @@ from copy import deepcopy + def _getAffiliation(node, entity): d = node.getAffiliation(entity) d.addCallback(lambda affiliation: (node, affiliation)) return d - class BackendService(service.Service, utility.EventDispatcher): """ Generic publish-subscribe backend service. @@ -294,7 +294,7 @@ if not item.getAttribute("id"): item["id"] = str(uuid.uuid4()) access_model, item_config = self.parseItemConfig(item) - parsed_items.append((access_model, item_config, item)) + parsed_items.append((access_model, item_config, item)) if persistItems: d = node.storeItems(parsed_items, requestor) @@ -326,8 +326,8 @@ set()) subs.add(subscription) - notifications = [(subscriber, subscriptions, items) - for subscriber, subscriptions + notifications = [(subscriber, subscriptions_, items) + for subscriber, subscriptions_ in subsBySubscriber.iteritems()] return notifications @@ -346,11 +346,9 @@ d.addCallback(toNotifications, nodeIdentifier, items) return d - def registerNotifier(self, observerfn, *args, **kwargs): self.addObserver('//event/pubsub/notify', observerfn, *args, **kwargs) - def subscribe(self, nodeIdentifier, subscriber, requestor): subscriberEntity = subscriber.userhostJID() if subscriberEntity != requestor.userhostJID(): @@ -445,7 +443,7 @@ is_user_jid = False else: is_user_jid = bool(nodeIdentifierJID.user) - + if is_user_jid and nodeIdentifierJID.userhostJID() != requestor.userhostJID(): #we have an user jid node, but not created by the owner of this jid print "Wrong creator" @@ -512,15 +510,15 @@ 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.roster.getRoster + - 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 + @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() - + if not _entity in roster: raise error.NotInRoster return (roster[_entity].groups.intersection(authorized_groups), roster) @@ -530,6 +528,10 @@ d.addCallback(lambda groups: (roster, groups)) return d + def _rosterEb(self, failure): + log.msg("Error while getting roster: {}".format(failure.value)) + return {} + def _doGetItems(self, result, requestor, maxItems, itemIdentifiers, ext_data): node, affiliation = result @@ -552,7 +554,7 @@ raise NotImplementedError else: raise error.BadAccessTypeError(access_model) - + ret.append(item) return ret @@ -590,7 +592,8 @@ access_model = node.getConfiguration()["pubsub#access_model"] d = node.getNodeOwner() - d.addCallback(self.roster.getRoster) + d.addCallback(self.privilege.getRoster) + d.addErrback(self._rosterEb) if access_model == 'open' or affiliation == 'owner': d.addCallback(lambda roster: (True, roster)) @@ -837,7 +840,7 @@ self.backend.registerNotifier(self._notify) self.backend.registerPreDelete(self._preDelete) - + if self.backend.supportsCreatorCheck(): self.features.append("creator-jid-check") #SàT custom feature: Check that a node (which correspond to # a jid in this server) is created by the right jid @@ -866,12 +869,12 @@ def _notify(self, data): items = data['items'] node = data['node'] - + def _notifyAllowed(result): """Check access of subscriber for each item, and notify only allowed ones""" notifications, (owner_jid,roster) = result - + #we filter items not allowed for the subscribers notifications_filtered = [] @@ -889,24 +892,24 @@ authorized_groups = item_config[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)) - + #we notify the owner #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 - + def getFullItem(item_data): """ Attach item configuration to this item Used to give item configuration back to node's owner (and *only* to owner) """ #TODO: a test should check that only the owner get the item configuration back - + access_model, item_config, item = item_data new_item = deepcopy(item) if item_config: @@ -914,28 +917,29 @@ return new_item notifications_filtered.append((owner_jid, - set([Subscription(node.nodeIdentifier, + set([Subscription(node.nodeIdentifier, owner_jid, 'subscribed')]), - [getFullItem(item_data) for item_data in items])) + [getFullItem(item_data) for item_data in items])) return self.pubsubService.notifyPublish( self.serviceJID, node.nodeIdentifier, notifications_filtered) - + if 'subscription' not in data: d1 = self.backend.getNotifications(node.nodeIdentifier, items) else: subscription = data['subscription'] d1 = defer.succeed([(subscription.subscriber, [subscription], items)]) - + def _got_owner(owner_jid): #return a tuple with owner_jid and roster - d = self.backend.roster.getRoster(owner_jid) - return d.addCallback(lambda roster: (owner_jid,roster)) + d = self.backend.privilege.getRoster(owner_jid) + d.addErrback(self._rosterEb) + d.addCallback(lambda roster: (owner_jid,roster)) d2 = node.getNodeOwner() d2.addCallback(_got_owner) @@ -943,7 +947,6 @@ d = defer.gatherResults([d1, d2]) d.addCallback(_notifyAllowed) - def _preDelete(self, data): nodeIdentifier = data['node'].nodeIdentifier redirectURI = data.get('redirectURI', None) diff -r dfc47748d8d8 -r a87c155d0fd5 sat_pubsub/privilege.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sat_pubsub/privilege.py Tue Mar 31 17:31:56 2015 +0200 @@ -0,0 +1,157 @@ +#!/usr/bin/python +#-*- coding: utf-8 -*- +# +""" +Copyright (c) 2003-2011 Ralph Meijer +Copyright (c) 2012, 2013, 2014, 2015 Jérôme Poisson + + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +-- + +This program is based on Idavoll (http://idavoll.ik.nu/), +originaly written by Ralph Meijer (http://ralphm.net/blog/) +It is sublicensed under AGPL v3 (or any later version) as allowed by the original +license. + +-- + +Here is a copy of the original license: + +Copyright (c) 2003-2011 Ralph Meijer + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" + +""" +Remote roster client. + +This module access roster throught a hacked version of +remote roster management http://jkaluza.fedorapeople.org/remote-roster.html +""" + +from wokkel import xmppim +from wokkel.compat import IQ +from wokkel.subprotocols import XMPPHandler +from twisted.python import log +from twisted.python import failure + +PRIV_ENT_NS = 'urn:xmpp:privilege:1' +PRIV_ENT_ADV_XPATH = '/message/privilege[@xmlns="{}"]'.format(PRIV_ENT_NS) +ROSTER_NS = 'jabber:iq:roster' +PERM_ROSTER = 'roster' +PERM_MESSAGE = 'message' +PERM_PRESENCE = 'presence' +ALLOWED_ROSTER = ('none', 'get', 'set', 'both') +ALLOWED_MESSAGE = ('none', 'outgoing') +ALLOWED_PRESENCE = ('none', 'managed_entity', 'roster') +TO_CHECK = {PERM_ROSTER:ALLOWED_ROSTER, PERM_MESSAGE:ALLOWED_MESSAGE, PERM_PRESENCE:ALLOWED_PRESENCE} + + +class InvalidStanza(Exception): + pass + +class NotAllowedError(Exception): + pass + +class PrivilegesHandler(XMPPHandler): + #FIXME: need to manage updates, and database sync + #TODO: cache + + def __init__(self): + super(PrivilegesHandler, self).__init__() + self._permissions = {PERM_ROSTER: 'none', + PERM_MESSAGE: 'none', + PERM_PRESENCE: 'none'} + + @property + def permissions(self): + return self._permissions + + def connectionInitialized(self): + self.xmlstream.addObserver(PRIV_ENT_ADV_XPATH, self.onAdvertise) + + def onAdvertise(self, message): + """Managage the advertising privileges + + self._permissions will be updated according to advertised privileged + """ + privilege_elt = message.elements(PRIV_ENT_NS, 'privilege').next() + for perm_elt in privilege_elt.elements(PRIV_ENT_NS): + try: + if perm_elt.name != 'perm': + raise InvalidStanza(u'unexpected element {}'.format(perm_elt.name)) + perm_access = perm_elt['access'] + perm_type = perm_elt['type'] + try: + if perm_type not in TO_CHECK[perm_access]: + raise InvalidStanza(u'bad type [{}] for permission {}'.format(perm_type, perm_access)) + except KeyError: + raise InvalidStanza(u'bad permission [{}]'.format(perm_access)) + except InvalidStanza as e: + log.msg("Invalid stanza received ({}), setting permission to none".format(e)) + for perm in self._permissions: + self._permissions[perm] = 'none' + break + + self._permissions[perm_access] = perm_type or 'none' + + log.msg('Privileges updated: roster={roster}, message={message}, presence={presence}'.format(**self._permissions)) + + + def getRoster(self, to_jid): + """ + Retrieve contact list. + + @return: Roster as a mapping from L{JID} to L{RosterItem}. + @rtype: L{twisted.internet.defer.Deferred} + """ + if self._permissions[PERM_ROSTER] not in ('get', 'both'): + log.msg("WARNING: permission not allowed to get roster") + raise failure.Failure(NotAllowedError('roster get is not allowed')) + + def processRoster(result): + roster = {} + for element in result.elements(ROSTER_NS, 'item'): + item = xmppim.RosterItem.fromElement(element) + roster[item.entity] = item + + return roster + + iq = IQ(self.xmlstream, 'get') + iq.addElement((ROSTER_NS, 'query')) + iq["to"] = to_jid.userhost() + d = iq.send() + d.addCallback(processRoster) + return d + + diff -r dfc47748d8d8 -r a87c155d0fd5 sat_pubsub/remote_roster.py --- a/sat_pubsub/remote_roster.py Tue Mar 31 17:30:27 2015 +0200 +++ /dev/null Thu Jan 01 00:00:00 1970 +0000 @@ -1,97 +0,0 @@ -#!/usr/bin/python -#-*- coding: utf-8 -*- -# -""" -Copyright (c) 2003-2011 Ralph Meijer -Copyright (c) 2012, 2013, 2014, 2015 Jérôme Poisson - - -This program is free software: you can redistribute it and/or modify -it under the terms of the GNU Affero General Public License as published by -the Free Software Foundation, either version 3 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU Affero General Public License for more details. - -You should have received a copy of the GNU Affero General Public License -along with this program. If not, see . --- - -This program is based on Idavoll (http://idavoll.ik.nu/), -originaly written by Ralph Meijer (http://ralphm.net/blog/) -It is sublicensed under AGPL v3 (or any later version) as allowed by the original -license. - --- - -Here is a copy of the original license: - -Copyright (c) 2003-2011 Ralph Meijer - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -""" - -""" -Remote roster client. - -This module access roster throught a hacked version of -remote roster management http://jkaluza.fedorapeople.org/remote-roster.html -""" - -from wokkel import xmppim -from wokkel.compat import IQ -from twisted.words.xish import domish - -NS_ROSTER = 'jabber:iq:roster' - -class RosterClient(xmppim.RosterClientProtocol): - """Similar to classic RosterClient, but we can get any jid managed by the host server""" - #FIXME: need to manage updates, and database sync - #TODO: cache - - def getRoster(self, to_jid): - """ - Retrieve contact list. - - @return: Roster as a mapping from L{JID} to L{RosterItem}. - @rtype: L{twisted.internet.defer.Deferred} - """ - - def processRoster(result): - roster = {} - for element in domish.generateElementsQNamed(result.query.children, - 'item', NS_ROSTER): - item = xmppim.RosterItem.fromElement(element) - roster[item.entity] = item - - return roster - - iq = IQ(self.xmlstream, 'get') - iq.addElement((NS_ROSTER, 'query')) - iq["to"] = to_jid.userhost() - d = iq.send() - d.addCallback(processRoster) - return d - - diff -r dfc47748d8d8 -r a87c155d0fd5 sat_pubsub/tap.py --- a/sat_pubsub/tap.py Tue Mar 31 17:30:27 2015 +0200 +++ b/sat_pubsub/tap.py Tue Mar 31 17:31:56 2015 +0200 @@ -64,7 +64,7 @@ from sat_pubsub import __version__, const, mam from sat_pubsub.backend import BackendService -from sat_pubsub.remote_roster import RosterClient +from sat_pubsub.privilege import PrivilegesHandler class Options(usage.Options): @@ -138,6 +138,10 @@ VersionHandler(u'SàT Pubsub', __version__).setHandlerParent(cs) DiscoHandler().setHandlerParent(cs) + ph = PrivilegesHandler() + ph.setHandlerParent(cs) + bs.privilege = ph + resource = IPubSubResource(bs) resource.hideNodes = config["hide-nodes"] resource.serviceJID = config["jid"] @@ -146,10 +150,6 @@ ps.setHandlerParent(cs) resource.pubsubService = ps - rc = RosterClient() - rc.setHandlerParent(cs) - bs.roster = rc - if const.FLAG_ENABLE_MAM: mam_resource = mam.MAMResource(bs) mam_s = wokkel_mam.MAMService(mam_resource)