Software /
code /
prosody
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 |