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(-) (limited to 'plugins') 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. --- 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 +-- 11 files changed, 191 insertions(+), 122 deletions(-) (limited to 'plugins') 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 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). --- plugins/mod_auth_internal_hashed.lua | 14 +++++++++++++- plugins/mod_auth_internal_plain.lua | 16 +++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) (limited to 'plugins') 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 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). --- plugins/mod_authz_internal.lua | 159 +++++++++++++++++++++++++---------------- 1 file changed, 96 insertions(+), 63 deletions(-) (limited to 'plugins') 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(-) (limited to 'plugins') 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 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(-) (limited to 'plugins') 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 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 --- plugins/mod_authz_internal.lua | 166 ++++++++++++++++++++++++++++++++--------- plugins/mod_c2s.lua | 2 +- plugins/mod_tokenauth.lua | 2 +- 3 files changed, 132 insertions(+), 38 deletions(-) (limited to 'plugins') 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 --- plugins/mod_authz_internal.lua | 12 ++++++++++++ 1 file changed, 12 insertions(+) (limited to 'plugins') 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 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(-) (limited to 'plugins') 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(-) (limited to 'plugins') 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(-) (limited to 'plugins') 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(-) (limited to 'plugins') 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(-) (limited to 'plugins') 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