@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
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
* resolves the blueprint's `viewer` on — one read serves both. Which spec
|
|
15
15
|
* field names the blueprint is `kind_meta`'s to say (the instance kind's
|
|
16
16
|
* `additional_parents` entry for the blueprint kind), never restated
|
|
17
|
-
* here; an instance kind without that entry is a
|
|
18
|
-
* throws at first use rather than deriving nothing.
|
|
17
|
+
* here; an instance kind without that entry is a binding bug
|
|
18
|
+
* (bindings.ts) and throws at first use rather than deriving nothing.
|
|
19
19
|
*
|
|
20
20
|
* A user-created instance points at a blueprint whose pointer names a
|
|
21
21
|
* different row (or none), so the rule derives nothing for it and its
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `execution_viewer` derived rule of a workflow instance. The model
|
|
3
|
+
* (fga/model/agentic/workflow_instance.fga) keeps the instance's RUN
|
|
4
|
+
* history audience apart from `viewer`, so making an instance
|
|
5
|
+
* org-runnable never exposes other people's run inputs and outputs;
|
|
6
|
+
* workflow_execution reads it through `execution_viewer from
|
|
7
|
+
* workflow_instance`.
|
|
8
|
+
*
|
|
9
|
+
* It is derived from `spec.execution_visibility`, which `kind_meta`
|
|
10
|
+
* cannot express: `organization` derives
|
|
11
|
+
* `#execution_viewer@organization:<org>#viewer`, the organization's full
|
|
12
|
+
* read audience (the relation also admits `#member`, the legacy shape),
|
|
13
|
+
* and `private` or unset derives nothing, so each run stays its
|
|
14
|
+
* triggerer's. Pure over the row; no related row is read.
|
|
15
|
+
*/
|
|
16
|
+
import { isMessage } from "@bufbuild/protobuf";
|
|
17
|
+
|
|
18
|
+
import { WorkflowInstanceSchema } from "@stigmer/protos/ai/stigmer/agentic/workflowinstance/v1/api_pb";
|
|
19
|
+
import { WorkflowExecutionVisibility } from "@stigmer/protos/ai/stigmer/agentic/workflowinstance/v1/spec_pb";
|
|
20
|
+
|
|
21
|
+
import type { DerivedRelation } from "./rewrite.js";
|
|
22
|
+
|
|
23
|
+
export const executionViewer: DerivedRelation = (object, row) => {
|
|
24
|
+
if (!isMessage(row, WorkflowInstanceSchema)) {
|
|
25
|
+
return Promise.resolve([]);
|
|
26
|
+
}
|
|
27
|
+
const level =
|
|
28
|
+
row.spec?.executionVisibility ?? WorkflowExecutionVisibility.unspecified;
|
|
29
|
+
switch (level) {
|
|
30
|
+
case WorkflowExecutionVisibility.organization: {
|
|
31
|
+
const org = row.metadata?.org ?? "";
|
|
32
|
+
return Promise.resolve(
|
|
33
|
+
org === ""
|
|
34
|
+
? []
|
|
35
|
+
: [
|
|
36
|
+
{
|
|
37
|
+
object,
|
|
38
|
+
relation: "execution_viewer",
|
|
39
|
+
subject: {
|
|
40
|
+
form: "userset",
|
|
41
|
+
object: { type: "organization", id: org },
|
|
42
|
+
relation: "viewer",
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
case WorkflowExecutionVisibility.private:
|
|
49
|
+
case WorkflowExecutionVisibility.unspecified:
|
|
50
|
+
return Promise.resolve([]);
|
|
51
|
+
default: {
|
|
52
|
+
const exhaustive: never = level;
|
|
53
|
+
throw new Error(`unknown execution visibility: ${String(exhaustive)}`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
@@ -1,52 +1,37 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The built-in model
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* name (an object reference inside a tuple). A kind with no declaration
|
|
6
|
-
* is one this edition does not evaluate: as a check TARGET that is a
|
|
7
|
-
* registry gap the evaluator throws on (the driver refuses unserved kinds
|
|
8
|
-
* before any evaluation, so the throw is a backstop); reached through a
|
|
9
|
-
* tuple it is simply an object with no relations.
|
|
2
|
+
* The built-in model: every type of the authorization model, reached by
|
|
3
|
+
* kind (the driver's `AuthzCheck.resourceKind`) or by FGA type name (an
|
|
4
|
+
* object reference inside a tuple), in `fga.mod` order.
|
|
10
5
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
6
|
+
* It is built at module load from the one set of bytes every edition
|
|
7
|
+
* reads: data/authorization-model.json, OpenFGA's JSON compiled from the
|
|
8
|
+
* `.fga` files under fga/model (`make gen-authorization-model`), which the
|
|
9
|
+
* engine suites under fga/tests also run against. The reader
|
|
10
|
+
* (openfga-json.ts) turns it into the evaluator's vocabulary, and the
|
|
11
|
+
* binding table (bindings.ts) adds what JSON cannot carry: each kind's row
|
|
12
|
+
* schema and its derived rules. A model this server cannot evaluate, or a
|
|
13
|
+
* type and a binding that disagree, fails the load, so it fails the boot
|
|
14
|
+
* and every test that imports the model, never an answer.
|
|
15
|
+
*
|
|
16
|
+
* The model declares every type, whichever edition serves it. Which kinds
|
|
17
|
+
* an edition evaluates is the tier's question, answered before any
|
|
18
|
+
* evaluation (authorizer.ts, `kindServedByEdition`): open source refuses a
|
|
19
|
+
* check that targets `team`, `identity_provider`, `invitation` or
|
|
20
|
+
* `platform`, and a tuple that names one resolves over tuples alone, since
|
|
21
|
+
* this edition stores no row of those kinds.
|
|
14
22
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* files `fga.mod` lists that have no declaration here (`platform`,
|
|
18
|
-
* `identity_provider`, `invitation`) are kinds this edition does not
|
|
19
|
-
* serve: a check that targets one is refused by the
|
|
20
|
-
* driver, and a tuple that names one (`identity_provider#platform_user`)
|
|
21
|
-
* resolves to nobody. The registry test pins the list against the tier.
|
|
23
|
+
* `newModel` exists for tests that need a throwaway model (a cycle, a
|
|
24
|
+
* chain past the depth bound).
|
|
22
25
|
*/
|
|
23
|
-
import
|
|
26
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
24
27
|
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
import { channelAppDeclaration } from "./channel_app.js";
|
|
33
|
-
import { environmentDeclaration } from "./environment.js";
|
|
34
|
-
import { executionContextDeclaration } from "./execution_context.js";
|
|
35
|
-
import { iamPolicyDeclaration } from "./iam_policy.js";
|
|
36
|
-
import { identityAccountDeclaration } from "./identity_account.js";
|
|
37
|
-
import { mcpServerDeclaration } from "./mcp_server.js";
|
|
38
|
-
import { memoryDeclaration } from "./memory.js";
|
|
39
|
-
import { oauthAppDeclaration } from "./oauth_app.js";
|
|
40
|
-
import { organizationDeclaration } from "./organization.js";
|
|
41
|
-
import { platformClientDeclaration } from "./platform_client.js";
|
|
42
|
-
import { pluginDeclaration } from "./plugin.js";
|
|
43
|
-
import type { KindDeclaration } from "./rewrite.js";
|
|
44
|
-
import { scheduleDeclaration } from "./schedule.js";
|
|
45
|
-
import { sessionDeclaration } from "./session.js";
|
|
46
|
-
import { skillDeclaration } from "./skill.js";
|
|
47
|
-
import { workflowDeclaration } from "./workflow.js";
|
|
48
|
-
import { workflowExecutionDeclaration } from "./workflow_execution.js";
|
|
49
|
-
import { workflowInstanceDeclaration } from "./workflow_instance.js";
|
|
28
|
+
import { kindByEnumName } from "../../pipeline/apiresource-meta.js";
|
|
29
|
+
import type { KindBinding } from "./bindings.js";
|
|
30
|
+
import { KIND_BINDINGS } from "./bindings.js";
|
|
31
|
+
import compiledModel from "./data/authorization-model.json" with { type: "json" };
|
|
32
|
+
import type { ModelType } from "./openfga-json.js";
|
|
33
|
+
import { readOpenFgaModel } from "./openfga-json.js";
|
|
34
|
+
import type { DerivedRelation, KindDeclaration } from "./rewrite.js";
|
|
50
35
|
|
|
51
36
|
export interface Model {
|
|
52
37
|
/** In registry order. */
|
|
@@ -72,35 +57,59 @@ export function newModel(declarations: ReadonlyArray<KindDeclaration>): Model {
|
|
|
72
57
|
};
|
|
73
58
|
}
|
|
74
59
|
|
|
75
|
-
/**
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
60
|
+
/**
|
|
61
|
+
* Joins the compiled model's types with their bindings, in the model's
|
|
62
|
+
* order. Refuses a type that names no `ApiResourceKind`, a type with no
|
|
63
|
+
* binding, a binding with no type, and a derived rule for a relation the
|
|
64
|
+
* type does not define.
|
|
65
|
+
*/
|
|
66
|
+
export function bindModel(
|
|
67
|
+
types: ReadonlyArray<ModelType>,
|
|
68
|
+
bindings: ReadonlyMap<ApiResourceKind, KindBinding>,
|
|
69
|
+
): ReadonlyArray<KindDeclaration> {
|
|
70
|
+
const bound = new Set<ApiResourceKind>();
|
|
71
|
+
const declarations = types.map((entry): KindDeclaration => {
|
|
72
|
+
const kind = kindByEnumName(entry.type);
|
|
73
|
+
if (kind === ApiResourceKind.api_resource_kind_unknown) {
|
|
74
|
+
throw new Error(`${entry.source}: type '${entry.type}' names no ApiResourceKind`);
|
|
75
|
+
}
|
|
76
|
+
const binding = bindings.get(kind);
|
|
77
|
+
if (binding === undefined) {
|
|
78
|
+
throw new Error(`${entry.source}: type '${entry.type}' has no binding (model/bindings.ts)`);
|
|
79
|
+
}
|
|
80
|
+
bound.add(kind);
|
|
81
|
+
const derived = new Map<string, DerivedRelation>();
|
|
82
|
+
for (const [relation, rule] of Object.entries(binding.derived ?? {})) {
|
|
83
|
+
if (!entry.relations.has(relation)) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`model/bindings.ts: a derived rule for '${entry.type}#${relation}', which ${entry.source} does not define`,
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
derived.set(relation, rule);
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
kind,
|
|
92
|
+
type: entry.type,
|
|
93
|
+
schema: binding.schema,
|
|
94
|
+
source: entry.source,
|
|
95
|
+
relations: entry.relations,
|
|
96
|
+
derived,
|
|
97
|
+
};
|
|
98
|
+
});
|
|
99
|
+
for (const kind of bindings.keys()) {
|
|
100
|
+
if (!bound.has(kind)) {
|
|
101
|
+
throw new Error(`model/bindings.ts: a binding for '${ApiResourceKind[kind]}', which the model does not define`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return declarations;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Every type of the authorization model, in `fga.mod` order. */
|
|
108
|
+
export const builtInModel: Model = newModel(
|
|
109
|
+
bindModel(readOpenFgaModel(compiledModel as unknown), KIND_BINDINGS),
|
|
110
|
+
);
|
|
102
111
|
|
|
103
|
-
/** The built-in model's declaration for a kind, or undefined for a kind
|
|
112
|
+
/** The built-in model's declaration for a kind, or undefined for a kind the model does not define. */
|
|
104
113
|
export function declarationFor(
|
|
105
114
|
kind: ApiResourceKind,
|
|
106
115
|
): KindDeclaration | undefined {
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The reader of the compiled authorization model: OpenFGA's own JSON
|
|
3
|
+
* (data/authorization-model.json, written by `make gen-authorization-model`
|
|
4
|
+
* from the `.fga` files under fga/model) turned into the evaluator's
|
|
5
|
+
* vocabulary (rewrite.ts), one entry per type in the file's order.
|
|
6
|
+
*
|
|
7
|
+
* The file is OpenFGA's format, not this server's, so it is read as
|
|
8
|
+
* `unknown` and refused loudly (strict-json.ts): every key the compiled
|
|
9
|
+
* model carries is listed below, and any other key, at any depth, is a
|
|
10
|
+
* thrown fault naming where. It runs when model/index.ts loads, so a model
|
|
11
|
+
* this server cannot evaluate fails the boot and every test that imports
|
|
12
|
+
* the model, never an answer.
|
|
13
|
+
*
|
|
14
|
+
* What is accepted, which is what the compiled model uses:
|
|
15
|
+
* - `schema_version` "1.2" and `type_definitions`;
|
|
16
|
+
* - per type: `type`, `relations`, and `metadata.{module, relations,
|
|
17
|
+
* source_info.file}`;
|
|
18
|
+
* - per relation's metadata: `directly_related_user_types[]`, each an
|
|
19
|
+
* object type (`{type}`) or a userset (`{type, relation}`);
|
|
20
|
+
* - per rewrite node: `this`, `computedUserset.relation`,
|
|
21
|
+
* `tupleToUserset.{tupleset,computedUserset}.relation`, `union.child`
|
|
22
|
+
* and `intersection.child`, nested to any depth (the evaluator
|
|
23
|
+
* recurses through any nesting).
|
|
24
|
+
*
|
|
25
|
+
* What is refused by name, because the evaluator does not implement it
|
|
26
|
+
* and the day the model needs it is a design act: `difference` (`but
|
|
27
|
+
* not`), conditions (a `conditions` map, or a type reference carrying
|
|
28
|
+
* `condition`), wildcards (`type:*`), a `computedUserset` that names
|
|
29
|
+
* another object, and any schema version but 1.2.
|
|
30
|
+
*
|
|
31
|
+
* What is re-asserted, though OpenFGA's validator proved it when the file
|
|
32
|
+
* was compiled, because the file is committed data and a hand edit must
|
|
33
|
+
* fail here rather than in an answer: every computed target and tupleset
|
|
34
|
+
* is a relation of its own type; every subject type names a declared type,
|
|
35
|
+
* and a userset subject a relation that type declares; and a relation
|
|
36
|
+
* lists direct subject types exactly when its rewrite has a `this` node,
|
|
37
|
+
* which is where the evaluator reads that list from.
|
|
38
|
+
*
|
|
39
|
+
* Relations come out sorted by name (the canonical file sorts keys; the
|
|
40
|
+
* evaluator only looks relations up). A union's or an intersection's
|
|
41
|
+
* children keep the file's order: the evaluator short-circuits in that
|
|
42
|
+
* order, so it is what the cost of a check rides on.
|
|
43
|
+
*
|
|
44
|
+
* The reader imports nothing but the vocabulary and the strict-read
|
|
45
|
+
* helpers, so the reader and the evaluator stay a self-contained pair.
|
|
46
|
+
*/
|
|
47
|
+
import { list, record, text } from "../strict-json.js";
|
|
48
|
+
import type { Rewrite, SubjectType } from "./rewrite.js";
|
|
49
|
+
|
|
50
|
+
/** The only schema version this reader accepts: the modular DSL's. */
|
|
51
|
+
export const SUPPORTED_SCHEMA_VERSION = "1.2";
|
|
52
|
+
|
|
53
|
+
/** One type of the compiled model, in the evaluator's vocabulary. */
|
|
54
|
+
export interface ModelType {
|
|
55
|
+
/** The FGA type name: an `ApiResourceKind` member name (`agent`, `identity_account`). */
|
|
56
|
+
readonly type: string;
|
|
57
|
+
/** The `.fga` file the type is defined in, relative to the server package (`fga/model/agentic/agent.fga`). */
|
|
58
|
+
readonly source: string;
|
|
59
|
+
/** Every relation the type defines, sorted by name. */
|
|
60
|
+
readonly relations: ReadonlyMap<string, Rewrite>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Where the `.fga` files live, relative to the server package; `source_info.file` is relative to it. */
|
|
64
|
+
const MODEL_SOURCE_DIR = "fga/model";
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Reads a parsed compiled model. `where` names the file in every message.
|
|
68
|
+
* Throws on anything the module header says is refused or re-asserted.
|
|
69
|
+
*/
|
|
70
|
+
export function readOpenFgaModel(
|
|
71
|
+
value: unknown,
|
|
72
|
+
where = "authorization-model.json",
|
|
73
|
+
): ReadonlyArray<ModelType> {
|
|
74
|
+
const root = record(value, where, ["schema_version", "type_definitions", "conditions"]);
|
|
75
|
+
if (root["conditions"] !== undefined) {
|
|
76
|
+
throw new Error(`${where}: conditions are not evaluated by this server`);
|
|
77
|
+
}
|
|
78
|
+
const version = text(root["schema_version"], `${where}: schema_version`);
|
|
79
|
+
if (version !== SUPPORTED_SCHEMA_VERSION) {
|
|
80
|
+
throw new Error(
|
|
81
|
+
`${where}: schema_version '${version}' is not ${SUPPORTED_SCHEMA_VERSION}, the only version this server reads`,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const types: ModelType[] = [];
|
|
86
|
+
const seen = new Set<string>();
|
|
87
|
+
list(root["type_definitions"], `${where}: type_definitions`).forEach((entry, index) => {
|
|
88
|
+
const parsed = readType(entry, `${where}: type_definitions[${index}]`);
|
|
89
|
+
if (seen.has(parsed.type)) {
|
|
90
|
+
throw new Error(`${where}: type '${parsed.type}' is defined twice`);
|
|
91
|
+
}
|
|
92
|
+
seen.add(parsed.type);
|
|
93
|
+
types.push(parsed);
|
|
94
|
+
});
|
|
95
|
+
assertReferencesResolve(types, where);
|
|
96
|
+
return types;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function readType(value: unknown, where: string): ModelType {
|
|
100
|
+
const entry = record(value, where, ["type", "relations", "metadata"]);
|
|
101
|
+
const type = text(entry["type"], `${where}.type`);
|
|
102
|
+
const at = `${where} (${type})`;
|
|
103
|
+
const metadata = record(entry["metadata"], `${at}.metadata`, [
|
|
104
|
+
"module",
|
|
105
|
+
"relations",
|
|
106
|
+
"source_info",
|
|
107
|
+
]);
|
|
108
|
+
text(metadata["module"], `${at}.metadata.module`);
|
|
109
|
+
const sourceInfo = record(metadata["source_info"], `${at}.metadata.source_info`, ["file"]);
|
|
110
|
+
const file = text(sourceInfo["file"], `${at}.metadata.source_info.file`);
|
|
111
|
+
|
|
112
|
+
const directTypes = readDirectTypes(metadata["relations"], `${at}.metadata.relations`);
|
|
113
|
+
const rewrites =
|
|
114
|
+
entry["relations"] === undefined ? {} : record(entry["relations"], `${at}.relations`);
|
|
115
|
+
|
|
116
|
+
for (const relation of directTypes.keys()) {
|
|
117
|
+
if (!(relation in rewrites)) {
|
|
118
|
+
throw new Error(`${at}.metadata.relations.${relation}: metadata for a relation ${type} does not define`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const relations = new Map<string, Rewrite>();
|
|
123
|
+
for (const name of Object.keys(rewrites).sort()) {
|
|
124
|
+
const subjects = directTypes.get(name);
|
|
125
|
+
const rewrite = readRewrite(rewrites[name], `${at}.relations.${name}`, subjects);
|
|
126
|
+
const hasThis = containsThis(rewrite);
|
|
127
|
+
if (hasThis && subjects === undefined) {
|
|
128
|
+
throw new Error(`${at}.relations.${name}: a direct relation with no directly_related_user_types`);
|
|
129
|
+
}
|
|
130
|
+
if (!hasThis && subjects !== undefined) {
|
|
131
|
+
throw new Error(`${at}.relations.${name}: directly_related_user_types on a relation with no direct node`);
|
|
132
|
+
}
|
|
133
|
+
relations.set(name, rewrite);
|
|
134
|
+
}
|
|
135
|
+
return { type, source: `${MODEL_SOURCE_DIR}/${file}`, relations };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Each relation's direct subject types; a relation whose metadata is `{}` lists none and is absent. */
|
|
139
|
+
function readDirectTypes(value: unknown, where: string): ReadonlyMap<string, ReadonlyArray<SubjectType>> {
|
|
140
|
+
const out = new Map<string, ReadonlyArray<SubjectType>>();
|
|
141
|
+
if (value === undefined) {
|
|
142
|
+
return out;
|
|
143
|
+
}
|
|
144
|
+
for (const [relation, meta] of Object.entries(record(value, where))) {
|
|
145
|
+
const entry = record(meta, `${where}.${relation}`, ["directly_related_user_types"]);
|
|
146
|
+
if (entry["directly_related_user_types"] === undefined) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const at = `${where}.${relation}.directly_related_user_types`;
|
|
150
|
+
out.set(
|
|
151
|
+
relation,
|
|
152
|
+
list(entry["directly_related_user_types"], at).map((item, index) =>
|
|
153
|
+
readSubjectType(item, `${at}[${index}]`),
|
|
154
|
+
),
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function readSubjectType(value: unknown, where: string): SubjectType {
|
|
161
|
+
const entry = record(value, where, ["type", "relation", "wildcard", "condition"]);
|
|
162
|
+
if (entry["wildcard"] !== undefined) {
|
|
163
|
+
throw new Error(`${where}: wildcard subject types are not evaluated by this server`);
|
|
164
|
+
}
|
|
165
|
+
if (entry["condition"] !== undefined) {
|
|
166
|
+
throw new Error(`${where}: conditional subject types are not evaluated by this server`);
|
|
167
|
+
}
|
|
168
|
+
const type = text(entry["type"], `${where}.type`);
|
|
169
|
+
return entry["relation"] === undefined
|
|
170
|
+
? { form: "object", type }
|
|
171
|
+
: { form: "userset", type, relation: text(entry["relation"], `${where}.relation`) };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* One rewrite node. `subjects` is the relation's direct list, which a
|
|
176
|
+
* `this` node takes; it is checked against the node's presence by the
|
|
177
|
+
* caller.
|
|
178
|
+
*/
|
|
179
|
+
function readRewrite(
|
|
180
|
+
value: unknown,
|
|
181
|
+
where: string,
|
|
182
|
+
subjects: ReadonlyArray<SubjectType> | undefined,
|
|
183
|
+
): Rewrite {
|
|
184
|
+
const node = record(value, where, [
|
|
185
|
+
"this",
|
|
186
|
+
"computedUserset",
|
|
187
|
+
"tupleToUserset",
|
|
188
|
+
"union",
|
|
189
|
+
"intersection",
|
|
190
|
+
"difference",
|
|
191
|
+
]);
|
|
192
|
+
const keys = Object.keys(node);
|
|
193
|
+
if (keys.length !== 1) {
|
|
194
|
+
throw new Error(`${where}: a rewrite node has exactly one form, found ${keys.length}`);
|
|
195
|
+
}
|
|
196
|
+
const form = keys[0];
|
|
197
|
+
switch (form) {
|
|
198
|
+
case "this":
|
|
199
|
+
record(node["this"], `${where}.this`, []);
|
|
200
|
+
return { node: "this", subjects: subjects ?? [] };
|
|
201
|
+
case "computedUserset":
|
|
202
|
+
return { node: "computed", relation: localRelation(node["computedUserset"], `${where}.computedUserset`) };
|
|
203
|
+
case "tupleToUserset": {
|
|
204
|
+
const ttu = record(node["tupleToUserset"], `${where}.tupleToUserset`, [
|
|
205
|
+
"tupleset",
|
|
206
|
+
"computedUserset",
|
|
207
|
+
]);
|
|
208
|
+
return {
|
|
209
|
+
node: "from",
|
|
210
|
+
tupleset: localRelation(ttu["tupleset"], `${where}.tupleToUserset.tupleset`),
|
|
211
|
+
relation: localRelation(ttu["computedUserset"], `${where}.tupleToUserset.computedUserset`),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
case "union":
|
|
215
|
+
case "intersection": {
|
|
216
|
+
const operator = record(node[form], `${where}.${form}`, ["child"]);
|
|
217
|
+
const members = list(operator["child"], `${where}.${form}.child`).map((child, index) =>
|
|
218
|
+
readRewrite(child, `${where}.${form}.child[${index}]`, subjects),
|
|
219
|
+
);
|
|
220
|
+
if (members.length === 0) {
|
|
221
|
+
throw new Error(`${where}.${form}: an operator with no children`);
|
|
222
|
+
}
|
|
223
|
+
return { node: form, members };
|
|
224
|
+
}
|
|
225
|
+
case "difference":
|
|
226
|
+
throw new Error(`${where}: 'but not' (difference) is not evaluated by this server`);
|
|
227
|
+
default:
|
|
228
|
+
throw new Error(`${where}: unknown rewrite form '${String(form)}'`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** An `ObjectRelation` naming a relation of the same object: `object` must be unset. */
|
|
233
|
+
function localRelation(value: unknown, where: string): string {
|
|
234
|
+
const entry = record(value, where, ["relation", "object"]);
|
|
235
|
+
if (entry["object"] !== undefined) {
|
|
236
|
+
throw new Error(`${where}: a relation on another object is not evaluated by this server`);
|
|
237
|
+
}
|
|
238
|
+
return text(entry["relation"], `${where}.relation`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function containsThis(rewrite: Rewrite): boolean {
|
|
242
|
+
switch (rewrite.node) {
|
|
243
|
+
case "this":
|
|
244
|
+
return true;
|
|
245
|
+
case "computed":
|
|
246
|
+
case "from":
|
|
247
|
+
return false;
|
|
248
|
+
case "union":
|
|
249
|
+
case "intersection":
|
|
250
|
+
return rewrite.members.some(containsThis);
|
|
251
|
+
default: {
|
|
252
|
+
const exhaustive: never = rewrite;
|
|
253
|
+
throw new Error(`unknown rewrite node: ${JSON.stringify(exhaustive)}`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** The cross-type re-assertions of the module header. */
|
|
259
|
+
function assertReferencesResolve(types: ReadonlyArray<ModelType>, where: string): void {
|
|
260
|
+
const byType = new Map(types.map((entry) => [entry.type, entry]));
|
|
261
|
+
for (const entry of types) {
|
|
262
|
+
for (const [name, rewrite] of entry.relations) {
|
|
263
|
+
const at = `${where}: ${entry.type}#${name}`;
|
|
264
|
+
walk(rewrite, (node) => {
|
|
265
|
+
switch (node.node) {
|
|
266
|
+
case "computed":
|
|
267
|
+
if (!entry.relations.has(node.relation)) {
|
|
268
|
+
throw new Error(`${at}: names relation '${node.relation}', which ${entry.type} does not define`);
|
|
269
|
+
}
|
|
270
|
+
return;
|
|
271
|
+
case "from":
|
|
272
|
+
if (!entry.relations.has(node.tupleset)) {
|
|
273
|
+
throw new Error(`${at}: names tupleset '${node.tupleset}', which ${entry.type} does not define`);
|
|
274
|
+
}
|
|
275
|
+
return;
|
|
276
|
+
case "this":
|
|
277
|
+
for (const subject of node.subjects) {
|
|
278
|
+
const target = byType.get(subject.type);
|
|
279
|
+
if (target === undefined) {
|
|
280
|
+
throw new Error(`${at}: admits subject type '${subject.type}', which the model does not define`);
|
|
281
|
+
}
|
|
282
|
+
if (subject.form === "userset" && !target.relations.has(subject.relation)) {
|
|
283
|
+
throw new Error(
|
|
284
|
+
`${at}: admits '${subject.type}#${subject.relation}', which ${subject.type} does not define`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return;
|
|
289
|
+
case "union":
|
|
290
|
+
case "intersection":
|
|
291
|
+
return;
|
|
292
|
+
default: {
|
|
293
|
+
const exhaustive: never = node;
|
|
294
|
+
throw new Error(`unknown rewrite node: ${JSON.stringify(exhaustive)}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Visits every node of a rewrite, depth first, in order. */
|
|
303
|
+
function walk(rewrite: Rewrite, visit: (node: Rewrite) => void): void {
|
|
304
|
+
visit(rewrite);
|
|
305
|
+
if (rewrite.node === "union" || rewrite.node === "intersection") {
|
|
306
|
+
for (const member of rewrite.members) {
|
|
307
|
+
walk(member, visit);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|