smc-hub
Version:
CoCalc: Backend webserver component
314 lines (312 loc) • 15.8 kB
JavaScript
"use strict";
/*
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
* License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details
*/
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
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 };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.site_license_hook = void 0;
var lodash_1 = require("lodash");
var query_1 = require("../query");
var misc_1 = require("smc-util/misc");
var async_utils_1 = require("smc-util/async-utils");
var analytics_1 = require("./analytics");
var quota_1 = require("smc-util/upgrades/quota");
var licenses = undefined;
function get_valid_licenses(db) {
return __awaiter(this, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
if (!(licenses == null)) return [3 /*break*/, 2];
return [4 /*yield*/, async_utils_1.callback2(db.synctable.bind(db), {
table: "site_licenses",
columns: [
"title",
"expires",
"activates",
"upgrades",
"quota",
"run_limit",
],
// TODO: Not bothing with the where condition will be fine up to a few thousand (?) site
// licenses, but after that it could take nontrivial time/memory during hub startup.
// So... this is a ticking time bomb.
//, where: { expires: { ">=": new Date() }, activates: { "<=": new Date() } }
})];
case 1:
licenses = _a.sent();
_a.label = 2;
case 2: return [2 /*return*/, licenses.get()];
}
});
});
}
/*
Call this any time about to *start* the project.
Check for site licenses, then set the site_license field for this project.
The *value* for each key records what the license provides and whether or
not it is actually being used by the project.
If the license provides nothing new compared to what is already provided
by already applied **licenses** and upgrades, then the license is *not*
applied. See
https://github.com/sagemathinc/cocalc/issues/4979
*/
function site_license_hook(db, project_id) {
return __awaiter(this, void 0, void 0, function () {
var err_1;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
_a.trys.push([0, 2, , 3]);
return [4 /*yield*/, site_license_hook0(db, project_id)];
case 1:
_a.sent();
return [3 /*break*/, 3];
case 2:
err_1 = _a.sent();
db._dbg("site_license_hook")("ERROR -- " + err_1);
throw err_1;
case 3: return [2 /*return*/];
}
});
});
}
exports.site_license_hook = site_license_hook;
function site_license_hook0(db, project_id) {
var _a, _b;
return __awaiter(this, void 0, void 0, function () {
var dbg, project, site_license, new_site_license, licenses, _c, _d, _i, license_id, license, is_valid, expires, activates, run_limit, _e, upgrades, quota, field, run_quota, run_quota_with_license, _f, _g, _h, license_id;
var _j;
return __generator(this, function (_k) {
switch (_k.label) {
case 0:
dbg = db._dbg("site_license_hook(\"" + project_id + "\")");
dbg("site_license_hook -- checking for site license");
return [4 /*yield*/, query_1.query({
db: db,
select: ["site_license", "settings", "users"],
table: "projects",
where: { project_id: project_id },
one: true,
})];
case 1:
project = _k.sent();
dbg("project=" + JSON.stringify(project));
if (project.site_license == null || typeof project.site_license != "object") {
dbg("no site licenses set for this project.");
return [2 /*return*/];
}
site_license = project.site_license;
new_site_license = {};
return [4 /*yield*/, get_valid_licenses(db)];
case 2:
licenses = _k.sent();
_c = [];
for (_d in site_license)
_c.push(_d);
_i = 0;
_k.label = 3;
case 3:
if (!(_i < _c.length)) return [3 /*break*/, 11];
license_id = _c[_i];
if (!misc_1.is_valid_uuid_string(license_id)) {
// The site_license is supposed to be a map from uuid's to settings...
// We could put some sort of error here in case, though I don't know what
// we would do with it.
dbg("skipping invalid license " + license_id);
return [3 /*break*/, 10];
}
license = licenses.get(license_id);
dbg("considering license " + license_id + ": " + JSON.stringify(license === null || license === void 0 ? void 0 : license.toJS()));
is_valid = void 0;
if (!(license == null)) return [3 /*break*/, 4];
dbg("License \"" + license_id + "\" does not exist.");
is_valid = false;
return [3 /*break*/, 9];
case 4:
expires = license.get("expires");
activates = license.get("activates");
run_limit = license.get("run_limit");
if (!(expires != null && expires <= new Date())) return [3 /*break*/, 5];
dbg("License \"" + license_id + "\" expired " + expires + ".");
is_valid = false;
return [3 /*break*/, 9];
case 5:
if (!(activates == null || activates > new Date())) return [3 /*break*/, 6];
dbg("License \"" + license_id + "\" has not been explicitly activated yet " + activates + ".");
is_valid = false;
return [3 /*break*/, 9];
case 6:
_e = run_limit;
if (!_e) return [3 /*break*/, 8];
return [4 /*yield*/, analytics_1.number_of_running_projects_using_license(db, license_id)];
case 7:
_e = (_k.sent()) >=
run_limit;
_k.label = 8;
case 8:
if (_e) {
dbg("License \"" + license_id + "\" won't be applied since it would exceed the run limit " + run_limit + ".");
is_valid = false;
}
else {
dbg("license " + license_id + " is valid");
is_valid = true;
}
_k.label = 9;
case 9:
if (is_valid) {
if (license == null)
throw Error("bug");
upgrades = ((_b = (_a = license.get("upgrades")) === null || _a === void 0 ? void 0 : _a.toJS()) !== null && _b !== void 0 ? _b : {});
if (upgrades == null) {
// This is to make typescript happy since QuotaSetting may be null
// (though I don't think upgrades ever could be).
throw Error("bug");
}
quota = license.get("quota");
if (quota) {
upgrades["quota"] = quota.toJS();
}
// remove any zero values to make frontend client code simpler and avoid waste/clutter.
// NOTE: I do assume these 0 fields are removed in some client code, so don't just not do this!
for (field in upgrades) {
if (!upgrades[field]) {
delete upgrades[field];
}
}
dbg("computing run quotas...");
run_quota = quota_1.quota(project.settings, project.users, new_site_license);
run_quota_with_license = quota_1.quota(project.settings, project.users, __assign(__assign({}, new_site_license), (_j = {}, _j[license_id] = upgrades, _j)));
dbg("run_quota=" + JSON.stringify(run_quota));
dbg("run_quota_with_license=" + JSON.stringify(run_quota_with_license));
if (!lodash_1.isEqual(run_quota, run_quota_with_license)) {
dbg("Found a valid license \"" + license_id + "\". Upgrade using it to " + JSON.stringify(upgrades) + ".");
new_site_license[license_id] = upgrades;
}
else {
dbg("Found a valid license \"" + license_id + "\", but it provides nothing new so not using it.");
}
}
else {
dbg("Not currently valid license -- \"" + license_id + "\".");
}
_k.label = 10;
case 10:
_i++;
return [3 /*break*/, 3];
case 11:
if (!!lodash_1.isEqual(site_license, new_site_license)) return [3 /*break*/, 13];
// Now set the site license since something changed.
dbg("setup site license=" + JSON.stringify(new_site_license));
return [4 /*yield*/, query_1.query({
db: db,
query: "UPDATE projects",
where: { project_id: project_id },
jsonb_set: { site_license: new_site_license },
})];
case 12:
_k.sent();
return [3 /*break*/, 14];
case 13:
dbg("no change");
_k.label = 14;
case 14:
_f = [];
for (_g in new_site_license)
_f.push(_g);
_h = 0;
_k.label = 15;
case 15:
if (!(_h < _f.length)) return [3 /*break*/, 18];
license_id = _f[_h];
if (!(misc_1.len(new_site_license[license_id]) > 0)) return [3 /*break*/, 17];
return [4 /*yield*/, update_last_used(db, license_id, dbg)];
case 16:
_k.sent();
_k.label = 17;
case 17:
_h++;
return [3 /*break*/, 15];
case 18: return [2 /*return*/];
}
});
});
}
var last_used = {};
function update_last_used(db, license_id, dbg) {
return __awaiter(this, void 0, void 0, function () {
var now;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
dbg("update_last_used(\"" + license_id + "\")");
now = new Date().valueOf();
if (last_used[license_id] != null &&
now - last_used[license_id] <= 60 * 1000) {
dbg("recently updated so waiting");
// If we updated this entry in the database already within a minute, don't again.
return [2 /*return*/];
}
last_used[license_id] = now;
dbg("did NOT recently update, so updating in database");
return [4 /*yield*/, async_utils_1.callback2(db._query.bind(db), {
query: "UPDATE site_licenses",
set: { last_used: "NOW()" },
where: { id: license_id },
})];
case 1:
_a.sent();
return [2 /*return*/];
}
});
});
}
//# sourceMappingURL=hook.js.map