@proteinjs/user-server 1.11.1 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +32 -0
- package/dist/generated/index.d.ts.map +1 -1
- package/dist/generated/index.js +3 -1
- package/dist/generated/index.js.map +1 -1
- package/dist/src/authorization/UserActivityStamp.d.ts +32 -0
- package/dist/src/authorization/UserActivityStamp.d.ts.map +1 -0
- package/dist/src/authorization/UserActivityStamp.js +126 -0
- package/dist/src/authorization/UserActivityStamp.js.map +1 -0
- package/dist/src/authorization/userCache.d.ts.map +1 -1
- package/dist/src/authorization/userCache.js +9 -0
- package/dist/src/authorization/userCache.js.map +1 -1
- package/dist/src/migrations/SweepMalformedAccessGrants.d.ts +76 -0
- package/dist/src/migrations/SweepMalformedAccessGrants.d.ts.map +1 -0
- package/dist/src/migrations/SweepMalformedAccessGrants.js +354 -0
- package/dist/src/migrations/SweepMalformedAccessGrants.js.map +1 -0
- package/dist/src/routes/devLogin.d.ts +5 -0
- package/dist/src/routes/devLogin.d.ts.map +1 -1
- package/dist/src/routes/devLogin.js +13 -0
- package/dist/src/routes/devLogin.js.map +1 -1
- package/dist/src/services/Roles.d.ts +2 -0
- package/dist/src/services/Roles.d.ts.map +1 -1
- package/dist/src/services/Roles.js +22 -2
- package/dist/src/services/Roles.js.map +1 -1
- package/dist/test/DevLogin.test.js +41 -0
- package/dist/test/DevLogin.test.js.map +1 -1
- package/dist/test/MachineAccounts.integration.test.js +91 -7
- package/dist/test/MachineAccounts.integration.test.js.map +1 -1
- package/dist/test/Roles.integration.test.js +141 -2
- package/dist/test/Roles.integration.test.js.map +1 -1
- package/dist/test/SweepMalformedAccessGrants.integration.test.d.ts +2 -0
- package/dist/test/SweepMalformedAccessGrants.integration.test.d.ts.map +1 -0
- package/dist/test/SweepMalformedAccessGrants.integration.test.js +424 -0
- package/dist/test/SweepMalformedAccessGrants.integration.test.js.map +1 -0
- package/dist/test/UserActivityStamp.integration.test.d.ts +2 -0
- package/dist/test/UserActivityStamp.integration.test.d.ts.map +1 -0
- package/dist/test/UserActivityStamp.integration.test.js +255 -0
- package/dist/test/UserActivityStamp.integration.test.js.map +1 -0
- package/generated/index.ts +3 -1
- package/package.json +4 -3
- package/src/authorization/UserActivityStamp.ts +66 -0
- package/src/authorization/userCache.ts +9 -0
- package/src/migrations/SweepMalformedAccessGrants.ts +229 -0
- package/src/routes/devLogin.ts +15 -0
- package/src/services/Roles.ts +28 -2
- package/test/DevLogin.test.ts +26 -0
- package/test/MachineAccounts.integration.test.ts +76 -33
- package/test/Roles.integration.test.ts +67 -2
- package/test/SweepMalformedAccessGrants.integration.test.ts +255 -0
- package/test/UserActivityStamp.integration.test.ts +122 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
12
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
13
|
+
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
14
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
15
|
+
function step(op) {
|
|
16
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
17
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
18
|
+
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;
|
|
19
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
20
|
+
switch (op[0]) {
|
|
21
|
+
case 0: case 1: t = op; break;
|
|
22
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
23
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
24
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
25
|
+
default:
|
|
26
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
27
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
28
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
29
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
30
|
+
if (t[2]) _.ops.pop();
|
|
31
|
+
_.trys.pop(); continue;
|
|
32
|
+
}
|
|
33
|
+
op = body.call(thisArg, _);
|
|
34
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
35
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
39
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
40
|
+
};
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.UserActivityStamp = void 0;
|
|
43
|
+
var moment_1 = __importDefault(require("moment"));
|
|
44
|
+
var db_1 = require("@proteinjs/db");
|
|
45
|
+
var logger_1 = require("@proteinjs/logger");
|
|
46
|
+
var user_1 = require("@proteinjs/user");
|
|
47
|
+
/**
|
|
48
|
+
* Writes the LAST-ACTIVITY presence stamp (`user_activity`, one row per user — see
|
|
49
|
+
* UserActivityTable's contract): "this human made an interactive request just now".
|
|
50
|
+
*
|
|
51
|
+
* The TRANSPORT keying lives at the call site — userCache.create runs once per session-cookie
|
|
52
|
+
* request (wrapRoute's session-cache build) and only there, so seeded/background contexts
|
|
53
|
+
* (runInUserScope sets session data directly) structurally never reach this class. What this
|
|
54
|
+
* class owns is the ACCOUNT predicate: machine accounts (`isLoadedFromSource`) are refused —
|
|
55
|
+
* their requests do arrive over real sessions (e.g. the error bridge's polling), but a machine
|
|
56
|
+
* logging in is not a person being present.
|
|
57
|
+
*
|
|
58
|
+
* Write behavior mirrors DbSessionStore's touch: throttled per user (a presence fact consumed at
|
|
59
|
+
* day grain needs no finer cadence, and an unthrottled stamp would put a write on EVERY request),
|
|
60
|
+
* fail-open (a lost stamp is a few minutes of staleness, never a failed request — the returned
|
|
61
|
+
* promise NEVER rejects), and race-tolerant (concurrent first stamps contend on the scope-unique
|
|
62
|
+
* index; the loser's error is swallowed as debug).
|
|
63
|
+
*/
|
|
64
|
+
var UserActivityStamp = /** @class */ (function () {
|
|
65
|
+
function UserActivityStamp() {
|
|
66
|
+
this.logger = new logger_1.Logger({ name: this.constructor.name });
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Record that `user` is present on an interactive request. Fire-and-forget safe: errors are
|
|
70
|
+
* handled (and logged) here, so callers may `void` the returned promise.
|
|
71
|
+
*/
|
|
72
|
+
UserActivityStamp.prototype.recordInteractiveRequest = function (user) {
|
|
73
|
+
var _this = this;
|
|
74
|
+
var _a;
|
|
75
|
+
if (!user.id || user.isLoadedFromSource === true) {
|
|
76
|
+
return Promise.resolve();
|
|
77
|
+
}
|
|
78
|
+
var last = (_a = UserActivityStamp.lastStampMs.get(user.id)) !== null && _a !== void 0 ? _a : 0;
|
|
79
|
+
var now = Date.now();
|
|
80
|
+
if (now - last < UserActivityStamp.STAMP_INTERVAL_MS) {
|
|
81
|
+
return Promise.resolve();
|
|
82
|
+
}
|
|
83
|
+
UserActivityStamp.lastStampMs.set(user.id, now);
|
|
84
|
+
if (UserActivityStamp.lastStampMs.size > 10000) {
|
|
85
|
+
UserActivityStamp.lastStampMs.clear(); // bounded memory; worst case is one extra stamp per user
|
|
86
|
+
}
|
|
87
|
+
return this.upsert(user.id).catch(function (error) {
|
|
88
|
+
// Contention (a concurrent request stamped first, racing the scope-unique index) and real
|
|
89
|
+
// failures land here alike; both are harmless to the request. Un-throttle so the next
|
|
90
|
+
// request retries instead of waiting out a full interval on a stamp that never landed.
|
|
91
|
+
UserActivityStamp.lastStampMs.delete(user.id);
|
|
92
|
+
_this.logger.error({ message: 'Failed to write user activity stamp', error: error });
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
UserActivityStamp.prototype.upsert = function (userId) {
|
|
96
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
97
|
+
var db, existing;
|
|
98
|
+
return __generator(this, function (_a) {
|
|
99
|
+
switch (_a.label) {
|
|
100
|
+
case 0:
|
|
101
|
+
db = (0, db_1.getDbAsSystem)();
|
|
102
|
+
return [4 /*yield*/, db.get(user_1.tables.UserActivity, { scope: userId })];
|
|
103
|
+
case 1:
|
|
104
|
+
existing = _a.sent();
|
|
105
|
+
if (!existing) return [3 /*break*/, 3];
|
|
106
|
+
return [4 /*yield*/, db.update(user_1.tables.UserActivity, { id: existing.id, lastActiveAt: (0, moment_1.default)() })];
|
|
107
|
+
case 2:
|
|
108
|
+
_a.sent();
|
|
109
|
+
return [3 /*break*/, 5];
|
|
110
|
+
case 3: return [4 /*yield*/, db.insert(user_1.tables.UserActivity, { scope: userId, lastActiveAt: (0, moment_1.default)() })];
|
|
111
|
+
case 4:
|
|
112
|
+
_a.sent();
|
|
113
|
+
_a.label = 5;
|
|
114
|
+
case 5: return [2 /*return*/];
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
/** Stamp at most this often per user — same cadence class as DbSessionStore.TOUCH_INTERVAL. */
|
|
120
|
+
UserActivityStamp.STAMP_INTERVAL_MS = 1000 * 60 * 5;
|
|
121
|
+
/** Process-wide: userCache is a plain object, so throttle state can't live per-instance. */
|
|
122
|
+
UserActivityStamp.lastStampMs = new Map();
|
|
123
|
+
return UserActivityStamp;
|
|
124
|
+
}());
|
|
125
|
+
exports.UserActivityStamp = UserActivityStamp;
|
|
126
|
+
//# sourceMappingURL=UserActivityStamp.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UserActivityStamp.js","sourceRoot":"","sources":["../../../src/authorization/UserActivityStamp.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,kDAA4B;AAC5B,oCAA8C;AAC9C,4CAA2C;AAC3C,wCAAuE;AAEvE;;;;;;;;;;;;;;;;GAgBG;AACH;IAAA;QAMU,WAAM,GAAG,IAAI,eAAM,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,CAAC;IAqC/D,CAAC;IAnCC;;;OAGG;IACH,oDAAwB,GAAxB,UAAyB,IAAU;QAAnC,iBAoBC;;QAnBC,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,IAAI,CAAC,kBAAkB,KAAK,IAAI,EAAE;YAChD,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;SAC1B;QACD,IAAM,IAAI,GAAG,MAAA,iBAAiB,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC,mCAAI,CAAC,CAAC;QAC7D,IAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACvB,IAAI,GAAG,GAAG,IAAI,GAAG,iBAAiB,CAAC,iBAAiB,EAAE;YACpD,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;SAC1B;QACD,iBAAiB,CAAC,WAAW,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;QAChD,IAAI,iBAAiB,CAAC,WAAW,CAAC,IAAI,GAAG,KAAK,EAAE;YAC9C,iBAAiB,CAAC,WAAW,CAAC,KAAK,EAAE,CAAC,CAAC,yDAAyD;SACjG;QACD,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,UAAC,KAAK;YACtC,0FAA0F;YAC1F,sFAAsF;YACtF,uFAAuF;YACvF,iBAAiB,CAAC,WAAW,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YAC9C,KAAI,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,OAAO,EAAE,qCAAqC,EAAE,KAAK,OAAA,EAAE,CAAC,CAAC;QAC/E,CAAC,CAAC,CAAC;IACL,CAAC;IAEa,kCAAM,GAApB,UAAqB,MAAc;;;;;;wBAC3B,EAAE,GAAG,IAAA,kBAAa,GAAE,CAAC;wBACV,qBAAM,EAAE,CAAC,GAAG,CAAC,aAAM,CAAC,YAAY,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,EAAA;;wBAA/D,QAAQ,GAAG,SAAoD;6BACjE,QAAQ,EAAR,wBAAQ;wBACV,qBAAM,EAAE,CAAC,MAAM,CAAC,aAAM,CAAC,YAAY,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,EAAE,EAAE,YAAY,EAAE,IAAA,gBAAM,GAAE,EAAkB,CAAC,EAAA;;wBAAjG,SAAiG,CAAC;;4BAElG,qBAAM,EAAE,CAAC,MAAM,CAAC,aAAM,CAAC,YAAY,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,IAAA,gBAAM,GAAE,EAAkB,CAAC,EAAA;;wBAA/F,SAA+F,CAAC;;;;;;KAEnG;IAzCD,+FAA+F;IACvE,mCAAiB,GAAG,IAAI,GAAG,EAAE,GAAG,CAAC,AAAhB,CAAiB;IAC1D,4FAA4F;IAC7E,6BAAW,GAAG,IAAI,GAAG,EAAkB,AAA5B,CAA6B;IAuCzD,wBAAC;CAAA,AA3CD,IA2CC;AA3CY,8CAAiB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"userCache.d.ts","sourceRoot":"","sources":["../../../src/authorization/userCache.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAGzD,OAAO,EAAE,IAAI,EAA6C,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"userCache.d.ts","sourceRoot":"","sources":["../../../src/authorization/userCache.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AAGzD,OAAO,EAAE,IAAI,EAA6C,MAAM,iBAAiB,CAAC;AAOlF,eAAO,MAAM,SAAS,EAAE,gBAAgB,CAAC,IAAI,CAqD5C,CAAC"}
|
|
@@ -45,7 +45,9 @@ var db_1 = require("@proteinjs/db");
|
|
|
45
45
|
var logger_1 = require("@proteinjs/logger");
|
|
46
46
|
var user_1 = require("@proteinjs/user");
|
|
47
47
|
var DefaultAdminCredentials_1 = require("../authentication/DefaultAdminCredentials");
|
|
48
|
+
var UserActivityStamp_1 = require("./UserActivityStamp");
|
|
48
49
|
var logger = new logger_1.Logger({ name: 'userCache' });
|
|
50
|
+
var userActivityStamp = new UserActivityStamp_1.UserActivityStamp();
|
|
49
51
|
exports.userCache = {
|
|
50
52
|
key: user_1.USER_SESSION_CACHE_KEY,
|
|
51
53
|
create: function (sessionId, userEmail) { return __awaiter(void 0, void 0, void 0, function () {
|
|
@@ -84,6 +86,13 @@ exports.userCache = {
|
|
|
84
86
|
else if (accountUser) {
|
|
85
87
|
delete accountUser['password'];
|
|
86
88
|
user = accountUser;
|
|
89
|
+
// LAST ACTIVITY (human presence — UserActivityTable's contract): stamped HERE because
|
|
90
|
+
// this cache build runs exactly once per session-cookie request (wrapRoute), i.e. only
|
|
91
|
+
// for interactive transport. Background/seeded contexts (runInUserScope) set session
|
|
92
|
+
// data directly and never pass through, so machinery acting as the user structurally
|
|
93
|
+
// cannot stamp; the stamp itself refuses machine accounts. Fire-and-forget: the
|
|
94
|
+
// returned promise never rejects, and a request never waits on its own stamp.
|
|
95
|
+
void userActivityStamp.recordInteractiveRequest(user);
|
|
87
96
|
}
|
|
88
97
|
else {
|
|
89
98
|
// A session can outlive its account (row deleted, or a dev auto-login for a never-created
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"userCache.js","sourceRoot":"","sources":["../../../src/authorization/userCache.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,kDAA4B;AAE5B,oCAA8C;AAC9C,4CAA2C;AAC3C,wCAAkF;AAClF,qFAAoF;
|
|
1
|
+
{"version":3,"file":"userCache.js","sourceRoot":"","sources":["../../../src/authorization/userCache.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,kDAA4B;AAE5B,oCAA8C;AAC9C,4CAA2C;AAC3C,wCAAkF;AAClF,qFAAoF;AACpF,yDAAwD;AAExD,IAAM,MAAM,GAAG,IAAI,eAAM,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC,CAAC;AACjD,IAAM,iBAAiB,GAAG,IAAI,qCAAiB,EAAE,CAAC;AAErC,QAAA,SAAS,GAA2B;IAC/C,GAAG,EAAE,6BAAsB;IAC3B,MAAM,EAAE,UAAO,SAAiB,EAAE,SAAiB;;;;;oBAC7C,IAAI,GAAG,gBAAS,CAAC;yBACjB,SAAS,EAAT,wBAAS;oBACL,gBAAgB,GAAG,iDAAuB,CAAC,cAAc,EAAE,CAAC;yBAC9D,CAAA,gBAAgB,IAAI,SAAS,IAAI,gBAAgB,CAAC,QAAQ,CAAA,EAA1D,wBAA0D;oBACtD,SAAS,GAAS;wBACtB,IAAI,EAAE,OAAO;wBACb,KAAK,EAAE,gBAAgB,CAAC,QAAQ;wBAChC,QAAQ,EAAE,gBAAgB,CAAC,QAAQ;wBACnC,aAAa,EAAE,IAAI;wBACnB,KAAK,EAAE,CAAC,OAAO,CAAC;wBAChB,OAAO,EAAE,IAAA,gBAAM,GAAE;wBACjB,OAAO,EAAE,IAAA,gBAAM,GAAE;wBACjB,EAAE,EAAE,OAAO;qBACZ,CAAC;oBACF,IAAI,GAAG,SAAS,CAAC;;wBAEG,qBAAM,IAAA,kBAAa,GAAE,CAAC,GAAG,CAAC,aAAM,CAAC,IAAI,EAAE,EAAE,KAAK,EAAE,SAAS,CAAC,WAAW,EAAE,EAAE,CAAC,EAAA;;oBAAxF,WAAW,GAAG,SAA0E;oBAC9F,IAAI,WAAW,IAAI,WAAW,CAAC,MAAM,KAAK,aAAa,EAAE;wBACvD,sFAAsF;wBACtF,iFAAiF;wBACjF,4EAA4E;wBAC5E,MAAM,CAAC,IAAI,CAAC;4BACV,OAAO,EAAE,wEAAwE;4BACjF,GAAG,EAAE,EAAE,SAAS,WAAA,EAAE,SAAS,WAAA,EAAE;yBAC9B,CAAC,CAAC;qBACJ;yBAAM,IAAI,WAAW,EAAE;wBACtB,OAAQ,WAAmB,CAAC,UAAU,CAAC,CAAC;wBACxC,IAAI,GAAG,WAAW,CAAC;wBACnB,sFAAsF;wBACtF,uFAAuF;wBACvF,qFAAqF;wBACrF,qFAAqF;wBACrF,gFAAgF;wBAChF,8EAA8E;wBAC9E,KAAK,iBAAiB,CAAC,wBAAwB,CAAC,IAAI,CAAC,CAAC;qBACvD;yBAAM;wBACL,0FAA0F;wBAC1F,+EAA+E;wBAC/E,sFAAsF;wBACtF,yDAAyD;wBACzD,MAAM,CAAC,IAAI,CAAC;4BACV,OAAO,EAAE,iFAAiF;4BAC1F,GAAG,EAAE,EAAE,SAAS,WAAA,EAAE,SAAS,WAAA,EAAE;yBAC9B,CAAC,CAAC;qBACJ;;wBAIL,sBAAO,IAAI,EAAC;;;SACb;CACF,CAAC"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Migration, SourceRecordLoader, Table } from '@proteinjs/db';
|
|
2
|
+
import { AccessGrant } from '@proteinjs/user';
|
|
3
|
+
/** One access_grant row as the sweep logs and reports it. */
|
|
4
|
+
export interface AccessGrantSweepRow {
|
|
5
|
+
id: string;
|
|
6
|
+
resourceTable?: string;
|
|
7
|
+
accessLevel: AccessGrant['accessLevel'];
|
|
8
|
+
created?: string;
|
|
9
|
+
}
|
|
10
|
+
/** A well-formed grant whose principal or resource id no longer resolves to a row (report only). */
|
|
11
|
+
export interface DanglingAccessGrantRow extends AccessGrantSweepRow {
|
|
12
|
+
principal?: string;
|
|
13
|
+
resource?: string;
|
|
14
|
+
reason: 'principal missing' | 'resource missing' | 'resource table not registered';
|
|
15
|
+
}
|
|
16
|
+
export interface SweepMalformedAccessGrantsResult {
|
|
17
|
+
/** Rows on the invariant's exact complement (NULL principal or NULL resource) — deleted unless `dryRun`. */
|
|
18
|
+
malformed: number;
|
|
19
|
+
rows: AccessGrantSweepRow[];
|
|
20
|
+
dryRun: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface DanglingAccessGrantsReport {
|
|
23
|
+
danglingPrincipal: number;
|
|
24
|
+
danglingResource: number;
|
|
25
|
+
rows: DanglingAccessGrantRow[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* ONE-TIME SWEEP of malformed access_grant residue — rows whose `principal` or `resource`
|
|
29
|
+
* reference is NULL. The AccessGrant well-formedness invariant (`AccessGrantTable.assertWellFormed`)
|
|
30
|
+
* now refuses that shape at insert, and the session-less scope-root bootstrap no longer mints it;
|
|
31
|
+
* this migration clears what the two producers left behind before the fix (the test environment
|
|
32
|
+
* held one; app Deploy to Test 32614670162 crashed on it).
|
|
33
|
+
*
|
|
34
|
+
* The sweep's predicate is the invariant's exact complement (`principal IS NULL OR resource IS
|
|
35
|
+
* NULL`), and it rides BOTH the find and the delete — a well-formed row cannot be selected by the
|
|
36
|
+
* statement that deletes, whatever the id list. Deletes go through the system Db by id, so the
|
|
37
|
+
* registered after-delete watchers fire (a grantee's content_reference cleanup rides
|
|
38
|
+
* `ContentReferenceAccessGrantTableWatcher`). Idempotent: a re-run finds nothing.
|
|
39
|
+
*
|
|
40
|
+
* DANGLING references are a separate class and are NOT deleted here: a well-formed grant whose
|
|
41
|
+
* principal user or resource row was purged out from under it. Those are REPORTED (log +
|
|
42
|
+
* migration output) so the purge paths that strand them can be named; a delete would be a
|
|
43
|
+
* symptom patch on a producer this migration does not own.
|
|
44
|
+
*
|
|
45
|
+
* App-service/system-db path only — never raw Spanner.
|
|
46
|
+
*/
|
|
47
|
+
export declare class SweepMalformedAccessGrants implements SourceRecordLoader<Migration> {
|
|
48
|
+
table: Table<Migration>;
|
|
49
|
+
record: {
|
|
50
|
+
id: string;
|
|
51
|
+
description: string;
|
|
52
|
+
run: () => Promise<{
|
|
53
|
+
malformed: number;
|
|
54
|
+
malformedDryRun: number;
|
|
55
|
+
danglingPrincipal: number;
|
|
56
|
+
danglingResource: number;
|
|
57
|
+
malformedRows: AccessGrantSweepRow[];
|
|
58
|
+
danglingRows: DanglingAccessGrantRow[];
|
|
59
|
+
}>;
|
|
60
|
+
};
|
|
61
|
+
/** Find (and unless `dryRun`, delete) every malformed grant. Each row is logged either way. */
|
|
62
|
+
sweep(options: {
|
|
63
|
+
dryRun: boolean;
|
|
64
|
+
}): Promise<SweepMalformedAccessGrantsResult>;
|
|
65
|
+
/**
|
|
66
|
+
* Report-only scan for well-formed grants whose principal or resource row no longer exists —
|
|
67
|
+
* the purged-user / purged-resource class. Nothing is deleted.
|
|
68
|
+
*/
|
|
69
|
+
reportDangling(): Promise<DanglingAccessGrantsReport>;
|
|
70
|
+
/** The well-formedness invariant's exact complement: a NULL principal or a NULL resource. */
|
|
71
|
+
private malformedPredicate;
|
|
72
|
+
/** Which of `ids` exist in `table`, resolved as system in bounded IN chunks. */
|
|
73
|
+
private existingIds;
|
|
74
|
+
private toRow;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=SweepMalformedAccessGrants.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SweepMalformedAccessGrants.d.ts","sourceRoot":"","sources":["../../../src/migrations/SweepMalformedAccessGrants.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,SAAS,EAKT,kBAAkB,EAClB,KAAK,EAEN,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,WAAW,EAAwB,MAAM,iBAAiB,CAAC;AAGpE,6DAA6D;AAC7D,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAC;IACX,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,WAAW,CAAC,aAAa,CAAC,CAAC;IACxC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,oGAAoG;AACpG,MAAM,WAAW,sBAAuB,SAAQ,mBAAmB;IACjE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,mBAAmB,GAAG,kBAAkB,GAAG,+BAA+B,CAAC;CACpF;AAED,MAAM,WAAW,gCAAgC;IAC/C,4GAA4G;IAC5G,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,mBAAmB,EAAE,CAAC;IAC5B,MAAM,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,0BAA0B;IACzC,iBAAiB,EAAE,MAAM,CAAC;IAC1B,gBAAgB,EAAE,MAAM,CAAC;IACzB,IAAI,EAAE,sBAAsB,EAAE,CAAC;CAChC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,qBAAa,0BAA2B,YAAW,kBAAkB,CAAC,SAAS,CAAC;IAC9E,KAAK,mBAAsB;IAC3B,MAAM;;;;;;;;;;;MAqBJ;IAEF,+FAA+F;IACzF,KAAK,CAAC,OAAO,EAAE;QAAE,MAAM,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,gCAAgC,CAAC;IAgCpF;;;OAGG;IACG,cAAc,IAAI,OAAO,CAAC,0BAA0B,CAAC;IAoE3D,6FAA6F;IAC7F,OAAO,CAAC,kBAAkB;IAO1B,gFAAgF;YAClE,WAAW;IAkBzB,OAAO,CAAC,KAAK;CAQd"}
|
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __assign = (this && this.__assign) || function () {
|
|
3
|
+
__assign = Object.assign || function(t) {
|
|
4
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
5
|
+
s = arguments[i];
|
|
6
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
|
|
7
|
+
t[p] = s[p];
|
|
8
|
+
}
|
|
9
|
+
return t;
|
|
10
|
+
};
|
|
11
|
+
return __assign.apply(this, arguments);
|
|
12
|
+
};
|
|
13
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
14
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
15
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
16
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
17
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
18
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
19
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
var __generator = (this && this.__generator) || function (thisArg, body) {
|
|
23
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g;
|
|
24
|
+
return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
25
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
26
|
+
function step(op) {
|
|
27
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
28
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
29
|
+
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;
|
|
30
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
31
|
+
switch (op[0]) {
|
|
32
|
+
case 0: case 1: t = op; break;
|
|
33
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
34
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
35
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
36
|
+
default:
|
|
37
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
38
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
39
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
40
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
41
|
+
if (t[2]) _.ops.pop();
|
|
42
|
+
_.trys.pop(); continue;
|
|
43
|
+
}
|
|
44
|
+
op = body.call(thisArg, _);
|
|
45
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
46
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
var __asyncValues = (this && this.__asyncValues) || function (o) {
|
|
50
|
+
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
|
51
|
+
var m = o[Symbol.asyncIterator], i;
|
|
52
|
+
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
|
|
53
|
+
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
|
|
54
|
+
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
|
|
55
|
+
};
|
|
56
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
57
|
+
exports.SweepMalformedAccessGrants = void 0;
|
|
58
|
+
var db_1 = require("@proteinjs/db");
|
|
59
|
+
var user_1 = require("@proteinjs/user");
|
|
60
|
+
var logger_1 = require("@proteinjs/logger");
|
|
61
|
+
/**
|
|
62
|
+
* ONE-TIME SWEEP of malformed access_grant residue — rows whose `principal` or `resource`
|
|
63
|
+
* reference is NULL. The AccessGrant well-formedness invariant (`AccessGrantTable.assertWellFormed`)
|
|
64
|
+
* now refuses that shape at insert, and the session-less scope-root bootstrap no longer mints it;
|
|
65
|
+
* this migration clears what the two producers left behind before the fix (the test environment
|
|
66
|
+
* held one; app Deploy to Test 32614670162 crashed on it).
|
|
67
|
+
*
|
|
68
|
+
* The sweep's predicate is the invariant's exact complement (`principal IS NULL OR resource IS
|
|
69
|
+
* NULL`), and it rides BOTH the find and the delete — a well-formed row cannot be selected by the
|
|
70
|
+
* statement that deletes, whatever the id list. Deletes go through the system Db by id, so the
|
|
71
|
+
* registered after-delete watchers fire (a grantee's content_reference cleanup rides
|
|
72
|
+
* `ContentReferenceAccessGrantTableWatcher`). Idempotent: a re-run finds nothing.
|
|
73
|
+
*
|
|
74
|
+
* DANGLING references are a separate class and are NOT deleted here: a well-formed grant whose
|
|
75
|
+
* principal user or resource row was purged out from under it. Those are REPORTED (log +
|
|
76
|
+
* migration output) so the purge paths that strand them can be named; a delete would be a
|
|
77
|
+
* symptom patch on a producer this migration does not own.
|
|
78
|
+
*
|
|
79
|
+
* App-service/system-db path only — never raw Spanner.
|
|
80
|
+
*/
|
|
81
|
+
var SweepMalformedAccessGrants = /** @class */ (function () {
|
|
82
|
+
function SweepMalformedAccessGrants() {
|
|
83
|
+
var _this = this;
|
|
84
|
+
this.table = db_1.tables.Migration;
|
|
85
|
+
this.record = {
|
|
86
|
+
id: '2b7e4d0c-5f1a-4c3e-9b8d-6a2f1e0c7d45',
|
|
87
|
+
description: 'Sweep malformed access_grant rows (NULL principal or NULL resource — the well-formedness invariant’s complement); report dangling references, no delete.',
|
|
88
|
+
run: function () { return __awaiter(_this, void 0, void 0, function () {
|
|
89
|
+
var logger, dry, real, dangling, output;
|
|
90
|
+
return __generator(this, function (_a) {
|
|
91
|
+
switch (_a.label) {
|
|
92
|
+
case 0:
|
|
93
|
+
logger = new logger_1.Logger({ name: SweepMalformedAccessGrants.name });
|
|
94
|
+
return [4 /*yield*/, this.sweep({ dryRun: true })];
|
|
95
|
+
case 1:
|
|
96
|
+
dry = _a.sent();
|
|
97
|
+
return [4 /*yield*/, this.sweep({ dryRun: false })];
|
|
98
|
+
case 2:
|
|
99
|
+
real = _a.sent();
|
|
100
|
+
return [4 /*yield*/, this.reportDangling()];
|
|
101
|
+
case 3:
|
|
102
|
+
dangling = _a.sent();
|
|
103
|
+
output = {
|
|
104
|
+
malformed: real.malformed,
|
|
105
|
+
malformedDryRun: dry.malformed,
|
|
106
|
+
danglingPrincipal: dangling.danglingPrincipal,
|
|
107
|
+
danglingResource: dangling.danglingResource,
|
|
108
|
+
malformedRows: real.rows,
|
|
109
|
+
danglingRows: dangling.rows,
|
|
110
|
+
};
|
|
111
|
+
logger.info({ message: 'SweepMalformedAccessGrants completed', obj: output });
|
|
112
|
+
return [2 /*return*/, output];
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
}); },
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** Find (and unless `dryRun`, delete) every malformed grant. Each row is logged either way. */
|
|
119
|
+
SweepMalformedAccessGrants.prototype.sweep = function (options) {
|
|
120
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
121
|
+
var dryRun, logger, db, findQb, malformed, rows, _i, rows_1, row, _a, malformed_1, grant, deleteQb, result;
|
|
122
|
+
var _this = this;
|
|
123
|
+
return __generator(this, function (_b) {
|
|
124
|
+
switch (_b.label) {
|
|
125
|
+
case 0:
|
|
126
|
+
dryRun = options.dryRun;
|
|
127
|
+
logger = new logger_1.Logger({ name: SweepMalformedAccessGrants.name });
|
|
128
|
+
db = (0, db_1.getDbAsSystem)();
|
|
129
|
+
findQb = this.malformedPredicate(new db_1.QueryBuilderFactory().getQueryBuilder(user_1.tables.AccessGrant)).sort([
|
|
130
|
+
{ field: 'created' },
|
|
131
|
+
]);
|
|
132
|
+
return [4 /*yield*/, db.query(user_1.tables.AccessGrant, findQb)];
|
|
133
|
+
case 1:
|
|
134
|
+
malformed = _b.sent();
|
|
135
|
+
rows = malformed.map(function (grant) { return _this.toRow(grant); });
|
|
136
|
+
for (_i = 0, rows_1 = rows; _i < rows_1.length; _i++) {
|
|
137
|
+
row = rows_1[_i];
|
|
138
|
+
logger.info({
|
|
139
|
+
message: dryRun ? 'Malformed access_grant (dry run)' : 'Deleting malformed access_grant',
|
|
140
|
+
obj: row,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
if (!!dryRun) return [3 /*break*/, 5];
|
|
144
|
+
_a = 0, malformed_1 = malformed;
|
|
145
|
+
_b.label = 2;
|
|
146
|
+
case 2:
|
|
147
|
+
if (!(_a < malformed_1.length)) return [3 /*break*/, 5];
|
|
148
|
+
grant = malformed_1[_a];
|
|
149
|
+
deleteQb = this.malformedPredicate(new db_1.QueryBuilderFactory().getQueryBuilder(user_1.tables.AccessGrant, { id: grant.id }));
|
|
150
|
+
return [4 /*yield*/, db.delete(user_1.tables.AccessGrant, deleteQb)];
|
|
151
|
+
case 3:
|
|
152
|
+
_b.sent();
|
|
153
|
+
_b.label = 4;
|
|
154
|
+
case 4:
|
|
155
|
+
_a++;
|
|
156
|
+
return [3 /*break*/, 2];
|
|
157
|
+
case 5:
|
|
158
|
+
result = { malformed: rows.length, rows: rows, dryRun: dryRun };
|
|
159
|
+
logger.info({ message: dryRun ? 'Sweep dry run' : 'Sweep pass', obj: { malformed: rows.length, dryRun: dryRun } });
|
|
160
|
+
return [2 /*return*/, result];
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
};
|
|
165
|
+
/**
|
|
166
|
+
* Report-only scan for well-formed grants whose principal or resource row no longer exists —
|
|
167
|
+
* the purged-user / purged-resource class. Nothing is deleted.
|
|
168
|
+
*/
|
|
169
|
+
SweepMalformedAccessGrants.prototype.reportDangling = function () {
|
|
170
|
+
var _a, e_1, _b, _c;
|
|
171
|
+
var _d, _e, _f, _g;
|
|
172
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
173
|
+
var logger, db, grants, principalIds, resourceIdsByTable, _h, _j, _k, grant, principalId, resourceId, resourceTable, e_1_1, existingPrincipals, existingResourcesByTable, _loop_1, this_1, _i, _l, tableName, report, _m, grants_1, grant, resourceTable, existingResources, reason, row;
|
|
174
|
+
return __generator(this, function (_o) {
|
|
175
|
+
switch (_o.label) {
|
|
176
|
+
case 0:
|
|
177
|
+
logger = new logger_1.Logger({ name: SweepMalformedAccessGrants.name });
|
|
178
|
+
db = (0, db_1.getDbAsSystem)();
|
|
179
|
+
grants = [];
|
|
180
|
+
principalIds = new Set();
|
|
181
|
+
resourceIdsByTable = new Map();
|
|
182
|
+
_o.label = 1;
|
|
183
|
+
case 1:
|
|
184
|
+
_o.trys.push([1, 6, 7, 12]);
|
|
185
|
+
_h = true, _j = __asyncValues(new db_1.RecordIterator(user_1.tables.AccessGrant, {}, 200, db));
|
|
186
|
+
_o.label = 2;
|
|
187
|
+
case 2: return [4 /*yield*/, _j.next()];
|
|
188
|
+
case 3:
|
|
189
|
+
if (!(_k = _o.sent(), _a = _k.done, !_a)) return [3 /*break*/, 5];
|
|
190
|
+
_c = _k.value;
|
|
191
|
+
_h = false;
|
|
192
|
+
grant = _c;
|
|
193
|
+
principalId = (_d = grant.principal) === null || _d === void 0 ? void 0 : _d._id;
|
|
194
|
+
resourceId = (_e = grant.resource) === null || _e === void 0 ? void 0 : _e._id;
|
|
195
|
+
if (!principalId || !resourceId) {
|
|
196
|
+
return [3 /*break*/, 4]; // the malformed class — `sweep` owns it
|
|
197
|
+
}
|
|
198
|
+
grants.push(grant);
|
|
199
|
+
principalIds.add(principalId);
|
|
200
|
+
resourceTable = (_f = grant.resourceTable) !== null && _f !== void 0 ? _f : grant.resource._table;
|
|
201
|
+
if (!resourceIdsByTable.has(resourceTable)) {
|
|
202
|
+
resourceIdsByTable.set(resourceTable, new Set());
|
|
203
|
+
}
|
|
204
|
+
resourceIdsByTable.get(resourceTable).add(resourceId);
|
|
205
|
+
_o.label = 4;
|
|
206
|
+
case 4:
|
|
207
|
+
_h = true;
|
|
208
|
+
return [3 /*break*/, 2];
|
|
209
|
+
case 5: return [3 /*break*/, 12];
|
|
210
|
+
case 6:
|
|
211
|
+
e_1_1 = _o.sent();
|
|
212
|
+
e_1 = { error: e_1_1 };
|
|
213
|
+
return [3 /*break*/, 12];
|
|
214
|
+
case 7:
|
|
215
|
+
_o.trys.push([7, , 10, 11]);
|
|
216
|
+
if (!(!_h && !_a && (_b = _j.return))) return [3 /*break*/, 9];
|
|
217
|
+
return [4 /*yield*/, _b.call(_j)];
|
|
218
|
+
case 8:
|
|
219
|
+
_o.sent();
|
|
220
|
+
_o.label = 9;
|
|
221
|
+
case 9: return [3 /*break*/, 11];
|
|
222
|
+
case 10:
|
|
223
|
+
if (e_1) throw e_1.error;
|
|
224
|
+
return [7 /*endfinally*/];
|
|
225
|
+
case 11: return [7 /*endfinally*/];
|
|
226
|
+
case 12: return [4 /*yield*/, this.existingIds(user_1.tables.User, principalIds)];
|
|
227
|
+
case 13:
|
|
228
|
+
existingPrincipals = _o.sent();
|
|
229
|
+
existingResourcesByTable = new Map();
|
|
230
|
+
_loop_1 = function (tableName) {
|
|
231
|
+
var table, _p, _q, _r, _s;
|
|
232
|
+
return __generator(this, function (_t) {
|
|
233
|
+
switch (_t.label) {
|
|
234
|
+
case 0:
|
|
235
|
+
table = (0, db_1.getTables)().find(function (candidate) { return candidate.name === tableName; });
|
|
236
|
+
_q = (_p = existingResourcesByTable).set;
|
|
237
|
+
_r = [tableName];
|
|
238
|
+
if (!table) return [3 /*break*/, 2];
|
|
239
|
+
return [4 /*yield*/, this_1.existingIds(table, resourceIdsByTable.get(tableName))];
|
|
240
|
+
case 1:
|
|
241
|
+
_s = _t.sent();
|
|
242
|
+
return [3 /*break*/, 3];
|
|
243
|
+
case 2:
|
|
244
|
+
_s = null;
|
|
245
|
+
_t.label = 3;
|
|
246
|
+
case 3:
|
|
247
|
+
_q.apply(_p, _r.concat([_s]));
|
|
248
|
+
return [2 /*return*/];
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
};
|
|
252
|
+
this_1 = this;
|
|
253
|
+
_i = 0, _l = Array.from(resourceIdsByTable.keys());
|
|
254
|
+
_o.label = 14;
|
|
255
|
+
case 14:
|
|
256
|
+
if (!(_i < _l.length)) return [3 /*break*/, 17];
|
|
257
|
+
tableName = _l[_i];
|
|
258
|
+
return [5 /*yield**/, _loop_1(tableName)];
|
|
259
|
+
case 15:
|
|
260
|
+
_o.sent();
|
|
261
|
+
_o.label = 16;
|
|
262
|
+
case 16:
|
|
263
|
+
_i++;
|
|
264
|
+
return [3 /*break*/, 14];
|
|
265
|
+
case 17:
|
|
266
|
+
report = { danglingPrincipal: 0, danglingResource: 0, rows: [] };
|
|
267
|
+
for (_m = 0, grants_1 = grants; _m < grants_1.length; _m++) {
|
|
268
|
+
grant = grants_1[_m];
|
|
269
|
+
resourceTable = (_g = grant.resourceTable) !== null && _g !== void 0 ? _g : grant.resource._table;
|
|
270
|
+
existingResources = existingResourcesByTable.get(resourceTable);
|
|
271
|
+
reason = void 0;
|
|
272
|
+
if (!existingPrincipals.has(grant.principal._id)) {
|
|
273
|
+
reason = 'principal missing';
|
|
274
|
+
report.danglingPrincipal++;
|
|
275
|
+
}
|
|
276
|
+
else if (existingResources === null) {
|
|
277
|
+
reason = 'resource table not registered';
|
|
278
|
+
report.danglingResource++;
|
|
279
|
+
}
|
|
280
|
+
else if (!existingResources.has(grant.resource._id)) {
|
|
281
|
+
reason = 'resource missing';
|
|
282
|
+
report.danglingResource++;
|
|
283
|
+
}
|
|
284
|
+
if (!reason) {
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
row = __assign(__assign({}, this.toRow(grant)), { principal: grant.principal._id, resource: grant.resource._id, reason: reason });
|
|
288
|
+
report.rows.push(row);
|
|
289
|
+
logger.warn({ message: 'Dangling access_grant (report only, not deleted)', obj: row });
|
|
290
|
+
}
|
|
291
|
+
logger.info({
|
|
292
|
+
message: 'Dangling access_grant report',
|
|
293
|
+
obj: { danglingPrincipal: report.danglingPrincipal, danglingResource: report.danglingResource },
|
|
294
|
+
});
|
|
295
|
+
return [2 /*return*/, report];
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
};
|
|
300
|
+
/** The well-formedness invariant's exact complement: a NULL principal or a NULL resource. */
|
|
301
|
+
SweepMalformedAccessGrants.prototype.malformedPredicate = function (qb) {
|
|
302
|
+
return qb.or([
|
|
303
|
+
{ field: 'principal', operator: 'IS NULL' },
|
|
304
|
+
{ field: 'resource', operator: 'IS NULL' },
|
|
305
|
+
]);
|
|
306
|
+
};
|
|
307
|
+
/** Which of `ids` exist in `table`, resolved as system in bounded IN chunks. */
|
|
308
|
+
SweepMalformedAccessGrants.prototype.existingIds = function (table, ids) {
|
|
309
|
+
return __awaiter(this, void 0, void 0, function () {
|
|
310
|
+
var existing, all, chunkSize, start, chunk, qb, found, _i, found_1, record;
|
|
311
|
+
return __generator(this, function (_a) {
|
|
312
|
+
switch (_a.label) {
|
|
313
|
+
case 0:
|
|
314
|
+
existing = new Set();
|
|
315
|
+
all = Array.from(ids);
|
|
316
|
+
chunkSize = 200;
|
|
317
|
+
start = 0;
|
|
318
|
+
_a.label = 1;
|
|
319
|
+
case 1:
|
|
320
|
+
if (!(start < all.length)) return [3 /*break*/, 4];
|
|
321
|
+
chunk = all.slice(start, start + chunkSize);
|
|
322
|
+
qb = new db_1.QueryBuilderFactory()
|
|
323
|
+
.getQueryBuilder(table)
|
|
324
|
+
.select({ fields: ['id'] })
|
|
325
|
+
.condition({ field: 'id', operator: 'IN', value: chunk });
|
|
326
|
+
return [4 /*yield*/, (0, db_1.getDbAsSystem)().query(table, qb)];
|
|
327
|
+
case 2:
|
|
328
|
+
found = (_a.sent());
|
|
329
|
+
for (_i = 0, found_1 = found; _i < found_1.length; _i++) {
|
|
330
|
+
record = found_1[_i];
|
|
331
|
+
existing.add(record.id);
|
|
332
|
+
}
|
|
333
|
+
_a.label = 3;
|
|
334
|
+
case 3:
|
|
335
|
+
start += chunkSize;
|
|
336
|
+
return [3 /*break*/, 1];
|
|
337
|
+
case 4: return [2 /*return*/, existing];
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
};
|
|
342
|
+
SweepMalformedAccessGrants.prototype.toRow = function (grant) {
|
|
343
|
+
var _a;
|
|
344
|
+
return {
|
|
345
|
+
id: grant.id,
|
|
346
|
+
resourceTable: grant.resourceTable,
|
|
347
|
+
accessLevel: grant.accessLevel,
|
|
348
|
+
created: (_a = grant.created) === null || _a === void 0 ? void 0 : _a.toISOString(),
|
|
349
|
+
};
|
|
350
|
+
};
|
|
351
|
+
return SweepMalformedAccessGrants;
|
|
352
|
+
}());
|
|
353
|
+
exports.SweepMalformedAccessGrants = SweepMalformedAccessGrants;
|
|
354
|
+
//# sourceMappingURL=SweepMalformedAccessGrants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SweepMalformedAccessGrants.js","sourceRoot":"","sources":["../../../src/migrations/SweepMalformedAccessGrants.ts"],"names":[],"mappings":"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"}
|
|
@@ -13,6 +13,11 @@ import { Route } from '@proteinjs/server-api';
|
|
|
13
13
|
* Domain rail: `?email` must share `DEV_AUTO_LOGIN_EMAIL`'s domain — even a dev server must not
|
|
14
14
|
* mint sessions (much less accounts) for arbitrary domains; anything else answers 400.
|
|
15
15
|
*
|
|
16
|
+
* Shape rail: `?email` must be a well-formed address (the house `emailRegex`). The domain rail
|
|
17
|
+
* alone let `?email=brent+lane-a@…` through when the `+` was left unencoded — a query-string `+`
|
|
18
|
+
* decodes to a SPACE, so the route minted a stray `brent lane-a@…` account. The 400 names the
|
|
19
|
+
* remedy (`%2B`) because plus-addressing is the fan-out convention this door exists for.
|
|
20
|
+
*
|
|
16
21
|
* A missing account is created through the normal signup creation path (`Signup.createAccount`)
|
|
17
22
|
* as a normal test user — password `test`, matching the seeded test-account convention, so
|
|
18
23
|
* interactive login works for the same identity. Composes with userCache's missing-account→guest
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"devLogin.d.ts","sourceRoot":"","sources":["../../../src/routes/devLogin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"devLogin.d.ts","sourceRoot":"","sources":["../../../src/routes/devLogin.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC;AAY9C;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,eAAO,MAAM,QAAQ,EAAE,KAgDtB,CAAC"}
|