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,