Comparison

plugins/mod_invites.lua @ 12142:87532eebd0b8

mod_invites: Import from prosdy-modules@5fc306239db3
author Kim Alvefur <zash@zash.se>
date Mon, 27 Dec 2021 20:46:34 +0100
child 12143:51b7ade94d50
comparison
equal deleted inserted replaced
12141:3ac801630b4b 12142:87532eebd0b8
1 local id = require "util.id";
2 local it = require "util.iterators";
3 local url = require "socket.url";
4 local jid_node = require "util.jid".node;
5 local jid_split = require "util.jid".split;
6
7 local default_ttl = module:get_option_number("invite_expiry", 86400 * 7);
8
9 local token_storage;
10 if prosody.process_type == "prosody" or prosody.shutdown then
11 token_storage = module:open_store("invite_token", "map");
12 end
13
14 local function get_uri(action, jid, token, params) --> string
15 return url.build({
16 scheme = "xmpp",
17 path = jid,
18 query = action..";preauth="..token..(params and (";"..params) or ""),
19 });
20 end
21
22 local function create_invite(invite_action, invite_jid, allow_registration, additional_data, ttl, reusable)
23 local token = id.medium();
24
25 local created_at = os.time();
26 local expires = created_at + (ttl or default_ttl);
27
28 local invite_params = (invite_action == "roster" and allow_registration) and "ibr=y" or nil;
29
30 local invite = {
31 type = invite_action;
32 jid = invite_jid;
33
34 token = token;
35 allow_registration = allow_registration;
36 additional_data = additional_data;
37
38 uri = get_uri(invite_action, invite_jid, token, invite_params);
39
40 created_at = created_at;
41 expires = expires;
42
43 reusable = reusable;
44 };
45
46 module:fire_event("invite-created", invite);
47
48 if allow_registration then
49 local ok, err = token_storage:set(nil, token, invite);
50 if not ok then
51 module:log("warn", "Failed to store account invite: %s", err);
52 return nil, "internal-server-error";
53 end
54 end
55
56 if invite_action == "roster" then
57 local username = jid_node(invite_jid);
58 local ok, err = token_storage:set(username, token, expires);
59 if not ok then
60 module:log("warn", "Failed to store subscription invite: %s", err);
61 return nil, "internal-server-error";
62 end
63 end
64
65 return invite;
66 end
67
68 -- Create invitation to register an account (optionally restricted to the specified username)
69 function create_account(account_username, additional_data, ttl) --luacheck: ignore 131/create_account
70 local jid = account_username and (account_username.."@"..module.host) or module.host;
71 return create_invite("register", jid, true, additional_data, ttl);
72 end
73
74 -- Create invitation to reset the password for an account
75 function create_account_reset(account_username, ttl) --luacheck: ignore 131/create_account_reset
76 return create_account(account_username, { allow_reset = account_username }, ttl or 86400);
77 end
78
79 -- Create invitation to become a contact of a local user
80 function create_contact(username, allow_registration, additional_data, ttl) --luacheck: ignore 131/create_contact
81 return create_invite("roster", username.."@"..module.host, allow_registration, additional_data, ttl);
82 end
83
84 -- Create invitation to register an account and join a user group
85 -- If explicit ttl is passed, invite is valid for multiple signups
86 -- during that time period
87 function create_group(group_ids, additional_data, ttl) --luacheck: ignore 131/create_group
88 local merged_additional_data = {
89 groups = group_ids;
90 };
91 if additional_data then
92 for k, v in pairs(additional_data) do
93 merged_additional_data[k] = v;
94 end
95 end
96 return create_invite("register", module.host, true, merged_additional_data, ttl, not not ttl);
97 end
98
99 -- Iterates pending (non-expired, unused) invites that allow registration
100 function pending_account_invites() --luacheck: ignore 131/pending_account_invites
101 local store = module:open_store("invite_token");
102 local now = os.time();
103 local function is_valid_invite(_, invite)
104 return invite.expires > now;
105 end
106 return it.filter(is_valid_invite, pairs(store:get(nil) or {}));
107 end
108
109 function get_account_invite_info(token) --luacheck: ignore 131/get_account_invite_info
110 if not token then
111 return nil, "no-token";
112 end
113
114 -- Fetch from host store (account invite)
115 local token_info = token_storage:get(nil, token);
116 if not token_info then
117 return nil, "token-invalid";
118 elseif os.time() > token_info.expires then
119 return nil, "token-expired";
120 end
121
122 return token_info;
123 end
124
125 function delete_account_invite(token) --luacheck: ignore 131/delete_account_invite
126 if not token then
127 return nil, "no-token";
128 end
129
130 return token_storage:set(nil, token, nil);
131 end
132
133 local valid_invite_methods = {};
134 local valid_invite_mt = { __index = valid_invite_methods };
135
136 function valid_invite_methods:use()
137 if self.reusable then
138 return true;
139 end
140
141 if self.username then
142 -- Also remove the contact invite if present, on the
143 -- assumption that they now have a mutual subscription
144 token_storage:set(self.username, self.token, nil);
145 end
146 token_storage:set(nil, self.token, nil);
147
148 return true;
149 end
150
151 -- Get a validated invite (or nil, err). Must call :use() on the
152 -- returned invite after it is actually successfully used
153 -- For "roster" invites, the username of the local user (who issued
154 -- the invite) must be passed.
155 -- If no username is passed, but the registration is a roster invite
156 -- from a local user, the "inviter" field of the returned invite will
157 -- be set to their username.
158 function get(token, username)
159 if not token then
160 return nil, "no-token";
161 end
162
163 local valid_until, inviter;
164
165 -- Fetch from host store (account invite)
166 local token_info = token_storage:get(nil, token);
167
168 if username then -- token being used for subscription
169 -- Fetch from user store (subscription invite)
170 valid_until = token_storage:get(username, token);
171 else -- token being used for account creation
172 valid_until = token_info and token_info.expires;
173 if token_info and token_info.type == "roster" then
174 username = jid_node(token_info.jid);
175 inviter = username;
176 end
177 end
178
179 if not valid_until then
180 module:log("debug", "Got unknown token: %s", token);
181 return nil, "token-invalid";
182 elseif os.time() > valid_until then
183 module:log("debug", "Got expired token: %s", token);
184 return nil, "token-expired";
185 end
186
187 return setmetatable({
188 token = token;
189 username = username;
190 inviter = inviter;
191 type = token_info and token_info.type or "roster";
192 uri = token_info and token_info.uri or get_uri("roster", username.."@"..module.host, token);
193 additional_data = token_info and token_info.additional_data or nil;
194 reusable = token_info.reusable;
195 }, valid_invite_mt);
196 end
197
198 function use(token) --luacheck: ignore 131/use
199 local invite = get(token);
200 return invite and invite:use();
201 end
202
203 --- shell command
204 do
205 -- Since the console is global this overwrites the command for
206 -- each host it's loaded on, but this should be fine.
207
208 local get_module = require "core.modulemanager".get_module;
209
210 local console_env = module:shared("/*/admin_shell/env");
211
212 -- luacheck: ignore 212/self
213 console_env.invite = {};
214 function console_env.invite:create_account(user_jid)
215 local username, host = jid_split(user_jid);
216 local mod_invites, err = get_module(host, "invites");
217 if not mod_invites then return nil, err or "mod_invites not loaded on this host"; end
218 local invite, err = mod_invites.create_account(username);
219 if not invite then return nil, err; end
220 return true, invite.uri;
221 end
222
223 function console_env.invite:create_contact(user_jid, allow_registration)
224 local username, host = jid_split(user_jid);
225 local mod_invites, err = get_module(host, "invites");
226 if not mod_invites then return nil, err or "mod_invites not loaded on this host"; end
227 local invite, err = mod_invites.create_contact(username, allow_registration);
228 if not invite then return nil, err; end
229 return true, invite.uri;
230 end
231 end
232
233 --- prosodyctl command
234 function module.command(arg)
235 if #arg < 2 or arg[1] ~= "generate" then
236 print("usage: prosodyctl mod_"..module.name.." generate example.com");
237 return 2;
238 end
239 table.remove(arg, 1); -- pop command
240
241 local sm = require "core.storagemanager";
242 local mm = require "core.modulemanager";
243
244 local host = arg[1];
245 assert(hosts[host], "Host "..tostring(host).." does not exist");
246 sm.initialize_host(host);
247 table.remove(arg, 1); -- pop host
248 module.host = host; --luacheck: ignore 122/module
249 token_storage = module:open_store("invite_token", "map");
250
251 -- Load mod_invites
252 local invites = module:depends("invites");
253 local invites_page_module = module:get_option_string("invites_page_module", "invites_page");
254 if mm.get_modules_for_host(host):contains(invites_page_module) then
255 module:depends(invites_page_module);
256 end
257
258 local allow_reset;
259 local roles;
260 local groups = {};
261
262 while #arg > 0 do
263 local value = arg[1];
264 table.remove(arg, 1);
265 if value == "--help" then
266 print("usage: prosodyctl mod_"..module.name.." generate DOMAIN --reset USERNAME")
267 print("usage: prosodyctl mod_"..module.name.." generate DOMAIN [--admin] [--role ROLE] [--group GROUPID]...")
268 print()
269 print("This command has two modes: password reset and new account.")
270 print("If --reset is given, the command operates in password reset mode and in new account mode otherwise.")
271 print()
272 print("required arguments in password reset mode:")
273 print()
274 print(" --reset USERNAME Generate a password reset link for the given USERNAME.")
275 print()
276 print("optional arguments in new account mode:")
277 print()
278 print(" --admin Make the new user privileged")
279 print(" Equivalent to --role prosody:admin")
280 print(" --role ROLE Grant the given ROLE to the new user")
281 print(" --group GROUPID Add the user to the group with the given ID")
282 print(" Can be specified multiple times")
283 print()
284 print("--role and --admin override each other; the last one wins")
285 print("--group can be specified multiple times; the user will be added to all groups.")
286 print()
287 print("--reset and the other options cannot be mixed.")
288 return 2
289 elseif value == "--reset" then
290 local nodeprep = require "util.encodings".stringprep.nodeprep;
291 local username = nodeprep(arg[1])
292 table.remove(arg, 1);
293 if not username then
294 print("Please supply a valid username to generate a reset link for");
295 return 2;
296 end
297 allow_reset = username;
298 elseif value == "--admin" then
299 roles = { ["prosody:admin"] = true };
300 elseif value == "--role" then
301 local rolename = arg[1];
302 if not rolename then
303 print("Please supply a role name");
304 return 2;
305 end
306 roles = { [rolename] = true };
307 table.remove(arg, 1);
308 elseif value == "--group" or value == "-g" then
309 local groupid = arg[1];
310 if not groupid then
311 print("Please supply a group ID")
312 return 2;
313 end
314 table.insert(groups, groupid);
315 table.remove(arg, 1);
316 else
317 print("unexpected argument: "..value)
318 end
319 end
320
321 local invite;
322 if allow_reset then
323 if roles then
324 print("--role/--admin and --reset are mutually exclusive")
325 return 2;
326 end
327 if #groups > 0 then
328 print("--group and --reset are mutually exclusive")
329 end
330 invite = assert(invites.create_account_reset(allow_reset));
331 else
332 invite = assert(invites.create_account(nil, {
333 roles = roles,
334 groups = groups
335 }));
336 end
337
338 print(invite.landing_page or invite.uri);
339 end