@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
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The OpenFGA store-test kit: runs a `.fga.yaml` document (the format
|
|
3
|
+
* `fga model test` reads: fixture tuples, then named tests with `check`
|
|
4
|
+
* and `list_objects` assertions) against the built-in evaluator. The
|
|
5
|
+
* documents are the model's own suites (fga/tests/), which the real
|
|
6
|
+
* engine runs too (`make test-authorization-model`), so every assertion
|
|
7
|
+
* answered here is answered by OpenFGA over the same compiled model: the
|
|
8
|
+
* proof that the evaluator and the engine mean one thing.
|
|
9
|
+
*
|
|
10
|
+
* One case per named test. A case asserts every `check`, relation by
|
|
11
|
+
* relation, and every `list_objects`, and fails on the first disagreement
|
|
12
|
+
* with a message in the document's own words. Nothing is skipped: the
|
|
13
|
+
* built-in model declares every type a document can name.
|
|
14
|
+
*
|
|
15
|
+
* `list_objects` is evaluated exactly. An object holds a relation only
|
|
16
|
+
* through a tuple on that object: a direct tuple, a tupleset link, or a
|
|
17
|
+
* computed relation that bottoms out in one of those, which are the only
|
|
18
|
+
* forms the model uses (the reader refuses wildcards, the one form that
|
|
19
|
+
* would grant without a tuple on the object). So the objects of the asked
|
|
20
|
+
* type that the document's tuples name are the complete candidate set;
|
|
21
|
+
* each is checked, and the kept ones are compared with the expectation as
|
|
22
|
+
* a set, the way OpenFGA answers it (in no order). This is also how the
|
|
23
|
+
* server's list scope works: rows first, then checks.
|
|
24
|
+
*
|
|
25
|
+
* The parser is strict on purpose (strict-json.ts): an unknown key
|
|
26
|
+
* anywhere is a thrown fault, so a change to the format upstream is loud
|
|
27
|
+
* here rather than half-run. The model declares no condition, so a
|
|
28
|
+
* tuple's `condition` and a check's `context` are among the keys refused.
|
|
29
|
+
*/
|
|
30
|
+
import assert from "node:assert/strict";
|
|
31
|
+
|
|
32
|
+
import { checkRelation } from "../evaluator.js";
|
|
33
|
+
import type { Model } from "../model/index.js";
|
|
34
|
+
import { list, optionalList, record, text } from "../strict-json.js";
|
|
35
|
+
import type { Person, Tuple } from "../tuples.js";
|
|
36
|
+
import {
|
|
37
|
+
ACCOUNT_TYPE,
|
|
38
|
+
formatObjectRef,
|
|
39
|
+
newInMemoryTupleSource,
|
|
40
|
+
parseObjectRef,
|
|
41
|
+
parseSubject,
|
|
42
|
+
} from "../tuples.js";
|
|
43
|
+
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
// The document, typed exactly as the suites use the format.
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
export interface StoreTestTuple {
|
|
49
|
+
readonly user: string;
|
|
50
|
+
readonly relation: string;
|
|
51
|
+
readonly object: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface StoreTestCheck {
|
|
55
|
+
readonly user: string;
|
|
56
|
+
readonly object: string;
|
|
57
|
+
readonly assertions: Readonly<Record<string, boolean>>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface StoreTestListObjects {
|
|
61
|
+
readonly user: string;
|
|
62
|
+
readonly type: string;
|
|
63
|
+
readonly assertions: Readonly<Record<string, ReadonlyArray<string>>>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface StoreTest {
|
|
67
|
+
readonly name: string;
|
|
68
|
+
readonly check: ReadonlyArray<StoreTestCheck>;
|
|
69
|
+
readonly list_objects: ReadonlyArray<StoreTestListObjects>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface StoreTestDocument {
|
|
73
|
+
readonly name: string;
|
|
74
|
+
readonly tuples: ReadonlyArray<StoreTestTuple>;
|
|
75
|
+
readonly tests: ReadonlyArray<StoreTest>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Parses a loaded YAML value into a document, refusing anything the
|
|
80
|
+
* format the suites use does not carry (`sourceName` names the file in
|
|
81
|
+
* every message).
|
|
82
|
+
*/
|
|
83
|
+
export function parseStoreTestDocument(
|
|
84
|
+
value: unknown,
|
|
85
|
+
sourceName: string,
|
|
86
|
+
): StoreTestDocument {
|
|
87
|
+
const root = record(value, sourceName, [
|
|
88
|
+
"name",
|
|
89
|
+
"model_file",
|
|
90
|
+
"tuples",
|
|
91
|
+
"tests",
|
|
92
|
+
]);
|
|
93
|
+
return {
|
|
94
|
+
name: text(root["name"], `${sourceName}: name`),
|
|
95
|
+
tuples: list(root["tuples"], `${sourceName}: tuples`).map((entry, index) =>
|
|
96
|
+
parseTuple(entry, `${sourceName}: tuples[${index}]`),
|
|
97
|
+
),
|
|
98
|
+
tests: list(root["tests"], `${sourceName}: tests`).map((entry, index) =>
|
|
99
|
+
parseTest(entry, `${sourceName}: tests[${index}]`),
|
|
100
|
+
),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function parseTuple(value: unknown, where: string): StoreTestTuple {
|
|
105
|
+
const entry = record(value, where, ["user", "relation", "object"]);
|
|
106
|
+
return {
|
|
107
|
+
user: text(entry["user"], `${where}.user`),
|
|
108
|
+
relation: text(entry["relation"], `${where}.relation`),
|
|
109
|
+
object: text(entry["object"], `${where}.object`),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function parseTest(value: unknown, where: string): StoreTest {
|
|
114
|
+
const entry = record(value, where, ["name", "check", "list_objects"]);
|
|
115
|
+
return {
|
|
116
|
+
name: text(entry["name"], `${where}.name`),
|
|
117
|
+
check: optionalList(entry["check"], `${where}.check`).map((item, index) =>
|
|
118
|
+
parseCheck(item, `${where}.check[${index}]`),
|
|
119
|
+
),
|
|
120
|
+
list_objects: optionalList(
|
|
121
|
+
entry["list_objects"],
|
|
122
|
+
`${where}.list_objects`,
|
|
123
|
+
).map((item, index) =>
|
|
124
|
+
parseListObjects(item, `${where}.list_objects[${index}]`),
|
|
125
|
+
),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function parseCheck(value: unknown, where: string): StoreTestCheck {
|
|
130
|
+
const entry = record(value, where, ["user", "object", "assertions"]);
|
|
131
|
+
const assertions = record(entry["assertions"], `${where}.assertions`);
|
|
132
|
+
const parsed: Record<string, boolean> = {};
|
|
133
|
+
for (const [relation, expected] of Object.entries(assertions)) {
|
|
134
|
+
if (typeof expected !== "boolean") {
|
|
135
|
+
throw new Error(`${where}.assertions.${relation}: expected a boolean`);
|
|
136
|
+
}
|
|
137
|
+
parsed[relation] = expected;
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
user: text(entry["user"], `${where}.user`),
|
|
141
|
+
object: text(entry["object"], `${where}.object`),
|
|
142
|
+
assertions: parsed,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function parseListObjects(value: unknown, where: string): StoreTestListObjects {
|
|
147
|
+
const entry = record(value, where, ["user", "type", "assertions"]);
|
|
148
|
+
const assertions = record(entry["assertions"], `${where}.assertions`);
|
|
149
|
+
const parsed: Record<string, ReadonlyArray<string>> = {};
|
|
150
|
+
for (const [relation, expected] of Object.entries(assertions)) {
|
|
151
|
+
parsed[relation] = list(expected, `${where}.assertions.${relation}`).map(
|
|
152
|
+
(item, index) => text(item, `${where}.assertions.${relation}[${index}]`),
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
user: text(entry["user"], `${where}.user`),
|
|
157
|
+
type: text(entry["type"], `${where}.type`),
|
|
158
|
+
assertions: parsed,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// The cases.
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
|
|
166
|
+
export interface StoreTestCase {
|
|
167
|
+
/** `<document> :: <test name>`: the framework prints it as the test name. */
|
|
168
|
+
readonly name: string;
|
|
169
|
+
run(): Promise<void>;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** The document's tests as runnable cases over `model`. */
|
|
173
|
+
export function storeTestCases(
|
|
174
|
+
document: StoreTestDocument,
|
|
175
|
+
model: Model,
|
|
176
|
+
): ReadonlyArray<StoreTestCase> {
|
|
177
|
+
const tuples = document.tuples.map(tupleOf);
|
|
178
|
+
const source = newInMemoryTupleSource(tuples);
|
|
179
|
+
const deps = { model, source };
|
|
180
|
+
|
|
181
|
+
return document.tests.map((test) => ({
|
|
182
|
+
name: `${document.name} :: ${test.name}`,
|
|
183
|
+
async run() {
|
|
184
|
+
for (const item of test.check) {
|
|
185
|
+
const object = parseObjectRef(item.object);
|
|
186
|
+
const person = personOf(item.user);
|
|
187
|
+
for (const [relation, expected] of Object.entries(item.assertions)) {
|
|
188
|
+
const actual = await checkRelation(deps, object, relation, person);
|
|
189
|
+
assert.equal(
|
|
190
|
+
actual,
|
|
191
|
+
expected,
|
|
192
|
+
`${document.name} :: ${test.name}: ${item.user} ${relation} ${item.object} expected ${String(expected)}, got ${String(actual)}`,
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
for (const item of test.list_objects) {
|
|
197
|
+
const person = personOf(item.user);
|
|
198
|
+
const candidates = objectsOfType(tuples, item.type);
|
|
199
|
+
for (const [relation, expected] of Object.entries(item.assertions)) {
|
|
200
|
+
const kept: string[] = [];
|
|
201
|
+
for (const candidate of candidates) {
|
|
202
|
+
if (await checkRelation(deps, parseObjectRef(candidate), relation, person)) {
|
|
203
|
+
kept.push(candidate);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
assert.deepEqual(
|
|
207
|
+
kept.sort(),
|
|
208
|
+
[...expected].sort(),
|
|
209
|
+
`${document.name} :: ${test.name}: ${item.user} ${relation} objects of type ${item.type}`,
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
}));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Every object of `type` a tuple is written on, each once, in first-seen order: the complete candidate set (the module header). */
|
|
218
|
+
export function objectsOfType(
|
|
219
|
+
tuples: ReadonlyArray<Tuple>,
|
|
220
|
+
type: string,
|
|
221
|
+
): ReadonlyArray<string> {
|
|
222
|
+
const seen = new Set<string>();
|
|
223
|
+
for (const tuple of tuples) {
|
|
224
|
+
if (tuple.object.type === type) {
|
|
225
|
+
seen.add(formatObjectRef(tuple.object));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return [...seen];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function tupleOf(entry: StoreTestTuple): Tuple {
|
|
232
|
+
return {
|
|
233
|
+
object: parseObjectRef(entry.object),
|
|
234
|
+
relation: entry.relation,
|
|
235
|
+
subject: parseSubject(entry.user),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** The document's users are plain accounts; a user of another type is a format the kit does not run. */
|
|
240
|
+
function personOf(user: string): Person {
|
|
241
|
+
const object = parseObjectRef(user);
|
|
242
|
+
if (object.type !== ACCOUNT_TYPE || object.id === "*") {
|
|
243
|
+
throw new Error(`store test users are accounts; got '${user}'`);
|
|
244
|
+
}
|
|
245
|
+
return { accountId: object.id, aliases: new Set([object.id]) };
|
|
246
|
+
}
|
|
@@ -1,24 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Runs the
|
|
3
|
-
* evaluator
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* evaluated over derived tuples" means the SAME model.
|
|
2
|
+
* Runs the model's own suites (fga/tests/*.fga.yaml) through the built-in
|
|
3
|
+
* evaluator. The same documents run on the real OpenFGA engine against the
|
|
4
|
+
* same compiled model (`make test-authorization-model`, and CI's
|
|
5
|
+
* authorization-model lane), so every `check` and every `list_objects`
|
|
6
|
+
* assertion answered here is answered by OpenFGA too: the proof that the
|
|
7
|
+
* evaluator and the engine mean one thing by the model.
|
|
9
8
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* type this edition does not declare come back from the kit as a skipped
|
|
14
|
-
* set this file pins entry by entry, so a fixture that gains a scenario
|
|
15
|
-
* is a visible diff here and not a silently smaller proof. The second
|
|
16
|
-
* describe below pins the walk-dependent reason's own behaviour over a
|
|
17
|
-
* throwaway model, so the kit is proven and not only used.
|
|
9
|
+
* Nothing is skipped. `DOCUMENTS` is the folder's listing, so a suite
|
|
10
|
+
* added to the folder without a line here fails, and a suite is never
|
|
11
|
+
* silently out of the proof.
|
|
18
12
|
*
|
|
19
|
-
* The
|
|
20
|
-
*
|
|
21
|
-
*
|
|
13
|
+
* The second describe proves the kit's `list_objects` rule over a
|
|
14
|
+
* throwaway model: candidates are the objects the document's tuples are
|
|
15
|
+
* written on, checked one by one, so an object reached through a tupleset
|
|
16
|
+
* link is listed, one no relation reaches is not, and a wrong expectation
|
|
17
|
+
* fails.
|
|
22
18
|
*/
|
|
23
19
|
import { readdirSync, readFileSync } from "node:fs";
|
|
24
20
|
import { fileURLToPath } from "node:url";
|
|
@@ -28,26 +24,28 @@ import { describe, expect, it } from "vitest";
|
|
|
28
24
|
|
|
29
25
|
import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
30
26
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
27
|
+
import { OrganizationSchema } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
|
|
31
28
|
|
|
32
29
|
import { builtInModel, newModel } from "../model/index.js";
|
|
30
|
+
import { parseObjectRef } from "../tuples.js";
|
|
31
|
+
import type { StoreTestDocument } from "./store-test-kit.js";
|
|
32
|
+
import {
|
|
33
|
+
objectsOfType,
|
|
34
|
+
parseStoreTestDocument,
|
|
35
|
+
storeTestCases,
|
|
36
|
+
} from "./store-test-kit.js";
|
|
33
37
|
import {
|
|
34
38
|
computed,
|
|
35
|
-
declareKind,
|
|
36
39
|
direct,
|
|
40
|
+
from,
|
|
37
41
|
objectOf,
|
|
42
|
+
throwawayDeclaration,
|
|
38
43
|
union,
|
|
39
|
-
|
|
40
|
-
} from "../model/rewrite.js";
|
|
41
|
-
import type { StoreTestDocument, StoreTestSkip } from "../store-test-kit.js";
|
|
42
|
-
import { parseStoreTestDocument, storeTestCases } from "../store-test-kit.js";
|
|
44
|
+
} from "./throwaway-model.js";
|
|
43
45
|
|
|
44
|
-
const
|
|
46
|
+
const SUITES = new URL("../../../fga/tests/", import.meta.url);
|
|
45
47
|
|
|
46
|
-
/**
|
|
47
|
-
* Every document the cloud's `fga/tests/` folder holds, by file name — the
|
|
48
|
-
* fixtures folder is a byte-exact mirror of it; a file added to either
|
|
49
|
-
* without a line here fails.
|
|
50
|
-
*/
|
|
48
|
+
/** Every suite the model's folder holds, by file name. */
|
|
51
49
|
const DOCUMENTS = [
|
|
52
50
|
"agent-channel-owner.fga.yaml",
|
|
53
51
|
"agent-instance-creation.fga.yaml",
|
|
@@ -58,247 +56,117 @@ const DOCUMENTS = [
|
|
|
58
56
|
"channel-participation.fga.yaml",
|
|
59
57
|
"channel-session-visibility.fga.yaml",
|
|
60
58
|
"default-instance-inheritance.fga.yaml",
|
|
59
|
+
"identity-provider-administration.fga.yaml",
|
|
60
|
+
"invitation-administration.fga.yaml",
|
|
61
|
+
"license-issuer.fga.yaml",
|
|
61
62
|
"mcp-server-authoring.fga.yaml",
|
|
62
63
|
"memory-subject-only.fga.yaml",
|
|
63
64
|
"org-admin-owner-inheritance.fga.yaml",
|
|
64
65
|
"org-shared-environment.fga.yaml",
|
|
65
66
|
"platform-visibility.fga.yaml",
|
|
67
|
+
"plugin-owner.fga.yaml",
|
|
66
68
|
"provider-standing-viewer.fga.yaml",
|
|
67
69
|
"reserved-label-writer.fga.yaml",
|
|
68
70
|
"schedule-owner.fga.yaml",
|
|
69
71
|
"schedule-session-visibility.fga.yaml",
|
|
70
72
|
"session-personal-resource.fga.yaml",
|
|
73
|
+
"team-membership.fga.yaml",
|
|
71
74
|
"workflow-execution-sharing.fga.yaml",
|
|
72
75
|
] as const;
|
|
73
76
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const types = skip.types === undefined ? "" : ` (${skip.types.join(", ")})`;
|
|
77
|
-
return `${skip.document} :: ${skip.test} :: ${skip.subject}${relation} ${skip.object} :: ${skip.reason}${types}`;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Every assertion the kit did not run, pinned, in the kit's three
|
|
82
|
-
* reasons. `list-objects` lines are the list scope's proof and run when
|
|
83
|
-
* it lands. `undeclared-kind` lines target `platform`, a kind this
|
|
84
|
-
* edition never serves (the driver refuses it before any evaluation).
|
|
85
|
-
* `undeclared-subject-type` lines are the checks whose walk read the
|
|
86
|
-
* cloud's platform-tenancy grant, `identity_provider:<idp>#platform_user`
|
|
87
|
-
* — a userset on a kind this edition does not declare, so the answer here
|
|
88
|
-
* (nobody) is not the oracle's and is not asserted; the cloud runs the
|
|
89
|
-
* same lines over its full model at the re-pin.
|
|
90
|
-
*/
|
|
91
|
-
const SKIPPED_PINNED: ReadonlyArray<string> = [
|
|
92
|
-
// list-objects
|
|
93
|
-
"agent-channel-owner :: listing channels reflects ownership and grants, not membership :: identity_account:alice <agent_channel> :: list-objects",
|
|
94
|
-
"agent-channel-owner :: listing channels reflects ownership and grants, not membership :: identity_account:carol <agent_channel> :: list-objects",
|
|
95
|
-
"agent-channel-owner :: listing channels reflects ownership and grants, not membership :: identity_account:mallory <agent_channel> :: list-objects",
|
|
96
|
-
"agent-share-owner :: listing shares reflects ownership and grants, not membership :: identity_account:alice <agent_share> :: list-objects",
|
|
97
|
-
"agent-share-owner :: listing shares reflects ownership and grants, not membership :: identity_account:carol <agent_share> :: list-objects",
|
|
98
|
-
"agent-share-owner :: listing shares reflects ownership and grants, not membership :: identity_account:mallory <agent_share> :: list-objects",
|
|
99
|
-
"artifact-org-and-owner :: listing reflects org inheritance plus creator ownership :: identity_account:acme-guest <artifact> :: list-objects",
|
|
100
|
-
"artifact-org-and-owner :: listing reflects org inheritance plus creator ownership :: identity_account:eve <artifact> :: list-objects",
|
|
101
|
-
"artifact-org-and-owner :: listing reflects org inheritance plus creator ownership :: identity_account:victor <artifact> :: list-objects",
|
|
102
|
-
"blueprint-private-visibility :: org admin's listable agents include private ones (T08) :: identity_account:oscar <agent> :: list-objects",
|
|
103
|
-
"blueprint-private-visibility :: org member's listable agents exclude private ones :: identity_account:dave <agent> :: list-objects",
|
|
104
|
-
"blueprint-private-visibility :: org viewer's listable agents are exactly the viewer-shaped org-visible ones :: identity_account:vera <agent> :: list-objects",
|
|
105
|
-
"blueprint-private-visibility :: owner's listable agents include everything they authored :: identity_account:carol <agent> :: list-objects",
|
|
106
|
-
"channel-app-owner :: listing channel apps reflects ownership and grants, not membership :: identity_account:alice <channel_app> :: list-objects",
|
|
107
|
-
"channel-app-owner :: listing channel apps reflects ownership and grants, not membership :: identity_account:carol <channel_app> :: list-objects",
|
|
108
|
-
"channel-app-owner :: listing channel apps reflects ownership and grants, not membership :: identity_account:mallory <channel_app> :: list-objects",
|
|
109
|
-
"channel-participation :: list viewable and participable channels resolve per grant (drives listConversations) :: identity_account:carol <agent_channel> :: list-objects",
|
|
110
|
-
"channel-participation :: list viewable and participable channels resolve per grant (drives listConversations) :: identity_account:dave <agent_channel> :: list-objects",
|
|
111
|
-
"channel-participation :: list viewable and participable channels resolve per grant (drives listConversations) :: identity_account:trina <agent_channel> :: list-objects",
|
|
112
|
-
"channel-session-visibility :: list viewable sessions resolves the channel chain (drives listByChannel) :: identity_account:alice <session> :: list-objects",
|
|
113
|
-
"channel-session-visibility :: list viewable sessions resolves the channel chain (drives listByChannel) :: identity_account:bob <session> :: list-objects",
|
|
114
|
-
"channel-session-visibility :: list viewable sessions resolves the channel chain (drives listByChannel) :: identity_account:carol <session> :: list-objects",
|
|
115
|
-
"default-instance-inheritance :: list managed-org member's viewable agent instances resolves the default_of chain :: identity_account:alice <agent_instance> :: list-objects",
|
|
116
|
-
"memory-subject-only :: list-objects returns only the subject's own records :: identity_account:alice <memory> :: list-objects",
|
|
117
|
-
"memory-subject-only :: list-objects returns only the subject's own records :: identity_account:mallory <memory> :: list-objects",
|
|
118
|
-
"org-admin-owner-inheritance :: admin's listable resources include private blueprints, never personal kinds :: identity_account:ana <agent> :: list-objects",
|
|
119
|
-
"org-admin-owner-inheritance :: admin's listable resources include private blueprints, never personal kinds :: identity_account:ana <agent_instance> :: list-objects",
|
|
120
|
-
"org-admin-owner-inheritance :: admin's listable resources include private blueprints, never personal kinds :: identity_account:ana <session> :: list-objects",
|
|
121
|
-
"org-shared-environment :: org-members-can-list-shared-environments-only :: identity_account:dave <environment> :: list-objects",
|
|
122
|
-
"platform-visibility :: list managed-org member's viewable agents includes only the shared one :: identity_account:alice <agent> :: list-objects",
|
|
123
|
-
"schedule-owner :: listing schedules reflects ownership and grants, not membership :: identity_account:alice <schedule> :: list-objects",
|
|
124
|
-
"schedule-owner :: listing schedules reflects ownership and grants, not membership :: identity_account:carol <schedule> :: list-objects",
|
|
125
|
-
"schedule-owner :: listing schedules reflects ownership and grants, not membership :: identity_account:mallory <schedule> :: list-objects",
|
|
126
|
-
"schedule-session-visibility :: list viewable sessions resolves the schedule chain :: identity_account:alice <session> :: list-objects",
|
|
127
|
-
"schedule-session-visibility :: list viewable sessions resolves the schedule chain :: identity_account:bob <session> :: list-objects",
|
|
128
|
-
"schedule-session-visibility :: list viewable sessions resolves the schedule chain :: identity_account:carol <session> :: list-objects",
|
|
129
|
-
"workflow-execution-sharing :: list a teammate's viewable runs reflects both sharing axes :: identity_account:bob <workflow_execution> :: list-objects",
|
|
130
|
-
// undeclared-kind: platform
|
|
131
|
-
"provider-standing-viewer :: operator may view provider standing, tenants may not :: identity_account:mallory platform:stigmer :: undeclared-kind",
|
|
132
|
-
"provider-standing-viewer :: operator may view provider standing, tenants may not :: identity_account:opal platform:stigmer :: undeclared-kind",
|
|
133
|
-
"reserved-label-writer :: operator may write reserved labels, tenants may not :: identity_account:machine platform:stigmer :: undeclared-kind",
|
|
134
|
-
"reserved-label-writer :: operator may write reserved labels, tenants may not :: identity_account:mallory platform:stigmer :: undeclared-kind",
|
|
135
|
-
// undeclared-subject-type: identity_provider#platform_user
|
|
136
|
-
"agent-instance-creation :: the tenant-family case, the agent half from platform_viewer and the organization half at home :: identity_account:mia can_create_instance agent:open-bot :: undeclared-subject-type (identity_provider)",
|
|
137
|
-
"default-instance-inheritance :: managed-org member reaches the default instance of a platform-shared agent :: identity_account:alice can_execute agent_instance:sara-default :: undeclared-subject-type (identity_provider)",
|
|
138
|
-
"default-instance-inheritance :: managed-org member reaches the default instance of a platform-shared agent :: identity_account:alice can_view agent_instance:sara-default :: undeclared-subject-type (identity_provider)",
|
|
139
|
-
"default-instance-inheritance :: managed-org member reaches the default instance of a platform-shared workflow :: identity_account:alice can_execute workflow_instance:onboarding-default :: undeclared-subject-type (identity_provider)",
|
|
140
|
-
"default-instance-inheritance :: managed-org member reaches the default instance of a platform-shared workflow :: identity_account:alice can_view workflow_instance:onboarding-default :: undeclared-subject-type (identity_provider)",
|
|
141
|
-
"default-instance-inheritance :: outsiders get nothing from the default_of link :: identity_account:mallory can_execute agent_instance:sara-default :: undeclared-subject-type (identity_provider)",
|
|
142
|
-
"default-instance-inheritance :: outsiders get nothing from the default_of link :: identity_account:mallory can_view agent_instance:sara-default :: undeclared-subject-type (identity_provider)",
|
|
143
|
-
"platform-visibility :: managed-org members can view and execute a platform-shared agent :: identity_account:alice can_create_instance agent:sara :: undeclared-subject-type (identity_provider)",
|
|
144
|
-
"platform-visibility :: managed-org members can view and execute a platform-shared agent :: identity_account:alice can_execute agent:sara :: undeclared-subject-type (identity_provider)",
|
|
145
|
-
"platform-visibility :: managed-org members can view and execute a platform-shared agent :: identity_account:alice can_view agent:sara :: undeclared-subject-type (identity_provider)",
|
|
146
|
-
"platform-visibility :: managed-org members can view and execute a platform-shared agent :: identity_account:bob can_execute agent:sara :: undeclared-subject-type (identity_provider)",
|
|
147
|
-
"platform-visibility :: managed-org members can view and execute a platform-shared agent :: identity_account:bob can_view agent:sara :: undeclared-subject-type (identity_provider)",
|
|
148
|
-
"platform-visibility :: outsiders and unrelated orgs get nothing from platform visibility :: identity_account:mallory can_create_instance agent:sara :: undeclared-subject-type (identity_provider)",
|
|
149
|
-
"platform-visibility :: outsiders and unrelated orgs get nothing from platform visibility :: identity_account:mallory can_execute agent:sara :: undeclared-subject-type (identity_provider)",
|
|
150
|
-
"platform-visibility :: outsiders and unrelated orgs get nothing from platform visibility :: identity_account:mallory can_view agent:sara :: undeclared-subject-type (identity_provider)",
|
|
151
|
-
"platform-visibility :: platform-shared mcp_server is usable by managed-org members :: identity_account:alice can_connect mcp_server:github :: undeclared-subject-type (identity_provider)",
|
|
152
|
-
"platform-visibility :: platform-shared mcp_server is usable by managed-org members :: identity_account:alice can_use mcp_server:github :: undeclared-subject-type (identity_provider)",
|
|
153
|
-
"platform-visibility :: platform-shared mcp_server is usable by managed-org members :: identity_account:alice can_view mcp_server:github :: undeclared-subject-type (identity_provider)",
|
|
154
|
-
"platform-visibility :: platform-shared skill is usable by managed-org members :: identity_account:alice can_use skill:research :: undeclared-subject-type (identity_provider)",
|
|
155
|
-
"platform-visibility :: platform-shared skill is usable by managed-org members :: identity_account:alice can_view skill:research :: undeclared-subject-type (identity_provider)",
|
|
156
|
-
"platform-visibility :: platform-shared workflow is viewable and executable by managed-org members :: identity_account:alice can_execute workflow:onboarding :: undeclared-subject-type (identity_provider)",
|
|
157
|
-
"platform-visibility :: platform-shared workflow is viewable and executable by managed-org members :: identity_account:alice can_view workflow:onboarding :: undeclared-subject-type (identity_provider)",
|
|
158
|
-
];
|
|
159
|
-
|
|
160
|
-
describe("the cloud's OpenFGA store tests over the built-in evaluator", () => {
|
|
161
|
-
it("the fixtures folder holds exactly the documents this file pins", () => {
|
|
77
|
+
describe("the model's suites over the built-in evaluator", () => {
|
|
78
|
+
it("the suites folder holds exactly the documents this file pins", () => {
|
|
162
79
|
expect(
|
|
163
|
-
readdirSync(
|
|
80
|
+
readdirSync(SUITES)
|
|
164
81
|
.filter((name) => name.endsWith(".fga.yaml"))
|
|
165
82
|
.sort(),
|
|
166
83
|
).toEqual([...DOCUMENTS]);
|
|
167
84
|
});
|
|
168
85
|
|
|
169
|
-
const
|
|
86
|
+
for (const name of DOCUMENTS) {
|
|
170
87
|
const document = parseStoreTestDocument(
|
|
171
|
-
yaml.load(readFileSync(fileURLToPath(new URL(name,
|
|
88
|
+
yaml.load(readFileSync(fileURLToPath(new URL(name, SUITES)), "utf8")),
|
|
172
89
|
name,
|
|
173
90
|
);
|
|
174
|
-
return { document, kit: storeTestCases(document, builtInModel) };
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
for (const { document, kit } of kits) {
|
|
178
|
-
if (kit.cases.length === 0) {
|
|
179
|
-
// Every check targets a kind this edition does not declare (the
|
|
180
|
-
// platform documents); reported as skipped, pinned below.
|
|
181
|
-
it.skip(`${document.name}: no check targets a declared kind`, () => {});
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
91
|
describe(document.name, () => {
|
|
185
|
-
for (const storeTestCase of
|
|
92
|
+
for (const storeTestCase of storeTestCases(document, builtInModel)) {
|
|
186
93
|
it(storeTestCase.name, storeTestCase.run);
|
|
187
94
|
}
|
|
188
95
|
});
|
|
189
96
|
}
|
|
190
|
-
|
|
191
|
-
// Declared after every case: a kit's `skipped` is complete once its
|
|
192
|
-
// cases have run (the walk-dependent reason is decided per assertion).
|
|
193
|
-
it("reports every assertion it did not run, and the list is the pinned one", () => {
|
|
194
|
-
const skipped = kits.flatMap(({ kit }) => kit.skipped.map(skipLine));
|
|
195
|
-
expect(skipped.sort()).toEqual([...SKIPPED_PINNED].sort());
|
|
196
|
-
});
|
|
197
97
|
});
|
|
198
98
|
|
|
199
99
|
/**
|
|
200
|
-
* The kit's
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
* platform-tenancy shape — so each arm below settles at a different point
|
|
204
|
-
* of the union.
|
|
100
|
+
* The kit's `list_objects` rule over a throwaway model: an agent whose
|
|
101
|
+
* `owner` is a direct person or an admin of its organization, reached
|
|
102
|
+
* through the `organization` tupleset link.
|
|
205
103
|
*/
|
|
206
|
-
describe("the kit
|
|
104
|
+
describe("the kit lists exactly the objects a relation reaches", () => {
|
|
207
105
|
const model = newModel([
|
|
208
|
-
|
|
106
|
+
throwawayDeclaration({
|
|
107
|
+
kind: ApiResourceKind.organization,
|
|
108
|
+
schema: OrganizationSchema,
|
|
109
|
+
relations: [["admin", direct(objectOf("identity_account"))]],
|
|
110
|
+
}),
|
|
111
|
+
throwawayDeclaration({
|
|
209
112
|
kind: ApiResourceKind.agent,
|
|
210
113
|
schema: AgentSchema,
|
|
211
|
-
source: "fga/model/agentic/agent.fga",
|
|
212
114
|
relations: [
|
|
213
|
-
["
|
|
214
|
-
[
|
|
215
|
-
"platform_viewer",
|
|
216
|
-
direct(usersetOf("identity_provider", "platform_user")),
|
|
217
|
-
],
|
|
218
|
-
[
|
|
219
|
-
"viewer",
|
|
220
|
-
union(
|
|
221
|
-
direct(objectOf("identity_account")),
|
|
222
|
-
computed("owner"),
|
|
223
|
-
computed("platform_viewer"),
|
|
224
|
-
),
|
|
225
|
-
],
|
|
226
|
-
["can_view", computed("viewer")],
|
|
115
|
+
["organization", direct(objectOf("organization"))],
|
|
116
|
+
["owner", union(direct(objectOf("identity_account")), from("admin", "organization"))],
|
|
227
117
|
["can_edit", computed("owner")],
|
|
228
118
|
],
|
|
229
119
|
}),
|
|
230
120
|
]);
|
|
231
121
|
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
relation: "platform_viewer",
|
|
239
|
-
object: "agent:a",
|
|
240
|
-
},
|
|
241
|
-
{ user: "identity_account:carol", relation: "owner", object: "agent:b" },
|
|
242
|
-
],
|
|
243
|
-
tests: [
|
|
244
|
-
{
|
|
245
|
-
name: "arms",
|
|
246
|
-
check: [
|
|
247
|
-
// The owner: `viewer` settles on the direct line; `can_edit` never reads platform_viewer.
|
|
248
|
-
{
|
|
249
|
-
user: "identity_account:carol",
|
|
250
|
-
object: "agent:a",
|
|
251
|
-
assertions: { can_view: true, can_edit: true },
|
|
252
|
-
},
|
|
253
|
-
// A stranger: the union walks to platform_viewer and reads the undeclared userset.
|
|
254
|
-
{
|
|
255
|
-
user: "identity_account:mallory",
|
|
256
|
-
object: "agent:a",
|
|
257
|
-
assertions: { can_view: false, can_edit: false },
|
|
258
|
-
},
|
|
259
|
-
// No such tuple on this object: the same stranger's walk reads nothing undeclared.
|
|
260
|
-
{
|
|
261
|
-
user: "identity_account:mallory",
|
|
262
|
-
object: "agent:b",
|
|
263
|
-
assertions: { can_view: false },
|
|
264
|
-
},
|
|
265
|
-
],
|
|
266
|
-
list_objects: [],
|
|
267
|
-
},
|
|
268
|
-
],
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
it("runs the arms the walk settles before the undeclared type, skips exactly the one that read it — whatever it would have answered", async () => {
|
|
272
|
-
const kit = storeTestCases(document, model);
|
|
273
|
-
expect(kit.skipped).toEqual([]);
|
|
274
|
-
for (const storeTestCase of kit.cases) {
|
|
275
|
-
await storeTestCase.run();
|
|
276
|
-
}
|
|
277
|
-
expect(kit.skipped.map(skipLine)).toEqual([
|
|
278
|
-
"throwaway :: arms :: identity_account:mallory can_view agent:a :: undeclared-subject-type (identity_provider)",
|
|
279
|
-
]);
|
|
280
|
-
});
|
|
122
|
+
const tuples = [
|
|
123
|
+
{ user: "identity_account:ada", relation: "owner", object: "agent:mine" },
|
|
124
|
+
{ user: "organization:acme", relation: "organization", object: "agent:org" },
|
|
125
|
+
{ user: "identity_account:ada", relation: "admin", object: "organization:acme" },
|
|
126
|
+
{ user: "organization:other", relation: "organization", object: "agent:elsewhere" },
|
|
127
|
+
];
|
|
281
128
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
129
|
+
function documentExpecting(expected: ReadonlyArray<string>): StoreTestDocument {
|
|
130
|
+
return {
|
|
131
|
+
name: "throwaway",
|
|
132
|
+
tuples,
|
|
285
133
|
tests: [
|
|
286
134
|
{
|
|
287
|
-
name: "
|
|
288
|
-
check: [
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
object: "agent:a",
|
|
292
|
-
assertions: { can_edit: false },
|
|
293
|
-
},
|
|
135
|
+
name: "listing",
|
|
136
|
+
check: [],
|
|
137
|
+
list_objects: [
|
|
138
|
+
{ user: "identity_account:ada", type: "agent", assertions: { can_edit: expected } },
|
|
294
139
|
],
|
|
295
|
-
list_objects: [],
|
|
296
140
|
},
|
|
297
141
|
],
|
|
298
142
|
};
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
it("takes as candidates every object of the type a tuple is written on", () => {
|
|
146
|
+
expect(
|
|
147
|
+
objectsOfType(
|
|
148
|
+
tuples.map((tuple) => ({
|
|
149
|
+
object: parseObjectRef(tuple.object),
|
|
150
|
+
relation: tuple.relation,
|
|
151
|
+
subject: { form: "object" as const, object: parseObjectRef(tuple.user) },
|
|
152
|
+
})),
|
|
153
|
+
"agent",
|
|
154
|
+
),
|
|
155
|
+
).toEqual(["agent:mine", "agent:org", "agent:elsewhere"]);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it("lists the direct object and the one reached through the tupleset, in any order", async () => {
|
|
159
|
+
const [listing] = storeTestCases(documentExpecting(["agent:org", "agent:mine"]), model);
|
|
160
|
+
await expect(listing?.run()).resolves.toBeUndefined();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("fails an expectation that lists an object no relation reaches, or misses one", async () => {
|
|
164
|
+
const [extra] = storeTestCases(
|
|
165
|
+
documentExpecting(["agent:mine", "agent:org", "agent:elsewhere"]),
|
|
166
|
+
model,
|
|
302
167
|
);
|
|
168
|
+
await expect(extra?.run()).rejects.toThrow("identity_account:ada can_edit objects of type agent");
|
|
169
|
+
const [missing] = storeTestCases(documentExpecting(["agent:mine"]), model);
|
|
170
|
+
await expect(missing?.run()).rejects.toThrow("identity_account:ada can_edit objects of type agent");
|
|
303
171
|
});
|
|
304
172
|
});
|