@stigmer/server 3.24.0 → 3.25.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/dist/authorization/authorizer.d.ts +10 -4
- package/dist/authorization/authorizer.d.ts.map +1 -1
- package/dist/authorization/authorizer.js +11 -4
- package/dist/authorization/authorizer.js.map +1 -1
- package/dist/authorization/model/index.d.ts +4 -4
- package/dist/authorization/model/index.d.ts.map +1 -1
- package/dist/authorization/model/index.js +2 -0
- package/dist/authorization/model/index.js.map +1 -1
- package/dist/authorization/model/oauth_app.d.ts.map +1 -1
- package/dist/authorization/model/oauth_app.js +3 -2
- package/dist/authorization/model/oauth_app.js.map +1 -1
- package/dist/authorization/model/platform_client.d.ts +2 -0
- package/dist/authorization/model/platform_client.d.ts.map +1 -0
- package/dist/authorization/model/platform_client.js +31 -0
- package/dist/authorization/model/platform_client.js.map +1 -0
- package/dist/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +88 -5
- package/dist/boot/compose.js.map +1 -1
- package/dist/boot/route-shadowing.d.ts +25 -0
- package/dist/boot/route-shadowing.d.ts.map +1 -0
- package/dist/boot/route-shadowing.js +17 -0
- package/dist/boot/route-shadowing.js.map +1 -0
- package/dist/domain/iampolicy/membership.d.ts +6 -1
- package/dist/domain/iampolicy/membership.d.ts.map +1 -1
- package/dist/domain/iampolicy/membership.js +8 -2
- package/dist/domain/iampolicy/membership.js.map +1 -1
- package/dist/domain/identityaccount/constants.d.ts +13 -0
- package/dist/domain/identityaccount/constants.d.ts.map +1 -1
- package/dist/domain/identityaccount/constants.js +17 -0
- package/dist/domain/identityaccount/constants.js.map +1 -1
- package/dist/domain/identityaccount/controller.d.ts +3 -1
- package/dist/domain/identityaccount/controller.d.ts.map +1 -1
- package/dist/domain/identityaccount/controller.js +15 -6
- package/dist/domain/identityaccount/controller.js.map +1 -1
- package/dist/domain/identityaccount/operator.d.ts.map +1 -1
- package/dist/domain/identityaccount/operator.js +1 -0
- package/dist/domain/identityaccount/operator.js.map +1 -1
- package/dist/domain/identityaccount/provisioning.d.ts +18 -0
- package/dist/domain/identityaccount/provisioning.d.ts.map +1 -1
- package/dist/domain/identityaccount/provisioning.js +1 -0
- package/dist/domain/identityaccount/provisioning.js.map +1 -1
- package/dist/domain/identityaccount/resource-store.js +9 -5
- package/dist/domain/identityaccount/resource-store.js.map +1 -1
- package/dist/domain/identityaccount/steps.d.ts +11 -2
- package/dist/domain/identityaccount/steps.d.ts.map +1 -1
- package/dist/domain/identityaccount/steps.js +42 -4
- package/dist/domain/identityaccount/steps.js.map +1 -1
- package/dist/domain/identityaccount/store-contract.d.ts.map +1 -1
- package/dist/domain/identityaccount/store-contract.js +33 -0
- package/dist/domain/identityaccount/store-contract.js.map +1 -1
- package/dist/domain/identityaccount/store.d.ts +7 -5
- package/dist/domain/identityaccount/store.d.ts.map +1 -1
- package/dist/domain/identityaccount/store.js.map +1 -1
- package/dist/domain/platform/controller.d.ts +10 -0
- package/dist/domain/platform/controller.d.ts.map +1 -1
- package/dist/domain/platform/controller.js +2 -1
- package/dist/domain/platform/controller.js.map +1 -1
- package/dist/domain/platformclient/constants.d.ts +60 -0
- package/dist/domain/platformclient/constants.d.ts.map +1 -0
- package/dist/domain/platformclient/constants.js +78 -0
- package/dist/domain/platformclient/constants.js.map +1 -0
- package/dist/domain/platformclient/controller.d.ts +59 -0
- package/dist/domain/platformclient/controller.d.ts.map +1 -0
- package/dist/domain/platformclient/controller.js +191 -0
- package/dist/domain/platformclient/controller.js.map +1 -0
- package/dist/domain/platformclient/credentials.d.ts +13 -0
- package/dist/domain/platformclient/credentials.d.ts.map +1 -0
- package/dist/domain/platformclient/credentials.js +56 -0
- package/dist/domain/platformclient/credentials.js.map +1 -0
- package/dist/domain/platformclient/mint.d.ts +23 -0
- package/dist/domain/platformclient/mint.d.ts.map +1 -0
- package/dist/domain/platformclient/mint.js +254 -0
- package/dist/domain/platformclient/mint.js.map +1 -0
- package/dist/domain/platformclient/origin-guard.d.ts +9 -0
- package/dist/domain/platformclient/origin-guard.d.ts.map +1 -0
- package/dist/domain/platformclient/origin-guard.js +81 -0
- package/dist/domain/platformclient/origin-guard.js.map +1 -0
- package/dist/domain/platformclient/resource-store.d.ts +4 -0
- package/dist/domain/platformclient/resource-store.d.ts.map +1 -0
- package/dist/domain/platformclient/resource-store.js +117 -0
- package/dist/domain/platformclient/resource-store.js.map +1 -0
- package/dist/domain/platformclient/steps.d.ts +51 -0
- package/dist/domain/platformclient/steps.d.ts.map +1 -0
- package/dist/domain/platformclient/steps.js +339 -0
- package/dist/domain/platformclient/steps.js.map +1 -0
- package/dist/domain/platformclient/store-contract.d.ts +8 -0
- package/dist/domain/platformclient/store-contract.d.ts.map +1 -0
- package/dist/domain/platformclient/store-contract.js +208 -0
- package/dist/domain/platformclient/store-contract.js.map +1 -0
- package/dist/domain/platformclient/store.d.ts +58 -0
- package/dist/domain/platformclient/store.d.ts.map +1 -0
- package/dist/domain/platformclient/store.js +13 -0
- package/dist/domain/platformclient/store.js.map +1 -0
- package/dist/domain/platformclient/system-managed.d.ts +18 -0
- package/dist/domain/platformclient/system-managed.d.ts.map +1 -0
- package/dist/domain/platformclient/system-managed.js +80 -0
- package/dist/domain/platformclient/system-managed.js.map +1 -0
- package/dist/domain/platformclient/token-controller.d.ts +11 -0
- package/dist/domain/platformclient/token-controller.d.ts.map +1 -0
- package/dist/domain/platformclient/token-controller.js +32 -0
- package/dist/domain/platformclient/token-controller.js.map +1 -0
- package/dist/domain/platformclient/verifier.d.ts +10 -0
- package/dist/domain/platformclient/verifier.d.ts.map +1 -0
- package/dist/domain/platformclient/verifier.js +76 -0
- package/dist/domain/platformclient/verifier.js.map +1 -0
- package/dist/encryption/key-manager.d.ts +20 -3
- package/dist/encryption/key-manager.d.ts.map +1 -1
- package/dist/encryption/key-manager.js +64 -42
- package/dist/encryption/key-manager.js.map +1 -1
- package/dist/extensions/caller-guards.d.ts +8 -3
- package/dist/extensions/caller-guards.d.ts.map +1 -1
- package/dist/extensions/drivers.d.ts +37 -0
- package/dist/extensions/drivers.d.ts.map +1 -1
- package/dist/extensions/guest-token-minting.d.ts +26 -0
- package/dist/extensions/guest-token-minting.d.ts.map +1 -0
- package/dist/extensions/guest-token-minting.js +2 -0
- package/dist/extensions/guest-token-minting.js.map +1 -0
- package/dist/extensions/identity.d.ts +6 -4
- package/dist/extensions/identity.d.ts.map +1 -1
- package/dist/extensions/identity.js.map +1 -1
- package/dist/extensions/registry.d.ts +30 -1
- package/dist/extensions/registry.d.ts.map +1 -1
- package/dist/extensions/registry.js +34 -1
- package/dist/extensions/registry.js.map +1 -1
- package/dist/index.d.ts +13 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/pipeline/interceptors/auth.d.ts +14 -0
- package/dist/pipeline/interceptors/auth.d.ts.map +1 -1
- package/dist/pipeline/interceptors/auth.js +24 -0
- package/dist/pipeline/interceptors/auth.js.map +1 -1
- package/dist/pipeline/steps/build-update-state.d.ts +11 -1
- package/dist/pipeline/steps/build-update-state.d.ts.map +1 -1
- package/dist/pipeline/steps/build-update-state.js +5 -2
- package/dist/pipeline/steps/build-update-state.js.map +1 -1
- package/dist/platformtoken/envelope.d.ts +100 -0
- package/dist/platformtoken/envelope.d.ts.map +1 -0
- package/dist/platformtoken/envelope.js +184 -0
- package/dist/platformtoken/envelope.js.map +1 -0
- package/dist/platformtoken/key-ring.d.ts +85 -0
- package/dist/platformtoken/key-ring.d.ts.map +1 -0
- package/dist/platformtoken/key-ring.js +164 -0
- package/dist/platformtoken/key-ring.js.map +1 -0
- package/package.json +6 -6
- package/src/authorization/README.md +1 -1
- package/src/authorization/__tests__/authorizer.test.ts +6 -2
- package/src/authorization/__tests__/wire-permissions.test.ts +1 -0
- package/src/authorization/authorizer.ts +11 -4
- package/src/authorization/model/__tests__/registry.test.ts +8 -1
- package/src/authorization/model/index.ts +6 -4
- package/src/authorization/model/oauth_app.ts +3 -2
- package/src/authorization/model/platform_client.ts +43 -0
- package/src/boot/__tests__/auth-enabled-composition.test.ts +2 -1
- package/src/boot/__tests__/route-shadowing.test.ts +67 -0
- package/src/boot/compose.ts +90 -5
- package/src/boot/route-shadowing.ts +47 -0
- package/src/domain/iampolicy/__tests__/membership.test.ts +7 -0
- package/src/domain/iampolicy/membership.ts +8 -2
- package/src/domain/identityaccount/__tests__/platform-client-accounts.test.ts +124 -0
- package/src/domain/identityaccount/__tests__/resource-store.test.ts +1 -0
- package/src/domain/identityaccount/constants.ts +20 -0
- package/src/domain/identityaccount/controller.ts +16 -4
- package/src/domain/identityaccount/operator.ts +1 -0
- package/src/domain/identityaccount/provisioning.ts +17 -0
- package/src/domain/identityaccount/resource-store.ts +9 -5
- package/src/domain/identityaccount/steps.ts +58 -3
- package/src/domain/identityaccount/store-contract.ts +51 -0
- package/src/domain/identityaccount/store.ts +7 -5
- package/src/domain/platform/__tests__/platform.test.ts +7 -1
- package/src/domain/platform/controller.ts +12 -1
- package/src/domain/platformclient/__tests__/controller.test.ts +397 -0
- package/src/domain/platformclient/__tests__/credentials.test.ts +46 -0
- package/src/domain/platformclient/__tests__/mint.test.ts +415 -0
- package/src/domain/platformclient/__tests__/resource-store.test.ts +140 -0
- package/src/domain/platformclient/__tests__/system-managed.test.ts +101 -0
- package/src/domain/platformclient/__tests__/verifier-and-guard.test.ts +207 -0
- package/src/domain/platformclient/constants.ts +120 -0
- package/src/domain/platformclient/controller.ts +449 -0
- package/src/domain/platformclient/credentials.ts +68 -0
- package/src/domain/platformclient/mint.ts +412 -0
- package/src/domain/platformclient/origin-guard.ts +117 -0
- package/src/domain/platformclient/resource-store.ts +150 -0
- package/src/domain/platformclient/steps.ts +472 -0
- package/src/domain/platformclient/store-contract.ts +318 -0
- package/src/domain/platformclient/store.ts +63 -0
- package/src/domain/platformclient/system-managed.ts +115 -0
- package/src/domain/platformclient/token-controller.ts +49 -0
- package/src/domain/platformclient/verifier.ts +106 -0
- package/src/encryption/key-manager.ts +87 -44
- package/src/extensions/__tests__/extension-composition.test.ts +3 -2
- package/src/extensions/__tests__/platform-client-composed.test.ts +288 -0
- package/src/extensions/__tests__/registry.test.ts +4 -1
- package/src/extensions/__tests__/tier-truthfulness.test.ts +8 -0
- package/src/extensions/caller-guards.ts +8 -3
- package/src/extensions/drivers.ts +37 -0
- package/src/extensions/guest-token-minting.ts +32 -0
- package/src/extensions/identity.ts +6 -4
- package/src/extensions/registry.ts +77 -3
- package/src/index.ts +55 -0
- package/src/pipeline/interceptors/auth.ts +27 -0
- package/src/pipeline/steps/__tests__/references.test.ts +2 -0
- package/src/pipeline/steps/build-update-state.ts +5 -2
- package/src/platformtoken/__tests__/envelope.test.ts +277 -0
- package/src/platformtoken/__tests__/key-ring.test.ts +205 -0
- package/src/platformtoken/envelope.ts +276 -0
- package/src/platformtoken/key-ring.ts +266 -0
- package/src/runnerauth/__tests__/runner-subject-composed.test.ts +9 -3
|
@@ -78,10 +78,16 @@ import { formatObjectRef } from "./tuples.js";
|
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
80
|
* Kinds whose missing row is a denial, never `not-found` (arm 4): the
|
|
81
|
-
* cloud's `PROBE_EXEMPT_KINDS`
|
|
82
|
-
*
|
|
83
|
-
* `invitation` are refused by
|
|
84
|
-
*
|
|
81
|
+
* cloud's `PROBE_EXEMPT_KINDS` (stigmer-cloud
|
|
82
|
+
* src/authorizer/fga-authorizer.ts) restricted to the kinds this edition
|
|
83
|
+
* serves — `platform`, `identity_provider` and `invitation` are refused by
|
|
84
|
+
* arm 2 before any row is asked about. The cloud exempts them because
|
|
85
|
+
* their rows live in its own tables, out of its probe's reach; matching
|
|
86
|
+
* the set here keeps the wire identical across editions (an unknown
|
|
87
|
+
* platform client's id is PERMISSION_DENIED with the annotation's copy in
|
|
88
|
+
* both). The cloud's model drift test pins every kind here inside its
|
|
89
|
+
* `PROBE_EXEMPT_KINDS`, which is why this set is exported; a kind that
|
|
90
|
+
* moves into open source joins this set in the same change.
|
|
85
91
|
*
|
|
86
92
|
* `iam_policy` is reachable as a target through the IamPolicy RPCs' own
|
|
87
93
|
* `resource.kind` (a caller may name it); the grant scope refuses such a
|
|
@@ -91,6 +97,7 @@ import { formatObjectRef } from "./tuples.js";
|
|
|
91
97
|
export const NOT_FOUND_EXEMPT_KINDS: ReadonlySet<ApiResourceKind> = new Set([
|
|
92
98
|
ApiResourceKind.identity_account,
|
|
93
99
|
ApiResourceKind.iam_policy,
|
|
100
|
+
ApiResourceKind.platform_client,
|
|
94
101
|
]);
|
|
95
102
|
|
|
96
103
|
/** The Java RequestAuthorizationService's pre-check copy, byte for byte (the cloud renders the same). */
|
|
@@ -34,6 +34,7 @@ const DECLARED_KINDS = [
|
|
|
34
34
|
ApiResourceKind.iam_policy,
|
|
35
35
|
ApiResourceKind.api_key,
|
|
36
36
|
ApiResourceKind.oauth_app,
|
|
37
|
+
ApiResourceKind.platform_client,
|
|
37
38
|
ApiResourceKind.organization,
|
|
38
39
|
ApiResourceKind.agent,
|
|
39
40
|
ApiResourceKind.agent_channel,
|
|
@@ -94,6 +95,13 @@ const WIRE_PERMISSIONS_BY_TYPE: Readonly<
|
|
|
94
95
|
"can_grant_access",
|
|
95
96
|
"can_view_access",
|
|
96
97
|
],
|
|
98
|
+
platform_client: [
|
|
99
|
+
"can_view",
|
|
100
|
+
"can_edit",
|
|
101
|
+
"can_delete",
|
|
102
|
+
"can_grant_access",
|
|
103
|
+
"can_view_access",
|
|
104
|
+
],
|
|
97
105
|
organization: [
|
|
98
106
|
"can_view",
|
|
99
107
|
"can_edit",
|
|
@@ -262,7 +270,6 @@ describe("the built-in model's registry", () => {
|
|
|
262
270
|
for (const unserved of [
|
|
263
271
|
ApiResourceKind.platform,
|
|
264
272
|
ApiResourceKind.identity_provider,
|
|
265
|
-
ApiResourceKind.platform_client,
|
|
266
273
|
ApiResourceKind.invitation,
|
|
267
274
|
]) {
|
|
268
275
|
expect(declarationFor(unserved), kindEnumName(unserved)).toBeUndefined();
|
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
* chain past the depth bound) and for the cloud's drift test, which
|
|
13
13
|
* builds a model from the live `.fga` files and compares.
|
|
14
14
|
*
|
|
15
|
-
* Registered: every kind of the open-source tier — the twenty-
|
|
16
|
-
* `kind_meta.tier: open_source` members — and nothing else. The
|
|
15
|
+
* Registered: every kind of the open-source tier — the twenty-four
|
|
16
|
+
* `kind_meta.tier: open_source` members — and nothing else. The three
|
|
17
17
|
* files `fga.mod` lists that have no declaration here (`platform`,
|
|
18
|
-
* `identity_provider`, `
|
|
19
|
-
*
|
|
18
|
+
* `identity_provider`, `invitation`) are kinds this edition does not
|
|
19
|
+
* serve: a check that targets one is refused by the
|
|
20
20
|
* driver, and a tuple that names one (`identity_provider#platform_user`)
|
|
21
21
|
* resolves to nobody. The registry test pins the list against the tier.
|
|
22
22
|
*/
|
|
@@ -38,6 +38,7 @@ import { mcpServerDeclaration } from "./mcp_server.js";
|
|
|
38
38
|
import { memoryDeclaration } from "./memory.js";
|
|
39
39
|
import { oauthAppDeclaration } from "./oauth_app.js";
|
|
40
40
|
import { organizationDeclaration } from "./organization.js";
|
|
41
|
+
import { platformClientDeclaration } from "./platform_client.js";
|
|
41
42
|
import { pluginDeclaration } from "./plugin.js";
|
|
42
43
|
import type { KindDeclaration } from "./rewrite.js";
|
|
43
44
|
import { scheduleDeclaration } from "./schedule.js";
|
|
@@ -77,6 +78,7 @@ export const builtInModel: Model = newModel([
|
|
|
77
78
|
iamPolicyDeclaration,
|
|
78
79
|
apiKeyDeclaration,
|
|
79
80
|
oauthAppDeclaration,
|
|
81
|
+
platformClientDeclaration,
|
|
80
82
|
organizationDeclaration,
|
|
81
83
|
agentDeclaration,
|
|
82
84
|
agentChannelDeclaration,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Transcript of fga/model/iam/oauth_app.fga —
|
|
3
|
-
*
|
|
2
|
+
* Transcript of fga/model/iam/oauth_app.fga — an organization-scoped kind
|
|
3
|
+
* whose `owner` has NO admin arm while its `viewer` does (platform_client
|
|
4
|
+
* shares the shape, for the same reason): the file's
|
|
4
5
|
* "administrative visibility" for the organization's admins over a
|
|
5
6
|
* registration that holds vendor client credentials, with change and
|
|
6
7
|
* deletion the creator's (or an explicit grant's) alone, and no public
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Transcript of fga/model/iam/platform_client.fga — relation for relation
|
|
3
|
+
* the oauth_app shape: organization-scoped, an `owner` with no admin arm
|
|
4
|
+
* and a `viewer` that has one. A platform client holds credential material,
|
|
5
|
+
* so the model is restricted: its creator (the owner) edits, deletes,
|
|
6
|
+
* rotates and grants; the organization's admins may view it; ordinary
|
|
7
|
+
* members have no access at all, which is why listByOrg narrows through
|
|
8
|
+
* the list read scope. There is no public level.
|
|
9
|
+
*/
|
|
10
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
11
|
+
import { PlatformClientSchema } from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/api_pb";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
computed,
|
|
15
|
+
declareKind,
|
|
16
|
+
direct,
|
|
17
|
+
from,
|
|
18
|
+
objectOf,
|
|
19
|
+
union,
|
|
20
|
+
} from "./rewrite.js";
|
|
21
|
+
|
|
22
|
+
export const platformClientDeclaration = declareKind({
|
|
23
|
+
kind: ApiResourceKind.platform_client,
|
|
24
|
+
schema: PlatformClientSchema,
|
|
25
|
+
source: "fga/model/iam/platform_client.fga",
|
|
26
|
+
relations: [
|
|
27
|
+
["organization", direct(objectOf("organization"))],
|
|
28
|
+
["owner", direct(objectOf("identity_account"))],
|
|
29
|
+
[
|
|
30
|
+
"viewer",
|
|
31
|
+
union(
|
|
32
|
+
direct(objectOf("identity_account")),
|
|
33
|
+
computed("owner"),
|
|
34
|
+
from("admin", "organization"),
|
|
35
|
+
),
|
|
36
|
+
],
|
|
37
|
+
["can_view", computed("viewer")],
|
|
38
|
+
["can_edit", computed("owner")],
|
|
39
|
+
["can_delete", computed("owner")],
|
|
40
|
+
["can_grant_access", computed("owner")],
|
|
41
|
+
["can_view_access", computed("viewer")],
|
|
42
|
+
],
|
|
43
|
+
});
|
|
@@ -236,10 +236,11 @@ describe("the require-authentication posture on the wire", () => {
|
|
|
236
236
|
);
|
|
237
237
|
});
|
|
238
238
|
|
|
239
|
-
it("is_public methods stay reachable tokenless (getServerInfo)", async () => {
|
|
239
|
+
it("is_public methods stay reachable tokenless (getServerInfo), and it reports the posture", async () => {
|
|
240
240
|
const platform = createClient(PlatformQueryController, transportWith());
|
|
241
241
|
const info = await platform.getServerInfo({});
|
|
242
242
|
expect(info.edition).not.toBe("");
|
|
243
|
+
expect(info.authenticationRequired).toBe(true);
|
|
243
244
|
});
|
|
244
245
|
|
|
245
246
|
it("the gRPC health service stays reachable tokenless — a Kubernetes grpc probe survives the posture (stigmer#974)", async () => {
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the routes stage's refusal of a shadowing registration: Connect's
|
|
3
|
+
* router lets the last registration of a path win silently, so an
|
|
4
|
+
* extension that registers a service the core already serves — or that
|
|
5
|
+
* another extension already registered — must fail boot, naming itself and
|
|
6
|
+
* whoever serves the path. Distinct services pass untouched, in order.
|
|
7
|
+
*/
|
|
8
|
+
import { createConnectRouter } from "@connectrpc/connect";
|
|
9
|
+
import { describe, expect, it } from "vitest";
|
|
10
|
+
|
|
11
|
+
import { OAuthAppQueryController } from "@stigmer/protos/ai/stigmer/iam/oauthapp/v1/query_pb";
|
|
12
|
+
import { PlatformClientQueryController } from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/query_pb";
|
|
13
|
+
import { PlatformClientTokenController } from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/token_pb";
|
|
14
|
+
|
|
15
|
+
import { registerExtensionServicesUnshadowed } from "../route-shadowing.js";
|
|
16
|
+
|
|
17
|
+
describe("registerExtensionServicesUnshadowed", () => {
|
|
18
|
+
it("refuses an extension service the core already serves, naming the unit and the path", () => {
|
|
19
|
+
const router = createConnectRouter();
|
|
20
|
+
router.service(PlatformClientTokenController, {});
|
|
21
|
+
expect(() =>
|
|
22
|
+
registerExtensionServicesUnshadowed(router, [
|
|
23
|
+
{
|
|
24
|
+
unit: "cloud-iam",
|
|
25
|
+
register: (r) => void r.service(PlatformClientTokenController, {}),
|
|
26
|
+
},
|
|
27
|
+
]),
|
|
28
|
+
).toThrow(
|
|
29
|
+
/extension 'cloud-iam' registers \/ai\.stigmer\.iam\.platformclient\.v1\.PlatformClientTokenController\/mintUserToken, which the core already serves/,
|
|
30
|
+
);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("refuses a second extension registering what a first one did", () => {
|
|
34
|
+
const router = createConnectRouter();
|
|
35
|
+
expect(() =>
|
|
36
|
+
registerExtensionServicesUnshadowed(router, [
|
|
37
|
+
{
|
|
38
|
+
unit: "first",
|
|
39
|
+
register: (r) => void r.service(OAuthAppQueryController, {}),
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
unit: "second",
|
|
43
|
+
register: (r) =>
|
|
44
|
+
void r.rpc(OAuthAppQueryController.method.get, () => {
|
|
45
|
+
throw new Error("never called");
|
|
46
|
+
}),
|
|
47
|
+
},
|
|
48
|
+
]),
|
|
49
|
+
).toThrow(
|
|
50
|
+
/extension 'second' registers .*OAuthAppQueryController\/get, which extension 'first' already serves/,
|
|
51
|
+
);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("registers distinct services from every unit", () => {
|
|
55
|
+
const router = createConnectRouter();
|
|
56
|
+
router.service(OAuthAppQueryController, {});
|
|
57
|
+
registerExtensionServicesUnshadowed(router, [
|
|
58
|
+
{
|
|
59
|
+
unit: "billing",
|
|
60
|
+
register: (r) => void r.service(PlatformClientQueryController, {}),
|
|
61
|
+
},
|
|
62
|
+
]);
|
|
63
|
+
expect(router.handlers.map((handler) => handler.requestPath)).toContain(
|
|
64
|
+
"/ai.stigmer.iam.platformclient.v1.PlatformClientQueryController/listByOrg",
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
});
|
package/src/boot/compose.ts
CHANGED
|
@@ -108,6 +108,11 @@ import { registerExecutionContextServices } from "../domain/executioncontext/con
|
|
|
108
108
|
import { registerGitHubServices } from "../domain/github/controller.js";
|
|
109
109
|
import { registerMemoryServices } from "../domain/memory/controller.js";
|
|
110
110
|
import { registerOAuthAppServices } from "../domain/oauthapp/controller.js";
|
|
111
|
+
import { registerPlatformClientServices } from "../domain/platformclient/controller.js";
|
|
112
|
+
import { newPlatformClientOriginGuard } from "../domain/platformclient/origin-guard.js";
|
|
113
|
+
import { newResourcePlatformClientStore } from "../domain/platformclient/resource-store.js";
|
|
114
|
+
import { registerPlatformClientTokenService } from "../domain/platformclient/token-controller.js";
|
|
115
|
+
import { newPlatformClientTokenVerifier } from "../domain/platformclient/verifier.js";
|
|
111
116
|
import { registerOrganizationServices } from "../domain/organization/controller.js";
|
|
112
117
|
import { registerMcpServerServices } from "../domain/mcpserver/controller.js";
|
|
113
118
|
import { registerPlatformServices } from "../domain/platform/controller.js";
|
|
@@ -144,6 +149,7 @@ import {
|
|
|
144
149
|
import type { SecretCodec } from "../encryption/codec.js";
|
|
145
150
|
import { getOrCreateNamedKey } from "../encryption/key-manager.js";
|
|
146
151
|
import { V1_VERSION } from "../encryption/v1-codec.js";
|
|
152
|
+
import { resolvePlatformTokenKeys } from "../platformtoken/key-ring.js";
|
|
147
153
|
import { registerActivityServices } from "../query/activity/controller.js";
|
|
148
154
|
import { ActivityHandler } from "../query/activity/handler.js";
|
|
149
155
|
import { registerSearchServices } from "../query/search/controller.js";
|
|
@@ -203,6 +209,7 @@ import {
|
|
|
203
209
|
} from "../transport/console/handler.js";
|
|
204
210
|
import { createRegistryLanes } from "../transport/registry/lanes.js";
|
|
205
211
|
import { createUnifiedPortServer } from "../transport/server.js";
|
|
212
|
+
import { registerExtensionServicesUnshadowed } from "./route-shadowing.js";
|
|
206
213
|
import type { ServerConfig } from "./config.js";
|
|
207
214
|
import { createInProcessClients } from "./inprocess.js";
|
|
208
215
|
import type { InProcessClients } from "./inprocess.js";
|
|
@@ -413,6 +420,14 @@ export async function composeServer(
|
|
|
413
420
|
// bindings are what the IamPolicy controller registers over (routes).
|
|
414
421
|
const iamPolicies =
|
|
415
422
|
extensions.drivers.iamPolicyStore ?? newResourceIamPolicyStore(store);
|
|
423
|
+
// The PlatformClient store PORT, bound the same way
|
|
424
|
+
// (`drivers.platformClientStore`; open source's adapter over `store`
|
|
425
|
+
// when absent): the chains, the mint and the verifier's liveness read all
|
|
426
|
+
// go through this one binding, so a composed driver is followed
|
|
427
|
+
// everywhere by construction.
|
|
428
|
+
const platformClients =
|
|
429
|
+
extensions.drivers.platformClientStore ??
|
|
430
|
+
newResourcePlatformClientStore(store);
|
|
416
431
|
const iamPolicyGrantPath = newIamPolicyGrantPath({
|
|
417
432
|
policies: iamPolicies,
|
|
418
433
|
lifecycle: extensions.drivers.resourceAuthorizationLifecycle,
|
|
@@ -690,6 +705,26 @@ export async function composeServer(
|
|
|
690
705
|
writeVersionValue === "" ? DEFAULT_WRITE_VERSION : writeVersionValue,
|
|
691
706
|
registeredCodecs: [...secretCodecs.keys()].sort(),
|
|
692
707
|
});
|
|
708
|
+
// The platform-token key ring joins the FATAL side of the asymmetry:
|
|
709
|
+
// under an authentication posture a server that cannot sign or verify
|
|
710
|
+
// its own tokens would refuse every PlatformClient-minted caller, and an
|
|
711
|
+
// explicitly configured key that does not load is configuration the
|
|
712
|
+
// operator meant (the ladder's rule). No posture, no ring
|
|
713
|
+
// (resolvePlatformTokenKeys carries the four arms).
|
|
714
|
+
const platformTokenKeys = resolvePlatformTokenKeys({
|
|
715
|
+
requireAuthentication,
|
|
716
|
+
supplied: extensions.drivers.platformTokenKeys,
|
|
717
|
+
edition: extensions.edition,
|
|
718
|
+
});
|
|
719
|
+
logger.info("platform-token keys resolved", {
|
|
720
|
+
source:
|
|
721
|
+
platformTokenKeys === undefined
|
|
722
|
+
? "none (no authentication posture)"
|
|
723
|
+
: extensions.drivers.platformTokenKeys !== undefined
|
|
724
|
+
? "extension"
|
|
725
|
+
: "open-source ladder",
|
|
726
|
+
signing: platformTokenKeys?.signer !== undefined,
|
|
727
|
+
});
|
|
693
728
|
// Stage: temporal (#18). Construction is sync and connection-free —
|
|
694
729
|
// initialConnect/startWorkers/startHealthMonitor run in start(), all
|
|
695
730
|
// NON-fatal (Go server.go: the server boots and serves with the engine
|
|
@@ -1284,6 +1319,34 @@ export async function composeServer(
|
|
|
1284
1319
|
authorizer,
|
|
1285
1320
|
authorizationLifecycle,
|
|
1286
1321
|
});
|
|
1322
|
+
// PlatformClient, OAuthApp's inbound counterpart: served in every
|
|
1323
|
+
// composition over the store PORT bound above. The token service is
|
|
1324
|
+
// registered here too — mintGuestToken dispatches to the composed
|
|
1325
|
+
// capability — so nothing else may register it (route-shadowing.ts).
|
|
1326
|
+
// The mint writes end users through the identity-account create path
|
|
1327
|
+
// and the one grant path, as the platform client (mint.ts), and signs
|
|
1328
|
+
// with the ring the keys stage resolved (none without a posture: the
|
|
1329
|
+
// mint then refuses).
|
|
1330
|
+
registerPlatformClientServices(router, {
|
|
1331
|
+
clients: platformClients,
|
|
1332
|
+
store,
|
|
1333
|
+
logger,
|
|
1334
|
+
authorizer,
|
|
1335
|
+
authorizationLifecycle,
|
|
1336
|
+
listReadScope,
|
|
1337
|
+
});
|
|
1338
|
+
registerPlatformClientTokenService(router, {
|
|
1339
|
+
mint: {
|
|
1340
|
+
clients: platformClients,
|
|
1341
|
+
accounts: identityAccounts,
|
|
1342
|
+
createAccount: createIdentityAccount,
|
|
1343
|
+
grantPath: iamPolicyGrantPath,
|
|
1344
|
+
keys: platformTokenKeys,
|
|
1345
|
+
logger,
|
|
1346
|
+
now: () => new Date(),
|
|
1347
|
+
},
|
|
1348
|
+
guestTokenMinting: extensions.drivers.guestTokenMinting,
|
|
1349
|
+
});
|
|
1287
1350
|
registerExecutionContextServices(router, {
|
|
1288
1351
|
store,
|
|
1289
1352
|
logger,
|
|
@@ -1549,6 +1612,7 @@ export async function composeServer(
|
|
|
1549
1612
|
runnerAuthService: runnerCredentials,
|
|
1550
1613
|
edition: extensions.edition,
|
|
1551
1614
|
version: options.version ?? SERVER_VERSION,
|
|
1615
|
+
authenticationRequired: requireAuthentication,
|
|
1552
1616
|
// The built-in `absent` answer installs here, at the consumer, when
|
|
1553
1617
|
// no unit registered a license-status provider: open source and the
|
|
1554
1618
|
// cloud hold no key, and only an Enterprise unit ever has one.
|
|
@@ -1559,10 +1623,9 @@ export async function composeServer(
|
|
|
1559
1623
|
// Extension services register after the whole OSS set, inside the ONE
|
|
1560
1624
|
// routes closure — so the serving router AND the in-process transport
|
|
1561
1625
|
// see them by construction (blueprint §2a; DD-002's parity doctrine
|
|
1562
|
-
// extends to extension services unchanged).
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
}
|
|
1626
|
+
// extends to extension services unchanged). A registration that would
|
|
1627
|
+
// shadow a route already served is a boot throw (route-shadowing.ts).
|
|
1628
|
+
registerExtensionServicesUnshadowed(router, extensions.services);
|
|
1566
1629
|
};
|
|
1567
1630
|
const inProcessWiring = createInProcessClients(routes, logger);
|
|
1568
1631
|
inProcess = inProcessWiring.clients;
|
|
@@ -1610,6 +1673,18 @@ export async function composeServer(
|
|
|
1610
1673
|
...(requireAuthentication
|
|
1611
1674
|
? [newApiKeyIdentityVerifier({ store, accounts: identityAccounts })]
|
|
1612
1675
|
: []),
|
|
1676
|
+
// The PlatformClient user-token lane rides the ring, which the keys
|
|
1677
|
+
// stage resolves only under an authentication posture. It sits before
|
|
1678
|
+
// `oidc` for the runner-subject verifier's reason (stigmer#1137): the
|
|
1679
|
+
// OIDC verifier would claim the platform token and throw on it.
|
|
1680
|
+
...(platformTokenKeys !== undefined
|
|
1681
|
+
? [
|
|
1682
|
+
newPlatformClientTokenVerifier({
|
|
1683
|
+
keys: platformTokenKeys,
|
|
1684
|
+
clients: platformClients,
|
|
1685
|
+
}),
|
|
1686
|
+
]
|
|
1687
|
+
: []),
|
|
1613
1688
|
...(authorizationPosture === "built-in"
|
|
1614
1689
|
? [
|
|
1615
1690
|
newRunnerSubjectIdentityVerifier({
|
|
@@ -1652,6 +1727,16 @@ export async function composeServer(
|
|
|
1652
1727
|
? [`extension '${extensions.requireAuthentication.declaredBy}'`]
|
|
1653
1728
|
: []),
|
|
1654
1729
|
];
|
|
1730
|
+
// Caller guards: open source's own first, then the extensions' — the
|
|
1731
|
+
// rule the verifier list follows ("OSS entries first"). The origin guard
|
|
1732
|
+
// exists only where PlatformClient tokens can be presented, under the
|
|
1733
|
+
// posture that verifies them.
|
|
1734
|
+
const callerGuards = [
|
|
1735
|
+
...(platformTokenKeys !== undefined
|
|
1736
|
+
? [newPlatformClientOriginGuard({ clients: platformClients, logger })]
|
|
1737
|
+
: []),
|
|
1738
|
+
...extensions.callerGuards,
|
|
1739
|
+
];
|
|
1655
1740
|
logger.info("authentication posture resolved", {
|
|
1656
1741
|
posture: requireAuthentication ? "required" : "trusted-local",
|
|
1657
1742
|
source: postureSources.length > 0 ? postureSources.join(" + ") : "none",
|
|
@@ -1680,7 +1765,7 @@ export async function composeServer(
|
|
|
1680
1765
|
logger,
|
|
1681
1766
|
createVerifierChainInterceptor(
|
|
1682
1767
|
identityVerifiers,
|
|
1683
|
-
|
|
1768
|
+
callerGuards,
|
|
1684
1769
|
logger,
|
|
1685
1770
|
requireAuthentication,
|
|
1686
1771
|
),
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The routes stage's one refusal: an extension may not register a route
|
|
3
|
+
* the router already serves.
|
|
4
|
+
*
|
|
5
|
+
* Connect's router keeps every registration, and both the serving adapter
|
|
6
|
+
* and the in-process transport build their path table with a plain map
|
|
7
|
+
* set, so a second registration of `/<service>/<method>` silently wins.
|
|
8
|
+
* Extensions register after the whole core set (compose.ts), so a
|
|
9
|
+
* composition that re-registers a service the core has come to serve —
|
|
10
|
+
* the moment a cloud domain moves into open source and the cloud's pin
|
|
11
|
+
* advances without deleting its own handlers — would shadow the core with
|
|
12
|
+
* no error, and serve from whatever store its old handlers read. Refusing
|
|
13
|
+
* at boot turns that into the loudest possible failure; the fix is always
|
|
14
|
+
* the domain's driver point, never a second service.
|
|
15
|
+
*
|
|
16
|
+
* Checked per request path, because `router.service` registers every
|
|
17
|
+
* method of a descriptor (the unimplemented ones answering UNIMPLEMENTED)
|
|
18
|
+
* and `router.rpc` registers one; two extensions colliding with each
|
|
19
|
+
* other are refused the same way.
|
|
20
|
+
*/
|
|
21
|
+
import type { ConnectRouter } from "@connectrpc/connect";
|
|
22
|
+
|
|
23
|
+
import type { ResolvedServiceRegistration } from "../extensions/registry.js";
|
|
24
|
+
|
|
25
|
+
/** Runs each extension registration, refusing any path already routed. */
|
|
26
|
+
export function registerExtensionServicesUnshadowed(
|
|
27
|
+
router: ConnectRouter,
|
|
28
|
+
registrations: ReadonlyArray<ResolvedServiceRegistration>,
|
|
29
|
+
): void {
|
|
30
|
+
// Request path → who serves it, as the refusal names them.
|
|
31
|
+
const servedBy = new Map<string, string>(
|
|
32
|
+
router.handlers.map((handler) => [handler.requestPath, "the core"]),
|
|
33
|
+
);
|
|
34
|
+
for (const { unit, register } of registrations) {
|
|
35
|
+
const before = router.handlers.length;
|
|
36
|
+
register(router);
|
|
37
|
+
for (const handler of router.handlers.slice(before)) {
|
|
38
|
+
const server = servedBy.get(handler.requestPath);
|
|
39
|
+
if (server !== undefined) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`extension '${unit}' registers ${handler.requestPath}, which ${server} already serves — a route is never registered twice; an edition changes a core domain's behaviour through its driver points`,
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
servedBy.set(handler.requestPath, `extension '${unit}'`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -639,6 +639,13 @@ describe("membership rules", () => {
|
|
|
639
639
|
IdentityAccountProvisioningMode.machine;
|
|
640
640
|
expect(isPersonAccount(machineMode)).toBe(false);
|
|
641
641
|
});
|
|
642
|
+
|
|
643
|
+
it("a platform-client end user is not: the mint grants it its client's role and nothing else", () => {
|
|
644
|
+
const endUser = account("stgm_pc|acme|user-7", OPERATOR_EMAIL);
|
|
645
|
+
endUser.spec!.provisioningMode =
|
|
646
|
+
IdentityAccountProvisioningMode.platform_client;
|
|
647
|
+
expect(isPersonAccount(endUser)).toBe(false);
|
|
648
|
+
});
|
|
642
649
|
});
|
|
643
650
|
|
|
644
651
|
describe("ensureRolesForExistingAccounts", () => {
|
|
@@ -195,14 +195,20 @@ export function isPersonStamp(stamp: string): boolean {
|
|
|
195
195
|
* `user`-class gate, for the reconciliation, which has a row and no
|
|
196
196
|
* caller. A machine account (`is_machine_account`, or the `machine`
|
|
197
197
|
* provisioning mode) earns no role, exactly as a `machine`-class caller
|
|
198
|
-
* earns none at the hook.
|
|
198
|
+
* earns none at the hook. Nor does a platform-client end user: it is a
|
|
199
|
+
* person on its platform's product, not on this install — the mint grants
|
|
200
|
+
* it exactly its client's auto-grant role on the owning organization and
|
|
201
|
+
* never runs these rules, and the reconciliation must not either, or every
|
|
202
|
+
* product user would become a member of every organization (and the
|
|
203
|
+
* operator-email arm would read an email the platform asserted).
|
|
199
204
|
*/
|
|
200
205
|
export function isPersonAccount(account: IdentityAccount): boolean {
|
|
201
206
|
const spec = account.spec;
|
|
202
207
|
return (
|
|
203
208
|
spec !== undefined &&
|
|
204
209
|
!spec.isMachineAccount &&
|
|
205
|
-
spec.provisioningMode !== IdentityAccountProvisioningMode.machine
|
|
210
|
+
spec.provisioningMode !== IdentityAccountProvisioningMode.machine &&
|
|
211
|
+
spec.provisioningMode !== IdentityAccountProvisioningMode.platform_client
|
|
206
212
|
);
|
|
207
213
|
}
|
|
208
214
|
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the identity-account create path's platform-client arm over the OSS
|
|
3
|
+
* adapter on sqlite:
|
|
4
|
+
* - `platform_client` provisioning writes the mode, the owning
|
|
5
|
+
* organization and the derived id, with the actor the caller named;
|
|
6
|
+
* - the `stgm_pc|` subject namespace is reserved in both directions: a
|
|
7
|
+
* direct account refuses it INVALID_ARGUMENT before anything is written,
|
|
8
|
+
* and a platform-client account without it is the caller's bug;
|
|
9
|
+
* - the direct lookups never answer the platform-client account, while the
|
|
10
|
+
* any-mode lookup (the mint's) does.
|
|
11
|
+
*/
|
|
12
|
+
import { Code, ConnectError } from "@connectrpc/connect";
|
|
13
|
+
import { create } from "@bufbuild/protobuf";
|
|
14
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
15
|
+
|
|
16
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
17
|
+
import { IdentityAccountProvisioningMode } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/enum_pb";
|
|
18
|
+
import { IdentityAccountSpecSchema } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/spec_pb";
|
|
19
|
+
|
|
20
|
+
import { silentLogger } from "../../../extensions/__tests__/composed-support.js";
|
|
21
|
+
import { serverActingFor } from "../../../pipeline/interceptors/auth.js";
|
|
22
|
+
import { newPermissiveSingleTeamAuthorizer } from "../../../pipeline/steps/authorize.js";
|
|
23
|
+
import { tempStore } from "../../../store/sqlite/__tests__/support.js";
|
|
24
|
+
import type { TempStore } from "../../../store/sqlite/__tests__/support.js";
|
|
25
|
+
import { accountIdFor, reservedSubjectMessage } from "../constants.js";
|
|
26
|
+
import { newCreateAccountPath } from "../controller.js";
|
|
27
|
+
import type { CreateAccount } from "../provisioning.js";
|
|
28
|
+
import { newResourceIdentityAccountStore } from "../resource-store.js";
|
|
29
|
+
import type { IdentityAccountStore } from "../store.js";
|
|
30
|
+
|
|
31
|
+
let temp: TempStore;
|
|
32
|
+
let accounts: IdentityAccountStore;
|
|
33
|
+
let createAccount: CreateAccount;
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
temp = tempStore();
|
|
37
|
+
accounts = newResourceIdentityAccountStore(temp.store);
|
|
38
|
+
createAccount = newCreateAccountPath(
|
|
39
|
+
{
|
|
40
|
+
accounts,
|
|
41
|
+
logger: silentLogger,
|
|
42
|
+
authorizer: newPermissiveSingleTeamAuthorizer(),
|
|
43
|
+
authorizationLifecycle: undefined,
|
|
44
|
+
},
|
|
45
|
+
ApiResourceKind.identity_account,
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
afterEach(() => {
|
|
50
|
+
temp.cleanup();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const SUBJECT = "stgm_pc|acme|user-7";
|
|
54
|
+
const CLIENT = serverActingFor("pcl_dashboard");
|
|
55
|
+
|
|
56
|
+
async function refusal(promise: Promise<unknown>): Promise<ConnectError> {
|
|
57
|
+
try {
|
|
58
|
+
await promise;
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (error instanceof ConnectError) return error;
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
throw new Error("expected a ConnectError refusal");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe("the create path's platform-client arm", () => {
|
|
67
|
+
it("writes the mode, the owning organization and the derived id, stamped as the client", async () => {
|
|
68
|
+
const account = await createAccount(
|
|
69
|
+
{
|
|
70
|
+
name: "pat@example.com",
|
|
71
|
+
spec: create(IdentityAccountSpecSchema, {
|
|
72
|
+
idpId: SUBJECT,
|
|
73
|
+
email: "pat@example.com",
|
|
74
|
+
}),
|
|
75
|
+
provisioning: { mode: "platform_client", org: "acme" },
|
|
76
|
+
},
|
|
77
|
+
CLIENT,
|
|
78
|
+
);
|
|
79
|
+
expect(account.metadata?.id).toBe(accountIdFor(SUBJECT));
|
|
80
|
+
expect(account.metadata?.org).toBe("acme");
|
|
81
|
+
expect(account.spec?.provisioningMode).toBe(
|
|
82
|
+
IdentityAccountProvisioningMode.platform_client,
|
|
83
|
+
);
|
|
84
|
+
expect(account.status?.audit?.specAudit?.createdBy?.id).toBe(
|
|
85
|
+
"pcl_dashboard",
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
expect(await accounts.findDirectByIdpId(SUBJECT)).toBeUndefined();
|
|
89
|
+
expect(await accounts.findDirectByEmail("pat@example.com")).toBeUndefined();
|
|
90
|
+
expect((await accounts.findByIdpId(SUBJECT))?.metadata?.id).toBe(
|
|
91
|
+
accountIdFor(SUBJECT),
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("refuses a direct account under the reserved subject prefix, writing nothing", async () => {
|
|
96
|
+
const denied = await refusal(
|
|
97
|
+
createAccount(
|
|
98
|
+
{
|
|
99
|
+
name: "impostor",
|
|
100
|
+
spec: create(IdentityAccountSpecSchema, { idpId: SUBJECT }),
|
|
101
|
+
provisioning: { mode: "direct" },
|
|
102
|
+
},
|
|
103
|
+
CLIENT,
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
expect(denied.code).toBe(Code.InvalidArgument);
|
|
107
|
+
expect(denied.rawMessage).toBe(reservedSubjectMessage(SUBJECT));
|
|
108
|
+
expect(await accounts.findByIdpId(SUBJECT)).toBeUndefined();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("refuses a platform-client account without the reserved prefix as a server fault", async () => {
|
|
112
|
+
const denied = await refusal(
|
|
113
|
+
createAccount(
|
|
114
|
+
{
|
|
115
|
+
name: "stray",
|
|
116
|
+
spec: create(IdentityAccountSpecSchema, { idpId: "auth0|stray" }),
|
|
117
|
+
provisioning: { mode: "platform_client", org: "acme" },
|
|
118
|
+
},
|
|
119
|
+
CLIENT,
|
|
120
|
+
),
|
|
121
|
+
);
|
|
122
|
+
expect(denied.code).toBe(Code.Internal);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -54,6 +54,7 @@ const CONTRACT_CASE_NAMES = [
|
|
|
54
54
|
"a disconnected store is an infrastructure fault, never 'not found'",
|
|
55
55
|
"two concurrent saves of one subject end in fulfilments and DuplicateAccountErrors only, and the winner is readable by subject",
|
|
56
56
|
"findDirectByIdpId and findDirectByEmail never answer a federated account, while findByIdpId (any mode) does",
|
|
57
|
+
"findDirectByIdpId and findDirectByEmail never answer a platform-client account, while findByIdpId (any mode) does",
|
|
57
58
|
"findByIds answers one row per distinct id, in first-occurrence order; an empty request answers an empty list",
|
|
58
59
|
"update of an unknown id is a no-op: no row appears",
|
|
59
60
|
"deleteById of an unknown id resolves",
|
|
@@ -110,6 +110,26 @@ export function isMachineSubject(idpId: string): boolean {
|
|
|
110
110
|
return idpId.endsWith(MACHINE_ACCOUNT_SUFFIX);
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/**
|
|
114
|
+
* The subject namespace of PlatformClient end users:
|
|
115
|
+
* `stgm_pc|<org>|<external_user_id>` (the composite every client of one
|
|
116
|
+
* organization resolves a user to; domain/platformclient/mint.ts builds
|
|
117
|
+
* it). Reserved in both directions, because derived account ids make every
|
|
118
|
+
* subject one namespace: a direct account may not take a `stgm_pc|`
|
|
119
|
+
* subject (an issuer emitting one would otherwise provision an account the
|
|
120
|
+
* mint then resolves), and the mint refuses a `stgm_pc|` account in any
|
|
121
|
+
* other mode.
|
|
122
|
+
*/
|
|
123
|
+
export const PLATFORM_CLIENT_SUBJECT_PREFIX = "stgm_pc|";
|
|
124
|
+
|
|
125
|
+
export function isPlatformClientSubject(idpId: string): boolean {
|
|
126
|
+
return idpId.startsWith(PLATFORM_CLIENT_SUBJECT_PREFIX);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function reservedSubjectMessage(idpId: string): string {
|
|
130
|
+
return `subject '${idpId}' uses the '${PLATFORM_CLIENT_SUBJECT_PREFIX}' prefix reserved for accounts a platform client provisions`;
|
|
131
|
+
}
|
|
132
|
+
|
|
113
133
|
// ---------------------------------------------------------------------------
|
|
114
134
|
// Byte-pinned copy (the cloud handlers' sentences, moved as-is).
|
|
115
135
|
// ---------------------------------------------------------------------------
|