@stigmer/server 3.15.2 → 3.15.3-dev.20260914121148
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/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +103 -9
- package/dist/boot/compose.js.map +1 -1
- package/dist/domain/iampolicy/access-lists.d.ts +27 -0
- package/dist/domain/iampolicy/access-lists.d.ts.map +1 -0
- package/dist/domain/iampolicy/access-lists.js +124 -0
- package/dist/domain/iampolicy/access-lists.js.map +1 -0
- package/dist/domain/iampolicy/constants.d.ts +135 -0
- package/dist/domain/iampolicy/constants.d.ts.map +1 -0
- package/dist/domain/iampolicy/constants.js +203 -0
- package/dist/domain/iampolicy/constants.js.map +1 -0
- package/dist/domain/iampolicy/controller.d.ts +29 -0
- package/dist/domain/iampolicy/controller.d.ts.map +1 -0
- package/dist/domain/iampolicy/controller.js +451 -0
- package/dist/domain/iampolicy/controller.js.map +1 -0
- package/dist/domain/iampolicy/display-resolver.d.ts +6 -0
- package/dist/domain/iampolicy/display-resolver.d.ts.map +1 -0
- package/dist/domain/iampolicy/display-resolver.js +90 -0
- package/dist/domain/iampolicy/display-resolver.js.map +1 -0
- package/dist/domain/iampolicy/grant-path.d.ts +42 -0
- package/dist/domain/iampolicy/grant-path.d.ts.map +1 -0
- package/dist/domain/iampolicy/grant-path.js +183 -0
- package/dist/domain/iampolicy/grant-path.js.map +1 -0
- package/dist/domain/iampolicy/grant-scope.d.ts +4 -0
- package/dist/domain/iampolicy/grant-scope.d.ts.map +1 -0
- package/dist/domain/iampolicy/grant-scope.js +34 -0
- package/dist/domain/iampolicy/grant-scope.js.map +1 -0
- package/dist/domain/iampolicy/membership.d.ts +111 -0
- package/dist/domain/iampolicy/membership.d.ts.map +1 -0
- package/dist/domain/iampolicy/membership.js +152 -0
- package/dist/domain/iampolicy/membership.js.map +1 -0
- package/dist/domain/iampolicy/permissions.d.ts +19 -0
- package/dist/domain/iampolicy/permissions.d.ts.map +1 -0
- package/dist/domain/iampolicy/permissions.js +27 -0
- package/dist/domain/iampolicy/permissions.js.map +1 -0
- package/dist/domain/iampolicy/resource-store.d.ts +4 -0
- package/dist/domain/iampolicy/resource-store.d.ts.map +1 -0
- package/dist/domain/iampolicy/resource-store.js +135 -0
- package/dist/domain/iampolicy/resource-store.js.map +1 -0
- package/dist/domain/iampolicy/role-lifecycle.d.ts +9 -0
- package/dist/domain/iampolicy/role-lifecycle.d.ts.map +1 -0
- package/dist/domain/iampolicy/role-lifecycle.js +106 -0
- package/dist/domain/iampolicy/role-lifecycle.js.map +1 -0
- package/dist/domain/iampolicy/roles.d.ts +11 -0
- package/dist/domain/iampolicy/roles.d.ts.map +1 -0
- package/dist/domain/iampolicy/roles.js +78 -0
- package/dist/domain/iampolicy/roles.js.map +1 -0
- package/dist/domain/iampolicy/specs.d.ts +7 -0
- package/dist/domain/iampolicy/specs.d.ts.map +1 -0
- package/dist/domain/iampolicy/specs.js +45 -0
- package/dist/domain/iampolicy/specs.js.map +1 -0
- package/dist/domain/iampolicy/steps.d.ts +18 -0
- package/dist/domain/iampolicy/steps.d.ts.map +1 -0
- package/dist/domain/iampolicy/steps.js +117 -0
- package/dist/domain/iampolicy/steps.js.map +1 -0
- package/dist/domain/iampolicy/store-contract.d.ts +8 -0
- package/dist/domain/iampolicy/store-contract.d.ts.map +1 -0
- package/dist/domain/iampolicy/store-contract.js +258 -0
- package/dist/domain/iampolicy/store-contract.js.map +1 -0
- package/dist/domain/iampolicy/store.d.ts +78 -0
- package/dist/domain/iampolicy/store.d.ts.map +1 -0
- package/dist/domain/iampolicy/store.js +13 -0
- package/dist/domain/iampolicy/store.js.map +1 -0
- package/dist/domain/iampolicy/wire-refusals.d.ts +25 -0
- package/dist/domain/iampolicy/wire-refusals.d.ts.map +1 -0
- package/dist/domain/iampolicy/wire-refusals.js +76 -0
- package/dist/domain/iampolicy/wire-refusals.js.map +1 -0
- package/dist/domain/identityaccount/constants.d.ts +33 -0
- package/dist/domain/identityaccount/constants.d.ts.map +1 -1
- package/dist/domain/identityaccount/constants.js +2 -48
- package/dist/domain/identityaccount/constants.js.map +1 -1
- package/dist/domain/identityaccount/controller.d.ts +8 -1
- package/dist/domain/identityaccount/controller.d.ts.map +1 -1
- package/dist/domain/identityaccount/controller.js +54 -29
- package/dist/domain/identityaccount/controller.js.map +1 -1
- package/dist/domain/identityaccount/operator.d.ts.map +1 -1
- package/dist/domain/identityaccount/operator.js +2 -4
- package/dist/domain/identityaccount/operator.js.map +1 -1
- package/dist/domain/identityaccount/provisioning.d.ts +20 -1
- package/dist/domain/identityaccount/provisioning.d.ts.map +1 -1
- package/dist/domain/identityaccount/provisioning.js +17 -3
- package/dist/domain/identityaccount/provisioning.js.map +1 -1
- package/dist/domain/identityaccount/resolve.d.ts +23 -0
- package/dist/domain/identityaccount/resolve.d.ts.map +1 -1
- package/dist/domain/identityaccount/resolve.js +17 -0
- package/dist/domain/identityaccount/resolve.js.map +1 -1
- package/dist/domain/identityaccount/store-contract.d.ts +4 -18
- package/dist/domain/identityaccount/store-contract.d.ts.map +1 -1
- package/dist/domain/identityaccount/store-contract.js +12 -47
- package/dist/domain/identityaccount/store-contract.js.map +1 -1
- package/dist/extensions/authorization-queries.d.ts +81 -0
- package/dist/extensions/authorization-queries.d.ts.map +1 -0
- package/dist/extensions/authorization-queries.js +2 -0
- package/dist/extensions/authorization-queries.js.map +1 -0
- package/dist/extensions/drivers.d.ts +49 -2
- package/dist/extensions/drivers.d.ts.map +1 -1
- package/dist/extensions/identity.d.ts +14 -0
- package/dist/extensions/identity.d.ts.map +1 -1
- package/dist/extensions/identity.js +18 -1
- package/dist/extensions/identity.js.map +1 -1
- package/dist/extensions/list-read-scope.d.ts +41 -4
- package/dist/extensions/list-read-scope.d.ts.map +1 -1
- package/dist/extensions/list-read-scope.js +44 -7
- package/dist/extensions/list-read-scope.js.map +1 -1
- package/dist/extensions/policy-grant-scope.d.ts +50 -0
- package/dist/extensions/policy-grant-scope.d.ts.map +1 -0
- package/dist/extensions/policy-grant-scope.js +2 -0
- package/dist/extensions/policy-grant-scope.js.map +1 -0
- package/dist/extensions/registry.d.ts +23 -1
- package/dist/extensions/registry.d.ts.map +1 -1
- package/dist/extensions/registry.js +30 -0
- package/dist/extensions/registry.js.map +1 -1
- package/dist/extensions/resource-authorization.d.ts +63 -0
- package/dist/extensions/resource-authorization.d.ts.map +1 -1
- package/dist/index.d.ts +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/pipeline/apiresource-meta.d.ts +77 -1
- package/dist/pipeline/apiresource-meta.d.ts.map +1 -1
- package/dist/pipeline/apiresource-meta.js +183 -1
- package/dist/pipeline/apiresource-meta.js.map +1 -1
- package/dist/pipeline/interceptors/auth.d.ts +12 -0
- package/dist/pipeline/interceptors/auth.d.ts.map +1 -1
- package/dist/pipeline/interceptors/auth.js +13 -1
- package/dist/pipeline/interceptors/auth.js.map +1 -1
- package/dist/pipeline/steps/authorization-tuples.d.ts.map +1 -1
- package/dist/pipeline/steps/authorization-tuples.js +7 -26
- package/dist/pipeline/steps/authorization-tuples.js.map +1 -1
- package/dist/pipeline/steps/authorize.d.ts +20 -5
- package/dist/pipeline/steps/authorize.d.ts.map +1 -1
- package/dist/pipeline/steps/authorize.js +37 -17
- package/dist/pipeline/steps/authorize.js.map +1 -1
- package/dist/pipeline/steps/defaults.d.ts +10 -0
- package/dist/pipeline/steps/defaults.d.ts.map +1 -1
- package/dist/pipeline/steps/defaults.js +39 -0
- package/dist/pipeline/steps/defaults.js.map +1 -1
- package/dist/pipeline/steps/shapes.d.ts +18 -0
- package/dist/pipeline/steps/shapes.d.ts.map +1 -1
- package/dist/pipeline/steps/shapes.js +29 -0
- package/dist/pipeline/steps/shapes.js.map +1 -1
- package/dist/store/port-contract.d.ts +61 -0
- package/dist/store/port-contract.d.ts.map +1 -0
- package/dist/store/port-contract.js +67 -0
- package/dist/store/port-contract.js.map +1 -0
- package/package.json +4 -4
- package/src/boot/compose.ts +107 -10
- package/src/domain/iampolicy/__tests__/access-lists.test.ts +238 -0
- package/src/domain/iampolicy/__tests__/constants.test.ts +388 -0
- package/src/domain/iampolicy/__tests__/controller.test.ts +960 -0
- package/src/domain/iampolicy/__tests__/display-resolver.test.ts +138 -0
- package/src/domain/iampolicy/__tests__/grant-path.test.ts +474 -0
- package/src/domain/iampolicy/__tests__/grant-scope.test.ts +61 -0
- package/src/domain/iampolicy/__tests__/iampolicy.test.ts +532 -0
- package/src/domain/iampolicy/__tests__/membership.test.ts +447 -0
- package/src/domain/iampolicy/__tests__/permissions.test.ts +41 -0
- package/src/domain/iampolicy/__tests__/resource-store.test.ts +165 -0
- package/src/domain/iampolicy/__tests__/role-lifecycle.test.ts +361 -0
- package/src/domain/iampolicy/__tests__/roles.test.ts +53 -0
- package/src/domain/iampolicy/__tests__/specs.test.ts +41 -0
- package/src/domain/iampolicy/__tests__/steps.test.ts +278 -0
- package/src/domain/iampolicy/__tests__/support.ts +195 -0
- package/src/domain/iampolicy/__tests__/wire-refusals.test.ts +117 -0
- package/src/domain/iampolicy/access-lists.ts +191 -0
- package/src/domain/iampolicy/constants.ts +243 -0
- package/src/domain/iampolicy/controller.ts +798 -0
- package/src/domain/iampolicy/display-resolver.ts +123 -0
- package/src/domain/iampolicy/grant-path.ts +270 -0
- package/src/domain/iampolicy/grant-scope.ts +36 -0
- package/src/domain/iampolicy/membership.ts +298 -0
- package/src/domain/iampolicy/permissions.ts +35 -0
- package/src/domain/iampolicy/resource-store.ts +186 -0
- package/src/domain/iampolicy/role-lifecycle.ts +132 -0
- package/src/domain/iampolicy/roles.ts +95 -0
- package/src/domain/iampolicy/specs.ts +53 -0
- package/src/domain/iampolicy/steps.ts +178 -0
- package/src/domain/iampolicy/store-contract.ts +450 -0
- package/src/domain/iampolicy/store.ts +103 -0
- package/src/domain/iampolicy/wire-refusals.ts +110 -0
- package/src/domain/identityaccount/__tests__/provisioning.test.ts +19 -7
- package/src/domain/identityaccount/__tests__/resolve.test.ts +113 -2
- package/src/domain/identityaccount/constants.ts +16 -31
- package/src/domain/identityaccount/controller.ts +65 -35
- package/src/domain/identityaccount/operator.ts +5 -5
- package/src/domain/identityaccount/provisioning.ts +43 -5
- package/src/domain/identityaccount/resolve.ts +43 -0
- package/src/domain/identityaccount/store-contract.ts +22 -70
- package/src/extensions/__tests__/composed-support.ts +87 -0
- package/src/extensions/__tests__/iam-policy-composed.test.ts +373 -0
- package/src/extensions/__tests__/iam-policy-points.test.ts +169 -0
- package/src/extensions/__tests__/identity-account-composed.test.ts +12 -78
- package/src/extensions/__tests__/identity.test.ts +62 -0
- package/src/extensions/__tests__/list-read-scope.test.ts +138 -0
- package/src/extensions/__tests__/tier-truthfulness.test.ts +8 -0
- package/src/extensions/authorization-queries.ts +97 -0
- package/src/extensions/drivers.ts +49 -2
- package/src/extensions/identity.ts +21 -0
- package/src/extensions/list-read-scope.ts +87 -9
- package/src/extensions/policy-grant-scope.ts +50 -0
- package/src/extensions/registry.ts +62 -1
- package/src/extensions/resource-authorization.ts +65 -0
- package/src/index.ts +20 -0
- package/src/pipeline/__tests__/grantable-roles-for.test.ts +87 -0
- package/src/pipeline/__tests__/inherited-authorization-parent.test.ts +51 -0
- package/src/pipeline/__tests__/kind-by-enum-name.test.ts +107 -0
- package/src/pipeline/__tests__/kind-served-by-edition.test.ts +107 -0
- package/src/pipeline/apiresource-meta.ts +218 -1
- package/src/pipeline/interceptors/auth.ts +14 -1
- package/src/pipeline/steps/__tests__/authorize-annotation-completeness.test.ts +73 -3
- package/src/pipeline/steps/__tests__/authorize.test.ts +134 -9
- package/src/pipeline/steps/__tests__/derived-id.test.ts +49 -0
- package/src/pipeline/steps/authorization-tuples.ts +7 -28
- package/src/pipeline/steps/authorize.ts +46 -21
- package/src/pipeline/steps/defaults.ts +42 -0
- package/src/pipeline/steps/shapes.ts +31 -0
- package/src/store/__tests__/port-contract.test.ts +123 -0
- package/src/store/port-contract.ts +102 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The port-contract runner — the scaffolding every domain store PORT's
|
|
3
|
+
* contract kit is built on (identity-account's store-contract.ts since
|
|
4
|
+
* 20260911.11 A11; IamPolicy's since 20260913.01 slice 2, when this was
|
|
5
|
+
* lifted out of the first kit so the second did not carry a copy).
|
|
6
|
+
*
|
|
7
|
+
* A port kit is a list of named cases a DRIVER's test iterates: open
|
|
8
|
+
* source runs them over its adapter on sqlite and Postgres, a composition
|
|
9
|
+
* runs the same list over the store it registers as a driver, so "the port
|
|
10
|
+
* holds" is one statement proven per driver, never restated per
|
|
11
|
+
* repository. This module owns the part every kit shares and no kit should
|
|
12
|
+
* restate: a FRESH fixture per case, the body run over it, cleanup after,
|
|
13
|
+
* and the rule that a failing body is the failure reported (a cleanup that
|
|
14
|
+
* fails after a failed body must not replace the assertion that matters
|
|
15
|
+
* with a teardown detail; a cleanup that fails after a passing body is a
|
|
16
|
+
* real failure).
|
|
17
|
+
*
|
|
18
|
+
* Shape. The package's precedent is store/__tests__/store-contract.ts —
|
|
19
|
+
* `describeStoreContract(makeFixture)` over the generic Store, a vitest
|
|
20
|
+
* `describe`. A port kit is consumed by ANOTHER package's tests through the
|
|
21
|
+
* published barrel, so it ships in dist/ and must not import vitest (a
|
|
22
|
+
* devDependency that would enter the root barrel's runtime import graph).
|
|
23
|
+
* It therefore returns cases instead of calling `describe`, asserts through
|
|
24
|
+
* node:assert/strict, and the consumer's framework does
|
|
25
|
+
* `for (const c of cases) it(c.name, c.run)`. Do not "fix" it back to the
|
|
26
|
+
* precedent's vitest shape.
|
|
27
|
+
*
|
|
28
|
+
* `disconnect` is the one escape hatch a port cannot express: a port has
|
|
29
|
+
* no lifecycle by design (a composition owns its store's), yet "an outage
|
|
30
|
+
* never reads as not-found" is the line that matters most, so a fixture
|
|
31
|
+
* cuts its store from the database and the kit asserts that the fault
|
|
32
|
+
* propagates. `cleanup` must tolerate a disconnected store.
|
|
33
|
+
*/
|
|
34
|
+
/** One fresh, isolated store per case, plus the one escape hatch the port cannot express. */
|
|
35
|
+
export interface PortContractFixture<Port> {
|
|
36
|
+
readonly store: Port;
|
|
37
|
+
/**
|
|
38
|
+
* Cuts the store from its database so every later call is an
|
|
39
|
+
* infrastructure fault. `cleanup` still runs afterwards and must tolerate
|
|
40
|
+
* a disconnected store (both OSS drivers' `close` is idempotent; a pool
|
|
41
|
+
* fixture ends a per-case pool here and skips it in cleanup).
|
|
42
|
+
*/
|
|
43
|
+
disconnect(): Promise<void>;
|
|
44
|
+
cleanup(): Promise<void>;
|
|
45
|
+
}
|
|
46
|
+
export interface PortContractCase {
|
|
47
|
+
/** The contract line, in the port's words; the framework prints it as the test name. */
|
|
48
|
+
readonly name: string;
|
|
49
|
+
/** Makes a FRESH fixture, runs the case, cleans up. A failing assertion wins over a failing cleanup. */
|
|
50
|
+
run(): Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
/** A case body over a live fixture; the runner owns the fixture's lifecycle around it. */
|
|
53
|
+
export type PortContractBody<Port> = (fixture: PortContractFixture<Port>) => Promise<void>;
|
|
54
|
+
/** A kit's declaration: the contract line and the body that proves it. */
|
|
55
|
+
export type PortContractDeclaration<Port> = readonly [
|
|
56
|
+
name: string,
|
|
57
|
+
body: PortContractBody<Port>
|
|
58
|
+
];
|
|
59
|
+
/** The kit's declarations as runnable cases over `makeFixture`, one fresh fixture per case, in declared order. */
|
|
60
|
+
export declare function portContractCases<Port>(declarations: ReadonlyArray<PortContractDeclaration<Port>>, makeFixture: () => Promise<PortContractFixture<Port>>): ReadonlyArray<PortContractCase>;
|
|
61
|
+
//# sourceMappingURL=port-contract.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"port-contract.d.ts","sourceRoot":"","sources":["../../src/store/port-contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,6FAA6F;AAC7F,MAAM,WAAW,mBAAmB,CAAC,IAAI;IACvC,QAAQ,CAAC,KAAK,EAAE,IAAI,CAAC;IACrB;;;;;OAKG;IACH,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,MAAM,WAAW,gBAAgB;IAC/B,wFAAwF;IACxF,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,wGAAwG;IACxG,GAAG,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACtB;AAED,0FAA0F;AAC1F,MAAM,MAAM,gBAAgB,CAAC,IAAI,IAAI,CACnC,OAAO,EAAE,mBAAmB,CAAC,IAAI,CAAC,KAC/B,OAAO,CAAC,IAAI,CAAC,CAAC;AAEnB,0EAA0E;AAC1E,MAAM,MAAM,uBAAuB,CAAC,IAAI,IAAI,SAAS;IACnD,IAAI,EAAE,MAAM;IACZ,IAAI,EAAE,gBAAgB,CAAC,IAAI,CAAC;CAC7B,CAAC;AA6BF,kHAAkH;AAClH,wBAAgB,iBAAiB,CAAC,IAAI,EACpC,YAAY,EAAE,aAAa,CAAC,uBAAuB,CAAC,IAAI,CAAC,CAAC,EAC1D,WAAW,EAAE,MAAM,OAAO,CAAC,mBAAmB,CAAC,IAAI,CAAC,CAAC,GACpD,aAAa,CAAC,gBAAgB,CAAC,CAKjC"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The port-contract runner — the scaffolding every domain store PORT's
|
|
3
|
+
* contract kit is built on (identity-account's store-contract.ts since
|
|
4
|
+
* 20260911.11 A11; IamPolicy's since 20260913.01 slice 2, when this was
|
|
5
|
+
* lifted out of the first kit so the second did not carry a copy).
|
|
6
|
+
*
|
|
7
|
+
* A port kit is a list of named cases a DRIVER's test iterates: open
|
|
8
|
+
* source runs them over its adapter on sqlite and Postgres, a composition
|
|
9
|
+
* runs the same list over the store it registers as a driver, so "the port
|
|
10
|
+
* holds" is one statement proven per driver, never restated per
|
|
11
|
+
* repository. This module owns the part every kit shares and no kit should
|
|
12
|
+
* restate: a FRESH fixture per case, the body run over it, cleanup after,
|
|
13
|
+
* and the rule that a failing body is the failure reported (a cleanup that
|
|
14
|
+
* fails after a failed body must not replace the assertion that matters
|
|
15
|
+
* with a teardown detail; a cleanup that fails after a passing body is a
|
|
16
|
+
* real failure).
|
|
17
|
+
*
|
|
18
|
+
* Shape. The package's precedent is store/__tests__/store-contract.ts —
|
|
19
|
+
* `describeStoreContract(makeFixture)` over the generic Store, a vitest
|
|
20
|
+
* `describe`. A port kit is consumed by ANOTHER package's tests through the
|
|
21
|
+
* published barrel, so it ships in dist/ and must not import vitest (a
|
|
22
|
+
* devDependency that would enter the root barrel's runtime import graph).
|
|
23
|
+
* It therefore returns cases instead of calling `describe`, asserts through
|
|
24
|
+
* node:assert/strict, and the consumer's framework does
|
|
25
|
+
* `for (const c of cases) it(c.name, c.run)`. Do not "fix" it back to the
|
|
26
|
+
* precedent's vitest shape.
|
|
27
|
+
*
|
|
28
|
+
* `disconnect` is the one escape hatch a port cannot express: a port has
|
|
29
|
+
* no lifecycle by design (a composition owns its store's), yet "an outage
|
|
30
|
+
* never reads as not-found" is the line that matters most, so a fixture
|
|
31
|
+
* cuts its store from the database and the kit asserts that the fault
|
|
32
|
+
* propagates. `cleanup` must tolerate a disconnected store.
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* Runs `body` over a fresh fixture. The body's failure is the one reported:
|
|
36
|
+
* a cleanup failure after a failed body would otherwise replace the
|
|
37
|
+
* assertion that matters with a teardown detail.
|
|
38
|
+
*/
|
|
39
|
+
async function withFixture(makeFixture, body) {
|
|
40
|
+
const fixture = await makeFixture();
|
|
41
|
+
let failed = false;
|
|
42
|
+
try {
|
|
43
|
+
await body(fixture);
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
failed = true;
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
try {
|
|
51
|
+
await fixture.cleanup();
|
|
52
|
+
}
|
|
53
|
+
catch (cleanupError) {
|
|
54
|
+
if (!failed) {
|
|
55
|
+
throw cleanupError;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** The kit's declarations as runnable cases over `makeFixture`, one fresh fixture per case, in declared order. */
|
|
61
|
+
export function portContractCases(declarations, makeFixture) {
|
|
62
|
+
return declarations.map(([name, body]) => ({
|
|
63
|
+
name,
|
|
64
|
+
run: () => withFixture(makeFixture, body),
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=port-contract.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"port-contract.js","sourceRoot":"","sources":["../../src/store/port-contract.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAiCH;;;;GAIG;AACH,KAAK,UAAU,WAAW,CACxB,WAAqD,EACrD,IAA4B;IAE5B,MAAM,OAAO,GAAG,MAAM,WAAW,EAAE,CAAC;IACpC,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,IAAI,CAAC;QACH,MAAM,IAAI,CAAC,OAAO,CAAC,CAAC;IACtB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,GAAG,IAAI,CAAC;QACd,MAAM,KAAK,CAAC;IACd,CAAC;YAAS,CAAC;QACT,IAAI,CAAC;YACH,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;QAC1B,CAAC;QAAC,OAAO,YAAY,EAAE,CAAC;YACtB,IAAI,CAAC,MAAM,EAAE,CAAC;gBACZ,MAAM,YAAY,CAAC;YACrB,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC;AAED,kHAAkH;AAClH,MAAM,UAAU,iBAAiB,CAC/B,YAA0D,EAC1D,WAAqD;IAErD,OAAO,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,EAAE,CAAC,CAAC;QACzC,IAAI;QACJ,GAAG,EAAE,GAAG,EAAE,CAAC,WAAW,CAAC,WAAW,EAAE,IAAI,CAAC;KAC1C,CAAC,CAAC,CAAC;AACN,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stigmer/server",
|
|
3
|
-
"version": "3.15.
|
|
3
|
+
"version": "3.15.3-dev.20260914121148",
|
|
4
4
|
"description": "The Stigmer OSS control plane as a composable library: composeServer, the extension registry and the driver seams a composition builds on. The self-contained deployable of the same server is @stigmer/server-slim.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -62,9 +62,9 @@
|
|
|
62
62
|
"@connectrpc/connect-node": "^2.0.0",
|
|
63
63
|
"@kubernetes/client-node": "2.0.0",
|
|
64
64
|
"@opentelemetry/api": "^1.9.0",
|
|
65
|
-
"@stigmer/protos": "3.15.
|
|
66
|
-
"@stigmer/temporal-codecs": "3.15.
|
|
67
|
-
"@stigmer/zip-structure": "3.15.
|
|
65
|
+
"@stigmer/protos": "3.15.3-dev.20260914121148",
|
|
66
|
+
"@stigmer/temporal-codecs": "3.15.3-dev.20260914121148",
|
|
67
|
+
"@stigmer/zip-structure": "3.15.3-dev.20260914121148",
|
|
68
68
|
"@temporalio/activity": "1.16.2",
|
|
69
69
|
"@temporalio/client": "1.16.2",
|
|
70
70
|
"@temporalio/common": "1.16.2",
|
package/src/boot/compose.ts
CHANGED
|
@@ -72,6 +72,12 @@ import {
|
|
|
72
72
|
import { ensureOperatorAccount } from "../domain/identityaccount/operator.js";
|
|
73
73
|
import { newDirectAccountProvisioner } from "../domain/identityaccount/provisioning.js";
|
|
74
74
|
import { newResourceIdentityAccountStore } from "../domain/identityaccount/resource-store.js";
|
|
75
|
+
import { registerIamPolicyServices } from "../domain/iampolicy/controller.js";
|
|
76
|
+
import { newIamPolicyGrantPath } from "../domain/iampolicy/grant-path.js";
|
|
77
|
+
import { newOrganizationOnlyGrantScope } from "../domain/iampolicy/grant-scope.js";
|
|
78
|
+
import { newMembershipRules } from "../domain/iampolicy/membership.js";
|
|
79
|
+
import { newResourceIamPolicyStore } from "../domain/iampolicy/resource-store.js";
|
|
80
|
+
import { newBuiltInRoleLifecycle } from "../domain/iampolicy/role-lifecycle.js";
|
|
75
81
|
import { newIssuerDiscovery } from "../identity/oidc-discovery.js";
|
|
76
82
|
import { newOidcUserInfoClient } from "../identity/oidc-userinfo.js";
|
|
77
83
|
import { newApiKeyIdentityVerifier } from "../domain/apikey/verifier.js";
|
|
@@ -285,13 +291,13 @@ export async function composeServer(
|
|
|
285
291
|
// position 1 of every chain calls it (O2).
|
|
286
292
|
const authorizer =
|
|
287
293
|
extensions.authorizer ?? newPermissiveSingleTeamAuthorizer();
|
|
288
|
-
// The
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
|
|
294
|
-
|
|
294
|
+
// The built-in authorization POSTURE (20260913.01, Q-OR-6), named once:
|
|
295
|
+
// no unit registered an Authorizer, so the roles open source records
|
|
296
|
+
// exist to feed the built-in one. Under it — and only under it — the
|
|
297
|
+
// identity-accounts stage below installs the built-in role lifecycle
|
|
298
|
+
// and the membership rules; a composition with its own Authorizer has
|
|
299
|
+
// its own onboarding and gets neither.
|
|
300
|
+
const builtInAuthorization = extensions.authorizer === undefined;
|
|
295
301
|
|
|
296
302
|
// Stage: storage — the driver selection seam (DD-010): DATABASE_URL
|
|
297
303
|
// present → Postgres (async connect + advisory-locked migrations), else
|
|
@@ -338,11 +344,67 @@ export async function composeServer(
|
|
|
338
344
|
const identityAccounts =
|
|
339
345
|
extensions.drivers.identityAccountStore ??
|
|
340
346
|
newResourceIdentityAccountStore(store);
|
|
347
|
+
// The IamPolicy row half (20260913.01): the store PORT bound the same way
|
|
348
|
+
// (`drivers.iamPolicyStore`, Q-OR-10; open source's adapter over `store`
|
|
349
|
+
// when absent) and the domain's ONE grant and revoke path built over it.
|
|
350
|
+
// The grant path's lifecycle is the composed DRIVER — the cloud's tuple
|
|
351
|
+
// hooks — never the built-in lifecycle below, which sits ON TOP of the
|
|
352
|
+
// grant path; that direction keeps the two acyclic (S1 finding B). Both
|
|
353
|
+
// bindings are what the IamPolicy controller registers over (routes).
|
|
354
|
+
const iamPolicies =
|
|
355
|
+
extensions.drivers.iamPolicyStore ?? newResourceIamPolicyStore(store);
|
|
356
|
+
const iamPolicyGrantPath = newIamPolicyGrantPath({
|
|
357
|
+
policies: iamPolicies,
|
|
358
|
+
lifecycle: extensions.drivers.resourceAuthorizationLifecycle,
|
|
359
|
+
logger,
|
|
360
|
+
});
|
|
361
|
+
// The C2 tuple-lifecycle driver (ruling Q2): undefined = the three
|
|
362
|
+
// shared tuple steps (CreateAuthorizationTuples / CleanupIamPolicies /
|
|
363
|
+
// UpdateVisibilityTuples) no-op before any resolution work — OSS
|
|
364
|
+
// behavior byte-identical. Handed to every resource controller as an
|
|
365
|
+
// explicit dependency, the same threading as the authorizer.
|
|
366
|
+
const authorizationLifecycle =
|
|
367
|
+
extensions.drivers.resourceAuthorizationLifecycle;
|
|
368
|
+
// The lifecycle the ORGANIZATION and IDENTITY-ACCOUNT controllers get
|
|
369
|
+
// (20260913.01 slice 4, Q-S4-8): the composed driver when a unit
|
|
370
|
+
// registers one; otherwise, under the built-in posture, open source's
|
|
371
|
+
// role lifecycle — the organization creator's `owner` row, cleanup on
|
|
372
|
+
// organization and account deletes — the row half of the seam that
|
|
373
|
+
// records authorization at lifecycle points in every edition
|
|
374
|
+
// (extensions/resource-authorization.ts, corrected 2026-09-13). Those
|
|
375
|
+
// two kinds ONLY, because a lifecycle on a controller turns on the
|
|
376
|
+
// steps' full event RESOLUTION for that kind, and resolution fails a
|
|
377
|
+
// request whose configured parent id is missing (Java parity the
|
|
378
|
+
// cloud relies on) — a state open source admits by contract (a memory
|
|
379
|
+
// captured with no credential has no subject). The built-in object is
|
|
380
|
+
// a two-kind role recorder, so only two controllers see it; every other
|
|
381
|
+
// controller keeps the composed driver above and stays byte-identical.
|
|
382
|
+
const roleLifecycle =
|
|
383
|
+
authorizationLifecycle ??
|
|
384
|
+
(builtInAuthorization
|
|
385
|
+
? newBuiltInRoleLifecycle({
|
|
386
|
+
grantPath: iamPolicyGrantPath,
|
|
387
|
+
accounts: identityAccounts,
|
|
388
|
+
})
|
|
389
|
+
: undefined);
|
|
390
|
+
// The operator seam is read once here for both its consumers below: the
|
|
391
|
+
// trusted-local account ensure and the membership rules' operator-email
|
|
392
|
+
// arm (which also applies under an authentication posture — the
|
|
393
|
+
// operator who signs in through OIDC is admin of what the laptop has).
|
|
394
|
+
const operatorIdentity = operatorIdentitySnapshot();
|
|
395
|
+
const membership = builtInAuthorization
|
|
396
|
+
? newMembershipRules({
|
|
397
|
+
grantPath: iamPolicyGrantPath,
|
|
398
|
+
policies: iamPolicies,
|
|
399
|
+
store,
|
|
400
|
+
operatorEmail: operatorIdentity.email,
|
|
401
|
+
})
|
|
402
|
+
: undefined;
|
|
341
403
|
const identityAccountPath = {
|
|
342
404
|
accounts: identityAccounts,
|
|
343
405
|
logger,
|
|
344
406
|
authorizer,
|
|
345
|
-
authorizationLifecycle,
|
|
407
|
+
authorizationLifecycle: roleLifecycle,
|
|
346
408
|
};
|
|
347
409
|
const createIdentityAccount = newCreateAccountPath(
|
|
348
410
|
identityAccountPath,
|
|
@@ -377,11 +439,22 @@ export async function composeServer(
|
|
|
377
439
|
if (!requireAuthentication) {
|
|
378
440
|
const operator = await ensureOperatorAccount(
|
|
379
441
|
{ accounts: identityAccounts, createAccount: createIdentityAccount },
|
|
380
|
-
|
|
442
|
+
operatorIdentity,
|
|
381
443
|
);
|
|
382
444
|
logger.info("trusted-local operator account ensured", {
|
|
383
445
|
accountId: operator.metadata?.id ?? "",
|
|
384
446
|
});
|
|
447
|
+
// The laptop's one principal owns what the laptop has (Q-OR-6c):
|
|
448
|
+
// `owner` on every organization with no owner row, create-if-absent,
|
|
449
|
+
// so the Members page tells the truth from the first boot after roles
|
|
450
|
+
// exist. Under the built-in posture only — a unit with its own
|
|
451
|
+
// Authorizer and no authentication posture keeps its own rules.
|
|
452
|
+
if (membership !== undefined) {
|
|
453
|
+
await membership.ensureOperatorOwnership(operator);
|
|
454
|
+
logger.info("trusted-local operator ownership ensured", {
|
|
455
|
+
accountId: operator.metadata?.id ?? "",
|
|
456
|
+
});
|
|
457
|
+
}
|
|
385
458
|
}
|
|
386
459
|
|
|
387
460
|
// Stage: keys — the ratified fail-loud boot ASYMMETRY (D2 cross-domain
|
|
@@ -899,7 +972,9 @@ export async function composeServer(
|
|
|
899
972
|
logger,
|
|
900
973
|
authorizer,
|
|
901
974
|
gateSteps: extensions.gateSteps,
|
|
902
|
-
|
|
975
|
+
// The role lifecycle (the identity stage): the composed driver, or
|
|
976
|
+
// open source's owner-row writer under the built-in posture.
|
|
977
|
+
authorizationLifecycle: roleLifecycle,
|
|
903
978
|
organizationDirectory: extensions.drivers.organizationDirectory,
|
|
904
979
|
});
|
|
905
980
|
// ApiKey is the first domain born AFTER the Go port (O3, 20260827.06 —
|
|
@@ -919,11 +994,33 @@ export async function composeServer(
|
|
|
919
994
|
// composition adds what differs per edition through the registry —
|
|
920
995
|
// the federation capability (the four federated RPC arms) and the
|
|
921
996
|
// provision slot's gate steps (the cloud's personal organization).
|
|
997
|
+
// The membership rules are core, selected by posture (20260913.01).
|
|
922
998
|
registerIdentityAccountServices(router, {
|
|
923
999
|
...identityAccountPath,
|
|
924
1000
|
provisioner: identityAccountProvisioner,
|
|
925
1001
|
federation: extensions.drivers.identityFederation,
|
|
926
1002
|
gateSteps: extensions.gateSteps,
|
|
1003
|
+
membership,
|
|
1004
|
+
});
|
|
1005
|
+
// IamPolicy (20260913.01): the ROW half served once by open source in
|
|
1006
|
+
// every edition over the store port and the grant path bound in the
|
|
1007
|
+
// identity stage. A composition adds what differs per edition through
|
|
1008
|
+
// the registry — a wider grant scope (`drivers.policyGrantScope`; the
|
|
1009
|
+
// default is open source's organization-only scope, installed at this
|
|
1010
|
+
// `??` site like the permissive Authorizer) and the tuple-half query
|
|
1011
|
+
// engine (`drivers.authorizationQueries`; absent, those RPCs refuse
|
|
1012
|
+
// UNIMPLEMENTED). The chains take no lifecycle: a policy is the
|
|
1013
|
+
// authorization record, not a protected resource.
|
|
1014
|
+
registerIamPolicyServices(router, {
|
|
1015
|
+
grantPath: iamPolicyGrantPath,
|
|
1016
|
+
policies: iamPolicies,
|
|
1017
|
+
accounts: identityAccounts,
|
|
1018
|
+
authorizer,
|
|
1019
|
+
grantScope:
|
|
1020
|
+
extensions.drivers.policyGrantScope ?? newOrganizationOnlyGrantScope(),
|
|
1021
|
+
queries: extensions.drivers.authorizationQueries,
|
|
1022
|
+
edition: extensions.edition,
|
|
1023
|
+
logger,
|
|
927
1024
|
});
|
|
928
1025
|
registerEnvironmentServices(router, {
|
|
929
1026
|
store,
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins access-lists.ts (20260913.01 slice 5): the row-driven answer to
|
|
3
|
+
* "who has access, with which roles" — the hierarchy walk over scope
|
|
4
|
+
* tuples (child → parent through the resource's one structural link, at
|
|
5
|
+
* most five steps, `platform` terminal) and the grouping of assignable-
|
|
6
|
+
* role rows by principal in first-seen order, enriched where the display
|
|
7
|
+
* resolver answers and rendered as kind/id with the id as its name
|
|
8
|
+
* everywhere else (the Java PrincipalEnricher fallback). Moved from the
|
|
9
|
+
* cloud's iam/policy/access-lists.ts; the arms below are the Java
|
|
10
|
+
* contract read from that file.
|
|
11
|
+
*/
|
|
12
|
+
import { create } from "@bufbuild/protobuf";
|
|
13
|
+
import { describe, expect, it } from "vitest";
|
|
14
|
+
|
|
15
|
+
import { IamPolicySchema } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/api_pb";
|
|
16
|
+
import { ApiResourceRefViewSchema } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/io_pb";
|
|
17
|
+
import type { IamPolicySpec } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/spec_pb";
|
|
18
|
+
|
|
19
|
+
import { buildPrincipalAccessList, resolveHierarchy } from "../access-lists.js";
|
|
20
|
+
import type { PrincipalDisplayResolver } from "../access-lists.js";
|
|
21
|
+
import { policyIdFor } from "../constants.js";
|
|
22
|
+
import { fakeIamPolicyStore, orgRole, triple } from "./support.js";
|
|
23
|
+
|
|
24
|
+
function row(spec: IamPolicySpec) {
|
|
25
|
+
return create(IamPolicySchema, {
|
|
26
|
+
metadata: { id: policyIdFor(spec) },
|
|
27
|
+
spec,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** `child` is scoped under `parent`: the structural link the walk follows. */
|
|
32
|
+
function scopeLink(
|
|
33
|
+
child: { kind: string; id: string },
|
|
34
|
+
parent: { kind: string; id: string },
|
|
35
|
+
): IamPolicySpec {
|
|
36
|
+
return triple(parent, parent.kind, child);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const nobody: PrincipalDisplayResolver = {
|
|
40
|
+
resolveIdentityAccounts: () => Promise.resolve(new Map()),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
describe("resolveHierarchy", () => {
|
|
44
|
+
it("is the resource alone when inherited grants are not asked for", async () => {
|
|
45
|
+
const store = fakeIamPolicyStore();
|
|
46
|
+
await store.save(
|
|
47
|
+
row(
|
|
48
|
+
scopeLink(
|
|
49
|
+
{ kind: "agent", id: "a1" },
|
|
50
|
+
{ kind: "organization", id: "acme" },
|
|
51
|
+
),
|
|
52
|
+
),
|
|
53
|
+
);
|
|
54
|
+
expect(await resolveHierarchy(store, "agent", "a1", false)).toEqual([
|
|
55
|
+
{ kind: "agent", id: "a1" },
|
|
56
|
+
]);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("climbs the scope links child-first and stops where no link exists", async () => {
|
|
60
|
+
const store = fakeIamPolicyStore();
|
|
61
|
+
await store.save(
|
|
62
|
+
row(
|
|
63
|
+
scopeLink(
|
|
64
|
+
{ kind: "agent", id: "a1" },
|
|
65
|
+
{ kind: "organization", id: "acme" },
|
|
66
|
+
),
|
|
67
|
+
),
|
|
68
|
+
);
|
|
69
|
+
expect(await resolveHierarchy(store, "agent", "a1", true)).toEqual([
|
|
70
|
+
{ kind: "agent", id: "a1" },
|
|
71
|
+
{ kind: "organization", id: "acme" },
|
|
72
|
+
]);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("stops at platform, the terminal kind, even when platform carries a link", async () => {
|
|
76
|
+
const store = fakeIamPolicyStore();
|
|
77
|
+
await store.save(
|
|
78
|
+
row(
|
|
79
|
+
scopeLink(
|
|
80
|
+
{ kind: "organization", id: "acme" },
|
|
81
|
+
{ kind: "platform", id: "stigmer" },
|
|
82
|
+
),
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
await store.save(
|
|
86
|
+
row(
|
|
87
|
+
scopeLink(
|
|
88
|
+
{ kind: "platform", id: "stigmer" },
|
|
89
|
+
{ kind: "organization", id: "loop" },
|
|
90
|
+
),
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
expect(await resolveHierarchy(store, "organization", "acme", true)).toEqual(
|
|
94
|
+
[
|
|
95
|
+
{ kind: "organization", id: "acme" },
|
|
96
|
+
{ kind: "platform", id: "stigmer" },
|
|
97
|
+
],
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("walks at most five steps — a cycle cannot spin it", async () => {
|
|
102
|
+
const store = fakeIamPolicyStore();
|
|
103
|
+
await store.save(
|
|
104
|
+
row(scopeLink({ kind: "agent", id: "a" }, { kind: "agent", id: "b" })),
|
|
105
|
+
);
|
|
106
|
+
await store.save(
|
|
107
|
+
row(scopeLink({ kind: "agent", id: "b" }, { kind: "agent", id: "a" })),
|
|
108
|
+
);
|
|
109
|
+
const levels = await resolveHierarchy(store, "agent", "a", true);
|
|
110
|
+
expect(levels).toHaveLength(6);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("ignores rows whose principal is a person or whose relation is owner/creator — those are grants, not links", async () => {
|
|
114
|
+
const store = fakeIamPolicyStore();
|
|
115
|
+
await store.save(row(orgRole("ida_alice", "owner", "acme")));
|
|
116
|
+
await store.save(
|
|
117
|
+
row(
|
|
118
|
+
triple({ kind: "organization", id: "parent" }, "owner", {
|
|
119
|
+
kind: "organization",
|
|
120
|
+
id: "acme",
|
|
121
|
+
}),
|
|
122
|
+
),
|
|
123
|
+
);
|
|
124
|
+
expect(await resolveHierarchy(store, "organization", "acme", true)).toEqual(
|
|
125
|
+
[{ kind: "organization", id: "acme" }],
|
|
126
|
+
);
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe("buildPrincipalAccessList", () => {
|
|
131
|
+
it("answers nothing for a resource with no assignable-role rows — structural rows never show", async () => {
|
|
132
|
+
const store = fakeIamPolicyStore();
|
|
133
|
+
await store.save(
|
|
134
|
+
row(
|
|
135
|
+
scopeLink(
|
|
136
|
+
{ kind: "agent", id: "a1" },
|
|
137
|
+
{ kind: "organization", id: "acme" },
|
|
138
|
+
),
|
|
139
|
+
),
|
|
140
|
+
);
|
|
141
|
+
expect(
|
|
142
|
+
await buildPrincipalAccessList(store, nobody, [
|
|
143
|
+
{ kind: "agent", id: "a1" },
|
|
144
|
+
]),
|
|
145
|
+
).toEqual([]);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("groups by principal in first-seen order, one RoleGrant per row, direct grants without an owner resource", async () => {
|
|
149
|
+
const store = fakeIamPolicyStore();
|
|
150
|
+
await store.save(row(orgRole("ida_bob", "member", "acme")));
|
|
151
|
+
await store.save(row(orgRole("ida_alice", "owner", "acme")));
|
|
152
|
+
await store.save(row(orgRole("ida_bob", "viewer", "acme")));
|
|
153
|
+
const entries = await buildPrincipalAccessList(store, nobody, [
|
|
154
|
+
{ kind: "organization", id: "acme" },
|
|
155
|
+
]);
|
|
156
|
+
expect(entries.map((e) => e.principal?.id)).toEqual([
|
|
157
|
+
"ida_bob",
|
|
158
|
+
"ida_alice",
|
|
159
|
+
]);
|
|
160
|
+
const bob = entries[0];
|
|
161
|
+
expect(bob?.roles.map((g) => g.role?.code)).toEqual(["member", "viewer"]);
|
|
162
|
+
expect(
|
|
163
|
+
bob?.roles.every((g) => !g.isInherited && g.ownerResource === undefined),
|
|
164
|
+
).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("marks grants from a parent level as inherited and names the owning level", async () => {
|
|
168
|
+
const store = fakeIamPolicyStore();
|
|
169
|
+
await store.save(row(orgRole("ida_alice", "admin", "acme")));
|
|
170
|
+
const entries = await buildPrincipalAccessList(store, nobody, [
|
|
171
|
+
{ kind: "agent", id: "a1" },
|
|
172
|
+
{ kind: "organization", id: "acme" },
|
|
173
|
+
]);
|
|
174
|
+
expect(entries).toHaveLength(1);
|
|
175
|
+
const grant = entries[0]?.roles[0];
|
|
176
|
+
expect(grant?.isInherited).toBe(true);
|
|
177
|
+
expect(grant?.ownerResource?.kind).toBe("organization");
|
|
178
|
+
expect(grant?.ownerResource?.id).toBe("acme");
|
|
179
|
+
expect(grant?.role?.code).toBe("admin");
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("renders an unresolved principal of ANY kind as kind/id with the id as its name", async () => {
|
|
183
|
+
const store = fakeIamPolicyStore();
|
|
184
|
+
await store.save(row(orgRole("ida_ghost", "viewer", "acme")));
|
|
185
|
+
await store.save(
|
|
186
|
+
row(
|
|
187
|
+
triple({ kind: "team", id: "tm_1" }, "viewer", {
|
|
188
|
+
kind: "organization",
|
|
189
|
+
id: "acme",
|
|
190
|
+
}),
|
|
191
|
+
),
|
|
192
|
+
);
|
|
193
|
+
const entries = await buildPrincipalAccessList(store, nobody, [
|
|
194
|
+
{ kind: "organization", id: "acme" },
|
|
195
|
+
]);
|
|
196
|
+
expect(
|
|
197
|
+
entries.map((e) => [
|
|
198
|
+
e.principal?.kind,
|
|
199
|
+
e.principal?.id,
|
|
200
|
+
e.principal?.name,
|
|
201
|
+
]),
|
|
202
|
+
).toEqual([
|
|
203
|
+
["identity_account", "ida_ghost", "ida_ghost"],
|
|
204
|
+
["team", "tm_1", "tm_1"],
|
|
205
|
+
]);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("asks the resolver once for the identity-account ids and uses its view where it answers", async () => {
|
|
209
|
+
const store = fakeIamPolicyStore();
|
|
210
|
+
await store.save(row(orgRole("ida_alice", "owner", "acme")));
|
|
211
|
+
await store.save(row(orgRole("ida_bob", "member", "acme")));
|
|
212
|
+
const asked: string[][] = [];
|
|
213
|
+
const resolver: PrincipalDisplayResolver = {
|
|
214
|
+
resolveIdentityAccounts: (ids) => {
|
|
215
|
+
asked.push([...ids]);
|
|
216
|
+
return Promise.resolve(
|
|
217
|
+
new Map([
|
|
218
|
+
[
|
|
219
|
+
"ida_alice",
|
|
220
|
+
create(ApiResourceRefViewSchema, {
|
|
221
|
+
kind: "identity_account",
|
|
222
|
+
id: "ida_alice",
|
|
223
|
+
name: "Alice",
|
|
224
|
+
email: "alice@example.com",
|
|
225
|
+
}),
|
|
226
|
+
],
|
|
227
|
+
]),
|
|
228
|
+
);
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
const entries = await buildPrincipalAccessList(store, resolver, [
|
|
232
|
+
{ kind: "organization", id: "acme" },
|
|
233
|
+
]);
|
|
234
|
+
expect(asked).toEqual([["ida_alice", "ida_bob"]]);
|
|
235
|
+
expect(entries.map((e) => e.principal?.name)).toEqual(["Alice", "ida_bob"]);
|
|
236
|
+
expect(entries[0]?.principal?.email).toBe("alice@example.com");
|
|
237
|
+
});
|
|
238
|
+
});
|