# HG changeset patch # User Goffi # Date 1340559349 -7200 # Node ID eb14b8d30cba38871c26927c409dab92a091644a # Parent aaf5e34ff765fba3dde6a3fa487f2db84fd2a64c fine tuning per-item permissions diff -r aaf5e34ff765 -r eb14b8d30cba sat_pubsub/backend.py --- a/sat_pubsub/backend.py Fri Jun 08 18:34:45 2012 +0200 +++ b/sat_pubsub/backend.py Sun Jun 24 19:35:49 2012 +0200 @@ -73,13 +73,14 @@ from twisted.words.protocols.jabber.jid import JID, InvalidFormat from twisted.words.xish import utility -from wokkel import disco +from wokkel import disco, data_form from wokkel.iwokkel import IPubSubResource from wokkel.pubsub import PubSubResource, PubSubError -from sat_pubsub import error, iidavoll +from sat_pubsub import error, iidavoll, const from sat_pubsub.iidavoll import IBackendService, ILeafNode + def _getAffiliation(node, entity): d = node.getAffiliation(entity) d.addCallback(lambda affiliation: (node, affiliation)) @@ -141,6 +142,10 @@ return True + def supportsGroupBlog(self): + return True + + def supportsOutcastAffiliation(self): return True @@ -188,10 +193,33 @@ d.addCallback(check, node) return d + def parseItemConfig(self, item): + """Get and remove item configuration information + @param item: + """ + item_config = None + access_model = const.VAL_DEFAULT + for i in range(len(item.children)): + elt = item.children[i] + if not (elt.uri,elt.name)==(data_form.NS_X_DATA,'x'): + continue + form = data_form.Form.fromElement(elt) + if (form.formNamespace == const.NS_ITEM_CONFIG): + item_config = form + del item.children[i] #we need to remove the config from item + break + + if item_config: + access_model = item_config.get(const.OPT_ACCESS_MODEL, const.VAL_DEFAULT) + + return (access_model, item_config) + def publish(self, nodeIdentifier, items, requestor): d = self.storage.getNode(nodeIdentifier) d.addCallback(self._checkAuth, requestor) + #FIXME: owner and publisher are not necessarly the same. So far we use only owner to get roster. + #FIXME: in addition, there can be several owners: that is not managed yet d.addCallback(self._doPublish, items, requestor) return d @@ -209,29 +237,32 @@ elif not items and (persistItems or deliverPayloads): raise error.ItemRequired() - if persistItems or deliverPayloads: - for item in items: + parsed_items = [] + for item in items: + if persistItems or deliverPayloads: item.uri = None item.defaultUri = None 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)) if persistItems: - d = node.storeItems(items, requestor) + d = node.storeItems(parsed_items, requestor) else: d = defer.succeed(None) - d.addCallback(self._doNotify, node.nodeIdentifier, items, + d.addCallback(self._doNotify, node, parsed_items, deliverPayloads) return d - def _doNotify(self, result, nodeIdentifier, items, deliverPayloads): + def _doNotify(self, result, node, items, deliverPayloads): if items and not deliverPayloads: - for item in items: + for access_model, item_config, item in items: item.children = [] - self.dispatch({'items': items, 'nodeIdentifier': nodeIdentifier}, + self.dispatch({'items': items, 'node': node}, '//event/pubsub/notify') @@ -284,6 +315,7 @@ def _doSubscribe(self, result, subscriber): node, affiliation = result + #FIXME: must check node's access_model before subscribing if affiliation == 'outcast': raise error.Forbidden() @@ -353,8 +385,9 @@ nodeIdentifier = 'generic/%s' % uuid.uuid4() if self.supportsCreatorCheck(): + groupblog = nodeIdentifier.startswith(const.NS_GROUPBLOG_PREFIX) try: - nodeIdentifierJID = JID(nodeIdentifier) + nodeIdentifierJID = JID(nodeIdentifier[len(const.NS_GROUPBLOG_PREFIX):] if groupblog else nodeIdentifier) except InvalidFormat: is_user_jid = False else: @@ -667,20 +700,60 @@ if self.backend.supportsPublisherAffiliation(): self.features.append("publisher-affiliation") + if self.backend.supportsGroupBlog(): + self.features.append("groupblog") + def _notify(self, data): items = data['items'] - nodeIdentifier = data['nodeIdentifier'] + node = data['node'] + + def _notifyAllowed(result): + """Check access of subscriber for each item, + and notify only allowed ones""" + notifications, roster = result + + #we filter items not allowed for the subscribers + notifications_filtered = [] + + for subscriber, subscriptions, items in notifications: + allowed_items = [] #we keep only item which subscriber can access + + for access_model, item_config, item in items: + if access_model == 'open': + allowed_items.append(item) + elif access_model == 'roster': + _subscriber = subscriber.userhost() + if not _subscriber in roster: + continue + #the subscriber is known, is he in the right group ? + 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 + + notifications_filtered.append((subscriber, subscriptions, allowed_items)) + + return self.pubsubService.notifyPublish( + self.serviceJID, + node.nodeIdentifier, + notifications_filtered) + + if 'subscription' not in data: - d = self.backend.getNotifications(nodeIdentifier, items) + d1 = self.backend.getNotifications(node.nodeIdentifier, items) else: subscription = data['subscription'] - d = defer.succeed([(subscription.subscriber, [subscription], + d1 = defer.succeed([(subscription.subscriber, [subscription], items)]) - d.addCallback(lambda notifications: self.pubsubService.notifyPublish( - self.serviceJID, - nodeIdentifier, - notifications)) + + d2 = node.getNodeOwner() + d2.addCallback(self.backend.roster.getRoster) + + d = defer.gatherResults([d1, d2]) + d.addCallback(_notifyAllowed) def _preDelete(self, data): diff -r aaf5e34ff765 -r eb14b8d30cba sat_pubsub/const.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/sat_pubsub/const.py Sun Jun 24 19:35:49 2012 +0200 @@ -0,0 +1,62 @@ +#!/usr/bin/python +#-*- coding: utf-8 -*- + +""" +Copyright (c) 2003-2011 Ralph Meijer +Copyright (c) 2012 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. + +""" + + +NS_GROUPBLOG_PREFIX = 'urn:xmpp:groupblog:' +NS_ITEM_CONFIG = "http://jabber.org/protocol/pubsub#item-config" +OPT_ACCESS_MODEL = 'pubsub#access_model' +OPT_ROSTER_GROUPS_ALLOWED = 'pubsub#roster_groups_allowed' +VAL_OPEN = 'open' +VAL_ROSTER = 'roster' +VAL_DEFAULT = VAL_OPEN diff -r aaf5e34ff765 -r eb14b8d30cba sat_pubsub/error.py --- a/sat_pubsub/error.py Fri Jun 08 18:34:45 2012 +0200 +++ b/sat_pubsub/error.py Sun Jun 24 19:35:49 2012 +0200 @@ -62,6 +62,8 @@ def __str__(self): return self.msg +class Deprecated(Exception): + pass class NodeNotFound(Error): diff -r aaf5e34ff765 -r eb14b8d30cba sat_pubsub/pgsql_storage.py --- a/sat_pubsub/pgsql_storage.py Fri Jun 08 18:34:45 2012 +0200 +++ b/sat_pubsub/pgsql_storage.py Sun Jun 24 19:35:49 2012 +0200 @@ -61,16 +61,8 @@ from wokkel.generic import parseXml, stripNamespace from wokkel.pubsub import Subscription -from wokkel import data_form -from sat_pubsub import error, iidavoll - -NS_ITEM_CONFIG = "http://jabber.org/protocol/pubsub#item-config" -OPT_ACCESS_MODEL = 'pubsub#access_model' -OPT_ROSTER_GROUPS_ALLOWED = 'pubsub#roster_groups_allowed' -VAL_OPEN = 'open' -VAL_ROSTER = 'roster' -VAL_DEFAULT = VAL_OPEN +from sat_pubsub import error, iidavoll, const class Storage: @@ -507,32 +499,18 @@ nodeType = 'leaf' - def storeItems(self, items, publisher): - return self.dbpool.runInteraction(self._storeItems, items, publisher) - - - def _storeItems(self, cursor, items, publisher): - self._checkNodeExists(cursor) - for item in items: - self._storeItem(cursor, item, publisher) + def storeItems(self, item_data, publisher): + return self.dbpool.runInteraction(self._storeItems, item_data, publisher) - def _storeItem(self, cursor, item, publisher): - item_config = None - access_model = VAL_DEFAULT - for i in range(len(item.children)): - elt = item.children[i] - if not (elt.uri,elt.name)==(data_form.NS_X_DATA,'x'): - continue - form = data_form.Form.fromElement(elt) - if (form.formNamespace == NS_ITEM_CONFIG): - item_config = form - del item.children[i] #we need to remove the config from item - break + def _storeItems(self, cursor, item_data, publisher): + self._checkNodeExists(cursor) + for item_datum in item_data: + self._storeItem(cursor, item_datum, publisher) - if item_config: - access_model = item_config.get(OPT_ACCESS_MODEL, VAL_DEFAULT) - + + def _storeItem(self, cursor, item_datum, publisher): + access_model, item_config, item = item_datum data = item.toXml() cursor.execute("""UPDATE items SET date=now(), publisher=%s, data=%s @@ -556,11 +534,11 @@ access_model, self.nodeIdentifier)) - if access_model == VAL_ROSTER: + if access_model == const.VAL_ROSTER: item_id = cursor.fetchone()[0]; - if OPT_ROSTER_GROUPS_ALLOWED in item_config: - item_config.fields[OPT_ROSTER_GROUPS_ALLOWED].fieldType='list-multi' #XXX: needed to have a list if there is only one value - allowed_groups = item_config[OPT_ROSTER_GROUPS_ALLOWED] + if const.OPT_ROSTER_GROUPS_ALLOWED in item_config: + item_config.fields[const.OPT_ROSTER_GROUPS_ALLOWED].fieldType='list-multi' #XXX: needed to force list if there is only one value + allowed_groups = item_config[const.OPT_ROSTER_GROUPS_ALLOWED] else: allowed_groups = [] for group in allowed_groups: