Mercurial > prosody-modules
changeset 5009:459a4001c1d9
mod_restrict_xmpp: XMPP-layer access control using Prosody's permissions API
author | Matthew Wild <mwild1@gmail.com> |
---|---|
date | Mon, 22 Aug 2022 20:03:23 +0100 |
parents | bd63feda3704 |
children | a1f49586d28a |
files | mod_restrict_xmpp/README.markdown mod_restrict_xmpp/mod_restrict_xmpp.lua |
diffstat | 2 files changed, 171 insertions(+), 0 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_restrict_xmpp/README.markdown Mon Aug 22 20:03:23 2022 +0100 @@ -0,0 +1,51 @@ +--- +labels: +- Stage-Alpha +summary: XMPP-layer access control for Prosody +--- + +Introduction +============ + +This module enforces access policies using Prosody's new [roles and +permissions framework](https://prosody.im/doc/developers/permissions). It can +be used to grant restricted access to an XMPP account or services. + +This module is still in its early stages, and prone to change. Feedback from +testers is welcome. At this early stage, it should not be solely relied upon +for account security purposes. + +Configuration +============= + +There is no configuration, apart from Prosody's normal roles and permissions +configuration. + +Permissions +=========== + +`xmpp:federate` +: Communicate with other users and services on other hosts on the XMPP network +`xmpp:account:messages:read` +: Read incoming messages +`xmpp:account:messages:write` +: Send outgoing messages +`xmpp:account:presence:write` +: Update presence for the account +`xmpp:account:contacts:read`/`xmpp:account:contacts:write` +: Controls access to the contact list (roster) +`xmpp:account:bookmarks:read`/`xmpp:account:bookmarks:write` +: Controls access to the bookmarks (group chats list) +`xmpp:account:profile:read`/`xmpp:account:profile:write` +: Controls access to the user's profile (e.g. vCard/avatar) +`xmpp:account:omemo:read`/`xmpp:account:omemo:write` +: Controls access to the user's OMEMO data +`xmpp:account:blocklist:read`/`xmpp:account:blocklist:write` +: Controls access to the user's block list +`xmpp:account:disco:read` +: Controls access to the user's service discovery information + +Compatibility +============= + +Requires Prosody trunk 72f431b4dc2c (build 1444) or later.
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mod_restrict_xmpp/mod_restrict_xmpp.lua Mon Aug 22 20:03:23 2022 +0100 @@ -0,0 +1,120 @@ +local array = require "util.array"; +local it = require "util.iterators"; +local set = require "util.set"; +local st = require "util.stanza"; + +module:default_permission("prosody:user", "xmpp:federate"); +module:hook("route/remote", function (event) + if not module:may("xmpp:federate", event) then + if event.stanza.attr.type ~= "result" and event.stanza.attr.type ~= "error" then + module:log("warn", "Access denied: xmpp:federate for %s -> %s", event.stanza.attr.from, event.stanza.attr.to); + local reply = st.error_reply(event.stanza, "auth", "forbidden"); + event.origin.send(reply); + end + return true; + end +end); + +local iq_namespaces = { + ["jabber:iq:roster"] = "contacts"; + ["jabber:iq:private"] = "storage"; + + ["vcard-temp"] = "profile"; + ["urn:xmpp:mam:0"] = "history"; + ["urn:xmpp:mam:1"] = "history"; + ["urn:xmpp:mam:2"] = "history"; + + ["urn:xmpp:carbons:0"] = "carbons"; + ["urn:xmpp:carbons:1"] = "carbons"; + ["urn:xmpp:carbons:2"] = "carbons"; + + ["urn:xmpp:blocking"] = "blocklist"; + + ["http://jabber.org/protocol/pubsub"] = "pep"; + ["http://jabber.org/protocol/disco#info"] = "disco"; +}; + +local legacy_storage_nodes = { + ["storage:bookmarks"] = "bookmarks"; + ["storage:rosternotes"] = "contacts"; + ["roster:delimiter"] = "contacts"; + ["storage:metacontacts"] = "contacts"; +}; + +local pep_nodes = { + ["storage:bookmarks"] = "bookmarks"; + ["urn:xmpp:bookmarks:1"] = "bookmarks"; + + ["urn:xmpp:avatar:data"] = "profile"; + ["urn:xmpp:avatar:metadata"] = "profile"; + ["http://jabber.org/protocol/nick"] = "profile"; + + ["eu.siacs.conversations.axolotl.devicelist"] = "omemo"; + ["urn:xmpp:omemo:1:devices"] = "omemo"; + ["urn:xmpp:omemo:1:bundles"] = "omemo"; + ["urn:xmpp:omemo:2:devices"] = "omemo"; + ["urn:xmpp:omemo:2:bundles"] = "omemo"; +}; + +module:hook("pre-iq/bare", function (event) + if not event.to_self then return; end + local origin, stanza = event.origin, event.stanza; + + local typ = stanza.attr.type; + if typ ~= "set" and typ ~= "get" then return; end + local action = typ == "get" and "read" or "write"; + + local payload = stanza.tags[1]; + local ns = payload and payload.attr.xmlns; + local proto = iq_namespaces[ns]; + if proto == "pep" then + local pubsub = payload:get_child("pubsub", "http://jabber.org/protocol/pubsub"); + local node = pubsub and #pubsub.tags == 1 and pubsub.tags[1].attr.node or nil; + proto = pep_nodes[node] or "pep"; + if proto == "pep" and node and node:match("^eu%.siacs%.conversations%.axolotl%.bundles%.%d+$") then + proto = "omemo"; -- COMPAT w/ original OMEMO + end + elseif proto == "storage" then + local data = payload.tags[1]; + proto = data and legacy_storage_nodes[data.attr.xmlns] or "legacy-storage"; + elseif proto == "carbons" then + -- This allows access to live messages + proto, action = "messages", "read"; + end + local permission_name = "xmpp:account:"..(proto and (proto..":") or "")..action; + if not module:may(permission_name, event) then + module:log("warn", "Access denied: %s ({%s}%s) for %s", permission_name, ns, payload.name, origin.full_jid or origin.id); + origin.send(st.error_reply(stanza, "auth", "forbidden", "You do not have permission to make this request ("..permission_name..")")); + return true; + end +end); + +--module:default_permission("prosody:restricted", "xmpp:account:read"); +--module:default_permission("prosody:restricted", "xmpp:account:write"); +module:default_permission("prosody:restricted", "xmpp:account:messages:read"); +module:default_permission("prosody:restricted", "xmpp:account:messages:write"); +for _, property_list in ipairs({ iq_namespaces, legacy_storage_nodes, pep_nodes }) do + for account_property in set.new(array.collect(it.values(property_list))) do + module:default_permission("prosody:restricted", "xmpp:account:"..account_property..":read"); + module:default_permission("prosody:restricted", "xmpp:account:"..account_property..":write"); + end +end + +module:default_permission("prosody:restricted", "xmpp:account:presence:write"); +module:hook("pre-presence/bare", function (event) + if not event.to_self then return; end + local stanza = event.stanza; + if not module:may("xmpp:account:presence:write", event) then + module:log("warn", "Access denied: xmpp:account:presence:write for %s", event.origin.full_jid or event.origin.id); + event.origin.send(st.error_reply(stanza, "auth", "forbidden", "You do not have permission to send account presence")); + return true; + end + local priority = stanza:get_child_text("priority"); + if priority ~= "-1" then + if not module:may("xmpp:account:messages:read", event) then + module:log("warn", "Access denied: xmpp:account:messages:read for %s", event.origin.full_jid or event.origin.id); + event.origin.send(st.error_reply(stanza, "auth", "forbidden", "You do not have permission to receive messages (use presence priority -1)")); + return true; + end + end +end);