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 <host>
This commit is contained in:
Matthew Wild 2022-08-17 16:38:53 +01:00
parent 2b0676396d
commit f5768f63c9
6 changed files with 188 additions and 63 deletions

View file

@ -8,8 +8,9 @@ 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_store = module:open_store("account_roles");
local role_map_store = module:open_store("account_roles", "map");
local role_registry = {};
@ -98,52 +99,96 @@ end
-- 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];
end
return user_roles;
end
function set_user_roles(user, user_roles)
role_store:set(user, user_roles)
return 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;
-- 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
if not default_role then return nil; end
return default_role;
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
-- 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 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 @@ function get_users_with_role(role_name)
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 @@ end
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 "<none given>"));
print(" Hint: try 'migrate'?");
end
end