Mercurial > libervia-pubsub
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(