@astram/identity 0.13.0 → 0.14.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/dist/features/authz/grant-admin.service.d.ts +1 -1
- package/dist/features/authz/grant-admin.service.d.ts.map +1 -1
- package/dist/features/authz/grant-admin.service.js +8 -0
- package/dist/features/authz/grant-ceiling.d.ts +11 -3
- package/dist/features/authz/grant-ceiling.d.ts.map +1 -1
- package/dist/features/authz/grant-ceiling.js +20 -3
- package/dist/features/authz/record-authorization.d.ts +1 -1
- package/dist/features/authz/record-authorization.d.ts.map +1 -1
- package/dist/features/authz/record-authorization.js +3 -2
- package/dist/features/company/company-hierarchy.service.d.ts +6 -0
- package/dist/features/company/company-hierarchy.service.d.ts.map +1 -1
- package/dist/features/company/company-hierarchy.service.js +19 -0
- package/dist/features/identity-feature.module.d.ts.map +1 -1
- package/dist/features/identity-feature.module.js +13 -0
- package/dist/features/membership/membership.service.d.ts +9 -0
- package/dist/features/membership/membership.service.d.ts.map +1 -1
- package/dist/features/membership/membership.service.js +40 -0
- package/dist/features/membership/posting.service.d.ts +18 -0
- package/dist/features/membership/posting.service.d.ts.map +1 -1
- package/dist/features/membership/posting.service.js +85 -27
- package/dist/features/org-bootstrap/org-bootstrap-error.d.ts +20 -0
- package/dist/features/org-bootstrap/org-bootstrap-error.d.ts.map +1 -0
- package/dist/features/org-bootstrap/org-bootstrap-error.js +24 -0
- package/dist/features/org-bootstrap/org-bootstrap-file.d.ts +58 -0
- package/dist/features/org-bootstrap/org-bootstrap-file.d.ts.map +1 -0
- package/dist/features/org-bootstrap/org-bootstrap-file.js +323 -0
- package/dist/features/org-bootstrap/org-bootstrap-plan.d.ts +79 -0
- package/dist/features/org-bootstrap/org-bootstrap-plan.d.ts.map +1 -0
- package/dist/features/org-bootstrap/org-bootstrap-plan.js +288 -0
- package/dist/features/org-bootstrap/org-bootstrap-runner.harness.d.ts +259 -0
- package/dist/features/org-bootstrap/org-bootstrap-runner.harness.d.ts.map +1 -0
- package/dist/features/org-bootstrap/org-bootstrap-runner.harness.js +604 -0
- package/dist/features/org-bootstrap/org-bootstrap-runner.mocks.d.ts +24 -0
- package/dist/features/org-bootstrap/org-bootstrap-runner.mocks.d.ts.map +1 -0
- package/dist/features/org-bootstrap/org-bootstrap-runner.mocks.js +39 -0
- package/dist/features/org-bootstrap/org-bootstrap.manifest.d.ts +19 -0
- package/dist/features/org-bootstrap/org-bootstrap.manifest.d.ts.map +1 -0
- package/dist/features/org-bootstrap/org-bootstrap.manifest.js +30 -0
- package/dist/features/org-bootstrap/org-bootstrap.tokens.d.ts +8 -0
- package/dist/features/org-bootstrap/org-bootstrap.tokens.d.ts.map +1 -0
- package/dist/features/org-bootstrap/org-bootstrap.tokens.js +10 -0
- package/dist/features/org-bootstrap/org-names.d.ts +12 -0
- package/dist/features/org-bootstrap/org-names.d.ts.map +1 -0
- package/dist/features/org-bootstrap/org-names.js +26 -0
- package/dist/features/org-bootstrap/org-state.reader.d.ts +15 -0
- package/dist/features/org-bootstrap/org-state.reader.d.ts.map +1 -0
- package/dist/features/org-bootstrap/org-state.reader.js +225 -0
- package/dist/features/org-bootstrap/root-password-source.d.ts +9 -0
- package/dist/features/org-bootstrap/root-password-source.d.ts.map +1 -0
- package/dist/features/org-bootstrap/root-password-source.js +57 -0
- package/dist/features/org-bootstrap/run-org-bootstrap.d.ts +42 -0
- package/dist/features/org-bootstrap/run-org-bootstrap.d.ts.map +1 -0
- package/dist/features/org-bootstrap/run-org-bootstrap.js +688 -0
- package/dist/features/org-bootstrap/set-password-links.d.ts +30 -0
- package/dist/features/org-bootstrap/set-password-links.d.ts.map +1 -0
- package/dist/features/org-bootstrap/set-password-links.js +55 -0
- package/dist/features/org-structure/org-catalog.service.d.ts +13 -0
- package/dist/features/org-structure/org-catalog.service.d.ts.map +1 -1
- package/dist/features/org-structure/org-catalog.service.js +40 -0
- package/dist/features/role/role-owner-transition.d.ts +17 -0
- package/dist/features/role/role-owner-transition.d.ts.map +1 -0
- package/dist/features/role/role-owner-transition.js +23 -0
- package/dist/features/role/role.controller.d.ts +1 -1
- package/dist/features/role/role.controller.d.ts.map +1 -1
- package/dist/features/role/role.controller.js +6 -4
- package/dist/features/role/role.service.d.ts +8 -2
- package/dist/features/role/role.service.d.ts.map +1 -1
- package/dist/features/role/role.service.js +13 -3
- package/dist/features/user/credential-provisioner.token.d.ts +8 -0
- package/dist/features/user/credential-provisioner.token.d.ts.map +1 -0
- package/dist/features/user/credential-provisioner.token.js +10 -0
- package/dist/features/user/user.controller.d.ts +1 -3
- package/dist/features/user/user.controller.d.ts.map +1 -1
- package/dist/features/user/user.controller.js +2 -2
- package/dist/features/user/user.service.d.ts +28 -5
- package/dist/features/user/user.service.d.ts.map +1 -1
- package/dist/features/user/user.service.js +45 -12
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -1
- package/dist/internal.d.ts +2 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +3 -1
- package/dist/role.object.d.ts +1 -0
- package/dist/role.object.d.ts.map +1 -1
- package/dist/role.object.js +8 -0
- package/migrations/1710000000101-AddRoleOwnerCompany.cjs +98 -0
- package/package.json +3 -3
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.auditRowsFor = exports.linkLines = exports.callsOf = exports.writeCalls = exports.isWriteCall = exports.sha256 = exports.FakeApp = exports.current = exports.emptyState = exports.FILE_DAVE = exports.FILE_MINI = exports.FILE_TWO_TENANTS = exports.FILE_MAIN = exports.PROD_ENV = exports.TEST_ENV = exports.ROOT_PW_VAR = exports.LOCK_NAME = exports.DEFAULT_DEV_BASE = exports.BASE = exports.OPERATOR = exports.PW = exports.SCHEMAS = void 0;
|
|
4
|
+
exports.stateMini = stateMini;
|
|
5
|
+
exports.makeWorld = makeWorld;
|
|
6
|
+
exports.noWritesAtAll = noWritesAtAll;
|
|
7
|
+
exports.lockProblems = lockProblems;
|
|
8
|
+
exports.visibleText = visibleText;
|
|
9
|
+
exports.linkFor = linkFor;
|
|
10
|
+
/**
|
|
11
|
+
* G-009 T10 test harness for the org bootstrap runner specs (fakes only: no DB, no Redis, no Nest).
|
|
12
|
+
*
|
|
13
|
+
* Not a spec. It builds a `World`: a fake `INestApplicationContext` plus recording fakes for every
|
|
14
|
+
* collaborator the runner reaches. The runner contract these fakes pin down is stated in
|
|
15
|
+
* `org-bootstrap-runner.spec.ts` (header). Summary of the seams:
|
|
16
|
+
*
|
|
17
|
+
* - singletons come from `app.get(token, { strict: false })`: the authz pool (`IDENTITY_AUTHZ_POOL`),
|
|
18
|
+
* `IDENTITY_FRAMEWORK_SCHEMAS`, `IDENTITY_CREDENTIAL_PROVISIONER`, `SHIELD_RESET_TOKEN_STORE`,
|
|
19
|
+
* `GrantAdminService` (listCatalog + grant take an explicit principal; no request needed);
|
|
20
|
+
* - request-scoped services come from `ContextIdFactory.create()`, `app.registerRequestByContextId(
|
|
21
|
+
* { principal }, ctx)` and `app.resolve(ServiceClass, ctx, { strict: false })`, one context per step;
|
|
22
|
+
* - `OrgStateReader` and `transitionRoleOwner` are module-mocked (their own specs cover them).
|
|
23
|
+
*/
|
|
24
|
+
const node_crypto_1 = require("node:crypto");
|
|
25
|
+
const guard_1 = require("@astram/guard");
|
|
26
|
+
const company_service_js_1 = require("../company/company.service.js");
|
|
27
|
+
const company_hierarchy_service_js_1 = require("../company/company-hierarchy.service.js");
|
|
28
|
+
const role_service_js_1 = require("../role/role.service.js");
|
|
29
|
+
const user_service_js_1 = require("../user/user.service.js");
|
|
30
|
+
const membership_service_js_1 = require("../membership/membership.service.js");
|
|
31
|
+
const posting_service_js_1 = require("../membership/posting.service.js");
|
|
32
|
+
const org_catalog_service_js_1 = require("../org-structure/org-catalog.service.js");
|
|
33
|
+
const grant_admin_service_js_1 = require("../authz/grant-admin.service.js");
|
|
34
|
+
const framework_schemas_token_js_1 = require("../framework-schemas.token.js");
|
|
35
|
+
const credential_provisioner_token_js_1 = require("../user/credential-provisioner.token.js");
|
|
36
|
+
const org_bootstrap_runner_mocks_js_1 = require("./org-bootstrap-runner.mocks.js");
|
|
37
|
+
Object.defineProperty(exports, "current", { enumerable: true, get: function () { return org_bootstrap_runner_mocks_js_1.current; } });
|
|
38
|
+
const org_bootstrap_manifest_js_1 = require("./org-bootstrap.manifest.js");
|
|
39
|
+
// ---------------------------------------------------------------- constants
|
|
40
|
+
exports.SCHEMAS = {
|
|
41
|
+
namespace: 'zzns',
|
|
42
|
+
app: 'zzns_app',
|
|
43
|
+
identity: 'zzns_identity',
|
|
44
|
+
authz: 'zzns_authz',
|
|
45
|
+
audit: 'zzns_audit',
|
|
46
|
+
isDefault: false,
|
|
47
|
+
};
|
|
48
|
+
exports.PW = 'Zq-root-pass-1';
|
|
49
|
+
exports.OPERATOR = 'op.person@example.test';
|
|
50
|
+
exports.BASE = 'https://app.example.test/reset-password';
|
|
51
|
+
exports.DEFAULT_DEV_BASE = 'http://localhost:5173/reset-password';
|
|
52
|
+
exports.LOCK_NAME = 'astram_org_bootstrap:zzns_app';
|
|
53
|
+
exports.ROOT_PW_VAR = 'ASTRAM_BOOTSTRAP_ROOT_PASSWORD_ZROOT';
|
|
54
|
+
/** Env of a development/test run that would pass every refusal. */
|
|
55
|
+
exports.TEST_ENV = {
|
|
56
|
+
NODE_ENV: 'test',
|
|
57
|
+
ASTRAM_BOOTSTRAP_OPERATOR: exports.OPERATOR,
|
|
58
|
+
[exports.ROOT_PW_VAR]: exports.PW,
|
|
59
|
+
RESET_LINK_BASE_URL: exports.BASE,
|
|
60
|
+
};
|
|
61
|
+
/** Env of a production run; the root password must be secret-ref resolved (the spec adds ROOT_PW_VAR to its hoisted secretRefs set). */
|
|
62
|
+
exports.PROD_ENV = {
|
|
63
|
+
NODE_ENV: 'production',
|
|
64
|
+
ASTRAM_BOOTSTRAP_OPERATOR: exports.OPERATOR,
|
|
65
|
+
[exports.ROOT_PW_VAR]: exports.PW,
|
|
66
|
+
RESET_LINK_BASE_URL: exports.BASE,
|
|
67
|
+
};
|
|
68
|
+
// ---------------------------------------------------------------- fixtures
|
|
69
|
+
/** Fresh-database file: 1 root, 2 companies, 1 dept, 1 branch, 3 roles, 3 non-root users. */
|
|
70
|
+
exports.FILE_MAIN = {
|
|
71
|
+
version: 1,
|
|
72
|
+
roots: [{ username: 'zroot', email: 'zroot@example.test', homeCompany: 'Acme Co' }],
|
|
73
|
+
tenants: [
|
|
74
|
+
{
|
|
75
|
+
name: 'Acme Co',
|
|
76
|
+
departments: ['Sales'],
|
|
77
|
+
branches: ['HQ'],
|
|
78
|
+
roles: [
|
|
79
|
+
{ name: 'Zrole Admin', permissions: ['identity:user:read', 'identity:company:read'] },
|
|
80
|
+
{ name: 'Zrole Empty', permissions: [] },
|
|
81
|
+
],
|
|
82
|
+
users: [
|
|
83
|
+
{
|
|
84
|
+
username: 'zalice',
|
|
85
|
+
email: 'zalice@example.test',
|
|
86
|
+
postings: [{ role: 'Zrole Admin', department: 'Sales', branch: 'HQ' }],
|
|
87
|
+
},
|
|
88
|
+
{ username: 'zbob', email: 'zbob@example.test' },
|
|
89
|
+
],
|
|
90
|
+
children: [
|
|
91
|
+
{
|
|
92
|
+
name: 'Acme Sub',
|
|
93
|
+
roles: [{ name: 'Zrole Sub', permissions: [] }],
|
|
94
|
+
users: [
|
|
95
|
+
{
|
|
96
|
+
username: 'zcarol',
|
|
97
|
+
email: 'zcarol@example.test',
|
|
98
|
+
postings: [{ role: 'Zrole Sub' }],
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
};
|
|
106
|
+
/** Two tenants: used to prove `operationRoleIds` never reach across tenants. */
|
|
107
|
+
exports.FILE_TWO_TENANTS = {
|
|
108
|
+
version: 1,
|
|
109
|
+
roots: [{ username: 'zroot', email: 'zroot@example.test', homeCompany: 'Acme Co' }],
|
|
110
|
+
tenants: [
|
|
111
|
+
{
|
|
112
|
+
name: 'Acme Co',
|
|
113
|
+
roles: [{ name: 'Zrole Admin', permissions: [] }],
|
|
114
|
+
users: [
|
|
115
|
+
{ username: 'zalice', email: 'zalice@example.test', postings: [{ role: 'Zrole Admin' }] },
|
|
116
|
+
],
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: 'Beta Co',
|
|
120
|
+
roles: [{ name: 'Zrole Beta', permissions: [] }],
|
|
121
|
+
users: [{ username: 'zdan', email: 'zdan@example.test', postings: [{ role: 'Zrole Beta' }] }],
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
};
|
|
125
|
+
/** One tenant already holding everything: the state below makes every entry `unchanged`. */
|
|
126
|
+
exports.FILE_MINI = {
|
|
127
|
+
version: 1,
|
|
128
|
+
roots: [{ username: 'zroot', email: 'zroot@example.test', homeCompany: 'Acme Co' }],
|
|
129
|
+
tenants: [
|
|
130
|
+
{
|
|
131
|
+
name: 'Acme Co',
|
|
132
|
+
roles: [{ name: 'Zrole Admin', permissions: ['identity:user:read'] }],
|
|
133
|
+
users: [
|
|
134
|
+
{ username: 'zalice', email: 'zalice@example.test', postings: [{ role: 'Zrole Admin' }] },
|
|
135
|
+
],
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
};
|
|
139
|
+
/** A tenant with one user whose membership does not exist yet (for the "existing user" cases). */
|
|
140
|
+
exports.FILE_DAVE = {
|
|
141
|
+
version: 1,
|
|
142
|
+
roots: [{ username: 'zroot', email: 'zroot@example.test', homeCompany: 'Acme Co' }],
|
|
143
|
+
tenants: [
|
|
144
|
+
{
|
|
145
|
+
name: 'Acme Co',
|
|
146
|
+
roles: [{ name: 'Zrole Admin', permissions: [] }],
|
|
147
|
+
users: [
|
|
148
|
+
{ username: 'zdave', email: 'zdave@example.test', postings: [{ role: 'Zrole Admin' }] },
|
|
149
|
+
],
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
};
|
|
153
|
+
const emptyState = () => ({
|
|
154
|
+
companies: [],
|
|
155
|
+
departments: [],
|
|
156
|
+
branches: [],
|
|
157
|
+
roles: [],
|
|
158
|
+
users: [],
|
|
159
|
+
emailHolders: [],
|
|
160
|
+
ambiguousCompanies: [],
|
|
161
|
+
});
|
|
162
|
+
exports.emptyState = emptyState;
|
|
163
|
+
/** The state that makes FILE_MINI entirely `unchanged`, alice's credential as given. */
|
|
164
|
+
function stateMini(aliceCredential = 'usable') {
|
|
165
|
+
return {
|
|
166
|
+
companies: [{ path: 'Acme Co', departments: [], branches: [] }],
|
|
167
|
+
departments: [],
|
|
168
|
+
branches: [],
|
|
169
|
+
roles: [
|
|
170
|
+
{
|
|
171
|
+
name: 'Zrole Admin',
|
|
172
|
+
ownerTenant: 'Acme Co',
|
|
173
|
+
heldOutside: false,
|
|
174
|
+
permissions: ['identity:user:read'],
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
users: [
|
|
178
|
+
{
|
|
179
|
+
username: 'zroot',
|
|
180
|
+
email: 'zroot@example.test',
|
|
181
|
+
credential: 'usable',
|
|
182
|
+
isPlatformSuperuser: true,
|
|
183
|
+
// A bootstrapped root holds no membership (R1-1: an existing root with one is a conflict).
|
|
184
|
+
memberships: [],
|
|
185
|
+
membershipsOutside: false,
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
username: 'zalice',
|
|
189
|
+
email: 'zalice@example.test',
|
|
190
|
+
credential: aliceCredential,
|
|
191
|
+
isPlatformSuperuser: false,
|
|
192
|
+
memberships: [
|
|
193
|
+
{ companyPath: 'Acme Co', active: true, postings: [{ role: 'Zrole Admin' }] },
|
|
194
|
+
],
|
|
195
|
+
membershipsOutside: false,
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
emailHolders: [],
|
|
199
|
+
ambiguousCompanies: [],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
const READ_METHODS = new Set(['listMemberships', 'listCatalog']);
|
|
203
|
+
const isBootstrapPrincipal = (a) => typeof a === 'object' && a !== null && a.id === org_bootstrap_manifest_js_1.ORG_BOOTSTRAP_PRINCIPAL_ID;
|
|
204
|
+
class FakeApp {
|
|
205
|
+
singletons;
|
|
206
|
+
scoped;
|
|
207
|
+
initCalls = 0;
|
|
208
|
+
requests = new Map();
|
|
209
|
+
resolved = [];
|
|
210
|
+
constructor(singletons, scoped) {
|
|
211
|
+
this.singletons = singletons;
|
|
212
|
+
this.scoped = scoped;
|
|
213
|
+
}
|
|
214
|
+
async init() {
|
|
215
|
+
this.initCalls += 1;
|
|
216
|
+
return this;
|
|
217
|
+
}
|
|
218
|
+
get(token, _options) {
|
|
219
|
+
if (!this.singletons.has(token)) {
|
|
220
|
+
throw new Error(`Nest could not find ${String(token)} element (this provider does not exist)`);
|
|
221
|
+
}
|
|
222
|
+
return this.singletons.get(token);
|
|
223
|
+
}
|
|
224
|
+
registerRequestByContextId(request, contextId) {
|
|
225
|
+
this.requests.set(contextId, request);
|
|
226
|
+
}
|
|
227
|
+
async resolve(token, contextId, _options) {
|
|
228
|
+
const factory = this.scoped.get(token);
|
|
229
|
+
if (factory === undefined) {
|
|
230
|
+
if (this.singletons.has(token))
|
|
231
|
+
return this.singletons.get(token);
|
|
232
|
+
throw new Error(`Nest could not find ${String(token)} element (this provider does not exist)`);
|
|
233
|
+
}
|
|
234
|
+
const principal = this.requests.get(contextId)?.principal;
|
|
235
|
+
this.resolved.push({ token, principal });
|
|
236
|
+
return factory(principal);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
exports.FakeApp = FakeApp;
|
|
240
|
+
const uuidN = (n) => `00000000-0000-4000-8000-${String(n).padStart(12, '0')}`;
|
|
241
|
+
function makeWorld(options) {
|
|
242
|
+
const idMap = new Map();
|
|
243
|
+
let counter = 100;
|
|
244
|
+
const world = {};
|
|
245
|
+
world.state = options.state ?? (0, exports.emptyState)();
|
|
246
|
+
world.out = [];
|
|
247
|
+
world.outFn = (line) => {
|
|
248
|
+
world.out.push(line);
|
|
249
|
+
};
|
|
250
|
+
world.events = [];
|
|
251
|
+
world.calls = [];
|
|
252
|
+
world.queries = [];
|
|
253
|
+
world.transitions = [];
|
|
254
|
+
world.issued = [];
|
|
255
|
+
world.invalidated = [];
|
|
256
|
+
world.provisionedLocked = [];
|
|
257
|
+
world.provisionedPassword = [];
|
|
258
|
+
world.conns = [];
|
|
259
|
+
world.readerCalls = 0;
|
|
260
|
+
world.lockMode = options.lockMode ?? 'acquired';
|
|
261
|
+
world.failOn = options.failOn;
|
|
262
|
+
world.failTransition = undefined;
|
|
263
|
+
world.failQuery = undefined;
|
|
264
|
+
world.roleHeldOutside = false;
|
|
265
|
+
world.tables = options.tables ?? {};
|
|
266
|
+
world.id = (kind, name) => {
|
|
267
|
+
const key = `${kind}:${name}`;
|
|
268
|
+
let v = idMap.get(key);
|
|
269
|
+
if (v === undefined) {
|
|
270
|
+
counter += 1;
|
|
271
|
+
v = uuidN(counter);
|
|
272
|
+
idMap.set(key, v);
|
|
273
|
+
}
|
|
274
|
+
return v;
|
|
275
|
+
};
|
|
276
|
+
const memberships = [...(options.memberships ?? [])];
|
|
277
|
+
const rec = (service, method, principal, args) => {
|
|
278
|
+
world.calls.push({
|
|
279
|
+
seq: world.calls.length,
|
|
280
|
+
service,
|
|
281
|
+
method,
|
|
282
|
+
args,
|
|
283
|
+
principal,
|
|
284
|
+
actor: args.find(isBootstrapPrincipal),
|
|
285
|
+
});
|
|
286
|
+
world.events.push(`${service}.${method}`);
|
|
287
|
+
const err = world.failOn?.(service, method, args);
|
|
288
|
+
if (err !== undefined)
|
|
289
|
+
throw err;
|
|
290
|
+
};
|
|
291
|
+
// ---- pool and connections (lock, claim transaction, superuser flag, audit rows)
|
|
292
|
+
const lookup = (sql, params) => {
|
|
293
|
+
// membership ids are read by (user, company) from the memberships the fake services recorded
|
|
294
|
+
if (/FROM\s+`?[\w]+`?\.`?company_membership`?/i.test(sql)) {
|
|
295
|
+
const [userUuid, companyUuid] = params;
|
|
296
|
+
return memberships
|
|
297
|
+
.filter((m) => m.userUuid === userUuid && m.companyUuid === companyUuid)
|
|
298
|
+
.map((m) => ({ cm_uuid: m.id }));
|
|
299
|
+
}
|
|
300
|
+
const t = /FROM\s+`?[\w]+`?\.`?(users|companies|roles|departments|branches)`?/i.exec(sql);
|
|
301
|
+
if (!t)
|
|
302
|
+
return [];
|
|
303
|
+
const rows = world.tables[t[1].toLowerCase()] ?? [];
|
|
304
|
+
const wanted = params
|
|
305
|
+
.filter((p) => typeof p === 'string')
|
|
306
|
+
.map((p) => p.toLowerCase());
|
|
307
|
+
const hit = wanted.length === 0
|
|
308
|
+
? rows
|
|
309
|
+
: rows.filter((r) => Object.values(r).some((v) => typeof v === 'string' && wanted.includes(v.toLowerCase())));
|
|
310
|
+
// honour `col AS alias` so any alias the runner picks resolves
|
|
311
|
+
const aliases = [...sql.matchAll(/(?:[\w]+\.)?`?(\w+)`?\s+AS\s+`?(\w+)`?/gi)];
|
|
312
|
+
return hit.map((r) => {
|
|
313
|
+
const copy = { ...r };
|
|
314
|
+
for (const a of aliases)
|
|
315
|
+
if (a[1] in r)
|
|
316
|
+
copy[a[2]] = r[a[1]];
|
|
317
|
+
return copy;
|
|
318
|
+
});
|
|
319
|
+
};
|
|
320
|
+
const run = (connId, sql, params = []) => {
|
|
321
|
+
world.queries.push({ conn: connId, sql, params });
|
|
322
|
+
const failure = world.failQuery?.(connId, sql, params);
|
|
323
|
+
if (failure !== undefined)
|
|
324
|
+
throw failure;
|
|
325
|
+
if (/^\s*SELECT\b/i.test(sql) && /FROM\s+`?\w+`?\.`?roles`?[^]*FOR UPDATE/i.test(sql)) {
|
|
326
|
+
world.events.push('LOCK_ROLE');
|
|
327
|
+
return [{ r_uuid: params[0] }];
|
|
328
|
+
}
|
|
329
|
+
if (/^\s*SELECT\b/i.test(sql) && /membership_posting/i.test(sql)) {
|
|
330
|
+
world.events.push('CHECK_HELD_OUTSIDE');
|
|
331
|
+
return world.roleHeldOutside ? [{ held: 1 }] : [];
|
|
332
|
+
}
|
|
333
|
+
if (/GET_LOCK/i.test(sql)) {
|
|
334
|
+
world.events.push('GET_LOCK');
|
|
335
|
+
const alias = /\bAS\s+`?(\w+)`?/i.exec(sql)?.[1] ?? 'GET_LOCK(?, ?)';
|
|
336
|
+
return [{ [alias]: world.lockMode === 'acquired' ? 1 : 0 }];
|
|
337
|
+
}
|
|
338
|
+
if (/RELEASE_LOCK/i.test(sql)) {
|
|
339
|
+
world.events.push('RELEASE_LOCK');
|
|
340
|
+
const alias = /\bAS\s+`?(\w+)`?/i.exec(sql)?.[1] ?? 'RELEASE_LOCK(?)';
|
|
341
|
+
return [{ [alias]: 1 }];
|
|
342
|
+
}
|
|
343
|
+
if (/^\s*UPDATE\b/i.test(sql) && /u_is_platform_superuser/i.test(sql)) {
|
|
344
|
+
world.events.push('UPDATE_SUPERUSER');
|
|
345
|
+
return { affectedRows: 1 };
|
|
346
|
+
}
|
|
347
|
+
if (/^\s*(INSERT|UPDATE|DELETE)\b/i.test(sql))
|
|
348
|
+
return { affectedRows: 1 };
|
|
349
|
+
if (/^\s*SELECT\b/i.test(sql))
|
|
350
|
+
return lookup(sql, params);
|
|
351
|
+
return [];
|
|
352
|
+
};
|
|
353
|
+
const pool = {
|
|
354
|
+
query: async (sql, params) => run('pool', sql, params),
|
|
355
|
+
getConnection: async () => {
|
|
356
|
+
const id = `conn${world.conns.length + 1}`;
|
|
357
|
+
const conn = {
|
|
358
|
+
id,
|
|
359
|
+
released: false,
|
|
360
|
+
log: [],
|
|
361
|
+
query: async (sql, params) => run(id, sql, params),
|
|
362
|
+
beginTransaction: async () => {
|
|
363
|
+
conn.log.push('begin');
|
|
364
|
+
world.events.push(`begin:${id}`);
|
|
365
|
+
},
|
|
366
|
+
commit: async () => {
|
|
367
|
+
conn.log.push('commit');
|
|
368
|
+
world.events.push(`commit:${id}`);
|
|
369
|
+
},
|
|
370
|
+
rollback: async () => {
|
|
371
|
+
conn.log.push('rollback');
|
|
372
|
+
world.events.push(`rollback:${id}`);
|
|
373
|
+
},
|
|
374
|
+
release: async () => {
|
|
375
|
+
conn.released = true;
|
|
376
|
+
world.events.push(`release:${id}`);
|
|
377
|
+
},
|
|
378
|
+
end: async () => {
|
|
379
|
+
conn.released = true;
|
|
380
|
+
world.events.push(`release:${id}`);
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
world.conns.push(conn);
|
|
384
|
+
return conn;
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
// ---- guard fakes
|
|
388
|
+
const store = {
|
|
389
|
+
issue: async (principalId, hash, expiresAtMs) => {
|
|
390
|
+
world.events.push('issue');
|
|
391
|
+
world.issued.push({ principalId, hash, expiresAtMs });
|
|
392
|
+
},
|
|
393
|
+
consume: async () => null,
|
|
394
|
+
invalidateAllForPrincipal: async (principalId) => {
|
|
395
|
+
world.events.push('invalidate');
|
|
396
|
+
world.invalidated.push(principalId);
|
|
397
|
+
},
|
|
398
|
+
};
|
|
399
|
+
// The real provisioner INSERTs through the `query` it is handed (a transaction connection) or the pool.
|
|
400
|
+
const insertCredential = async (query, principalId) => {
|
|
401
|
+
const sql = 'INSERT INTO `zzns_identity`.`identity_credential` (ic_principal_id) VALUES (?)';
|
|
402
|
+
await (query ?? ((q, p) => pool.query(q, p)))(sql, [principalId]);
|
|
403
|
+
};
|
|
404
|
+
const provisioner = {
|
|
405
|
+
provisionLocked: async (input, query) => {
|
|
406
|
+
world.events.push('provisionLocked');
|
|
407
|
+
await insertCredential(query, input.principalId);
|
|
408
|
+
world.provisionedLocked.push({ ...input });
|
|
409
|
+
},
|
|
410
|
+
provisionPassword: async (input, query) => {
|
|
411
|
+
world.events.push('provisionPassword');
|
|
412
|
+
await insertCredential(query, input.principalId);
|
|
413
|
+
world.provisionedPassword.push({ ...input });
|
|
414
|
+
},
|
|
415
|
+
stateOf: async () => 'none',
|
|
416
|
+
};
|
|
417
|
+
const grantAdmin = {
|
|
418
|
+
listCatalog: async () => ({
|
|
419
|
+
catalog: [
|
|
420
|
+
{
|
|
421
|
+
key: 'identity:user:read',
|
|
422
|
+
label: 'Read users',
|
|
423
|
+
module: 'identity',
|
|
424
|
+
typeKey: 'user',
|
|
425
|
+
action: 'read',
|
|
426
|
+
},
|
|
427
|
+
{
|
|
428
|
+
key: 'identity:company:read',
|
|
429
|
+
label: 'Read companies',
|
|
430
|
+
module: 'identity',
|
|
431
|
+
typeKey: 'company',
|
|
432
|
+
action: 'read',
|
|
433
|
+
},
|
|
434
|
+
{
|
|
435
|
+
key: 'identity:audit:read',
|
|
436
|
+
label: 'Read audit',
|
|
437
|
+
module: 'identity',
|
|
438
|
+
typeKey: 'audit',
|
|
439
|
+
action: 'read',
|
|
440
|
+
},
|
|
441
|
+
],
|
|
442
|
+
wildcards: ['identity:*', 'identity:user:*', 'identity:company:*', 'identity:audit:*'],
|
|
443
|
+
}),
|
|
444
|
+
grant: async (roleUuid, key, grantor) => {
|
|
445
|
+
rec('GrantAdminService', 'grant', undefined, [roleUuid, key, grantor]);
|
|
446
|
+
},
|
|
447
|
+
};
|
|
448
|
+
const singletons = new Map();
|
|
449
|
+
const scoped = new Map();
|
|
450
|
+
if (options.wired !== false) {
|
|
451
|
+
singletons.set(options.poolToken, pool);
|
|
452
|
+
singletons.set(framework_schemas_token_js_1.IDENTITY_FRAMEWORK_SCHEMAS, exports.SCHEMAS);
|
|
453
|
+
singletons.set(credential_provisioner_token_js_1.IDENTITY_CREDENTIAL_PROVISIONER, provisioner);
|
|
454
|
+
singletons.set(guard_1.SHIELD_RESET_TOKEN_STORE, store);
|
|
455
|
+
singletons.set(grant_admin_service_js_1.GrantAdminService, grantAdmin);
|
|
456
|
+
scoped.set(company_service_js_1.CompanyService, (p) => ({
|
|
457
|
+
createCompany: async (actor, name) => {
|
|
458
|
+
rec('CompanyService', 'createCompany', p, [actor, name]);
|
|
459
|
+
return world.id('company', name);
|
|
460
|
+
},
|
|
461
|
+
}));
|
|
462
|
+
scoped.set(company_hierarchy_service_js_1.CompanyHierarchyService, (p) => ({
|
|
463
|
+
move: async (actor, nodeUuid, newParentUuid) => {
|
|
464
|
+
rec('CompanyHierarchyService', 'move', p, [actor, nodeUuid, newParentUuid]);
|
|
465
|
+
},
|
|
466
|
+
}));
|
|
467
|
+
scoped.set(role_service_js_1.RoleService, (p) => ({
|
|
468
|
+
createRole: async (name, actor) => {
|
|
469
|
+
rec('RoleService', 'createRole', p, [name, actor]);
|
|
470
|
+
return world.id('role', name);
|
|
471
|
+
},
|
|
472
|
+
}));
|
|
473
|
+
scoped.set(user_service_js_1.UserService, (p) => ({
|
|
474
|
+
createUserWithCredential: async (actor, input) => {
|
|
475
|
+
rec('UserService', 'createUserWithCredential', p, [actor, input]);
|
|
476
|
+
if (input.companyUuid === undefined)
|
|
477
|
+
world.events[world.events.length - 1] += ':root';
|
|
478
|
+
const id = world.id('user', input.username);
|
|
479
|
+
if (input.companyUuid !== undefined) {
|
|
480
|
+
memberships.push({
|
|
481
|
+
// keyed by user id, like addActiveMembership below and the specs' expectations
|
|
482
|
+
id: world.id('membership', `${id}@${input.companyUuid}`),
|
|
483
|
+
userUuid: id,
|
|
484
|
+
companyUuid: input.companyUuid,
|
|
485
|
+
});
|
|
486
|
+
return { id, credential: 'locked' };
|
|
487
|
+
}
|
|
488
|
+
return { id, credential: 'none', reason: 'no_company' };
|
|
489
|
+
},
|
|
490
|
+
}));
|
|
491
|
+
scoped.set(membership_service_js_1.MembershipService, (p) => ({
|
|
492
|
+
addActiveMembership: async (actor, userUuid, companyUuid) => {
|
|
493
|
+
rec('MembershipService', 'addActiveMembership', p, [actor, userUuid, companyUuid]);
|
|
494
|
+
const id = world.id('membership', `${userUuid}@${companyUuid}`);
|
|
495
|
+
memberships.push({ id, userUuid, companyUuid });
|
|
496
|
+
return id;
|
|
497
|
+
},
|
|
498
|
+
listMemberships: async (userUuid) => {
|
|
499
|
+
rec('MembershipService', 'listMemberships', p, [userUuid]);
|
|
500
|
+
return memberships
|
|
501
|
+
.filter((m) => m.userUuid === userUuid)
|
|
502
|
+
.map((m) => ({
|
|
503
|
+
id: m.id,
|
|
504
|
+
fields: { userUuid, companyUuid: m.companyUuid, isActive: true },
|
|
505
|
+
}));
|
|
506
|
+
},
|
|
507
|
+
}));
|
|
508
|
+
scoped.set(posting_service_js_1.PostingService, (p) => ({
|
|
509
|
+
addPostings: async (actor, membershipUuid, toAdd, opts) => {
|
|
510
|
+
rec('PostingService', 'addPostings', p, [actor, membershipUuid, toAdd, opts]);
|
|
511
|
+
},
|
|
512
|
+
}));
|
|
513
|
+
scoped.set(org_catalog_service_js_1.OrgCatalogService, (p) => ({
|
|
514
|
+
ensureDepartment: async (name) => {
|
|
515
|
+
rec('OrgCatalogService', 'ensureDepartment', p, [name]);
|
|
516
|
+
return world.id('department', name);
|
|
517
|
+
},
|
|
518
|
+
ensureBranch: async (name) => {
|
|
519
|
+
rec('OrgCatalogService', 'ensureBranch', p, [name]);
|
|
520
|
+
return world.id('branch', name);
|
|
521
|
+
},
|
|
522
|
+
attachDepartment: async (companyUuid, departmentUuid) => {
|
|
523
|
+
rec('OrgCatalogService', 'attachDepartment', p, [companyUuid, departmentUuid]);
|
|
524
|
+
},
|
|
525
|
+
attachBranch: async (companyUuid, branchUuid) => {
|
|
526
|
+
rec('OrgCatalogService', 'attachBranch', p, [companyUuid, branchUuid]);
|
|
527
|
+
},
|
|
528
|
+
}));
|
|
529
|
+
}
|
|
530
|
+
world.app = new FakeApp(singletons, scoped);
|
|
531
|
+
org_bootstrap_runner_mocks_js_1.current.world = world;
|
|
532
|
+
return world;
|
|
533
|
+
}
|
|
534
|
+
// ---------------------------------------------------------------- assertion helpers
|
|
535
|
+
const sha256 = (s) => (0, node_crypto_1.createHash)('sha256').update(s).digest('hex');
|
|
536
|
+
exports.sha256 = sha256;
|
|
537
|
+
const isWriteCall = (c) => !READ_METHODS.has(c.method);
|
|
538
|
+
exports.isWriteCall = isWriteCall;
|
|
539
|
+
const writeCalls = (w) => w.calls.filter(exports.isWriteCall);
|
|
540
|
+
exports.writeCalls = writeCalls;
|
|
541
|
+
const callsOf = (w, service, method) => w.calls.filter((c) => c.service === service && c.method === method);
|
|
542
|
+
exports.callsOf = callsOf;
|
|
543
|
+
/** Every non-audit write the runner could make is absent. Lock and SELECT traffic is allowed. */
|
|
544
|
+
function noWritesAtAll(w) {
|
|
545
|
+
const problems = [];
|
|
546
|
+
if ((0, exports.writeCalls)(w).length > 0)
|
|
547
|
+
problems.push(`service writes: ${(0, exports.writeCalls)(w)
|
|
548
|
+
.map((c) => c.method)
|
|
549
|
+
.join(',')}`);
|
|
550
|
+
if (w.provisionedLocked.length + w.provisionedPassword.length > 0)
|
|
551
|
+
problems.push('credential provisioned');
|
|
552
|
+
if (w.issued.length > 0)
|
|
553
|
+
problems.push('reset token issued');
|
|
554
|
+
if (w.invalidated.length > 0)
|
|
555
|
+
problems.push('reset tokens invalidated');
|
|
556
|
+
if (w.transitions.length > 0)
|
|
557
|
+
problems.push('role owner transitioned');
|
|
558
|
+
const dml = w.queries.filter((q) => /^\s*(INSERT|UPDATE|DELETE)\b/i.test(q.sql));
|
|
559
|
+
if (dml.length > 0)
|
|
560
|
+
problems.push(`DML: ${dml.map((q) => q.sql.slice(0, 40)).join(' | ')}`);
|
|
561
|
+
return problems;
|
|
562
|
+
}
|
|
563
|
+
/** Lock discipline: every GET_LOCK ran on a dedicated connection and was released on that one. */
|
|
564
|
+
function lockProblems(w) {
|
|
565
|
+
const problems = [];
|
|
566
|
+
const gets = w.queries.filter((q) => /GET_LOCK/i.test(q.sql));
|
|
567
|
+
const rels = w.queries.filter((q) => /RELEASE_LOCK/i.test(q.sql));
|
|
568
|
+
for (const g of gets) {
|
|
569
|
+
if (g.conn === 'pool')
|
|
570
|
+
problems.push('GET_LOCK ran on the shared pool, not a dedicated connection');
|
|
571
|
+
if (w.lockMode === 'acquired' && !rels.some((r) => r.conn === g.conn)) {
|
|
572
|
+
problems.push('lock acquired but not released on the same connection');
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
for (const c of w.conns)
|
|
576
|
+
if (!c.released)
|
|
577
|
+
problems.push(`${c.id} was never released`);
|
|
578
|
+
return problems;
|
|
579
|
+
}
|
|
580
|
+
/** Everything the run said or stored that must never carry a secret. Provisioner args excluded. */
|
|
581
|
+
function visibleText(w, extra = []) {
|
|
582
|
+
return [
|
|
583
|
+
w.out.join('\n'),
|
|
584
|
+
JSON.stringify(w.queries),
|
|
585
|
+
JSON.stringify(w.transitions.map((t) => t.filter((x) => typeof x !== 'function'))),
|
|
586
|
+
...extra.map((e) => e instanceof Error ? `${e.name} ${e.message} ${JSON.stringify(e)}` : JSON.stringify(e)),
|
|
587
|
+
].join('\n');
|
|
588
|
+
}
|
|
589
|
+
/** The raw token of the link printed for `username` (a line with the name and `<base>?token=<hex>`). */
|
|
590
|
+
function linkFor(w, username) {
|
|
591
|
+
for (const line of w.out) {
|
|
592
|
+
if (!line.includes(username))
|
|
593
|
+
continue;
|
|
594
|
+
const m = /https?:\/\/\S+?\?token=([0-9a-f]{32,})/i.exec(line);
|
|
595
|
+
if (m)
|
|
596
|
+
return { raw: m[1], line };
|
|
597
|
+
}
|
|
598
|
+
return undefined;
|
|
599
|
+
}
|
|
600
|
+
const linkLines = (w) => w.out.filter((l) => /\?token=/.test(l));
|
|
601
|
+
exports.linkLines = linkLines;
|
|
602
|
+
/** The audit rows (INSERTs into audit_log) naming `action`, from any connection. */
|
|
603
|
+
const auditRowsFor = (w, action) => w.queries.filter((q) => /audit_log/i.test(q.sql) && q.params.some((p) => p === action));
|
|
604
|
+
exports.auditRowsFor = auditRowsFor;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { OrgState } from './org-bootstrap-plan.js';
|
|
2
|
+
/** The slice of the harness `World` the mocks touch. */
|
|
3
|
+
export interface MockWorld {
|
|
4
|
+
state: OrgState;
|
|
5
|
+
events: string[];
|
|
6
|
+
readerCalls: number;
|
|
7
|
+
transitions: unknown[][];
|
|
8
|
+
failTransition: Error | undefined;
|
|
9
|
+
}
|
|
10
|
+
/** Shared handle the module mocks read from; `makeWorld` sets it. */
|
|
11
|
+
export declare const current: {
|
|
12
|
+
world: MockWorld | undefined;
|
|
13
|
+
};
|
|
14
|
+
export declare function readerModule(): {
|
|
15
|
+
OrgStateReader: {
|
|
16
|
+
new (): {
|
|
17
|
+
read(): Promise<OrgState>;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
export declare function transitionModule(): {
|
|
22
|
+
transitionRoleOwner: import("vitest").Mock<(...args: unknown[]) => Promise<void>>;
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=org-bootstrap-runner.mocks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"org-bootstrap-runner.mocks.d.ts","sourceRoot":"","sources":["../../../src/features/org-bootstrap/org-bootstrap-runner.mocks.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAExD,wDAAwD;AACxD,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,QAAQ,CAAC;IAChB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,OAAO,EAAE,EAAE,CAAC;IACzB,cAAc,EAAE,KAAK,GAAG,SAAS,CAAC;CACnC;AAED,qEAAqE;AACrE,eAAO,MAAM,OAAO,EAAE;IAAE,KAAK,EAAE,SAAS,GAAG,SAAS,CAAA;CAAyB,CAAC;AAE9E,wBAAgB,YAAY;;;oBAGR,OAAO,CAAC,QAAQ,CAAC;;;EAQpC;AAED,wBAAgB,gBAAgB;yDAEe,OAAO,EAAE;EAOvD"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.current = void 0;
|
|
4
|
+
exports.readerModule = readerModule;
|
|
5
|
+
exports.transitionModule = transitionModule;
|
|
6
|
+
/**
|
|
7
|
+
* G-009 T10: module-mock factories for the runner specs. Deliberately imports NOTHING heavy (only
|
|
8
|
+
* `vitest` and types): `vi.mock` factories run while the harness is still loading its service imports,
|
|
9
|
+
* so a factory that imports the harness would deadlock on the circular import.
|
|
10
|
+
*
|
|
11
|
+
* vi.mock('./org-state.reader.js', async () => (await import('./org-bootstrap-runner.mocks.js')).readerModule());
|
|
12
|
+
* vi.mock('../role/role-owner-transition.js', async () => (await import('./org-bootstrap-runner.mocks.js')).transitionModule());
|
|
13
|
+
*/
|
|
14
|
+
const vitest_1 = require("vitest");
|
|
15
|
+
/** Shared handle the module mocks read from; `makeWorld` sets it. */
|
|
16
|
+
exports.current = { world: undefined };
|
|
17
|
+
function readerModule() {
|
|
18
|
+
return {
|
|
19
|
+
OrgStateReader: class {
|
|
20
|
+
async read() {
|
|
21
|
+
const w = exports.current.world;
|
|
22
|
+
w.readerCalls += 1;
|
|
23
|
+
w.events.push('read');
|
|
24
|
+
return w.state;
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function transitionModule() {
|
|
30
|
+
return {
|
|
31
|
+
transitionRoleOwner: vitest_1.vi.fn(async (...args) => {
|
|
32
|
+
const w = exports.current.world;
|
|
33
|
+
w.events.push('transitionRoleOwner');
|
|
34
|
+
w.transitions.push(args);
|
|
35
|
+
if (w.failTransition)
|
|
36
|
+
throw w.failTransition;
|
|
37
|
+
}),
|
|
38
|
+
};
|
|
39
|
+
}
|