UNPKG

smc-hub

Version:

CoCalc: Backend webserver component

581 lines 30.9 kB
"use strict"; /* * This file is part of CoCalc: Copyright © 2020 Sagemath, Inc. * License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } }); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (_) try { if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; var __read = (this && this.__read) || function (o, n) { var m = typeof Symbol === "function" && o[Symbol.iterator]; if (!m) return o; var i = m.call(o), r, ar = [], e; try { while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); } catch (error) { e = { error: error }; } finally { try { if (r && !r.done && (m = i["return"])) m.call(i); } finally { if (e) throw e.error; } } return ar; }; var __values = (this && this.__values) || function(o) { var s = typeof Symbol === "function" && Symbol.iterator, m = s && o[s], i = 0; if (m) return m.call(o); if (o && typeof o.length === "number") return { next: function () { if (o && i >= o.length) o = void 0; return { value: o && o[i++], done: !o }; } }; throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined."); }; var __spreadArray = (this && this.__spreadArray) || function (to, from) { for (var i = 0, il = from.length, j = to.length; i < il; i++, j++) to[j] = from[i]; return to; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.delete_account = exports.create_account = exports.is_valid_password = void 0; /* Client account creation and deletion */ var MAX_ACCOUNTS_PER_30MIN = 150; var MAX_ACCOUNTS_PER_30MIN_GOLD = 1500; var auth = require("../auth"); var parse_domain_1 = require("parse-domain"); var message = __importStar(require("smc-util/message")); var misc_1 = require("smc-util/misc"); var awaiting_1 = require("awaiting"); var async_utils_1 = require("smc-util/async-utils"); var api_key_action = require("../api/manage").api_key_action; var utils_1 = require("../utils"); var logger_1 = require("smc-hub/logger"); var winston = logger_1.getLogger("create-account"); function is_valid_password(password) { if (typeof password !== "string") { return [false, "Password must be specified."]; } if (password.length >= 6 && password.length <= 64) { return [true, ""]; } else { return [false, "Password must be between 6 and 64 characters in length."]; } } exports.is_valid_password = is_valid_password; function issues_with_create_account(mesg) { var issues = {}; if (mesg.email_address && !misc_1.is_valid_email_address(mesg.email_address)) { issues.email_address = "Email address does not appear to be valid."; } if (mesg.password) { var _a = __read(is_valid_password(mesg.password), 2), valid = _a[0], reason = _a[1]; if (!valid) { issues.password = reason; } } return issues; } function get_db_client(db) { return __awaiter(this, void 0, void 0, function () { var t0, client; return __generator(this, function (_a) { switch (_a.label) { case 0: t0 = new Date().getTime(); _a.label = 1; case 1: if (!(new Date().getTime() - t0 < 10 * 1000)) return [3 /*break*/, 5]; client = db._client(); if (!(client != null)) return [3 /*break*/, 2]; return [2 /*return*/, client]; case 2: return [4 /*yield*/, awaiting_1.delay(100)]; case 3: _a.sent(); _a.label = 4; case 4: return [3 /*break*/, 1]; case 5: throw new Error("Unable to get a database client"); } }); }); } // if the email address's domain should go through SSO, return the domain name function is_domain_exclusive_sso(db, email) { var _a, _b; return __awaiter(this, void 0, void 0, function () { var raw_domain, parsed, passports, blocked, passports_1, passports_1_1, pp, _c, _d, domain, domain, topLevelDomains, canonical; var e_1, _e, e_2, _f; return __generator(this, function (_g) { switch (_g.label) { case 0: if (email == null) return [2 /*return*/, undefined]; raw_domain = (_a = email.split("@")[1]) === null || _a === void 0 ? void 0 : _a.trim().toLowerCase(); if (raw_domain == null) return [2 /*return*/]; parsed = parse_domain_1.parseDomain(raw_domain); return [4 /*yield*/, utils_1.get_passports(db)]; case 1: passports = _g.sent(); blocked = new Set([]); try { for (passports_1 = __values(passports), passports_1_1 = passports_1.next(); !passports_1_1.done; passports_1_1 = passports_1.next()) { pp = passports_1_1.value; try { for (_c = (e_2 = void 0, __values((_b = pp.conf.exclusive_domains) !== null && _b !== void 0 ? _b : [])), _d = _c.next(); !_d.done; _d = _c.next()) { domain = _d.value; blocked.add(domain); } } catch (e_2_1) { e_2 = { error: e_2_1 }; } finally { try { if (_d && !_d.done && (_f = _c.return)) _f.call(_c); } finally { if (e_2) throw e_2.error; } } } } catch (e_1_1) { e_1 = { error: e_1_1 }; } finally { try { if (passports_1_1 && !passports_1_1.done && (_e = passports_1.return)) _e.call(passports_1); } finally { if (e_1) throw e_1.error; } } if (parsed.type == parse_domain_1.ParseResultType.Listed) { domain = parsed.domain, topLevelDomains = parsed.topLevelDomains; canonical = __spreadArray([domain !== null && domain !== void 0 ? domain : ""], __read(topLevelDomains)).join("."); if (blocked.has(canonical)) { return [2 /*return*/, canonical]; } } return [2 /*return*/]; } }); }); } // return true if allowed to continue creating an account (either no token required or token matches) function check_registration_token(db, token) { return __awaiter(this, void 0, void 0, function () { var have_tokens, client, q_match, match, _a, expires, counter_raw, limit, disabled_raw, counter, disabled, q_inc, e_3; return __generator(this, function (_b) { switch (_b.label) { case 0: return [4 /*yield*/, utils_1.have_active_registration_tokens(db)]; case 1: have_tokens = _b.sent(); // if there are no tokens set, it's ok if (!have_tokens) return [2 /*return*/]; if (token == null || token == "") { return [2 /*return*/, "No registration token provided"]; } return [4 /*yield*/, get_db_client(db)]; case 2: client = _b.sent(); _b.label = 3; case 3: _b.trys.push([3, 10, , 12]); return [4 /*yield*/, client.query("BEGIN")]; case 4: _b.sent(); q_match = "SELECT \"expires\", \"counter\", \"limit\", \"disabled\"\n FROM registration_tokens\n WHERE token = $1::TEXT\n FOR UPDATE"; return [4 /*yield*/, client.query(q_match, [token])]; case 5: match = _b.sent(); if (match.rows.length != 1) { return [2 /*return*/, "Registration token is wrong."]; } _a = match.rows[0], expires = _a.expires, counter_raw = _a.counter, limit = _a.limit, disabled_raw = _a.disabled; counter = counter_raw !== null && counter_raw !== void 0 ? counter_raw : 0; disabled = disabled_raw !== null && disabled_raw !== void 0 ? disabled_raw : false; if (disabled) { return [2 /*return*/, "Registration token disabled."]; } if (expires != null && expires.getTime() < new Date().getTime()) { return [2 /*return*/, "Registration token no longer valid."]; } if (!(limit != null && limit <= counter)) return [3 /*break*/, 6]; return [2 /*return*/, "Registration token used up."]; case 6: q_inc = "UPDATE registration_tokens SET \"counter\" = coalesce(\"counter\", 0) + 1\n WHERE token = $1::TEXT"; return [4 /*yield*/, client.query(q_inc, [token])]; case 7: _b.sent(); _b.label = 8; case 8: // all good, let's commit return [4 /*yield*/, client.query("COMMIT")]; case 9: // all good, let's commit _b.sent(); return [3 /*break*/, 12]; case 10: e_3 = _b.sent(); return [4 /*yield*/, client.query("ROLLBACK")]; case 11: _b.sent(); throw e_3; case 12: return [2 /*return*/]; } }); }); } // This should not actually throw in case of trouble, but instead send // error directly to the client. function create_account(opts) { return __awaiter(this, void 0, void 0, function () { function dbg(m) { winston.debug("create_account (" + opts.mesg.email_address + "): " + m); } function createAccount() { return __awaiter(this, void 0, void 0, function () { var settings, issues, n, m, _a, not_available, is_banned, check_token, check_domain, data, err_2, _b; return __generator(this, function (_c) { switch (_c.label) { case 0: dbg("run tests on generic validity of input"); return [4 /*yield*/, utils_1.get_server_settings(opts.database)]; case 1: settings = _c.sent(); if (!settings.email_signup) { return [2 /*return*/, { other: "Signing up via email/password is disabled." }]; } issues = issues_with_create_account(opts.mesg); // TODO -- only uncomment this for easy testing to allow any password choice. // the client test suite will then fail, which is good, so we are reminded // to comment this out before release! // delete issues['password'] if (misc_1.len(issues) > 0) { return [2 /*return*/, issues]; } // Make sure this ip address hasn't requested too many accounts recently, // just to avoid really nasty abuse, but still allow for demo registration // behind a single router. dbg("make sure not too many accounts were created from the given ip"); return [4 /*yield*/, async_utils_1.callback2(opts.database.count_accounts_created_by, { ip_address: opts.client.ip_address, age_s: 60 * 30, })]; case 2: n = _c.sent(); if (!(n >= MAX_ACCOUNTS_PER_30MIN)) return [3 /*break*/, 5]; m = MAX_ACCOUNTS_PER_30MIN; _a = opts.client.account_id != null; if (!_a) return [3 /*break*/, 4]; return [4 /*yield*/, async_utils_1.callback2(opts.database.user_is_in_group, { account_id: opts.client.account_id, group: "gold", })]; case 3: _a = (_c.sent()); _c.label = 4; case 4: // Check if account is being created via API by a "gold" user. if (_a) { m = MAX_ACCOUNTS_PER_30MIN_GOLD; } if (n >= m) { return [2 /*return*/, { other: "Too many accounts are being created from the ip address " + opts.client.ip_address + "; try again later. By default at most " + m + " accounts can be created every 30 minutes from one IP; if you're using the API and need a higher limit, contact us.", }]; } _c.label = 5; case 5: if (!opts.mesg.email_address) return [3 /*break*/, 8]; dbg("query database to determine whether the email address is available"); return [4 /*yield*/, async_utils_1.callback2(opts.database.account_exists, { email_address: opts.mesg.email_address, })]; case 6: not_available = _c.sent(); if (not_available) { return [2 /*return*/, { email_address: "This e-mail address is already taken." }]; } dbg("check that account is not banned"); return [4 /*yield*/, async_utils_1.callback2(opts.database.is_banned_user, { email_address: opts.mesg.email_address, })]; case 7: is_banned = _c.sent(); if (is_banned) { return [2 /*return*/, { email_address: "This e-mail address is banned." }]; } _c.label = 8; case 8: dbg("check if a registration token is required"); return [4 /*yield*/, check_registration_token(opts.database, opts.mesg.token)]; case 9: check_token = _c.sent(); if (check_token) { return [2 /*return*/, { token: check_token }]; } dbg("check if email domain has to go through an SSO mechanism"); return [4 /*yield*/, is_domain_exclusive_sso(opts.database, opts.mesg.email_address)]; case 10: check_domain = _c.sent(); if (check_domain != null) { return [2 /*return*/, { email_address: "To sign up with \"@" + check_domain + "\", you have to use the corresponding SSO connect mechanism listed above!", }]; } dbg("create new account"); return [4 /*yield*/, async_utils_1.callback2(opts.database.create_account, { first_name: opts.mesg.first_name, last_name: opts.mesg.last_name, email_address: opts.mesg.email_address, password_hash: opts.mesg.password ? auth.password_hash(opts.mesg.password) : undefined, created_by: opts.client.ip_address, usage_intent: opts.mesg.usage_intent, })]; case 11: account_id = _c.sent(); if (!(opts.mesg.token != null)) return [3 /*break*/, 13]; // we also record that we used a registration token ... return [4 /*yield*/, async_utils_1.callback2(opts.database.log, { event: "create_account_registration_token", value: { token: opts.mesg.token, account_id: account_id }, })]; case 12: // we also record that we used a registration token ... _c.sent(); _c.label = 13; case 13: data = { account_id: account_id, first_name: opts.mesg.first_name, last_name: opts.mesg.last_name, email_address: opts.mesg.email_address, created_by: opts.client.ip_address, }; return [4 /*yield*/, async_utils_1.callback2(opts.database.log, { event: "create_account", value: data, })]; case 14: _c.sent(); if (!opts.mesg.email_address) return [3 /*break*/, 16]; dbg("check for any account creation actions"); // do not block return [4 /*yield*/, async_utils_1.callback2(opts.database.do_account_creation_actions, { email_address: opts.mesg.email_address, account_id: account_id, })]; case 15: // do not block _c.sent(); _c.label = 16; case 16: if (!opts.sign_in) return [3 /*break*/, 18]; dbg("set remember_me cookie..."); // so that proxy server will allow user to connect and // download images, etc., the very first time right after they make a new account. return [4 /*yield*/, async_utils_1.callback2(opts.client.remember_me, { account_id: account_id, })]; case 17: // so that proxy server will allow user to connect and // download images, etc., the very first time right after they make a new account. _c.sent(); dbg("send message back to user that they are logged in as the new user (in " + misc_1.walltime(tm) + "seconds)"); // no analytics token is logged, because it is already done in the create_account entry above. mesg1 = message.signed_in({ id: id, account_id: account_id, email_address: opts.mesg.email_address, first_name: opts.mesg.first_name, last_name: opts.mesg.last_name, remember_me: false, hub: opts.host + ":" + opts.port, }); opts.client.signed_in(mesg1); // records this creation in database... return [3 /*break*/, 19]; case 18: mesg1 = message.account_created({ id: id, account_id: account_id }); _c.label = 19; case 19: if (!(opts.mesg.email_address != null)) return [3 /*break*/, 23]; _c.label = 20; case 20: _c.trys.push([20, 22, , 23]); dbg("send email verification request"); return [4 /*yield*/, async_utils_1.callback2(auth.verify_email_send_token, { account_id: account_id, database: opts.database, })]; case 21: _c.sent(); return [3 /*break*/, 23]; case 22: err_2 = _c.sent(); // We make this nonfatal since email might just be misconfigured, // and we don't want that to completely break account creation. dbg("WARNING -- error sending email verification (non-fatal): " + err_2); return [3 /*break*/, 23]; case 23: if (!opts.mesg.get_api_key) return [3 /*break*/, 25]; dbg("get_api_key -- generate key and include in response message"); _b = mesg1; return [4 /*yield*/, async_utils_1.callback2(api_key_action, { database: opts.database, account_id: account_id, password: opts.mesg.password, action: "regenerate", })]; case 24: _b.api_key = _c.sent(); _c.label = 25; case 25: opts.client.push_to_client(mesg1); return [2 /*return*/]; } }); }); } var id, mesg1, tm, account_id, reason, err_1; return __generator(this, function (_a) { switch (_a.label) { case 0: // we still use defaults/required due to coffeescript client. opts = misc_1.defaults(opts, { client: misc_1.required, mesg: misc_1.required, database: misc_1.required, host: undefined, port: undefined, sign_in: false, // if true, the newly created user will also be signed in; only makes sense for browser clients! }); id = opts.mesg.id; winston.info("create_account " + opts.mesg.first_name + " " + opts.mesg.last_name + " " + opts.mesg.email_address); tm = misc_1.walltime(); if (opts.mesg.email_address != null) { opts.mesg.email_address = misc_1.lower_email_address(opts.mesg.email_address); } account_id = ""; reason = undefined; _a.label = 1; case 1: _a.trys.push([1, 3, , 4]); return [4 /*yield*/, createAccount()]; case 2: reason = _a.sent(); return [3 /*break*/, 4]; case 3: err_1 = _a.sent(); // This can happen, e.g., the call to opts.database.create_account above // is not wrapped in try/catch and if the first name is wstein.org, // then there is an exception saying the first name is a URL. (ASIDE: This is // really minimal security, since as of writing this you can just change your // name to anything after you make an account.) reason = { other: "" + err_1 }; return [3 /*break*/, 4]; case 4: if (reason) { // IMPORTANT: There are various settings where the user simply never sees // this, since they aren't setup to listen for it... dbg("send message to user that there was an error (in " + misc_1.walltime(tm) + "seconds) -- " + JSON.stringify(reason)); opts.client.push_to_client(message.account_creation_failed({ id: id, reason: reason })); } return [2 /*return*/]; } }); }); } exports.create_account = create_account; // This should not actually throw in case of trouble, but instead send // error directly to the client. function delete_account(opts) { return __awaiter(this, void 0, void 0, function () { var error, err_3; return __generator(this, function (_a) { switch (_a.label) { case 0: opts = misc_1.defaults(opts, { client: undefined, mesg: misc_1.required, database: misc_1.required, }); winston.info("delete_account(\"" + opts.mesg.account_id + "\")"); error = undefined; _a.label = 1; case 1: _a.trys.push([1, 3, , 4]); return [4 /*yield*/, async_utils_1.callback2(opts.database.mark_account_deleted, { account_id: opts.mesg.account_id, })]; case 2: _a.sent(); return [3 /*break*/, 4]; case 3: err_3 = _a.sent(); error = err_3; return [3 /*break*/, 4]; case 4: if (opts.client != null) { opts.client.push_to_client(message.account_deleted({ id: opts.mesg.id, error: error })); } return [2 /*return*/]; } }); }); } exports.delete_account = delete_account; //# sourceMappingURL=create-account.js.map