From 7ebea134e6fd45a524b8a924be04c3b51f8f7799 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Wed, 15 Jun 2022 11:37:09 +0100 Subject: util.session: Add role management methods --- util/session.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/util/session.lua b/util/session.lua index 25b22faf..d908476a 100644 --- a/util/session.lua +++ b/util/session.lua @@ -57,10 +57,16 @@ local function set_send(session) return session; end +local function set_role(session, role) + session.role = role; +end + return { new = new_session; + set_id = set_id; set_logger = set_logger; set_conn = set_conn; set_send = set_send; + set_role = set_role; } -- cgit v1.2.3 From 8b384dc77fe54fc6a6c228dd5b6208d403f74216 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Wed, 15 Jun 2022 11:47:39 +0100 Subject: mod_saslauth: Rename field from 'scope'->'role' The 'scope' term derives from OAuth, and represents a bundle of permissions. We're now setting on the term 'role' for a bundle of permissions. This change does not affect any public modules I'm aware of. --- plugins/mod_saslauth.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/mod_saslauth.lua b/plugins/mod_saslauth.lua index e94b2d78..c7228b10 100644 --- a/plugins/mod_saslauth.lua +++ b/plugins/mod_saslauth.lua @@ -52,7 +52,7 @@ local function handle_status(session, status, ret, err_msg) module:fire_event("authentication-failure", { session = session, condition = ret, text = err_msg }); session.sasl_handler = session.sasl_handler:clean_clone(); elseif status == "success" then - local ok, err = sm_make_authenticated(session, session.sasl_handler.username, session.sasl_handler.scope); + local ok, err = sm_make_authenticated(session, session.sasl_handler.username, session.sasl_handler.role); if ok then module:fire_event("authentication-success", { session = session }); session.sasl_handler = nil; -- cgit v1.2.3 From d73714b4f426da4f9c79d5ddf0b8cb11d09e9f3f Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Wed, 15 Jun 2022 12:15:01 +0100 Subject: Switch to a new role-based authorization framework, removing is_admin() We began moving away from simple "is this user an admin?" permission checks before 0.12, with the introduction of mod_authz_internal and the ability to dynamically change the roles of individual users. The approach in 0.12 still had various limitations however, and apart from the introduction of roles other than "admin" and the ability to pull that info from storage, not much actually changed. This new framework shakes things up a lot, though aims to maintain the same functionality and behaviour on the surface for a default Prosody configuration. That is, if you don't take advantage of any of the new features, you shouldn't notice any change. The biggest change visible to developers is that usermanager.is_admin() (and the auth provider is_admin() method) have been removed. Gone. Completely. Permission checks should now be performed using a new module API method: module:may(action_name, context) This method accepts an action name, followed by either a JID (string) or (preferably) a table containing 'origin'/'session' and 'stanza' fields (e.g. the standard object passed to most events). It will return true if the action should be permitted, or false/nil otherwise. Modules should no longer perform permission checks based on the role name. E.g. a lot of code previously checked if the user's role was prosody:admin before permitting some action. Since many roles might now exist with similar permissions, and the permissions of prosody:admin may be redefined dynamically, it is no longer suitable to use this method for permission checks. Use module:may(). If you start an action name with ':' (recommended) then the current module's name will automatically be used as a prefix. To define a new permission, use the new module API: module:default_permission(role_name, action_name) module:default_permissions(role_name, { action_name[, action_name...] }) This grants the specified role permission to execute the named action(s) by default. This may be overridden via other mechanisms external to your module. The built-in roles that developers should use are: - prosody:user (normal user) - prosody:admin (host admin) - prosody:operator (global admin) The new prosody:operator role is intended for server-wide actions (such as shutting down Prosody). Finally, all usage of is_admin() in modules has been fixed by this commit. Some of these changes were trickier than others, but no change is expected to break existing deployments. EXCEPT: mod_auth_ldap no longer supports the ldap_admin_filter option. It's very possible nobody is using this, but if someone is then we can later update it to pull roles from LDAP somehow. --- .luacheckrc | 3 + core/moduleapi.lua | 63 +++++++++++++++++ core/sessionmanager.lua | 15 +++- core/usermanager.lua | 6 -- plugins/adhoc/adhoc.lib.lua | 10 ++- plugins/adhoc/mod_adhoc.lua | 37 +++------- plugins/mod_announce.lua | 6 +- plugins/mod_auth_ldap.lua | 26 ++----- plugins/mod_authz_internal.lua | 145 +++++++++++++++++++++++++++++++++----- plugins/mod_disco.lua | 9 +-- plugins/mod_invites_adhoc.lua | 38 ++-------- plugins/mod_pubsub/mod_pubsub.lua | 4 +- plugins/muc/hidden.lib.lua | 8 +-- plugins/muc/mod_muc.lua | 19 ++--- plugins/muc/persistent.lib.lua | 11 +-- 15 files changed, 269 insertions(+), 131 deletions(-) diff --git a/.luacheckrc b/.luacheckrc index 6c948b17..e1bb7134 100644 --- a/.luacheckrc +++ b/.luacheckrc @@ -62,6 +62,8 @@ files["plugins/"] = { "module.broadcast", "module.context", "module.depends", + "module.default_permission", + "module.default_permissions", "module.fire_event", "module.get_directory", "module.get_host", @@ -86,6 +88,7 @@ files["plugins/"] = { "module.load_resource", "module.log", "module.log_status", + "module.may", "module.measure", "module.metric", "module.open_store", diff --git a/core/moduleapi.lua b/core/moduleapi.lua index 36d82193..f0b412f3 100644 --- a/core/moduleapi.lua +++ b/core/moduleapi.lua @@ -19,6 +19,7 @@ local promise = require "util.promise"; local time_now = require "util.time".now; local format = require "util.format".format; local jid_node = require "util.jid".node; +local jid_split = require "util.jid".split; local jid_resource = require "util.jid".resource; local t_insert, t_remove, t_concat = table.insert, table.remove, table.concat; @@ -601,4 +602,66 @@ function api:get_status() return self.status_type, self.status_message, self.status_time; end +function api:default_permission(role_name, permission) + permission = permission:gsub("^:", self.name..":"); + hosts[self.host].authz.add_default_permission(role_name, permission); +end + +function api:default_permissions(role_name, permissions) + for _, permission in ipairs(permissions) do + permission = permission:gsub("^:", self.name..":"); + self:default_permission(role_name, permission); + end +end + +function api:may(action, context) + if type(context) == "string" then -- check JID permissions + local role; + local node, host = jid_split(context); + if host == self.host then + role = hosts[host].authz.get_user_role(node); + else + role = hosts[self.host].authz.get_jid_role(context); + 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(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 not session then + error("Unable to identify actor session from context"); + end + if action:byte(1) == 58 then -- action begins with ':' + action = self.name..action; -- prepend module name + end + if session.type == "s2sin" or (session.type == "c2s" and session.host ~= self.host) then + local actor_jid = context.stanza.attr.from; + local role = hosts[self.host].authz.get_jid_role(actor_jid); + if not role then + self:log("debug", "Access denied: JID <%s> may not %s (no role found)", actor_jid, action); + return false; + end + local permit = role:may(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; + elseif session.role then + local permit = session.role:may(action, context); + if not permit then + self:log("debug", "Access denied: session %s (%s) may not %s (not permitted by role %s)", + session.id, session.full_jid, action, session.role.name + ); + end + return permit; + end +end + return api; diff --git a/core/sessionmanager.lua b/core/sessionmanager.lua index 7f296ff1..426afa7b 100644 --- a/core/sessionmanager.lua +++ b/core/sessionmanager.lua @@ -123,15 +123,24 @@ local function destroy_session(session, err) retire_session(session); end -local function make_authenticated(session, username, scope) +local function make_authenticated(session, username, role_name) username = nodeprep(username); if not username or #username == 0 then return nil, "Invalid username"; end session.username = username; if session.type == "c2s_unauthed" then session.type = "c2s_unbound"; end - session.auth_scope = scope; - session.log("info", "Authenticated as %s@%s", username, session.host or "(unknown)"); + + local role; + if role_name then + role = hosts[session.host].authz.get_role_info(role_name); + else + role = hosts[session.host].authz.get_user_default_role(username); + end + if role then + sessionlib.set_role(session, role); + end + session.log("info", "Authenticated as %s@%s [%s]", username, session.host or "(unknown)", role and role.name or "no role"); return true; end diff --git a/core/usermanager.lua b/core/usermanager.lua index 45f104fa..970140ef 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -188,11 +188,6 @@ local function set_roles(jid, host, roles) end end -local function is_admin(jid, host) - local roles = get_roles(jid, host); - return roles and roles["prosody:admin"]; -end - local function get_users_with_role(role, host) if not hosts[host] then return false; end if type(role) ~= "string" then return false; end @@ -224,7 +219,6 @@ return { get_provider = get_provider; get_roles = get_roles; set_roles = set_roles; - is_admin = is_admin; get_users_with_role = get_users_with_role; get_jids_with_role = get_jids_with_role; }; diff --git a/plugins/adhoc/adhoc.lib.lua b/plugins/adhoc/adhoc.lib.lua index eb91f252..9f091e3b 100644 --- a/plugins/adhoc/adhoc.lib.lua +++ b/plugins/adhoc/adhoc.lib.lua @@ -23,10 +23,16 @@ end function _M.new(name, node, handler, permission) if not permission then error "adhoc.new() expects a permission argument, none given" - end - if permission == "user" then + elseif permission == "user" then error "the permission mode 'user' has been renamed 'any', please update your code" end + if permission == "admin" then + module:default_permission("prosody:admin", "mod_adhoc:"..node); + permission = "check"; + elseif permission == "global_admin" then + module:default_permission("prosody:operator", "mod_adhoc:"..node); + permission = "check"; + end return { name = name, node = node, handler = handler, cmdtag = _cmdtag, permission = permission }; end diff --git a/plugins/adhoc/mod_adhoc.lua b/plugins/adhoc/mod_adhoc.lua index 9d6ff77a..c94ff24f 100644 --- a/plugins/adhoc/mod_adhoc.lua +++ b/plugins/adhoc/mod_adhoc.lua @@ -7,7 +7,6 @@ local it = require "util.iterators"; local st = require "util.stanza"; -local is_admin = require "core.usermanager".is_admin; local jid_host = require "util.jid".host; local adhoc_handle_cmd = module:require "adhoc".handle_cmd; local xmlns_cmd = "http://jabber.org/protocol/commands"; @@ -15,18 +14,17 @@ local commands = {}; module:add_feature(xmlns_cmd); +local function check_permissions(event, node, command) + return (command.permission == "check" and module:may("mod_adhoc:"..node, event)) + or (command.permission == "local_user" and jid_host(event.stanza.attr.from) == module.host) + or (command.permission == "any"); +end + module:hook("host-disco-info-node", function (event) local stanza, origin, reply, node = event.stanza, event.origin, event.reply, event.node; if commands[node] then - local from = stanza.attr.from; - local privileged = is_admin(from, stanza.attr.to); - local global_admin = is_admin(from); - local hostname = jid_host(from); local command = commands[node]; - if (command.permission == "admin" and privileged) - or (command.permission == "global_admin" and global_admin) - or (command.permission == "local_user" and hostname == module.host) - or (command.permission == "any") then + if check_permissions(event, node, command) then reply:tag("identity", { name = command.name, category = "automation", type = "command-node" }):up(); reply:tag("feature", { var = xmlns_cmd }):up(); @@ -44,20 +42,13 @@ module:hook("host-disco-info-node", function (event) end); module:hook("host-disco-items-node", function (event) - local stanza, reply, disco_node = event.stanza, event.reply, event.node; + local reply, disco_node = event.reply, event.node; if disco_node ~= xmlns_cmd then return; end - local from = stanza.attr.from; - local admin = is_admin(from, stanza.attr.to); - local global_admin = is_admin(from); - local hostname = jid_host(from); for node, command in it.sorted_pairs(commands) do - if (command.permission == "admin" and admin) - or (command.permission == "global_admin" and global_admin) - or (command.permission == "local_user" and hostname == module.host) - or (command.permission == "any") then + if check_permissions(event, node, command) then reply:tag("item", { name = command.name, node = node, jid = module:get_host() }); reply:up(); @@ -71,15 +62,9 @@ module:hook("iq-set/host/"..xmlns_cmd..":command", function (event) local node = stanza.tags[1].attr.node local command = commands[node]; if command then - local from = stanza.attr.from; - local admin = is_admin(from, stanza.attr.to); - local global_admin = is_admin(from); - local hostname = jid_host(from); - if (command.permission == "admin" and not admin) - or (command.permission == "global_admin" and not global_admin) - or (command.permission == "local_user" and hostname ~= module.host) then + if not check_permissions(event, node, command) then origin.send(st.error_reply(stanza, "auth", "forbidden", "You don't have permission to execute this command"):up() - :add_child(command:cmdtag("canceled") + :add_child(command:cmdtag("canceled") :tag("note", {type="error"}):text("You don't have permission to execute this command"))); return true end diff --git a/plugins/mod_announce.lua b/plugins/mod_announce.lua index c742ebb8..8161d4ba 100644 --- a/plugins/mod_announce.lua +++ b/plugins/mod_announce.lua @@ -9,7 +9,6 @@ local st, jid = require "util.stanza", require "util.jid"; local hosts = prosody.hosts; -local is_admin = require "core.usermanager".is_admin; function send_to_online(message, host) local sessions; @@ -34,6 +33,7 @@ function send_to_online(message, host) return c; end +module:default_permission("prosody:admin", ":send-announcement"); -- Old -based jabberd-style announcement sending function handle_announcement(event) @@ -45,8 +45,8 @@ function handle_announcement(event) return; -- Not an announcement end - if not is_admin(stanza.attr.from, host) then - -- Not an admin? Not allowed! + if not module:may(":send-announcement", event) then + -- Not allowed! module:log("warn", "Non-admin '%s' tried to send server announcement", stanza.attr.from); return; end diff --git a/plugins/mod_auth_ldap.lua b/plugins/mod_auth_ldap.lua index 4d484aaa..a3ea880c 100644 --- a/plugins/mod_auth_ldap.lua +++ b/plugins/mod_auth_ldap.lua @@ -1,6 +1,5 @@ -- mod_auth_ldap -local jid_split = require "util.jid".split; local new_sasl = require "util.sasl".new; local lualdap = require "lualdap"; @@ -21,6 +20,13 @@ local ldap_admins = module:get_option_string("ldap_admin_filter", module:get_option_string("ldap_admins")); -- COMPAT with mistake in documentation local host = ldap_filter_escape(module:get_option_string("realm", module.host)); +if ldap_admins then + module:log("error", "The 'ldap_admin_filter' option has been deprecated, ".. + "and will be ignored. Equivalent functionality may be added in ".. + "the future if there is demand." + ); +end + -- Initiate connection local ld = nil; module.unload = function() if ld then pcall(ld, ld.close); end end @@ -133,22 +139,4 @@ else module:log("error", "Unsupported ldap_mode %s", tostring(ldap_mode)); end -if ldap_admins then - function provider.is_admin(jid) - local username, user_host = jid_split(jid); - if user_host ~= module.host then - return false; - end - return ldap_do("search", 2, { - base = ldap_base; - scope = ldap_scope; - sizelimit = 1; - filter = ldap_admins:gsub("%$(%a+)", { - user = ldap_filter_escape(username); - host = host; - }); - }); - end -end - module:provides("auth", provider); diff --git a/plugins/mod_authz_internal.lua b/plugins/mod_authz_internal.lua index 17687959..35bc3929 100644 --- a/plugins/mod_authz_internal.lua +++ b/plugins/mod_authz_internal.lua @@ -1,20 +1,89 @@ local array = require "util.array"; local it = require "util.iterators"; local set = require "util.set"; -local jid_split = require "util.jid".split; +local jid_split, jid_bare = require "util.jid".split, require "util.jid".bare; local normalize = require "util.jid".prep; +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 admin_role = { ["prosody:admin"] = true }; +local role_methods = {}; +local role_mt = { __index = role_methods }; + +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; + }; +}; + +-- 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 = {}; + 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; + end + end + return false; +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"]; +}; function get_user_roles(user) - if config_admin_jids:contains(user.."@"..host) then - return admin_role; + local bare_jid = user.."@"..host; + if config_global_admin_jids:contains(bare_jid) then + return config_operator_role_set; + elseif config_admin_jids:contains(bare_jid) then + return config_admin_role_set; + end + local role_names = role_store:get(user); + if not role_names then return {}; end + local roles = {}; + for role_name in pairs(role_names) do + roles[role_name] = role_registry[role_name]; end - return role_store:get(user); + return roles; end function set_user_roles(user, roles) @@ -22,10 +91,29 @@ function set_user_roles(user, roles) return true; end -function get_users_with_role(role) - local storage_role_users = it.to_array(it.keys(role_map_store:get_all(role) or {})); - if role == "prosody:admin" then - local config_admin_users = config_admin_jids / function (admin_jid) +function get_user_default_role(user) + local roles = get_user_roles(user); + if not 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 + default_role = role_info; + end + end + if not default_role then return nil; end + return default_role; +end + +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 config_set; + if role_name == "prosody:admin" then + config_set = config_admin_jids; + elseif role_name == "prosody:operator" then + config_set = config_global_admin_jids; + end + if config_set then + local config_admin_users = config_set / function (admin_jid) local j_node, j_host = jid_split(admin_jid); if j_host == host then return j_node; @@ -36,24 +124,49 @@ function get_users_with_role(role) return storage_role_users; end -function get_jid_roles(jid) - if config_admin_jids:contains(jid) then - return admin_role; +function get_jid_role(jid) + local bare_jid = jid_bare(jid); + if config_global_admin_jids:contains(bare_jid) then + return role_registry["prosody:operator"]; + elseif config_admin_jids:contains(bare_jid) then + return role_registry["prosody:admin"]; end return nil; end -function set_jid_roles(jid) -- luacheck: ignore 212 +function set_jid_role(jid) -- luacheck: ignore 212 return false; end -function get_jids_with_role(role) +function get_jids_with_role(role_name) -- Fetch role users from storage - local storage_role_jids = array.map(get_users_with_role(role), function (username) + local storage_role_jids = array.map(get_users_with_role(role_name), function (username) return username.."@"..host; end); - if role == "prosody:admin" then + if role_name == "prosody:admin" then return it.to_array(config_admin_jids + set.new(storage_role_jids)); + elseif role_name == "prosody:operator" then + return it.to_array(config_global_admin_jids + set.new(storage_role_jids)); end return storage_role_jids; end + +function add_default_permission(role_name, action, policy) + local role = role_registry[role_name]; + if not role then + 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; +end + +function get_role_info(role_name) + return role_registry[role_name]; +end diff --git a/plugins/mod_disco.lua b/plugins/mod_disco.lua index 79249c52..7b3e5caf 100644 --- a/plugins/mod_disco.lua +++ b/plugins/mod_disco.lua @@ -8,7 +8,6 @@ local get_children = require "core.hostmanager".get_children; local is_contact_subscribed = require "core.rostermanager".is_contact_subscribed; -local um_is_admin = require "core.usermanager".is_admin; local jid_split = require "util.jid".split; local jid_bare = require "util.jid".bare; local st = require "util.stanza" @@ -162,14 +161,16 @@ module:hook("s2s-stream-features", function (event) end end); +module:default_permission("prosody:admin", ":be-discovered-admin"); + -- Handle disco requests to user accounts if module:get_host_type() ~= "local" then return end -- skip for components module:hook("iq-get/bare/http://jabber.org/protocol/disco#info:query", function(event) local origin, stanza = event.origin, event.stanza; local node = stanza.tags[1].attr.node; local username = jid_split(stanza.attr.to) or origin.username; - local is_admin = um_is_admin(stanza.attr.to or origin.full_jid, module.host) - if not stanza.attr.to or (expose_admins and is_admin) or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then + local target_is_admin = module:may(":be-discovered-admin", stanza.attr.to or origin.full_jid); + if not stanza.attr.to or (expose_admins and target_is_admin) or is_contact_subscribed(username, module.host, jid_bare(stanza.attr.from)) then if node and node ~= "" then local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#info', node=node}); if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account @@ -185,7 +186,7 @@ module:hook("iq-get/bare/http://jabber.org/protocol/disco#info:query", function( end local reply = st.reply(stanza):tag('query', {xmlns='http://jabber.org/protocol/disco#info'}); if not reply.attr.from then reply.attr.from = origin.username.."@"..origin.host; end -- COMPAT To satisfy Psi when querying own account - if is_admin then + if target_is_admin then reply:tag('identity', {category='account', type='admin'}):up(); elseif prosody.hosts[module.host].users.name == "anonymous" then reply:tag('identity', {category='account', type='anonymous'}):up(); diff --git a/plugins/mod_invites_adhoc.lua b/plugins/mod_invites_adhoc.lua index bd6f0c2e..04c74461 100644 --- a/plugins/mod_invites_adhoc.lua +++ b/plugins/mod_invites_adhoc.lua @@ -2,7 +2,6 @@ local dataforms = require "util.dataforms"; local datetime = require "util.datetime"; local split_jid = require "util.jid".split; -local usermanager = require "core.usermanager"; local new_adhoc = module:require("adhoc").new; @@ -13,8 +12,7 @@ local allow_user_invites = module:get_option_boolean("allow_user_invites", false -- on the server, use the option above instead. local allow_contact_invites = module:get_option_boolean("allow_contact_invites", true); -local allow_user_invite_roles = module:get_option_set("allow_user_invites_by_roles"); -local deny_user_invite_roles = module:get_option_set("deny_user_invites_by_roles"); +module:default_permission(allow_user_invites and "prosody:user" or "prosody:admin", ":invite-users"); local invites; if prosody.shutdown then -- COMPAT hack to detect prosodyctl @@ -42,36 +40,8 @@ local invite_result_form = dataforms.new({ -- This is for checking if the specified JID may create invites -- that allow people to register accounts on this host. -local function may_invite_new_users(jid) - if usermanager.get_roles then - local user_roles = usermanager.get_roles(jid, module.host); - if not user_roles then - -- User has no roles we can check, just return default - return allow_user_invites; - end - - if user_roles["prosody:admin"] then - return true; - end - if allow_user_invite_roles then - for allowed_role in allow_user_invite_roles do - if user_roles[allowed_role] then - return true; - end - end - end - if deny_user_invite_roles then - for denied_role in deny_user_invite_roles do - if user_roles[denied_role] then - return false; - end - end - end - elseif usermanager.is_admin(jid, module.host) then -- COMPAT w/0.11 - return true; -- Admins may always create invitations - end - -- No role matches, so whatever the default is - return allow_user_invites; +local function may_invite_new_users(context) + return module:may(":invite-users", context); end module:depends("adhoc"); @@ -91,7 +61,7 @@ module:provides("adhoc", new_adhoc("Create new contact invite", "urn:xmpp:invite }; }; end - local invite = invites.create_contact(username, may_invite_new_users(data.from), { + local invite = invites.create_contact(username, may_invite_new_users(data), { source = data.from }); --TODO: check errors diff --git a/plugins/mod_pubsub/mod_pubsub.lua b/plugins/mod_pubsub/mod_pubsub.lua index ef31f326..f51e8fe4 100644 --- a/plugins/mod_pubsub/mod_pubsub.lua +++ b/plugins/mod_pubsub/mod_pubsub.lua @@ -1,7 +1,6 @@ local pubsub = require "util.pubsub"; local st = require "util.stanza"; local jid_bare = require "util.jid".bare; -local usermanager = require "core.usermanager"; local new_id = require "util.id".medium; local storagemanager = require "core.storagemanager"; local xtemplate = require "util.xtemplate"; @@ -177,9 +176,10 @@ module:hook("host-disco-items", function (event) end); local admin_aff = module:get_option_string("default_admin_affiliation", "owner"); +module:default_permission("prosody:admin", ":service-admin"); local function get_affiliation(jid) local bare_jid = jid_bare(jid); - if bare_jid == module.host or usermanager.is_admin(bare_jid, module.host) then + if bare_jid == module.host or module:may(":service-admin", bare_jid) then return admin_aff; end end diff --git a/plugins/muc/hidden.lib.lua b/plugins/muc/hidden.lib.lua index 153df21a..087fa102 100644 --- a/plugins/muc/hidden.lib.lua +++ b/plugins/muc/hidden.lib.lua @@ -8,7 +8,7 @@ -- local restrict_public = not module:get_option_boolean("muc_room_allow_public", true); -local um_is_admin = require "core.usermanager".is_admin; +module:default_permission(restrict_public and "prosody:admin" or "prosody:user", ":create-public-room"); local function get_hidden(room) return room._data.hidden; @@ -22,8 +22,8 @@ local function set_hidden(room, hidden) end module:hook("muc-config-form", function(event) - if restrict_public and not um_is_admin(event.actor, module.host) then - -- Don't show option if public rooms are restricted and user is not admin of this host + if not module:may(":create-public-room", event.actor) then + -- Hide config option if this user is not allowed to create public rooms return; end table.insert(event.form, { @@ -36,7 +36,7 @@ module:hook("muc-config-form", function(event) end, 100-9); module:hook("muc-config-submitted/muc#roomconfig_publicroom", function(event) - if restrict_public and not um_is_admin(event.actor, module.host) then + if not module:may(":create-public-room", event.actor) then return; -- Not allowed end if set_hidden(event.room, not event.value) then diff --git a/plugins/muc/mod_muc.lua b/plugins/muc/mod_muc.lua index 5873b1a2..08be3586 100644 --- a/plugins/muc/mod_muc.lua +++ b/plugins/muc/mod_muc.lua @@ -100,7 +100,6 @@ local jid_prep = require "util.jid".prep; local jid_bare = require "util.jid".bare; local st = require "util.stanza"; local cache = require "util.cache"; -local um_is_admin = require "core.usermanager".is_admin; module:require "muc/config_form_sections"; @@ -111,21 +110,23 @@ module:depends "muc_unique" module:require "muc/hats"; module:require "muc/lock"; -local function is_admin(jid) - return um_is_admin(jid, module.host); -end +module:default_permissions("prosody:admin", { + ":automatic-ownership"; + ":create-room"; + ":recreate-destroyed-room"; +}); if module:get_option_boolean("component_admins_as_room_owners", true) then -- Monkey patch to make server admins room owners local _get_affiliation = room_mt.get_affiliation; function room_mt:get_affiliation(jid) - if is_admin(jid) then return "owner"; end + if module:may(":automatic-ownership", jid) then return "owner"; end return _get_affiliation(self, jid); end local _set_affiliation = room_mt.set_affiliation; function room_mt:set_affiliation(actor, jid, affiliation, reason, data) - if affiliation ~= "owner" and is_admin(jid) then return nil, "modify", "not-acceptable"; end + if affiliation ~= "owner" and module:may(":automatic-ownership", jid) then return nil, "modify", "not-acceptable"; end return _set_affiliation(self, actor, jid, affiliation, reason, data); end end @@ -412,6 +413,8 @@ if module:get_option_boolean("muc_tombstones", true) then end, -10); end +module:default_permission("prosody:admin", ":create-room"); + do local restrict_room_creation = module:get_option("restrict_room_creation"); if restrict_room_creation == true then @@ -422,7 +425,7 @@ do module:hook("muc-room-pre-create", function(event) local origin, stanza = event.origin, event.stanza; local user_jid = stanza.attr.from; - if not is_admin(user_jid) and not ( + if not module:may(":create-room", event) and not ( restrict_room_creation == "local" and select(2, jid_split(user_jid)) == host_suffix ) then @@ -465,7 +468,7 @@ for event_name, method in pairs { if room and room._data.destroyed then if room._data.locked < os.time() - or (is_admin(stanza.attr.from) and stanza.name == "presence" and stanza.attr.type == nil) then + or (module:may(":recreate-destroyed-room", event) and stanza.name == "presence" and stanza.attr.type == nil) then -- Allow the room to be recreated by admin or after time has passed delete_room(room); room = nil; diff --git a/plugins/muc/persistent.lib.lua b/plugins/muc/persistent.lib.lua index c3b16ea4..4c753921 100644 --- a/plugins/muc/persistent.lib.lua +++ b/plugins/muc/persistent.lib.lua @@ -8,7 +8,10 @@ -- local restrict_persistent = not module:get_option_boolean("muc_room_allow_persistent", true); -local um_is_admin = require "core.usermanager".is_admin; +module:default_permission( + restrict_persistent and "prosody:admin" or "prosody:user", + ":create-persistent-room" +); local function get_persistent(room) return room._data.persistent; @@ -22,8 +25,8 @@ local function set_persistent(room, persistent) end module:hook("muc-config-form", function(event) - if restrict_persistent and not um_is_admin(event.actor, module.host) then - -- Don't show option if hidden rooms are restricted and user is not admin of this host + if not module:may(":create-persistent-room", event.actor) then + -- Hide config option if this user is not allowed to create persistent rooms return; end table.insert(event.form, { @@ -36,7 +39,7 @@ module:hook("muc-config-form", function(event) end, 100-5); module:hook("muc-config-submitted/muc#roomconfig_persistentroom", function(event) - if restrict_persistent and not um_is_admin(event.actor, module.host) then + if not module:may(":create-persistent-room", event.actor) then return; -- Not allowed end if set_persistent(event.room, event.value) then -- cgit v1.2.3 From 57ed2e28a09933dd6220dd6ddd84f0f442e9b71f Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Wed, 15 Jun 2022 14:22:26 +0100 Subject: teal-src: update module.d.tl with new access control methods --- teal-src/module.d.tl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/teal-src/module.d.tl b/teal-src/module.d.tl index cb7771e2..24eb9558 100644 --- a/teal-src/module.d.tl +++ b/teal-src/module.d.tl @@ -126,6 +126,11 @@ global record moduleapi path : string resource_path : string + -- access control + may : function (moduleapi, string, table|string) + default_permission : function (string, string) + default_permissions : function (string, { string }) + -- methods the module can add load : function () add_host : function (moduleapi) -- cgit v1.2.3 From c776c71066e22eff9f8c5b0401946513bd22f103 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 15 Jun 2022 23:03:15 +0200 Subject: core.moduleapi: Fixup method name `get_user_role()` did not exist anywhere else. MattJ said `get_user_default_role()` was indented --- core/moduleapi.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/moduleapi.lua b/core/moduleapi.lua index f0b412f3..0652e032 100644 --- a/core/moduleapi.lua +++ b/core/moduleapi.lua @@ -619,7 +619,7 @@ function api:may(action, context) local role; local node, host = jid_split(context); if host == self.host then - role = hosts[host].authz.get_user_role(node); + role = hosts[host].authz.get_user_default_role(node); else role = hosts[self.host].authz.get_jid_role(context); end -- cgit v1.2.3 From af339f0e66480da6825fd655a5bf35e2824cfc00 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 15 Jun 2022 23:04:17 +0200 Subject: core.moduleapi: Expand permission name ':' prefix earlier Ensures it applies to the context as string case Somehow this fixes everything --- core/moduleapi.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/core/moduleapi.lua b/core/moduleapi.lua index 0652e032..fe248c20 100644 --- a/core/moduleapi.lua +++ b/core/moduleapi.lua @@ -615,6 +615,9 @@ function api:default_permissions(role_name, permissions) end function api:may(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); @@ -638,9 +641,6 @@ function api:may(action, context) if not session then error("Unable to identify actor session from context"); end - if action:byte(1) == 58 then -- action begins with ':' - action = self.name..action; -- prepend module name - end if session.type == "s2sin" or (session.type == "c2s" and session.host ~= self.host) then local actor_jid = context.stanza.attr.from; local role = hosts[self.host].authz.get_jid_role(actor_jid); -- cgit v1.2.3 From 4db3d1572390ce5b615282cb1112358d9e3ba892 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Tue, 12 Jul 2022 13:14:47 +0100 Subject: usermanager, mod_auth_*: Add get_account_info() returning creation/update time This is useful for a number of things. For example, listing users that need to rotate their passwords after some event. It also provides a safer way for code to determine that a user password has changed without needing to set a handler for the password change event (which is a more fragile approach). --- core/usermanager.lua | 7 +++++++ plugins/mod_auth_internal_hashed.lua | 14 +++++++++++++- plugins/mod_auth_internal_plain.lua | 16 +++++++++++++++- 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/core/usermanager.lua b/core/usermanager.lua index 970140ef..23571fe7 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -116,6 +116,12 @@ local function set_password(username, password, host, resource) return ok, err; end +local function get_account_info(username, host) + local method = hosts[host].users.get_account_info; + if not method then return nil, "method-not-supported"; end + return method(username); +end + local function user_exists(username, host) if hosts[host].sessions[username] then return true; end return hosts[host].users.user_exists(username); @@ -211,6 +217,7 @@ return { test_password = test_password; get_password = get_password; set_password = set_password; + get_account_info = get_account_info; user_exists = user_exists; create_user = create_user; delete_user = delete_user; diff --git a/plugins/mod_auth_internal_hashed.lua b/plugins/mod_auth_internal_hashed.lua index cf851eef..397d82e9 100644 --- a/plugins/mod_auth_internal_hashed.lua +++ b/plugins/mod_auth_internal_hashed.lua @@ -86,11 +86,21 @@ function provider.set_password(username, password) account.server_key = server_key_hex account.password = nil; + account.updated = os.time(); return accounts:set(username, account); end return nil, "Account not available."; end +function provider.get_account_info(username) + local account = accounts:get(username); + if not account then return nil, "Account not available"; end + return { + created = account.created; + password_updated = account.updated; + }; +end + function provider.user_exists(username) local account = accounts:get(username); if not account then @@ -115,9 +125,11 @@ function provider.create_user(username, password) end local stored_key_hex = to_hex(stored_key); local server_key_hex = to_hex(server_key); + local now = os.time(); return accounts:set(username, { stored_key = stored_key_hex, server_key = server_key_hex, - salt = salt, iteration_count = default_iteration_count + salt = salt, iteration_count = default_iteration_count, + created = now, updated = now; }); end diff --git a/plugins/mod_auth_internal_plain.lua b/plugins/mod_auth_internal_plain.lua index 8a50e820..0f65323c 100644 --- a/plugins/mod_auth_internal_plain.lua +++ b/plugins/mod_auth_internal_plain.lua @@ -48,11 +48,21 @@ function provider.set_password(username, password) local account = accounts:get(username); if account then account.password = password; + account.updated = os.time(); return accounts:set(username, account); end return nil, "Account not available."; end +function provider.get_account_info(username) + local account = accounts:get(username); + if not account then return nil, "Account not available"; end + return { + created = account.created; + password_updated = account.updated; + }; +end + function provider.user_exists(username) local account = accounts:get(username); if not account then @@ -71,7 +81,11 @@ function provider.create_user(username, password) if not password then return nil, "Password fails SASLprep."; end - return accounts:set(username, {password = password}); + local now = os.time(); + return accounts:set(username, { + password = password; + created = now, updated = now; + }); end function provider.delete_user(username) -- cgit v1.2.3 From 77146aa4e7841eb5ca35cca28881d37eb53e5a96 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Tue, 19 Jul 2022 17:44:26 +0100 Subject: util.roles: Add new utility module to consolidate role objects and methods --- util/roles.lua | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 util/roles.lua diff --git a/util/roles.lua b/util/roles.lua new file mode 100644 index 00000000..831ce6ae --- /dev/null +++ b/util/roles.lua @@ -0,0 +1,100 @@ +local array = require "util.array"; +local it = require "util.iterators"; +local new_short_id = require "util.id".short; + +local role_methods = {}; +local role_mt = { + __index = role_methods; + __name = "role"; + __add = nil; +}; + +local function is_role(o) + local mt = getmetatable(o); + return mt == role_mt; +end + +local function _new_may(permissions, inherited_mays) + local n_inherited = inherited_mays and #inherited_mays; + return function (role, action, context) + -- Note: 'role' may be a descendent role, not only the one we're attached to + local policy = permissions[action]; + if policy ~= nil then + return policy; + end + if n_inherited then + for i = 1, n_inherited do + policy = inherited_mays[i](role, action, context); + if policy ~= nil then + return policy; + end + end + end + return false; + end +end + +local permissions_key = {}; + +-- { +-- Required: +-- name = "My fancy role"; +-- +-- Optional: +-- inherits = { role_obj... } +-- default = true +-- priority = 100 +-- permissions = { +-- ["foo"] = true; -- allow +-- ["bar"] = false; -- deny +-- } +-- } +local function new(base_config, overrides) + local config = setmetatable(overrides or {}, { __index = base_config }); + local permissions = {}; + local inherited_mays; + if config.inherits then + inherited_mays = array.pluck(config.inherits, "may"); + end + local new_role = { + id = new_short_id(); + name = config.name; + description = config.description; + default = config.default; + priority = config.priority; + may = _new_may(permissions, inherited_mays); + inherits = config.inherits; + [permissions_key] = permissions; + }; + local desired_permissions = config.permissions or config[permissions_key]; + for k, v in pairs(desired_permissions or {}) do + permissions[k] = v; + end + return setmetatable(new_role, role_mt); +end + +function role_methods:clone(overrides) + return new(self, overrides); +end + +function role_methods:set_permission(permission_name, policy, overwrite) + local permissions = self[permissions_key]; + if overwrite ~= true and permissions[permission_name] ~= nil and permissions[permission_name] ~= policy then + return false, "policy-already-exists"; + end + permissions[permission_name] = policy; + return true; +end + +function role_mt.__tostring(self) + return ("role<[%s] %s>"):format(self.id or "nil", self.name or "[no name]"); +end + +function role_mt.__pairs(self) + return it.filter(permissions_key, next, self); +end + +return { + is_role = is_role; + new = new; +}; -- cgit v1.2.3 From c0b857e5fb2a670d0a7a6ef29977ee58528e842f Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Tue, 19 Jul 2022 18:02:02 +0100 Subject: 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 | 2 +- core/usermanager.lua | 80 +++++++++++++-------- plugins/mod_authz_internal.lua | 159 +++++++++++++++++++++++++---------------- 3 files changed, 146 insertions(+), 95 deletions(-) diff --git a/core/sessionmanager.lua b/core/sessionmanager.lua index 426afa7b..924c4968 100644 --- a/core/sessionmanager.lua +++ b/core/sessionmanager.lua @@ -133,7 +133,7 @@ local function make_authenticated(session, username, role_name) 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 diff --git a/core/usermanager.lua b/core/usermanager.lua index 23571fe7..af82c940 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -10,8 +10,6 @@ local modulemanager = require "core.modulemanager"; 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 @@ local function get_provider(host) 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 - 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 +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 - return roles; + host = host or "*"; + + local authz_provider = (host ~= "*" and hosts[host].authz) or global_authz_provider; + 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 @@ local function get_jids_with_role(role, host) 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 @@ return { 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; }; diff --git a/plugins/mod_authz_internal.lua b/plugins/mod_authz_internal.lua index 35bc3929..135c7e61 100644 --- a/plugins/mod_authz_internal.lua +++ b/plugins/mod_authz_internal.lua @@ -3,62 +3,97 @@ local it = require "util.iterators"; 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 = { - ["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; - }; +local role_registry = {}; + +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" }; +}; + +register_role { + name = "prosody:admin"; + priority = 50; + inherits = { "prosody:user" }; }; --- 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: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; +-- 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 - 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; +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 - return false; end -- Public API @@ -69,6 +104,9 @@ local config_operator_role_set = { 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 @@ function get_user_roles(user) 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 @@ function get_jid_role(jid) 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 @@ function add_default_permission(role_name, action, policy) 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 -- cgit v1.2.3 From a0f2f9ee193826cfb595bf93e237e33a926214f0 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Wed, 20 Jul 2022 10:52:17 +0100 Subject: mod_tokenauth: New API that better fits how modules are using token auth This also updates the module to the new role API, and improves support for scope/role selection (currently treated as the same thing, which they almost are). --- plugins/mod_tokenauth.lua | 52 ++++++++++++++++++++++++++++++++++++----------- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/plugins/mod_tokenauth.lua b/plugins/mod_tokenauth.lua index c04a1aa4..6610036c 100644 --- a/plugins/mod_tokenauth.lua +++ b/plugins/mod_tokenauth.lua @@ -1,10 +1,19 @@ local id = require "util.id"; local jid = require "util.jid"; local base64 = require "util.encodings".base64; +local usermanager = require "core.usermanager"; +local generate_identifier = require "util.id".short; local token_store = module:open_store("auth_tokens", "map"); -function create_jid_token(actor_jid, token_jid, token_scope, token_ttl) +local function select_role(username, host, role) + if role then + return prosody.hosts[host].authz.get_role_by_name(role); + end + return usermanager.get_user_default_role(username, host); +end + +function create_jid_token(actor_jid, token_jid, token_role, token_ttl) token_jid = jid.prep(token_jid); if not actor_jid or token_jid ~= actor_jid and not jid.compare(token_jid, actor_jid) then return nil, "not-authorized"; @@ -21,13 +30,9 @@ function create_jid_token(actor_jid, token_jid, token_scope, token_ttl) created = os.time(); expires = token_ttl and (os.time() + token_ttl) or nil; jid = token_jid; - session = { - username = token_username; - host = token_host; - resource = token_resource; - auth_scope = token_scope; - }; + resource = token_resource; + role = token_role; }; local token_id = id.long(); @@ -46,11 +51,7 @@ local function parse_token(encoded_token) return token_id, token_user, token_host; end -function get_token_info(token) - local token_id, token_user, token_host = parse_token(token); - if not token_id then - return nil, "invalid-token-format"; - end +local function _get_parsed_token_info(token_id, token_user, token_host) if token_host ~= module.host then return nil, "invalid-host"; end @@ -70,6 +71,33 @@ function get_token_info(token) return token_info end +function get_token_info(token) + local token_id, token_user, token_host = parse_token(token); + if not token_id then + return nil, "invalid-token-format"; + end + return _get_parsed_token_info(token_id, token_user, token_host); +end + +function get_token_session(token, resource) + local token_id, token_user, token_host = parse_token(token); + if not token_id then + return nil, "invalid-token-format"; + end + + local token_info, err = _get_parsed_token_info(token_id, token_user, token_host); + if not token_info then return nil, err; end + + return { + username = token_user; + host = token_host; + resource = token_info.resource or resource or generate_identifier(); + + role = select_role(token_user, token_host, token_info.role); + }; +end + + function revoke_token(token) local token_id, token_user, token_host = parse_token(token); if not token_id then -- cgit v1.2.3 From 5f8e4414499e33a05dc8eda7a224e69c70bdb7a2 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 20 Jul 2022 13:05:35 +0200 Subject: moduleapi: Distribute permissions set from global modules to all hosts Roles and permissions will always happen in the context of a host. Prevents error upon indexing since `hosts["*"] == nil` --- core/moduleapi.lua | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/moduleapi.lua b/core/moduleapi.lua index fe248c20..0ec96fb6 100644 --- a/core/moduleapi.lua +++ b/core/moduleapi.lua @@ -604,6 +604,14 @@ end function api:default_permission(role_name, permission) permission = permission:gsub("^:", self.name..":"); + if self.host == "*" then + for _, host in pairs(hosts) do + if host.authz then + host.authz.add_default_permission(role_name, permission); + end + end + return + end hosts[self.host].authz.add_default_permission(role_name, permission); end -- cgit v1.2.3 From 55378f128acb817baab9fbeb412768f1a748a969 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 20 Jul 2022 13:07:04 +0200 Subject: moduleapi: Remove redundant expansion of ':' prefix in permission names --- core/moduleapi.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/core/moduleapi.lua b/core/moduleapi.lua index 0ec96fb6..c1d8851e 100644 --- a/core/moduleapi.lua +++ b/core/moduleapi.lua @@ -617,7 +617,6 @@ end function api:default_permissions(role_name, permissions) for _, permission in ipairs(permissions) do - permission = permission:gsub("^:", self.name..":"); self:default_permission(role_name, permission); end end -- cgit v1.2.3 From cdd5608f4a42e7740536d09eeda14451924c9657 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 20 Jul 2022 13:08:07 +0200 Subject: moduleapi: Stricter type check for actor in permission check Non-table but truthy values would trigger "attempt to index a foo value" on the next line otherwise --- core/moduleapi.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/moduleapi.lua b/core/moduleapi.lua index c1d8851e..8790a9d3 100644 --- a/core/moduleapi.lua +++ b/core/moduleapi.lua @@ -645,7 +645,7 @@ function api:may(action, context) end local session = context.origin or context.session; - if not session then + 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 -- cgit v1.2.3 From a88c982ae232f85bbf30401aa7690dbccc482866 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Wed, 20 Jul 2022 13:10:47 +0200 Subject: core.usermanager: Add missing stub authz methods to global authz provider Except, should we have a global authz provider at all? --- core/usermanager.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/core/usermanager.lua b/core/usermanager.lua index af82c940..ec782906 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -54,6 +54,13 @@ local global_authz_provider = { end; set_user_roles = function (user, roles) end; -- luacheck: ignore 212 set_jid_roles = function (jid, roles) end; -- luacheck: ignore 212 + + get_user_default_role = function (user) 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_info = function (role_name) end; -- luacheck: ignore 212 }; local provider_mt = { __index = new_null_provider() }; -- cgit v1.2.3 From b1f0061da37e72d12a71eb067ae103fe2ae6ca91 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Mon, 1 Aug 2022 20:26:00 +0100 Subject: usermanager: Handle local JIDs being passed to get/set_jid_role() There is no reasonable fallback for set_jid_role() because users may have multiple roles, so that's an error. --- core/usermanager.lua | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/core/usermanager.lua b/core/usermanager.lua index ec782906..09beb6d4 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -10,7 +10,7 @@ local modulemanager = require "core.modulemanager"; local log = require "util.logger".init("usermanager"); local type = type; local it = require "util.iterators"; -local jid_prep = require "util.jid".prep; +local jid_prep, jid_split = require "util.jid".prep, require "util.jid".split; local config = require "core.configmanager"; local sasl_new = require "util.sasl".new; local storagemanager = require "core.storagemanager"; @@ -196,12 +196,20 @@ end local function get_jid_role(jid, host) host = host or "*"; local authz_provider = (host ~= "*" and hosts[host].authz) or global_authz_provider; + local jid_node, jid_host = jid_split(jid); + if host == jid_host and jid_node then + return authz_provider.get_user_default_role(jid_node); + end 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; + local _, jid_host = jid_split(jid); + if host == jid_host then + return nil, "unexpected-local-jid"; + end return authz_provider.set_jid_role(jid, role_name) end -- cgit v1.2.3 From 341a5fd99fdee635257dd03d1f098a5a09338455 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Thu, 11 Aug 2022 16:47:09 +0100 Subject: features: Add "permissions" feature for role-auth --- core/features.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/features.lua b/core/features.lua index 7248f881..96023b09 100644 --- a/core/features.lua +++ b/core/features.lua @@ -4,5 +4,7 @@ return { available = set.new{ -- mod_bookmarks bundled "mod_bookmarks"; + -- Roles, module.may and per-session authz + "permissions"; }; }; -- cgit v1.2.3 From afbc47f8bafd599ab7cf7dc5dcac4931757b417a Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Thu, 11 Aug 2022 16:56:59 +0100 Subject: usermanager: Remove obsolete function from global authz provider --- core/usermanager.lua | 5 ----- 1 file changed, 5 deletions(-) diff --git a/core/usermanager.lua b/core/usermanager.lua index 09beb6d4..9adf1f25 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -43,11 +43,6 @@ local global_admins = set.new(global_admins_config) / jid_prep; local admin_role = { ["prosody:admin"] = true }; local global_authz_provider = { get_user_roles = function (user) end; --luacheck: ignore 212/user - get_jid_roles = function (jid) - if global_admins:contains(jid) then - return admin_role; - end - end; get_jids_with_role = function (role) if role ~= "prosody:admin" then return {}; end return it.to_array(global_admins); -- cgit v1.2.3 From bd2b2af7b736d9ef13be1228432443592fbaf6cd Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 12 Aug 2022 11:58:25 +0100 Subject: usermanager: Fix method name of global authz provider (thanks Zash) --- core/usermanager.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/usermanager.lua b/core/usermanager.lua index 9adf1f25..a299f9d9 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -55,7 +55,7 @@ local global_authz_provider = { 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_info = function (role_name) end; -- luacheck: ignore 212 + get_role_by_name = function (role_name) end; -- luacheck: ignore 212 }; local provider_mt = { __index = new_null_provider() }; -- cgit v1.2.3 From 7ccf41ebb5e7a1a21fdf5945c5dd157e40b7024c Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Fri, 12 Aug 2022 16:21:57 +0100 Subject: usermanager: Remove concept of global authz provider Rationale: - Removes a bunch of code! - We don't have many cases where an actor is not bound to one of our hosts - A notable exception is the admin shell, but if we ever attempt to lock those sessions down, there is a load of other work that also has to be done. And it's not clear if we would need a global authz provider for that anyway. - Removes an extra edge case from the necessary mental model for operators - Sessions that aren't bound to a host generally are anonymous or have an alternative auth model (such as by IP addres). - With the encapsulation now provided by util.roles, ad-hoc "detached roles" can still be created anyway by code that needs them. --- core/usermanager.lua | 58 +++++++++++----------------------------------------- 1 file changed, 12 insertions(+), 46 deletions(-) diff --git a/core/usermanager.lua b/core/usermanager.lua index a299f9d9..2311ce9e 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -9,12 +9,10 @@ local modulemanager = require "core.modulemanager"; local log = require "util.logger".init("usermanager"); local type = type; -local it = require "util.iterators"; -local jid_prep, jid_split = require "util.jid".prep, require "util.jid".split; +local jid_split = require "util.jid".split; local config = require "core.configmanager"; local sasl_new = require "util.sasl".new; local storagemanager = require "core.storagemanager"; -local set = require "util.set"; local prosody = _G.prosody; local hosts = prosody.hosts; @@ -34,19 +32,9 @@ local function new_null_provider() }); end -local global_admins_config = config.get("*", "admins"); -if type(global_admins_config) ~= "table" then - global_admins_config = nil; -- TODO: factor out moduleapi magic config handling and use it here -end -local global_admins = set.new(global_admins_config) / jid_prep; - -local admin_role = { ["prosody:admin"] = true }; -local global_authz_provider = { +local fallback_authz_provider = { get_user_roles = function (user) end; --luacheck: ignore 212/user - get_jids_with_role = function (role) - if role ~= "prosody:admin" then return {}; end - return it.to_array(global_admins); - end; + 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 @@ -66,7 +54,7 @@ local function initialize_host(host) local authz_provider_name = config.get(host, "authorization") or "internal"; local authz_mod = modulemanager.load(host, "authz_"..authz_provider_name); - host_session.authz = authz_mod or global_authz_provider; + host_session.authz = authz_mod or fallback_authz_provider; if host_session.type ~= "local" then return; end @@ -155,20 +143,14 @@ local function get_user_roles(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; - return authz_provider.get_user_roles(user); + return hosts[host].authz.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; - return authz_provider.get_user_default_role(user); + return hosts[host].authz.get_user_default_role(user); end -- Accepts a set of role names which the user is allowed to assume @@ -176,10 +158,7 @@ local function set_user_roles(user, host, roles) 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; - local ok, err = authz_provider.set_user_roles(user, roles); + local ok, err = hosts[host].authz.set_user_roles(user, roles); if ok then prosody.events.fire_event("user-roles-changed", { username = user, host = host @@ -189,50 +168,37 @@ local function set_user_roles(user, host, roles) end local function get_jid_role(jid, host) - host = host or "*"; - local authz_provider = (host ~= "*" and hosts[host].authz) or global_authz_provider; local jid_node, jid_host = jid_split(jid); if host == jid_host and jid_node then - return authz_provider.get_user_default_role(jid_node); + return hosts[host].authz.get_user_default_role(jid_node); end - return authz_provider.get_jid_role(jid); + return hosts[host].authz.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; local _, jid_host = jid_split(jid); if host == jid_host then return nil, "unexpected-local-jid"; end - return authz_provider.set_jid_role(jid, role_name) + return hosts[host].authz.set_jid_role(jid, role_name) end local function get_users_with_role(role, host) if not hosts[host] then return false; end if type(role) ~= "string" then return false; end - return hosts[host].authz.get_users_with_role(role); end local function get_jids_with_role(role, host) if host and not hosts[host] then return false; end if type(role) ~= "string" then return false; end - - host = host or "*"; - - local authz_provider = (host ~= "*" and hosts[host].authz) or global_authz_provider; - return authz_provider.get_jids_with_role(role); + return hosts[host].authz.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); + return hosts[host].authz.get_role_by_name(role_name); end return { -- cgit v1.2.3 From ae3a89375d7dcb1df54c072ddebe2a60248c58b0 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Mon, 15 Aug 2022 15:25:07 +0100 Subject: usermanager: Add back temporary is_admin to warn about deprecated API usage Goal: Introduce role-auth with minimal disruption is_admin() is unsafe in a system with per-session permissions, so it has been deprecated. Roll-out approach: 1) First, log a warning when is_admin() is used. It should continue to function normally, backed by the new role API. Nothing is really using per-session authz yet, so there is minimal security concern. The 'strict_deprecate_is_admin' global setting can be set to 'true' to force a hard failure of is_admin() attempts (it will log an error and always return false). 2) In some time (at least 1 week), but possibly longer depending on the number of affected deployments: switch 'strict_deprecate_is_admin' to 'true' by default. It can still be disabled for systems that need it. 3) Further in the future, before the next release, the option will be removed and is_admin() will be permanently disabled. --- core/usermanager.lua | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/core/usermanager.lua b/core/usermanager.lua index 2311ce9e..0a2f5c4d 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -21,6 +21,8 @@ local setmetatable = setmetatable; local default_provider = "internal_hashed"; +local debug = debug; + local _ENV = nil; -- luacheck: std none @@ -183,6 +185,20 @@ local function set_jid_role(jid, host, role_name) return hosts[host].authz.set_jid_role(jid, role_name) end +local strict_deprecate_is_admin; +local legacy_admin_roles = { ["prosody:admin"] = true, ["prosody:operator"] = true }; +local function is_admin(jid, host) + if strict_deprecate_is_admin == nil then + strict_deprecate_is_admin = (config.get("*", "strict_deprecate_is_admin") == true); + end + if strict_deprecate_is_admin then + log("error", "Attempt to use deprecated is_admin() API: %s", debug.traceback()); + return false; + end + log("warn", "Usage of legacy is_admin() API, which will be disabled in a future build: %s", debug.traceback()); + return legacy_admin_roles[get_jid_role(jid, host)] or false; +end + local function get_users_with_role(role, host) if not hosts[host] then return false; end if type(role) ~= "string" then return false; end @@ -222,4 +238,7 @@ return { set_jid_role = set_jid_role; get_jids_with_role = get_jids_with_role; get_role_by_name = get_role_by_name; + + -- Deprecated + is_admin = is_admin; }; -- cgit v1.2.3 From 1fac00b2affd58bcfbe47347280a406eccefb805 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Mon, 15 Aug 2022 16:36:00 +0200 Subject: mod_admin_shell: Show session role in c2s:show --- plugins/mod_admin_shell.lua | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua index 84ae0f72..bf682979 100644 --- a/plugins/mod_admin_shell.lua +++ b/plugins/mod_admin_shell.lua @@ -943,6 +943,15 @@ available_columns = { end end }; + role = { + title = "Role"; + description = "Session role"; + width = 20; + key = "role"; + mapper = function(role) + return role.name; + end; + } }; local function get_colspec(colspec, default) @@ -963,7 +972,7 @@ end function def_env.c2s:show(match_jid, colspec) local print = self.session.print; - local columns = get_colspec(colspec, { "id"; "jid"; "ipv"; "status"; "secure"; "smacks"; "csi" }); + local columns = get_colspec(colspec, { "id"; "jid"; "role"; "ipv"; "status"; "secure"; "smacks"; "csi" }); local row = format_table(columns, self.session.width); local function match(session) -- cgit v1.2.3 From 2b0676396dc84dad48735a9e3782bb4f13b36471 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Fri, 12 Aug 2022 22:09:09 +0200 Subject: util.roles: Add Teal interface declaration --- teal-src/util/roles.d.tl | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 teal-src/util/roles.d.tl diff --git a/teal-src/util/roles.d.tl b/teal-src/util/roles.d.tl new file mode 100644 index 00000000..fef4f88a --- /dev/null +++ b/teal-src/util/roles.d.tl @@ -0,0 +1,32 @@ +local record util_roles + + type context = any + + record Role + id : string + name : string + description : string + default : boolean + priority : number -- or integer? + permissions : { string : boolean } + + may : function (Role, string, context) + clone : function (Role, role_config) + set_permission : function (Role, string, boolean, boolean) + end + + is_role : function (any) : boolean + + record role_config + name : string + description : string + default : boolean + priority : number -- or integer? + inherits : { Role } + permissions : { string : boolean } + end + + new : function (role_config, Role) : Role +end + +return util_roles -- cgit v1.2.3 From f5768f63c993cee9f7f8e3c89db7e4e3080beab5 Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Wed, 17 Aug 2022 16:38:53 +0100 Subject: 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 --- core/moduleapi.lua | 3 +- core/sessionmanager.lua | 2 +- core/usermanager.lua | 68 ++++++++++++----- plugins/mod_authz_internal.lua | 166 ++++++++++++++++++++++++++++++++--------- plugins/mod_c2s.lua | 2 +- plugins/mod_tokenauth.lua | 2 +- 6 files changed, 184 insertions(+), 59 deletions(-) diff --git a/core/moduleapi.lua b/core/moduleapi.lua index 8790a9d3..73ce4911 100644 --- a/core/moduleapi.lua +++ b/core/moduleapi.lua @@ -538,6 +538,7 @@ function api:load_resource(path, mode) 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 @@ function api:may(action, context) 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 --git a/core/sessionmanager.lua b/core/sessionmanager.lua index 924c4968..dec21674 100644 --- a/core/sessionmanager.lua +++ b/core/sessionmanager.lua @@ -135,7 +135,7 @@ local function make_authenticated(session, username, role_name) 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 --git a/core/usermanager.lua b/core/usermanager.lua index 0a2f5c4d..cf54fc31 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -37,13 +37,17 @@ end 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_default_role = function (user) end; -- luacheck: ignore 212 - get_users_with_role = function (role_name) end; -- luacheck: ignore 212 + get_user_role = function (user) end; -- luacheck: ignore 212 + set_user_role = function (user, roles) 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) 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 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 @@ local function get_provider(host) 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 @@ return { 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 --git a/plugins/mod_authz_internal.lua b/plugins/mod_authz_internal.lua index 135c7e61..af402d3e 100644 --- a/plugins/mod_authz_internal.lua +++ b/plugins/mod_authz_internal.lua @@ -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 + + -- 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 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 - 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]; + 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 - return user_roles; + role_map_store:set(user, role_name, true); end -function set_user_roles(user, user_roles) - role_store:set(user, user_roles) - return true; +function remove_user_secondary_role(user, role_name) + role_map_store:set(user, role_name, nil); 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; +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 - if not default_role then return nil; end - return default_role; + 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 "")); + print(" Hint: try 'migrate'?"); + end +end diff --git a/plugins/mod_c2s.lua b/plugins/mod_c2s.lua index 8c0844ae..e8241687 100644 --- a/plugins/mod_c2s.lua +++ b/plugins/mod_c2s.lua @@ -259,7 +259,7 @@ local function disconnect_user_sessions(reason, leave_resource) 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 --git a/plugins/mod_tokenauth.lua b/plugins/mod_tokenauth.lua index 6610036c..85602747 100644 --- a/plugins/mod_tokenauth.lua +++ b/plugins/mod_tokenauth.lua @@ -10,7 +10,7 @@ local function select_role(username, host, role) 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) -- cgit v1.2.3 From f75ac951b518b04fb6b5f425950cfb2a8c8bb67b Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Thu, 18 Aug 2022 10:37:59 +0100 Subject: mod_authz_internal: Expose convenience method to test if user can assume role --- core/usermanager.lua | 8 ++++++++ plugins/mod_authz_internal.lua | 12 ++++++++++++ 2 files changed, 20 insertions(+) diff --git a/core/usermanager.lua b/core/usermanager.lua index cf54fc31..4f15c302 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -164,6 +164,13 @@ local function set_user_role(user, host, role_name) return role, err; end +local function user_can_assume_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.user_can_assume_role(user, role_name); +end + 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 @@ -260,6 +267,7 @@ return { get_provider = get_provider; get_user_role = get_user_role; set_user_role = set_user_role; + user_can_assume_role = user_can_assume_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; diff --git a/plugins/mod_authz_internal.lua b/plugins/mod_authz_internal.lua index af402d3e..4f88b176 100644 --- a/plugins/mod_authz_internal.lua +++ b/plugins/mod_authz_internal.lua @@ -181,6 +181,18 @@ function get_user_secondary_roles(user) return stored_roles; end +function user_can_assume_role(user, role_name) + local primary_role = get_user_role(user); + if primary_role and primary_role.role_name == role_name then + return true; + end + local secondary_roles = get_user_secondary_roles(user); + if secondary_roles and secondary_roles[role_name] then + return true; + end + return false; +end + -- This function is *expensive* function get_users_with_role(role_name) local function role_filter(username, default_role) --luacheck: ignore 212/username -- cgit v1.2.3 From 8bed557afd8266b7c5578608c35e973f42669772 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Thu, 18 Aug 2022 14:07:54 +0200 Subject: core.usermanager: Add scoped luacheck ignore rule to reduce clutter --- core/usermanager.lua | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/core/usermanager.lua b/core/usermanager.lua index 4f15c302..03b27327 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -35,21 +35,22 @@ local function new_null_provider() end local fallback_authz_provider = { - get_user_roles = function (user) end; --luacheck: ignore 212/user - get_jids_with_role = function (role) end; --luacheck: ignore 212 + -- luacheck: ignore 212 + get_user_roles = function (user) end; + get_jids_with_role = function (role) end; - get_user_role = function (user) end; -- luacheck: ignore 212 - set_user_role = function (user, roles) end; -- luacheck: ignore 212 + get_user_role = function (user) end; + set_user_role = function (user, roles) end; - 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 + add_user_secondary_role = function (user, host, role_name) end; + remove_user_secondary_role = function (user, host, role_name) end; - get_jid_role = function (jid) end; -- luacheck: ignore 212 - set_jid_role = function (jid, role) end; -- luacheck: ignore 212 + get_jid_role = function (jid) end; + set_jid_role = function (jid, role) end; - get_users_with_role = function (role_name) 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 + get_users_with_role = function (role_name) end; + add_default_permission = function (role_name, action, policy) end; + get_role_by_name = function (role_name) end; }; local provider_mt = { __index = new_null_provider() }; -- cgit v1.2.3 From 51b17f1c20421231995d9aecc73e95d7d3e55948 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Thu, 18 Aug 2022 14:10:21 +0200 Subject: core.usermanager: Add missing methods to fallback authz provider --- core/usermanager.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/core/usermanager.lua b/core/usermanager.lua index 03b27327..4bb63477 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -42,9 +42,12 @@ local fallback_authz_provider = { get_user_role = function (user) end; set_user_role = function (user, roles) end; + get_user_secondary_roles = function (user) end; add_user_secondary_role = function (user, host, role_name) end; remove_user_secondary_role = function (user, host, role_name) end; + user_can_assume_role = function(user, role_name) end; + get_jid_role = function (jid) end; set_jid_role = function (jid, role) end; -- cgit v1.2.3 From 1f4b94adabd686877059752453ecc4cb247b903d Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Thu, 18 Aug 2022 15:38:18 +0200 Subject: core.usermanager: Remove obsolete authz fallback method --- core/usermanager.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/core/usermanager.lua b/core/usermanager.lua index 4bb63477..f8514bc4 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -36,7 +36,6 @@ end local fallback_authz_provider = { -- luacheck: ignore 212 - get_user_roles = function (user) end; get_jids_with_role = function (role) end; get_user_role = function (user) end; -- cgit v1.2.3 From b608af36ed2d57505ce4ba603dece395a68f3b17 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Thu, 18 Aug 2022 15:42:07 +0200 Subject: core.usermanager: Update argument name in authz fallback method It's not plural --- core/usermanager.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/usermanager.lua b/core/usermanager.lua index f8514bc4..2c469269 100644 --- a/core/usermanager.lua +++ b/core/usermanager.lua @@ -39,7 +39,7 @@ local fallback_authz_provider = { get_jids_with_role = function (role) end; get_user_role = function (user) end; - set_user_role = function (user, roles) end; + set_user_role = function (user, role_name) end; get_user_secondary_roles = function (user) end; add_user_secondary_role = function (user, host, role_name) end; -- cgit v1.2.3 From 4db3f8cf46824bd682cbf764369ed474d804f96b Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Thu, 18 Aug 2022 16:46:07 +0100 Subject: mod_admin_shell: Update with new role management commands and help text --- plugins/mod_admin_shell.lua | 91 ++++++++++++++++++++++++++------------------- 1 file changed, 52 insertions(+), 39 deletions(-) diff --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua index bf682979..dcbb4d09 100644 --- a/plugins/mod_admin_shell.lua +++ b/plugins/mod_admin_shell.lua @@ -271,20 +271,19 @@ function commands.help(session, data) print [[user:create(jid, password, roles) - Create the specified user account]] print [[user:password(jid, password) - Set the password for the specified user account]] print [[user:roles(jid, host) - Show current roles for an user]] - print [[user:setroles(jid, host, roles) - Set roles for an user (see 'help roles')]] + print [[user:setrole(jid, host, role) - Set primary role of a user (see 'help roles')]] + print [[user:addrole(jid, host, role) - Add a secondary role to a user]] + print [[user:delrole(jid, host, role) - Remove a secondary role from a user]] print [[user:delete(jid) - Permanently remove the specified user account]] print [[user:list(hostname, pattern) - List users on the specified host, optionally filtering with a pattern]] elseif section == "roles" then print [[Roles may grant access or restrict users from certain operations]] print [[Built-in roles are:]] - print [[ prosody:admin - Administrator]] - print [[ (empty set) - Normal user]] + print [[ prosody:user - Normal user (default)]] + print [[ prosody:admin - Host administrator]] + print [[ prosody:operator - Server administrator]] print [[]] - print [[The canonical role format looks like: { ["example:role"] = true }]] - print [[For convenience, the following formats are also accepted:]] - print [["admin" - short for "prosody:admin", the normal admin status (like the admins config option)]] - print [["example:role" - short for {["example:role"]=true}]] - print [[{"example:role"} - short for {["example:role"]=true}]] + print [[Roles can be assigned using the user management commands (see 'help user').]] elseif section == "muc" then -- TODO `muc:room():foo()` commands print [[muc:create(roomjid, { config }) - Create the specified MUC room with the given config]] @@ -1383,15 +1382,8 @@ end local um = require"core.usermanager"; -local function coerce_roles(roles) - if roles == "admin" then roles = "prosody:admin"; end - if type(roles) == "string" then roles = { [roles] = true }; end - if roles[1] then for i, role in ipairs(roles) do roles[role], roles[i] = true, nil; end end - return roles; -end - def_env.user = {}; -function def_env.user:create(jid, password, roles) +function def_env.user:create(jid, password, role) local username, host = jid_split(jid); if not prosody.hosts[host] then return nil, "No such host: "..host; @@ -1400,10 +1392,9 @@ function def_env.user:create(jid, password, roles) end local ok, err = um.create_user(username, password, host); if ok then - if ok and roles then - roles = coerce_roles(roles); - local roles_ok, rerr = um.set_roles(jid, host, roles); - if not roles_ok then return nil, "User created, but could not set roles: " .. tostring(rerr); end + if ok and role then + local role_ok, rerr = um.set_user_role(jid, host, role); + if not role_ok then return nil, "User created, but could not set role: " .. tostring(rerr); end end return true, "User created"; else @@ -1441,41 +1432,63 @@ function def_env.user:password(jid, password) end end -function def_env.user:roles(jid, host, new_roles) - if new_roles or type(host) == "table" then - return nil, "Use user:setroles(jid, host, roles) to change user roles"; - end +function def_env.user:role(jid, host) local username, userhost = jid_split(jid); if host == nil then host = userhost; end - if host ~= "*" and not prosody.hosts[host] then + if not prosody.hosts[host] then return nil, "No such host: "..host; elseif prosody.hosts[userhost] and not um.user_exists(username, userhost) then return nil, "No such user"; end - local roles = um.get_roles(jid, host); - if not roles then return true, "No roles"; end - local count = 0; - local print = self.session.print; - for role in pairs(roles) do + + local primary_role = um.get_user_role(username, host); + local secondary_roles = um.get_user_secondary_roles(username, host); + + print(primary_role and primary_role.name or ""); + + local count = primary_role and 1 or 0; + for role_name in pairs(secondary_roles or {}) do count = count + 1; - print(role); + print(role_name.." (secondary)"); end + return true, count == 1 and "1 role" or count.." roles"; end -def_env.user.showroles = def_env.user.roles; -- COMPAT +def_env.user.roles = def_env.user.role; --- user:roles("someone@example.com", "example.com", {"prosody:admin"}) --- user:roles("someone@example.com", {"prosody:admin"}) -function def_env.user:setroles(jid, host, new_roles) +-- user:setrole("someone@example.com", "example.com", "prosody:admin") +-- user:setrole("someone@example.com", "prosody:admin") +function def_env.user:setrole(jid, host, new_role) local username, userhost = jid_split(jid); - if new_roles == nil then host, new_roles = userhost, host; end - if host ~= "*" and not prosody.hosts[host] then + if new_role == nil then host, new_role = userhost, host; end + if not prosody.hosts[host] then + return nil, "No such host: "..host; + elseif prosody.hosts[userhost] and not um.user_exists(username, userhost) then + return nil, "No such user"; + end + return um.set_user_role(username, host, new_role); +end + +function def_env.user:addrole(jid, host, new_role) + local username, userhost = jid_split(jid); + if new_role == nil then host, new_role = userhost, host; end + if not prosody.hosts[host] then + return nil, "No such host: "..host; + elseif prosody.hosts[userhost] and not um.user_exists(username, userhost) then + return nil, "No such user"; + end + return um.add_user_secondary_role(username, host, new_role); +end + +function def_env.user:delrole(jid, host, role_name) + local username, userhost = jid_split(jid); + if role_name == nil then host, role_name = userhost, host; end + if not prosody.hosts[host] then return nil, "No such host: "..host; elseif prosody.hosts[userhost] and not um.user_exists(username, userhost) then return nil, "No such user"; end - if host == "*" then host = nil; end - return um.set_roles(jid, host, coerce_roles(new_roles)); + return um.remove_user_secondary_role(username, host, role_name); end -- TODO switch to table view, include roles -- cgit v1.2.3 From 8ff2f04e4ce842ae70b0edfaef1d237dc69d6dec Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Thu, 18 Aug 2022 17:50:56 +0200 Subject: mod_auth_internal_hashed: Allow creating disabled account without password Otherwise, create_user(username, nil) leads to the account being deleted. --- plugins/mod_auth_internal_hashed.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/mod_auth_internal_hashed.lua b/plugins/mod_auth_internal_hashed.lua index 397d82e9..ddff31e9 100644 --- a/plugins/mod_auth_internal_hashed.lua +++ b/plugins/mod_auth_internal_hashed.lua @@ -115,8 +115,9 @@ function provider.users() end function provider.create_user(username, password) + local now = os.time(); if password == nil then - return accounts:set(username, {}); + return accounts:set(username, { created = now; updated = now; disabled = true }); end local salt = generate_uuid(); local valid, stored_key, server_key = get_auth_db(password, salt, default_iteration_count); @@ -125,7 +126,6 @@ function provider.create_user(username, password) end local stored_key_hex = to_hex(stored_key); local server_key_hex = to_hex(server_key); - local now = os.time(); return accounts:set(username, { stored_key = stored_key_hex, server_key = server_key_hex, salt = salt, iteration_count = default_iteration_count, -- cgit v1.2.3 From 6f11c198b30fc581a2de25cdb3fe0b29c1d48eda Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Thu, 18 Aug 2022 18:10:18 +0200 Subject: mod_admin_shell: Update help for user:create to reflect singular role argument --- plugins/mod_admin_shell.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua index dcbb4d09..087b8768 100644 --- a/plugins/mod_admin_shell.lua +++ b/plugins/mod_admin_shell.lua @@ -268,7 +268,7 @@ function commands.help(session, data) print [[host:deactivate(hostname) - Disconnects all clients on this host and deactivates]] print [[host:list() - List the currently-activated hosts]] elseif section == "user" then - print [[user:create(jid, password, roles) - Create the specified user account]] + print [[user:create(jid, password, role) - Create the specified user account]] print [[user:password(jid, password) - Set the password for the specified user account]] print [[user:roles(jid, host) - Show current roles for an user]] print [[user:setrole(jid, host, role) - Set primary role of a user (see 'help roles')]] -- cgit v1.2.3 From 742153c55540bd9de365e775bd71c5c4544d88f8 Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Thu, 18 Aug 2022 18:10:44 +0200 Subject: mod_auth_insecure: Store creation and update timestamps on account This ensures that the store is not empty in case no password is provided, so the underlying data storage won't consider the store empty. --- plugins/mod_auth_insecure.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/mod_auth_insecure.lua b/plugins/mod_auth_insecure.lua index dc5ee616..5428d1fa 100644 --- a/plugins/mod_auth_insecure.lua +++ b/plugins/mod_auth_insecure.lua @@ -27,6 +27,7 @@ function provider.set_password(username, password) return nil, "Password fails SASLprep."; end if account then + account.updated = os.time(); account.password = password; return datamanager.store(username, host, "accounts", account); end @@ -38,7 +39,8 @@ function provider.user_exists(username) end function provider.create_user(username, password) - return datamanager.store(username, host, "accounts", {password = password}); + local now = os.time(); + return datamanager.store(username, host, "accounts", { created = now; updated = now; password = password }); end function provider.delete_user(username) -- cgit v1.2.3 From 96e172167d9b0d135d2937a83b252700f458f4fe Mon Sep 17 00:00:00 2001 From: Kim Alvefur Date: Thu, 18 Aug 2022 19:00:01 +0200 Subject: mod_admin_shell: Ensure account has role before it is usable By creating the account first without a password it can't be used until the role has set. This is most important for restricted accounts, as a failure to set the role would lead to the account having more privileges than indented. --- plugins/mod_admin_shell.lua | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/plugins/mod_admin_shell.lua b/plugins/mod_admin_shell.lua index 087b8768..49e07dae 100644 --- a/plugins/mod_admin_shell.lua +++ b/plugins/mod_admin_shell.lua @@ -1390,16 +1390,24 @@ function def_env.user:create(jid, password, role) elseif um.user_exists(username, host) then return nil, "User exists"; end - local ok, err = um.create_user(username, password, host); - if ok then - if ok and role then - local role_ok, rerr = um.set_user_role(jid, host, role); - if not role_ok then return nil, "User created, but could not set role: " .. tostring(rerr); end - end - return true, "User created"; - else + local ok, err = um.create_user(username, nil, host); + if not ok then return nil, "Could not create user: "..err; end + + if role then + local role_ok, rerr = um.set_user_role(jid, host, role); + if not role_ok then + return nil, "Could not set role: " .. tostring(rerr); + end + end + + local ok, err = um.set_password(username, password, host, nil); + if not ok then + return nil, "Could not set password for user: "..err; + end + + return true, "User created"; end function def_env.user:delete(jid) -- cgit v1.2.3 From 227f6c033697210a54f671f5b9128cde8699fdcd Mon Sep 17 00:00:00 2001 From: Matthew Wild Date: Mon, 22 Aug 2022 13:03:10 +0100 Subject: CHANGES: Add role auth --- CHANGES | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGES b/CHANGES index 3eafb718..d96c5d63 100644 --- a/CHANGES +++ b/CHANGES @@ -18,6 +18,7 @@ TRUNK - Advertise supported SASL Channel-Binding types (XEP-0440) - Implement RFC 9266 'tls-exporter' channel binding with TLS 1.3 +- New role and permissions framework and API ## Changes -- cgit v1.2.3