@stigmer/server 3.27.1 → 3.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/authorization/authorizer.d.ts +7 -5
- package/dist/authorization/authorizer.d.ts.map +1 -1
- package/dist/authorization/authorizer.js +7 -5
- package/dist/authorization/authorizer.js.map +1 -1
- package/dist/authorization/derived-tuples.d.ts +3 -1
- package/dist/authorization/derived-tuples.d.ts.map +1 -1
- package/dist/authorization/derived-tuples.js +52 -2
- package/dist/authorization/derived-tuples.js.map +1 -1
- package/dist/authorization/evaluator.d.ts +9 -10
- package/dist/authorization/evaluator.d.ts.map +1 -1
- package/dist/authorization/evaluator.js.map +1 -1
- package/dist/authorization/list-read-scope.d.ts.map +1 -1
- package/dist/authorization/list-read-scope.js +29 -18
- package/dist/authorization/list-read-scope.js.map +1 -1
- package/dist/authorization/model/bindings.d.ts +41 -0
- package/dist/authorization/model/bindings.d.ts.map +1 -0
- package/dist/authorization/model/bindings.js +89 -0
- package/dist/authorization/model/bindings.js.map +1 -0
- package/dist/authorization/model/data/authorization-model.json +3114 -0
- package/dist/authorization/model/default-of.d.ts +2 -2
- package/dist/authorization/model/execution-viewer.d.ts +3 -0
- package/dist/authorization/model/execution-viewer.d.ts.map +1 -0
- package/dist/authorization/model/execution-viewer.js +50 -0
- package/dist/authorization/model/execution-viewer.js.map +1 -0
- package/dist/authorization/model/index.d.ts +33 -21
- package/dist/authorization/model/index.d.ts.map +1 -1
- package/dist/authorization/model/index.js +74 -52
- package/dist/authorization/model/index.js.map +1 -1
- package/dist/authorization/model/openfga-json.d.ts +18 -0
- package/dist/authorization/model/openfga-json.d.ts.map +1 -0
- package/dist/authorization/model/openfga-json.js +266 -0
- package/dist/authorization/model/openfga-json.js.map +1 -0
- package/dist/authorization/model/rewrite.d.ts +39 -67
- package/dist/authorization/model/rewrite.d.ts.map +1 -1
- package/dist/authorization/model/rewrite.js +1 -117
- package/dist/authorization/model/rewrite.js.map +1 -1
- package/dist/authorization/strict-json.d.ts +20 -0
- package/dist/authorization/strict-json.d.ts.map +1 -0
- package/dist/authorization/strict-json.js +45 -0
- package/dist/authorization/strict-json.js.map +1 -0
- package/dist/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +9 -7
- package/dist/boot/compose.js.map +1 -1
- package/dist/boot/list-indexes.d.ts.map +1 -1
- package/dist/boot/list-indexes.js +6 -0
- package/dist/boot/list-indexes.js.map +1 -1
- package/dist/domain/iampolicy/list-index.d.ts +2 -0
- package/dist/domain/iampolicy/list-index.d.ts.map +1 -0
- package/dist/domain/iampolicy/list-index.js +32 -0
- package/dist/domain/iampolicy/list-index.js.map +1 -0
- package/dist/domain/iampolicy/resource-store.d.ts.map +1 -1
- package/dist/domain/iampolicy/resource-store.js +24 -11
- package/dist/domain/iampolicy/resource-store.js.map +1 -1
- package/dist/domain/workflowexecution/list-index.d.ts +1 -1
- package/dist/extensions/list-read-scope.d.ts +20 -3
- package/dist/extensions/list-read-scope.d.ts.map +1 -1
- package/dist/extensions/list-read-scope.js +22 -2
- package/dist/extensions/list-read-scope.js.map +1 -1
- package/dist/index.d.ts +8 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -5
- package/dist/index.js.map +1 -1
- package/package.json +7 -6
- package/src/authorization/README.md +48 -39
- package/src/authorization/__tests__/authorizer.test.ts +5 -14
- package/src/authorization/__tests__/derived-tuples.test.ts +192 -17
- package/src/authorization/__tests__/evaluator.test.ts +53 -160
- package/src/authorization/__tests__/facts.test.ts +5 -11
- package/src/authorization/__tests__/list-read-scope.measure.test.ts +424 -62
- package/src/authorization/__tests__/list-read-scope.test.ts +15 -14
- package/src/authorization/__tests__/organization-directory.test.ts +2 -2
- package/src/authorization/__tests__/store-test-kit.ts +246 -0
- package/src/authorization/__tests__/store-tests.test.ts +91 -223
- package/src/authorization/__tests__/support.ts +30 -2
- package/src/authorization/__tests__/throwaway-model.ts +75 -0
- package/src/authorization/__tests__/wire-permissions.test.ts +141 -107
- package/src/authorization/authorizer.ts +7 -5
- package/src/authorization/derived-tuples.ts +105 -16
- package/src/authorization/evaluator.ts +8 -10
- package/src/authorization/list-read-scope.ts +37 -22
- package/src/authorization/model/__tests__/openfga-json.test.ts +353 -0
- package/src/authorization/model/__tests__/registry.test.ts +102 -276
- package/src/authorization/model/bindings.ts +131 -0
- package/src/authorization/model/data/authorization-model.json +3114 -0
- package/src/authorization/model/default-of.ts +2 -2
- package/src/authorization/model/execution-viewer.ts +56 -0
- package/src/authorization/model/index.ts +81 -72
- package/src/authorization/model/openfga-json.ts +310 -0
- package/src/authorization/model/rewrite.ts +39 -188
- package/src/authorization/strict-json.ts +52 -0
- package/src/boot/__tests__/list-indexes.test.ts +4 -0
- package/src/boot/compose.ts +9 -7
- package/src/boot/list-indexes.ts +6 -0
- package/src/domain/iampolicy/__tests__/resource-store.test.ts +65 -5
- package/src/domain/iampolicy/list-index.ts +33 -0
- package/src/domain/iampolicy/resource-store.ts +24 -11
- package/src/extensions/__tests__/built-in-authorization-composed.test.ts +3 -6
- package/src/extensions/list-read-scope.ts +32 -4
- package/src/index.ts +20 -44
- package/dist/authorization/model/agent.d.ts +0 -2
- package/dist/authorization/model/agent.d.ts.map +0 -1
- package/dist/authorization/model/agent.js +0 -43
- package/dist/authorization/model/agent.js.map +0 -1
- package/dist/authorization/model/agent_channel.d.ts +0 -2
- package/dist/authorization/model/agent_channel.d.ts.map +0 -1
- package/dist/authorization/model/agent_channel.js +0 -39
- package/dist/authorization/model/agent_channel.js.map +0 -1
- package/dist/authorization/model/agent_execution.d.ts +0 -2
- package/dist/authorization/model/agent_execution.d.ts.map +0 -1
- package/dist/authorization/model/agent_execution.js +0 -25
- package/dist/authorization/model/agent_execution.js.map +0 -1
- package/dist/authorization/model/agent_instance.d.ts +0 -2
- package/dist/authorization/model/agent_instance.d.ts.map +0 -1
- package/dist/authorization/model/agent_instance.js +0 -48
- package/dist/authorization/model/agent_instance.js.map +0 -1
- package/dist/authorization/model/agent_share.d.ts +0 -2
- package/dist/authorization/model/agent_share.d.ts.map +0 -1
- package/dist/authorization/model/agent_share.js +0 -29
- package/dist/authorization/model/agent_share.js.map +0 -1
- package/dist/authorization/model/api_key.d.ts +0 -2
- package/dist/authorization/model/api_key.d.ts.map +0 -1
- package/dist/authorization/model/api_key.js +0 -27
- package/dist/authorization/model/api_key.js.map +0 -1
- package/dist/authorization/model/artifact.d.ts +0 -2
- package/dist/authorization/model/artifact.d.ts.map +0 -1
- package/dist/authorization/model/artifact.js +0 -27
- package/dist/authorization/model/artifact.js.map +0 -1
- package/dist/authorization/model/channel_app.d.ts +0 -2
- package/dist/authorization/model/channel_app.d.ts.map +0 -1
- package/dist/authorization/model/channel_app.js +0 -28
- package/dist/authorization/model/channel_app.js.map +0 -1
- package/dist/authorization/model/environment.d.ts +0 -2
- package/dist/authorization/model/environment.d.ts.map +0 -1
- package/dist/authorization/model/environment.js +0 -33
- package/dist/authorization/model/environment.js.map +0 -1
- package/dist/authorization/model/execution_context.d.ts +0 -2
- package/dist/authorization/model/execution_context.d.ts.map +0 -1
- package/dist/authorization/model/execution_context.js +0 -19
- package/dist/authorization/model/execution_context.js.map +0 -1
- package/dist/authorization/model/iam_policy.d.ts +0 -2
- package/dist/authorization/model/iam_policy.d.ts.map +0 -1
- package/dist/authorization/model/iam_policy.js +0 -31
- package/dist/authorization/model/iam_policy.js.map +0 -1
- package/dist/authorization/model/identity_account.d.ts +0 -2
- package/dist/authorization/model/identity_account.d.ts.map +0 -1
- package/dist/authorization/model/identity_account.js +0 -25
- package/dist/authorization/model/identity_account.js.map +0 -1
- package/dist/authorization/model/mcp_server.d.ts +0 -2
- package/dist/authorization/model/mcp_server.d.ts.map +0 -1
- package/dist/authorization/model/mcp_server.js +0 -38
- package/dist/authorization/model/mcp_server.js.map +0 -1
- package/dist/authorization/model/memory.d.ts +0 -2
- package/dist/authorization/model/memory.d.ts.map +0 -1
- package/dist/authorization/model/memory.js +0 -26
- package/dist/authorization/model/memory.js.map +0 -1
- package/dist/authorization/model/oauth_app.d.ts +0 -2
- package/dist/authorization/model/oauth_app.d.ts.map +0 -1
- package/dist/authorization/model/oauth_app.js +0 -31
- package/dist/authorization/model/oauth_app.js.map +0 -1
- package/dist/authorization/model/organization.d.ts +0 -2
- package/dist/authorization/model/organization.d.ts.map +0 -1
- package/dist/authorization/model/organization.js +0 -58
- package/dist/authorization/model/organization.js.map +0 -1
- package/dist/authorization/model/platform_client.d.ts +0 -2
- package/dist/authorization/model/platform_client.d.ts.map +0 -1
- package/dist/authorization/model/platform_client.js +0 -31
- package/dist/authorization/model/platform_client.js.map +0 -1
- package/dist/authorization/model/plugin.d.ts +0 -2
- package/dist/authorization/model/plugin.d.ts.map +0 -1
- package/dist/authorization/model/plugin.js +0 -45
- package/dist/authorization/model/plugin.js.map +0 -1
- package/dist/authorization/model/schedule.d.ts +0 -2
- package/dist/authorization/model/schedule.d.ts.map +0 -1
- package/dist/authorization/model/schedule.js +0 -34
- package/dist/authorization/model/schedule.js.map +0 -1
- package/dist/authorization/model/session.d.ts +0 -2
- package/dist/authorization/model/session.d.ts.map +0 -1
- package/dist/authorization/model/session.js +0 -38
- package/dist/authorization/model/session.js.map +0 -1
- package/dist/authorization/model/skill.d.ts +0 -2
- package/dist/authorization/model/skill.d.ts.map +0 -1
- package/dist/authorization/model/skill.js +0 -39
- package/dist/authorization/model/skill.js.map +0 -1
- package/dist/authorization/model/workflow.d.ts +0 -2
- package/dist/authorization/model/workflow.d.ts.map +0 -1
- package/dist/authorization/model/workflow.js +0 -39
- package/dist/authorization/model/workflow.js.map +0 -1
- package/dist/authorization/model/workflow_execution.d.ts +0 -2
- package/dist/authorization/model/workflow_execution.d.ts.map +0 -1
- package/dist/authorization/model/workflow_execution.js +0 -33
- package/dist/authorization/model/workflow_execution.js.map +0 -1
- package/dist/authorization/model/workflow_instance.d.ts +0 -2
- package/dist/authorization/model/workflow_instance.d.ts.map +0 -1
- package/dist/authorization/model/workflow_instance.js +0 -96
- package/dist/authorization/model/workflow_instance.js.map +0 -1
- package/dist/authorization/store-test-kit.d.ts +0 -62
- package/dist/authorization/store-test-kit.d.ts.map +0 -1
- package/dist/authorization/store-test-kit.js +0 -265
- package/dist/authorization/store-test-kit.js.map +0 -1
- package/src/authorization/__tests__/fixtures/fga/agent-channel-owner.fga.yaml +0 -112
- package/src/authorization/__tests__/fixtures/fga/agent-instance-creation.fga.yaml +0 -177
- package/src/authorization/__tests__/fixtures/fga/agent-share-owner.fga.yaml +0 -109
- package/src/authorization/__tests__/fixtures/fga/artifact-org-and-owner.fga.yaml +0 -128
- package/src/authorization/__tests__/fixtures/fga/blueprint-private-visibility.fga.yaml +0 -318
- package/src/authorization/__tests__/fixtures/fga/channel-app-owner.fga.yaml +0 -125
- package/src/authorization/__tests__/fixtures/fga/channel-participation.fga.yaml +0 -201
- package/src/authorization/__tests__/fixtures/fga/channel-session-visibility.fga.yaml +0 -187
- package/src/authorization/__tests__/fixtures/fga/default-instance-inheritance.fga.yaml +0 -214
- package/src/authorization/__tests__/fixtures/fga/mcp-server-authoring.fga.yaml +0 -129
- package/src/authorization/__tests__/fixtures/fga/memory-subject-only.fga.yaml +0 -72
- package/src/authorization/__tests__/fixtures/fga/org-admin-owner-inheritance.fga.yaml +0 -308
- package/src/authorization/__tests__/fixtures/fga/org-shared-environment.fga.yaml +0 -144
- package/src/authorization/__tests__/fixtures/fga/platform-visibility.fga.yaml +0 -199
- package/src/authorization/__tests__/fixtures/fga/provider-standing-viewer.fga.yaml +0 -40
- package/src/authorization/__tests__/fixtures/fga/reserved-label-writer.fga.yaml +0 -41
- package/src/authorization/__tests__/fixtures/fga/schedule-owner.fga.yaml +0 -113
- package/src/authorization/__tests__/fixtures/fga/schedule-session-visibility.fga.yaml +0 -214
- package/src/authorization/__tests__/fixtures/fga/session-personal-resource.fga.yaml +0 -117
- package/src/authorization/__tests__/fixtures/fga/workflow-execution-sharing.fga.yaml +0 -190
- package/src/authorization/model/agent.ts +0 -64
- package/src/authorization/model/agent_channel.ts +0 -57
- package/src/authorization/model/agent_execution.ts +0 -33
- package/src/authorization/model/agent_instance.ts +0 -66
- package/src/authorization/model/agent_share.ts +0 -40
- package/src/authorization/model/api_key.ts +0 -28
- package/src/authorization/model/artifact.ts +0 -38
- package/src/authorization/model/channel_app.ts +0 -39
- package/src/authorization/model/environment.ts +0 -48
- package/src/authorization/model/execution_context.ts +0 -20
- package/src/authorization/model/iam_policy.ts +0 -42
- package/src/authorization/model/identity_account.ts +0 -26
- package/src/authorization/model/mcp_server.ts +0 -59
- package/src/authorization/model/memory.ts +0 -27
- package/src/authorization/model/oauth_app.ts +0 -43
- package/src/authorization/model/organization.ts +0 -59
- package/src/authorization/model/platform_client.ts +0 -43
- package/src/authorization/model/plugin.ts +0 -66
- package/src/authorization/model/schedule.ts +0 -49
- package/src/authorization/model/session.ts +0 -50
- package/src/authorization/model/skill.ts +0 -60
- package/src/authorization/model/workflow.ts +0 -60
- package/src/authorization/model/workflow_execution.ts +0 -46
- package/src/authorization/model/workflow_instance.ts +0 -125
- package/src/authorization/store-test-kit.ts +0 -395
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* - owner SELF: <row>#owner@identity_account:<row id>
|
|
13
13
|
* - creator: <row>#creator@identity_account:<creator stamp> (kinds flagged requires_creator_tuple)
|
|
14
14
|
* - org-viewer: <row>#viewer@organization:<org>#viewer (visibility_org; cloud#257's shape)
|
|
15
|
-
* - platform-viewer: none — it fans out over identity providers,
|
|
15
|
+
* - platform-viewer: none — it fans out over identity providers, which this edition does not serve: the model defines the type, and no tuple of it is derived here
|
|
16
16
|
*
|
|
17
17
|
* Two facts are this edition's own and are stated here, nowhere else:
|
|
18
18
|
* - The ORGANIZATION's owner is a ROW, not a derivation. 2b's role
|
|
@@ -26,18 +26,39 @@
|
|
|
26
26
|
* rules' one predicate). The person comparison itself is the
|
|
27
27
|
* evaluator's, over the aliases `Person` carries.
|
|
28
28
|
*
|
|
29
|
-
* The SOURCE joins two records per (object, relation): the
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* read
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
29
|
+
* The SOURCE joins two records per (object, relation): the IamPolicy rows
|
|
30
|
+
* a check can meet on the object, and the tuples derived from the
|
|
31
|
+
* object's row (loaded once per object through the loader, which a
|
|
32
|
+
* declaration's `derived` rule also reads related rows through). The rows
|
|
33
|
+
* are read ONCE per source through `findByPrincipal`, keyed by the
|
|
34
|
+
* principals a check can reach the person through:
|
|
35
|
+
*
|
|
36
|
+
* - the person's account — every role and every grant made to them;
|
|
37
|
+
* - every team the person holds `member` on — a grant made to
|
|
38
|
+
* `team:<id>#member` names the team, never the person, so it is read
|
|
39
|
+
* by the team's id. The set is complete without recursion: a team's
|
|
40
|
+
* membership is a direct grant only and a team never contains a team,
|
|
41
|
+
* so every team that can admit the person appears among the person's
|
|
42
|
+
* own rows. The rows grant nothing by being read: the evaluator still
|
|
43
|
+
* asks whether the person is the team's member, which the model bounds
|
|
44
|
+
* by the organization (`member: [identity_account] and viewer from
|
|
45
|
+
* organization`), so a person who left the organization reads the
|
|
46
|
+
* team's rows and is admitted by none of them.
|
|
47
|
+
*
|
|
48
|
+
* Open source serves no team (the kind's tier is enterprise), so the
|
|
49
|
+
* person's rows name none and the second read never happens: one read per
|
|
50
|
+
* source, an indexed read on the OSS adapter (its list index by
|
|
51
|
+
* principal) whatever the number of organizations walked or rows held.
|
|
52
|
+
* An edition that composes teams over the built-in
|
|
53
|
+
* evaluator pays one read more per team the person holds. The rows are
|
|
54
|
+
* indexed by (object, relation) once, so a list's thousands of candidates
|
|
55
|
+
* each find theirs without walking the rest.
|
|
56
|
+
*
|
|
57
|
+
* An absent row is no tuples — the target's own not-found is the driver's
|
|
58
|
+
* arm, and a parent that no longer exists simply grants nothing, as in the
|
|
59
|
+
* cloud; any other store fault propagates so the driver folds it to
|
|
60
|
+
* `unavailable`. One source per check (or per list call for the list
|
|
61
|
+
* scope): its memo is the check's.
|
|
41
62
|
*
|
|
42
63
|
* A source may be SEEDED with the facts of objects the caller already
|
|
43
64
|
* holds (the list scope's candidates, decoded by the lane and carried on
|
|
@@ -102,6 +123,10 @@ export const ROW_RECORDED_OWNER_KINDS: ReadonlySet<ApiResourceKind> = new Set([
|
|
|
102
123
|
ApiResourceKind.organization,
|
|
103
124
|
]);
|
|
104
125
|
|
|
126
|
+
/** The FGA type a team grant names, and the relation that makes a person one of its members (the model's `team#member`). */
|
|
127
|
+
const TEAM_TYPE = kindEnumName(ApiResourceKind.team);
|
|
128
|
+
const TEAM_MEMBER_RELATION = "member";
|
|
129
|
+
|
|
105
130
|
function account(id: string): Subject {
|
|
106
131
|
return { form: "object", object: { type: ACCOUNT_TYPE, id } };
|
|
107
132
|
}
|
|
@@ -209,7 +234,9 @@ export interface DerivedTupleSource extends TupleSource {
|
|
|
209
234
|
* The person's own IamPolicy rows as tuples — the ONE read of them this
|
|
210
235
|
* source makes, shared with `tuplesOf`. A list-shaped consumer (the
|
|
211
236
|
* organization directory; the list scope) reads its candidate objects
|
|
212
|
-
* from here instead of scanning the port a second time.
|
|
237
|
+
* from here instead of scanning the port a second time. The rows granted
|
|
238
|
+
* to the person's teams are not among them: they name the team, and
|
|
239
|
+
* `tuplesOf` serves them where a check meets them.
|
|
213
240
|
*/
|
|
214
241
|
personTuples(): Promise<ReadonlyArray<Tuple>>;
|
|
215
242
|
}
|
|
@@ -235,6 +262,9 @@ export function newDerivedTupleSource(
|
|
|
235
262
|
);
|
|
236
263
|
}
|
|
237
264
|
let personRows: Promise<ReadonlyArray<Tuple>> | undefined;
|
|
265
|
+
let reachableRows:
|
|
266
|
+
| Promise<ReadonlyMap<string, ReadonlyArray<Tuple>>>
|
|
267
|
+
| undefined;
|
|
238
268
|
|
|
239
269
|
const loader: RowLoader = {
|
|
240
270
|
load(object) {
|
|
@@ -284,6 +314,38 @@ export function newDerivedTupleSource(
|
|
|
284
314
|
return personRows;
|
|
285
315
|
}
|
|
286
316
|
|
|
317
|
+
/**
|
|
318
|
+
* Every row a check can meet, keyed by (object, relation): the person's
|
|
319
|
+
* own, then the rows granted to each team the person holds `member` on
|
|
320
|
+
* (the module header says why that set is complete). A person in no
|
|
321
|
+
* team costs nothing beyond their own rows.
|
|
322
|
+
*/
|
|
323
|
+
function reachableTuples(): Promise<
|
|
324
|
+
ReadonlyMap<string, ReadonlyArray<Tuple>>
|
|
325
|
+
> {
|
|
326
|
+
if (reachableRows === undefined) {
|
|
327
|
+
reachableRows = rowTuples().then(async (own) => {
|
|
328
|
+
const teams = new Set<string>();
|
|
329
|
+
for (const tuple of own) {
|
|
330
|
+
if (
|
|
331
|
+
tuple.object.type === TEAM_TYPE &&
|
|
332
|
+
tuple.relation === TEAM_MEMBER_RELATION &&
|
|
333
|
+
tuple.object.id !== ""
|
|
334
|
+
) {
|
|
335
|
+
teams.add(tuple.object.id);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
const granted = await Promise.all(
|
|
339
|
+
[...teams].map((team) =>
|
|
340
|
+
deps.policies.findByPrincipal(TEAM_TYPE, team),
|
|
341
|
+
),
|
|
342
|
+
);
|
|
343
|
+
return indexByPair([...own, ...granted.flat().map(tupleOfRow)]);
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
return reachableRows;
|
|
347
|
+
}
|
|
348
|
+
|
|
287
349
|
return {
|
|
288
350
|
loader,
|
|
289
351
|
personTuples: rowTuples,
|
|
@@ -292,7 +354,8 @@ export function newDerivedTupleSource(
|
|
|
292
354
|
tuple.relation === relation &&
|
|
293
355
|
tuple.object.type === object.type &&
|
|
294
356
|
tuple.object.id === object.id;
|
|
295
|
-
const fromRows =
|
|
357
|
+
const fromRows =
|
|
358
|
+
(await reachableTuples()).get(pairKey(object, relation)) ?? [];
|
|
296
359
|
const declaration = model.byType(object.type);
|
|
297
360
|
if (declaration === undefined) {
|
|
298
361
|
return fromRows;
|
|
@@ -309,7 +372,11 @@ export function newDerivedTupleSource(
|
|
|
309
372
|
};
|
|
310
373
|
}
|
|
311
374
|
|
|
312
|
-
/**
|
|
375
|
+
/**
|
|
376
|
+
* The decoded row for an object, or undefined when the kind is not
|
|
377
|
+
* declared, is rowless (`platform`, which resolves over tuples alone), or
|
|
378
|
+
* the row does not exist.
|
|
379
|
+
*/
|
|
313
380
|
async function loadRow(
|
|
314
381
|
deps: Pick<DerivedTupleSourceDeps, "store" | "accounts">,
|
|
315
382
|
model: Model,
|
|
@@ -319,6 +386,7 @@ async function loadRow(
|
|
|
319
386
|
const kind = kindByEnumName(object.type);
|
|
320
387
|
if (
|
|
321
388
|
declaration === undefined ||
|
|
389
|
+
declaration.schema === undefined ||
|
|
322
390
|
kind === ApiResourceKind.api_resource_kind_unknown ||
|
|
323
391
|
object.id === ""
|
|
324
392
|
) {
|
|
@@ -337,6 +405,27 @@ async function loadRow(
|
|
|
337
405
|
}
|
|
338
406
|
}
|
|
339
407
|
|
|
408
|
+
/** The key a tuple is served under: its object and relation. */
|
|
409
|
+
function pairKey(object: ObjectRef, relation: string): string {
|
|
410
|
+
return `${formatObjectRef(object)}#${relation}`;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function indexByPair(
|
|
414
|
+
tuples: ReadonlyArray<Tuple>,
|
|
415
|
+
): ReadonlyMap<string, ReadonlyArray<Tuple>> {
|
|
416
|
+
const index = new Map<string, Tuple[]>();
|
|
417
|
+
for (const tuple of tuples) {
|
|
418
|
+
const key = pairKey(tuple.object, tuple.relation);
|
|
419
|
+
const held = index.get(key);
|
|
420
|
+
if (held === undefined) {
|
|
421
|
+
index.set(key, [tuple]);
|
|
422
|
+
} else {
|
|
423
|
+
held.push(tuple);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return index;
|
|
427
|
+
}
|
|
428
|
+
|
|
340
429
|
/** An IamPolicy row as the tuple it records: `resource#relation@principal`. */
|
|
341
430
|
function tupleOfRow(row: IamPolicy): Tuple {
|
|
342
431
|
const spec = row.spec;
|
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
* it is being resolved is a cycle in the declarations, which is a bug,
|
|
31
31
|
* and a resolution deeper than MAX_RESOLUTION_DEPTH is the same class of
|
|
32
32
|
* bug; both throw, because the alternative — a quiet `false` — would turn
|
|
33
|
-
* a broken
|
|
34
|
-
* not declare is a fault as the check's TARGET (the
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
33
|
+
* a broken model into a lockout nobody can see. A kind the model does
|
|
34
|
+
* not declare is a fault as the check's TARGET (the built-in model
|
|
35
|
+
* declares every kind, so reaching here is a registry gap) and simply an
|
|
36
|
+
* object with no relations when reached through a tuple (only a test's
|
|
37
|
+
* throwaway model omits a type).
|
|
38
38
|
* A relation the kind does not declare is `false` — the ruled answer for
|
|
39
39
|
* a wire question the model has no line for, where OpenFGA
|
|
40
40
|
* itself answers a validation error.
|
|
@@ -59,14 +59,12 @@ import { ACCOUNT_TYPE, formatObjectRef, pairKey } from "./tuples.js";
|
|
|
59
59
|
export const MAX_RESOLUTION_DEPTH = 25;
|
|
60
60
|
|
|
61
61
|
/**
|
|
62
|
-
* The faults an evaluation can raise.
|
|
63
|
-
* contracts (a kind the model does not declare
|
|
64
|
-
*
|
|
65
|
-
* answers before any driver); the rest are the model's own.
|
|
62
|
+
* The faults an evaluation can raise. One is a consumer bug by the seam
|
|
63
|
+
* contracts (a kind the model does not declare, or a rowless one asked
|
|
64
|
+
* for its rows); the rest are the model's own.
|
|
66
65
|
*/
|
|
67
66
|
export type EvaluationFault =
|
|
68
67
|
| "undeclared-target-kind"
|
|
69
|
-
| "internal-caller-offered"
|
|
70
68
|
| "resolution-depth-exceeded"
|
|
71
69
|
| "resolution-cycle";
|
|
72
70
|
|
|
@@ -40,27 +40,30 @@
|
|
|
40
40
|
* returns the org-narrowed rows for the class before any driver is asked
|
|
41
41
|
* (extensions/list-read-scope.ts, the header's contract line), the
|
|
42
42
|
* Authorize step's trust-domain rule applied to a list answer. So this
|
|
43
|
-
* driver's `restrictListEntries` REFUSES the class
|
|
44
|
-
* being offered it means a caller reached
|
|
45
|
-
* consumption idiom, and evaluating the server
|
|
46
|
-
* a quiet short list — the failure shape
|
|
47
|
-
* enumeration verb has no helper in front of it
|
|
48
|
-
* every id of the kind, as the built-in directory
|
|
49
|
-
* every organization. No in-process edge reaches
|
|
50
|
-
* today; the arm is pinned so a future one meets the
|
|
43
|
+
* driver's `restrictListEntries` REFUSES the class with the seam's
|
|
44
|
+
* `InternalCallerOfferedError`: being offered it means a caller reached
|
|
45
|
+
* the driver around the one consumption idiom, and evaluating the server
|
|
46
|
+
* as a person would return a quiet short list — the failure shape
|
|
47
|
+
* stigmer#1207 fixed. The enumeration verb has no helper in front of it
|
|
48
|
+
* and keeps its own arm: every id of the kind, as the built-in directory
|
|
49
|
+
* answers the class with every organization. No in-process edge reaches
|
|
50
|
+
* an enumeration lane today; the arm is pinned so a future one meets the
|
|
51
|
+
* rule.
|
|
51
52
|
*
|
|
52
53
|
* Faults THROW and are never an empty answer (the seam: an empty set means
|
|
53
54
|
* "authorized to see nothing"; an outage is the pipeline's sanitized
|
|
54
|
-
* INTERNAL). A kind the model does not declare,
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
* served lane
|
|
55
|
+
* INTERNAL). A kind the model does not declare, or a rowless one
|
|
56
|
+
* (`platform`, which has no stored rows to list), is, like the internal
|
|
57
|
+
* class above, a consumer bug by the seam's contract, and faults through
|
|
58
|
+
* the evaluator's own error, loud; every served list lane lists a kind
|
|
59
|
+
* with rows.
|
|
58
60
|
*
|
|
59
61
|
* Logging. One debug line per call — kind, offered, kept, elapsed — what
|
|
60
62
|
* an operator needs to answer "why is Alice's list short" beside the
|
|
61
63
|
* Authorizer's per-denial lines; never the token.
|
|
62
64
|
*/
|
|
63
65
|
import { fromBinary } from "@bufbuild/protobuf";
|
|
66
|
+
import type { DescMessage } from "@bufbuild/protobuf";
|
|
64
67
|
|
|
65
68
|
import type { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
66
69
|
|
|
@@ -71,6 +74,7 @@ import type {
|
|
|
71
74
|
ListEntryMeta,
|
|
72
75
|
ListReadScope,
|
|
73
76
|
} from "../extensions/list-read-scope.js";
|
|
77
|
+
import { InternalCallerOfferedError } from "../extensions/list-read-scope.js";
|
|
74
78
|
import { kindEnumName } from "../pipeline/apiresource-meta.js";
|
|
75
79
|
import type { Store } from "../store/interface.js";
|
|
76
80
|
import { newDerivedTupleSource } from "./derived-tuples.js";
|
|
@@ -87,6 +91,9 @@ import type { ObjectRef, Person } from "./tuples.js";
|
|
|
87
91
|
/** The relation every list lane reads through (the cloud's LIST_RELATION). */
|
|
88
92
|
const LIST_RELATION = "can_view";
|
|
89
93
|
|
|
94
|
+
/** A declaration of a kind with stored rows: the only kind a list lane can list. */
|
|
95
|
+
type ListableDeclaration = KindDeclaration & { readonly schema: DescMessage };
|
|
96
|
+
|
|
90
97
|
export interface BuiltInListReadScopeDeps {
|
|
91
98
|
readonly store: Store;
|
|
92
99
|
/** The port the grant path writes through — the person's rows are read from the same instance. */
|
|
@@ -153,8 +160,12 @@ export function newBuiltInListReadScope(
|
|
|
153
160
|
return kept;
|
|
154
161
|
}
|
|
155
162
|
|
|
156
|
-
/**
|
|
157
|
-
|
|
163
|
+
/**
|
|
164
|
+
* The declaration a verb evaluates against, which must be a kind with
|
|
165
|
+
* stored rows. An undeclared kind, or a rowless one, is the evaluator's
|
|
166
|
+
* own fault, raised before any read.
|
|
167
|
+
*/
|
|
168
|
+
function requireListable(kind: ApiResourceKind): ListableDeclaration {
|
|
158
169
|
const declaration = model.byKind(kind);
|
|
159
170
|
if (declaration === undefined) {
|
|
160
171
|
throw new AuthorizationEvaluationError(
|
|
@@ -162,11 +173,18 @@ export function newBuiltInListReadScope(
|
|
|
162
173
|
`no declaration for kind '${kindEnumName(kind)}' — a list lane asked the scope about a kind the model does not declare`,
|
|
163
174
|
);
|
|
164
175
|
}
|
|
165
|
-
|
|
176
|
+
const schema = declaration.schema;
|
|
177
|
+
if (schema === undefined) {
|
|
178
|
+
throw new AuthorizationEvaluationError(
|
|
179
|
+
"undeclared-target-kind",
|
|
180
|
+
`kind '${kindEnumName(kind)}' has no stored rows — a list lane asked the scope about a rowless kind`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return { ...declaration, schema };
|
|
166
184
|
}
|
|
167
185
|
|
|
168
186
|
/** Every row of `kind` the store holds, as facts; a row that does not decode is skipped. */
|
|
169
|
-
async function everyRowOf(declaration:
|
|
187
|
+
async function everyRowOf(declaration: ListableDeclaration): Promise<RowFacts[]> {
|
|
170
188
|
const kind = declaration.kind;
|
|
171
189
|
const rows = await deps.store.listResources(kind);
|
|
172
190
|
const facts: RowFacts[] = [];
|
|
@@ -189,12 +207,9 @@ export function newBuiltInListReadScope(
|
|
|
189
207
|
kind: ApiResourceKind,
|
|
190
208
|
entries: ReadonlyArray<ListEntryMeta>,
|
|
191
209
|
): Promise<ReadonlySet<string>> {
|
|
192
|
-
|
|
210
|
+
requireListable(kind);
|
|
193
211
|
if (caller.callerClass === "internal") {
|
|
194
|
-
throw new
|
|
195
|
-
"internal-caller-offered",
|
|
196
|
-
`the internal caller class was offered to the list scope for kind '${kindEnumName(kind)}' — the shared helper answers that class before any driver; a list lane reached the driver around it`,
|
|
197
|
-
);
|
|
212
|
+
throw new InternalCallerOfferedError(kind);
|
|
198
213
|
}
|
|
199
214
|
// One candidate per unique id: a lane offers each row once, and a
|
|
200
215
|
// duplicate would only spend a walk to learn the same answer.
|
|
@@ -211,7 +226,7 @@ export function newBuiltInListReadScope(
|
|
|
211
226
|
caller: CallerIdentity,
|
|
212
227
|
kind: ApiResourceKind,
|
|
213
228
|
): Promise<ReadonlySet<string>> {
|
|
214
|
-
const candidates = await everyRowOf(
|
|
229
|
+
const candidates = await everyRowOf(requireListable(kind));
|
|
215
230
|
if (caller.callerClass === "internal") {
|
|
216
231
|
return new Set(candidates.map((facts) => facts.id));
|
|
217
232
|
}
|