Comparison

plugins/mod_authz_internal.lua @ 12662:07424992d7fc

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 <host>
author Matthew Wild <mwild1@gmail.com>
date Wed, 17 Aug 2022 16:38:53 +0100
parent 12648:f299e570a0fe
child 12663:cf88f6b03942
comparison
equal deleted inserted replaced
12661:1c391c17a907 12662:07424992d7fc
6 local roles = require "util.roles"; 6 local roles = require "util.roles";
7 7
8 local config_global_admin_jids = module:context("*"):get_option_set("admins", {}) / normalize; 8 local config_global_admin_jids = module:context("*"):get_option_set("admins", {}) / normalize;
9 local config_admin_jids = module:get_option_inherited_set("admins", {}) / normalize; 9 local config_admin_jids = module:get_option_inherited_set("admins", {}) / normalize;
10 local host = module.host; 10 local host = module.host;
11 local role_store = module:open_store("roles"); 11
12 local role_map_store = module:open_store("roles", "map"); 12 local role_store = module:open_store("account_roles");
13 local role_map_store = module:open_store("account_roles", "map");
13 14
14 local role_registry = {}; 15 local role_registry = {};
15 16
16 function register_role(role) 17 function register_role(role)
17 if role_registry[role.name] ~= nil then 18 if role_registry[role.name] ~= nil then
96 end 97 end
97 end 98 end
98 99
99 -- Public API 100 -- Public API
100 101
101 local config_operator_role_set = { 102 -- Get the primary role of a user
102 ["prosody:operator"] = role_registry["prosody:operator"]; 103 function get_user_role(user)
103 };
104 local config_admin_role_set = {
105 ["prosody:admin"] = role_registry["prosody:admin"];
106 };
107 local default_role_set = {
108 ["prosody:user"] = role_registry["prosody:user"];
109 };
110
111 function get_user_roles(user)
112 local bare_jid = user.."@"..host; 104 local bare_jid = user.."@"..host;
105
106 -- Check config first
113 if config_global_admin_jids:contains(bare_jid) then 107 if config_global_admin_jids:contains(bare_jid) then
114 return config_operator_role_set; 108 return role_registry["prosody:operator"];
115 elseif config_admin_jids:contains(bare_jid) then 109 elseif config_admin_jids:contains(bare_jid) then
116 return config_admin_role_set; 110 return role_registry["prosody:admin"];
117 end 111 end
118 local role_names = role_store:get(user); 112
119 if not role_names then return default_role_set; end 113 -- Check storage
120 local user_roles = {}; 114 local stored_roles, err = role_store:get(user);
121 for role_name in pairs(role_names) do 115 if not stored_roles then
122 user_roles[role_name] = role_registry[role_name]; 116 if err then
123 end 117 -- Unable to fetch role, fail
124 return user_roles; 118 return nil, err;
125 end 119 end
126 120 -- No role set, use default role
127 function set_user_roles(user, user_roles) 121 return role_registry["prosody:user"];
128 role_store:set(user, user_roles) 122 end
129 return true; 123 if stored_roles._default == nil then
130 end 124 -- No primary role explicitly set, return default
131 125 return role_registry["prosody:user"];
132 function get_user_default_role(user) 126 end
133 local user_roles = get_user_roles(user); 127 local primary_stored_role = role_registry[stored_roles._default];
134 if not user_roles then return nil; end 128 if not primary_stored_role then
135 local default_role; 129 return nil, "unknown-role";
136 for role_name, role_info in pairs(user_roles) do --luacheck: ignore 213/role_name 130 end
137 if role_info.default ~= false and (not default_role or role_info.priority > default_role.priority) then 131 return primary_stored_role;
138 default_role = role_info; 132 end
139 end 133
140 end 134 -- Set the primary role of a user
141 if not default_role then return nil; end 135 function set_user_role(user, role_name)
142 return default_role; 136 local role = role_registry[role_name];
143 end 137 if not role then
144 138 return error("Cannot assign default user an unknown role: "..tostring(role_name));
139 end
140 local keys_update = {
141 _default = role_name;
142 -- Primary role cannot be secondary role
143 [role_name] = role_map_store.remove;
144 };
145 if role_name == "prosody:user" then
146 -- Don't store default
147 keys_update._default = role_map_store.remove;
148 end
149 local ok, err = role_map_store:set_keys(user, keys_update);
150 if not ok then
151 return nil, err;
152 end
153 return role;
154 end
155
156 function add_user_secondary_role(user, role_name)
157 if not role_registry[role_name] then
158 return error("Cannot assign default user an unknown role: "..tostring(role_name));
159 end
160 role_map_store:set(user, role_name, true);
161 end
162
163 function remove_user_secondary_role(user, role_name)
164 role_map_store:set(user, role_name, nil);
165 end
166
167 function get_user_secondary_roles(user)
168 local stored_roles, err = role_store:get(user);
169 if not stored_roles then
170 if err then
171 -- Unable to fetch role, fail
172 return nil, err;
173 end
174 -- No role set
175 return {};
176 end
177 stored_roles._default = nil;
178 for role_name in pairs(stored_roles) do
179 stored_roles[role_name] = role_registry[role_name];
180 end
181 return stored_roles;
182 end
183
184 -- This function is *expensive*
145 function get_users_with_role(role_name) 185 function get_users_with_role(role_name)
146 local storage_role_users = it.to_array(it.keys(role_map_store:get_all(role_name) or {})); 186 local function role_filter(username, default_role) --luacheck: ignore 212/username
187 return default_role == role_name;
188 end
189 local primary_role_users = set.new(it.to_array(it.filter(role_filter, pairs(role_map_store:get_all("_default") or {}))));
190 local secondary_role_users = set.new(it.to_array(it.keys(role_map_store:get_all(role_name) or {})));
191
147 local config_set; 192 local config_set;
148 if role_name == "prosody:admin" then 193 if role_name == "prosody:admin" then
149 config_set = config_admin_jids; 194 config_set = config_admin_jids;
150 elseif role_name == "prosody:operator" then 195 elseif role_name == "prosody:operator" then
151 config_set = config_global_admin_jids; 196 config_set = config_global_admin_jids;
155 local j_node, j_host = jid_split(admin_jid); 200 local j_node, j_host = jid_split(admin_jid);
156 if j_host == host then 201 if j_host == host then
157 return j_node; 202 return j_node;
158 end 203 end
159 end; 204 end;
160 return it.to_array(config_admin_users + set.new(storage_role_users)); 205 return it.to_array(config_admin_users + primary_role_users + secondary_role_users);
161 end 206 end
162 return storage_role_users; 207 return it.to_array(primary_role_users + secondary_role_users);
163 end 208 end
164 209
165 function get_jid_role(jid) 210 function get_jid_role(jid)
166 local bare_jid = jid_bare(jid); 211 local bare_jid = jid_bare(jid);
167 if config_global_admin_jids:contains(bare_jid) then 212 if config_global_admin_jids:contains(bare_jid) then
201 end 246 end
202 247
203 function get_role_by_name(role_name) 248 function get_role_by_name(role_name)
204 return assert(role_registry[role_name], role_name); 249 return assert(role_registry[role_name], role_name);
205 end 250 end
251
252 -- COMPAT: Migrate from 0.12 role storage
253 local function do_migration(migrate_host)
254 local old_role_store = assert(module:context(migrate_host):open_store("roles"));
255 local new_role_store = assert(module:context(migrate_host):open_store("account_roles"));
256
257 local migrated, failed, skipped = 0, 0, 0;
258 -- Iterate all users
259 for username in assert(old_role_store:users()) do
260 local old_roles = it.to_array(it.filter(function (k) return k:sub(1,1) ~= "_"; end, it.keys(old_role_store:get(username))));
261 if #old_roles == 1 then
262 local ok, err = new_role_store:set(username, {
263 _default = old_roles[1];
264 });
265 if ok then
266 migrated = migrated + 1;
267 else
268 failed = failed + 1;
269 print("EE: Failed to store new role info for '"..username.."': "..err);
270 end
271 else
272 print("WW: User '"..username.."' has multiple roles and cannot be automatically migrated");
273 skipped = skipped + 1;
274 end
275 end
276 return migrated, failed, skipped;
277 end
278
279 function module.command(arg)
280 if arg[1] == "migrate" then
281 table.remove(arg, 1);
282 local migrate_host = arg[1];
283 if not migrate_host or not prosody.hosts[migrate_host] then
284 print("EE: Please supply a valid host to migrate to the new role storage");
285 return 1;
286 end
287
288 -- Initialize storage layer
289 require "core.storagemanager".initialize_host(migrate_host);
290
291 print("II: Migrating roles...");
292 local migrated, failed, skipped = do_migration(migrate_host);
293 print(("II: %d migrated, %d failed, %d skipped"):format(migrated, failed, skipped));
294 return (failed + skipped == 0) and 0 or 1;
295 else
296 print("EE: Unknown command: "..(arg[1] or "<none given>"));
297 print(" Hint: try 'migrate'?");
298 end
299 end