changeset 250:eb14b8d30cba

fine tuning per-item permissions
author Goffi <goffi@goffi.org>
date Sun, 24 Jun 2012 19:35:49 +0200
parents aaf5e34ff765
children 0a7d43b3dad6
files sat_pubsub/backend.py sat_pubsub/const.py sat_pubsub/error.py sat_pubsub/pgsql_storage.py
diffstat 4 files changed, 168 insertions(+), 53 deletions(-) [+]
line wrap: on
line diff
--- 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):
--- /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 <http://www.gnu.org/licenses/>.
+--
+
+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
--- 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):
--- 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: