mod_authz_internal: Use util.roles, some API changes and config support
authorMatthew Wild <mwild1@gmail.com>
Tue, 19 Jul 2022 18:02:02 +0100
changeset 12652 f299e570a0fe
parent 12651 a661292d074a
child 12653 86e1187f6274
mod_authz_internal: Use util.roles, some API changes and config support This commit was too awkward to split (hg record didn't like it), so: - Switch to the new util.roles lib to provide a consistent representation of a role object. - Change API method from get_role_info() to get_role_by_name() (touches sessionmanager and usermanager) - Change get_roles() to get_user_roles(), take a username instead of a JID This is more consistent with all other usermanager API methods. - Support configuration of custom roles and permissions via the config file (to be documented).
core/sessionmanager.lua
core/usermanager.lua
plugins/mod_authz_internal.lua
--- a/core/sessionmanager.lua	Tue Jul 19 17:44:26 2022 +0100
+++ b/core/sessionmanager.lua	Tue Jul 19 18:02:02 2022 +0100
@@ -133,7 +133,7 @@
 
 	local role;
 	if role_name then
-		role = hosts[session.host].authz.get_role_info(role_name);
+		role = hosts[session.host].authz.get_role_by_name(role_name);
 	else
 		role = hosts[session.host].authz.get_user_default_role(username);
 	end
--- a/core/usermanager.lua	Tue Jul 19 17:44:26 2022 +0100
+++ b/core/usermanager.lua	Tue Jul 19 18:02:02 2022 +0100
@@ -10,8 +10,6 @@
 local log = require "util.logger".init("usermanager");
 local type = type;
 local it = require "util.iterators";
-local jid_bare = require "util.jid".bare;
-local jid_split = require "util.jid".split;
 local jid_prep = require "util.jid".prep;
 local config = require "core.configmanager";
 local sasl_new = require "util.sasl".new;
@@ -150,48 +148,54 @@
 	return hosts[host].users;
 end
 
-local function get_roles(jid, host)
+-- Returns a map of { [role_name] = role, ... } that a user is allowed to assume
+local function get_user_roles(user, host)
 	if host and not hosts[host] then return false; end
-	if type(jid) ~= "string" then return false; end
+	if type(user) ~= "string" then return false; end
 
-	jid = jid_bare(jid);
 	host = host or "*";
 
-	local actor_user, actor_host = jid_split(jid);
-	local roles;
+	local authz_provider = (host ~= "*" and hosts[host].authz) or global_authz_provider;
+	return authz_provider.get_user_roles(user);
+end
+
+local function get_user_default_role(user, host)
+	if host and not hosts[host] then return false; end
+	if type(user) ~= "string" then return false; end
+
+	host = host or "*";
 
 	local authz_provider = (host ~= "*" and hosts[host].authz) or global_authz_provider;
-
-	if actor_user and actor_host == host then -- Local user
-		roles = authz_provider.get_user_roles(actor_user);
-	else -- Remote user/JID
-		roles = authz_provider.get_jid_roles(jid);
-	end
-
-	return roles;
+	return authz_provider.get_user_default_role(user);
 end
 
-local function set_roles(jid, host, roles)
+-- Accepts a set of role names which the user is allowed to assume
+local function set_user_roles(user, host, roles)
 	if host and not hosts[host] then return false; end
-	if type(jid) ~= "string" then return false; end
+	if type(user) ~= "string" then return false; end
 
-	jid = jid_bare(jid);
 	host = host or "*";
 
-	local actor_user, actor_host = jid_split(jid);
-
 	local authz_provider = (host ~= "*" and hosts[host].authz) or global_authz_provider;
-	if actor_user and actor_host == host then -- Local user
-		local ok, err = authz_provider.set_user_roles(actor_user, roles);
-		if ok then
-			prosody.events.fire_event("user-roles-changed", {
-				username = actor_user, host = actor_host
-			});
-		end
-		return ok, err;
-	else -- Remote entity
-		return authz_provider.set_jid_roles(jid, roles)
+	local ok, err = authz_provider.set_user_roles(user, roles);
+	if ok then
+		prosody.events.fire_event("user-roles-changed", {
+			username = user, host = host
+		});
 	end
+	return ok, err;
+end
+
+local function get_jid_role(jid, host)
+	host = host or "*";
+	local authz_provider = (host ~= "*" and hosts[host].authz) or global_authz_provider;
+	return authz_provider.get_jid_role(jid);
+end
+
+local function set_jid_role(jid, host, role_name)
+	host = host or "*";
+	local authz_provider = (host ~= "*" and hosts[host].authz) or global_authz_provider;
+	return authz_provider.set_jid_role(jid, role_name)
 end
 
 local function get_users_with_role(role, host)
@@ -211,6 +215,16 @@
 	return authz_provider.get_jids_with_role(role);
 end
 
+local function get_role_by_name(role_name, host)
+	if host and not hosts[host] then return false; end
+	if type(role_name) ~= "string" then return false; end
+
+	host = host or "*";
+
+	local authz_provider = (host ~= "*" and hosts[host].authz) or global_authz_provider;
+	return authz_provider.get_role_by_name(role_name);
+end
+
 return {
 	new_null_provider = new_null_provider;
 	initialize_host = initialize_host;
@@ -224,8 +238,12 @@
 	users = users;
 	get_sasl_handler = get_sasl_handler;
 	get_provider = get_provider;
-	get_roles = get_roles;
-	set_roles = set_roles;
+	get_user_default_role = get_user_default_role;
+	get_user_roles = get_user_roles;
+	set_user_roles = set_user_roles;
 	get_users_with_role = get_users_with_role;
+	get_jid_role = get_jid_role;
+	set_jid_role = set_jid_role;
 	get_jids_with_role = get_jids_with_role;
+	get_role_by_name = get_role_by_name;
 };
--- a/plugins/mod_authz_internal.lua	Tue Jul 19 17:44:26 2022 +0100
+++ b/plugins/mod_authz_internal.lua	Tue Jul 19 18:02:02 2022 +0100
@@ -3,62 +3,97 @@
 local set = require "util.set";
 local jid_split, jid_bare = require "util.jid".split, require "util.jid".bare;
 local normalize = require "util.jid".prep;
+local roles = require "util.roles";
+
 local config_global_admin_jids = module:context("*"):get_option_set("admins", {}) / normalize;
 local config_admin_jids = module:get_option_inherited_set("admins", {}) / normalize;
 local host = module.host;
 local role_store = module:open_store("roles");
 local role_map_store = module:open_store("roles", "map");
 
-local role_methods = {};
-local role_mt = { __index = role_methods };
+local role_registry = {};
 
-local role_registry = {
-	["prosody:operator"] = {
-		default = true;
-		priority = 75;
-		includes = { "prosody:admin" };
-	};
-	["prosody:admin"] = {
-		default = true;
-		priority = 50;
-		includes = { "prosody:user" };
-	};
-	["prosody:user"] = {
-		default = true;
-		priority = 25;
-		includes = { "prosody:restricted" };
-	};
-	["prosody:restricted"] = {
-		default = true;
-		priority = 15;
-	};
+function register_role(role)
+	if role_registry[role.name] ~= nil then
+		return error("A role '"..role.name.."' is already registered");
+	end
+	if not roles.is_role(role) then
+		-- Convert table syntax to real role object
+		for i, inherited_role in ipairs(role.inherits or {}) do
+			if type(inherited_role) == "string" then
+				role.inherits[i] = assert(role_registry[inherited_role], "The named role '"..inherited_role.."' is not registered");
+			end
+		end
+		if not role.permissions then role.permissions = {}; end
+		for _, allow_permission in ipairs(role.allow or {}) do
+			role.permissions[allow_permission] = true;
+		end
+		for _, deny_permission in ipairs(role.deny or {}) do
+			role.permissions[deny_permission] = false;
+		end
+		role = roles.new(role);
+	end
+	role_registry[role.name] = role;
+end
+
+-- Default roles
+register_role {
+	name = "prosody:restricted";
+	priority = 15;
+};
+
+register_role {
+	name = "prosody:user";
+	priority = 25;
+	inherits = { "prosody:restricted" };
 };
 
--- Some processing on the role registry
-for role_name, role_info in pairs(role_registry) do
-	role_info.name = role_name;
-	role_info.includes = set.new(role_info.includes) / function (included_role_name)
-		return role_registry[included_role_name];
-	end;
-	if not role_info.permissions then
-		role_info.permissions = {};
+register_role {
+	name = "prosody:admin";
+	priority = 50;
+	inherits = { "prosody:user" };
+};
+
+register_role {
+	name = "prosody:operator";
+	priority = 75;
+	inherits = { "prosody:admin" };
+};
+
+
+-- Process custom roles from config
+
+local custom_roles = module:get_option("custom_roles", {});
+for n, role_config in ipairs(custom_roles) do
+	local ok, err = pcall(register_role, role_config);
+	if not ok then
+		module:log("error", "Error registering custom role %s: %s", role_config.name or tostring(n), err);
 	end
-	setmetatable(role_info, role_mt);
 end
 
-function role_methods:may(action, context)
-	local policy = self.permissions[action];
-	if policy ~= nil then
-		return policy;
-	end
-	for inherited_role in self.includes do
-		module:log("debug", "Checking included role '%s' for %s", inherited_role.name, action);
-		policy = inherited_role:may(action, context);
-		if policy ~= nil then
-			return policy;
+-- Process custom permissions from config
+
+local config_add_perms = module:get_option("add_permissions", {});
+local config_remove_perms = module:get_option("remove_permissions", {});
+
+for role_name, added_permissions in pairs(config_add_perms) do
+	if not role_registry[role_name] then
+		module:log("error", "Cannot add permissions to unknown role '%s'", role_name);
+	else
+		for _, permission in ipairs(added_permissions) do
+			role_registry[role_name]:set_permission(permission, true, true);
 		end
 	end
-	return false;
+end
+
+for role_name, removed_permissions in pairs(config_remove_perms) do
+	if not role_registry[role_name] then
+		module:log("error", "Cannot remove permissions from unknown role '%s'", role_name);
+	else
+		for _, permission in ipairs(removed_permissions) do
+			role_registry[role_name]:set_permission(permission, false, true);
+		end
+	end
 end
 
 -- Public API
@@ -69,6 +104,9 @@
 local config_admin_role_set = {
 	["prosody:admin"] = role_registry["prosody:admin"];
 };
+local default_role_set = {
+	["prosody:user"] = role_registry["prosody:user"];
+};
 
 function get_user_roles(user)
 	local bare_jid = user.."@"..host;
@@ -78,25 +116,25 @@
 		return config_admin_role_set;
 	end
 	local role_names = role_store:get(user);
-	if not role_names then return {}; end
-	local roles = {};
+	if not role_names then return default_role_set; end
+	local user_roles = {};
 	for role_name in pairs(role_names) do
-		roles[role_name] = role_registry[role_name];
+		user_roles[role_name] = role_registry[role_name];
 	end
-	return roles;
+	return user_roles;
 end
 
-function set_user_roles(user, roles)
-	role_store:set(user, roles)
+function set_user_roles(user, user_roles)
+	role_store:set(user, user_roles)
 	return true;
 end
 
 function get_user_default_role(user)
-	local roles = get_user_roles(user);
-	if not roles then return nil; end
+	local user_roles = get_user_roles(user);
+	if not user_roles then return nil; end
 	local default_role;
-	for role_name, role_info in pairs(roles) do --luacheck: ignore 213/role_name
-		if role_info.default and (not default_role or role_info.priority > default_role.priority) then
+	for role_name, role_info in pairs(user_roles) do --luacheck: ignore 213/role_name
+		if role_info.default ~= false and (not default_role or role_info.priority > default_role.priority) then
 			default_role = role_info;
 		end
 	end
@@ -134,7 +172,7 @@
 	return nil;
 end
 
-function set_jid_role(jid) -- luacheck: ignore 212
+function set_jid_role(jid, role_name) -- luacheck: ignore 212
 	return false;
 end
 
@@ -157,16 +195,11 @@
 		module:log("warn", "Attempt to add default permission for unknown role: %s", role_name);
 		return nil, "no-such-role";
 	end
-	if role.permissions[action] == nil then
-		if policy == nil then
-			policy = true;
-		end
-		module:log("debug", "Adding permission, role '%s' may '%s': %s", role_name, action, policy and "allow" or "deny");
-		role.permissions[action] = policy;
-	end
-	return true;
+	if policy == nil then policy = true; end
+	module:log("debug", "Adding policy %s for permission %s on role %s", policy, action, role_name);
+	return role:set_permission(action, policy);
 end
 
-function get_role_info(role_name)
-	return role_registry[role_name];
+function get_role_by_name(role_name)
+	return assert(role_registry[role_name], role_name);
 end