Skip to content

Commit 7903048

Browse files
hotlongclaude
andcommitted
test(runtime): the write-scope pins hand the vetted organization on the execution context
Claude-Session: https://claude.ai/code/session_local_1d2a197c-c20e-4e90-9be8-413d4d432289 Co-authored-by: Claude <noreply@anthropic.com>
1 parent b2c374d commit 7903048

2 files changed

Lines changed: 43 additions & 28 deletions

File tree

‎packages/runtime/src/domains/meta-save-capability-gate.test.ts‎

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -276,8 +276,11 @@ describe('#7019 — dispatcher PUT /meta/:type/:name: the capability gate', () =
276276
describe('#12702 — dispatcher PUT: `manage_org_presentation`, org-scoped tier-A admission', () => {
277277
/**
278278
* The `boot()` double above, plus an auth service whose session carries an
279-
* active organization — the value `deps.resolveActiveOrganizationId` reads,
280-
* which the gate and the write threading now share.
279+
* active organization — and [#20408] `caller()`, the execution context the
280+
* identity resolver builds from that session: its VETTED active
281+
* organization rides it as `tenantId` (no tenancy wall here, so the claim
282+
* is kept). That is the value the gate and the write threading share — the
283+
* one `RestServer`'s `PUT` door reads — never the session's claim as stored.
281284
*/
282285
function bootOrg(activeOrganizationId?: string) {
283286
const saveMetaItem = vi.fn(async ({ type, name }: any) => ({ success: true, type, name }));
@@ -290,7 +293,9 @@ describe('#12702 — dispatcher PUT: `manage_org_presentation`, org-scoped tier-
290293
getService: (n: string) => (n === 'protocol' ? protocol : n === 'auth' ? auth : null),
291294
},
292295
} as any;
293-
return { dispatcher: new HttpDispatcher(kernel), saveMetaItem };
296+
const caller = (identity: Record<string, unknown>) =>
297+
ctx({ ...identity, ...(activeOrganizationId ? { tenantId: activeOrganizationId } : {}) });
298+
return { dispatcher: new HttpDispatcher(kernel), saveMetaItem, caller };
294299
}
295300

296301
const HOLDER = { userId: 'u_orgadmin', systemPermissions: ['manage_org_presentation'] };
@@ -299,7 +304,7 @@ describe('#12702 — dispatcher PUT: `manage_org_presentation`, org-scoped tier-
299304
const stack = bootOrg('org_a');
300305

301306
const res = await stack.dispatcher.handleMetadata(
302-
'/view/org_grid', ctx(HOLDER), 'PUT', { name: 'org_grid', label: 'Org Grid' },
307+
'/view/org_grid', stack.caller(HOLDER), 'PUT', { name: 'org_grid', label: 'Org Grid' },
303308
);
304309

305310
expect(res.response?.status).toBe(200);
@@ -315,7 +320,7 @@ describe('#12702 — dispatcher PUT: `manage_org_presentation`, org-scoped tier-
315320
const stack = bootOrg('org_a');
316321

317322
const res = await stack.dispatcher.handleMetadata(
318-
'/email_templates/welcome', ctx(HOLDER), 'PUT', { name: 'welcome', subject: 'Hi' },
323+
'/email_templates/welcome', stack.caller(HOLDER), 'PUT', { name: 'welcome', subject: 'Hi' },
319324
);
320325

321326
expect(res.response?.status).toBe(200);
@@ -333,7 +338,7 @@ describe('#12702 — dispatcher PUT: `manage_org_presentation`, org-scoped tier-
333338
const stack = bootOrg('org_a');
334339

335340
const res = await stack.dispatcher.handleMetadata(
336-
path, ctx(HOLDER), 'PUT', { label: 'x' },
341+
path, stack.caller(HOLDER), 'PUT', { label: 'x' },
337342
);
338343

339344
expect(res.response?.status).toBe(403);
@@ -345,7 +350,7 @@ describe('#12702 — dispatcher PUT: `manage_org_presentation`, org-scoped tier-
345350
const stack = bootOrg(undefined);
346351

347352
const res = await stack.dispatcher.handleMetadata(
348-
'/view/org_grid', ctx(HOLDER), 'PUT', { name: 'org_grid', label: 'Org Grid' },
353+
'/view/org_grid', stack.caller(HOLDER), 'PUT', { name: 'org_grid', label: 'Org Grid' },
349354
);
350355

351356
expect(res.response?.status).toBe(403);
@@ -360,7 +365,7 @@ describe('#12702 — dispatcher PUT: `manage_org_presentation`, org-scoped tier-
360365
const stack = bootOrg('org_a');
361366

362367
const res = await stack.dispatcher.handleMetadata(
363-
'/view/org_grid', ctx(HOLDER), 'PUT',
368+
'/view/org_grid', stack.caller(HOLDER), 'PUT',
364369
{ name: 'org_grid', label: 'Org Grid', organization_id: 'org_b', organizationId: 'org_b' },
365370
);
366371

@@ -375,7 +380,7 @@ describe('#12702 — dispatcher PUT: `manage_org_presentation`, org-scoped tier-
375380

376381
const res = await stack.dispatcher.handleMetadata(
377382
'/view/org_grid',
378-
ctx({ userId: 'u_author', systemPermissions: ['manage_metadata'] }),
383+
stack.caller({ userId: 'u_author', systemPermissions: ['manage_metadata'] }),
379384
'PUT',
380385
{ name: 'org_grid', label: 'Org Grid' },
381386
);

‎packages/runtime/src/meta-write-org-scope.test.ts‎

Lines changed: 29 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -254,10 +254,20 @@ function makeDispatcher(
254254
* file order- and shard-independent, which is how the 403 reached CI at all:
255255
* the branch was cut before #7027 merged, so the gate did not exist locally.
256256
*/
257-
const ctx = (): any => ({
257+
const ctx = (activeOrganizationId?: string): any => ({
258258
request: { headers: {} },
259259
environmentId: 'env_1',
260-
executionContext: { userId: 'usr_1', systemPermissions: ['manage_metadata'] },
260+
// [#20408] The execution context the identity resolver builds from the
261+
// session `makeDispatcher` answers: that session's active organization rides
262+
// it as `tenantId` (there is no tenancy wall here, so the claim is kept, not
263+
// dropped). The dispatcher's `/meta` doors scope by THIS value — the vetted
264+
// one `RestServer`'s doors read — never the claim as stored, so a case whose
265+
// session has an active organization hands the same one in here.
266+
executionContext: {
267+
userId: 'usr_1',
268+
systemPermissions: ['manage_metadata'],
269+
...(activeOrganizationId ? { tenantId: activeOrganizationId } : {}),
270+
},
261271
});
262272

263273
function makeStack(activeOrganizationId: string | undefined, metadataService?: unknown) {
@@ -361,7 +371,7 @@ describe('#7018 — the registry decides whether a metadata write carries the se
361371
it('a NON-overridable type lands env-wide even though the session has an active org', async () => {
362372
const { engine, dispatcher } = makeStack(ACTIVE_ORG);
363373

364-
const res = responseOf(await dispatcher.handleMetadata(`/flow/${FLOW.name}`, ctx(), 'PUT', FLOW));
374+
const res = responseOf(await dispatcher.handleMetadata(`/flow/${FLOW.name}`, ctx(ACTIVE_ORG), 'PUT', FLOW));
365375

366376
expect(res.status).toBe(200);
367377
const row = metaRow(engine, 'flow', FLOW.name);
@@ -383,7 +393,7 @@ describe('#7018 — the registry decides whether a metadata write carries the se
383393
fields: { subject: { type: 'text', label: 'Subject' } },
384394
};
385395

386-
const res = responseOf(await dispatcher.handleMetadata(`/object/${OBJECT.name}`, ctx(), 'PUT', OBJECT));
396+
const res = responseOf(await dispatcher.handleMetadata(`/object/${OBJECT.name}`, ctx(ACTIVE_ORG), 'PUT', OBJECT));
387397

388398
expect(res.status).toBe(200);
389399
expect(metaRow(engine, 'object', OBJECT.name)!.organization_id).toBeNull();
@@ -398,7 +408,7 @@ describe('#7018 — the registry decides whether a metadata write carries the se
398408
const withOrg = makeStack(ACTIVE_ORG);
399409
const withoutOrg = makeStack(undefined);
400410

401-
const a = responseOf(await withOrg.dispatcher.handleMetadata(`/flow/${FLOW.name}`, ctx(), 'PUT', FLOW));
411+
const a = responseOf(await withOrg.dispatcher.handleMetadata(`/flow/${FLOW.name}`, ctx(ACTIVE_ORG), 'PUT', FLOW));
402412
const b = responseOf(await withoutOrg.dispatcher.handleMetadata(`/flow/${FLOW.name}`, ctx(), 'PUT', FLOW));
403413

404414
expect(a.status).toBe(200);
@@ -409,7 +419,7 @@ describe('#7018 — the registry decides whether a metadata write carries the se
409419
it('CONTROL — an `allowOrgOverride: true` type keeps its org scoping exactly as before', async () => {
410420
const { engine, dispatcher } = makeStack(ACTIVE_ORG);
411421

412-
const res = responseOf(await dispatcher.handleMetadata(`/view/${VIEW.name}`, ctx(), 'PUT', VIEW));
422+
const res = responseOf(await dispatcher.handleMetadata(`/view/${VIEW.name}`, ctx(ACTIVE_ORG), 'PUT', VIEW));
413423

414424
expect(res.status).toBe(200);
415425
// ADR-0005's per-org overlay is the point of the flag and must survive
@@ -420,7 +430,7 @@ describe('#7018 — the registry decides whether a metadata write carries the se
420430
it('CONTROL — the plural URL spelling of an overridable type is scoped the same way', async () => {
421431
const { engine, dispatcher } = makeStack(ACTIVE_ORG);
422432

423-
const res = responseOf(await dispatcher.handleMetadata(`/views/${VIEW.name}`, ctx(), 'PUT', VIEW));
433+
const res = responseOf(await dispatcher.handleMetadata(`/views/${VIEW.name}`, ctx(ACTIVE_ORG), 'PUT', VIEW));
424434

425435
expect(res.status).toBe(200);
426436
expect(metaRow(engine, 'view', VIEW.name)!.organization_id).toBe(ACTIVE_ORG);
@@ -449,7 +459,7 @@ describe('#7018 — the registry decides whether a metadata write carries the se
449459
success: true, publishedCount: 0, failedCount: 0, published: [], failed: [],
450460
});
451461

452-
const res = responseOf(await dispatcher.handlePackages('/crm_pkg/publish-drafts', 'POST', {}, {}, ctx()));
462+
const res = responseOf(await dispatcher.handlePackages('/crm_pkg/publish-drafts', 'POST', {}, {}, ctx(ACTIVE_ORG)));
453463

454464
expect(res.status).toBe(200);
455465
expect(res.body.data.unhiddenApps).toEqual([APP.name]);
@@ -484,7 +494,7 @@ describe('#7018 — the registry decides whether a metadata write carries the se
484494
});
485495
error.mockClear();
486496

487-
const res = responseOf(await dispatcher.handlePackages('/crm_pkg/publish-drafts', 'POST', {}, {}, ctx()));
497+
const res = responseOf(await dispatcher.handlePackages('/crm_pkg/publish-drafts', 'POST', {}, {}, ctx(ACTIVE_ORG)));
488498

489499
expect(res.body.data.unhiddenApps).toEqual([APP.name]);
490500
const flipComplaints = (error.mock.calls as unknown[][])
@@ -600,7 +610,7 @@ describe('#10503 the dispatcher /metadata transport decides org scope on the FOL
600610
// receipted 200, served by nothing.
601611
const viaPlural = makeStack(ACTIVE_ORG);
602612
const resPlural = responseOf(
603-
await viaPlural.dispatcher.handleMetadata(`/${plural}/${item.name}`, ctx(), 'PUT', item),
613+
await viaPlural.dispatcher.handleMetadata(`/${plural}/${item.name}`, ctx(ACTIVE_ORG), 'PUT', item),
604614
);
605615
expect(resPlural.status).toBe(200);
606616
const pluralRow = metaRow(viaPlural.engine, singular, item.name);
@@ -612,7 +622,7 @@ describe('#10503 the dispatcher /metadata transport decides org scope on the FOL
612622
// must not have moved.
613623
const viaSingular = makeStack(ACTIVE_ORG);
614624
const resSingular = responseOf(
615-
await viaSingular.dispatcher.handleMetadata(`/${singular}/${item.name}`, ctx(), 'PUT', item),
625+
await viaSingular.dispatcher.handleMetadata(`/${singular}/${item.name}`, ctx(ACTIVE_ORG), 'PUT', item),
616626
);
617627
expect(resSingular.status).toBe(200);
618628
const singularRow = metaRow(viaSingular.engine, singular, item.name);
@@ -628,10 +638,10 @@ describe('#10503 the dispatcher /metadata transport decides org scope on the FOL
628638
const { engine, dispatcher } = makeStack(ACTIVE_ORG);
629639

630640
expect(responseOf(
631-
await dispatcher.handleMetadata(`/${plural}/${item.name}`, ctx(), 'PUT', item),
641+
await dispatcher.handleMetadata(`/${plural}/${item.name}`, ctx(ACTIVE_ORG), 'PUT', item),
632642
).status).toBe(200);
633643
expect(responseOf(
634-
await dispatcher.handleMetadata(`/${singular}/${item.name}`, ctx(), 'PUT', item),
644+
await dispatcher.handleMetadata(`/${singular}/${item.name}`, ctx(ACTIVE_ORG), 'PUT', item),
635645
).status).toBe(200);
636646

637647
const rows = engine.metaRows().filter(
@@ -669,7 +679,7 @@ describe('#10503 the dispatcher /metadata transport decides org scope on the FOL
669679
fields: { subject: { type: 'text', label: 'Subject' } },
670680
};
671681

672-
const res = responseOf(await dispatcher.handleMetadata(`/objects/${OBJECT.name}`, ctx(), 'PUT', OBJECT));
682+
const res = responseOf(await dispatcher.handleMetadata(`/objects/${OBJECT.name}`, ctx(ACTIVE_ORG), 'PUT', OBJECT));
673683

674684
expect(res.status).toBe(200);
675685
expect(metaRow(engine, 'object', OBJECT.name)!.organization_id).toBeNull();
@@ -695,7 +705,7 @@ describe('#10503 the dispatcher /metadata transport decides org scope on the FOL
695705
const saveMetaItem = vi.fn().mockResolvedValue({ success: true, version: 'v1', seq: 1 });
696706
protocol.saveMetaItem = saveMetaItem;
697707

698-
await dispatcher.handleMetadata(`/${spelling}/specimen`, ctx(), 'PUT', { name: 'specimen' });
708+
await dispatcher.handleMetadata(`/${spelling}/specimen`, ctx(ACTIVE_ORG), 'PUT', { name: 'specimen' });
699709

700710
expect(
701711
saveMetaItem.mock.calls[0][0].organizationId,
@@ -714,7 +724,7 @@ describe('#10503 the dispatcher /metadata transport decides org scope on the FOL
714724
const saveMetaItem = vi.fn().mockResolvedValue({ success: true, version: 'v1', seq: 1 });
715725
protocol.saveMetaItem = saveMetaItem;
716726

717-
await dispatcher.handleMetadata('/translations/zh-CN', ctx(), 'PUT', { name: 'zh-CN' });
727+
await dispatcher.handleMetadata('/translations/zh-CN', ctx(ACTIVE_ORG), 'PUT', { name: 'zh-CN' });
718728

719729
const request = saveMetaItem.mock.calls[0][0];
720730
expect(request.type).toBe('translations');
@@ -749,11 +759,11 @@ describe('#10503 the dispatcher /metadata transport decides org scope on the FOL
749759
it(`/${plural}/:name/published answers what /${singular}/:name/published answers`, async () => {
750760
const viaPlural = publishedStore(singular, item.name);
751761
const pluralRes = responseOf(await makeStack(ACTIVE_ORG, viaPlural.service)
752-
.dispatcher.handleMetadata(`/${plural}/${item.name}/published`, ctx(), 'GET'));
762+
.dispatcher.handleMetadata(`/${plural}/${item.name}/published`, ctx(ACTIVE_ORG), 'GET'));
753763

754764
const viaSingular = publishedStore(singular, item.name);
755765
const singularRes = responseOf(await makeStack(ACTIVE_ORG, viaSingular.service)
756-
.dispatcher.handleMetadata(`/${singular}/${item.name}/published`, ctx(), 'GET'));
766+
.dispatcher.handleMetadata(`/${singular}/${item.name}/published`, ctx(ACTIVE_ORG), 'GET'));
757767

758768
// Same status and same body — before the fold the plural was a
759769
// 404 for an item that IS published, the singular a 200.
@@ -775,7 +785,7 @@ describe('#10503 the dispatcher /metadata transport decides org scope on the FOL
775785
expect(canonicalMetaUrlType('webhook')).toBe('webhook');
776786
const store = publishedStore('webhook', 'stripe');
777787
const res = responseOf(await makeStack(ACTIVE_ORG, store.service)
778-
.dispatcher.handleMetadata('/webhook/stripe/published', ctx(), 'GET'));
788+
.dispatcher.handleMetadata('/webhook/stripe/published', ctx(ACTIVE_ORG), 'GET'));
779789

780790
expect(res.status).toBe(200);
781791
expect(store.asked).toEqual([['webhook', 'stripe']]);

0 commit comments

Comments
 (0)