diff sat_pubsub/privilege.py @ 478:b544109ab4c4

Privileged Entity update + Pubsub Account Management partial implementation + Public Pubsub Subscription /!\ pgsql schema needs to be updated /!\ /!\ server conf needs to be updated for privileged entity: only the new `urn:xmpp:privilege:2` namespace is handled now /!\ Privileged entity has been updated to hanlde the new namespace and IQ permission. Roster pushes are not managed yet. XEP-0376 (Pubsub Account Management) is partially implemented. The XEP is not fully specified at the moment, and my messages on standard@ haven't seen any reply. Thus for now only "Subscribing", "Unsubscribing" and "Listing Subscriptions" is implemented, "Auto Subscriptions" and "Filtering" is not. Public Pubsub Subscription (https://xmpp.org/extensions/inbox/pubsub-public-subscriptions.html) is implemented; the XEP has been accepted by council but is not yet published. It will be updated to use subscription options instead of the <public> element actually specified, I'm waiting for publication to update the XEP. unsubscribe has been updated to return the `<subscription>` element as expected by XEP-0060 (sat_tmp needs to be updated). database schema has been updated to add columns necessary to keep track of subscriptions to external nodes and to mark subscriptions as public.
author Goffi <goffi@goffi.org>
date Wed, 11 May 2022 13:39:08 +0200
parents ed9e12701e0f
children 8bbaa089cb10
line wrap: on
line diff
--- a/sat_pubsub/privilege.py	Mon Jan 03 16:48:22 2022 +0100
+++ b/sat_pubsub/privilege.py	Wed May 11 13:39:08 2022 +0200
@@ -19,13 +19,13 @@
 "This module implements XEP-0356 (Privileged Entity) to manage rosters, messages and "
 "presences"
 
-from typing import Dict, List, Optional, Set
+from typing import Dict, List, Optional, Union, Set
 import time
 
 from twisted.internet import defer
 from twisted.python import log
-from twisted.python import failure
 from twisted.words.protocols.jabber import error, jid
+from twisted.words.protocols.jabber import xmlstream
 from twisted.words.xish import domish
 from wokkel import xmppim
 from wokkel import pubsub
@@ -35,13 +35,14 @@
 
 from .error import NotAllowedError
 
-FORWARDED_NS = 'urn:xmpp:forward:0'
-PRIV_ENT_NS = 'urn:xmpp:privilege:1'
-PRIV_ENT_ADV_XPATH = '/message/privilege[@xmlns="{}"]'.format(PRIV_ENT_NS)
+NS_FORWARDED = 'urn:xmpp:forward:0'
+NS_PRIV_ENT = 'urn:xmpp:privilege:2'
+PRIV_ENT_ADV_XPATH = '/message/privilege[@xmlns="{}"]'.format(NS_PRIV_ENT)
 ROSTER_NS = 'jabber:iq:roster'
 PERM_ROSTER = 'roster'
 PERM_MESSAGE = 'message'
 PERM_PRESENCE = 'presence'
+PERM_IQ = 'iq'
 ALLOWED_ROSTER = ('none', 'get', 'set', 'both')
 ALLOWED_MESSAGE = ('none', 'outgoing')
 ALLOWED_PRESENCE = ('none', 'managed_entity', 'roster')
@@ -50,6 +51,13 @@
     PERM_MESSAGE:ALLOWED_MESSAGE,
     PERM_PRESENCE:ALLOWED_PRESENCE
 }
+PERMS_BASE : dict[str, Optional[Union[str, Dict[str, Union[str, bool]]]]]= {
+    PERM_ROSTER: None,
+    PERM_MESSAGE: None,
+    PERM_PRESENCE: None,
+    PERM_IQ: None,
+}
+
 
 # Number of seconds before a roster cache is not considered valid anymore.
 # We keep this delay to avoid requesting roster too much in a row if an entity is
@@ -70,9 +78,7 @@
     def __init__(self, service_jid):
         super(PrivilegesHandler, self).__init__()
         self.backend = None
-        self._permissions = {PERM_ROSTER: 'none',
-                             PERM_MESSAGE: 'none',
-                             PERM_PRESENCE: 'none'}
+        self._permissions = PERMS_BASE.copy()
         self._pubsub_service = None
         self.caps_map = {}  # key: bare jid, value: dict of resources with caps hash
         # key: (hash,version), value: dict with DiscoInfo instance (infos) and nodes to
@@ -120,34 +126,69 @@
 
         self._permissions will be updated according to advertised privileged
         """
-        privilege_elt = next(message.elements(PRIV_ENT_NS, 'privilege'))
-        for perm_elt in privilege_elt.elements(PRIV_ENT_NS):
+        self._permissions = PERMS_BASE.copy()
+        privilege_elt = next(message.elements(NS_PRIV_ENT, 'privilege'))
+        for perm_elt in privilege_elt.elements(NS_PRIV_ENT, 'perm'):
             try:
-                if perm_elt.name != 'perm':
-                    raise InvalidStanza('unexpected element {}'.format(perm_elt.name))
-                perm_access = perm_elt['access']
-                perm_type = perm_elt['type']
+                perm_access = perm_elt["access"]
+            except KeyError:
+                log.err(f"missing 'access' attribute in perm element: {perm_elt.toXml()}")
+                continue
+            if perm_access in (PERM_ROSTER, PERM_MESSAGE, PERM_PRESENCE):
                 try:
+                    perm_type = perm_elt["type"]
+                except KeyError:
+                    log.err(
+                        "missing 'type' attribute in perm element: "
+                        f"{perm_elt.toXml()}"
+                    )
+                    continue
+                else:
                     if perm_type not in TO_CHECK[perm_access]:
-                        raise InvalidStanza(
-                            'bad type [{}] for permission {}'
-                            .format(perm_type, perm_access)
+                        log.err(
+                            f'bad type {perm_type!r}: {perm_elt.toXml()}'
+                        )
+                        continue
+                self._permissions[perm_access] = perm_type or None
+            elif perm_access == "iq":
+                iq_perms = self._permissions["iq"] = {}
+                for namespace_elt in perm_elt.elements(NS_PRIV_ENT, "namespace"):
+                    ns = namespace_elt.getAttribute("ns")
+                    perm_type = namespace_elt.getAttribute("type")
+                    if not ns or not perm_type:
+                        log.err(
+                            f"invalid namespace element: {namespace_elt.toXml()}"
                         )
-                except KeyError:
-                    raise InvalidStanza('bad permission [{}]'.format(perm_access))
-            except InvalidStanza as e:
-                log.msg(
-                    f"Invalid stanza received ({e}), setting permission to none"
-                )
-                for perm in self._permissions:
-                    self._permissions[perm] = 'none'
-                break
+                    else:
+                        if perm_type not in ("get", "set", "both"):
+                            log.err(
+                                f"invalid namespace type: {namespace_elt.toXml()}"
+                            )
+                        else:
+                            ns_perms = iq_perms[ns] = {"type": perm_type}
+                            ns_perms["get"] = perm_type in ("get", "both")
+                            ns_perms["set"] = perm_type in ("set", "both")
+            else:
+                log.err(f"unknown {perm_access!r} access: {perm_elt.toXml()}'")
 
-            self._permissions[perm_access] = perm_type or 'none'
+        perms = self._permissions
+        perms_iq = perms["iq"]
+        if perms_iq is None:
+            iq_perm_txt = "    no iq perm advertised"
+        elif not isinstance(perms_iq, dict):
+            raise ValueError('INTERNAL ERROR: "iq" perm should a dict')
+        else:
+            iq_perm_txt = "\n".join(
+                f"    - {ns}: {perms['type']}"
+                for ns, perms in perms_iq.items()
+            )
 
         log.msg(
-            'Privileges updated: roster={roster}, message={message}, presence={presence}'
-            .format(**self._permissions)
+            "Privileges updated:\n"
+            f"roster: {perms[PERM_ROSTER]}\n"
+            f"message: {perms[PERM_MESSAGE]}\n"
+            f"presence: {perms[PERM_PRESENCE]}\n"
+            f"iq:\n{iq_perm_txt}"
         )
 
     ## roster ##
@@ -308,8 +349,8 @@
         if to_jid is None:
             to_jid = self.backend.server_jid
         main_message['to'] = to_jid.full()
-        privilege_elt = main_message.addElement((PRIV_ENT_NS, 'privilege'))
-        forwarded_elt = privilege_elt.addElement((FORWARDED_NS, 'forwarded'))
+        privilege_elt = main_message.addElement((NS_PRIV_ENT, 'privilege'))
+        forwarded_elt = privilege_elt.addElement((NS_FORWARDED, 'forwarded'))
         priv_message['xmlns'] = 'jabber:client'
         forwarded_elt.addChild(priv_message)
         self.send(main_message)
@@ -452,6 +493,62 @@
             for pep_jid, node, item, item_access_model in last_items:
                 self.notifyPublish(pep_jid, node, [(from_jid, None, [item])])
 
+    ## IQ ##
+
+    async def sendIQ(
+        self,
+        priv_iq: domish.Element,
+        to: Optional[jid.JID] = None
+    ) -> domish.Element:
+        """Send privileged IQ stanza
+
+        @param priv_iq: privileged IQ stanza
+        @param to: bare jid of user on behalf of who the stanza is sent
+        The stanza will be wrapped and sent to the server. Result/Error stanza will sent
+        back as return value.
+        """
+        if to is None:
+            try:
+                to = jid.JID(priv_iq["from"])
+            except (KeyError, RuntimeError):
+                raise ValueError(
+                    'no "to" specified, and invalid "to" attribute in priv_iq'
+                )
+        if not to.user or to.resource or to.host != self.backend.server_jid.userhost():
+            raise NotAllowedError(
+                f'"to" attribute must be set to a bare jid of the server, {to} is invalid'
+            )
+        iq_type = priv_iq.getAttribute("type")
+        if iq_type not in ("get", "set"):
+            raise ValueError(f"invalid IQ type: {priv_iq.toXml()}")
+        first_child = priv_iq.firstChildElement()
+        iq_perms: Optional[dict] = self._permissions[PERM_IQ]
+
+        if ((not iq_perms or first_child is None or first_child.uri is None
+             or not iq_perms.get(first_child.uri, {}).get(iq_type, False))):
+            raise NotAllowedError(
+                "privileged IQ stanza not allowed for this namespace/type combination "
+                f"{priv_iq.toXml()}"
+            )
+
+        main_iq = xmlstream.IQ(self.xmlstream, iq_type)
+        main_iq.timeout = 120
+        privileged_iq_elt = main_iq.addElement((NS_PRIV_ENT, "privileged_iq"))
+        priv_iq['xmlns'] = 'jabber:client'
+        privileged_iq_elt.addChild(priv_iq)
+        ret_elt = await main_iq.send(to.full())
+        # we unwrap the result
+        for name, ns in (
+                ("privilege", NS_PRIV_ENT),
+                ("forwarded", NS_FORWARDED),
+                ("iq", "jabber:client")
+        ):
+            try:
+                ret_elt = next(ret_elt.elements(ns, name))
+            except StopIteration:
+                raise ValueError(f"Invalid privileged IQ result: {ret_elt.toXml()}")
+        return ret_elt
+
     ## misc ##
 
     async def getAutoSubscribers(