@stigmer/server 3.23.1-dev.20260923075205 → 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/domain/workflow/agent-call-references.d.ts +60 -33
- package/dist/domain/workflow/agent-call-references.d.ts.map +1 -1
- package/dist/domain/workflow/agent-call-references.js +43 -23
- package/dist/domain/workflow/agent-call-references.js.map +1 -1
- package/dist/domain/workflow/registry/data/task-kind-registry.json +2 -2
- 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/apiresource-labels.d.ts +10 -9
- package/dist/pipeline/apiresource-labels.d.ts.map +1 -1
- package/dist/pipeline/apiresource-labels.js +10 -9
- package/dist/pipeline/apiresource-labels.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/pipeline/steps/references.d.ts +30 -4
- package/dist/pipeline/steps/references.d.ts.map +1 -1
- package/dist/pipeline/steps/references.js +13 -1
- package/dist/pipeline/steps/references.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/domain/workflow/__tests__/agent-call-references.test.ts +178 -27
- package/src/domain/workflow/agent-call-references.ts +95 -48
- package/src/domain/workflow/registry/data/task-kind-registry.json +2 -2
- 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/apiresource-labels.ts +10 -9
- package/src/pipeline/interceptors/auth.ts +27 -0
- package/src/pipeline/steps/__tests__/references.test.ts +37 -1
- package/src/pipeline/steps/build-update-state.ts +5 -2
- package/src/pipeline/steps/references.ts +49 -5
- 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
|
@@ -144,14 +144,18 @@ export function newResourceIdentityAccountStore(
|
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
*
|
|
147
|
+
* The platform's own subject: neither federated nor a platform-client end
|
|
148
|
+
* user. A federated account carries its identity_provider_ref (open source
|
|
149
|
+
* never writes one, so that arm is the port's contract stated); a
|
|
150
|
+
* platform-client account is the mint's, whose email and name the platform
|
|
151
|
+
* asserted, so no direct lookup — a verifier's subject resolve, getByEmail —
|
|
152
|
+
* may answer it.
|
|
151
153
|
*/
|
|
152
154
|
function isDirect(account: IdentityAccount): boolean {
|
|
155
|
+
const mode = account.spec?.provisioningMode;
|
|
153
156
|
return (
|
|
154
157
|
account.spec?.identityProviderRef === undefined &&
|
|
155
|
-
|
|
158
|
+
mode !== IdentityAccountProvisioningMode.federated &&
|
|
159
|
+
mode !== IdentityAccountProvisioningMode.platform_client
|
|
156
160
|
);
|
|
157
161
|
}
|
|
@@ -26,7 +26,8 @@
|
|
|
26
26
|
* subject, never its slug (the guideline's "domains with different
|
|
27
27
|
* uniqueness rules bring their own step under the same name").
|
|
28
28
|
* - AssignBackendFields: `is_machine_account` from the `@clients`
|
|
29
|
-
* suffix and
|
|
29
|
+
* suffix and the provisioning mode the path was given (`direct`, or
|
|
30
|
+
* `platform_client` for the mint's end users) on create (the spec's
|
|
30
31
|
* "assigned by backend" fields); on update, every backend-assigned
|
|
31
32
|
* field and the email are preserved from the existing row (A9: the
|
|
32
33
|
* cloud's writable surface — first/last name, picture, preferences —
|
|
@@ -63,11 +64,15 @@ import { EXISTING_RESOURCE_KEY } from "../../pipeline/steps/load-existing.js";
|
|
|
63
64
|
import { TARGET_RESOURCE_KEY } from "../../pipeline/steps/load-target.js";
|
|
64
65
|
import { idValueOf, metadataOf } from "../../pipeline/steps/shapes.js";
|
|
65
66
|
import {
|
|
67
|
+
PLATFORM_CLIENT_SUBJECT_PREFIX,
|
|
66
68
|
accountIdFor,
|
|
67
69
|
accountNotFoundMessage,
|
|
68
70
|
idpIdImmutableMessage,
|
|
69
71
|
isMachineSubject,
|
|
72
|
+
isPlatformClientSubject,
|
|
73
|
+
reservedSubjectMessage,
|
|
70
74
|
} from "./constants.js";
|
|
75
|
+
import type { AccountProvisioning } from "./provisioning.js";
|
|
71
76
|
import { DuplicateAccountError } from "./store.js";
|
|
72
77
|
import type { IdentityAccountStore } from "./store.js";
|
|
73
78
|
|
|
@@ -158,11 +163,61 @@ export function assignDirectBackendFields(
|
|
|
158
163
|
return spec;
|
|
159
164
|
}
|
|
160
165
|
|
|
161
|
-
|
|
166
|
+
/**
|
|
167
|
+
* The backend-assigned fields of a PLATFORM-CLIENT account: the machine
|
|
168
|
+
* flag from the subject's shape (the cloud's rule for every mode), mode
|
|
169
|
+
* `platform_client`, no provider ref — the PlatformClient admits the user
|
|
170
|
+
* at mint time and is no ongoing authentication authority.
|
|
171
|
+
*/
|
|
172
|
+
function assignPlatformClientBackendFields(
|
|
173
|
+
spec: IdentityAccountSpec,
|
|
174
|
+
): IdentityAccountSpec {
|
|
175
|
+
spec.isMachineAccount = isMachineSubject(spec.idpId);
|
|
176
|
+
spec.provisioningMode = IdentityAccountProvisioningMode.platform_client;
|
|
177
|
+
spec.identityProviderRef = undefined;
|
|
178
|
+
return spec;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* AssignBackendFields on create, by how the account was provisioned. The
|
|
183
|
+
* `stgm_pc|` subject namespace is reserved in both directions: a direct
|
|
184
|
+
* account refuses it (INVALID_ARGUMENT, before anything is written), and a
|
|
185
|
+
* platform-client account must carry it — its absence is the mint's bug,
|
|
186
|
+
* never a client's input.
|
|
187
|
+
*/
|
|
188
|
+
export function newAssignBackendFieldsStep(
|
|
189
|
+
provisioning: AccountProvisioning,
|
|
190
|
+
): AccountStep {
|
|
162
191
|
return {
|
|
163
192
|
name: "AssignBackendFields",
|
|
164
193
|
execute(ctx: AccountContext): void {
|
|
165
|
-
|
|
194
|
+
const spec = specOf(ctx);
|
|
195
|
+
switch (provisioning.mode) {
|
|
196
|
+
case "direct":
|
|
197
|
+
if (isPlatformClientSubject(spec.idpId)) {
|
|
198
|
+
throw invalidArgumentError(reservedSubjectMessage(spec.idpId));
|
|
199
|
+
}
|
|
200
|
+
assignDirectBackendFields(spec);
|
|
201
|
+
return;
|
|
202
|
+
case "platform_client":
|
|
203
|
+
if (!isPlatformClientSubject(spec.idpId)) {
|
|
204
|
+
throw internalError(
|
|
205
|
+
new Error(
|
|
206
|
+
`a platform-client account's subject must carry the '${PLATFORM_CLIENT_SUBJECT_PREFIX}' prefix`,
|
|
207
|
+
),
|
|
208
|
+
"assign backend fields",
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
assignPlatformClientBackendFields(spec);
|
|
212
|
+
return;
|
|
213
|
+
default: {
|
|
214
|
+
const exhaustive: never = provisioning;
|
|
215
|
+
throw internalError(
|
|
216
|
+
new Error(`unknown provisioning ${JSON.stringify(exhaustive)}`),
|
|
217
|
+
"assign backend fields",
|
|
218
|
+
);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
166
221
|
},
|
|
167
222
|
};
|
|
168
223
|
}
|
|
@@ -109,6 +109,31 @@ function federatedAccount(overrides: {
|
|
|
109
109
|
});
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* A platform-client end user as the mint provisions it: the reserved
|
|
114
|
+
* composite subject, mode `platform_client`, the owning organization, and
|
|
115
|
+
* the derived id of its subject (the id every subject lookup reads).
|
|
116
|
+
*/
|
|
117
|
+
function platformClientAccount(overrides: {
|
|
118
|
+
idpId: string;
|
|
119
|
+
email: string;
|
|
120
|
+
}): IdentityAccount {
|
|
121
|
+
return create(IdentityAccountSchema, {
|
|
122
|
+
apiVersion: "iam.stigmer.ai/v1",
|
|
123
|
+
kind: "IdentityAccount",
|
|
124
|
+
metadata: {
|
|
125
|
+
id: accountIdFor(overrides.idpId),
|
|
126
|
+
name: overrides.email,
|
|
127
|
+
org: "acme",
|
|
128
|
+
},
|
|
129
|
+
spec: {
|
|
130
|
+
idpId: overrides.idpId,
|
|
131
|
+
email: overrides.email,
|
|
132
|
+
provisioningMode: IdentityAccountProvisioningMode.platform_client,
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
112
137
|
/** A direct account with no subject — the row no lookup could ever reach. */
|
|
113
138
|
function subjectlessDirectAccount(): IdentityAccount {
|
|
114
139
|
return create(IdentityAccountSchema, {
|
|
@@ -357,6 +382,32 @@ const CASES: ReadonlyArray<PortContractDeclaration<IdentityAccountStore>> = [
|
|
|
357
382
|
);
|
|
358
383
|
},
|
|
359
384
|
],
|
|
385
|
+
[
|
|
386
|
+
"findDirectByIdpId and findDirectByEmail never answer a platform-client account, while findByIdpId (any mode) does",
|
|
387
|
+
async ({ store }) => {
|
|
388
|
+
await store.save(
|
|
389
|
+
platformClientAccount({
|
|
390
|
+
idpId: "stgm_pc|acme|user-7",
|
|
391
|
+
email: "pat@example.com",
|
|
392
|
+
}),
|
|
393
|
+
);
|
|
394
|
+
assert.equal(
|
|
395
|
+
await store.findDirectByIdpId("stgm_pc|acme|user-7"),
|
|
396
|
+
undefined,
|
|
397
|
+
"the direct-subject lookup must not resolve to an account a platform client provisioned",
|
|
398
|
+
);
|
|
399
|
+
assert.equal(
|
|
400
|
+
await store.findDirectByEmail("pat@example.com"),
|
|
401
|
+
undefined,
|
|
402
|
+
"the direct-email lookup must not answer an email a platform asserted",
|
|
403
|
+
);
|
|
404
|
+
assert.equal(
|
|
405
|
+
(await store.findByIdpId("stgm_pc|acme|user-7"))?.spec?.provisioningMode,
|
|
406
|
+
IdentityAccountProvisioningMode.platform_client,
|
|
407
|
+
"the any-mode subject lookup — the mint's — must answer the platform-client account",
|
|
408
|
+
);
|
|
409
|
+
},
|
|
410
|
+
],
|
|
360
411
|
[
|
|
361
412
|
"findByIds answers one row per distinct id, in first-occurrence order; an empty request answers an empty list",
|
|
362
413
|
async ({ store }) => {
|
|
@@ -22,12 +22,14 @@
|
|
|
22
22
|
* with an empty `idp_id` is refused, since no lookup could reach it;
|
|
23
23
|
* - `update` replaces, never creates: an unknown id writes nothing;
|
|
24
24
|
* - `deleteById` of an unknown id resolves;
|
|
25
|
-
* - `findDirectByIdpId` answers only
|
|
26
|
-
*
|
|
27
|
-
* same `auth0|…` shape and the platform lane must never resolve to it
|
|
25
|
+
* - `findDirectByIdpId` answers only the platform's own subjects —
|
|
26
|
+
* never a federated account (a customer's federated IdP may mint the
|
|
27
|
+
* same `auth0|…` shape and the platform lane must never resolve to it)
|
|
28
|
+
* and never a platform-client end user (its subject is the mint's);
|
|
28
29
|
* - `findDirectByEmail` answers direct accounts only, by exact match:
|
|
29
30
|
* federation legitimately duplicates emails, so the email axis is
|
|
30
|
-
* non-unique
|
|
31
|
+
* non-unique, and a platform-client account's email is one its
|
|
32
|
+
* platform asserted, so no email lookup may answer it;
|
|
31
33
|
* - `findByIds` answers one row per distinct id, in first-occurrence
|
|
32
34
|
* order, and skips unknown ids;
|
|
33
35
|
* - a typed not-found reads as `undefined`; any other storage failure
|
|
@@ -59,7 +61,7 @@ export interface IdentityAccountStore {
|
|
|
59
61
|
findById(id: string): Promise<IdentityAccount | undefined>;
|
|
60
62
|
/** Any provisioning mode — the getByIdpId RPC's lookup. */
|
|
61
63
|
findByIdpId(idpId: string): Promise<IdentityAccount | undefined>;
|
|
62
|
-
/**
|
|
64
|
+
/** The platform's own subjects only (no federated, no platform-client) — the verifiers' and whoAmI's lookup. */
|
|
63
65
|
findDirectByIdpId(idpId: string): Promise<IdentityAccount | undefined>;
|
|
64
66
|
/** Direct accounts only, exact match — the getByEmail RPC's lookup. */
|
|
65
67
|
findDirectByEmail(email: string): Promise<IdentityAccount | undefined>;
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
* with the token fields empty (minting a proxy token is cloud-only);
|
|
18
18
|
* - getServerInfo reports the build stamp by default and the release a
|
|
19
19
|
* library consumer states through ComposeOptions.version otherwise
|
|
20
|
-
* (stigmer#1168: unbundled, the stamp is always "dev")
|
|
20
|
+
* (stigmer#1168: unbundled, the stamp is always "dev"), and a
|
|
21
|
+
* trusted-local server's posture as an explicit false;
|
|
21
22
|
* - getLicenseStatus answers `absent` with no claims and no key when no
|
|
22
23
|
* provider is composed, and the composed provider's report verbatim
|
|
23
24
|
* when one is — both stamped with the ONE instant the controller
|
|
@@ -138,6 +139,9 @@ describe("platform domain (composed server)", () => {
|
|
|
138
139
|
// Unbundled runs report Go's unstamped default; release bundles stamp
|
|
139
140
|
// the esbuild define (scripts/bundle-slim.mjs).
|
|
140
141
|
expect(first.version).toBe("dev");
|
|
142
|
+
// A server that trusts every request says so on the wire: an explicit
|
|
143
|
+
// false, never an absent field a console would read as "unknown".
|
|
144
|
+
expect(first.authenticationRequired).toBe(false);
|
|
141
145
|
|
|
142
146
|
const second = await client.getServerInfo({});
|
|
143
147
|
expect(second.edition).toBe(first.edition);
|
|
@@ -285,6 +289,7 @@ describe("platform domain (keyless runner-token service)", () => {
|
|
|
285
289
|
),
|
|
286
290
|
edition: ServerEdition.oss,
|
|
287
291
|
version: "dev",
|
|
292
|
+
authenticationRequired: false,
|
|
288
293
|
licenseStatus: ABSENT_LICENSE_STATUS,
|
|
289
294
|
now: () => new Date(),
|
|
290
295
|
logger: silentLogger,
|
|
@@ -320,6 +325,7 @@ describe("platform domain (license status)", () => {
|
|
|
320
325
|
),
|
|
321
326
|
edition: ServerEdition.enterprise,
|
|
322
327
|
version: "dev",
|
|
328
|
+
authenticationRequired: false,
|
|
323
329
|
licenseStatus: provider,
|
|
324
330
|
now: () => instant,
|
|
325
331
|
logger: silentLogger,
|
|
@@ -106,6 +106,16 @@ export interface PlatformControllerDeps {
|
|
|
106
106
|
* states its installed release and answers exactly as a bundle would.
|
|
107
107
|
*/
|
|
108
108
|
readonly version: string;
|
|
109
|
+
/**
|
|
110
|
+
* The resolved require-authentication posture (compose.ts: the OSS
|
|
111
|
+
* issuer OR a unit's declaration), reported so a console can tell a
|
|
112
|
+
* server that verifies its callers from one that trusts every request —
|
|
113
|
+
* the features that hand out credentials only a verifying server
|
|
114
|
+
* honours (PlatformClient token minting) are unavailable on the latter.
|
|
115
|
+
* Always set on the wire, true or false: the field's absence is how a
|
|
116
|
+
* client recognises a server that predates it.
|
|
117
|
+
*/
|
|
118
|
+
readonly authenticationRequired: boolean;
|
|
109
119
|
/**
|
|
110
120
|
* What license this server holds (extensions/license-status.ts). The
|
|
111
121
|
* composition root installs the built-in `absent` provider when no unit
|
|
@@ -136,11 +146,12 @@ export function registerPlatformServices(
|
|
|
136
146
|
});
|
|
137
147
|
}
|
|
138
148
|
|
|
139
|
-
/** Go GetServerInfo: the server edition and
|
|
149
|
+
/** Go GetServerInfo: the server edition, release version and authentication posture. */
|
|
140
150
|
function getServerInfo(deps: PlatformControllerDeps): GetServerInfoOutput {
|
|
141
151
|
return create(GetServerInfoOutputSchema, {
|
|
142
152
|
edition: deps.edition,
|
|
143
153
|
version: deps.version,
|
|
154
|
+
authenticationRequired: deps.authenticationRequired,
|
|
144
155
|
});
|
|
145
156
|
}
|
|
146
157
|
|
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the PlatformClient chains over the OSS adapter on sqlite, through the
|
|
3
|
+
* package's own interceptor chain (validation included):
|
|
4
|
+
* - create answers the secret once and a client whose stored hash never
|
|
5
|
+
* leaves: not on create, get, getByReference, listByOrg, update,
|
|
6
|
+
* rotateSecret or delete — while the ROW keeps the hash the mint needs;
|
|
7
|
+
* - update replaces the spec and keeps the credentials, addressed by id
|
|
8
|
+
* or by org-scoped slug;
|
|
9
|
+
* - rotateSecret replaces the secret and fingerprint under the same
|
|
10
|
+
* client_id, and the row's hash follows;
|
|
11
|
+
* - the reserved slug is refused, the two auto-grant rules are refused on
|
|
12
|
+
* the contract, and a system-managed client refuses update, delete and
|
|
13
|
+
* rotate with the cloud's copy;
|
|
14
|
+
* - create and update run the reference rule on `environment_refs`: a
|
|
15
|
+
* bare slug is stored under the client's organization, a missing
|
|
16
|
+
* environment is refused on either chain before anything is stored, and
|
|
17
|
+
* another organization's environment is refused with the rule's one
|
|
18
|
+
* sentence;
|
|
19
|
+
* - listByOrg answers the organization's clients, newest first.
|
|
20
|
+
* Who may read a client is the enforcing lane's conformance suite's.
|
|
21
|
+
*/
|
|
22
|
+
import {
|
|
23
|
+
Code,
|
|
24
|
+
ConnectError,
|
|
25
|
+
createClient,
|
|
26
|
+
createRouterTransport,
|
|
27
|
+
} from "@connectrpc/connect";
|
|
28
|
+
import type { Client } from "@connectrpc/connect";
|
|
29
|
+
import { clone, create } from "@bufbuild/protobuf";
|
|
30
|
+
import type { MessageInitShape } from "@bufbuild/protobuf";
|
|
31
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
32
|
+
|
|
33
|
+
import { EnvironmentSchema } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/api_pb";
|
|
34
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
35
|
+
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
36
|
+
import { ApiResourceReferenceSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
|
|
37
|
+
import { PlatformClientSchema } from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/api_pb";
|
|
38
|
+
import { PlatformClientCommandController } from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/command_pb";
|
|
39
|
+
import { PlatformClientQueryController } from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/query_pb";
|
|
40
|
+
import { PlatformClientSpecSchema } from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/spec_pb";
|
|
41
|
+
import type { PlatformClientSpec } from "@stigmer/protos/ai/stigmer/iam/platformclient/v1/spec_pb";
|
|
42
|
+
import { IamRole } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
|
|
43
|
+
|
|
44
|
+
import { silentLogger } from "../../../extensions/__tests__/composed-support.js";
|
|
45
|
+
import {
|
|
46
|
+
SYSTEM_MANAGED_LABEL,
|
|
47
|
+
RESERVED_LABEL_TRUE,
|
|
48
|
+
} from "../../../pipeline/apiresource-labels.js";
|
|
49
|
+
import { buildInterceptorChain } from "../../../pipeline/chain.js";
|
|
50
|
+
import { createInProcessCallerInterceptor } from "../../../pipeline/interceptors/auth.js";
|
|
51
|
+
import { newPermissiveSingleTeamAuthorizer } from "../../../pipeline/steps/authorize.js";
|
|
52
|
+
import {
|
|
53
|
+
missingReferencesMessage,
|
|
54
|
+
notAvailableReferenceMessage,
|
|
55
|
+
referenceTargetKind,
|
|
56
|
+
} from "../../../pipeline/steps/references.js";
|
|
57
|
+
import type { Store } from "../../../store/interface.js";
|
|
58
|
+
import { tempStore } from "../../../store/sqlite/__tests__/support.js";
|
|
59
|
+
import { reservedSlugMessage, systemManagedMessage } from "../constants.js";
|
|
60
|
+
import { registerPlatformClientServices } from "../controller.js";
|
|
61
|
+
import { hashClientSecret } from "../credentials.js";
|
|
62
|
+
import { newResourcePlatformClientStore } from "../resource-store.js";
|
|
63
|
+
import type { PlatformClientStore } from "../store.js";
|
|
64
|
+
|
|
65
|
+
let cleanup: () => void;
|
|
66
|
+
let store: Store;
|
|
67
|
+
let clients: PlatformClientStore;
|
|
68
|
+
let command: Client<typeof PlatformClientCommandController>;
|
|
69
|
+
let query: Client<typeof PlatformClientQueryController>;
|
|
70
|
+
|
|
71
|
+
beforeEach(() => {
|
|
72
|
+
const temp = tempStore();
|
|
73
|
+
cleanup = () => temp.cleanup();
|
|
74
|
+
store = temp.store;
|
|
75
|
+
clients = newResourcePlatformClientStore(temp.store);
|
|
76
|
+
const transport = createRouterTransport(
|
|
77
|
+
(router) =>
|
|
78
|
+
registerPlatformClientServices(router, {
|
|
79
|
+
clients,
|
|
80
|
+
store,
|
|
81
|
+
logger: silentLogger,
|
|
82
|
+
authorizer: newPermissiveSingleTeamAuthorizer(),
|
|
83
|
+
authorizationLifecycle: undefined,
|
|
84
|
+
listReadScope: undefined,
|
|
85
|
+
}),
|
|
86
|
+
{
|
|
87
|
+
router: {
|
|
88
|
+
interceptors: buildInterceptorChain(
|
|
89
|
+
silentLogger,
|
|
90
|
+
createInProcessCallerInterceptor(),
|
|
91
|
+
),
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
command = createClient(PlatformClientCommandController, transport);
|
|
96
|
+
query = createClient(PlatformClientQueryController, transport);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
afterEach(() => cleanup());
|
|
100
|
+
|
|
101
|
+
function input(
|
|
102
|
+
name: string,
|
|
103
|
+
spec: MessageInitShape<typeof PlatformClientSpecSchema> = {},
|
|
104
|
+
) {
|
|
105
|
+
return create(PlatformClientSchema, {
|
|
106
|
+
apiVersion: "iam.stigmer.ai/v1",
|
|
107
|
+
kind: "PlatformClient",
|
|
108
|
+
metadata: { name, org: "acme" },
|
|
109
|
+
spec: { autoProvisionAccounts: true, ...spec },
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function refusal(promise: Promise<unknown>): Promise<ConnectError> {
|
|
114
|
+
try {
|
|
115
|
+
await promise;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (error instanceof ConnectError) return error;
|
|
118
|
+
throw error;
|
|
119
|
+
}
|
|
120
|
+
throw new Error("expected a ConnectError refusal");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function seedEnvironment(org: string, slug: string): Promise<void> {
|
|
124
|
+
const id = `env_${org}_${slug}`;
|
|
125
|
+
await store.saveResource(
|
|
126
|
+
ApiResourceKind.environment,
|
|
127
|
+
id,
|
|
128
|
+
EnvironmentSchema,
|
|
129
|
+
create(EnvironmentSchema, {
|
|
130
|
+
apiVersion: "agentic.stigmer.ai/v1",
|
|
131
|
+
kind: "Environment",
|
|
132
|
+
metadata: {
|
|
133
|
+
id,
|
|
134
|
+
name: slug,
|
|
135
|
+
slug,
|
|
136
|
+
org,
|
|
137
|
+
visibility: ApiResourceVisibility.visibility_org,
|
|
138
|
+
},
|
|
139
|
+
}),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function environmentRef(slug: string, org = "") {
|
|
144
|
+
return { org, slug, kind: ApiResourceKind.environment };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function environmentTarget() {
|
|
148
|
+
const entry = referenceTargetKind(ApiResourceKind.environment);
|
|
149
|
+
if (entry === undefined)
|
|
150
|
+
throw new Error("environment is not a reference target kind");
|
|
151
|
+
return entry;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
describe("PlatformClient chains", () => {
|
|
155
|
+
it("shows the secret once and never returns the stored hash, which the row keeps", async () => {
|
|
156
|
+
const created = await command.create(input("Dashboard"));
|
|
157
|
+
const id = created.platformClient?.metadata?.id ?? "";
|
|
158
|
+
expect(id).toMatch(/^pcl_/);
|
|
159
|
+
expect(created.clientSecret).toMatch(/^stgm_cs_/);
|
|
160
|
+
expect(created.platformClient?.spec?.clientSecretHash).toBe("");
|
|
161
|
+
expect(created.platformClient?.spec?.secretFingerprint).toBe(
|
|
162
|
+
created.clientSecret.slice(-6),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
expect((await clients.findById(id))?.spec?.clientSecretHash).toBe(
|
|
166
|
+
hashClientSecret(created.clientSecret),
|
|
167
|
+
);
|
|
168
|
+
expect((await query.get({ value: id })).spec?.clientSecretHash).toBe("");
|
|
169
|
+
expect(
|
|
170
|
+
(
|
|
171
|
+
await query.getByReference({
|
|
172
|
+
org: "acme",
|
|
173
|
+
slug: "dashboard",
|
|
174
|
+
kind: ApiResourceKind.platform_client,
|
|
175
|
+
})
|
|
176
|
+
).spec?.clientSecretHash,
|
|
177
|
+
).toBe("");
|
|
178
|
+
const listed = await query.listByOrg({ org: "acme" });
|
|
179
|
+
expect(listed.entries.map((entry) => entry.spec?.clientSecretHash)).toEqual(
|
|
180
|
+
[""],
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("update keeps the credentials, addressed by id or by slug", async () => {
|
|
185
|
+
const created = await command.create(input("Dashboard"));
|
|
186
|
+
const stored = created.platformClient;
|
|
187
|
+
const clientId = stored?.spec?.clientId ?? "";
|
|
188
|
+
|
|
189
|
+
if (stored?.spec === undefined) throw new Error("create answered no spec");
|
|
190
|
+
const byId = clone(PlatformClientSchema, stored);
|
|
191
|
+
byId.spec = clone(PlatformClientSpecSchema, stored.spec);
|
|
192
|
+
byId.spec.clientId = "stgm_cid_forged";
|
|
193
|
+
byId.spec.allowedOrigins = ["https://a.example"];
|
|
194
|
+
const updated = await command.update(byId);
|
|
195
|
+
expect(updated.spec?.clientId).toBe(clientId);
|
|
196
|
+
expect(updated.spec?.allowedOrigins).toEqual(["https://a.example"]);
|
|
197
|
+
expect(updated.spec?.clientSecretHash).toBe("");
|
|
198
|
+
|
|
199
|
+
const bySlug = create(PlatformClientSchema, {
|
|
200
|
+
apiVersion: "iam.stigmer.ai/v1",
|
|
201
|
+
kind: "PlatformClient",
|
|
202
|
+
metadata: { name: "Dashboard", org: "acme", slug: "dashboard" },
|
|
203
|
+
spec: { autoProvisionAccounts: false },
|
|
204
|
+
});
|
|
205
|
+
const viaSlug = await command.update(bySlug);
|
|
206
|
+
expect(viaSlug.metadata?.id).toBe(stored?.metadata?.id);
|
|
207
|
+
expect(viaSlug.spec?.clientId).toBe(clientId);
|
|
208
|
+
expect(
|
|
209
|
+
(await clients.findById(stored?.metadata?.id ?? ""))?.spec
|
|
210
|
+
?.clientSecretHash,
|
|
211
|
+
).toBe(hashClientSecret(created.clientSecret));
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("rotateSecret replaces the secret under the same client_id, and the row follows", async () => {
|
|
215
|
+
const created = await command.create(input("Dashboard"));
|
|
216
|
+
const id = created.platformClient?.metadata?.id ?? "";
|
|
217
|
+
const rotated = await command.rotateSecret({ value: id });
|
|
218
|
+
|
|
219
|
+
expect(rotated.clientSecret).not.toBe(created.clientSecret);
|
|
220
|
+
expect(rotated.platformClient?.spec?.clientId).toBe(
|
|
221
|
+
created.platformClient?.spec?.clientId,
|
|
222
|
+
);
|
|
223
|
+
expect(rotated.platformClient?.spec?.secretFingerprint).toBe(
|
|
224
|
+
rotated.clientSecret.slice(-6),
|
|
225
|
+
);
|
|
226
|
+
expect(rotated.platformClient?.spec?.clientSecretHash).toBe("");
|
|
227
|
+
expect((await clients.findById(id))?.spec?.clientSecretHash).toBe(
|
|
228
|
+
hashClientSecret(rotated.clientSecret),
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("refuses the reserved slug and the two auto-grant rules", async () => {
|
|
233
|
+
const reserved = await refusal(
|
|
234
|
+
command.create(
|
|
235
|
+
create(PlatformClientSchema, {
|
|
236
|
+
apiVersion: "iam.stigmer.ai/v1",
|
|
237
|
+
kind: "PlatformClient",
|
|
238
|
+
metadata: { name: "System Share Client", org: "acme" },
|
|
239
|
+
}),
|
|
240
|
+
),
|
|
241
|
+
);
|
|
242
|
+
expect(reserved.code).toBe(Code.InvalidArgument);
|
|
243
|
+
expect(reserved.rawMessage).toBe(
|
|
244
|
+
reservedSlugMessage("system-share-client"),
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
const owner = await refusal(
|
|
248
|
+
command.create(
|
|
249
|
+
input("Owner grant", {
|
|
250
|
+
autoGrantOnOrg: true,
|
|
251
|
+
autoGrantRole: IamRole.owner,
|
|
252
|
+
}),
|
|
253
|
+
),
|
|
254
|
+
);
|
|
255
|
+
expect(owner.code).toBe(Code.InvalidArgument);
|
|
256
|
+
|
|
257
|
+
const noProvision = await refusal(
|
|
258
|
+
command.create(
|
|
259
|
+
input("No provision", {
|
|
260
|
+
autoProvisionAccounts: false,
|
|
261
|
+
autoGrantOnOrg: true,
|
|
262
|
+
}),
|
|
263
|
+
),
|
|
264
|
+
);
|
|
265
|
+
expect(noProvision.code).toBe(Code.InvalidArgument);
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("stores a bare environment slug under the client's organization", async () => {
|
|
269
|
+
await seedEnvironment("acme", "support-secrets");
|
|
270
|
+
const created = await command.create(
|
|
271
|
+
input("Dashboard", {
|
|
272
|
+
environmentRefs: [environmentRef("support-secrets")],
|
|
273
|
+
}),
|
|
274
|
+
);
|
|
275
|
+
const refsOf = (spec: PlatformClientSpec | undefined) =>
|
|
276
|
+
(spec?.environmentRefs ?? []).map((ref) => `${ref.org}/${ref.slug}`);
|
|
277
|
+
expect(refsOf(created.platformClient?.spec)).toEqual([
|
|
278
|
+
"acme/support-secrets",
|
|
279
|
+
]);
|
|
280
|
+
const id = created.platformClient?.metadata?.id ?? "";
|
|
281
|
+
expect(refsOf((await clients.findById(id))?.spec)).toEqual([
|
|
282
|
+
"acme/support-secrets",
|
|
283
|
+
]);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
it("refuses a missing environment on create and on update, storing nothing", async () => {
|
|
287
|
+
const missing = missingReferencesMessage(environmentTarget(), [
|
|
288
|
+
{ slug: "ghost", org: "acme" },
|
|
289
|
+
]);
|
|
290
|
+
|
|
291
|
+
const onCreate = await refusal(
|
|
292
|
+
command.create(
|
|
293
|
+
input("Dashboard", { environmentRefs: [environmentRef("ghost")] }),
|
|
294
|
+
),
|
|
295
|
+
);
|
|
296
|
+
expect(onCreate.code).toBe(Code.FailedPrecondition);
|
|
297
|
+
expect(onCreate.rawMessage).toBe(missing);
|
|
298
|
+
expect((await query.listByOrg({ org: "acme" })).entries).toEqual([]);
|
|
299
|
+
|
|
300
|
+
const created = await command.create(input("Dashboard"));
|
|
301
|
+
const stored = created.platformClient;
|
|
302
|
+
if (stored?.spec === undefined) throw new Error("create answered no spec");
|
|
303
|
+
const withGhost = clone(PlatformClientSchema, stored);
|
|
304
|
+
withGhost.spec = clone(PlatformClientSpecSchema, stored.spec);
|
|
305
|
+
withGhost.spec.environmentRefs = [
|
|
306
|
+
create(ApiResourceReferenceSchema, environmentRef("ghost")),
|
|
307
|
+
];
|
|
308
|
+
const onUpdate = await refusal(command.update(withGhost));
|
|
309
|
+
expect(onUpdate.code).toBe(Code.FailedPrecondition);
|
|
310
|
+
expect(onUpdate.rawMessage).toBe(missing);
|
|
311
|
+
expect(
|
|
312
|
+
(await clients.findById(stored.metadata?.id ?? ""))?.spec
|
|
313
|
+
?.environmentRefs,
|
|
314
|
+
).toEqual([]);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
it("refuses another organization's environment with the rule's one sentence", async () => {
|
|
318
|
+
await seedEnvironment("globex", "shared");
|
|
319
|
+
const foreign = await refusal(
|
|
320
|
+
command.create(
|
|
321
|
+
input("Dashboard", {
|
|
322
|
+
environmentRefs: [environmentRef("shared", "globex")],
|
|
323
|
+
}),
|
|
324
|
+
),
|
|
325
|
+
);
|
|
326
|
+
expect(foreign.code).toBe(Code.FailedPrecondition);
|
|
327
|
+
expect(foreign.rawMessage).toBe(
|
|
328
|
+
notAvailableReferenceMessage(environmentTarget(), {
|
|
329
|
+
kind: ApiResourceKind.environment,
|
|
330
|
+
org: "globex",
|
|
331
|
+
slug: "shared",
|
|
332
|
+
}),
|
|
333
|
+
);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("refuses to update, delete or rotate a system-managed client", async () => {
|
|
337
|
+
await clients.save(
|
|
338
|
+
create(PlatformClientSchema, {
|
|
339
|
+
apiVersion: "iam.stigmer.ai/v1",
|
|
340
|
+
kind: "PlatformClient",
|
|
341
|
+
metadata: {
|
|
342
|
+
id: "pcl_system",
|
|
343
|
+
name: "System Share Client",
|
|
344
|
+
org: "acme",
|
|
345
|
+
slug: "system-share-client",
|
|
346
|
+
labels: { [SYSTEM_MANAGED_LABEL]: RESERVED_LABEL_TRUE },
|
|
347
|
+
},
|
|
348
|
+
spec: { clientId: "stgm_cid_system" },
|
|
349
|
+
}),
|
|
350
|
+
);
|
|
351
|
+
const stored = await clients.findById("pcl_system");
|
|
352
|
+
if (stored === undefined) throw new Error("seeded client missing");
|
|
353
|
+
|
|
354
|
+
const update = await refusal(command.update(stored));
|
|
355
|
+
expect(update.code).toBe(Code.FailedPrecondition);
|
|
356
|
+
expect(update.rawMessage).toBe(systemManagedMessage("updated"));
|
|
357
|
+
|
|
358
|
+
const rotate = await refusal(command.rotateSecret({ value: "pcl_system" }));
|
|
359
|
+
expect(rotate.rawMessage).toBe(systemManagedMessage("rotated"));
|
|
360
|
+
|
|
361
|
+
const remove = await refusal(command.delete({ resourceId: "pcl_system" }));
|
|
362
|
+
expect(remove.rawMessage).toBe(systemManagedMessage("deleted"));
|
|
363
|
+
expect(await clients.findById("pcl_system")).toBeDefined();
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it("listByOrg answers the organization's clients newest first", async () => {
|
|
367
|
+
for (const [id, seconds] of [
|
|
368
|
+
["pcl_older", 1_000],
|
|
369
|
+
["pcl_newer", 2_000],
|
|
370
|
+
] as const) {
|
|
371
|
+
await clients.save(
|
|
372
|
+
create(PlatformClientSchema, {
|
|
373
|
+
metadata: { id, name: id, org: "acme", slug: id },
|
|
374
|
+
spec: { clientId: `stgm_cid_${id}` },
|
|
375
|
+
status: {
|
|
376
|
+
audit: { specAudit: { createdAt: { seconds: BigInt(seconds) } } },
|
|
377
|
+
},
|
|
378
|
+
}),
|
|
379
|
+
);
|
|
380
|
+
}
|
|
381
|
+
const listed = await query.listByOrg({ org: "acme" });
|
|
382
|
+
expect(listed.entries.map((entry) => entry.metadata?.id)).toEqual([
|
|
383
|
+
"pcl_newer",
|
|
384
|
+
"pcl_older",
|
|
385
|
+
]);
|
|
386
|
+
expect((await query.listByOrg({ org: "globex" })).entries).toEqual([]);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("delete answers the client without its hash and removes it", async () => {
|
|
390
|
+
const created = await command.create(input("Dashboard"));
|
|
391
|
+
const id = created.platformClient?.metadata?.id ?? "";
|
|
392
|
+
|
|
393
|
+
const deleted = await command.delete({ resourceId: id });
|
|
394
|
+
expect(deleted.spec?.clientSecretHash).toBe("");
|
|
395
|
+
expect(await clients.findById(id)).toBeUndefined();
|
|
396
|
+
});
|
|
397
|
+
});
|