Mercurial > libervia-pubsub
comparison 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 |
comparison
equal
deleted
inserted
replaced
477:9125a6e440c0 | 478:b544109ab4c4 |
---|---|
17 # along with this program. If not, see <http://www.gnu.org/licenses/>. | 17 # along with this program. If not, see <http://www.gnu.org/licenses/>. |
18 | 18 |
19 "This module implements XEP-0356 (Privileged Entity) to manage rosters, messages and " | 19 "This module implements XEP-0356 (Privileged Entity) to manage rosters, messages and " |
20 "presences" | 20 "presences" |
21 | 21 |
22 from typing import Dict, List, Optional, Set | 22 from typing import Dict, List, Optional, Union, Set |
23 import time | 23 import time |
24 | 24 |
25 from twisted.internet import defer | 25 from twisted.internet import defer |
26 from twisted.python import log | 26 from twisted.python import log |
27 from twisted.python import failure | |
28 from twisted.words.protocols.jabber import error, jid | 27 from twisted.words.protocols.jabber import error, jid |
28 from twisted.words.protocols.jabber import xmlstream | |
29 from twisted.words.xish import domish | 29 from twisted.words.xish import domish |
30 from wokkel import xmppim | 30 from wokkel import xmppim |
31 from wokkel import pubsub | 31 from wokkel import pubsub |
32 from wokkel import disco | 32 from wokkel import disco |
33 from wokkel.compat import IQ | 33 from wokkel.compat import IQ |
34 from wokkel.iwokkel import IPubSubService | 34 from wokkel.iwokkel import IPubSubService |
35 | 35 |
36 from .error import NotAllowedError | 36 from .error import NotAllowedError |
37 | 37 |
38 FORWARDED_NS = 'urn:xmpp:forward:0' | 38 NS_FORWARDED = 'urn:xmpp:forward:0' |
39 PRIV_ENT_NS = 'urn:xmpp:privilege:1' | 39 NS_PRIV_ENT = 'urn:xmpp:privilege:2' |
40 PRIV_ENT_ADV_XPATH = '/message/privilege[@xmlns="{}"]'.format(PRIV_ENT_NS) | 40 PRIV_ENT_ADV_XPATH = '/message/privilege[@xmlns="{}"]'.format(NS_PRIV_ENT) |
41 ROSTER_NS = 'jabber:iq:roster' | 41 ROSTER_NS = 'jabber:iq:roster' |
42 PERM_ROSTER = 'roster' | 42 PERM_ROSTER = 'roster' |
43 PERM_MESSAGE = 'message' | 43 PERM_MESSAGE = 'message' |
44 PERM_PRESENCE = 'presence' | 44 PERM_PRESENCE = 'presence' |
45 PERM_IQ = 'iq' | |
45 ALLOWED_ROSTER = ('none', 'get', 'set', 'both') | 46 ALLOWED_ROSTER = ('none', 'get', 'set', 'both') |
46 ALLOWED_MESSAGE = ('none', 'outgoing') | 47 ALLOWED_MESSAGE = ('none', 'outgoing') |
47 ALLOWED_PRESENCE = ('none', 'managed_entity', 'roster') | 48 ALLOWED_PRESENCE = ('none', 'managed_entity', 'roster') |
48 TO_CHECK = { | 49 TO_CHECK = { |
49 PERM_ROSTER:ALLOWED_ROSTER, | 50 PERM_ROSTER:ALLOWED_ROSTER, |
50 PERM_MESSAGE:ALLOWED_MESSAGE, | 51 PERM_MESSAGE:ALLOWED_MESSAGE, |
51 PERM_PRESENCE:ALLOWED_PRESENCE | 52 PERM_PRESENCE:ALLOWED_PRESENCE |
52 } | 53 } |
54 PERMS_BASE : dict[str, Optional[Union[str, Dict[str, Union[str, bool]]]]]= { | |
55 PERM_ROSTER: None, | |
56 PERM_MESSAGE: None, | |
57 PERM_PRESENCE: None, | |
58 PERM_IQ: None, | |
59 } | |
60 | |
53 | 61 |
54 # Number of seconds before a roster cache is not considered valid anymore. | 62 # Number of seconds before a roster cache is not considered valid anymore. |
55 # We keep this delay to avoid requesting roster too much in a row if an entity is | 63 # We keep this delay to avoid requesting roster too much in a row if an entity is |
56 # connecting/disconnecting often in a short time. | 64 # connecting/disconnecting often in a short time. |
57 ROSTER_TTL = 3600 | 65 ROSTER_TTL = 3600 |
68 # TODO: cache | 76 # TODO: cache |
69 | 77 |
70 def __init__(self, service_jid): | 78 def __init__(self, service_jid): |
71 super(PrivilegesHandler, self).__init__() | 79 super(PrivilegesHandler, self).__init__() |
72 self.backend = None | 80 self.backend = None |
73 self._permissions = {PERM_ROSTER: 'none', | 81 self._permissions = PERMS_BASE.copy() |
74 PERM_MESSAGE: 'none', | |
75 PERM_PRESENCE: 'none'} | |
76 self._pubsub_service = None | 82 self._pubsub_service = None |
77 self.caps_map = {} # key: bare jid, value: dict of resources with caps hash | 83 self.caps_map = {} # key: bare jid, value: dict of resources with caps hash |
78 # key: (hash,version), value: dict with DiscoInfo instance (infos) and nodes to | 84 # key: (hash,version), value: dict with DiscoInfo instance (infos) and nodes to |
79 # notify (notify) | 85 # notify (notify) |
80 self.hash_map = {} | 86 self.hash_map = {} |
118 def onAdvertise(self, message): | 124 def onAdvertise(self, message): |
119 """Managage the <message/> advertising privileges | 125 """Managage the <message/> advertising privileges |
120 | 126 |
121 self._permissions will be updated according to advertised privileged | 127 self._permissions will be updated according to advertised privileged |
122 """ | 128 """ |
123 privilege_elt = next(message.elements(PRIV_ENT_NS, 'privilege')) | 129 self._permissions = PERMS_BASE.copy() |
124 for perm_elt in privilege_elt.elements(PRIV_ENT_NS): | 130 privilege_elt = next(message.elements(NS_PRIV_ENT, 'privilege')) |
131 for perm_elt in privilege_elt.elements(NS_PRIV_ENT, 'perm'): | |
125 try: | 132 try: |
126 if perm_elt.name != 'perm': | 133 perm_access = perm_elt["access"] |
127 raise InvalidStanza('unexpected element {}'.format(perm_elt.name)) | 134 except KeyError: |
128 perm_access = perm_elt['access'] | 135 log.err(f"missing 'access' attribute in perm element: {perm_elt.toXml()}") |
129 perm_type = perm_elt['type'] | 136 continue |
137 if perm_access in (PERM_ROSTER, PERM_MESSAGE, PERM_PRESENCE): | |
130 try: | 138 try: |
139 perm_type = perm_elt["type"] | |
140 except KeyError: | |
141 log.err( | |
142 "missing 'type' attribute in perm element: " | |
143 f"{perm_elt.toXml()}" | |
144 ) | |
145 continue | |
146 else: | |
131 if perm_type not in TO_CHECK[perm_access]: | 147 if perm_type not in TO_CHECK[perm_access]: |
132 raise InvalidStanza( | 148 log.err( |
133 'bad type [{}] for permission {}' | 149 f'bad type {perm_type!r}: {perm_elt.toXml()}' |
134 .format(perm_type, perm_access) | |
135 ) | 150 ) |
136 except KeyError: | 151 continue |
137 raise InvalidStanza('bad permission [{}]'.format(perm_access)) | 152 self._permissions[perm_access] = perm_type or None |
138 except InvalidStanza as e: | 153 elif perm_access == "iq": |
139 log.msg( | 154 iq_perms = self._permissions["iq"] = {} |
140 f"Invalid stanza received ({e}), setting permission to none" | 155 for namespace_elt in perm_elt.elements(NS_PRIV_ENT, "namespace"): |
141 ) | 156 ns = namespace_elt.getAttribute("ns") |
142 for perm in self._permissions: | 157 perm_type = namespace_elt.getAttribute("type") |
143 self._permissions[perm] = 'none' | 158 if not ns or not perm_type: |
144 break | 159 log.err( |
145 | 160 f"invalid namespace element: {namespace_elt.toXml()}" |
146 self._permissions[perm_access] = perm_type or 'none' | 161 ) |
162 else: | |
163 if perm_type not in ("get", "set", "both"): | |
164 log.err( | |
165 f"invalid namespace type: {namespace_elt.toXml()}" | |
166 ) | |
167 else: | |
168 ns_perms = iq_perms[ns] = {"type": perm_type} | |
169 ns_perms["get"] = perm_type in ("get", "both") | |
170 ns_perms["set"] = perm_type in ("set", "both") | |
171 else: | |
172 log.err(f"unknown {perm_access!r} access: {perm_elt.toXml()}'") | |
173 | |
174 perms = self._permissions | |
175 perms_iq = perms["iq"] | |
176 if perms_iq is None: | |
177 iq_perm_txt = " no iq perm advertised" | |
178 elif not isinstance(perms_iq, dict): | |
179 raise ValueError('INTERNAL ERROR: "iq" perm should a dict') | |
180 else: | |
181 iq_perm_txt = "\n".join( | |
182 f" - {ns}: {perms['type']}" | |
183 for ns, perms in perms_iq.items() | |
184 ) | |
147 | 185 |
148 log.msg( | 186 log.msg( |
149 'Privileges updated: roster={roster}, message={message}, presence={presence}' | 187 "Privileges updated:\n" |
150 .format(**self._permissions) | 188 f"roster: {perms[PERM_ROSTER]}\n" |
189 f"message: {perms[PERM_MESSAGE]}\n" | |
190 f"presence: {perms[PERM_PRESENCE]}\n" | |
191 f"iq:\n{iq_perm_txt}" | |
151 ) | 192 ) |
152 | 193 |
153 ## roster ## | 194 ## roster ## |
154 | 195 |
155 def updatePresenceMap( | 196 def updatePresenceMap( |
306 | 347 |
307 main_message = domish.Element((None, "message")) | 348 main_message = domish.Element((None, "message")) |
308 if to_jid is None: | 349 if to_jid is None: |
309 to_jid = self.backend.server_jid | 350 to_jid = self.backend.server_jid |
310 main_message['to'] = to_jid.full() | 351 main_message['to'] = to_jid.full() |
311 privilege_elt = main_message.addElement((PRIV_ENT_NS, 'privilege')) | 352 privilege_elt = main_message.addElement((NS_PRIV_ENT, 'privilege')) |
312 forwarded_elt = privilege_elt.addElement((FORWARDED_NS, 'forwarded')) | 353 forwarded_elt = privilege_elt.addElement((NS_FORWARDED, 'forwarded')) |
313 priv_message['xmlns'] = 'jabber:client' | 354 priv_message['xmlns'] = 'jabber:client' |
314 forwarded_elt.addChild(priv_message) | 355 forwarded_elt.addChild(priv_message) |
315 self.send(main_message) | 356 self.send(main_message) |
316 | 357 |
317 def notifyPublish(self, pep_jid, nodeIdentifier, notifications): | 358 def notifyPublish(self, pep_jid, nodeIdentifier, notifications): |
450 # we send message with last item, as required by | 491 # we send message with last item, as required by |
451 # https://xmpp.org/extensions/xep-0163.html#notify-last | 492 # https://xmpp.org/extensions/xep-0163.html#notify-last |
452 for pep_jid, node, item, item_access_model in last_items: | 493 for pep_jid, node, item, item_access_model in last_items: |
453 self.notifyPublish(pep_jid, node, [(from_jid, None, [item])]) | 494 self.notifyPublish(pep_jid, node, [(from_jid, None, [item])]) |
454 | 495 |
496 ## IQ ## | |
497 | |
498 async def sendIQ( | |
499 self, | |
500 priv_iq: domish.Element, | |
501 to: Optional[jid.JID] = None | |
502 ) -> domish.Element: | |
503 """Send privileged IQ stanza | |
504 | |
505 @param priv_iq: privileged IQ stanza | |
506 @param to: bare jid of user on behalf of who the stanza is sent | |
507 The stanza will be wrapped and sent to the server. Result/Error stanza will sent | |
508 back as return value. | |
509 """ | |
510 if to is None: | |
511 try: | |
512 to = jid.JID(priv_iq["from"]) | |
513 except (KeyError, RuntimeError): | |
514 raise ValueError( | |
515 'no "to" specified, and invalid "to" attribute in priv_iq' | |
516 ) | |
517 if not to.user or to.resource or to.host != self.backend.server_jid.userhost(): | |
518 raise NotAllowedError( | |
519 f'"to" attribute must be set to a bare jid of the server, {to} is invalid' | |
520 ) | |
521 iq_type = priv_iq.getAttribute("type") | |
522 if iq_type not in ("get", "set"): | |
523 raise ValueError(f"invalid IQ type: {priv_iq.toXml()}") | |
524 first_child = priv_iq.firstChildElement() | |
525 iq_perms: Optional[dict] = self._permissions[PERM_IQ] | |
526 | |
527 if ((not iq_perms or first_child is None or first_child.uri is None | |
528 or not iq_perms.get(first_child.uri, {}).get(iq_type, False))): | |
529 raise NotAllowedError( | |
530 "privileged IQ stanza not allowed for this namespace/type combination " | |
531 f"{priv_iq.toXml()}" | |
532 ) | |
533 | |
534 main_iq = xmlstream.IQ(self.xmlstream, iq_type) | |
535 main_iq.timeout = 120 | |
536 privileged_iq_elt = main_iq.addElement((NS_PRIV_ENT, "privileged_iq")) | |
537 priv_iq['xmlns'] = 'jabber:client' | |
538 privileged_iq_elt.addChild(priv_iq) | |
539 ret_elt = await main_iq.send(to.full()) | |
540 # we unwrap the result | |
541 for name, ns in ( | |
542 ("privilege", NS_PRIV_ENT), | |
543 ("forwarded", NS_FORWARDED), | |
544 ("iq", "jabber:client") | |
545 ): | |
546 try: | |
547 ret_elt = next(ret_elt.elements(ns, name)) | |
548 except StopIteration: | |
549 raise ValueError(f"Invalid privileged IQ result: {ret_elt.toXml()}") | |
550 return ret_elt | |
551 | |
455 ## misc ## | 552 ## misc ## |
456 | 553 |
457 async def getAutoSubscribers( | 554 async def getAutoSubscribers( |
458 self, | 555 self, |
459 recipient: jid.JID, | 556 recipient: jid.JID, |