Skip to content

Commit 8967e34

Browse files
os-zhuangclaude
andcommitted
feat(showcase): demonstrate RLS check (D4) + compound sharing (#1887) as live examples
After landing the two capabilities, show them on the REAL showcase app so AI authors and readers see them in use (the showcase is living documentation): - RLS `check` (ADR-0058 D4): an `invoice_owner_immutable` policy on showcase_invoice — a contributor cannot reassign an invoice they own to a different owner (write-time post-image validation, not a read filter). - Compound sharing condition (ADR-0058 D3 / #1887): a new HighValueRedProjectRule with `record.health == 'red' && record.budget > 100000` — before #1887 a compound `&&` condition was silently skipped; now it compiles to a compound criteria_json and enforces (the AND matters). dogfood proof (showcase-d3-d4-capabilities): asserts the compound condition seeds as `{$and:[{health:'red'},{budget:{$gt:100000}}]}` and matches ONLY the red-AND-high project, and that the RLS check denies an owner reassignment over HTTP while allowing a same-owner edit. Full dogfood 141; no regression. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 2e97ff4 commit 8967e34

2 files changed

Lines changed: 140 additions & 1 deletion

File tree

examples/app-showcase/src/security/index.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,22 @@ export const ContributorPermissionSet = {
8080
enabled: true,
8181
priority: 10,
8282
},
83+
// [ADR-0058 D4] RLS `check` — write-side post-image validation (NOT a read
84+
// filter). On UPDATE the new row must still be owned by the caller, so a
85+
// contributor cannot reassign an invoice they own to someone else. `check`
86+
// is compiled by the canonical CEL compiler and matched against the post-
87+
// image (pre-image ∪ change set); a violating write is denied (fail-closed).
88+
{
89+
name: 'invoice_owner_immutable',
90+
label: 'Invoice Owner Cannot Be Reassigned',
91+
description: 'A contributor cannot change an invoice they own to a different owner (write-time CHECK, ADR-0058 D4).',
92+
object: 'showcase_invoice',
93+
operation: 'update' as const,
94+
check: "owner == current_user.email",
95+
roles: ['contributor'],
96+
enabled: true,
97+
priority: 10,
98+
},
8399
],
84100
};
85101

@@ -124,6 +140,25 @@ export const RedProjectSharingRule = {
124140
active: true,
125141
};
126142

143+
/**
144+
* [ADR-0058 D3 / closes #1887] criteria-based with a COMPOUND CEL condition.
145+
* Before #1887 a multi-clause `&&` condition was silently skipped (the sharing
146+
* rule was decorative metadata); now it compiles to a compound `criteria_json`
147+
* and enforces. Shares only projects that are BOTH at-risk (red) AND high-budget
148+
* with managers — the AND matters: a red but low-budget project is NOT shared.
149+
*/
150+
export const HighValueRedProjectRule = {
151+
type: 'criteria' as const,
152+
name: 'share_high_value_red_projects_with_managers',
153+
label: 'High-Value Red Projects → Managers',
154+
description: 'Share at-risk (red health) projects over the budget threshold with managers (compound condition, ADR-0058 D3).',
155+
object: 'showcase_project',
156+
condition: "record.health == 'red' && record.budget > 100000",
157+
accessLevel: 'read' as const,
158+
sharedWith: { type: 'role' as const, value: 'manager' },
159+
active: true,
160+
};
161+
127162
/** owner-based: a contributor's tasks are shared read-only with managers. */
128163
export const ContributorTaskSharingRule = {
129164
type: 'owner' as const,
@@ -156,5 +191,5 @@ export const ShowcasePolicy = {
156191

157192
export const allRoles = [ContributorRole, ManagerRole, ExecRole];
158193
export const allPermissionSets = [ContributorPermissionSet, MemberDefaultProfile];
159-
export const allSharingRules = [RedProjectSharingRule, ContributorTaskSharingRule];
194+
export const allSharingRules = [RedProjectSharingRule, HighValueRedProjectRule, ContributorTaskSharingRule];
160195
export const allPolicies = [ShowcasePolicy];
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// SHOWCASE proof for the two new authz capabilities on the REAL showcase app:
4+
// • ADR-0058 D3 / #1887 — a COMPOUND sharing `condition` (`&&`) compiles to a
5+
// compound criteria_json and enforces (the AND matters; before #1887 it was
6+
// silently skipped).
7+
// • ADR-0058 D4 — an RLS `check` clause validates the write POST-IMAGE: a
8+
// contributor cannot reassign an invoice they own to a different owner.
9+
//
10+
// @proof: showcase-d3-d4-capabilities
11+
12+
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
13+
import showcaseStack from '@objectstack/example-showcase';
14+
import { bootStack, type VerifyStack } from '@objectstack/verify';
15+
import { SecurityPlugin, securityDefaultPermissionSets } from '@objectstack/plugin-security';
16+
import { PermissionSetSchema } from '@objectstack/spec/security';
17+
18+
const MEMBER = 'd34-member@verify.test';
19+
const SYS = { context: { isSystem: true } } as const;
20+
21+
// Member set: invoice CRUD + an OWNER read/pre-image policy AND the D4 CHECK
22+
// (post-image must keep owner == caller). Mirrors the showcase contributor's
23+
// `invoice_owner_immutable` rule, exercised end-to-end over HTTP.
24+
const memberSet = PermissionSetSchema.parse({
25+
name: 'showcase_d34_member',
26+
label: 'D3/D4 Member',
27+
isProfile: true,
28+
objects: {
29+
showcase_project: { allowRead: true },
30+
showcase_invoice: { allowRead: true, allowCreate: true, allowEdit: true },
31+
},
32+
rowLevelSecurity: [
33+
{ name: 'inv_own_read', object: 'showcase_invoice', operation: 'select', using: 'owner == current_user.email' },
34+
{ name: 'inv_own_write', object: 'showcase_invoice', operation: 'update', using: 'owner == current_user.email' },
35+
{ name: 'inv_owner_check', object: 'showcase_invoice', operation: 'update', check: 'owner == current_user.email' },
36+
],
37+
});
38+
39+
describe('showcase: D3 compound sharing (#1887) + D4 RLS check', () => {
40+
let stack: VerifyStack;
41+
let ql: any;
42+
let token: string;
43+
let invId: string;
44+
45+
beforeAll(async () => {
46+
stack = await bootStack(showcaseStack, {
47+
security: new SecurityPlugin({
48+
defaultPermissionSets: [...securityDefaultPermissionSets, memberSet],
49+
fallbackPermissionSet: 'showcase_d34_member',
50+
}),
51+
});
52+
await stack.signIn();
53+
token = await stack.signUp(MEMBER);
54+
ql = await stack.kernel.getServiceAsync('objectql');
55+
56+
const idOf = (r: any) => r?.id ?? r?.record?.id ?? r;
57+
const acct = idOf(await ql.insert('showcase_account', { name: 'D34 Co', status: 'prospect' }, SYS));
58+
59+
// Three projects: only the at-risk (red) AND high-budget (>100k) one matches.
60+
await ql.insert('showcase_project', { id: 'pj_red_hi', name: 'Red-Hi', account: acct, status: 'planned', health: 'red', budget: 250000 }, SYS);
61+
await ql.insert('showcase_project', { id: 'pj_red_lo', name: 'Red-Lo', account: acct, status: 'planned', health: 'red', budget: 40000 }, SYS);
62+
await ql.insert('showcase_project', { id: 'pj_grn_hi', name: 'Grn-Hi', account: acct, status: 'planned', health: 'green', budget: 250000 }, SYS);
63+
64+
// An invoice owned by the member (system insert sidesteps authoring rules).
65+
invId = idOf(await ql.insert('showcase_invoice', { name: 'INV-D34', account: acct, owner: MEMBER, status: 'draft' }, SYS));
66+
}, 90_000);
67+
68+
afterAll(async () => { await stack?.stop(); });
69+
70+
// ── ADR-0058 D3 / #1887 ────────────────────────────────────────────────────
71+
it('compound sharing condition is SEEDED as a compound criteria_json (not skipped)', async () => {
72+
const rule = await ql.findOne('sys_sharing_rule', { where: { name: 'share_high_value_red_projects_with_managers' }, context: SYS.context });
73+
expect(rule, 'compound rule was seeded').toBeTruthy();
74+
expect(JSON.parse(rule.criteria_json)).toEqual({
75+
$and: [{ health: 'red' }, { budget: { $gt: 100000 } }],
76+
});
77+
});
78+
79+
it('the compound criteria_json matches ONLY the project satisfying BOTH clauses', async () => {
80+
const rule = await ql.findOne('sys_sharing_rule', { where: { name: 'share_high_value_red_projects_with_managers' }, context: SYS.context });
81+
const criteria = JSON.parse(rule.criteria_json);
82+
// Apply the SEEDED compound criteria to our three projects: only red-AND-high passes.
83+
const hit = await ql.find('showcase_project', {
84+
where: { $and: [criteria, { id: { $in: ['pj_red_hi', 'pj_red_lo', 'pj_grn_hi'] } }] },
85+
fields: ['id'], context: SYS.context,
86+
});
87+
expect((hit ?? []).map((r: any) => r.id).sort()).toEqual(['pj_red_hi']);
88+
// The rule also evaluates end-to-end (matched count includes seed data).
89+
const rules: any = stack.kernel.getService('sharingRules');
90+
const res = await rules.evaluateRule('share_high_value_red_projects_with_managers', SYS.context);
91+
expect(res.matchedRecords, 'rule is evaluable + at least Red-Hi matches').toBeGreaterThanOrEqual(1);
92+
});
93+
94+
// ── ADR-0058 D4 ────────────────────────────────────────────────────────────
95+
it('RLS check ALLOWS an update that keeps the owner (post-image valid)', async () => {
96+
const r = await stack.apiAs(token, 'PATCH', `/data/showcase_invoice/${invId}`, { name: 'INV-D34-v2' });
97+
expect(r.status, 'owner unchanged → write allowed').toBeLessThan(300);
98+
});
99+
100+
it('RLS check DENIES reassigning the invoice to a different owner (post-image invalid)', async () => {
101+
const r = await stack.apiAs(token, 'PATCH', `/data/showcase_invoice/${invId}`, { owner: 'someone-else@verify.test' });
102+
expect(r.status, 'owner reassignment → write denied (fail-closed)').toBeGreaterThanOrEqual(400);
103+
});
104+
});

0 commit comments

Comments
 (0)