@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
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
* parent), which is what lets the parity pin run over all of them.
|
|
16
16
|
* `spec` and `status` overrides sit on top for the arms that need a
|
|
17
17
|
* pointer or a level set.
|
|
18
|
+
*
|
|
19
|
+
* A fixture row exists only for a kind this edition stores rows of: every
|
|
20
|
+
* type of the model but a rowless one (`platform`, model/bindings.ts), so
|
|
21
|
+
* the builders take a StoredKindDeclaration and `storedDeclaration`
|
|
22
|
+
* refuses a rowless type by name.
|
|
18
23
|
*/
|
|
19
24
|
import { create } from "@bufbuild/protobuf";
|
|
20
25
|
import type { DescMessage, Message } from "@bufbuild/protobuf";
|
|
@@ -26,6 +31,7 @@ import {
|
|
|
26
31
|
getKindMeta,
|
|
27
32
|
kindEnumName,
|
|
28
33
|
} from "../../pipeline/apiresource-meta.js";
|
|
34
|
+
import { builtInModel } from "../model/index.js";
|
|
29
35
|
import type { KindDeclaration } from "../model/rewrite.js";
|
|
30
36
|
|
|
31
37
|
export interface FixtureRowFacts {
|
|
@@ -39,6 +45,28 @@ export interface FixtureRowFacts {
|
|
|
39
45
|
readonly status?: Readonly<Record<string, unknown>>;
|
|
40
46
|
}
|
|
41
47
|
|
|
48
|
+
/** A declaration of a kind with stored rows: what a fixture row is built, saved and decoded with. */
|
|
49
|
+
export type StoredKindDeclaration = KindDeclaration & { readonly schema: DescMessage };
|
|
50
|
+
|
|
51
|
+
/** Whether rows of the declaration's kind are stored: every type but a rowless one. */
|
|
52
|
+
export function hasStoredRows(
|
|
53
|
+
declaration: KindDeclaration,
|
|
54
|
+
): declaration is StoredKindDeclaration {
|
|
55
|
+
return declaration.schema !== undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The built-in model's declaration of `type`, which must be a kind with stored rows. */
|
|
59
|
+
export function storedDeclaration(type: string): StoredKindDeclaration {
|
|
60
|
+
const declaration = builtInModel.byType(type);
|
|
61
|
+
if (declaration === undefined) {
|
|
62
|
+
throw new Error(`${type} is not a type of the model`);
|
|
63
|
+
}
|
|
64
|
+
if (!hasStoredRows(declaration)) {
|
|
65
|
+
throw new Error(`${type} is rowless: no row of it is stored`);
|
|
66
|
+
}
|
|
67
|
+
return declaration;
|
|
68
|
+
}
|
|
69
|
+
|
|
42
70
|
/** The id the fixture gives the parent of `parentKind`: `<type>-1`. */
|
|
43
71
|
export function fixtureParentId(parentKind: string): string {
|
|
44
72
|
return `${kindEnumName(getKindEnum(parentKind))}-1`;
|
|
@@ -46,7 +74,7 @@ export function fixtureParentId(parentKind: string): string {
|
|
|
46
74
|
|
|
47
75
|
/** A row of `declaration.kind` carrying exactly the given facts, its parents filled. */
|
|
48
76
|
export function fixtureRow(
|
|
49
|
-
declaration:
|
|
77
|
+
declaration: StoredKindDeclaration,
|
|
50
78
|
facts: FixtureRowFacts,
|
|
51
79
|
): Message {
|
|
52
80
|
// Typed loosely on purpose: the schema is any declared kind's, and
|
|
@@ -70,7 +98,7 @@ export function fixtureRow(
|
|
|
70
98
|
}
|
|
71
99
|
|
|
72
100
|
/** `kind_meta`'s parent spec fields for the kind, by generated name, each naming `<parentKind>-1`. */
|
|
73
|
-
function parentFieldsOf(declaration:
|
|
101
|
+
function parentFieldsOf(declaration: StoredKindDeclaration): Record<string, string> {
|
|
74
102
|
const config = getKindMeta(declaration.kind).authorization;
|
|
75
103
|
const spec = specSchemaOf(declaration.schema);
|
|
76
104
|
const fields: Record<string, string> = {};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builders for tests' throwaway models: a declaration written by hand in
|
|
3
|
+
* the evaluator's vocabulary (model/rewrite.ts), for the shapes the one
|
|
4
|
+
* real model must never hold (a cycle, a chain past the depth bound) and
|
|
5
|
+
* for proving a mechanism over a model small enough to read in the test.
|
|
6
|
+
* The built-in model is never written this way: it is the compiled
|
|
7
|
+
* OpenFGA JSON, read by model/openfga-json.ts.
|
|
8
|
+
*
|
|
9
|
+
* Nothing here validates: a test that declares a cycle means to. The
|
|
10
|
+
* builders are named so a declaration reads like the `.fga` line it
|
|
11
|
+
* stands for:
|
|
12
|
+
*
|
|
13
|
+
* define viewer: [identity_account, organization#viewer] or owner
|
|
14
|
+
* → union(direct(objectOf("identity_account"), usersetOf("organization", "viewer")), computed("owner"))
|
|
15
|
+
*/
|
|
16
|
+
import type { DescMessage } from "@bufbuild/protobuf";
|
|
17
|
+
|
|
18
|
+
import type { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
19
|
+
|
|
20
|
+
import { kindEnumName } from "../../pipeline/apiresource-meta.js";
|
|
21
|
+
import type {
|
|
22
|
+
DerivedRelation,
|
|
23
|
+
KindDeclaration,
|
|
24
|
+
Rewrite,
|
|
25
|
+
SubjectType,
|
|
26
|
+
} from "../model/rewrite.js";
|
|
27
|
+
|
|
28
|
+
export function objectOf(type: string): SubjectType {
|
|
29
|
+
return { form: "object", type };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function usersetOf(type: string, relation: string): SubjectType {
|
|
33
|
+
return { form: "userset", type, relation };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function direct(...subjects: ReadonlyArray<SubjectType>): Rewrite {
|
|
37
|
+
return { node: "this", subjects };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function computed(relation: string): Rewrite {
|
|
41
|
+
return { node: "computed", relation };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function from(relation: string, tupleset: string): Rewrite {
|
|
45
|
+
return { node: "from", relation, tupleset };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function union(...members: ReadonlyArray<Rewrite>): Rewrite {
|
|
49
|
+
return { node: "union", members };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function intersection(...members: ReadonlyArray<Rewrite>): Rewrite {
|
|
53
|
+
return { node: "intersection", members };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface ThrowawayDeclarationInput {
|
|
57
|
+
readonly kind: ApiResourceKind;
|
|
58
|
+
/** Omitted for a rowless type. */
|
|
59
|
+
readonly schema?: DescMessage;
|
|
60
|
+
readonly source?: string;
|
|
61
|
+
readonly relations: ReadonlyArray<readonly [name: string, rewrite: Rewrite]>;
|
|
62
|
+
readonly derived?: ReadonlyArray<readonly [name: string, rule: DerivedRelation]>;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** A declaration exactly as written, for `newModel`. */
|
|
66
|
+
export function throwawayDeclaration(input: ThrowawayDeclarationInput): KindDeclaration {
|
|
67
|
+
return {
|
|
68
|
+
kind: input.kind,
|
|
69
|
+
type: kindEnumName(input.kind),
|
|
70
|
+
schema: input.schema,
|
|
71
|
+
source: input.source ?? "throwaway",
|
|
72
|
+
relations: new Map(input.relations),
|
|
73
|
+
derived: new Map(input.derived ?? []),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -1,83 +1,136 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Pins
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
2
|
+
* Pins the contract to the model over the whole wire: every question an
|
|
3
|
+
* RPC annotation asks the authorization model is a relation the model
|
|
4
|
+
* defines. The model (fga/model) and the contract (apis/, reaching this
|
|
5
|
+
* package as `@stigmer/protos`) are both this repository's, so their
|
|
6
|
+
* agreement is this repository's to keep, whichever edition serves the
|
|
7
|
+
* RPC. That is why the scope here is every service in `@stigmer/protos`,
|
|
8
|
+
* where authorize-annotation-completeness.test.ts scopes itself to the
|
|
9
|
+
* methods open source serves: that test governs the Authorize step, and a
|
|
10
|
+
* composition's own services are its own conformance suite's business;
|
|
11
|
+
* this one asks whether the model and the contract say the same thing.
|
|
11
12
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
13
|
+
* Two invariants:
|
|
14
|
+
* - every `rpc.config` annotation that names a static kind asks a
|
|
15
|
+
* permission that kind's type defines. A relation the type does not
|
|
16
|
+
* define answers `deny` in the built-in evaluator and a validation
|
|
17
|
+
* error in OpenFGA, so such an RPC refuses every caller, dark until
|
|
18
|
+
* someone runs it under enforcement;
|
|
19
|
+
* - the permission of every `resource_kind_path` lane (the IamPolicy
|
|
20
|
+
* lanes, whose kind is the request's) is defined on every kind whose
|
|
21
|
+
* `kind_meta` lists grantable roles, because each such kind is one the
|
|
22
|
+
* grant path admits.
|
|
23
|
+
*
|
|
24
|
+
* Outside the pin, as in the annotation-completeness precedent: `is_public`
|
|
25
|
+
* and `is_skip_authorization` methods (their annotation is never
|
|
26
|
+
* resolved), and a `config` that names no kind (`IamPolicyQueryController.get`,
|
|
27
|
+
* which authorizes the loaded policy's resource).
|
|
28
|
+
*
|
|
29
|
+
* What does not hold today is pinned, never dropped: `KNOWN_GAPS` lists
|
|
30
|
+
* each disagreement, and the test requires the computed list to equal it,
|
|
31
|
+
* so a fix to the model or the contract fails here until its line is
|
|
32
|
+
* removed.
|
|
33
|
+
*
|
|
34
|
+
* The walk reads the stubs' built `dist`. `make test-server` builds them
|
|
35
|
+
* first; a bare `vitest` run in a checkout whose `dist` is stale reads
|
|
36
|
+
* services the contract no longer has.
|
|
21
37
|
*/
|
|
22
|
-
import {
|
|
23
|
-
import {
|
|
38
|
+
import { readdirSync } from "node:fs";
|
|
39
|
+
import { createRequire } from "node:module";
|
|
24
40
|
import path from "node:path";
|
|
25
41
|
|
|
42
|
+
import type { DescService } from "@bufbuild/protobuf";
|
|
26
43
|
import { getOption, hasOption } from "@bufbuild/protobuf";
|
|
27
|
-
import {
|
|
44
|
+
import { beforeAll, describe, expect, it } from "vitest";
|
|
28
45
|
|
|
29
|
-
import {
|
|
46
|
+
import {
|
|
47
|
+
ApiResourceKind,
|
|
48
|
+
ApiResourceKindSchema,
|
|
49
|
+
} from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
30
50
|
import {
|
|
31
51
|
config as rpcAuthorizationConfig,
|
|
32
52
|
is_public,
|
|
33
53
|
is_skip_authorization,
|
|
34
54
|
} from "@stigmer/protos/ai/stigmer/commons/rpc/method_options_pb";
|
|
35
55
|
import { IamPermission } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
|
|
36
|
-
import { ServerEdition } from "@stigmer/protos/ai/stigmer/platform/v1/server_info_pb";
|
|
37
56
|
|
|
38
|
-
import {
|
|
39
|
-
import {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
57
|
+
import { grantableRolesFor, kindEnumName } from "../../pipeline/apiresource-meta.js";
|
|
58
|
+
import { builtInModel } from "../model/index.js";
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Today's disagreements between the contract and the model, each with its
|
|
62
|
+
* reason. Removing a gap from the model or the contract fails the test
|
|
63
|
+
* until its line is removed here.
|
|
64
|
+
*/
|
|
65
|
+
const KNOWN_GAPS: ReadonlyArray<string> = [
|
|
66
|
+
// Contract ahead of model, on purpose: Plan's proto says the relation
|
|
67
|
+
// lands in the model with the entry that serves the kind
|
|
68
|
+
// (apis/ai/stigmer/billing/plan/v1/command.proto), and no edition serves
|
|
69
|
+
// Plan yet.
|
|
70
|
+
"ai.stigmer.billing.plan.v1.PlanCommandController/create asks can_manage_plans on platform",
|
|
71
|
+
"ai.stigmer.billing.plan.v1.PlanCommandController/retire asks can_manage_plans on platform",
|
|
72
|
+
// Artifacts are grantable in kind_meta, but the model defines no access
|
|
73
|
+
// relations on them: https://github.com/stigmer/stigmer/issues/1268.
|
|
74
|
+
"the resource_kind_path lanes ask can_grant_access on artifact",
|
|
75
|
+
"the resource_kind_path lanes ask can_view_access on artifact",
|
|
76
|
+
];
|
|
51
77
|
|
|
52
|
-
/** One annotated
|
|
78
|
+
/** One annotated method: the question it asks the model. */
|
|
53
79
|
interface WireQuestion {
|
|
54
80
|
readonly method: string;
|
|
81
|
+
/** The static kind, or unknown when the kind is the request's (`kindPath`). */
|
|
55
82
|
readonly kind: ApiResourceKind;
|
|
83
|
+
readonly kindPath: string;
|
|
56
84
|
readonly permission: string;
|
|
57
85
|
}
|
|
58
86
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
87
|
+
/** Every service descriptor the stubs package exports, loaded through its own specifiers. */
|
|
88
|
+
async function everyService(): Promise<ReadonlyArray<DescService>> {
|
|
89
|
+
const require = createRequire(import.meta.url);
|
|
90
|
+
const anchor = "ai/stigmer/iam/v1/enum_pb";
|
|
91
|
+
const resolved = require.resolve(`@stigmer/protos/${anchor}`);
|
|
92
|
+
const dist = resolved.slice(0, resolved.length - `${anchor}.js`.length);
|
|
93
|
+
const modules = readdirSync(path.join(dist, "ai"), { recursive: true, encoding: "utf8" })
|
|
94
|
+
.filter((file) => file.endsWith("_pb.js"))
|
|
95
|
+
.map((file) => `ai/${file.slice(0, -".js".length).split(path.sep).join("/")}`)
|
|
96
|
+
.sort();
|
|
97
|
+
const services: DescService[] = [];
|
|
98
|
+
for (const specifier of modules) {
|
|
99
|
+
const loaded: Record<string, unknown> = await import(`@stigmer/protos/${specifier}`);
|
|
100
|
+
for (const value of Object.values(loaded)) {
|
|
101
|
+
if (isService(value)) {
|
|
102
|
+
services.push(value);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return services;
|
|
107
|
+
}
|
|
62
108
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
109
|
+
function isService(value: unknown): value is DescService {
|
|
110
|
+
return (
|
|
111
|
+
typeof value === "object" &&
|
|
112
|
+
value !== null &&
|
|
113
|
+
"kind" in value &&
|
|
114
|
+
value.kind === "service" &&
|
|
115
|
+
"methods" in value
|
|
116
|
+
);
|
|
117
|
+
}
|
|
72
118
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
119
|
+
/** The model's relations on a kind, or none when the model does not define it. */
|
|
120
|
+
function relationsOf(kind: ApiResourceKind): ReadonlySet<string> {
|
|
121
|
+
return new Set(builtInModel.byKind(kind)?.relations.keys() ?? []);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
describe("the contract asks the model only what the model defines", () => {
|
|
125
|
+
let services: ReadonlyArray<DescService>;
|
|
126
|
+
|
|
127
|
+
beforeAll(async () => {
|
|
128
|
+
services = await everyService();
|
|
76
129
|
});
|
|
77
130
|
|
|
78
|
-
function
|
|
79
|
-
const
|
|
80
|
-
for (const service of
|
|
131
|
+
function annotated(): ReadonlyArray<WireQuestion> {
|
|
132
|
+
const out: WireQuestion[] = [];
|
|
133
|
+
for (const service of services) {
|
|
81
134
|
for (const method of service.methods) {
|
|
82
135
|
if (
|
|
83
136
|
!hasOption(method, rpcAuthorizationConfig) ||
|
|
@@ -87,68 +140,49 @@ describe("every static (kind, permission) the served RPCs ask about is a declare
|
|
|
87
140
|
continue;
|
|
88
141
|
}
|
|
89
142
|
const config = getOption(method, rpcAuthorizationConfig);
|
|
90
|
-
|
|
91
|
-
config.resourceKind === ApiResourceKind.api_resource_kind_unknown ||
|
|
92
|
-
!kindServedByEdition(config.resourceKind, ServerEdition.oss)
|
|
93
|
-
) {
|
|
94
|
-
continue;
|
|
95
|
-
}
|
|
96
|
-
questions.push({
|
|
143
|
+
out.push({
|
|
97
144
|
method: `${service.typeName}/${method.name}`,
|
|
98
145
|
kind: config.resourceKind,
|
|
146
|
+
kindPath: config.resourceKindPath,
|
|
99
147
|
permission: IamPermission[config.permission] ?? "",
|
|
100
148
|
});
|
|
101
149
|
}
|
|
102
150
|
}
|
|
103
|
-
return
|
|
151
|
+
return out;
|
|
104
152
|
}
|
|
105
153
|
|
|
106
|
-
it("
|
|
107
|
-
const questions =
|
|
108
|
-
expect(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
(q) => declarationFor(q.kind)?.relations.has(q.permission) !== true,
|
|
112
|
-
)
|
|
113
|
-
.map(
|
|
114
|
-
(q) =>
|
|
115
|
-
`${q.method} asks ${q.permission} on ${kindEnumName(q.kind)}, which its transcript does not declare`,
|
|
116
|
-
);
|
|
117
|
-
expect(undeclared).toEqual([]);
|
|
154
|
+
it("reads the whole contract, the IamPolicy lanes included", () => {
|
|
155
|
+
const questions = annotated();
|
|
156
|
+
expect(services.length).toBeGreaterThan(60);
|
|
157
|
+
expect(questions.length).toBeGreaterThan(150);
|
|
158
|
+
expect(questions.some((q) => q.kindPath !== "")).toBe(true);
|
|
118
159
|
});
|
|
119
160
|
|
|
120
|
-
it("
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
"agent_share",
|
|
133
|
-
"api_key",
|
|
134
|
-
"artifact",
|
|
135
|
-
"channel_app",
|
|
136
|
-
"environment",
|
|
137
|
-
"execution_context",
|
|
138
|
-
"identity_account",
|
|
139
|
-
"mcp_server",
|
|
140
|
-
"memory",
|
|
141
|
-
"oauth_app",
|
|
142
|
-
"organization",
|
|
143
|
-
"platform_client",
|
|
144
|
-
"plugin",
|
|
145
|
-
"schedule",
|
|
146
|
-
"session",
|
|
147
|
-
"skill",
|
|
148
|
-
"workflow",
|
|
149
|
-
"workflow_execution",
|
|
150
|
-
"workflow_instance",
|
|
151
|
-
].sort(),
|
|
161
|
+
it("defines every relation the contract asks about, except the known gaps", () => {
|
|
162
|
+
const questions = annotated();
|
|
163
|
+
const gaps: string[] = [];
|
|
164
|
+
|
|
165
|
+
for (const q of questions) {
|
|
166
|
+
if (q.kind !== ApiResourceKind.api_resource_kind_unknown && !relationsOf(q.kind).has(q.permission)) {
|
|
167
|
+
gaps.push(`${q.method} asks ${q.permission} on ${kindEnumName(q.kind)}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const pathPermissions = new Set(
|
|
172
|
+
questions.filter((q) => q.kindPath !== "").map((q) => q.permission),
|
|
152
173
|
);
|
|
174
|
+
for (const value of ApiResourceKindSchema.values) {
|
|
175
|
+
const kind = value.number as ApiResourceKind;
|
|
176
|
+
if (kind === ApiResourceKind.api_resource_kind_unknown || grantableRolesFor(kind).length === 0) {
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
for (const permission of [...pathPermissions].sort()) {
|
|
180
|
+
if (!relationsOf(kind).has(permission)) {
|
|
181
|
+
gaps.push(`the resource_kind_path lanes ask ${permission} on ${kindEnumName(kind)}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
expect(gaps.sort()).toEqual([...KNOWN_GAPS].sort());
|
|
153
187
|
});
|
|
154
188
|
});
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* tuples this row would have had, derived on the fly (evaluator.ts over
|
|
6
6
|
* derived-tuples.ts). Every arm below mirrors the cloud's driver
|
|
7
7
|
* (stigmer-cloud src/authorizer/fga-authorizer.ts) by name and copy, so
|
|
8
|
-
* the two editions answer one question the same way
|
|
9
|
-
*
|
|
8
|
+
* the two editions answer one question the same way, over the one model
|
|
9
|
+
* both read (model/index.ts).
|
|
10
10
|
*
|
|
11
11
|
* The arms, in order:
|
|
12
12
|
* 1. The cloud's four pre-checks with the Java service's copy, each a
|
|
@@ -47,8 +47,10 @@
|
|
|
47
47
|
*
|
|
48
48
|
* Cost, per check (measured at slice 4): the target row (1), the caller's
|
|
49
49
|
* account (1 primary-key read; 2 for a caller a composition verifier left
|
|
50
|
-
* idp-shaped), the person's IamPolicy rows (1
|
|
51
|
-
* source
|
|
50
|
+
* idp-shaped), the person's IamPolicy rows (1 read of the port, once per
|
|
51
|
+
* source, indexed by principal on the OSS adapter; one more per team the
|
|
52
|
+
* person holds, which open source never has — derived-tuples.ts), and one
|
|
53
|
+
* row per parent hop the model walks.
|
|
52
54
|
*/
|
|
53
55
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
54
56
|
import { IamPermission } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
|
|
@@ -85,7 +87,7 @@ import { formatObjectRef } from "./tuples.js";
|
|
|
85
87
|
* their rows live in its own tables, out of its probe's reach; matching
|
|
86
88
|
* the set here keeps the wire identical across editions (an unknown
|
|
87
89
|
* platform client's id is PERMISSION_DENIED with the annotation's copy in
|
|
88
|
-
* both). The cloud
|
|
90
|
+
* both). The cloud pins every kind here inside its own
|
|
89
91
|
* `PROBE_EXEMPT_KINDS`, which is why this set is exported; a kind that
|
|
90
92
|
* moves into open source joins this set in the same change.
|
|
91
93
|
*
|