@stigmer/server 3.15.2-dev.20260913223433 → 3.15.3-dev.20260914100347
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/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 +56 -1
- package/dist/pipeline/apiresource-meta.d.ts.map +1 -1
- package/dist/pipeline/apiresource-meta.js +155 -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/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/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__/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/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__/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 +181 -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/authorize.ts +46 -21
- package/src/pipeline/steps/defaults.ts +42 -0
- package/src/store/__tests__/port-contract.test.ts +123 -0
- package/src/store/port-contract.ts +102 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The access-list display resolver (20260913.01 slice 5; Q-S5-4, Q-S5-8):
|
|
3
|
+
* the account half of the Java PrincipalEnricher — batch-loads identity
|
|
4
|
+
* accounts through the identity-account domain's PORT (`findByIds`, one
|
|
5
|
+
* row per distinct id, the port's own line) and renders the
|
|
6
|
+
* ApiResourceRefView shape access listings carry: display name, email,
|
|
7
|
+
* avatar, identity origin. Moved from the cloud's iam/account/
|
|
8
|
+
* display-resolver.ts; built by the IamPolicy controller over the accounts
|
|
9
|
+
* port it already holds, never composed as a seam — the data is a domain
|
|
10
|
+
* READ, the same in every edition.
|
|
11
|
+
*
|
|
12
|
+
* The display-name precedence is `first + last > first > last >
|
|
13
|
+
* metadata.name > email`. The `metadata.name` arm is this domain's one
|
|
14
|
+
* deliberate divergence from the Java code (Q-S5-4): the trusted-local
|
|
15
|
+
* operator account carries its display name THERE and empty first/last
|
|
16
|
+
* names (identityaccount/operator.ts), while the cloud names every
|
|
17
|
+
* provisioned account by its EMAIL and fills first/last from userinfo
|
|
18
|
+
* (identityaccount/provisioning.ts; the DefaultAccountName step) — so on
|
|
19
|
+
* the cloud this arm and the email arm answer the same string, and on a
|
|
20
|
+
* laptop the Members row reads the name the operator configured instead
|
|
21
|
+
* of an address. `email` stays as the last resort for a row whose name
|
|
22
|
+
* was never defaulted.
|
|
23
|
+
*
|
|
24
|
+
* The federated provider label stays empty (the cloud's 3C provider
|
|
25
|
+
* display-name lookup never landed in the shared shape); direct and
|
|
26
|
+
* machine accounts carry the "Stigmer" label, the Java constant.
|
|
27
|
+
*/
|
|
28
|
+
import { create } from "@bufbuild/protobuf";
|
|
29
|
+
|
|
30
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
31
|
+
import type { IdentityAccount } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/api_pb";
|
|
32
|
+
import { IdentityAccountProvisioningMode } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/enum_pb";
|
|
33
|
+
import type { ApiResourceRefView } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/io_pb";
|
|
34
|
+
import {
|
|
35
|
+
ApiResourceRefViewSchema,
|
|
36
|
+
IdentityOriginSchema,
|
|
37
|
+
} from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/io_pb";
|
|
38
|
+
|
|
39
|
+
import { kindEnumName } from "../../pipeline/apiresource-meta.js";
|
|
40
|
+
import type { IdentityAccountStore } from "../identityaccount/store.js";
|
|
41
|
+
import type { PrincipalDisplayResolver } from "./access-lists.js";
|
|
42
|
+
|
|
43
|
+
/** The Java STIGMER_PROVIDER_LABEL constant. */
|
|
44
|
+
const STIGMER_PROVIDER_LABEL = "Stigmer";
|
|
45
|
+
|
|
46
|
+
/** The one read enrichment needs from the identity-account port. */
|
|
47
|
+
export type AccountsByIds = Pick<IdentityAccountStore, "findByIds">;
|
|
48
|
+
|
|
49
|
+
export function newAccountDisplayResolver(
|
|
50
|
+
accounts: AccountsByIds,
|
|
51
|
+
): PrincipalDisplayResolver {
|
|
52
|
+
return {
|
|
53
|
+
async resolveIdentityAccounts(
|
|
54
|
+
ids,
|
|
55
|
+
): Promise<ReadonlyMap<string, ApiResourceRefView>> {
|
|
56
|
+
const rows = await accounts.findByIds(ids);
|
|
57
|
+
const resolved = new Map<string, ApiResourceRefView>();
|
|
58
|
+
for (const account of rows) {
|
|
59
|
+
const id = account.metadata?.id ?? "";
|
|
60
|
+
resolved.set(id, viewOf(id, account));
|
|
61
|
+
}
|
|
62
|
+
return resolved;
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function viewOf(id: string, account: IdentityAccount): ApiResourceRefView {
|
|
68
|
+
const spec = account.spec;
|
|
69
|
+
return create(ApiResourceRefViewSchema, {
|
|
70
|
+
kind: kindEnumName(ApiResourceKind.identity_account),
|
|
71
|
+
id,
|
|
72
|
+
name: displayNameOf(
|
|
73
|
+
spec?.firstName ?? "",
|
|
74
|
+
spec?.lastName ?? "",
|
|
75
|
+
account.metadata?.name ?? "",
|
|
76
|
+
spec?.email ?? "",
|
|
77
|
+
),
|
|
78
|
+
email: spec?.email ?? "",
|
|
79
|
+
avatar: spec?.pictureUrl ?? "",
|
|
80
|
+
identityOrigin: create(IdentityOriginSchema, {
|
|
81
|
+
provisioningMode:
|
|
82
|
+
spec?.provisioningMode ??
|
|
83
|
+
IdentityAccountProvisioningMode.identity_account_provisioning_mode_unspecified,
|
|
84
|
+
providerDisplayName: providerLabelOf(spec?.provisioningMode),
|
|
85
|
+
}),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The Java buildDisplayName precedence, with the account's own name before the email (Q-S5-4). */
|
|
90
|
+
function displayNameOf(
|
|
91
|
+
firstName: string,
|
|
92
|
+
lastName: string,
|
|
93
|
+
accountName: string,
|
|
94
|
+
email: string,
|
|
95
|
+
): string {
|
|
96
|
+
if (firstName !== "" && lastName !== "") {
|
|
97
|
+
return `${firstName} ${lastName}`;
|
|
98
|
+
}
|
|
99
|
+
if (firstName !== "") {
|
|
100
|
+
return firstName;
|
|
101
|
+
}
|
|
102
|
+
if (lastName !== "") {
|
|
103
|
+
return lastName;
|
|
104
|
+
}
|
|
105
|
+
if (accountName !== "") {
|
|
106
|
+
return accountName;
|
|
107
|
+
}
|
|
108
|
+
return email;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function providerLabelOf(
|
|
112
|
+
mode: IdentityAccountProvisioningMode | undefined,
|
|
113
|
+
): string {
|
|
114
|
+
switch (mode) {
|
|
115
|
+
case IdentityAccountProvisioningMode.direct:
|
|
116
|
+
case IdentityAccountProvisioningMode.machine:
|
|
117
|
+
return STIGMER_PROVIDER_LABEL;
|
|
118
|
+
default:
|
|
119
|
+
// federated / platform_client / unspecified carry no external
|
|
120
|
+
// provider label (the Java switch arms).
|
|
121
|
+
return "";
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ONE grant and revoke path of the IamPolicy domain (20260913.01,
|
|
3
|
+
* T01_0_plan.md §3a; T01_1_review.md Q-OR-1), moved from the cloud's
|
|
4
|
+
* iam/policy/service.ts into open source so every edition writes and
|
|
5
|
+
* deletes a policy row the same way. Its callers: the IamPolicy command
|
|
6
|
+
* controller (user grants and the three system RPCs), the built-in role
|
|
7
|
+
* lifecycle (the organization creator's `owner` row; cleanup on delete)
|
|
8
|
+
* and the membership rules (first-sign-in roles). Nothing else builds,
|
|
9
|
+
* saves or deletes a policy row; a composition reaches this path as
|
|
10
|
+
* in-process RPCs, never through an exported constructor.
|
|
11
|
+
*
|
|
12
|
+
* The two cloud#425 ordering invariants live here, nowhere else:
|
|
13
|
+
* CREATE: row before tuple — a crash between the two leaves
|
|
14
|
+
* row-without-tuple (granted on paper, denied in practice), healed by
|
|
15
|
+
* the caller's natural retry (the tuple write deliberately runs on
|
|
16
|
+
* duplicates too — the inline heal) or the boot backfill.
|
|
17
|
+
* DELETE: tuple before row — a crash leaves row+tuple (revoke failed,
|
|
18
|
+
* retry works); the reverse order was the fail-open incident class
|
|
19
|
+
* ("revoke access" silently becoming "keep access forever").
|
|
20
|
+
* "Tuple" here is whatever the composed ResourceAuthorizationLifecycle does
|
|
21
|
+
* in `onPolicyGranted` and `onPolicyRevoked`; with none composed, or one
|
|
22
|
+
* without the two optional hooks, the path writes rows and notifies
|
|
23
|
+
* nothing (the OSS posture: rows are the record).
|
|
24
|
+
*
|
|
25
|
+
* The row for a triple is found BY TRIPLE, never by derived id. Open
|
|
26
|
+
* source's rows carry the derived id (constants.ts policyIdFor), so the
|
|
27
|
+
* two lookups would agree here; the cloud's Java-era rows carry random
|
|
28
|
+
* `iamp_<ulid>` ids and must revoke and deduplicate through this same
|
|
29
|
+
* code, so `findByTriple` (the pair's rows filtered by relation) is the
|
|
30
|
+
* one definition of "the row for this triple" and `findById` serves the
|
|
31
|
+
* `get` RPC alone. On the OSS adapter that read is one scan of the kind
|
|
32
|
+
* (resource-store.ts); §4 measures it.
|
|
33
|
+
*
|
|
34
|
+
* The path is the one writer, so it refuses before it writes (Q-S2-1;
|
|
35
|
+
* slice 1 ruling 2): before any read, write or hook, `grant` and
|
|
36
|
+
* `revokeBySpec` run the domain's wire refusals (wire-refusals.ts — a spec
|
|
37
|
+
* whose resource or principal kind is not an ApiResourceKind member name,
|
|
38
|
+
* or whose fields hold a canonical-text delimiter, is INVALID_ARGUMENT
|
|
39
|
+
* with the pinned copy; slice 5 made that module the one home of the rule
|
|
40
|
+
* the controller and the ValidateGrantableRole step share). The
|
|
41
|
+
* controller refuses the same kinds BEFORE position 1 (Q-S6-1), so a wire
|
|
42
|
+
* caller never reaches this check; it stands for the callers that enter
|
|
43
|
+
* here without the controller — the built-in lifecycle and the membership
|
|
44
|
+
* rules — and is what keeps a garbage row out of the store and a garbage
|
|
45
|
+
* spec away from a composition's tuple delete from every door.
|
|
46
|
+
* `cleanupResource` and `revokeOrgAccess` take refs and ids their callers
|
|
47
|
+
* read from rows, so they do not refuse.
|
|
48
|
+
*
|
|
49
|
+
* The row this path builds: the proto's apiVersion const and kind, the
|
|
50
|
+
* derived id, the spec as given, and the caller's audit stamp through the
|
|
51
|
+
* platform's one stamper (setAuditFieldsForCreate) — the cloud's
|
|
52
|
+
* `buildNewPolicy` stamped neither org nor creator; this path stamps the
|
|
53
|
+
* creator and, like the cloud, no `metadata.org`, because a policy may
|
|
54
|
+
* span organizations and none owns it.
|
|
55
|
+
*/
|
|
56
|
+
import { create } from "@bufbuild/protobuf";
|
|
57
|
+
|
|
58
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
59
|
+
import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
|
|
60
|
+
import type { IamPolicy } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/api_pb";
|
|
61
|
+
import { IamPolicySchema } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/api_pb";
|
|
62
|
+
import type {
|
|
63
|
+
ApiResourceRef,
|
|
64
|
+
IamPolicySpec,
|
|
65
|
+
} from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/spec_pb";
|
|
66
|
+
|
|
67
|
+
import type { Logger } from "../../boot/logger.js";
|
|
68
|
+
import type { CallerIdentity } from "../../extensions/identity.js";
|
|
69
|
+
import type { ResourceAuthorizationLifecycle } from "../../extensions/resource-authorization.js";
|
|
70
|
+
import { kindEnumName } from "../../pipeline/apiresource-meta.js";
|
|
71
|
+
import { setAuditFieldsForCreate } from "../../pipeline/steps/defaults.js";
|
|
72
|
+
import {
|
|
73
|
+
IAM_POLICY_API_VERSION,
|
|
74
|
+
IAM_POLICY_KIND,
|
|
75
|
+
policyIdFor,
|
|
76
|
+
} from "./constants.js";
|
|
77
|
+
import { DuplicatePolicyError } from "./store.js";
|
|
78
|
+
import type { IamPolicyStore } from "./store.js";
|
|
79
|
+
import { refsOf, requireWellFormedTriple } from "./wire-refusals.js";
|
|
80
|
+
|
|
81
|
+
export interface GrantResult {
|
|
82
|
+
readonly policy: IamPolicy;
|
|
83
|
+
/** True when the triple was already held: no row was written, the hook still fired. */
|
|
84
|
+
readonly duplicate: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface IamPolicyGrantPath {
|
|
88
|
+
/**
|
|
89
|
+
* Grant the triple as `caller`: gate, find by triple, write the row when
|
|
90
|
+
* absent (the caller's audit stamp), then `onPolicyGranted` — on the
|
|
91
|
+
* duplicate arm too. A hook throw fails the grant with the row in place.
|
|
92
|
+
*/
|
|
93
|
+
grant(spec: IamPolicySpec, caller: CallerIdentity): Promise<GrantResult>;
|
|
94
|
+
/**
|
|
95
|
+
* Revoke the triple: gate, find by triple, `onPolicyRevoked` (with the
|
|
96
|
+
* row, or with the spec alone on the absent arm), then delete the row.
|
|
97
|
+
* A hook throw leaves row and tuple. Answers the revoked row, or
|
|
98
|
+
* `undefined` when there was none.
|
|
99
|
+
*/
|
|
100
|
+
revokeBySpec(spec: IamPolicySpec): Promise<IamPolicy | undefined>;
|
|
101
|
+
/**
|
|
102
|
+
* Revoke every row naming the ref as its resource, then every row naming
|
|
103
|
+
* it as its principal, each through the revoke order; a row that is both
|
|
104
|
+
* is revoked once. Nothing to clean is a no-op.
|
|
105
|
+
*/
|
|
106
|
+
cleanupResource(ref: ApiResourceRef): Promise<void>;
|
|
107
|
+
/** Revoke every relation the account holds directly on the organization. */
|
|
108
|
+
revokeOrgAccess(
|
|
109
|
+
identityAccountId: string,
|
|
110
|
+
organizationId: string,
|
|
111
|
+
): Promise<void>;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface IamPolicyGrantPathDeps {
|
|
115
|
+
readonly policies: IamPolicyStore;
|
|
116
|
+
/** The composed driver, if any; a driver without the two policy hooks is the same as none. */
|
|
117
|
+
readonly lifecycle: ResourceAuthorizationLifecycle | undefined;
|
|
118
|
+
readonly logger: Logger;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function newIamPolicyGrantPath(
|
|
122
|
+
deps: IamPolicyGrantPathDeps,
|
|
123
|
+
): IamPolicyGrantPath {
|
|
124
|
+
const { policies, lifecycle, logger } = deps;
|
|
125
|
+
|
|
126
|
+
/** The row holding this exact triple — the pair's rows, filtered by relation. */
|
|
127
|
+
async function findByTriple(
|
|
128
|
+
spec: IamPolicySpec,
|
|
129
|
+
): Promise<IamPolicy | undefined> {
|
|
130
|
+
const { principal, resource } = refsOf(spec);
|
|
131
|
+
const onPair = await policies.findByPrincipalAndResource(
|
|
132
|
+
principal.kind,
|
|
133
|
+
principal.id,
|
|
134
|
+
resource.kind,
|
|
135
|
+
resource.id,
|
|
136
|
+
);
|
|
137
|
+
return onPair.find(
|
|
138
|
+
(policy) =>
|
|
139
|
+
policy.spec !== undefined &&
|
|
140
|
+
policy.spec.relation === spec.relation &&
|
|
141
|
+
policy.spec.principal?.relation === principal.relation,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** The revoke order for one row: the hook, then the delete. */
|
|
146
|
+
async function revokeRow(policy: IamPolicy): Promise<void> {
|
|
147
|
+
const spec = policy.spec;
|
|
148
|
+
if (spec === undefined) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`policy ${policy.metadata?.id ?? "?"} has no spec — corrupt row`,
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
await lifecycle?.onPolicyRevoked?.({ spec, policy });
|
|
154
|
+
const id = policy.metadata?.id ?? "";
|
|
155
|
+
await policies.deleteById(id);
|
|
156
|
+
logger.info("iam policy revoked", fieldsOf(id, spec));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return {
|
|
160
|
+
async grant(spec, caller): Promise<GrantResult> {
|
|
161
|
+
requireWellFormedTriple(spec);
|
|
162
|
+
let policy = await findByTriple(spec);
|
|
163
|
+
let duplicate = policy !== undefined;
|
|
164
|
+
if (policy === undefined) {
|
|
165
|
+
const fresh = buildRow(spec, caller);
|
|
166
|
+
try {
|
|
167
|
+
await policies.save(fresh);
|
|
168
|
+
policy = fresh;
|
|
169
|
+
logger.info("iam policy granted", fieldsOf(policyIdFor(spec), spec));
|
|
170
|
+
} catch (error) {
|
|
171
|
+
if (!(error instanceof DuplicatePolicyError)) {
|
|
172
|
+
throw error;
|
|
173
|
+
}
|
|
174
|
+
// Lost the race to another writer of the same triple: the
|
|
175
|
+
// winner's row is the grant, exactly as if it had been found
|
|
176
|
+
// above. A winner that cannot be read back means the refusal
|
|
177
|
+
// was not a race — propagate it.
|
|
178
|
+
const winner = await findByTriple(spec);
|
|
179
|
+
if (winner === undefined) {
|
|
180
|
+
throw error;
|
|
181
|
+
}
|
|
182
|
+
policy = winner;
|
|
183
|
+
duplicate = true;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (duplicate) {
|
|
187
|
+
logger.debug(
|
|
188
|
+
"iam policy already held",
|
|
189
|
+
fieldsOf(policy.metadata?.id ?? "", spec),
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
// Deliberately unconditional (cloud#425): a duplicate re-grant is the
|
|
193
|
+
// inline heal for a row whose tuple never landed.
|
|
194
|
+
await lifecycle?.onPolicyGranted?.({ policy, duplicate });
|
|
195
|
+
return { policy, duplicate };
|
|
196
|
+
},
|
|
197
|
+
|
|
198
|
+
async revokeBySpec(spec): Promise<IamPolicy | undefined> {
|
|
199
|
+
requireWellFormedTriple(spec);
|
|
200
|
+
const existing = await findByTriple(spec);
|
|
201
|
+
if (existing === undefined) {
|
|
202
|
+
// No row: a composition still deletes the bare tuple (one written
|
|
203
|
+
// before the row mirror existed); converges the store either way.
|
|
204
|
+
await lifecycle?.onPolicyRevoked?.({ spec, policy: undefined });
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
await revokeRow(existing);
|
|
208
|
+
return existing;
|
|
209
|
+
},
|
|
210
|
+
|
|
211
|
+
async cleanupResource(ref): Promise<void> {
|
|
212
|
+
const asResource = await policies.findByResource(ref.kind, ref.id);
|
|
213
|
+
const asPrincipal = await policies.findByPrincipal(ref.kind, ref.id);
|
|
214
|
+
for (const policy of distinctById([...asResource, ...asPrincipal])) {
|
|
215
|
+
await revokeRow(policy);
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
async revokeOrgAccess(identityAccountId, organizationId): Promise<void> {
|
|
220
|
+
const direct = await policies.findByPrincipalAndResource(
|
|
221
|
+
kindEnumName(ApiResourceKind.identity_account),
|
|
222
|
+
identityAccountId,
|
|
223
|
+
kindEnumName(ApiResourceKind.organization),
|
|
224
|
+
organizationId,
|
|
225
|
+
);
|
|
226
|
+
for (const policy of direct) {
|
|
227
|
+
await revokeRow(policy);
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** The row for a fresh grant: the contract's identity strings, the derived id, the caller's stamp. */
|
|
234
|
+
function buildRow(spec: IamPolicySpec, caller: CallerIdentity): IamPolicy {
|
|
235
|
+
const policy = create(IamPolicySchema, {
|
|
236
|
+
apiVersion: IAM_POLICY_API_VERSION,
|
|
237
|
+
kind: IAM_POLICY_KIND,
|
|
238
|
+
metadata: create(ApiResourceMetadataSchema, { id: policyIdFor(spec) }),
|
|
239
|
+
spec,
|
|
240
|
+
});
|
|
241
|
+
setAuditFieldsForCreate(IamPolicySchema, policy, caller);
|
|
242
|
+
return policy;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** One row per id, at its first position. */
|
|
246
|
+
function distinctById(
|
|
247
|
+
policies: ReadonlyArray<IamPolicy>,
|
|
248
|
+
): ReadonlyArray<IamPolicy> {
|
|
249
|
+
const seen = new Set<string>();
|
|
250
|
+
return policies.filter((policy) => {
|
|
251
|
+
const id = policy.metadata?.id ?? "";
|
|
252
|
+
if (seen.has(id)) {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
seen.add(id);
|
|
256
|
+
return true;
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** The structured fields every log line carries — ids and kinds, never a token. */
|
|
261
|
+
function fieldsOf(id: string, spec: IamPolicySpec): Record<string, string> {
|
|
262
|
+
return {
|
|
263
|
+
policyId: id,
|
|
264
|
+
relation: spec.relation,
|
|
265
|
+
principalKind: spec.principal?.kind ?? "",
|
|
266
|
+
principalId: spec.principal?.id ?? "",
|
|
267
|
+
resourceKind: spec.resource?.kind ?? "",
|
|
268
|
+
resourceId: spec.resource?.id ?? "",
|
|
269
|
+
};
|
|
270
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Open source's PolicyGrantScope (20260913.01, T01_1_review.md Q-OR-3 and
|
|
3
|
+
* Q-OR-4): the ORGANIZATION grants the roles its `kind_meta` lists —
|
|
4
|
+
* owner, admin, member, viewer — and no other kind grants anything. This
|
|
5
|
+
* is what lights the console's Members page in every edition while a
|
|
6
|
+
* viewer on one agent, the per-resource grant, stays what the Enterprise
|
|
7
|
+
* and Cloud editions add by composing a wider scope
|
|
8
|
+
* (extensions/policy-grant-scope.ts carries the contract every scope is
|
|
9
|
+
* held to: narrows the proto, total over the enum, synchronous).
|
|
10
|
+
*
|
|
11
|
+
* Installed by the composition root when no unit registers
|
|
12
|
+
* `drivers.policyGrantScope` — the `newPermissiveSingleTeamAuthorizer`
|
|
13
|
+
* shape: the default lives with the domain that defines its semantics and
|
|
14
|
+
* is called at the `??` site, never held by the registry.
|
|
15
|
+
*
|
|
16
|
+
* The organization's roles are READ from the proto through
|
|
17
|
+
* `grantableRolesFor`, not written here: the four words the Members page
|
|
18
|
+
* shows and entry 3's authorizer enforces have one source, and a proto
|
|
19
|
+
* change is the only way to change them (grant-scope.test.ts pins the
|
|
20
|
+
* equality; grantable-roles-for.test.ts pins the four).
|
|
21
|
+
*/
|
|
22
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
23
|
+
|
|
24
|
+
import type { PolicyGrantScope } from "../../extensions/policy-grant-scope.js";
|
|
25
|
+
import { grantableRolesFor } from "../../pipeline/apiresource-meta.js";
|
|
26
|
+
|
|
27
|
+
/** The open-source default: organization → its proto roles; every other kind → none. */
|
|
28
|
+
export function newOrganizationOnlyGrantScope(): PolicyGrantScope {
|
|
29
|
+
return {
|
|
30
|
+
grantableRoles(kind) {
|
|
31
|
+
return kind === ApiResourceKind.organization
|
|
32
|
+
? grantableRolesFor(kind)
|
|
33
|
+
: [];
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|