Skip to content

Commit f0c82ea

Browse files
committed
test(runtime): the capability hard stop at both share-link doors, with the capable-owner control
Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7a70962 commit f0c82ea

1 file changed

Lines changed: 84 additions & 2 deletions

File tree

‎packages/runtime/src/domains/share-links-enforcement-context.test.ts‎

Lines changed: 84 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1070,6 +1070,13 @@ describe('[#21405] the plugin route door answers the same refusal with the same
10701070
* for. Each refusal is read as the pair (`status`, `code`) and against the
10711071
* STORE: a refused mint writes no row.
10721072
*
1073+
* ## The capability hard stop
1074+
*
1075+
* A second object is gated on a capability too (`requiredPermissions`,
1076+
* ADR-0066 D3). An owner who lacks it is refused there: neither alternative
1077+
* applies past that gate. The control is an owner who holds the capability and
1078+
* is refused only by the CRUD grant, and mints.
1079+
*
10731080
* ## What is real here
10741081
*
10751082
* Both doors' production entries — the dispatcher domain body and the plugin's
@@ -1090,6 +1097,10 @@ describe('[#21329] mint authority on an owner-private object, at both doors (rul
10901097
const STRANGER = 'u_stranger';
10911098
const ADMIN = 'u_admin';
10921099
const MANAGER = 'u_manager';
1100+
/** Owns a record on the capability-gated object AND holds its capability. */
1101+
const CAPABLE_OWNER = 'u_capable_owner';
1102+
/** Owner-private AND capability-gated (ADR-0066 D3): a read needs `view_vault`. */
1103+
const VAULT = 'vault_notes';
10931104

10941105
const CONVERSATION_SCHEMA = {
10951106
name: CONV,
@@ -1136,7 +1147,28 @@ describe('[#21329] mint authority on an owner-private object, at both doors (rul
11361147
objects: { [CONV]: { allowEdit: true, writeScope: 'unit' } },
11371148
});
11381149

1139-
const SETS = [MEMBER_BASELINE, MODIFY_ALL, UNIT_WRITER];
1150+
/** The object a capability gates, on top of being owner-private. */
1151+
const VAULT_SCHEMA = {
1152+
name: VAULT,
1153+
access: { default: 'private' },
1154+
requiredPermissions: ['view_vault'],
1155+
fields: {
1156+
id: { name: 'id' },
1157+
title: { name: 'title' },
1158+
owner_id: { name: 'owner_id' },
1159+
},
1160+
publicSharing: { enabled: true, allowedAudiences: ['link_only'], allowedPermissions: ['view'] },
1161+
};
1162+
1163+
/** The capability, and nothing else: no object grant rides with it. */
1164+
const VAULT_CAPABLE: PermissionSet = PermissionSetSchema.parse({
1165+
name: 'conv_vault_capable',
1166+
label: 'Holds view_vault',
1167+
objects: {},
1168+
systemPermissions: ['view_vault'],
1169+
});
1170+
1171+
const SETS = [MEMBER_BASELINE, MODIFY_ALL, UNIT_WRITER, VAULT_CAPABLE];
11401172

11411173
/** The envelope `resolveExecutionContext` assembles for a human member of org A. */
11421174
const principal = (userId: string, permissions: string[]): ExecutionContext =>
@@ -1158,13 +1190,19 @@ describe('[#21329] mint authority on an owner-private object, at both doors (rul
11581190
const stranger = () => principal(STRANGER, ['conv_member_baseline']);
11591191
const admin = () => principal(ADMIN, ['conv_member_baseline', 'conv_modify_all']);
11601192
const manager = () => principal(MANAGER, ['conv_member_baseline', 'conv_unit_writer']);
1193+
const capableOwner = () => principal(CAPABLE_OWNER, ['conv_member_baseline', 'conv_vault_capable']);
11611194

11621195
interface World {
11631196
tables: Record<string, any[]>;
11641197
sharing: any;
11651198
mint(as: ExecutionContext): Promise<{ status: number; body: any }>;
11661199
/** The same mint through the plugin's route door. */
11671200
mintOnPlugin(as: ExecutionContext): Promise<{ status: number; body: any }>;
1201+
/** The same mint on the capability-gated object, at each door. */
1202+
mintVault(recordId: string, as: ExecutionContext): Promise<{ status: number; body: any }>;
1203+
mintVaultOnPlugin(recordId: string, as: ExecutionContext): Promise<{ status: number; body: any }>;
1204+
/** A data-door read of a vault record under `as`. */
1205+
readVault(recordId: string, as: ExecutionContext): Promise<unknown>;
11681206
resolve(token: string): Promise<{ status: number; body: any }>;
11691207
revoke(idOrToken: string, as: ExecutionContext): Promise<{ status: number; body: any }>;
11701208
/** A data-door read of the record under `as` — the visibility leg itself. */
@@ -1175,10 +1213,14 @@ describe('[#21329] mint authority on an owner-private object, at both doors (rul
11751213
async function bootWorld(): Promise<World> {
11761214
const tables: Record<string, any[]> = {
11771215
[CONV]: [{ id: CONV_ID, title: 'My chat', owner_id: OWNER, organization_id: ORG_A }],
1216+
[VAULT]: [
1217+
{ id: 'vault_owner', title: 'Owner\'s note', owner_id: OWNER, organization_id: ORG_A },
1218+
{ id: 'vault_capable', title: 'Capable owner\'s note', owner_id: CAPABLE_OWNER, organization_id: ORG_A },
1219+
],
11781220
sys_share_link: [],
11791221
sys_permission_set: [],
11801222
};
1181-
const engine = makeEngine(tables, { [CONV]: CONVERSATION_SCHEMA });
1223+
const engine = makeEngine(tables, { [CONV]: CONVERSATION_SCHEMA, [VAULT]: VAULT_SCHEMA });
11821224
const services: Record<string, any> = {
11831225
manifest: { register: vi.fn() },
11841226
objectql: engine,
@@ -1245,6 +1287,9 @@ describe('[#21329] mint authority on an owner-private object, at both doors (rul
12451287
sharing: services.sharing,
12461288
mint: (as) => drive('', 'POST', { object: CONV, recordId: CONV_ID }, as),
12471289
mintOnPlugin: (as) => mintOnPluginDoor(engine, svc, as, { object: CONV, recordId: CONV_ID }),
1290+
mintVault: (recordId, as) => drive('', 'POST', { object: VAULT, recordId }, as),
1291+
mintVaultOnPlugin: (recordId, as) => mintOnPluginDoor(engine, svc, as, { object: VAULT, recordId }),
1292+
readVault: async (recordId, as) => engine.find(VAULT, { where: { id: recordId }, limit: 1, context: as }),
12481293
resolve: (token) => drive(`/${token}/resolve`, 'GET', undefined, undefined),
12491294
revoke: (idOrToken, as) => drive(`/${idOrToken}`, 'DELETE', undefined, as),
12501295
read: async (as) => engine.find(CONV, { where: { id: CONV_ID }, limit: 1, context: as }),
@@ -1328,4 +1373,41 @@ describe('[#21329] mint authority on an owner-private object, at both doors (rul
13281373
expect(revoked.status, JSON.stringify(revoked.body)).toBe(200);
13291374
expect(w.tables.sys_share_link[0]?.revoked_at).toBeTruthy();
13301375
}, 30_000);
1376+
it('[capability persona] the vault refuses the owner at the CAPABILITY gate, and the capable owner only at the CRUD grant', async () => {
1377+
const w = await bootWorld();
1378+
const ownerRead = await w.readVault('vault_owner', owner()).then(() => null, (e) => e);
1379+
expect(ownerRead).toMatchObject({ code: 'PERMISSION_DENIED', statusCode: 403 });
1380+
expect(ownerRead?.details?.missingPermissions, 'the owner lacks view_vault').toEqual(['view_vault']);
1381+
const capableRead = await w.readVault('vault_capable', capableOwner()).then(() => null, (e) => e);
1382+
expect(capableRead).toMatchObject({ code: 'PERMISSION_DENIED', statusCode: 403 });
1383+
expect(capableRead?.details?.missingPermissions, 'the capable owner is refused by the CRUD grant, not the capability').toBeUndefined();
1384+
}, 30_000);
1385+
1386+
it('[capability] an owner lacking the object\'s required capability is refused with that refusal at both doors, and nothing lands', async () => {
1387+
const w = await bootWorld();
1388+
const res = await w.mintVault('vault_owner', owner());
1389+
const plugin = await w.mintVaultOnPlugin('vault_owner', owner());
1390+
1391+
expect(res.status, JSON.stringify(res.body)).toBe(403);
1392+
expect(expectDeclaredEnvelope(res).code).toBe('PERMISSION_DENIED');
1393+
// The wire carries the refusal's user-facing sentence only — which gate
1394+
// refused (the capability's `missingPermissions`) stays off the wire, so
1395+
// that half is pinned beside the service, on the rejection itself.
1396+
expect(res.body.error.message).toBe(BUILTIN_OPERATION_MESSAGES.en.permission_denied);
1397+
expect(plugin.status, JSON.stringify(plugin.body)).toBe(403);
1398+
expect(plugin.body).toMatchObject({ success: false, error: { code: 'PERMISSION_DENIED' } });
1399+
expect(w.tables.sys_share_link).toEqual([]);
1400+
// The owner alternative itself holds; the hard stop is what refused.
1401+
expect(await w.sharing.canManageShares(VAULT, 'vault_owner', owner())).toBe(true);
1402+
}, 30_000);
1403+
1404+
it('[capability control] the owner who HOLDS the capability mints on the same object at both doors', async () => {
1405+
const w = await bootWorld();
1406+
const res = await w.mintVault('vault_capable', capableOwner());
1407+
const plugin = await w.mintVaultOnPlugin('vault_capable', capableOwner());
1408+
1409+
expect(res.status, JSON.stringify(res.body)).toBe(201);
1410+
expect(plugin.status, JSON.stringify(plugin.body)).toBe(201);
1411+
expect(w.tables.sys_share_link.map((r) => r.created_by)).toEqual([CAPABLE_OWNER, CAPABLE_OWNER]);
1412+
}, 30_000);
13311413
});

0 commit comments

Comments
 (0)