Changeset

5008:bd63feda3704

Merge role-auth
author Matthew Wild <mwild1@gmail.com>
date Mon, 22 Aug 2022 15:39:02 +0100
parents 4994:cce12a660b98 (current diff) 5007:34fb3d239ac1 (diff)
children 5009:459a4001c1d9
files
diffstat 20 files changed, 173 insertions(+), 50 deletions(-) [+]
line wrap: on
line diff
--- a/.luacheckrc	Tue Aug 16 13:10:39 2022 +0200
+++ b/.luacheckrc	Mon Aug 22 15:39:02 2022 +0100
@@ -29,6 +29,8 @@
 	"module.hourly",
 	"module.broadcast",
 	"module.context",
+	"module.default_permission",
+	"module.default_permissions",
 	"module.depends",
 	"module.fire_event",
 	"module.get_directory",
@@ -54,6 +56,7 @@
 	"module.load_resource",
 	"module.log",
 	"module.log_status",
+	"module.may",
 	"module.measure",
 	"module.metric",
 	"module.open_store",
--- a/mod_admin_message/README.markdown	Tue Aug 16 13:10:39 2022 +0200
+++ b/mod_admin_message/README.markdown	Mon Aug 22 15:39:02 2022 +0100
@@ -29,7 +29,7 @@
 =============
 
   --------- ---------------
-  trunk     Works
+  trunk     Doesn't work (uses is_admin)
   0.9       Works
   \<= 0.8   Not supported
   --------- ---------------
--- a/mod_admin_probe/README.markdown	Tue Aug 16 13:10:39 2022 +0200
+++ b/mod_admin_probe/README.markdown	Mon Aug 22 15:39:02 2022 +0100
@@ -5,3 +5,11 @@
 This module lets server administrators send `<presence type="probe"/>`
 to any local user and receive their presence in response, bypassing
 roster checks.
+
+Compatibility
+=============
+
+  ------- --------------
+  trunk   Doesn't work (uses is_admin)
+  0.12    Works?
+  ------- --------------
--- a/mod_block_outgoing/README.markdown	Tue Aug 16 13:10:39 2022 +0200
+++ b/mod_block_outgoing/README.markdown	Mon Aug 22 15:39:02 2022 +0100
@@ -26,3 +26,13 @@
 ```
 
 block_outgoing_stanzas defaults to "message" if not specified.
+
+Compatibility
+=============
+
+  ------- --------------
+  trunk   Doesn't work (uses is_admin)
+  0.12    Works
+  0.11    Works
+  ------- --------------
+
--- a/mod_broadcast/README.markdown	Tue Aug 16 13:10:39 2022 +0200
+++ b/mod_broadcast/README.markdown	Mon Aug 22 15:39:02 2022 +0100
@@ -32,4 +32,5 @@
   ------ -------
   0.9    Works
   0.10   Works
+  trunk  Doesn't work (uses is_admin)
   ------ -------
--- a/mod_cloud_notify/mod_cloud_notify.lua	Tue Aug 16 13:10:39 2022 +0200
+++ b/mod_cloud_notify/mod_cloud_notify.lua	Mon Aug 22 15:39:02 2022 +0100
@@ -390,7 +390,7 @@
 				notification_stanza = push_publish;
 				notification_payload = push_notification_payload;
 				original_stanza = stanza;
-				node = node;
+				username = node;
 				push_info = push_info;
 				push_summary = form_data;
 				important = not not form_data["last-message-body"];
--- a/mod_data_access/README.markdown	Tue Aug 16 13:10:39 2022 +0200
+++ b/mod_data_access/README.markdown	Mon Aug 22 15:39:02 2022 +0100
@@ -72,3 +72,11 @@
 ### TODO
 
 -   Use `Accept` header.
+
+Compatibility
+=============
+
+  ------- --------------
+  trunk   Doesn't work (uses is_admin)
+  0.12    Works?
+  ------- --------------
--- a/mod_firewall/README.markdown	Tue Aug 16 13:10:39 2022 +0200
+++ b/mod_firewall/README.markdown	Mon Aug 22 15:39:02 2022 +0100
@@ -435,8 +435,40 @@
     NOT SENT DIRECTED PRESENCE TO SENDER?
     BOUNCE=service-unavailable
 
+### Permissions
+
+Rules can consult Prosody's internal role and permissions system to check whether a certain action may
+be performed. The acting entity, their role, and appropriate context is automatically inferred. All you
+need to do is provide the identifier of the permission that should be checked.
+
+  Condition               Description
+  ----------------------- --------------------------------------------------------------------
+  `MAY=permission`        Checks whether 'permission' is allowed in the current context.
+
+As with all other conditions, `MAY` can be combined with `NOT` to negate the result of the check.
+
+Example, blocking outgoing stanzas from users with roles that do not allow the 'xmpp:federate' permission:
+
+```
+::deliver_remote
+MAY NOT: xmpp:federate
+BOUNCE=policy-violation (You are not allowed access to the federation)
+```
+
+### Roles
+
+  Condition        Matches
+  ---------------- -------------------------------------------------------------------------------------
+  `TO ROLE`       When the recipient JID of the stanza has the named role
+  `FROM ROLE`     When the sender JID of the stanza has the named role
+
+**Note:** In most cases, you should avoid checking for specific roles, and instead check for
+permissions granted by those roles (using the 'MAY' condition).
+
 ### Admins
 
+**Deprecated:** These conditions should no longer be used. Prefer 'MAY', 'TO ROLE' or 'FROM ROLE'.
+
 Prosody allows certain JIDs to be declared as administrators of a host, component or the whole server.
 
   Condition        Matches
--- a/mod_firewall/conditions.lib.lua	Tue Aug 16 13:10:39 2022 +0200
+++ b/mod_firewall/conditions.lib.lua	Mon Aug 22 15:39:02 2022 +0100
@@ -175,22 +175,39 @@
 	return "not "..table.concat(code, " or "), { "group_contains", "bare_to", "bare_from" };
 end
 
+-- COMPAT w/0.12: Deprecated
 function condition_handlers.FROM_ADMIN_OF(host)
 	return ("is_admin(bare_from, %s)"):format(host ~= "*" and metaq(host) or nil), { "is_admin", "bare_from" };
 end
 
+-- COMPAT w/0.12: Deprecated
 function condition_handlers.TO_ADMIN_OF(host)
 	return ("is_admin(bare_to, %s)"):format(host ~= "*" and metaq(host) or nil), { "is_admin", "bare_to" };
 end
 
+-- COMPAT w/0.12: Deprecated
 function condition_handlers.FROM_ADMIN()
 	return ("is_admin(bare_from, current_host)"), { "is_admin", "bare_from", "current_host" };
 end
 
+-- COMPAT w/0.12: Deprecated
 function condition_handlers.TO_ADMIN()
 	return ("is_admin(bare_to, current_host)"), { "is_admin", "bare_to", "current_host" };
 end
 
+-- MAY: permission_to_check
+function condition_handlers.MAY(permission_to_check)
+	return ("module:may(%q, event)"):format(permission_to_check);
+end
+
+function condition_handlers.TO_ROLE(role_name)
+	return ("get_jid_role(bare_to, current_host) == %q"):format(role_name), { "get_jid_role", "current_host", "bare_to" };
+end
+
+function condition_handlers.FROM_ROLE(role_name)
+	return ("get_jid_role(bare_from, current_host) == %q"):format(role_name), { "get_jid_role", "current_host", "bare_from" };
+end
+
 local day_numbers = { sun = 0, mon = 2, tue = 3, wed = 4, thu = 5, fri = 6, sat = 7 };
 
 local function current_time_check(op, hour, minute)
--- a/mod_firewall/mod_firewall.lua	Tue Aug 16 13:10:39 2022 +0200
+++ b/mod_firewall/mod_firewall.lua	Mon Aug 22 15:39:02 2022 +0100
@@ -6,6 +6,9 @@
 local it = require "util.iterators";
 local set = require "util.set";
 
+local have_features, features = pcall(require, "core.features");
+features = have_features and features.available or set.new();
+
 -- [definition_type] = definition_factory(param)
 local definitions = module:shared("definitions");
 
@@ -181,7 +184,8 @@
 	group_contains = {
 		global_code = [[local group_contains = module:depends("groups").group_contains]];
 	};
-	is_admin = { global_code = [[local is_admin = require "core.usermanager".is_admin;]]};
+	is_admin = features:contains("permissions") and { global_code = [[local is_admin = require "core.usermanager".is_admin;]]} or nil;
+	get_jid_role = require "core.usermanager".get_jid_role and { global_code = [[local get_jid_role = require "core.usermanager".get_jid_role;]] } or nil;
 	core_post_stanza = { global_code = [[local core_post_stanza = prosody.core_post_stanza;]] };
 	zone = { global_code = function (zone)
 		local var = zone;
--- a/mod_http_admin_api/mod_http_admin_api.lua	Tue Aug 16 13:10:39 2022 +0200
+++ b/mod_http_admin_api/mod_http_admin_api.lua	Mon Aug 22 15:39:02 2022 +0100
@@ -1,5 +1,6 @@
 local usermanager = require "core.usermanager";
 
+local it = require "util.iterators";
 local json = require "util.json";
 local st = require "util.stanza";
 local array = require "util.array";
@@ -33,25 +34,24 @@
 	end
 
 	if auth_type == "Bearer" then
-		local token_info = tokens.get_token_info(auth_data);
-		if not token_info or not token_info.session then
-			return false;
-		end
-		return token_info.session;
+		return tokens.get_token_session(auth_data);
 	end
 	return nil;
 end
 
+module:default_permission("prosody:admin", ":access-admin-api");
+
 function check_auth(routes)
 	local function check_request_auth(event)
 		local session = check_credentials(event.request);
 		if not session then
 			event.response.headers.authorization = www_authenticate_header;
 			return false, 401;
-		elseif session.auth_scope ~= "prosody:scope:admin" then
+		end
+		event.session = session;
+		if not module:may(":access-admin-api", event) then
 			return false, 403;
 		end
-		event.session = session;
 		return true;
 	end
 
@@ -179,21 +179,24 @@
 		end
 	end
 
-	local roles = nil;
-	if usermanager.get_roles then
-		local roles_map = usermanager.get_roles(username.."@"..module.host, module.host)
-		roles = array()
-		if roles_map then
-			for role in pairs(roles_map) do
-				roles:push(role)
-			end
+	local primary_role, secondary_roles, legacy_roles;
+	if usermanager.get_user_role then
+		primary_role = usermanager.get_user_role(username, module.host);
+		secondary_roles = array.collect(it.keys(usermanager.get_user_secondary_roles(username, module.host)));
+	elseif usermanager.get_user_roles then -- COMPAT w/0.12
+		legacy_roles = array();
+		local roles_map = usermanager.get_user_roles(username, module.host);
+		for role_name in pairs(roles_map) do
+			legacy_roles:push(role_name);
 		end
 	end
 
 	return {
 		username = username;
 		display_name = display_name;
-		roles = roles;
+		role = primary_role and primary_role.name or nil;
+		secondary_roles = secondary_roles;
+		roles = legacy_roles; -- COMPAT w/0.12
 	};
 end
 
@@ -309,7 +312,7 @@
 	};
 	-- Online sessions
 	do
-		local user_sessions = hosts[module.host].sessions[username];
+		local user_sessions = prosody.hosts[module.host].sessions[username];
 		if user_sessions then
 			user_sessions = user_sessions.sessions
 		end
@@ -415,8 +418,18 @@
 		end
 	end
 
-	if new_user.roles then
-		if not usermanager.set_roles then
+	if new_user.role then
+		if not usermanager.set_user_role then
+			return 500, "feature-not-implemented";
+		end
+		if not usermanager.set_user_role(username, module.host, new_user.role) then
+			module:log("error", "failed to set role %s for %s", new_user.role, username);
+			return 500;
+		end
+	end
+
+	if new_user.roles then -- COMPAT w/0.12
+		if not usermanager.set_user_roles then
 			return 500, "feature-not-implemented"
 		end
 
@@ -425,7 +438,7 @@
 			backend_roles[role] = true;
 		end
 		local jid = username.."@"..module.host;
-		if not usermanager.set_roles(jid, module.host, backend_roles) then
+		if not usermanager.set_user_roles(username, module.host, backend_roles) then
 			module:log("error", "failed to set roles %q for %s", backend_roles, jid)
 			return 500
 		end
--- a/mod_http_oauth2/mod_http_oauth2.lua	Tue Aug 16 13:10:39 2022 +0200
+++ b/mod_http_oauth2/mod_http_oauth2.lua	Mon Aug 22 15:39:02 2022 +0100
@@ -14,13 +14,20 @@
 
 local clients = module:open_store("oauth2_clients", "map");
 
-local function filter_scopes(request_jid, requested_scope_string) --luacheck: ignore 212/requested_scope_string
-	-- We currently don't really support scopes, so override
-	-- to whatever real permissions the user has
-	if usermanager.is_admin(request_jid, module.host) then
-		return "prosody:scope:admin";
+local function filter_scopes(username, host, requested_scope_string)
+	if host ~= module.host then
+		return usermanager.get_jid_role(username.."@"..host, module.host).name;
 	end
-	return "prosody:scope:default";
+
+	if requested_scope_string then -- Specific role requested
+		-- TODO: The requested scope string is technically a space-delimited list
+		-- of scopes, but for simplicity we're mapping this slot to role names.
+		if usermanager.user_can_assume_role(username, module.host, requested_scope_string) then
+			return requested_scope_string;
+		end
+	end
+
+	return usermanager.get_user_role(username, module.host).name;
 end
 
 local function code_expires_in(code)
@@ -81,7 +88,7 @@
 	end
 
 	local granted_jid = jid.join(request_username, request_host, request_resource);
-	local granted_scopes = filter_scopes(granted_jid, params.scope);
+	local granted_scopes = filter_scopes(request_username, request_host, params.scope);
 	return json.encode(new_access_token(granted_jid, granted_scopes, nil));
 end
 
@@ -99,7 +106,7 @@
 		return oauth_error("invalid_client", "incorrect credentials");
 	end
 
-	local granted_scopes = filter_scopes(granted_jid, params.scope);
+	local granted_scopes = filter_scopes(client_owner, client_host, params.scope);
 
 	local code = uuid.generate();
 	local ok = codes:set(params.client_id .. "#" .. code, {
--- a/mod_http_xep227/mod_http_xep227.lua	Tue Aug 16 13:10:39 2022 +0200
+++ b/mod_http_xep227/mod_http_xep227.lua	Mon Aug 22 15:39:02 2022 +0100
@@ -346,11 +346,7 @@
 	end
 
 	if auth_type == "Bearer" then
-		local token_info = tokens.get_token_info(auth_data);
-		if not token_info or not token_info.session then
-			return false;
-		end
-		return token_info.session;
+		return tokens.get_token_session(auth_data);
 	end
 	return nil;
 end
--- a/mod_invites_adhoc/mod_invites_adhoc.lua	Tue Aug 16 13:10:39 2022 +0200
+++ b/mod_invites_adhoc/mod_invites_adhoc.lua	Mon Aug 22 15:39:02 2022 +0100
@@ -13,9 +13,19 @@
 -- on the server, use the option above instead.
 local allow_contact_invites = module:get_option_boolean("allow_contact_invites", true);
 
+-- These options are deprecated since module:may()
 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");
 
+if module.may then
+	if allow_user_invites then
+		module:default_permission("prosody:user", ":invite-new-users");
+	end
+	if not allow_user_invite_roles:empty() or not deny_user_invite_roles:empty() then
+		return error("allow_user_invites_by_roles and deny_user_invites_by_roles are deprecated options");
+	end
+end
+
 local invites;
 if prosody.shutdown then -- COMPAT hack to detect prosodyctl
 	invites = module:depends("invites");
@@ -42,8 +52,10 @@
 
 -- 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 function may_invite_new_users(jid, context)
+	if module.may then
+		return module:may(":invite-new-users", context);
+	elseif usermanager.get_roles then -- COMPAT w/0.12
 		local user_roles = usermanager.get_roles(jid, module.host);
 		if not user_roles then return; end
 		if user_roles["prosody:admin"] then
@@ -87,7 +99,7 @@
 					};
 				};
 			end
-			local invite = invites.create_contact(username, may_invite_new_users(data.from), {
+			local invite = invites.create_contact(username, may_invite_new_users(data.from, data), {
 				source = data.from
 			});
 			--TODO: check errors
--- a/mod_isolate_host/mod_isolate_host.lua	Tue Aug 16 13:10:39 2022 +0200
+++ b/mod_isolate_host/mod_isolate_host.lua	Mon Aug 22 15:39:02 2022 +0100
@@ -1,6 +1,5 @@
 local jid = require "util.jid";
-local jid_bare, jid_split = jid.bare, jid.split;
-local is_admin = require "core.usermanager".is_admin;
+local jid_bare, jid_host = jid.bare, jid.host;
 local set = require "util.set";
 local st = require "util.stanza";
 
@@ -10,10 +9,14 @@
 local except_domains = module:get_option_inherited_set("isolate_except_domains", {});
 local except_users = module:get_option_inherited_set("isolate_except_users", {});
 
+if not module.may then
+	module:depends("compat_roles");
+end
+
 function check_stanza(event)
 	local origin, stanza = event.origin, event.stanza;
 	if origin.no_host_isolation then return; end
-	local to_user, to_host = jid_split(event.stanza.attr.to);
+	local to_host = jid_host(event.stanza.attr.to);
 	if to_host and to_host ~= origin.host and not except_domains:contains(to_host) then
 		if to_host:match("^[^.]+%.(.+)$") == origin.host then -- Permit subdomains
 			except_domains:add(to_host);
@@ -31,10 +34,12 @@
 	end
 end
 
+module:default_permission("prosody:admin", "xmpp:federate");
+
 function check_user_isolated(event)
 	local session = event.session;
 	local bare_jid = jid_bare(session.full_jid);
-	if is_admin(bare_jid, module.host) or except_users:contains(bare_jid) then
+	if module:may("xmpp:federate") or except_users:contains(bare_jid) then
 		session.no_host_isolation = true;
 	end
 	module:log("debug", "%s is %sisolated", session.full_jid or "[?]", session.no_host_isolation and "" or "not ");
--- a/mod_muc_config_restrict/README.markdown	Tue Aug 16 13:10:39 2022 +0200
+++ b/mod_muc_config_restrict/README.markdown	Mon Aug 22 15:39:02 2022 +0100
@@ -57,7 +57,6 @@
 =============
 
   ------- --------------
-  trunk   Works
-  0.9     Doesn't work
-  0.8     Doesn't work
+  trunk   Doesn't work (uses is_admin)
+  0.12    Works?
   ------- --------------
--- a/mod_muc_restrict_rooms/README.markdown	Tue Aug 16 13:10:39 2022 +0200
+++ b/mod_muc_restrict_rooms/README.markdown	Mon Aug 22 15:39:02 2022 +0100
@@ -49,6 +49,7 @@
 =============
 
   ----- -------------
+  trunk Doesn't work (uses is_admin)
   0.9   Works
   0.8   Should work
   ----- -------------
--- a/mod_rest/mod_rest.lua	Tue Aug 16 13:10:39 2022 +0200
+++ b/mod_rest/mod_rest.lua	Mon Aug 22 15:39:02 2022 +0100
@@ -49,11 +49,15 @@
 		end
 		return { username = username, host = module.host };
 	elseif auth_type == "Bearer" then
-		local token_info = tokens.get_token_info(auth_data);
-		if not token_info or not token_info.session then
-			return false;
+		if tokens.get_token_session then
+			return tokens.get_token_session(auth_data);
+		else -- COMPAT w/0.12
+			local token_info = tokens.get_token_info(auth_data);
+			if not token_info or not token_info.session then
+				return false;
+			end
+			return token_info.session;
 		end
-		return token_info.session;
 	end
 	return nil;
 end
--- a/mod_sentry/mod_sentry.lua	Tue Aug 16 13:10:39 2022 +0200
+++ b/mod_sentry/mod_sentry.lua	Mon Aug 22 15:39:02 2022 +0100
@@ -29,6 +29,8 @@
 	end;
 };
 
+local serialize = require "util.serialization".serialize;
+
 local function sentry_error_handler(e)
 	module:log("error", "Failed to submit event to sentry: %s", e);
 end
--- a/mod_sentry/sentry.lib.lua	Tue Aug 16 13:10:39 2022 +0200
+++ b/mod_sentry/sentry.lib.lua	Mon Aug 22 15:39:02 2022 +0100
@@ -221,6 +221,7 @@
 		local data = json.decode(response.body);
 		return data;
 	end
+	module:log("warn", "Unexpected response from server: %d: %s", response.code, response.body);
 	return promise.reject(response);
 end