@stigmer/server 3.26.0 → 3.27.1
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/authorization/evaluator.d.ts +4 -1
- package/dist/authorization/evaluator.d.ts.map +1 -1
- package/dist/authorization/evaluator.js +8 -0
- package/dist/authorization/evaluator.js.map +1 -1
- package/dist/authorization/model/agent.d.ts.map +1 -1
- package/dist/authorization/model/agent.js +4 -2
- package/dist/authorization/model/agent.js.map +1 -1
- package/dist/authorization/model/agent_channel.d.ts.map +1 -1
- package/dist/authorization/model/agent_channel.js +5 -4
- package/dist/authorization/model/agent_channel.js.map +1 -1
- package/dist/authorization/model/agent_instance.d.ts.map +1 -1
- package/dist/authorization/model/agent_instance.js +3 -2
- package/dist/authorization/model/agent_instance.js.map +1 -1
- package/dist/authorization/model/mcp_server.d.ts.map +1 -1
- package/dist/authorization/model/mcp_server.js +3 -2
- package/dist/authorization/model/mcp_server.js.map +1 -1
- package/dist/authorization/model/organization.d.ts.map +1 -1
- package/dist/authorization/model/organization.js +3 -0
- package/dist/authorization/model/organization.js.map +1 -1
- package/dist/authorization/model/plugin.d.ts.map +1 -1
- package/dist/authorization/model/plugin.js +4 -2
- package/dist/authorization/model/plugin.js.map +1 -1
- package/dist/authorization/model/rewrite.d.ts +25 -7
- package/dist/authorization/model/rewrite.d.ts.map +1 -1
- package/dist/authorization/model/rewrite.js +40 -1
- package/dist/authorization/model/rewrite.js.map +1 -1
- package/dist/authorization/model/schedule.d.ts.map +1 -1
- package/dist/authorization/model/schedule.js +8 -3
- package/dist/authorization/model/schedule.js.map +1 -1
- package/dist/authorization/model/skill.d.ts.map +1 -1
- package/dist/authorization/model/skill.js +3 -2
- package/dist/authorization/model/skill.js.map +1 -1
- package/dist/authorization/model/workflow.d.ts.map +1 -1
- package/dist/authorization/model/workflow.js +3 -2
- package/dist/authorization/model/workflow.js.map +1 -1
- package/dist/authorization/model/workflow_execution.d.ts.map +1 -1
- package/dist/authorization/model/workflow_execution.js +4 -3
- package/dist/authorization/model/workflow_execution.js.map +1 -1
- package/dist/authorization/model/workflow_instance.d.ts.map +1 -1
- package/dist/authorization/model/workflow_instance.js +5 -1
- package/dist/authorization/model/workflow_instance.js.map +1 -1
- package/dist/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +2 -0
- package/dist/boot/compose.js.map +1 -1
- package/dist/domain/iampolicy/access-lists.d.ts +5 -3
- package/dist/domain/iampolicy/access-lists.d.ts.map +1 -1
- package/dist/domain/iampolicy/access-lists.js +72 -19
- package/dist/domain/iampolicy/access-lists.js.map +1 -1
- package/dist/domain/iampolicy/constants.d.ts +32 -6
- package/dist/domain/iampolicy/constants.d.ts.map +1 -1
- package/dist/domain/iampolicy/constants.js +41 -6
- package/dist/domain/iampolicy/constants.js.map +1 -1
- package/dist/domain/iampolicy/controller.d.ts +6 -0
- package/dist/domain/iampolicy/controller.d.ts.map +1 -1
- package/dist/domain/iampolicy/controller.js +16 -7
- package/dist/domain/iampolicy/controller.js.map +1 -1
- package/dist/domain/iampolicy/resource-store.d.ts.map +1 -1
- package/dist/domain/iampolicy/resource-store.js +2 -6
- package/dist/domain/iampolicy/resource-store.js.map +1 -1
- package/dist/domain/iampolicy/steps.d.ts.map +1 -1
- package/dist/domain/iampolicy/steps.js +49 -7
- package/dist/domain/iampolicy/steps.js.map +1 -1
- package/dist/domain/workflow/registry/data/task-kind-registry.json +1 -0
- package/dist/extensions/drivers.d.ts +12 -0
- package/dist/extensions/drivers.d.ts.map +1 -1
- package/dist/extensions/gate-slots.d.ts +10 -1
- package/dist/extensions/gate-slots.d.ts.map +1 -1
- package/dist/extensions/gate-slots.js +1 -0
- package/dist/extensions/gate-slots.js.map +1 -1
- package/dist/extensions/principal-display.d.ts +34 -0
- package/dist/extensions/principal-display.d.ts.map +1 -0
- package/dist/extensions/principal-display.js +2 -0
- package/dist/extensions/principal-display.js.map +1 -0
- package/dist/extensions/registry.d.ts +6 -0
- package/dist/extensions/registry.d.ts.map +1 -1
- package/dist/extensions/registry.js +10 -0
- package/dist/extensions/registry.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -1
- package/dist/index.js.map +1 -1
- package/dist/pipeline/apiresource-meta.d.ts +10 -0
- package/dist/pipeline/apiresource-meta.d.ts.map +1 -1
- package/dist/pipeline/apiresource-meta.js +16 -0
- package/dist/pipeline/apiresource-meta.js.map +1 -1
- package/package.json +6 -6
- package/src/authorization/__tests__/evaluator.test.ts +127 -2
- package/src/authorization/evaluator.ts +12 -1
- package/src/authorization/model/__tests__/registry.test.ts +64 -2
- package/src/authorization/model/agent.ts +4 -1
- package/src/authorization/model/agent_channel.ts +11 -3
- package/src/authorization/model/agent_instance.ts +3 -1
- package/src/authorization/model/mcp_server.ts +3 -1
- package/src/authorization/model/organization.ts +3 -0
- package/src/authorization/model/plugin.ts +4 -1
- package/src/authorization/model/rewrite.ts +65 -8
- package/src/authorization/model/schedule.ts +11 -2
- package/src/authorization/model/skill.ts +3 -1
- package/src/authorization/model/workflow.ts +3 -1
- package/src/authorization/model/workflow_execution.ts +4 -2
- package/src/authorization/model/workflow_instance.ts +5 -0
- package/src/boot/compose.ts +2 -0
- package/src/domain/iampolicy/__tests__/access-lists.test.ts +120 -6
- package/src/domain/iampolicy/__tests__/constants.test.ts +26 -4
- package/src/domain/iampolicy/__tests__/controller.test.ts +55 -1
- package/src/domain/iampolicy/__tests__/grant-path.test.ts +3 -3
- package/src/domain/iampolicy/__tests__/steps.test.ts +123 -1
- package/src/domain/iampolicy/access-lists.ts +98 -23
- package/src/domain/iampolicy/constants.ts +50 -6
- package/src/domain/iampolicy/controller.ts +30 -7
- package/src/domain/iampolicy/resource-store.ts +3 -6
- package/src/domain/iampolicy/steps.ts +89 -7
- package/src/domain/workflow/registry/data/task-kind-registry.json +1 -0
- package/src/extensions/__tests__/iam-policy-points.test.ts +30 -0
- package/src/extensions/__tests__/registry.test.ts +3 -0
- package/src/extensions/drivers.ts +12 -0
- package/src/extensions/gate-slots.ts +10 -0
- package/src/extensions/principal-display.ts +37 -0
- package/src/extensions/registry.ts +19 -0
- package/src/index.ts +26 -0
- package/src/pipeline/apiresource-meta.ts +21 -0
|
@@ -36,7 +36,9 @@
|
|
|
36
36
|
* console and SDK show these sentences verbatim, so they are contract. The
|
|
37
37
|
* new sentences are the two edition refusals (the federation shape), the
|
|
38
38
|
* unknown permission, the unknown principal kind (the resource sentence's
|
|
39
|
-
* shape) and the
|
|
39
|
+
* shape), the malformed triple, and the four principal refusals a team
|
|
40
|
+
* grantee brought (a person's qualifier, a team's qualifier, a kind no
|
|
41
|
+
* team may be granted, a role a team may not hold).
|
|
40
42
|
*/
|
|
41
43
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
42
44
|
import type {
|
|
@@ -157,8 +159,8 @@ export const ROLES_RECONCILED_KEY = "membership_rules_reconciled";
|
|
|
157
159
|
/**
|
|
158
160
|
* The principal kinds a PERSON may grant a role to — the user `create`
|
|
159
161
|
* lane's grantee vocabulary (2026-09-14, session 9 ruling Q-S9-2; the
|
|
160
|
-
* security read's finding 41)
|
|
161
|
-
*
|
|
162
|
+
* security read's finding 41): a person (the identity account) and a team
|
|
163
|
+
* of people. A row whose principal is a RESOURCE
|
|
162
164
|
* (`organization:A#organization@platform_client:X`, `#managed_org`) is a
|
|
163
165
|
* structural link, and structural links are `bootstrapPolicy`'s — the
|
|
164
166
|
* platform's own lane, which skips role validation by design.
|
|
@@ -169,14 +171,27 @@ export const ROLES_RECONCILED_KEY = "membership_rules_reconciled";
|
|
|
169
171
|
* two cannot drift), and the hierarchy walk follows it. Without the arm, a
|
|
170
172
|
* person with `can_grant_access` on organization B could write
|
|
171
173
|
* `organization:A#member@organization:B` and B's Members page, asked for
|
|
172
|
-
* inherited access, would list A's people.
|
|
173
|
-
*
|
|
174
|
-
*
|
|
174
|
+
* inherited access, would list A's people.
|
|
175
|
+
*
|
|
176
|
+
* Each kind grants through its own list: a person the kind's
|
|
177
|
+
* `grantable_roles`, a team its `team_grantable_roles`, always as the
|
|
178
|
+
* team's members (`TEAM_MEMBERS_RELATION`). A kind that lists no team role
|
|
179
|
+
* — `organization` among them, so open source's organization-only scope —
|
|
180
|
+
* grants a team nothing.
|
|
175
181
|
*/
|
|
176
182
|
export const USER_GRANT_PRINCIPAL_KINDS: ReadonlyArray<ApiResourceKind> = [
|
|
177
183
|
ApiResourceKind.identity_account,
|
|
184
|
+
ApiResourceKind.team,
|
|
178
185
|
];
|
|
179
186
|
|
|
187
|
+
/**
|
|
188
|
+
* The one relation a team principal names: the team's members. A grant to
|
|
189
|
+
* `team:<id>#member` is a grant to every member, and membership is bounded
|
|
190
|
+
* by organization membership in the model, so leaving the organization
|
|
191
|
+
* ends the grant with no cleanup.
|
|
192
|
+
*/
|
|
193
|
+
export const TEAM_MEMBERS_RELATION = "member";
|
|
194
|
+
|
|
180
195
|
// ---------------------------------------------------------------------------
|
|
181
196
|
// Byte-pinned copy (the cloud handlers' sentences, moved as-is).
|
|
182
197
|
// ---------------------------------------------------------------------------
|
|
@@ -249,6 +264,35 @@ export function principalNotGrantableMessage(kind: string): string {
|
|
|
249
264
|
return `Principal kind '${kind}' cannot be granted a role. Grantable principal kinds: [${grantable.join(", ")}]`;
|
|
250
265
|
}
|
|
251
266
|
|
|
267
|
+
/**
|
|
268
|
+
* A person principal carrying a relation qualifier (INVALID_ARGUMENT). A
|
|
269
|
+
* person is granted a role directly; `identity_account:<id>#<relation>`
|
|
270
|
+
* names no one, and OpenFGA would refuse the tuple after the row was
|
|
271
|
+
* written.
|
|
272
|
+
*/
|
|
273
|
+
export function personQualifierMessage(relation: string): string {
|
|
274
|
+
return `Principal kind 'identity_account' takes no relation; got '${relation}'`;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** A team principal naming a relation other than its members (INVALID_ARGUMENT). */
|
|
278
|
+
export function teamQualifierMessage(relation: string): string {
|
|
279
|
+
return `A team principal must name the relation '${TEAM_MEMBERS_RELATION}' (the team's members); got '${relation}'`;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** A team grant on a kind that lists no team role (INVALID_ARGUMENT). */
|
|
283
|
+
export function teamNotGrantableMessage(kind: string): string {
|
|
284
|
+
return `A team cannot be granted access to resource kind '${kind}'.`;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** A team grant of a role the kind does not let a team hold (INVALID_ARGUMENT); the list in kind_meta order. */
|
|
288
|
+
export function teamRoleNotGrantableMessage(
|
|
289
|
+
role: string,
|
|
290
|
+
kind: string,
|
|
291
|
+
grantable: ReadonlyArray<string>,
|
|
292
|
+
): string {
|
|
293
|
+
return `Role '${role}' cannot be granted to a team on resource kind '${kind}'. Roles a team can hold: [${grantable.join(", ")}]`;
|
|
294
|
+
}
|
|
295
|
+
|
|
252
296
|
/** A triple field holding a canonical-text delimiter (INVALID_ARGUMENT; Q-S2-1). */
|
|
253
297
|
export function malformedTripleMessage(field: string): string {
|
|
254
298
|
return `policy ${field} must not contain ':', '#' or '@'`;
|
|
@@ -31,9 +31,11 @@
|
|
|
31
31
|
* triple — strictly safer than the cloud's silent default instance.
|
|
32
32
|
* - `create` validates the role BEFORE it writes (the cloud's order): a
|
|
33
33
|
* held row under a role the kind does not grant is refused, never
|
|
34
|
-
* answered as a duplicate. It also grants to PEOPLE only
|
|
35
|
-
* 2026-09-14):
|
|
36
|
-
*
|
|
34
|
+
* answered as a duplicate. It also grants to PEOPLE and TEAMS only
|
|
35
|
+
* (Q-S9-2, 2026-09-14): any other principal is INVALID_ARGUMENT, and a
|
|
36
|
+
* team only on the roles its kind lets a team hold (steps.ts) and,
|
|
37
|
+
* where an edition serves teams, only a team the create gate slot
|
|
38
|
+
* admits — a row naming a resource as its principal is the
|
|
37
39
|
* structural link the hierarchy walk follows, `bootstrapPolicy`'s to
|
|
38
40
|
* write, and through this lane would have let a right on one
|
|
39
41
|
* organization list another's members. The cloud's create had the
|
|
@@ -136,9 +138,12 @@ import type { ServerEdition } from "@stigmer/protos/ai/stigmer/platform/v1/serve
|
|
|
136
138
|
import type { Logger } from "../../boot/logger.js";
|
|
137
139
|
import type { AuthorizationQueryEngine } from "../../extensions/authorization-queries.js";
|
|
138
140
|
import type { Authorizer } from "../../extensions/authorizer.js";
|
|
141
|
+
import { stepsForSlot } from "../../extensions/gate-slots.js";
|
|
142
|
+
import type { ResolvedGateSteps } from "../../extensions/gate-slots.js";
|
|
139
143
|
import { isPlatformPipelineCaller } from "../../extensions/identity.js";
|
|
140
144
|
import type { CallerIdentity } from "../../extensions/identity.js";
|
|
141
145
|
import type { PolicyGrantScope } from "../../extensions/policy-grant-scope.js";
|
|
146
|
+
import type { PrincipalDisplay } from "../../extensions/principal-display.js";
|
|
142
147
|
import {
|
|
143
148
|
kindByEnumName,
|
|
144
149
|
kindEnumName,
|
|
@@ -209,6 +214,10 @@ export interface IamPolicyControllerDeps {
|
|
|
209
214
|
readonly grantScope: PolicyGrantScope;
|
|
210
215
|
/** The composed query engine — undefined = the tuple-half RPCs refuse UNIMPLEMENTED. */
|
|
211
216
|
readonly queries: AuthorizationQueryEngine | undefined;
|
|
217
|
+
/** The composed principal display — undefined = a non-person grantee renders in the id fallback shape. */
|
|
218
|
+
readonly principalDisplay: PrincipalDisplay | undefined;
|
|
219
|
+
/** The merged gate steps; `create` splices `iam-policy-create:pre-side-effect-gate`. */
|
|
220
|
+
readonly gateSteps: ResolvedGateSteps;
|
|
212
221
|
/** The served edition — `checkMyPermission`'s first arm reads a kind's tier against it. */
|
|
213
222
|
readonly edition: ServerEdition;
|
|
214
223
|
readonly logger: Logger;
|
|
@@ -285,7 +294,10 @@ function guardSystemRpc(method: DescMethod, caller: CallerIdentity): void {
|
|
|
285
294
|
// The command chains
|
|
286
295
|
// ---------------------------------------------------------------------------
|
|
287
296
|
|
|
288
|
-
/**
|
|
297
|
+
/**
|
|
298
|
+
* `create`: the wire refusal, then Authorize → ValidateProto →
|
|
299
|
+
* ValidateGrantableRole → [iam-policy-create:pre-side-effect-gate] → Grant.
|
|
300
|
+
*/
|
|
289
301
|
function createPolicy(
|
|
290
302
|
deps: IamPolicyControllerDeps,
|
|
291
303
|
spec: IamPolicySpec,
|
|
@@ -303,8 +315,8 @@ function createPolicy(
|
|
|
303
315
|
|
|
304
316
|
/**
|
|
305
317
|
* The grant chain both `create` and `bootstrapPolicy` run; only the user
|
|
306
|
-
* lane validates the role — bootstrap writes
|
|
307
|
-
* scope would admit.
|
|
318
|
+
* lane validates the role and runs the create gate slot — bootstrap writes
|
|
319
|
+
* structural relations no scope would admit and no edition gates.
|
|
308
320
|
*/
|
|
309
321
|
async function grantThroughChain(
|
|
310
322
|
deps: IamPolicyControllerDeps,
|
|
@@ -327,6 +339,12 @@ async function grantThroughChain(
|
|
|
327
339
|
.addStep(newValidateProtoStep());
|
|
328
340
|
if (validateRole) {
|
|
329
341
|
pipeline.addStep(newValidateGrantableRoleStep(deps.grantScope));
|
|
342
|
+
for (const step of stepsForSlot<typeof IamPolicySpecSchema>(
|
|
343
|
+
deps.gateSteps,
|
|
344
|
+
"iam-policy-create:pre-side-effect-gate",
|
|
345
|
+
)) {
|
|
346
|
+
pipeline.addStep(step);
|
|
347
|
+
}
|
|
330
348
|
}
|
|
331
349
|
await pipeline.addStep(newGrantStep(deps.grantPath)).build().execute(reqCtx);
|
|
332
350
|
return policyResultOf(reqCtx, method);
|
|
@@ -600,7 +618,12 @@ async function listResourceAccessByPrincipal(
|
|
|
600
618
|
resource.id,
|
|
601
619
|
input.includeInherited,
|
|
602
620
|
);
|
|
603
|
-
return buildPrincipalAccessList(
|
|
621
|
+
return buildPrincipalAccessList(
|
|
622
|
+
deps.policies,
|
|
623
|
+
displayResolver,
|
|
624
|
+
deps.principalDisplay,
|
|
625
|
+
hierarchy,
|
|
626
|
+
);
|
|
604
627
|
}, "failed to list resource access");
|
|
605
628
|
return create(ResourceAccessByPrincipalListSchema, {
|
|
606
629
|
entries: [...entries],
|
|
@@ -54,13 +54,10 @@ const KIND = ApiResourceKind.iam_policy;
|
|
|
54
54
|
* principal half is derived from the user lane's grantee vocabulary
|
|
55
55
|
* (constants.ts USER_GRANT_PRINCIPAL_KINDS; Q-S9-2) so the writer's "who a
|
|
56
56
|
* person may grant to" and the reader's "what is a person, not a parent"
|
|
57
|
-
* are one definition
|
|
58
|
-
* ApiResourceKind, so no wire spec names it).
|
|
57
|
+
* are one definition: the identity account and the team.
|
|
59
58
|
*/
|
|
60
|
-
const NON_STRUCTURAL_PRINCIPAL_KINDS: ReadonlyArray<string> =
|
|
61
|
-
|
|
62
|
-
"team",
|
|
63
|
-
];
|
|
59
|
+
const NON_STRUCTURAL_PRINCIPAL_KINDS: ReadonlyArray<string> =
|
|
60
|
+
USER_GRANT_PRINCIPAL_KINDS.map((kind) => kindEnumName(kind));
|
|
64
61
|
const NON_STRUCTURAL_RELATIONS: ReadonlyArray<string> = ["owner", "creator"];
|
|
65
62
|
|
|
66
63
|
export function newResourceIamPolicyStore(store: Store): IamPolicyStore {
|
|
@@ -30,17 +30,32 @@
|
|
|
30
30
|
* intersection — the cloud's second copy listing it. Intersecting is
|
|
31
31
|
* what keeps a scope from WIDENING the contract by accident;
|
|
32
32
|
* 5. the PRINCIPAL (Q-S9-2, 2026-09-14; constants.ts
|
|
33
|
-
* USER_GRANT_PRINCIPAL_KINDS): a person grants roles to people
|
|
34
|
-
*
|
|
33
|
+
* USER_GRANT_PRINCIPAL_KINDS): a person grants roles to people and to
|
|
34
|
+
* teams of people, and to nothing else. A row whose principal is a
|
|
35
35
|
* resource is a structural link (the hierarchy walk's parent edge),
|
|
36
36
|
* which is `bootstrapPolicy`'s to write; through this lane it would
|
|
37
37
|
* let a right on one organization read another's members. Last, so
|
|
38
38
|
* every answer the cloud's create gives today is unchanged and only
|
|
39
|
-
* the row it should never have written is refused.
|
|
39
|
+
* the row it should never have written is refused. A person names no
|
|
40
|
+
* relation qualifier: `identity_account:<id>#<anything>` names
|
|
41
|
+
* nobody, and OpenFGA would refuse the tuple after the row was
|
|
42
|
+
* written, a grant on paper that is denied in practice.
|
|
43
|
+
*
|
|
44
|
+
* A TEAM principal takes its own list in place of arm 4: the kind's
|
|
45
|
+
* `team_grantable_roles` ∩ scope, never the person list, because a team
|
|
46
|
+
* is granted only what its members could each be granted and never
|
|
47
|
+
* ownership. It must name the team's members (`TEAM_MEMBERS_RELATION`).
|
|
48
|
+
* A kind that lists no team role refuses a team whatever the role, so
|
|
49
|
+
* open source (organization-only scope, and `organization` lists none)
|
|
50
|
+
* grants a team nothing without a branch of its own. Whether the team
|
|
51
|
+
* exists and belongs to the resource's organization needs a read, which
|
|
52
|
+
* this synchronous step does not make: that is the
|
|
53
|
+
* `iam-policy-create:pre-side-effect-gate` slot's, spliced after it.
|
|
40
54
|
*/
|
|
41
55
|
import { create } from "@bufbuild/protobuf";
|
|
42
56
|
import { Code, ConnectError } from "@connectrpc/connect";
|
|
43
57
|
|
|
58
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
44
59
|
import { IamPolicySchema } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/api_pb";
|
|
45
60
|
import type { RevokeOrgAccessInputSchema } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/io_pb";
|
|
46
61
|
import type {
|
|
@@ -51,15 +66,23 @@ import type {
|
|
|
51
66
|
import { IamRole } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
|
|
52
67
|
|
|
53
68
|
import type { PolicyGrantScope } from "../../extensions/policy-grant-scope.js";
|
|
54
|
-
import {
|
|
69
|
+
import {
|
|
70
|
+
grantableRolesFor,
|
|
71
|
+
kindEnumName,
|
|
72
|
+
teamGrantableRolesFor,
|
|
73
|
+
} from "../../pipeline/apiresource-meta.js";
|
|
55
74
|
import type { PipelineStep } from "../../pipeline/pipeline.js";
|
|
56
75
|
import type { RequestContext } from "../../pipeline/request-context.js";
|
|
57
76
|
import {
|
|
58
77
|
PER_RESOURCE_GRANTS_UNIMPLEMENTED_MESSAGE,
|
|
59
|
-
|
|
78
|
+
TEAM_MEMBERS_RELATION,
|
|
60
79
|
noGrantableRolesMessage,
|
|
80
|
+
personQualifierMessage,
|
|
61
81
|
principalNotGrantableMessage,
|
|
62
82
|
roleNotGrantableMessage,
|
|
83
|
+
teamNotGrantableMessage,
|
|
84
|
+
teamQualifierMessage,
|
|
85
|
+
teamRoleNotGrantableMessage,
|
|
63
86
|
} from "./constants.js";
|
|
64
87
|
import type { IamPolicyGrantPath } from "./grant-path.js";
|
|
65
88
|
import {
|
|
@@ -94,6 +117,17 @@ export function newValidateGrantableRoleStep(
|
|
|
94
117
|
Code.Unimplemented,
|
|
95
118
|
);
|
|
96
119
|
}
|
|
120
|
+
const principal = spec.principal;
|
|
121
|
+
if (principal?.kind === TEAM_KIND_NAME) {
|
|
122
|
+
requireTeamGrant(
|
|
123
|
+
spec.relation,
|
|
124
|
+
principal.relation,
|
|
125
|
+
kind,
|
|
126
|
+
kindName,
|
|
127
|
+
scoped,
|
|
128
|
+
);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
97
131
|
const grantable = contract.filter((role) => scoped.has(role));
|
|
98
132
|
if (!grantable.some((role) => IamRole[role] === spec.relation)) {
|
|
99
133
|
throw new ConnectError(
|
|
@@ -105,18 +139,66 @@ export function newValidateGrantableRoleStep(
|
|
|
105
139
|
Code.InvalidArgument,
|
|
106
140
|
);
|
|
107
141
|
}
|
|
108
|
-
const principalKindName =
|
|
142
|
+
const principalKindName = principal?.kind ?? "";
|
|
109
143
|
const principalKind = requireKnownPrincipalKind(principalKindName);
|
|
110
|
-
if (
|
|
144
|
+
if (principalKind !== ApiResourceKind.identity_account) {
|
|
111
145
|
throw new ConnectError(
|
|
112
146
|
principalNotGrantableMessage(principalKindName),
|
|
113
147
|
Code.InvalidArgument,
|
|
114
148
|
);
|
|
115
149
|
}
|
|
150
|
+
const qualifier = principal?.relation ?? "";
|
|
151
|
+
if (qualifier !== "") {
|
|
152
|
+
throw new ConnectError(
|
|
153
|
+
personQualifierMessage(qualifier),
|
|
154
|
+
Code.InvalidArgument,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
116
157
|
},
|
|
117
158
|
};
|
|
118
159
|
}
|
|
119
160
|
|
|
161
|
+
/** The wire name of the team kind, as an `ApiResourceRef.kind` carries it. */
|
|
162
|
+
const TEAM_KIND_NAME = kindEnumName(ApiResourceKind.team);
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* The team arm of ValidateGrantableRole (the module header): the role
|
|
166
|
+
* against the kind's team list narrowed by the scope, then the qualifier.
|
|
167
|
+
*/
|
|
168
|
+
function requireTeamGrant(
|
|
169
|
+
role: string,
|
|
170
|
+
qualifier: string,
|
|
171
|
+
kind: ApiResourceKind,
|
|
172
|
+
kindName: string,
|
|
173
|
+
scoped: ReadonlySet<IamRole>,
|
|
174
|
+
): void {
|
|
175
|
+
const teamGrantable = teamGrantableRolesFor(kind).filter((r) =>
|
|
176
|
+
scoped.has(r),
|
|
177
|
+
);
|
|
178
|
+
if (teamGrantable.length === 0) {
|
|
179
|
+
throw new ConnectError(
|
|
180
|
+
teamNotGrantableMessage(kindName),
|
|
181
|
+
Code.InvalidArgument,
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
if (!teamGrantable.some((r) => IamRole[r] === role)) {
|
|
185
|
+
throw new ConnectError(
|
|
186
|
+
teamRoleNotGrantableMessage(
|
|
187
|
+
role,
|
|
188
|
+
kindName,
|
|
189
|
+
teamGrantable.map((r) => IamRole[r]),
|
|
190
|
+
),
|
|
191
|
+
Code.InvalidArgument,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
if (qualifier !== TEAM_MEMBERS_RELATION) {
|
|
195
|
+
throw new ConnectError(
|
|
196
|
+
teamQualifierMessage(qualifier),
|
|
197
|
+
Code.InvalidArgument,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
120
202
|
/** Grant the spec as the chain's caller; the row (fresh or held) is the result. */
|
|
121
203
|
export function newGrantStep(
|
|
122
204
|
grantPath: IamPolicyGrantPath,
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
* contextual checkMyPermission refuse UNIMPLEMENTED with the edition
|
|
16
16
|
* reason (the domain suite pins the refusal; this file pins the
|
|
17
17
|
* registry);
|
|
18
|
+
* - `drivers.principalDisplay` — how an access list names a grantee
|
|
19
|
+
* that is not a person (a team); single instance; absent = the id
|
|
20
|
+
* fallback shape;
|
|
18
21
|
* - `ResourceAuthorizationLifecycle` gains the OPTIONAL `onPolicyGranted`
|
|
19
22
|
* and `onPolicyRevoked` (Q7 i): a unit's lifecycle that carries them
|
|
20
23
|
* resolves through the existing single-instance point with both hooks
|
|
@@ -26,6 +29,7 @@ import { describe, expect, it } from "vitest";
|
|
|
26
29
|
import type { IamPolicyStore } from "../../domain/iampolicy/store.js";
|
|
27
30
|
import type { AuthorizationQueryEngine } from "../authorization-queries.js";
|
|
28
31
|
import type { PolicyGrantScope } from "../policy-grant-scope.js";
|
|
32
|
+
import type { PrincipalDisplay } from "../principal-display.js";
|
|
29
33
|
import { resolveExtensions } from "../registry.js";
|
|
30
34
|
import type { ResourceAuthorizationLifecycle } from "../resource-authorization.js";
|
|
31
35
|
|
|
@@ -125,6 +129,32 @@ describe("the authorizationQueries capability point", () => {
|
|
|
125
129
|
});
|
|
126
130
|
});
|
|
127
131
|
|
|
132
|
+
describe("the principalDisplay driver point", () => {
|
|
133
|
+
const display: PrincipalDisplay = { resolve: unimplemented };
|
|
134
|
+
|
|
135
|
+
it("is undefined with no extensions — a non-person grantee renders by its id", () => {
|
|
136
|
+
expect(resolveExtensions([]).drivers.principalDisplay).toBeUndefined();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("carries the one registered instance through", () => {
|
|
140
|
+
const resolved = resolveExtensions([
|
|
141
|
+
{ name: "cloud-iam", drivers: { principalDisplay: display } },
|
|
142
|
+
]);
|
|
143
|
+
expect(resolved.drivers.principalDisplay).toBe(display);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("throws on a second registration, naming both units", () => {
|
|
147
|
+
expect(() =>
|
|
148
|
+
resolveExtensions([
|
|
149
|
+
{ name: "display-a", drivers: { principalDisplay: display } },
|
|
150
|
+
{ name: "display-b", drivers: { principalDisplay: display } },
|
|
151
|
+
]),
|
|
152
|
+
).toThrowError(
|
|
153
|
+
/extension 'display-b' registers a PrincipalDisplay, but 'display-a' already did/,
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
|
|
128
158
|
describe("the widened ResourceAuthorizationLifecycle contract", () => {
|
|
129
159
|
const resourceHooks = {
|
|
130
160
|
onResourceCreated: unimplemented,
|
|
@@ -480,6 +480,9 @@ describe("resolveExtensions — loud-fail throws (DD-006 §2b)", () => {
|
|
|
480
480
|
"agent-execution-create:pre-side-effect-gate",
|
|
481
481
|
"agent-execution-recover:pre-side-effect-gate",
|
|
482
482
|
"agent-execution-submit-approval:gate",
|
|
483
|
+
// The eighth: the IamPolicy create chain before the write, where an
|
|
484
|
+
// edition that serves teams refuses a team it cannot admit.
|
|
485
|
+
"iam-policy-create:pre-side-effect-gate",
|
|
483
486
|
// 20260911.11 Q-IA-9: the seventh ratified slot — after the account
|
|
484
487
|
// persists inside provisionMyAccount, before the reply (the cloud's
|
|
485
488
|
// personal-organization ensure and backfill ride it).
|
|
@@ -31,6 +31,9 @@
|
|
|
31
31
|
* port, which kinds an edition grants on, and the tuple-half query
|
|
32
32
|
* engine only an authorization backend can answer) — landed with
|
|
33
33
|
* 20260913.01, gate ruling Q-OR-10
|
|
34
|
+
* - principal display (how an access list names a grantee that is not
|
|
35
|
+
* a person: a team, in the editions that serve teams) — landed with
|
|
36
|
+
* the Team kind
|
|
34
37
|
* - outbound egress (which addresses the control plane may dial when
|
|
35
38
|
* it reaches a URL a user supplied: the MCP endpoint it probes at save
|
|
36
39
|
* time and the login server it reaches on Sign in) — landed with the
|
|
@@ -70,6 +73,7 @@ import type { OutboundEgressPolicy } from "./outbound-egress.js";
|
|
|
70
73
|
import type { ListReadScope } from "./list-read-scope.js";
|
|
71
74
|
import type { OrganizationDirectory } from "./organization-directory.js";
|
|
72
75
|
import type { PolicyGrantScope } from "./policy-grant-scope.js";
|
|
76
|
+
import type { PrincipalDisplay } from "./principal-display.js";
|
|
73
77
|
import type { ResourceAuthorizationLifecycle } from "./resource-authorization.js";
|
|
74
78
|
import type { ScheduleFireCallerMint } from "./schedule-fire-caller.js";
|
|
75
79
|
|
|
@@ -233,6 +237,14 @@ export interface ExtensionDrivers {
|
|
|
233
237
|
* with the edition reason — the identityFederation absent shape.
|
|
234
238
|
*/
|
|
235
239
|
readonly authorizationQueries?: AuthorizationQueryEngine;
|
|
240
|
+
/**
|
|
241
|
+
* The principal display (single-instance point): how an access list
|
|
242
|
+
* names a grantee that is not a person — a team, in the editions that
|
|
243
|
+
* serve teams (extensions/principal-display.ts carries the contract).
|
|
244
|
+
* When absent, such a grantee renders by its id, the fallback shape
|
|
245
|
+
* every access list has always had.
|
|
246
|
+
*/
|
|
247
|
+
readonly principalDisplay?: PrincipalDisplay;
|
|
236
248
|
/**
|
|
237
249
|
* The license-status provider (single-instance point): what license
|
|
238
250
|
* this server holds, answered by PlatformQueryController.getLicenseStatus
|
|
@@ -34,6 +34,15 @@
|
|
|
34
34
|
* Non-transactional in the `org-create:post-persist` sense: a gate
|
|
35
35
|
* failure fails the request, the row survives, the next call heals.
|
|
36
36
|
*
|
|
37
|
+
* The eighth, `iam-policy-create:pre-side-effect-gate`: the IamPolicy
|
|
38
|
+
* `create` chain (the user grant lane), after ValidateGrantableRole and
|
|
39
|
+
* before Grant, so nothing is written when a gate refuses. It exists for
|
|
40
|
+
* the checks a grant needs that take a read, which the synchronous grant
|
|
41
|
+
* scope may not make: the Enterprise and Cloud editions refuse a team
|
|
42
|
+
* that does not exist, a team of another organization than the granted
|
|
43
|
+
* resource, and a team member who is not one of the organization's
|
|
44
|
+
* viewers. Never on `bootstrapPolicy`, the platform's structural lane.
|
|
45
|
+
*
|
|
37
46
|
* Two enforcement layers, deliberately redundant, both derived from the
|
|
38
47
|
* ONE literal tuple below (lockstep by construction):
|
|
39
48
|
* - GateSlotName (compile time): the union of declared slot literals.
|
|
@@ -71,6 +80,7 @@ export const GATE_SLOT_NAMES = [
|
|
|
71
80
|
"org-create:post-persist",
|
|
72
81
|
"sandbox-acquisition:gate",
|
|
73
82
|
"identity-account-provision:post-persist",
|
|
83
|
+
"iam-policy-create:pre-side-effect-gate",
|
|
74
84
|
] as const;
|
|
75
85
|
|
|
76
86
|
/** The declared slot-name union — a registration outside it fails tsc. */
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The principal-display driver point: how an access list names a grantee
|
|
3
|
+
* that is not a person. Single instance, registered as
|
|
4
|
+
* `drivers.principalDisplay`.
|
|
5
|
+
*
|
|
6
|
+
* An access list ("who has access to this agent, with which roles") names
|
|
7
|
+
* every grantee. People are resolved by the core from the identity-account
|
|
8
|
+
* port (domain/iampolicy/display-resolver.ts), the same in every edition.
|
|
9
|
+
* A grantee of another kind — a team, in the Enterprise and Cloud editions
|
|
10
|
+
* — lives in a store only that edition holds, so its name comes from this
|
|
11
|
+
* point. Absent (open source, which grants to no team), such a grantee
|
|
12
|
+
* renders in the fallback shape the access list has always used: its kind,
|
|
13
|
+
* with its id standing in as its name.
|
|
14
|
+
*
|
|
15
|
+
* The contract:
|
|
16
|
+
* - It answers for the ids it knows and omits the rest; an omitted id
|
|
17
|
+
* takes the fallback shape, never an error (a team deleted between
|
|
18
|
+
* the grant read and this call is an omission, not a fault).
|
|
19
|
+
* - It is never asked about the identity account; the core owns people.
|
|
20
|
+
* - A fault it cannot answer through THROWS (the ListReadScope rule:
|
|
21
|
+
* an empty answer is a real answer, never an outage in disguise), and
|
|
22
|
+
* the list RPC fails with it.
|
|
23
|
+
*/
|
|
24
|
+
import type { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
25
|
+
import type { ApiResourceRefView } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/io_pb";
|
|
26
|
+
|
|
27
|
+
/** The principal-display contract (single-instance point, ExtensionDrivers.principalDisplay). */
|
|
28
|
+
export interface PrincipalDisplay {
|
|
29
|
+
/**
|
|
30
|
+
* Display views for the principals of `kind` with these ids, keyed by
|
|
31
|
+
* id; ids it does not know are absent from the map.
|
|
32
|
+
*/
|
|
33
|
+
resolve(
|
|
34
|
+
kind: ApiResourceKind,
|
|
35
|
+
ids: ReadonlyArray<string>,
|
|
36
|
+
): Promise<ReadonlyMap<string, ApiResourceRefView>>;
|
|
37
|
+
}
|
|
@@ -60,6 +60,7 @@ import type { LicenseStatusProvider } from "./license-status.js";
|
|
|
60
60
|
import type { OutboundEgressPolicy } from "./outbound-egress.js";
|
|
61
61
|
import type { ListReadScope } from "./list-read-scope.js";
|
|
62
62
|
import type { PolicyGrantScope } from "./policy-grant-scope.js";
|
|
63
|
+
import type { PrincipalDisplay } from "./principal-display.js";
|
|
63
64
|
import type { ModelCatalogProvider } from "../domain/workflow/registry/model-catalog-provider.js";
|
|
64
65
|
import type { VisitorErrorPolicy } from "../pipeline/interceptors/error-boundary.js";
|
|
65
66
|
import type { PipelineStep } from "../pipeline/pipeline.js";
|
|
@@ -288,6 +289,11 @@ export interface ResolvedExtensionDrivers {
|
|
|
288
289
|
* tuple-half RPC arms refuse UNIMPLEMENTED with the edition reason.
|
|
289
290
|
*/
|
|
290
291
|
readonly authorizationQueries: AuthorizationQueryEngine | undefined;
|
|
292
|
+
/**
|
|
293
|
+
* The principal display — undefined = an access list names a grantee
|
|
294
|
+
* that is not a person by its id (the fallback shape).
|
|
295
|
+
*/
|
|
296
|
+
readonly principalDisplay: PrincipalDisplay | undefined;
|
|
291
297
|
/**
|
|
292
298
|
* The license-status provider — undefined = compose.ts installs the
|
|
293
299
|
* built-in `absent` answer, so every edition serves getLicenseStatus.
|
|
@@ -367,6 +373,8 @@ export function resolveExtensions(
|
|
|
367
373
|
let policyGrantScopeDeclaredBy: string | undefined;
|
|
368
374
|
let authorizationQueries: AuthorizationQueryEngine | undefined;
|
|
369
375
|
let authorizationQueriesDeclaredBy: string | undefined;
|
|
376
|
+
let principalDisplay: PrincipalDisplay | undefined;
|
|
377
|
+
let principalDisplayDeclaredBy: string | undefined;
|
|
370
378
|
let licenseStatus: LicenseStatusProvider | undefined;
|
|
371
379
|
let licenseStatusDeclaredBy: string | undefined;
|
|
372
380
|
let outboundEgress: OutboundEgressPolicy | undefined;
|
|
@@ -568,6 +576,16 @@ export function resolveExtensions(
|
|
|
568
576
|
authorizationQueriesDeclaredBy = unit.name;
|
|
569
577
|
}
|
|
570
578
|
|
|
579
|
+
if (unit.drivers?.principalDisplay !== undefined) {
|
|
580
|
+
if (principalDisplayDeclaredBy !== undefined) {
|
|
581
|
+
throw new Error(
|
|
582
|
+
`extension '${unit.name}' registers a PrincipalDisplay, but '${principalDisplayDeclaredBy}' already did — exactly one may be composed`,
|
|
583
|
+
);
|
|
584
|
+
}
|
|
585
|
+
principalDisplay = unit.drivers.principalDisplay;
|
|
586
|
+
principalDisplayDeclaredBy = unit.name;
|
|
587
|
+
}
|
|
588
|
+
|
|
571
589
|
if (unit.drivers?.licenseStatus !== undefined) {
|
|
572
590
|
if (licenseStatusDeclaredBy !== undefined) {
|
|
573
591
|
throw new Error(
|
|
@@ -748,6 +766,7 @@ export function resolveExtensions(
|
|
|
748
766
|
iamPolicyStore,
|
|
749
767
|
policyGrantScope,
|
|
750
768
|
authorizationQueries,
|
|
769
|
+
principalDisplay,
|
|
751
770
|
licenseStatus,
|
|
752
771
|
outboundEgress,
|
|
753
772
|
platformClientStore,
|
package/src/index.ts
CHANGED
|
@@ -164,6 +164,9 @@ export type {
|
|
|
164
164
|
export { iamPolicyStoreContract } from "./domain/iampolicy/store-contract.js";
|
|
165
165
|
export type { PolicyGrantScope } from "./extensions/policy-grant-scope.js";
|
|
166
166
|
export type { AuthorizationQueryEngine } from "./extensions/authorization-queries.js";
|
|
167
|
+
// How an access list names a grantee that is not a person
|
|
168
|
+
// (drivers.principalDisplay): a team, in the editions that serve teams.
|
|
169
|
+
export type { PrincipalDisplay } from "./extensions/principal-display.js";
|
|
167
170
|
export type { IdentityFederation } from "./extensions/identity-federation.js";
|
|
168
171
|
export type { AccountsBySubject } from "./domain/identityaccount/resolve.js";
|
|
169
172
|
export { identityIdForSubject } from "./domain/identityaccount/resolve.js";
|
|
@@ -288,8 +291,14 @@ export { ScheduleFireCallerRefusedError } from "./extensions/schedule-fire-calle
|
|
|
288
291
|
// through the REAL boundary mechanism, never a re-derivation.
|
|
289
292
|
export type { VisitorErrorPolicy } from "./pipeline/interceptors/error-boundary.js";
|
|
290
293
|
export { createErrorBoundaryInterceptor } from "./pipeline/interceptors/error-boundary.js";
|
|
294
|
+
// The tuple lifecycle's resolution, for a kind a composition serves outside
|
|
295
|
+
// the generic chains (a cloud-served create): `resolveResourceCreatedEvent`
|
|
296
|
+
// derives the creation event from the kind's `kind_meta` exactly as the
|
|
297
|
+
// shared CreateAuthorizationTuples step does, so the driver sees one shape
|
|
298
|
+
// whichever chain created the row.
|
|
291
299
|
export {
|
|
292
300
|
diffVisibilityShapes,
|
|
301
|
+
resolveResourceCreatedEvent,
|
|
293
302
|
visibilityShapesFor,
|
|
294
303
|
} from "./pipeline/steps/authorization-tuples.js";
|
|
295
304
|
|
|
@@ -376,6 +385,23 @@ export { newValidateProtoStep } from "./pipeline/steps/validation.js";
|
|
|
376
385
|
// same name over its own repo.
|
|
377
386
|
export { newBuildNewStateStep } from "./pipeline/steps/defaults.js";
|
|
378
387
|
export { newResolveSlugStep } from "./pipeline/steps/slug.js";
|
|
388
|
+
// The update and reference-read steps for the same kinds, on the same
|
|
389
|
+
// terms. An update runs BuildUpdateState after the composition's own
|
|
390
|
+
// loader has stashed the stored row under EXISTING_RESOURCE_KEY, so which
|
|
391
|
+
// fields a client may change, how status carries over and how the audit
|
|
392
|
+
// slots are stamped stay the OSS step's. A read by reference runs
|
|
393
|
+
// AuthorizeResolvedTarget with `loadedTargetAsMethod(get)` after the
|
|
394
|
+
// loader has stashed the row under TARGET_RESOURCE_KEY, so the kind's
|
|
395
|
+
// `get` annotation owns the permission and the refusal copy for both
|
|
396
|
+
// reads. The loaders stay the composition's for CheckDuplicate's reason:
|
|
397
|
+
// they read its own table.
|
|
398
|
+
export { newBuildUpdateStateStep } from "./pipeline/steps/build-update-state.js";
|
|
399
|
+
export { EXISTING_RESOURCE_KEY } from "./pipeline/steps/load-existing.js";
|
|
400
|
+
export {
|
|
401
|
+
loadedTargetAsMethod,
|
|
402
|
+
newAuthorizeResolvedTargetStep,
|
|
403
|
+
} from "./pipeline/steps/authorize-resolved-target.js";
|
|
404
|
+
export { TARGET_RESOURCE_KEY } from "./pipeline/steps/load-target.js";
|
|
379
405
|
|
|
380
406
|
// The driver interfaces and the store-fault classes the ratified mapping
|
|
381
407
|
// keys on (typed not-found → NotFound; anything else rethrows as an
|
|
@@ -200,6 +200,27 @@ export function grantableRolesFor(
|
|
|
200
200
|
return getOption(valueDesc, kind_meta).authorization?.grantableRoles ?? [];
|
|
201
201
|
}
|
|
202
202
|
|
|
203
|
+
/**
|
|
204
|
+
* The roles a TEAM may be granted on a resource of `kind` according to the
|
|
205
|
+
* contract — `kind_meta.authorization.team_grantable_roles`, a subset of
|
|
206
|
+
* `grantableRolesFor(kind)` (the subset is pinned by a test, never assumed
|
|
207
|
+
* here). Empty for every kind a team cannot be granted access to, and for
|
|
208
|
+
* `organization`, so an edition that grants only on organizations grants
|
|
209
|
+
* nothing to a team. Total, never a throw, for the same reason as
|
|
210
|
+
* `grantableRolesFor`.
|
|
211
|
+
*/
|
|
212
|
+
export function teamGrantableRolesFor(
|
|
213
|
+
kind: ApiResourceKind,
|
|
214
|
+
): ReadonlyArray<IamRole> {
|
|
215
|
+
const valueDesc = kindValueDescriptor(kind);
|
|
216
|
+
if (valueDesc === undefined || !hasOption(valueDesc, kind_meta)) {
|
|
217
|
+
return [];
|
|
218
|
+
}
|
|
219
|
+
return (
|
|
220
|
+
getOption(valueDesc, kind_meta).authorization?.teamGrantableRoles ?? []
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
203
224
|
/**
|
|
204
225
|
* Whether an edition serves a tier: the tier names the MINIMUM edition,
|
|
205
226
|
* the editions are ordered oss < enterprise < cloud (each composes the
|