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. --- core/moduleapi.lua | 63 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) (limited to 'core/moduleapi.lua') 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; -- 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(-) (limited to 'core/moduleapi.lua') 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(-) (limited to 'core/moduleapi.lua') 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 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(+) (limited to 'core/moduleapi.lua') 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(-) (limited to 'core/moduleapi.lua') 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(-) (limited to 'core/moduleapi.lua') 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 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 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'core/moduleapi.lua') 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 -- cgit v1.2.3