view mod_restrict_xmpp/mod_restrict_xmpp.lua @ 5448:9d542e86e19a

mod_http_oauth2: Allow requesting a subset of scopes on token refresh This enables clients to request access tokens with fewer permissions than the grant they were given, reducing impact of token leak. Clients could e.g. request access tokens with some privileges and immediately revoke them after use, or other strategies.
author Kim Alvefur <zash@zash.se>
date Thu, 11 May 2023 21:40:09 +0200
parents a1f49586d28a
children 825c6fb76c48
line wrap: on
line source

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";
	elseif proto == "history" then
		action = "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);