# HG changeset patch # User Matthew Wild # Date 1660750733 -3600 # Node ID 07424992d7fca169a50deed595814ca630bf2f27 # Parent 1c391c17a907344a38633ec46473e8a1381685ed mod_authz_internal, and more: New iteration of role API These changes to the API (hopefully the last) introduce a cleaner separation between the user's primary (default) role, and their secondary (optional) roles. To keep the code sane and reduce complexity, a data migration is needed for people using stored roles in 0.12. This can be performed with prosodyctl mod_authz_internal migrate diff -r 1c391c17a907 -r 07424992d7fc core/moduleapi.lua --- a/core/moduleapi.lua Fri Aug 12 22:09:09 2022 +0200 +++ b/core/moduleapi.lua Wed Aug 17 16:38:53 2022 +0100 @@ -538,6 +538,7 @@ end function api:open_store(name, store_type) + if self.host == "*" then return nil, "global-storage-not-supported"; end return require"core.storagemanager".open(self.host, name or self.name, store_type); end @@ -629,7 +630,7 @@ local role; local node, host = jid_split(context); if host == self.host then - role = hosts[host].authz.get_user_default_role(node); + role = hosts[host].authz.get_user_role(node); else role = hosts[self.host].authz.get_jid_role(context); end diff -r 1c391c17a907 -r 07424992d7fc core/sessionmanager.lua --- a/core/sessionmanager.lua Fri Aug 12 22:09:09 2022 +0200 +++ b/core/sessionmanager.lua Wed Aug 17 16:38:53 2022 +0100 @@ -135,7 +135,7 @@ if role_name then role = hosts[session.host].authz.get_role_by_name(role_name); else - role = hosts[session.host].authz.get_user_default_role(username); + role = hosts[session.host].authz.get_user_role(username); end if role then sessionlib.set_role(session, role); diff -r 1c391c17a907 -r 07424992d7fc core/usermanager.lua --- a/core/usermanager.lua Fri Aug 12 22:09:09 2022 +0200 +++ b/core/usermanager.lua Wed Aug 17 16:38:53 2022 +0100 @@ -37,13 +37,17 @@ local fallback_authz_provider = { get_user_roles = function (user) end; --luacheck: ignore 212/user get_jids_with_role = function (role) end; --luacheck: ignore 212 - set_user_roles = function (user, roles) end; -- luacheck: ignore 212 - set_jid_roles = function (jid, roles) end; -- luacheck: ignore 212 + + get_user_role = function (user) end; -- luacheck: ignore 212 + set_user_role = function (user, roles) end; -- luacheck: ignore 212 - get_user_default_role = function (user) end; -- luacheck: ignore 212 + add_user_secondary_role = function (user, host, role_name) end; --luacheck: ignore 212 + remove_user_secondary_role = function (user, host, role_name) end; --luacheck: ignore 212 + + get_jid_role = function (jid) end; -- luacheck: ignore 212 + set_jid_role = function (jid, role) end; -- luacheck: ignore 212 + get_users_with_role = function (role_name) end; -- luacheck: ignore 212 - get_jid_role = function (jid) end; -- luacheck: ignore 212 - set_jid_role = function (jid) end; -- luacheck: ignore 212 add_default_permission = function (role_name, action, policy) end; -- luacheck: ignore 212 get_role_by_name = function (role_name) end; -- luacheck: ignore 212 }; @@ -140,39 +144,63 @@ return hosts[host].users; end --- Returns a map of { [role_name] = role, ... } that a user is allowed to assume -local function get_user_roles(user, host) +local function get_user_role(user, host) if host and not hosts[host] then return false; end if type(user) ~= "string" then return false; end - return hosts[host].authz.get_user_roles(user); + return hosts[host].authz.get_user_role(user); end -local function get_user_default_role(user, host) +local function set_user_role(user, host, role_name) if host and not hosts[host] then return false; end if type(user) ~= "string" then return false; end - return hosts[host].authz.get_user_default_role(user); + local role, err = hosts[host].authz.set_user_role(user, role_name); + if role then + prosody.events.fire_event("user-role-changed", { + username = user, host = host, role = role; + }); + end + return role, err; end --- Accepts a set of role names which the user is allowed to assume -local function set_user_roles(user, host, roles) +local function add_user_secondary_role(user, host, role_name) if host and not hosts[host] then return false; end if type(user) ~= "string" then return false; end - local ok, err = hosts[host].authz.set_user_roles(user, roles); + local role, err = hosts[host].authz.add_user_secondary_role(user, role_name); + if role then + prosody.events.fire_event("user-role-added", { + username = user, host = host, role = role; + }); + end + return role, err; +end + +local function remove_user_secondary_role(user, host, role_name) + if host and not hosts[host] then return false; end + if type(user) ~= "string" then return false; end + + local ok, err = hosts[host].authz.remove_user_secondary_role(user, role_name); if ok then - prosody.events.fire_event("user-roles-changed", { - username = user, host = host + prosody.events.fire_event("user-role-removed", { + username = user, host = host, role_name = role_name; }); end return ok, err; end +local function get_user_secondary_roles(user, host) + if host and not hosts[host] then return false; end + if type(user) ~= "string" then return false; end + + return hosts[host].authz.get_user_secondary_roles(user); +end + local function get_jid_role(jid, host) local jid_node, jid_host = jid_split(jid); if host == jid_host and jid_node then - return hosts[host].authz.get_user_default_role(jid_node); + return hosts[host].authz.get_user_role(jid_node); end return hosts[host].authz.get_jid_role(jid); end @@ -230,9 +258,11 @@ users = users; get_sasl_handler = get_sasl_handler; get_provider = get_provider; - get_user_default_role = get_user_default_role; - get_user_roles = get_user_roles; - set_user_roles = set_user_roles; + get_user_role = get_user_role; + set_user_role = set_user_role; + add_user_secondary_role = add_user_secondary_role; + remove_user_secondary_role = remove_user_secondary_role; + get_user_secondary_roles = get_user_secondary_roles; get_users_with_role = get_users_with_role; get_jid_role = get_jid_role; set_jid_role = set_jid_role; diff -r 1c391c17a907 -r 07424992d7fc plugins/mod_authz_internal.lua --- a/plugins/mod_authz_internal.lua Fri Aug 12 22:09:09 2022 +0200 +++ b/plugins/mod_authz_internal.lua Wed Aug 17 16:38:53 2022 +0100 @@ -8,8 +8,9 @@ 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_store = module:open_store("account_roles"); +local role_map_store = module:open_store("account_roles", "map"); local role_registry = {}; @@ -98,52 +99,96 @@ -- Public API -local config_operator_role_set = { - ["prosody:operator"] = role_registry["prosody:operator"]; -}; -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) +-- Get the primary role of a user +function get_user_role(user) local bare_jid = user.."@"..host; + + -- Check config first if config_global_admin_jids:contains(bare_jid) then - return config_operator_role_set; + return role_registry["prosody:operator"]; elseif config_admin_jids:contains(bare_jid) then - return config_admin_role_set; + return role_registry["prosody:admin"]; end - local role_names = role_store:get(user); - if not role_names then return default_role_set; end - local user_roles = {}; - for role_name in pairs(role_names) do - user_roles[role_name] = role_registry[role_name]; + + -- Check storage + local stored_roles, err = role_store:get(user); + if not stored_roles then + if err then + -- Unable to fetch role, fail + return nil, err; + end + -- No role set, use default role + return role_registry["prosody:user"]; end - return user_roles; + if stored_roles._default == nil then + -- No primary role explicitly set, return default + return role_registry["prosody:user"]; + end + local primary_stored_role = role_registry[stored_roles._default]; + if not primary_stored_role then + return nil, "unknown-role"; + end + return primary_stored_role; end -function set_user_roles(user, user_roles) - role_store:set(user, user_roles) - return true; +-- Set the primary role of a user +function set_user_role(user, role_name) + local role = role_registry[role_name]; + if not role then + return error("Cannot assign default user an unknown role: "..tostring(role_name)); + end + local keys_update = { + _default = role_name; + -- Primary role cannot be secondary role + [role_name] = role_map_store.remove; + }; + if role_name == "prosody:user" then + -- Don't store default + keys_update._default = role_map_store.remove; + end + local ok, err = role_map_store:set_keys(user, keys_update); + if not ok then + return nil, err; + end + return role; +end + +function add_user_secondary_role(user, role_name) + if not role_registry[role_name] then + return error("Cannot assign default user an unknown role: "..tostring(role_name)); + end + role_map_store:set(user, role_name, true); end -function get_user_default_role(user) - 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(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 - if not default_role then return nil; end - return default_role; +function remove_user_secondary_role(user, role_name) + role_map_store:set(user, role_name, nil); end +function get_user_secondary_roles(user) + local stored_roles, err = role_store:get(user); + if not stored_roles then + if err then + -- Unable to fetch role, fail + return nil, err; + end + -- No role set + return {}; + end + stored_roles._default = nil; + for role_name in pairs(stored_roles) do + stored_roles[role_name] = role_registry[role_name]; + end + return stored_roles; +end + +-- This function is *expensive* function get_users_with_role(role_name) - local storage_role_users = it.to_array(it.keys(role_map_store:get_all(role_name) or {})); + local function role_filter(username, default_role) --luacheck: ignore 212/username + return default_role == role_name; + end + local primary_role_users = set.new(it.to_array(it.filter(role_filter, pairs(role_map_store:get_all("_default") or {})))); + local secondary_role_users = set.new(it.to_array(it.keys(role_map_store:get_all(role_name) or {}))); + local config_set; if role_name == "prosody:admin" then config_set = config_admin_jids; @@ -157,9 +202,9 @@ return j_node; end end; - return it.to_array(config_admin_users + set.new(storage_role_users)); + return it.to_array(config_admin_users + primary_role_users + secondary_role_users); end - return storage_role_users; + return it.to_array(primary_role_users + secondary_role_users); end function get_jid_role(jid) @@ -203,3 +248,52 @@ function get_role_by_name(role_name) return assert(role_registry[role_name], role_name); end + +-- COMPAT: Migrate from 0.12 role storage +local function do_migration(migrate_host) + local old_role_store = assert(module:context(migrate_host):open_store("roles")); + local new_role_store = assert(module:context(migrate_host):open_store("account_roles")); + + local migrated, failed, skipped = 0, 0, 0; + -- Iterate all users + for username in assert(old_role_store:users()) do + local old_roles = it.to_array(it.filter(function (k) return k:sub(1,1) ~= "_"; end, it.keys(old_role_store:get(username)))); + if #old_roles == 1 then + local ok, err = new_role_store:set(username, { + _default = old_roles[1]; + }); + if ok then + migrated = migrated + 1; + else + failed = failed + 1; + print("EE: Failed to store new role info for '"..username.."': "..err); + end + else + print("WW: User '"..username.."' has multiple roles and cannot be automatically migrated"); + skipped = skipped + 1; + end + end + return migrated, failed, skipped; +end + +function module.command(arg) + if arg[1] == "migrate" then + table.remove(arg, 1); + local migrate_host = arg[1]; + if not migrate_host or not prosody.hosts[migrate_host] then + print("EE: Please supply a valid host to migrate to the new role storage"); + return 1; + end + + -- Initialize storage layer + require "core.storagemanager".initialize_host(migrate_host); + + print("II: Migrating roles..."); + local migrated, failed, skipped = do_migration(migrate_host); + print(("II: %d migrated, %d failed, %d skipped"):format(migrated, failed, skipped)); + return (failed + skipped == 0) and 0 or 1; + else + print("EE: Unknown command: "..(arg[1] or "")); + print(" Hint: try 'migrate'?"); + end +end diff -r 1c391c17a907 -r 07424992d7fc plugins/mod_c2s.lua --- a/plugins/mod_c2s.lua Fri Aug 12 22:09:09 2022 +0200 +++ b/plugins/mod_c2s.lua Wed Aug 17 16:38:53 2022 +0100 @@ -259,7 +259,7 @@ end module:hook_global("user-password-changed", disconnect_user_sessions({ condition = "reset", text = "Password changed" }, true), 200); -module:hook_global("user-roles-changed", disconnect_user_sessions({ condition = "reset", text = "Roles changed" }), 200); +module:hook_global("user-role-changed", disconnect_user_sessions({ condition = "reset", text = "Role changed" }), 200); module:hook_global("user-deleted", disconnect_user_sessions({ condition = "not-authorized", text = "Account deleted" }), 200); function runner_callbacks:ready() diff -r 1c391c17a907 -r 07424992d7fc plugins/mod_tokenauth.lua --- a/plugins/mod_tokenauth.lua Fri Aug 12 22:09:09 2022 +0200 +++ b/plugins/mod_tokenauth.lua Wed Aug 17 16:38:53 2022 +0100 @@ -10,7 +10,7 @@ if role then return prosody.hosts[host].authz.get_role_by_name(role); end - return usermanager.get_user_default_role(username, host); + return usermanager.get_user_role(username, host); end function create_jid_token(actor_jid, token_jid, token_role, token_ttl)