Skip to content

Commit 6ca20b3

Browse files
xuyushun441-sysos-zhuangclaude
authored
feat(security): ADR-0058 D1 follow-through — RLS predicates migrate to canonical CEL (#2099)
The expression surface now speaks ONE language. Migrate every seeded RLS `using`/`check` from the legacy SQL-ish dialect (`=`, `IN (...)`) to pure CEL (`==`, `in`): - default-permission-sets.ts (49) + app-showcase (2): all owner/tenant/self policies are CEL. - rls.zod.ts RLS.ownerPolicy/tenantPolicy/allowAllPolicy helper factories now emit CEL templates; doc examples updated to match. The `sqlPredicateToCel` bridge is downgraded to a DEPRECATED transitional shim: compileExpression warns when it actually rewrites a SQL-style predicate, so authored policies migrate, while stored/legacy SQL still compiles (no silent deny on existing data). The bridge is IDEMPOTENT on CEL input, so the migrated seeds pass through as no-ops with no warn. No runtime behavior change: CEL and the old SQL form compile to the identical FilterCondition (verified). New tests prove the bridge (SQL still compiles + warns; CEL is silent). Green: spec 6599, plugin-security 122, plugin-sharing 65, formula 201, dogfood 130. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 4557a17 commit 6ca20b3

9 files changed

Lines changed: 162 additions & 96 deletions

File tree

.changeset/rls-using-to-cel.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/plugin-security": minor
4+
---
5+
6+
ADR-0058 D1 follow-through — RLS predicates are now canonical CEL. Migrated every
7+
seeded RLS `using`/`check` (default permission sets, showcase, and the
8+
`RLS.ownerPolicy`/`tenantPolicy`/`allowAllPolicy` helper factories) from the
9+
legacy SQL-ish form (`=`, `IN (...)`) to pure CEL (`==`, `in`), so authors and AI
10+
learn ONE expression language. The `sqlPredicateToCel` bridge is retained as a
11+
DEPRECATED transitional shim: a stored SQL-style predicate still compiles (no
12+
silent deny on legacy data) but emits a deprecation warn; canonical CEL passes
13+
through as a no-op. No runtime behavior change — CEL and the old SQL form compile
14+
to the identical FilterCondition.

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ export const ContributorPermissionSet = {
6060
description: 'Contributors can only select tasks assigned to them.',
6161
object: 'showcase_task',
6262
operation: 'select' as const,
63-
using: "assignee = current_user.email",
63+
using: "assignee == current_user.email",
6464
roles: ['contributor'],
6565
enabled: true,
6666
priority: 10,
@@ -75,7 +75,7 @@ export const ContributorPermissionSet = {
7575
description: "Contributors only see invoices they own; their lines follow via controlled_by_parent.",
7676
object: 'showcase_invoice',
7777
operation: 'select' as const,
78-
using: "owner = current_user.email",
78+
using: "owner == current_user.email",
7979
roles: ['contributor'],
8080
enabled: true,
8181
priority: 10,

packages/plugins/plugin-security/src/objects/default-permission-sets.ts

Lines changed: 49 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ export const defaultPermissionSets: PermissionSet[] = [
157157
name: 'tenant_isolation',
158158
object: '*',
159159
operation: 'all',
160-
using: 'organization_id = current_user.organization_id',
160+
using: 'organization_id == current_user.organization_id',
161161
},
162162
// ── better-auth system tables that lack `organization_id` and would
163163
// otherwise be denied by the wildcard policy. Same self-only
@@ -167,79 +167,79 @@ export const defaultPermissionSets: PermissionSet[] = [
167167
name: 'sys_organization_self',
168168
object: 'sys_organization',
169169
operation: 'all',
170-
using: 'id = current_user.organization_id',
170+
using: 'id == current_user.organization_id',
171171
},
172172
{
173173
name: 'sys_user_self',
174174
object: 'sys_user',
175175
operation: 'select',
176-
using: 'id = current_user.id',
176+
using: 'id == current_user.id',
177177
},
178178
{
179179
name: 'sys_user_org_members',
180180
object: 'sys_user',
181181
operation: 'select',
182-
using: 'id IN (current_user.org_user_ids)',
182+
using: 'id in current_user.org_user_ids',
183183
},
184184
{
185185
name: 'sys_session_self',
186186
object: 'sys_session',
187187
operation: 'all',
188-
using: 'user_id = current_user.id',
188+
using: 'user_id == current_user.id',
189189
},
190190
{
191191
name: 'sys_account_self',
192192
object: 'sys_account',
193193
operation: 'select',
194-
using: 'user_id = current_user.id',
194+
using: 'user_id == current_user.id',
195195
},
196196
{
197197
name: 'sys_team_member_self',
198198
object: 'sys_team_member',
199199
operation: 'select',
200-
using: 'user_id = current_user.id',
200+
using: 'user_id == current_user.id',
201201
},
202202
{
203203
name: 'sys_two_factor_self',
204204
object: 'sys_two_factor',
205205
operation: 'all',
206-
using: 'user_id = current_user.id',
206+
using: 'user_id == current_user.id',
207207
},
208208
{
209209
name: 'sys_user_preference_self',
210210
object: 'sys_user_preference',
211211
operation: 'all',
212-
using: 'user_id = current_user.id',
212+
using: 'user_id == current_user.id',
213213
},
214214
{
215215
name: 'sys_api_key_self',
216216
object: 'sys_api_key',
217217
operation: 'all',
218-
using: 'user_id = current_user.id',
218+
using: 'user_id == current_user.id',
219219
},
220220
{
221221
name: 'sys_device_code_self',
222222
object: 'sys_device_code',
223223
operation: 'all',
224-
using: 'user_id = current_user.id',
224+
using: 'user_id == current_user.id',
225225
},
226226
{
227227
name: 'sys_oauth_access_token_self',
228228
object: 'sys_oauth_access_token',
229229
operation: 'select',
230-
using: 'user_id = current_user.id',
230+
using: 'user_id == current_user.id',
231231
},
232232
{
233233
name: 'sys_oauth_refresh_token_self',
234234
object: 'sys_oauth_refresh_token',
235235
operation: 'select',
236-
using: 'user_id = current_user.id',
236+
using: 'user_id == current_user.id',
237237
},
238238
{
239239
name: 'sys_oauth_consent_self',
240240
object: 'sys_oauth_consent',
241241
operation: 'all',
242-
using: 'user_id = current_user.id',
242+
using: 'user_id == current_user.id',
243243
},
244244
// OAuth applications a user has registered themselves (self-service
245245
// developer flow exposed in the Account app's Developer section).
@@ -249,7 +249,7 @@ export const defaultPermissionSets: PermissionSet[] = [
249249
name: 'sys_oauth_application_self',
250250
object: 'sys_oauth_application',
251251
operation: 'all',
252-
using: 'user_id = current_user.id',
252+
using: 'user_id == current_user.id',
253253
},
254254
// Org-scoped visibility for organization-owned identity-adjacent
255255
// tables. Org admins may inspect their own org's invitations and
@@ -258,19 +258,19 @@ export const defaultPermissionSets: PermissionSet[] = [
258258
name: 'sys_member_org',
259259
object: 'sys_member',
260260
operation: 'select',
261-
using: 'organization_id = current_user.organization_id',
261+
using: 'organization_id == current_user.organization_id',
262262
},
263263
{
264264
name: 'sys_invitation_org',
265265
object: 'sys_invitation',
266266
operation: 'select',
267-
using: 'organization_id = current_user.organization_id',
267+
using: 'organization_id == current_user.organization_id',
268268
},
269269
{
270270
name: 'sys_team_org',
271271
object: 'sys_team',
272272
operation: 'select',
273-
using: 'organization_id = current_user.organization_id',
273+
using: 'organization_id == current_user.organization_id',
274274
},
275275
],
276276
}),
@@ -293,7 +293,7 @@ export const defaultPermissionSets: PermissionSet[] = [
293293
name: 'tenant_isolation',
294294
object: '*',
295295
operation: 'all',
296-
using: 'organization_id = current_user.organization_id',
296+
using: 'organization_id == current_user.organization_id',
297297
},
298298
// Owner-scoped writes/deletes for rank-and-file members: you may modify
299299
// and delete the records you created, not other users'. Keyed on
@@ -311,13 +311,13 @@ export const defaultPermissionSets: PermissionSet[] = [
311311
name: 'owner_only_writes',
312312
object: '*',
313313
operation: 'update',
314-
using: 'created_by = current_user.id',
314+
using: 'created_by == current_user.id',
315315
},
316316
{
317317
name: 'owner_only_deletes',
318318
object: '*',
319319
operation: 'delete',
320-
using: 'created_by = current_user.id',
320+
using: 'created_by == current_user.id',
321321
},
322322
// ── better-auth system tables that lack `organization_id` and would
323323
// otherwise be left unprotected by the wildcard rule above. ────
@@ -334,13 +334,13 @@ export const defaultPermissionSets: PermissionSet[] = [
334334
name: 'sys_organization_self',
335335
object: 'sys_organization',
336336
operation: 'all',
337-
using: 'id = current_user.organization_id',
337+
using: 'id == current_user.organization_id',
338338
},
339339
{
340340
name: 'sys_user_self',
341341
object: 'sys_user',
342342
operation: 'select',
343-
using: 'id = current_user.id',
343+
using: 'id == current_user.id',
344344
},
345345
// Org collaborators: members can see other users in the same
346346
// organization. Without this, owner/assignee lookups, @-mention
@@ -354,67 +354,67 @@ export const defaultPermissionSets: PermissionSet[] = [
354354
name: 'sys_user_org_members',
355355
object: 'sys_user',
356356
operation: 'select',
357-
using: 'id IN (current_user.org_user_ids)',
357+
using: 'id in current_user.org_user_ids',
358358
},
359359
{
360360
name: 'sys_session_self',
361361
object: 'sys_session',
362362
operation: 'all',
363-
using: 'user_id = current_user.id',
363+
using: 'user_id == current_user.id',
364364
},
365365
{
366366
name: 'sys_account_self',
367367
object: 'sys_account',
368368
operation: 'select',
369-
using: 'user_id = current_user.id',
369+
using: 'user_id == current_user.id',
370370
},
371371
{
372372
name: 'sys_team_member_self',
373373
object: 'sys_team_member',
374374
operation: 'select',
375-
using: 'user_id = current_user.id',
375+
using: 'user_id == current_user.id',
376376
},
377377
{
378378
name: 'sys_two_factor_self',
379379
object: 'sys_two_factor',
380380
operation: 'all',
381-
using: 'user_id = current_user.id',
381+
using: 'user_id == current_user.id',
382382
},
383383
{
384384
name: 'sys_user_preference_self',
385385
object: 'sys_user_preference',
386386
operation: 'all',
387-
using: 'user_id = current_user.id',
387+
using: 'user_id == current_user.id',
388388
},
389389
{
390390
name: 'sys_api_key_self',
391391
object: 'sys_api_key',
392392
operation: 'all',
393-
using: 'user_id = current_user.id',
393+
using: 'user_id == current_user.id',
394394
},
395395
{
396396
name: 'sys_device_code_self',
397397
object: 'sys_device_code',
398398
operation: 'all',
399-
using: 'user_id = current_user.id',
399+
using: 'user_id == current_user.id',
400400
},
401401
{
402402
name: 'sys_oauth_access_token_self',
403403
object: 'sys_oauth_access_token',
404404
operation: 'select',
405-
using: 'user_id = current_user.id',
405+
using: 'user_id == current_user.id',
406406
},
407407
{
408408
name: 'sys_oauth_refresh_token_self',
409409
object: 'sys_oauth_refresh_token',
410410
operation: 'select',
411-
using: 'user_id = current_user.id',
411+
using: 'user_id == current_user.id',
412412
},
413413
{
414414
name: 'sys_oauth_consent_self',
415415
object: 'sys_oauth_consent',
416416
operation: 'all',
417-
using: 'user_id = current_user.id',
417+
using: 'user_id == current_user.id',
418418
},
419419
// OAuth applications a user has registered themselves (Account →
420420
// Developer → OAuth Applications). `sys_oauth_application` has no
@@ -424,7 +424,7 @@ export const defaultPermissionSets: PermissionSet[] = [
424424
name: 'sys_oauth_application_self',
425425
object: 'sys_oauth_application',
426426
operation: 'all',
427-
using: 'user_id = current_user.id',
427+
using: 'user_id == current_user.id',
428428
},
429429
],
430430
}),
@@ -449,87 +449,87 @@ export const defaultPermissionSets: PermissionSet[] = [
449449
name: 'tenant_isolation',
450450
object: '*',
451451
operation: 'select',
452-
using: 'organization_id = current_user.organization_id',
452+
using: 'organization_id == current_user.organization_id',
453453
},
454454
{
455455
name: 'sys_organization_self',
456456
object: 'sys_organization',
457457
operation: 'select',
458-
using: 'id = current_user.organization_id',
458+
using: 'id == current_user.organization_id',
459459
},
460460
{
461461
name: 'sys_user_self',
462462
object: 'sys_user',
463463
operation: 'select',
464-
using: 'id = current_user.id',
464+
using: 'id == current_user.id',
465465
},
466466
// Org collaborators (read-only): see `sys_user_org_members` in
467467
// `member_default` for rationale.
468468
{
469469
name: 'sys_user_org_members',
470470
object: 'sys_user',
471471
operation: 'select',
472-
using: 'id IN (current_user.org_user_ids)',
472+
using: 'id in current_user.org_user_ids',
473473
},
474474
{
475475
name: 'sys_session_self',
476476
object: 'sys_session',
477477
operation: 'select',
478-
using: 'user_id = current_user.id',
478+
using: 'user_id == current_user.id',
479479
},
480480
{
481481
name: 'sys_account_self',
482482
object: 'sys_account',
483483
operation: 'select',
484-
using: 'user_id = current_user.id',
484+
using: 'user_id == current_user.id',
485485
},
486486
{
487487
name: 'sys_team_member_self',
488488
object: 'sys_team_member',
489489
operation: 'select',
490-
using: 'user_id = current_user.id',
490+
using: 'user_id == current_user.id',
491491
},
492492
{
493493
name: 'sys_two_factor_self',
494494
object: 'sys_two_factor',
495495
operation: 'select',
496-
using: 'user_id = current_user.id',
496+
using: 'user_id == current_user.id',
497497
},
498498
{
499499
name: 'sys_user_preference_self',
500500
object: 'sys_user_preference',
501501
operation: 'select',
502-
using: 'user_id = current_user.id',
502+
using: 'user_id == current_user.id',
503503
},
504504
{
505505
name: 'sys_api_key_self',
506506
object: 'sys_api_key',
507507
operation: 'select',
508-
using: 'user_id = current_user.id',
508+
using: 'user_id == current_user.id',
509509
},
510510
{
511511
name: 'sys_device_code_self',
512512
object: 'sys_device_code',
513513
operation: 'select',
514-
using: 'user_id = current_user.id',
514+
using: 'user_id == current_user.id',
515515
},
516516
{
517517
name: 'sys_oauth_access_token_self',
518518
object: 'sys_oauth_access_token',
519519
operation: 'select',
520-
using: 'user_id = current_user.id',
520+
using: 'user_id == current_user.id',
521521
},
522522
{
523523
name: 'sys_oauth_refresh_token_self',
524524
object: 'sys_oauth_refresh_token',
525525
operation: 'select',
526-
using: 'user_id = current_user.id',
526+
using: 'user_id == current_user.id',
527527
},
528528
{
529529
name: 'sys_oauth_consent_self',
530530
object: 'sys_oauth_consent',
531531
operation: 'select',
532-
using: 'user_id = current_user.id',
532+
using: 'user_id == current_user.id',
533533
},
534534
],
535535
}),

0 commit comments

Comments
 (0)