Mercurial > prosody-modules
changeset 4913:3ddab718f717
mod_privilege: update to v0.4:
- now the namespace "urn:xmpp:privilege:2" is exclusively used
- IQ permission implementation
- README update
roster pushes are not implemented yet
author | Goffi <goffi@goffi.org> |
---|---|
date | Wed, 11 May 2022 12:43:26 +0200 (2022-05-11) |
parents | b45c23ce24ba |
children | bc8832c6696b |
files | mod_privilege/README.markdown mod_privilege/mod_privilege.lua |
diffstat | 2 files changed, 601 insertions(+), 343 deletions(-) [+] |
line wrap: on
line diff
--- a/mod_privilege/README.markdown Wed Mar 30 17:52:41 2022 +0200 +++ b/mod_privilege/README.markdown Wed May 11 12:43:26 2022 +0200 @@ -1,6 +1,6 @@ --- labels: -- 'Stage-Alpha' +- 'Stage-Beta' summary: 'XEP-0356 (Privileged Entity) implementation' ... @@ -8,9 +8,9 @@ ============ Privileged Entity is an extension which allows entity/component to have -privileged access to server (set/get roster, send message on behalf of -server, access presence informations). It can be used to build services -independently of server (e.g.: PEP service). +privileged access to server (set/get roster, send message on behalf of server, +send IQ stanza on behalf of user, access presence information). It can be used +to build services independently of server (e.g.: PEP service). Details ======= @@ -18,6 +18,12 @@ You can have all the details by reading the [XEP-0356](http://xmpp.org/extensions/xep-0356.html). +Only the latest version of the XEP is implemented (using namespace +`urn:xmpp:privilege:2`), if your component use an older version, please update. + +Note that roster permission is not fully implemented yet, roster pushes are not yet sent +to privileged entity. + Usage ===== @@ -33,7 +39,7 @@ [...] - Component "youcomponent.yourdomain.tld" + Component "pubsub.yourdomain.tld" component_secret = "yourpassword" modules_enabled = {"privilege"} @@ -51,22 +57,38 @@ message = "outgoing"; presence = "roster"; }, + ["pubsub.yourdomain.tld"] = { + roster = "get"; + message = "outgoing"; + presence = "roster"; + iq = { + ["http://jabber.org/protocol/pubsub"] = "set"; + }; + }, } -Here *romeo@montaigu.lit* can **get** roster of anybody on the host, and -will **have presence for any user** of the host, while -*juliet@capulet.lit* can **get** and **set** a roster, **send messages** -on the behalf of the server, and **access presence of anybody linked to -the host** (not only people on the server, but also people in rosters of -users of the server). +Here *romeo@montaigu.lit* can **get** roster of anybody on the host, and will +**have presence for any user** of the host, while *juliet@capulet.lit* can +**get** and **set** a roster, **send messages** on behalf of the server, and +**access presence of anybody linked to the host** (not only people on the +server, but also people in rosters of users of the server). -**/! Be extra careful when you give a permission to an entity/component, -it's a powerful access, only do it if you absoly trust the -component/entity, and you know where the software is coming from** +*pubsub.yourdomain.tld* is a Pubsub/PEP component which can **get** roster of +anybody on the host, **send messages** on the behalf of the server, **access +presence of anybody linked to the host**, and **send IQ stanza of type "set" for +the namespace "http://jabber.org/protocol/pubsub"** (this can be used to +implement XEP-0376 "Pubsub Account Management"). + +**/!\\ Be extra careful when you give a permission to an entity/component, it's +a powerful access, only do it if you absolutely trust the component/entity, and +you know where the software is coming from** Configuration ============= +roster +------ + All the permissions give access to all accounts of the virtual host. -------- ------------------------------------------------ ---------------------- @@ -76,6 +98,9 @@ both Allow **read** and **write** access to rosters -------- ------------------------------------------------ ---------------------- +Note that roster implementation is incomplete at the moment, roster pushes are not yet +send to privileged entity. + message ------- @@ -93,6 +118,22 @@ roster Receive all presence stanzas (except subsciptions) from host users and people in their rosters ------------------ ------------------------------------------------------------------------------------------------ +iq +-- + +IQ permission is a table mapping allowed namespaces to allowed stanza type. When +a namespace is specified, IQ stanza of the specified type (see below) can be +sent if and only if the first child element of the IQ stanza has the specified +namespace. See https://xmpp.org/extensions/xep-0356.html#iq for details. + +Allowed stanza type: + + -------- ------------------------------------------- + get Allow IQ stanza of type **get** + set Allow IQ stanza of type **set** + both Allow IQ stanza of type **get** and **set** + -------- ------------------------------------------- + Compatibility ============= @@ -118,13 +159,15 @@ `patch -p1 < /tmp/component.patch` - ----- ---------------------------------------------------- + ----- -------------------------------------------------- + trunk Works + 0.12 Works + 0.11 Works 0.10 Works 0.9 Need a patched core/mod\_component.lua (see above) - ----- ---------------------------------------------------- + ----- -------------------------------------------------- Note ==== -This module is often used with mod\_delegation (c.f. XEP for more -details) +This module is often used with mod\_delegation (c.f. XEP for more details)
--- a/mod_privilege/mod_privilege.lua Wed Mar 30 17:52:41 2022 +0200 +++ b/mod_privilege/mod_privilege.lua Wed May 11 12:43:26 2022 +0200 @@ -1,5 +1,5 @@ -- XEP-0356 (Privileged Entity) --- Copyright (C) 2015-2016 Jérôme Poisson +-- Copyright (C) 2015-2022 Jérôme Poisson -- -- This module is MIT/X11 licensed. Please see the -- COPYING file in the source package for more information. @@ -12,17 +12,17 @@ local set = require("util.set") local st = require("util.stanza") local roster_manager = require("core.rostermanager") -local usermanager_user_exists = require "core.usermanager".user_exists; +local usermanager_user_exists = require "core.usermanager".user_exists local hosts = prosody.hosts -local full_sessions = prosody.full_sessions; +local full_sessions = prosody.full_sessions local priv_session = module:shared("/*/privilege/session") if priv_session.connected_cb == nil then - -- set used to have connected event listeners - -- which allows a host to react on events from - -- other hosts - priv_session.connected_cb = set.new() + -- set used to have connected event listeners + -- which allows a host to react on events from + -- other hosts + priv_session.connected_cb = set.new() end local connected_cb = priv_session.connected_cb @@ -38,17 +38,17 @@ local _ALLOWED_PRESENCE = set.new({'none', 'managed_entity', 'roster'}) local _PRESENCE_MANAGED = set.new({'managed_entity', 'roster'}) local _TO_CHECK = {roster=_ALLOWED_ROSTER, message=_ALLOWED_MESSAGE, presence=_ALLOWED_PRESENCE} -local _PRIV_ENT_NS = 'urn:xmpp:privilege:1' +local _PRIV_ENT_NS = 'urn:xmpp:privilege:2' local _FORWARDED_NS = 'urn:xmpp:forward:0' local _MODULE_HOST = module:get_host() -module:log("debug", "Loading privileged entity module "); +module:log("debug", "Loading privileged entity module ") --> Permissions management <-- -local privileges = module:get_option("privileged_entities", {}) +local config_priv = module:get_option("privileged_entities", {}) local function get_session_privileges(session, host) if not session.privileges then return nil end @@ -57,134 +57,170 @@ local function advertise_perm(session, to_jid, perms) - -- send <message/> stanza to advertise permissions - -- as expained in § 4.2 - local message = st.message({from=module.host, to=to_jid}) - :tag("privilege", {xmlns=_PRIV_ENT_NS}) + -- send <message/> stanza to advertise permissions + -- as expained in § 4.2 + local message = st.message({from=module.host, to=to_jid}) + :tag("privilege", {xmlns=_PRIV_ENT_NS}) - for _, perm in pairs({'roster', 'message', 'presence'}) do - if perms[perm] then - message:tag("perm", {access=perm, type=perms[perm]}):up() - end - end - session.send(message) + for _, perm in pairs({'roster', 'message', 'presence'}) do + if perms[perm] then + message:tag("perm", {access=perm, type=perms[perm]}):up() + end + end + local iq_perm = perms["iq"] + if iq_perm ~= nil then + message:tag("perm", {access="iq"}) + for namespace, ns_perm in pairs(iq_perm) do + local perm_type + if ns_perm.set and ns_perm.get then + perm_type = "both" + elseif ns_perm.set then + perm_type = "set" + elseif ns_perm.get then + perm_type = "get" + else + perm_type = nil + end + message:tag("namespace", {ns=namespace, type=perm_type}) + end + end + session.send(message) end local function set_presence_perm_set(to_jid, perms) - -- fill the presence sets according to perms - if _PRESENCE_MANAGED:contains(perms.presence) then - presence_man_ent:add(to_jid) - end - if perms.presence == 'roster' then - presence_roster:add(to_jid) - end + -- fill the presence sets according to perms + if _PRESENCE_MANAGED:contains(perms.presence) then + presence_man_ent:add(to_jid) + end + if perms.presence == 'roster' then + presence_roster:add(to_jid) + end end local function advertise_presences(session, to_jid, perms) - -- send presence status for already conencted entities - -- as explained in § 7.1 - -- people in roster are probed only for active sessions - -- TODO: manage roster load for inactive sessions - if not perms.presence then return; end - local to_probe = {} - for _, user_session in pairs(full_sessions) do - if user_session.presence and _PRESENCE_MANAGED:contains(perms.presence) then - local presence = st.clone(user_session.presence) - presence.attr.to = to_jid - module:log("debug", "sending current presence for "..tostring(user_session.full_jid)) - session.send(presence) - end - if perms.presence == "roster" then - -- we reset the cache to avoid to miss a presence that just changed - priv_session.last_presence = nil + -- send presence status for already connected entities + -- as explained in § 7.1 + -- people in roster are probed only for active sessions + -- TODO: manage roster load for inactive sessions + if not perms.presence then return; end + local to_probe = {} + for _, user_session in pairs(full_sessions) do + if user_session.presence and _PRESENCE_MANAGED:contains(perms.presence) then + local presence = st.clone(user_session.presence) + presence.attr.to = to_jid + module:log("debug", "sending current presence for "..tostring(user_session.full_jid)) + session.send(presence) + end + if perms.presence == "roster" then + -- we reset the cache to avoid to miss a presence that just changed + priv_session.last_presence = nil - if user_session.roster then - local bare_jid = jid.bare(user_session.full_jid) - for entity, item in pairs(user_session.roster) do - if entity~=false and entity~="pending" and (item.subscription=="both" or item.subscription=="to") then - local _, host = jid.split(entity) - if not hosts[host] then -- we don't probe jid from hosts we manage - -- using a table with entity as key avoid probing several time the same one - to_probe[entity] = bare_jid - end - end - end - end - end - end + if user_session.roster then + local bare_jid = jid.bare(user_session.full_jid) + for entity, item in pairs(user_session.roster) do + if entity~=false and entity~="pending" and (item.subscription=="both" or item.subscription=="to") then + local _, host = jid.split(entity) + if not hosts[host] then -- we don't probe jid from hosts we manage + -- using a table with entity as key avoid probing several time the same one + to_probe[entity] = bare_jid + end + end + end + end + end + end - -- now we probe peoples for "roster" presence permission - for probe_to, probe_from in pairs(to_probe) do - module:log("debug", "probing presence for %s (on behalf of %s)", tostring(probe_to), tostring(probe_from)) - local probe = st.presence({from=probe_from, to=probe_to, type="probe"}) - prosody.core_route_stanza(nil, probe) - end + -- now we probe peoples for "roster" presence permission + for probe_to, probe_from in pairs(to_probe) do + module:log("debug", "probing presence for %s (on behalf of %s)", tostring(probe_to), tostring(probe_from)) + local probe = st.presence({from=probe_from, to=probe_to, type="probe"}) + prosody.core_route_stanza(nil, probe) + end end + local function on_auth(event) - -- Check if entity is privileged according to configuration, - -- and set session.privileges accordingly + -- Check if entity is privileged according to configuration, + -- and set session.privileges accordingly - local session = event.session - local bare_jid = jid.join(session.username, session.host) + local session = event.session + local bare_jid = jid.join(session.username, session.host) if not session.privileges then session.privileges = {} end - local ent_priv = privileges[bare_jid] - if ent_priv ~= nil then - module:log("debug", "Entity is privileged") - for perm_type, allowed_values in pairs(_TO_CHECK) do - local value = ent_priv[perm_type] - if value ~= nil then - if not allowed_values:contains(value) then - module:log('warn', 'Invalid value for '..perm_type..' privilege: ['..value..']') - module:log('warn', 'Setting '..perm_type..' privilege to none') - ent_priv[perm_type] = nil - end - if value == 'none' then - ent_priv[perm_type] = nil - end - end - end - -- extra checks for presence permission - if ent_priv.presence == 'roster' and not _ROSTER_GET_PERM:contains(ent_priv.roster) then - module:log("warn", "Can't allow roster presence privilege without roster \"get\" privilege") - module:log("warn", "Setting presence permission to none") - ent_priv.presence = nil - end + local conf_ent_priv = config_priv[bare_jid] + local ent_priv = {} + if conf_ent_priv ~= nil then + module:log("debug", "Entity is privileged") + for perm_type, allowed_values in pairs(_TO_CHECK) do + local value = conf_ent_priv[perm_type] + if value ~= nil then + if not allowed_values:contains(value) then + module:log('warn', 'Invalid value for '..perm_type..' privilege: ['..value..']') + module:log('warn', 'Setting '..perm_type..' privilege to none') + ent_priv[perm_type] = nil + elseif value == 'none' then + ent_priv[perm_type] = nil + else + ent_priv[perm_type] = value + end + else + ent_priv[perm_type] = nil + end + end + -- extra checks for presence permission + if ent_priv.presence == 'roster' and not _ROSTER_GET_PERM:contains(ent_priv.roster) then + module:log("warn", "Can't allow roster presence privilege without roster \"get\" privilege") + module:log("warn", "Setting presence permission to none") + ent_priv.presence = nil + end + -- iq permission + local iq_perm_config = conf_ent_priv["iq"] + if iq_perm_config ~= nil then + local iq_perm = {} + ent_priv["iq"] = iq_perm + for ns, ns_perm_config in pairs(iq_perm_config) do + iq_perm[ns] = { + ["get"] = ns_perm_config == "get" or ns_perm_config == "both", + ["set"] = ns_perm_config == "set" or ns_perm_config == "both" + } + end + else + ent_priv["iq"] = nil + end - if session.type == "component" then - -- we send the message stanza only for component - -- it will be sent at first <presence/> for other entities - advertise_perm(session, bare_jid, ent_priv) - set_presence_perm_set(bare_jid, ent_priv) - advertise_presences(session, bare_jid, ent_priv) - end - end + if session.type == "component" then + -- we send the message stanza only for component + -- it will be sent at first <presence/> for other entities + advertise_perm(session, bare_jid, ent_priv) + set_presence_perm_set(bare_jid, ent_priv) + advertise_presences(session, bare_jid, ent_priv) + end + end - session.privileges[_MODULE_HOST] = ent_priv + session.privileges[_MODULE_HOST] = ent_priv end local function on_presence(event) - -- Permission are already checked at this point, - -- we only advertise them to the entity - local session = event.origin + -- Permission are already checked at this point, + -- we only advertise them to the entity + local session = event.origin local session_privileges = get_session_privileges(session, _MODULE_HOST) - if session_privileges then - advertise_perm(session, session.full_jid, session_privileges) - set_presence_perm_set(session.full_jid, session_privileges) - advertise_presences(session, session.full_jid, session_privileges) - end + if session_privileges then + advertise_perm(session, session.full_jid, session_privileges) + set_presence_perm_set(session.full_jid, session_privileges) + advertise_presences(session, session.full_jid, session_privileges) + end end local function on_component_auth(event) - -- react to component-authenticated event from this host - -- and call the on_auth methods from all other hosts - -- needed for the component to get delegations advertising - for callback in connected_cb:items() do - callback(event) - end + -- react to component-authenticated event from this host + -- and call the on_auth methods from all other hosts + -- needed for the component to get delegations advertising + for callback in connected_cb:items() do + callback(event) + end end if module:get_host_type() ~= "component" then @@ -199,267 +235,446 @@ -- get module:hook("iq-get/bare/jabber:iq:roster:query", function(event) - local session, stanza = event.origin, event.stanza; - if not stanza.attr.to then - -- we don't want stanzas addressed to /self - return; - end - local node, host = jid.split(stanza.attr.to); + local session, stanza = event.origin, event.stanza + if not stanza.attr.to then + -- we don't want stanzas addressed to /self + return + end + local node, host = jid.split(stanza.attr.to) local session_privileges = get_session_privileges(session, host) - if session_privileges and _ROSTER_GET_PERM:contains(session_privileges.roster) then - module:log("debug", "Roster get from allowed privileged entity received") - -- following code is adapted from mod_remote_roster - local roster = roster_manager.load_roster(node, host); + if session_privileges and _ROSTER_GET_PERM:contains(session_privileges.roster) then + module:log("debug", "Roster get from allowed privileged entity received") + -- following code is adapted from mod_remote_roster + local roster = roster_manager.load_roster(node, host) - local reply = st.reply(stanza):query("jabber:iq:roster"); - for entity_jid, item in pairs(roster) do - if entity_jid and entity_jid ~= "pending" then - reply:tag("item", { - jid = entity_jid, - subscription = item.subscription, - ask = item.ask, - name = item.name, - }); - for group in pairs(item.groups) do - reply:tag("group"):text(group):up(); - end - reply:up(); -- move out from item - end - end - -- end of code adapted from mod_remote_roster - session.send(reply); - else - module:log("warn", "Entity "..tostring(session.full_jid).." try to get roster without permission") - session.send(st.error_reply(stanza, 'auth', 'forbidden')) - end + local reply = st.reply(stanza):query("jabber:iq:roster") + for entity_jid, item in pairs(roster) do + if entity_jid and entity_jid ~= "pending" then + reply:tag("item", { + jid = entity_jid, + subscription = item.subscription, + ask = item.ask, + name = item.name, + }) + for group in pairs(item.groups) do + reply:tag("group"):text(group):up() + end + reply:up(); -- move out from item + end + end + -- end of code adapted from mod_remote_roster + session.send(reply) + else + module:log("warn", "Entity "..tostring(session.full_jid).." try to get roster without permission") + session.send(st.error_reply(stanza, 'auth', 'forbidden')) + end - return true -end); + return true +end) -- set module:hook("iq-set/bare/jabber:iq:roster:query", function(event) - local session, stanza = event.origin, event.stanza; - if not stanza.attr.to then - -- we don't want stanzas addressed to /self - return; - end - local from_node, from_host = jid.split(stanza.attr.to); + local session, stanza = event.origin, event.stanza + if not stanza.attr.to then + -- we don't want stanzas addressed to /self + return + end + local from_node, from_host = jid.split(stanza.attr.to) local session_privileges = get_session_privileges(session, from_host) - if session_privileges and _ROSTER_SET_PERM:contains(session_privileges.roster) then - module:log("debug", "Roster set from allowed privileged entity received") - -- following code is adapted from mod_remote_roster - if not(usermanager_user_exists(from_node, from_host)) then return; end - local roster = roster_manager.load_roster(from_node, from_host); - if not(roster) then return; end + if session_privileges and _ROSTER_SET_PERM:contains(session_privileges.roster) then + module:log("debug", "Roster set from allowed privileged entity received") + -- following code is adapted from mod_remote_roster + if not(usermanager_user_exists(from_node, from_host)) then return; end + local roster = roster_manager.load_roster(from_node, from_host) + if not(roster) then return; end - local query = stanza.tags[1]; - for _, item in ipairs(query.tags) do - if item.name == "item" - and item.attr.xmlns == "jabber:iq:roster" and item.attr.jid - -- Protection against overwriting roster.pending, until we move it - and item.attr.jid ~= "pending" then + local query = stanza.tags[1] + for _, item in ipairs(query.tags) do + if item.name == "item" + and item.attr.xmlns == "jabber:iq:roster" and item.attr.jid + -- Protection against overwriting roster.pending, until we move it + and item.attr.jid ~= "pending" then - local item_jid = jid.prep(item.attr.jid); - local _, host, resource = jid.split(item_jid); - if not resource then - if item_jid ~= stanza.attr.to then -- not self-item_jid - if item.attr.subscription == "remove" then - local r_item = roster[item_jid]; - if r_item then - roster[item_jid] = nil; - if roster_manager.save_roster(from_node, from_host, roster) then - session.send(st.reply(stanza)); - roster_manager.roster_push(from_node, from_host, item_jid); - else - roster[item_jid] = item; - session.send(st.error_reply(stanza, "wait", "internal-server-error", "Unable to save roster")); - end - else - session.send(st.error_reply(stanza, "modify", "item-not-found")); - end - else - local subscription = item.attr.subscription; - if subscription ~= "both" and subscription ~= "to" and subscription ~= "from" and subscription ~= "none" then -- TODO error on invalid - subscription = roster[item_jid] and roster[item_jid].subscription or "none"; - end - local r_item = {name = item.attr.name, groups = {}}; - if r_item.name == "" then r_item.name = nil; end - r_item.subscription = subscription; - if subscription ~= "both" and subscription ~= "to" then - r_item.ask = roster[item_jid] and roster[item_jid].ask; - end - for _, child in ipairs(item) do - if child.name == "group" then - local text = table.concat(child); - if text and text ~= "" then - r_item.groups[text] = true; - end - end - end - local olditem = roster[item_jid]; - roster[item_jid] = r_item; - if roster_manager.save_roster(from_node, from_host, roster) then -- Ok, send success - session.send(st.reply(stanza)); - -- and push change to all resources - roster_manager.roster_push(from_node, from_host, item_jid); - else -- Adding to roster failed - roster[item_jid] = olditem; - session.send(st.error_reply(stanza, "wait", "internal-server-error", "Unable to save roster")); - end - end - else -- Trying to add self to roster - session.send(st.error_reply(stanza, "cancel", "not-allowed")); - end - else -- Invalid JID added to roster - module:log("warn", "resource: %s , host: %s", tostring(resource), tostring(host)) - session.send(st.error_reply(stanza, "modify", "bad-request")); -- FIXME what's the correct error? - end - else -- Roster set didn't include a single item, or its name wasn't 'item' - session.send(st.error_reply(stanza, "modify", "bad-request")); - end - end -- for loop end - -- end of code adapted from mod_remote_roster - else -- The permission is not granted - module:log("warn", "Entity "..tostring(session.full_jid).." try to set roster without permission") - session.send(st.error_reply(stanza, 'auth', 'forbidden')) - end + local item_jid = jid.prep(item.attr.jid) + local _, host, resource = jid.split(item_jid) + if not resource then + if item_jid ~= stanza.attr.to then -- not self-item_jid + if item.attr.subscription == "remove" then + local r_item = roster[item_jid] + if r_item then + roster[item_jid] = nil + if roster_manager.save_roster(from_node, from_host, roster) then + session.send(st.reply(stanza)) + roster_manager.roster_push(from_node, from_host, item_jid) + else + roster[item_jid] = item + session.send(st.error_reply(stanza, "wait", "internal-server-error", "Unable to save roster")) + end + else + session.send(st.error_reply(stanza, "modify", "item-not-found")) + end + else + local subscription = item.attr.subscription + if subscription ~= "both" and subscription ~= "to" and subscription ~= "from" and subscription ~= "none" then -- TODO error on invalid + subscription = roster[item_jid] and roster[item_jid].subscription or "none" + end + local r_item = {name = item.attr.name, groups = {}} + if r_item.name == "" then r_item.name = nil; end + r_item.subscription = subscription + if subscription ~= "both" and subscription ~= "to" then + r_item.ask = roster[item_jid] and roster[item_jid].ask + end + for _, child in ipairs(item) do + if child.name == "group" then + local text = table.concat(child) + if text and text ~= "" then + r_item.groups[text] = true + end + end + end + local olditem = roster[item_jid] + roster[item_jid] = r_item + if roster_manager.save_roster(from_node, from_host, roster) then -- Ok, send success + session.send(st.reply(stanza)) + -- and push change to all resources + roster_manager.roster_push(from_node, from_host, item_jid) + else -- Adding to roster failed + roster[item_jid] = olditem + session.send(st.error_reply(stanza, "wait", "internal-server-error", "Unable to save roster")) + end + end + else -- Trying to add self to roster + session.send(st.error_reply(stanza, "cancel", "not-allowed")) + end + else -- Invalid JID added to roster + module:log("warn", "resource: %s , host: %s", tostring(resource), tostring(host)) + session.send(st.error_reply(stanza, "modify", "bad-request")); -- FIXME what's the correct error? + end + else -- Roster set didn't include a single item, or its name wasn't 'item' + session.send(st.error_reply(stanza, "modify", "bad-request")) + end + end -- for loop end + -- end of code adapted from mod_remote_roster + else -- The permission is not granted + module:log("warn", "Entity "..tostring(session.full_jid).." try to set roster without permission") + session.send(st.error_reply(stanza, 'auth', 'forbidden')) + end - return true -end); + return true +end) --> message permission <-- local function clean_xmlns(node) - -- Recursively remove "jabber:client" attribute from node. - -- In Prosody internal routing, xmlns should not be set. - -- Keeping xmlns would lead to issues like mod_smacks ignoring the outgoing stanza, - -- so we remove all xmlns attributes with a value of "jabber:client" - if node.attr.xmlns == 'jabber:client' then - for childnode in node:childtags() do - clean_xmlns(childnode); - end - node.attr.xmlns = nil; - end + -- Recursively remove "jabber:client" attribute from node. + -- In Prosody internal routing, xmlns should not be set. + -- Keeping xmlns would lead to issues like mod_smacks ignoring the outgoing stanza, + -- so we remove all xmlns attributes with a value of "jabber:client" + if node.attr.xmlns == 'jabber:client' then + for childnode in node:childtags() do + clean_xmlns(childnode) + end + node.attr.xmlns = nil + end end module:hook("message/host", function(event) - local session, stanza = event.origin, event.stanza; - local privilege_elt = stanza:get_child('privilege', _PRIV_ENT_NS) - if privilege_elt==nil then return; end + local session, stanza = event.origin, event.stanza + local privilege_elt = stanza:get_child('privilege', _PRIV_ENT_NS) + if privilege_elt==nil then return; end local _, to_host = jid.split(stanza.attr.to) local session_privileges = get_session_privileges(session, to_host) - if session_privileges and session_privileges.message=="outgoing" then - if #privilege_elt.tags==1 and privilege_elt.tags[1].name == "forwarded" - and privilege_elt.tags[1].attr.xmlns==_FORWARDED_NS then - local message_elt = privilege_elt.tags[1]:get_child('message', 'jabber:client') - if message_elt ~= nil then - local _, from_host, from_resource = jid.split(message_elt.attr.from) - if from_resource == nil and hosts[from_host] then -- we only accept bare jids from one of the server hosts - clean_xmlns(message_elt); -- needed do to proper routing - -- at this point everything should be alright, we can send the message - prosody.core_route_stanza(nil, message_elt) - else -- trying to send a message from a forbidden entity - module:log("warn", "Entity "..tostring(session.full_jid).." try to send a message from "..tostring(message_elt.attr.from)) - session.send(st.error_reply(stanza, 'auth', 'forbidden')) - end - else -- incorrect message child - session.send(st.error_reply(stanza, "modify", "bad-request", "invalid forwarded <message/> element")); - end - else -- incorrect forwarded child - session.send(st.error_reply(stanza, "modify", "bad-request", "invalid <forwarded/> element")); - end; - else -- The permission is not granted - module:log("warn", "Entity "..tostring(session.full_jid).." try to send message without permission") - session.send(st.error_reply(stanza, 'auth', 'forbidden')) - end + if session_privileges and session_privileges.message=="outgoing" then + if #privilege_elt.tags==1 and privilege_elt.tags[1].name == "forwarded" + and privilege_elt.tags[1].attr.xmlns==_FORWARDED_NS then + local message_elt = privilege_elt.tags[1]:get_child('message', 'jabber:client') + if message_elt ~= nil then + local _, from_host, from_resource = jid.split(message_elt.attr.from) + if from_resource == nil and hosts[from_host] then -- we only accept bare jids from one of the server hosts + clean_xmlns(message_elt); -- needed do to proper routing + -- at this point everything should be alright, we can send the message + prosody.core_route_stanza(nil, message_elt) + else -- trying to send a message from a forbidden entity + module:log("warn", "Entity "..tostring(session.full_jid).." try to send a message from "..tostring(message_elt.attr.from)) + session.send(st.error_reply(stanza, 'auth', 'forbidden')) + end + else -- incorrect message child + session.send(st.error_reply(stanza, "modify", "bad-request", "invalid forwarded <message/> element")) + end + else -- incorrect forwarded child + session.send(st.error_reply(stanza, "modify", "bad-request", "invalid <forwarded/> element")) + end + else -- The permission is not granted + module:log("warn", "Entity "..tostring(session.full_jid).." try to send message without permission") + session.send(st.error_reply(stanza, 'auth', 'forbidden')) + end - return true -end); + return true +end) --> presence permission <-- local function same_tags(tag1, tag2) - -- check if two tags are equivalent + -- check if two tags are equivalent if tag1.name ~= tag2.name then return false; end - if #tag1 ~= #tag2 then return false; end + if #tag1 ~= #tag2 then return false; end - for name, value in pairs(tag1.attr) do - if tag2.attr[name] ~= value then return false; end - end + for name, value in pairs(tag1.attr) do + if tag2.attr[name] ~= value then return false; end + end - for i=1,#tag1 do - if type(tag1[i]) == "string" then - if tag1[i] ~= tag2[i] then return false; end - else - if not same_tags(tag1[i], tag2[i]) then return false; end - end - end + for i=1,#tag1 do + if type(tag1[i]) == "string" then + if tag1[i] ~= tag2[i] then return false; end + else + if not same_tags(tag1[i], tag2[i]) then return false; end + end + end - return true + return true end local function same_presences(presence1, presence2) - -- check that 2 <presence/> stanzas are equivalent (except for "to" attribute) - -- /!\ if the id change but everything else is equivalent, this method return false - -- this behaviour may change in the future - if presence1.attr.from ~= presence2.attr.from or presence1.attr.id ~= presence2.attr.id - or presence1.attr.type ~= presence2.attr.type then - return false - end + -- check that 2 <presence/> stanzas are equivalent (except for "to" attribute) + -- /!\ if the id change but everything else is equivalent, this method return false + -- this behaviour may change in the future + if presence1.attr.from ~= presence2.attr.from or presence1.attr.id ~= presence2.attr.id + or presence1.attr.type ~= presence2.attr.type then + return false + end - if presence1.attr.id and presence1.attr.id == presence2.attr.id then return true; end + if presence1.attr.id and presence1.attr.id == presence2.attr.id then return true; end - if #presence1 ~= #presence2 then return false; end + if #presence1 ~= #presence2 then return false; end - for i=1,#presence1 do - if type(presence1[i]) == "string" then - if presence1[i] ~= presence2[i] then return false; end - else - if not same_tags(presence1[i], presence2[i]) then return false; end - end - end + for i=1,#presence1 do + if type(presence1[i]) == "string" then + if presence1[i] ~= presence2[i] then return false; end + else + if not same_tags(presence1[i], presence2[i]) then return false; end + end + end - return true + return true end local function forward_presence(presence, to_jid) - local presence_fwd = st.clone(presence) - presence_fwd.attr.to = to_jid - module:log("debug", "presence forwarded to "..to_jid..": "..tostring(presence_fwd)) - module:send(presence_fwd) - -- cache used to avoid to send several times the same stanza - priv_session.last_presence = presence + local presence_fwd = st.clone(presence) + presence_fwd.attr.to = to_jid + module:log("debug", "presence forwarded to "..to_jid..": "..tostring(presence_fwd)) + module:send(presence_fwd) + -- cache used to avoid to send several times the same stanza + priv_session.last_presence = presence end module:hook("presence/bare", function(event) - if presence_man_ent:empty() and presence_roster:empty() then return; end + if presence_man_ent:empty() and presence_roster:empty() then return; end + + local stanza = event.stanza + if stanza.attr.type == nil or stanza.attr.type == "unavailable" then + if not stanza.attr.to then + for entity in presence_man_ent:items() do + if stanza.attr.from ~= entity then forward_presence(stanza, entity); end + end + else -- directed presence + -- we ignore directed presences from our own host, as we already have them + local _, from_host = jid.split(stanza.attr.from) + if hosts[from_host] then return; end + + -- we don't send several time the same presence, as recommended in §7 #2 + if priv_session.last_presence and same_presences(priv_session.last_presence, stanza) then + return + end + + for entity in presence_roster:items() do + if stanza.attr.from ~= entity then forward_presence(stanza, entity); end + end + end + end +end, 150) + +--> IQ permission <-- + +module:hook("iq/bare/".._PRIV_ENT_NS..":privileged_iq", function(event) + local session, stanza = event.origin, event.stanza + if not stanza.attr.to then + -- we don't want stanzas addressed to /self + return + end + local from_node, from_host, from_resource = jid.split(stanza.attr.to) + + if from_resource ~= nil or not usermanager_user_exists(from_node, from_host) then + session.send( + st.error_reply( + stanza, + "auth", + "forbidden", + "wrapping <IQ> stanza recipient must be a bare JID of a local user" + ) + ) + return true + end + + local session_privileges = get_session_privileges(session, from_host) + + if session_privileges == nil then + session.send( + st.error_reply( + stanza, + "auth", + "forbidden", + "no privilege granted" + ) + ) + return true + end + + local iq_privileges = session_privileges["iq"] + if iq_privileges == nil then + session.send( + session.send(st.error_reply(stanza, "auth", "forbidden", "you are not allowed to send privileged <IQ> stanzas")) + ) + return true + end + + local privileged_iq = stanza:get_child("privileged_iq", _PRIV_ENT_NS) + + local wrapped_iq = privileged_iq.tags[1] + if wrapped_iq == nil then + session.send( + st.error_reply(stanza, "auth", "forbidden", "missing <IQ> stanza to send") + ) + return true + end + + if wrapped_iq.attr.xmlns ~= "jabber:client" then + session.send( + st.error_reply( + stanza, + "auth", + "forbidden", + 'wrapped <IQ> must have a xmlns of "jabber:client"' + ) + ) + return true + end + + clean_xmlns(wrapped_iq) - local stanza = event.stanza - if stanza.attr.type == nil or stanza.attr.type == "unavailable" then - if not stanza.attr.to then - for entity in presence_man_ent:items() do - if stanza.attr.from ~= entity then forward_presence(stanza, entity); end - end - else -- directed presence - -- we ignore directed presences from our own host, as we already have them - local _, from_host = jid.split(stanza.attr.from) - if hosts[from_host] then return; end + if #wrapped_iq.tags ~= 1 then + session.send( + st.error_reply( + stanza, + "auth", + "forbidden", + 'invalid payload in wrapped <IQ>' + ) + ) + return true + end + + local payload = wrapped_iq.tags[1] + + local priv_ns = payload.attr.xmlns + if priv_ns == nil then + session.send( + st.error_reply(stanza, "auth", "forbidden", "xmlns not set in privileged <IQ>") + ) + return true + end + + local ns_perms = iq_privileges[priv_ns] + local iq_type = stanza.attr.type + if ns_perms == nil or iq_type == nil or not ns_perms[iq_type] then + session.send( + session.send(st.error_reply( + stanza, + "auth", + "forbidden", + "you are not allowed to send privileged <IQ> stanzas of this type and namespace") + ) + ) + return true + end + + if wrapped_iq.attr.from ~= nil and wrapped_iq.attr.from ~= stanza.attr.to then + session.send( + st.error_reply( + stanza, + "auth", + "forbidden", + 'wrapped <IQ> "from" attribute is inconsistent with main <IQ> "to" attribute' + ) + ) + return true + end + + wrapped_iq.attr.from = stanza.attr.to - -- we don't send several time the same presence, as recommended in §7 #2 - if priv_session.last_presence and same_presences(priv_session.last_presence, stanza) then - return - end + if wrapped_iq.attr.to == nil then + session.send( + st.error_reply( + stanza, + "auth", + "forbidden", + 'wrapped <IQ> "to" attribute is missing' + ) + ) + return true + end + + if wrapped_iq.attr.type ~= iq_type then + session.send( + st.error_reply( + stanza, + "auth", + "forbidden", + 'invalid wrapped <IQ>: type mismatch' + ) + ) + return true + end - for entity in presence_roster:items() do - if stanza.attr.from ~= entity then forward_presence(stanza, entity); end - end - end - end -end, 150) + if wrapped_iq.attr.id == nil then + session.send( + st.error_reply( + stanza, + "auth", + "forbidden", + 'invalid wrapped <IQ>: missing "id" attribute' + ) + ) + return true + end + + -- at this point, wrapped_iq is considered valid, and privileged entity is allowed to send it + + module:send_iq(wrapped_iq) + :next(function (response) + local reply = st.reply(stanza); + response.stanza.attr.xmlns = 'jabber:client' + reply:tag("privilege", {xmlns = _PRIV_ENT_NS}) + :tag("forwarded", {xmlns = _FORWARDED_NS}) + :add_child(response.stanza) + session.send(reply) + end, + function(response) + module:log("error", "Error while sending privileged <IQ>: %s", response); + session.send( + st.error_reply( + stanza, + "cancel", + "internal-server-error" + ) + ) + end) + + return true +end)