Changeset

4937: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
parents 4912:b45c23ce24ba
children 4938: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)