aboutsummaryrefslogtreecommitdiffstats
path: root/plugins
diff options
context:
space:
mode:
authorMatthew Wild <mwild1@gmail.com>2022-08-22 13:53:35 +0100
committerMatthew Wild <mwild1@gmail.com>2022-08-22 13:53:35 +0100
commite4ed9a570ab61ef45ca0e63dfd6d230e26812749 (patch)
tree361af05ddf1d012f9ce849528d36cfa83a0715ba /plugins
parentb79cb49bfba1d64dda54cf7243154624c53b5fb9 (diff)
parent227f6c033697210a54f671f5b9128cde8699fdcd (diff)
downloadprosody-e4ed9a570ab61ef45ca0e63dfd6d230e26812749.tar.gz
prosody-e4ed9a570ab61ef45ca0e63dfd6d230e26812749.zip
Merge role-auth->trunk
Diffstat (limited to 'plugins')
-rw-r--r--plugins/adhoc/adhoc.lib.lua10
-rw-r--r--plugins/adhoc/mod_adhoc.lua37
-rw-r--r--plugins/mod_admin_shell.lua122
-rw-r--r--plugins/mod_announce.lua6
-rw-r--r--plugins/mod_auth_insecure.lua4
-rw-r--r--plugins/mod_auth_internal_hashed.lua16
-rw-r--r--plugins/mod_auth_internal_plain.lua16
-rw-r--r--plugins/mod_auth_ldap.lua26
-rw-r--r--plugins/mod_authz_internal.lua300
-rw-r--r--plugins/mod_c2s.lua2
-rw-r--r--plugins/mod_disco.lua9
-rw-r--r--plugins/mod_invites_adhoc.lua38
-rw-r--r--plugins/mod_pubsub/mod_pubsub.lua4
-rw-r--r--plugins/mod_saslauth.lua2
-rw-r--r--plugins/mod_tokenauth.lua52
-rw-r--r--plugins/muc/hidden.lib.lua8
-rw-r--r--plugins/muc/mod_muc.lua19
-rw-r--r--plugins/muc/persistent.lib.lua11
18 files changed, 488 insertions, 194 deletions
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_admin_shell.lua b/plugins/mod_admin_shell.lua
index 84ae0f72..49e07dae 100644
--- a/plugins/mod_admin_shell.lua
+++ b/plugins/mod_admin_shell.lua
@@ -268,23 +268,22 @@ 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: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]]
@@ -943,6 +942,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 +971,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)
@@ -1374,32 +1382,32 @@ 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;
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 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
- 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)
@@ -1432,41 +1440,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 "<none>");
+
+ 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: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_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
--- 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)
+function def_env.user:delrole(jid, host, role_name)
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 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
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 <message>-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_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)
diff --git a/plugins/mod_auth_internal_hashed.lua b/plugins/mod_auth_internal_hashed.lua
index cf851eef..ddff31e9 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
@@ -105,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);
@@ -117,7 +128,8 @@ function provider.create_user(username, password)
local server_key_hex = to_hex(server_key);
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)
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..4f88b176 100644
--- a/plugins/mod_authz_internal.lua
+++ b/plugins/mod_authz_internal.lua
@@ -1,59 +1,311 @@
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 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 admin_role = { ["prosody:admin"] = true };
+local role_store = module:open_store("account_roles");
+local role_map_store = module:open_store("account_roles", "map");
+
+local role_registry = {};
-function get_user_roles(user)
- if config_admin_jids:contains(user.."@"..host) then
- return admin_role;
+function register_role(role)
+ if role_registry[role.name] ~= nil then
+ return error("A role '"..role.name.."' is already registered");
end
- return role_store:get(user);
+ 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
-function set_user_roles(user, roles)
- role_store:set(user, roles)
- return true;
+-- 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" };
+};
+
+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
+end
+
+-- 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
+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
+end
+
+-- Public API
+
+-- 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 role_registry["prosody:operator"];
+ elseif config_admin_jids:contains(bare_jid) then
+ 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
+ return role;
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 add_user_secondary_role(user, role_name)
+ if not role_registry[role_name] then
+ return error("Cannot assign default user an unknown role: "..tostring(role_name));
+ end
+ role_map_store:set(user, role_name, true);
+end
+
+function remove_user_secondary_role(user, role_name)
+ role_map_store:set(user, role_name, nil);
+end
+
+function get_user_secondary_roles(user)
+ local stored_roles, err = role_store:get(user);
+ if not stored_roles then
+ if err then
+ -- Unable to fetch role, fail
+ return nil, err;
+ end
+ -- No role set
+ return {};
+ end
+ stored_roles._default = nil;
+ for role_name in pairs(stored_roles) do
+ stored_roles[role_name] = role_registry[role_name];
+ end
+ return stored_roles;
+end
+
+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
+ 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;
+ 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;
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_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, role_name) -- 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 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_by_name(role_name)
+ return assert(role_registry[role_name], role_name);
+end
+
+-- COMPAT: Migrate from 0.12 role storage
+local function do_migration(migrate_host)
+ local old_role_store = assert(module:context(migrate_host):open_store("roles"));
+ local new_role_store = assert(module:context(migrate_host):open_store("account_roles"));
+
+ local migrated, failed, skipped = 0, 0, 0;
+ -- Iterate all users
+ for username in assert(old_role_store:users()) do
+ local old_roles = it.to_array(it.filter(function (k) return k:sub(1,1) ~= "_"; end, it.keys(old_role_store:get(username))));
+ if #old_roles == 1 then
+ local ok, err = new_role_store:set(username, {
+ _default = old_roles[1];
+ });
+ if ok then
+ migrated = migrated + 1;
+ else
+ failed = failed + 1;
+ print("EE: Failed to store new role info for '"..username.."': "..err);
+ end
+ else
+ print("WW: User '"..username.."' has multiple roles and cannot be automatically migrated");
+ skipped = skipped + 1;
+ end
+ end
+ return migrated, failed, skipped;
+end
+
+function module.command(arg)
+ if arg[1] == "migrate" then
+ table.remove(arg, 1);
+ local migrate_host = arg[1];
+ if not migrate_host or not prosody.hosts[migrate_host] then
+ print("EE: Please supply a valid host to migrate to the new role storage");
+ return 1;
+ end
+
+ -- Initialize storage layer
+ require "core.storagemanager".initialize_host(migrate_host);
+
+ print("II: Migrating roles...");
+ local migrated, failed, skipped = do_migration(migrate_host);
+ print(("II: %d migrated, %d failed, %d skipped"):format(migrated, failed, skipped));
+ return (failed + skipped == 0) and 0 or 1;
+ else
+ print("EE: Unknown command: "..(arg[1] or "<none given>"));
+ print(" Hint: try 'migrate'?");
+ end
+end
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_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/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;
diff --git a/plugins/mod_tokenauth.lua b/plugins/mod_tokenauth.lua
index c04a1aa4..85602747 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_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
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