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 |