changeset 4983:7c77058a1ac5

mod_compat_roles: New module providing compat shim for trunk's new role API The new role API is translated to is_admin() calls on older versions. On newer versions (which have the role API) this module does nothing. It allows modules to drop their use of is_admin() (which is not available in trunk) and switch to the new role API, while remaining compatible with previous Prosody versions.
author Matthew Wild <mwild1@gmail.com>
date Thu, 11 Aug 2022 17:49:33 +0100
parents 8a4b17e2e984
children fc6a618bfe4e
files mod_compat_roles/README.markdown mod_compat_roles/mod_compat_roles.lua
diffstat 2 files changed, 154 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_compat_roles/README.markdown	Thu Aug 11 17:49:33 2022 +0100
@@ -0,0 +1,49 @@
+---
+labels:
+- Stage-Alpha
+summary: 'Compatibility layer for Prosody's future roles API'
+...
+
+Introduction
+============
+
+This module provides compatibility with Prosody's new role and permissions
+system. It aims to run on Prosody 0.11 and 0.12, providing a limited version
+of the new API backed by is_admin() (which is not going to be present in trunk
+and future Prosody versions).
+
+It is designed for use by modules which want to be compatible with Prosody
+versions with and without the new permissions API.
+
+Configuration
+=============
+
+There is no configuration.
+
+Usage (for developers)
+======================
+
+If you are a module developer, and want your module to work with Prosody trunk
+and future releases, you should avoid the `usermanager.is_admin()` function.
+
+Instead, depend on this module:
+
+```
+module:depends("compat_roles")
+```
+
+Then use `module:may()` instead:
+
+```
+if module:may(":do-something") then
+  -- Blah
+end
+```
+
+For more information on the new role/permissions API, check Prosody's
+developer documentation at https://prosody.im/doc/developers/permissions
+
+Compatibility
+=============
+
+Requires Prosody 0.11 or 0.12.
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mod_compat_roles/mod_compat_roles.lua	Thu Aug 11 17:49:33 2022 +0100
@@ -0,0 +1,105 @@
+-- Export a module:may() that works on Prosody 0.12 and earlier
+-- (i.e. backed by is_admin).
+
+-- This API is safe because Prosody 0.12 and earlier do not support
+-- per-session roles - all authorization is based on JID alone. It is not
+-- safe on versions that support per-session authorization.
+
+module:set_global();
+
+local moduleapi = require "core.moduleapi";
+
+-- If module.may already exists, abort
+if moduleapi.may then return; end
+
+local jid_split = require "util.jid".split;
+local um_is_admin = require "core.usermanager".is_admin;
+
+local function get_jid_role_name(jid, host)
+	if um_is_admin(jid, "*") then
+		return "prosody:operator";
+	elseif um_is_admin(jid, host) then
+		return "prosody:admin";
+	end
+	return nil;
+end
+
+local function get_user_role_name(username, host)
+	return get_jid_role_name(username.."@"..host, host);
+end
+
+-- permissions[host][permission_name] = permitted_role_name
+local permissions = {};
+
+local function role_may(role_name, permission)
+	local role_permissions = permissions[role_name];
+	if not role_permissions then
+		return false;
+	end
+	return not not permissions[role_name][permission];
+end
+
+function moduleapi.may(self, action, context)
+	if action:byte(1) == 58 then -- action begins with ':'
+		action = self.name..action; -- prepend module name
+	end
+	if type(context) == "string" then -- check JID permissions
+		local role;
+		local node, host = jid_split(context);
+		if host == self.host then
+			role = get_user_role_name(node, self.host);
+		else
+			role = get_jid_role_name(context, self.host);
+		end
+		if not role then
+			self:log("debug", "Access denied: JID <%s> may not %s (no role found)", context, action);
+			return false;
+		end
+
+		local permit = role_may(role, action);
+		if not permit then
+			self:log("debug", "Access denied: JID <%s> may not %s (not permitted by role %s)", context, action, role.name);
+		end
+		return permit;
+	end
+
+	local session = context.origin or context.session;
+	if type(session) ~= "table" then
+		error("Unable to identify actor session from context");
+	end
+	if session.type == "s2sin" or (session.type == "c2s" and session.host ~= self.host) then
+		local actor_jid = context.stanza.attr.from;
+		local role_name = get_jid_role_name(actor_jid);
+		if not role_name then
+			self:log("debug", "Access denied: JID <%s> may not %s (no role found)", actor_jid, action);
+			return false;
+		end
+		local permit = role_may(role_name, action, context);
+		if not permit then
+			self:log("debug", "Access denied: JID <%s> may not %s (not permitted by role %s)", actor_jid, action, role_name);
+		end
+		return permit;
+	end
+end
+
+function moduleapi.default_permission(self, role_name, permission)
+	local r = permissions[self.host][role_name];
+	if not r then
+		r = {};
+		permissions[self.host][role_name] = r;
+	end
+	r[permission] = true;
+end
+
+function moduleapi.default_permissions(self, role_name, permission_list)
+	for _, permission in ipairs(permission_list) do
+		self:default_permission(role_name, permission);
+	end
+end
+
+function module.add_host(host_module)
+	permissions[host_module.host] = {};
+	function host_module.unload()
+		permissions[host_module.host] = nil;
+	end
+end