7.1 KiB
Category ACL — Design & Implementation Plan
Overview
Per-category access control — reusing the existing resource-agnostic ACL system (resource_acl table + IAclBackend + AclEditor).
Roles: admin, authenticated (logged-in users), anonymous
Phase 1: Only admins can manage category permissions
Phase 2: Users can create categories and manage permissions for their own categories
✅ Completed
Backend registration
serving/index.ts — registered the 'category' ACL backend:
import { DbAclBackend } from './db/db-acl-db.js';
registerAclBackend('category', new DbAclBackend('category'));
This enables the full ACL API for categories:
GET /api/acl/category/:categoryId— read ACLPOST /api/acl/category/:categoryId/grant— grant permissionPOST /api/acl/category/:categoryId/revoke— revoke permission
Virtual user ID fix
db-acl-db.ts — fixed DbAclBackend to handle virtual/sentinel user IDs (anonymous, authenticated). These aren't valid UUIDs and can't be stored in the user_id FK column, so they're transparently mapped to/from group_name:
| AclEntry.userId | DB column | Value |
|---|---|---|
anonymous |
group_name |
'anonymous' |
authenticated |
group_name |
'authenticated' |
<real-uuid> |
user_id |
UUID FK → auth.users |
| (group grant) | group_name |
e.g. 'editors' |
E2E tests
category-acl.e2e.test.ts — 19 tests, all passing:
| Group | Tests | Coverage |
|---|---|---|
| ACL CRUD | 9 | grant anon/auth/user, verify reads, upsert, full revoke cycle |
| Access control | 4 | unauth rejection, 404 missing revoke, input validation |
| Multi-user | 3 | regular user denied ACL management |
| Group grants | 2 | grant/revoke by group name |
| ACL isolation | 1 | grants don't leak between categories |
Run: npm run test:acl:categories
🔲 Next Steps — CategoryManager UI Integration
Goal
Add a Permissions section to CategoryManager.tsx's right panel, using the existing AclEditor component.
Where it goes
The right panel in CategoryManager has 3 states:
editingCategory? → Edit form (name, slug, visibility, variables, types, translate)
selectedCategoryId? → Selected view (name, description, page link actions) ← ADD HERE
else → Empty state ("Select a category...")
Add the AclEditor inside the selected view (state 2), below the "Current Page Link" section (around line 548):
Implementation
// 1. Import
import { AclEditor } from '@/components/admin/AclEditor';
// 2. Add to the right panel, after the page-link section (~line 548)
// Inside the `selectedCategoryId ? (...)` branch:
<div className="space-y-2">
<Label className="text-xs uppercase text-muted-foreground">
<T>Permissions</T>
</Label>
<AclEditor
resourceType="category"
mount={selectedCategoryId}
path="/"
/>
</div>
The AclEditor already handles:
- Anonymous toggle with permission picker
- Authenticated toggle with permission picker
- Per-user grants via
UserPicker - Active permissions table with revoke buttons
Props mapping
| AclEditor prop | StorageManager (VFS) | CategoryManager |
|---|---|---|
resourceType |
'vfs' (default) |
'category' |
mount |
mount name ('home') |
category UUID |
path |
folder path ('/shared') |
always '/' |
Files to change
| File | Change |
|---|---|
| CategoryManager.tsx | Import AclEditor, add it to selected-category view |
No other files need modification — the backend, API, and ACL editor component are all ready.
Architecture Reference
┌── Supabase ─────────────────────────┐
│ resource_acl table (RLS-enabled) │
│ ┌─ resource_type: 'category' │
│ ├─ resource_id: category UUID │
│ ├─ user_id / group_name │
│ ├─ permissions[] │
│ └─ path: '/' │
└─────────────────────────────────────┘
▲
┌── Server ───────────────────────────┐
│ db-acl.ts IAclBackend │
│ ├ registerAclBackend('vfs', ...) │
│ └ registerAclBackend('category',…) │ ✅
│ db-acl-db.ts DbAclBackend │ ✅ virtual ID fix
│ db-categories.ts CRUD + cache │
└─────────────────────────────────────┘
▲
┌── Client ───────────────────────────┐
│ client-acl.ts fetch/grant/revoke│ reuse as-is
│ AclEditor.tsx UI │ reuse as-is
│ CategoryManager + ACL section │ 🔲 next step
└─────────────────────────────────────┘
Permission Model
| Permission | Meaning |
|---|---|
read |
View category and its items |
list |
Browse category in tree navigation |
write |
Add/remove items from category |
manage |
Edit category metadata |
admin |
Full control including ACL |
Default Behavior
Categories default to their visibility field (public/unlisted/private). ACL entries override visibility for fine-grained control.
Phase 2 — Future
| Task | Description |
|---|---|
| ACL-filtered category reads | Filter fetchCategoriesServer by caller's list permission |
| ACL-filtered tree view | Hide restricted categories in CategoryTreeView.tsx |
| User-owned category ACL mgmt | Let users manage permissions on categories they own |
File Reference
| File | Role | Status |
|---|---|---|
| resource_acl.sql | DB schema | ✅ No changes needed |
| db-acl.ts | ACL orchestrator | ✅ No changes needed |
| db-acl-db.ts | Supabase backend | ✅ Virtual ID fix |
| index.ts | Backend registration | ✅ Category registered |
| category-acl.e2e.test.ts | E2E tests | ✅ 19/19 pass |
| CategoryManager.tsx | Category UI | 🔲 Add AclEditor |
| client-acl.ts | Client ACL API | ✅ No changes needed |
| AclEditor.tsx | ACL UI component | ✅ No changes needed |