@stigmer/server 3.31.0 → 3.32.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/model/data/authorization-model.json +16 -46
- package/dist/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +3 -0
- package/dist/boot/compose.js.map +1 -1
- package/dist/domain/agentexecution/create-execution-context-step.d.ts +7 -0
- package/dist/domain/agentexecution/create-execution-context-step.d.ts.map +1 -1
- package/dist/domain/agentexecution/create-execution-context-step.js +79 -4
- package/dist/domain/agentexecution/create-execution-context-step.js.map +1 -1
- package/dist/domain/iampolicy/constants.d.ts +11 -1
- package/dist/domain/iampolicy/constants.d.ts.map +1 -1
- package/dist/domain/iampolicy/constants.js +11 -1
- package/dist/domain/iampolicy/constants.js.map +1 -1
- package/dist/domain/iampolicy/controller.d.ts.map +1 -1
- package/dist/domain/iampolicy/controller.js +36 -12
- package/dist/domain/iampolicy/controller.js.map +1 -1
- package/dist/domain/iampolicy/steps.d.ts +98 -0
- package/dist/domain/iampolicy/steps.d.ts.map +1 -1
- package/dist/domain/iampolicy/steps.js +119 -4
- package/dist/domain/iampolicy/steps.js.map +1 -1
- package/dist/domain/platformclient/constants.d.ts +11 -1
- package/dist/domain/platformclient/constants.d.ts.map +1 -1
- package/dist/domain/platformclient/constants.js +11 -1
- package/dist/domain/platformclient/constants.js.map +1 -1
- package/dist/domain/platformclient/verifier.d.ts.map +1 -1
- package/dist/domain/platformclient/verifier.js +7 -2
- package/dist/domain/platformclient/verifier.js.map +1 -1
- package/dist/extensions/identity.d.ts +14 -0
- package/dist/extensions/identity.d.ts.map +1 -1
- package/dist/extensions/identity.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/pipeline/steps/defaults.d.ts +5 -1
- package/dist/pipeline/steps/defaults.d.ts.map +1 -1
- package/dist/pipeline/steps/defaults.js +6 -1
- package/dist/pipeline/steps/defaults.js.map +1 -1
- package/package.json +6 -6
- package/src/authorization/__tests__/store-tests.test.ts +1 -0
- package/src/authorization/model/data/authorization-model.json +16 -46
- package/src/boot/compose.ts +3 -0
- package/src/domain/agentexecution/__tests__/create-execution-context-step.test.ts +329 -0
- package/src/domain/agentexecution/__tests__/lifecycle.test.ts +7 -0
- package/src/domain/agentexecution/create-execution-context-step.ts +110 -4
- package/src/domain/iampolicy/__tests__/controller.test.ts +99 -3
- package/src/domain/iampolicy/__tests__/steps.test.ts +179 -0
- package/src/domain/iampolicy/constants.ts +15 -1
- package/src/domain/iampolicy/controller.ts +43 -9
- package/src/domain/iampolicy/steps.ts +164 -4
- package/src/domain/platformclient/__tests__/mint.test.ts +7 -1
- package/src/domain/platformclient/__tests__/verifier-and-guard.test.ts +4 -2
- package/src/domain/platformclient/constants.ts +11 -1
- package/src/domain/platformclient/verifier.ts +7 -2
- package/src/extensions/__tests__/role-reconciliation-composed.test.ts +18 -10
- package/src/extensions/identity.ts +14 -0
- package/src/index.ts +5 -0
- package/src/pipeline/__tests__/steps.test.ts +72 -1
- package/src/pipeline/steps/defaults.ts +6 -1
|
@@ -26,8 +26,12 @@
|
|
|
26
26
|
* The identity is the account the token was minted for: `sub` IS the
|
|
27
27
|
* account id (the mint signs it), so no account read is needed; class
|
|
28
28
|
* `user`, issuer "stigmer", the token carried (the memory-capture gate and
|
|
29
|
-
* the origin guard read its `platform_client_id
|
|
30
|
-
* the
|
|
29
|
+
* the origin guard read its `platform_client_id`, guards decoding their
|
|
30
|
+
* own skip logic from the raw token as extensions/caller-guards.ts
|
|
31
|
+
* prescribes), the email and name the platform asserted, for display
|
|
32
|
+
* only, and the minting client as `platformClientId` for the audit-actor
|
|
33
|
+
* seam, so every resource a minted user writes names the client they came
|
|
34
|
+
* through (stigmer/stigmer#1256).
|
|
31
35
|
*/
|
|
32
36
|
import { Code, ConnectError } from "@connectrpc/connect";
|
|
33
37
|
|
|
@@ -100,6 +104,7 @@ export function newPlatformClientTokenVerifier(
|
|
|
100
104
|
rawToken: token,
|
|
101
105
|
...(email !== undefined ? { email } : {}),
|
|
102
106
|
...(displayName !== undefined ? { displayName } : {}),
|
|
107
|
+
platformClientId,
|
|
103
108
|
};
|
|
104
109
|
},
|
|
105
110
|
};
|
|
@@ -17,10 +17,13 @@
|
|
|
17
17
|
* `owner`, the member `member` — each written AS the account it names
|
|
18
18
|
* (the audit actor), `findMyOrganizations` answers the organization
|
|
19
19
|
* to both, and the marker is set.
|
|
20
|
-
* 3. The founder
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* 3. The founder removes the member, and is refused removing themself
|
|
21
|
+
* (an organization keeps its last owner on the member lanes); the
|
|
22
|
+
* founder's row then goes from the store, the way boot 2 made its
|
|
23
|
+
* shape, and the directory boots once more: NO row returns. The
|
|
24
|
+
* marker, not "zero rows", is what makes the reconciliation one-shot
|
|
25
|
+
* — a founder whose row is gone is not handed the organization back
|
|
26
|
+
* by a reboot.
|
|
24
27
|
*
|
|
25
28
|
* The unit vouches for unsigned JWT-shaped tokens and declares the
|
|
26
29
|
* require-authentication posture with no Authorizer (composed-support.ts;
|
|
@@ -35,7 +38,7 @@ import { tmpdir } from "node:os";
|
|
|
35
38
|
import path from "node:path";
|
|
36
39
|
|
|
37
40
|
import { fromBinary } from "@bufbuild/protobuf";
|
|
38
|
-
import { createClient } from "@connectrpc/connect";
|
|
41
|
+
import { Code, ConnectError, createClient } from "@connectrpc/connect";
|
|
39
42
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
40
43
|
|
|
41
44
|
import { AgentCommandController } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/command_pb";
|
|
@@ -184,16 +187,21 @@ describe("role reconciliation (composed server, OIDC with no unit Authorizer, th
|
|
|
184
187
|
]);
|
|
185
188
|
});
|
|
186
189
|
|
|
187
|
-
it("boot 3: every role
|
|
190
|
+
it("boot 3: every role removed after the marker stays removed — a reboot hands nothing back", async () => {
|
|
188
191
|
const policies = createClient(IamPolicyCommandController, asFounder());
|
|
189
192
|
await policies.revokeOrgAccess({
|
|
190
193
|
identityAccountId: memberId,
|
|
191
194
|
organizationId: ORG,
|
|
192
195
|
});
|
|
193
|
-
await policies
|
|
194
|
-
identityAccountId: founderId,
|
|
195
|
-
|
|
196
|
-
|
|
196
|
+
const lastOwner = await policies
|
|
197
|
+
.revokeOrgAccess({ identityAccountId: founderId, organizationId: ORG })
|
|
198
|
+
.then(
|
|
199
|
+
() => undefined,
|
|
200
|
+
(error: unknown) => ConnectError.from(error),
|
|
201
|
+
);
|
|
202
|
+
expect(lastOwner?.code).toBe(Code.FailedPrecondition);
|
|
203
|
+
// The founder's row, the one left, goes the way boot 2 made its shape.
|
|
204
|
+
await server.store.deleteResourcesByKind(ApiResourceKind.iam_policy);
|
|
197
205
|
expect(await rolesIn(server.store)).toEqual([]);
|
|
198
206
|
await server.shutdown();
|
|
199
207
|
|
|
@@ -74,6 +74,20 @@ export interface CallerIdentity {
|
|
|
74
74
|
*/
|
|
75
75
|
readonly email?: string;
|
|
76
76
|
readonly displayName?: string;
|
|
77
|
+
/**
|
|
78
|
+
* The PlatformClient whose minted user token this caller presented,
|
|
79
|
+
* carried for the audit-actor seam as `email` and `displayName` are:
|
|
80
|
+
* the audit actor records it (ApiResourceAuditActor.platform_client_id),
|
|
81
|
+
* and that server-owned record is what later readers key on, never this
|
|
82
|
+
* field (stigmer/stigmer#1256). Set ONLY by the PlatformClient
|
|
83
|
+
* user-token verifier (domain/platformclient/verifier.ts), from the
|
|
84
|
+
* claim it has just verified and whose client it has just found alive;
|
|
85
|
+
* absent on every other credential. It is provenance, not authorization
|
|
86
|
+
* data (the account the token names is still the principal), and not a
|
|
87
|
+
* guard vocabulary: caller guards decode their own skip logic from
|
|
88
|
+
* `rawToken` (extensions/caller-guards.ts).
|
|
89
|
+
*/
|
|
90
|
+
readonly platformClientId?: string;
|
|
77
91
|
/**
|
|
78
92
|
* The transport the request entered through (C2 Stage 3, ruling R5).
|
|
79
93
|
* Absent = the wire; the in-process interceptor stamps `in-process` on
|
package/src/index.ts
CHANGED
|
@@ -168,6 +168,11 @@ export type {
|
|
|
168
168
|
IamPolicyStoreContractFixture,
|
|
169
169
|
} from "./domain/iampolicy/store-contract.js";
|
|
170
170
|
export { iamPolicyStoreContract } from "./domain/iampolicy/store-contract.js";
|
|
171
|
+
// The owner rule's refusal, the one sentence every door that grants the
|
|
172
|
+
// owner role answers when its caller is no owner: the IamPolicy lanes here,
|
|
173
|
+
// and an edition's own lanes that grant a role on a person's behalf (an
|
|
174
|
+
// owner invitation).
|
|
175
|
+
export { OWNER_ASSIGNMENT_DENIED_MESSAGE } from "./domain/iampolicy/constants.js";
|
|
171
176
|
export type { PolicyGrantScope } from "./extensions/policy-grant-scope.js";
|
|
172
177
|
export type { AuthorizationQueryEngine } from "./extensions/authorization-queries.js";
|
|
173
178
|
// How an access list names a grantee that is not a person
|
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
* state building (status clearing, ULID minting, audit slots #540,
|
|
6
6
|
* visibility default + oss#573 immutability), the loaders' error copy, the
|
|
7
7
|
* best-effort search indexing, visibility validation with cloud-identical
|
|
8
|
-
* copy, reference normalization/validation,
|
|
8
|
+
* copy, reference normalization/validation, operator identity (#400), and
|
|
9
|
+
* the PlatformClient provenance the audit actor records for a minted
|
|
10
|
+
* caller (#1256): stamped from the verified identity only, never from the
|
|
11
|
+
* request, and kept through an update by anyone else.
|
|
9
12
|
*
|
|
10
13
|
* Uses the organization resource (the vertical slice) and agent (whose
|
|
11
14
|
* spec carries ApiResourceReference fields) as vehicles.
|
|
@@ -248,6 +251,74 @@ describe("operator identity (#400) → audit-actor derivation (O2 ruling Q5)", (
|
|
|
248
251
|
});
|
|
249
252
|
});
|
|
250
253
|
|
|
254
|
+
describe("PlatformClient provenance on the audit actor (#1256)", () => {
|
|
255
|
+
const minted = testCallerIdentity({
|
|
256
|
+
identityId: "ida_pat",
|
|
257
|
+
issuer: "stigmer",
|
|
258
|
+
platformClientId: "pcl_dashboard",
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("a minted caller's actor names the client; every other identity stamps it empty", () => {
|
|
262
|
+
expect(auditActorFor(minted).platformClientId).toBe("pcl_dashboard");
|
|
263
|
+
expect(auditActorFor(minted).id).toBe("ida_pat");
|
|
264
|
+
expect(auditActorFor(trustedLocalIdentity()).platformClientId).toBe("");
|
|
265
|
+
expect(auditActorFor(testCallerIdentity()).platformClientId).toBe("");
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it("BuildNewState stamps the caller's client and discards a client the request claimed", async () => {
|
|
269
|
+
const forged = create(OrganizationSchema, {
|
|
270
|
+
apiVersion: "tenancy.stigmer.ai/v1",
|
|
271
|
+
kind: "Organization",
|
|
272
|
+
metadata: { name: "Acme" },
|
|
273
|
+
spec: { description: "a test organization" },
|
|
274
|
+
status: {
|
|
275
|
+
audit: {
|
|
276
|
+
specAudit: {
|
|
277
|
+
createdBy: { id: "ida_pat", platformClientId: "pcl_forged" },
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
});
|
|
282
|
+
const byMinted = new RequestContext(
|
|
283
|
+
OrganizationSchema,
|
|
284
|
+
clone(OrganizationSchema, forged),
|
|
285
|
+
minted,
|
|
286
|
+
ORG,
|
|
287
|
+
);
|
|
288
|
+
await newBuildNewStateStep<typeof OrganizationSchema>().execute(byMinted);
|
|
289
|
+
expect(
|
|
290
|
+
byMinted.newState.status?.audit?.specAudit?.createdBy?.platformClientId,
|
|
291
|
+
).toBe("pcl_dashboard");
|
|
292
|
+
|
|
293
|
+
const byOther = orgCtx(clone(OrganizationSchema, forged));
|
|
294
|
+
await newBuildNewStateStep<typeof OrganizationSchema>().execute(byOther);
|
|
295
|
+
expect(
|
|
296
|
+
byOther.newState.status?.audit?.specAudit?.createdBy?.platformClientId,
|
|
297
|
+
).toBe("");
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it("an update by another caller keeps the creator's client and stamps its own actor", async () => {
|
|
301
|
+
const createCtx = new RequestContext(
|
|
302
|
+
OrganizationSchema,
|
|
303
|
+
org({ name: "Acme" }),
|
|
304
|
+
minted,
|
|
305
|
+
ORG,
|
|
306
|
+
);
|
|
307
|
+
await newResolveSlugStep<typeof OrganizationSchema>().execute(createCtx);
|
|
308
|
+
await newBuildNewStateStep<typeof OrganizationSchema>().execute(createCtx);
|
|
309
|
+
const existing = createCtx.newState;
|
|
310
|
+
|
|
311
|
+
const ctx = orgCtx(org({ id: existing.metadata!.id, name: "Acme Renamed" }));
|
|
312
|
+
ctx.set(EXISTING_RESOURCE_KEY, clone(OrganizationSchema, existing));
|
|
313
|
+
await newBuildUpdateStateStep<typeof OrganizationSchema>().execute(ctx);
|
|
314
|
+
|
|
315
|
+
const audit = ctx.newState.status?.audit?.specAudit;
|
|
316
|
+
expect(audit?.createdBy?.platformClientId).toBe("pcl_dashboard");
|
|
317
|
+
expect(audit?.updatedBy?.id).toBe("test-caller");
|
|
318
|
+
expect(audit?.updatedBy?.platformClientId).toBe("");
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
251
322
|
describe("BuildUpdateState", () => {
|
|
252
323
|
it("preserves id/slug/org/visibility (oss#573), replaces spec, keeps creation audit", async () => {
|
|
253
324
|
// The stored resource, created through the real create steps.
|
|
@@ -237,13 +237,18 @@ export function operatorIdentitySnapshot(): {
|
|
|
237
237
|
* unconfigured → the "system" placeholder, which the runner deliberately
|
|
238
238
|
* demotes to anonymous (SYSTEM_CREATOR_SENTINEL). Verifier-produced
|
|
239
239
|
* identities (O3's OIDC claims onward) stamp their own email/displayName
|
|
240
|
-
* when present, identityId alone otherwise.
|
|
240
|
+
* when present, identityId alone otherwise. A PlatformClient-minted
|
|
241
|
+
* caller also stamps the client it came through (platform_client_id):
|
|
242
|
+
* the account alone cannot say, and the execution-context builder keys
|
|
243
|
+
* the PlatformClient environment layer on this record (#1256). Every
|
|
244
|
+
* other identity stamps it empty, so local-posture bytes are unchanged.
|
|
241
245
|
*/
|
|
242
246
|
export function auditActorFor(identity: CallerIdentity): ApiResourceAuditActor {
|
|
243
247
|
return create(ApiResourceAuditActorSchema, {
|
|
244
248
|
id: identity.identityId,
|
|
245
249
|
email: identity.email ?? "",
|
|
246
250
|
displayName: identity.displayName ?? "",
|
|
251
|
+
platformClientId: identity.platformClientId ?? "",
|
|
247
252
|
});
|
|
248
253
|
}
|
|
249
254
|
|