From 4b8525fabda24c824d5c527ccdd1eb3851ff8392 Mon Sep 17 00:00:00 2001 From: "Claude (Chronicler #61)" Date: Sun, 5 Apr 2026 11:23:01 +0000 Subject: [PATCH] docs: Add RBAC database schema from Gemini followup Complete PostgreSQL schema for Option C (Roles + Direct Overrides): - roles table - permissions table (auto-populated from module.json) - role_permissions mapping - user_roles mapping - user_permission_overrides (the magic table) - Performance indexes Includes permission resolution logic: 1. Check overrides first (explicit allow/deny trumps role) 2. Check role permissions second 3. Default to deny Signed-off-by: Claude (Chronicler #61) --- ...i-trinity-console-2-response-2026-04-05.md | 78 ++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/docs/consultations/gemini-trinity-console-2-response-2026-04-05.md b/docs/consultations/gemini-trinity-console-2-response-2026-04-05.md index 202fed8..c152fd7 100644 --- a/docs/consultations/gemini-trinity-console-2-response-2026-04-05.md +++ b/docs/consultations/gemini-trinity-console-2-response-2026-04-05.md @@ -63,7 +63,83 @@ You asked whether to use Direct Permissions (Option A), Roles (Option B), or Bot **Option C (Roles with Direct Overrides) is the only way this scales.** As The Wizard, you don't want to assign 50 individual permissions to a new moderator. You assign them the "Moderator" role. But, if you want Holly to have access to the Financials module (which Builder normally wouldn't have), you need a direct override without creating a hyper-specific "Builder + Finance" role. -**[VISUALIZATION: RBAC Resolver Logic - See screenshot]** +**[VISUALIZATION: RBAC Resolver Logic - Interactive matrix showing users vs permissions with role inheritance and override controls]** + +#### RBAC Database Schema + +```sql +-- 1. Roles Table +-- Defines the high-level buckets (e.g., 'Admin', 'Community', 'Builder') +CREATE TABLE roles ( + id SERIAL PRIMARY KEY, + name VARCHAR(50) UNIQUE NOT NULL, + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 2. Permissions Table +-- Populated automatically when Trinity Console boots and reads module.json files. +-- Using 'permission_key' (e.g., 'tasks.view') as the primary key makes lookups fast. +CREATE TABLE permissions ( + permission_key VARCHAR(100) PRIMARY KEY, + module_id VARCHAR(50) NOT NULL, -- e.g., 'tasks', 'scheduler' + name VARCHAR(100) NOT NULL, -- e.g., 'View tasks' + description TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- 3. Role Permissions Table +-- Maps which permissions belong to which roles. +CREATE TABLE role_permissions ( + role_id INT REFERENCES roles(id) ON DELETE CASCADE, + permission_key VARCHAR(100) REFERENCES permissions(permission_key) ON DELETE CASCADE, + PRIMARY KEY (role_id, permission_key) +); + +-- 4. User Roles Table +-- Maps a user's Discord ID (or internal user ID) to a specific role. +CREATE TABLE user_roles ( + user_id VARCHAR(50) PRIMARY KEY, -- Assuming Discord ID from OAuth + role_id INT REFERENCES roles(id) ON DELETE CASCADE, + assigned_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + assigned_by VARCHAR(50) -- Discord ID of the admin who assigned it +); + +-- 5. User Permission Overrides Table +-- The "Option C" magic. This table holds explicit ALLOWs and DENYs that trump the role. +CREATE TABLE user_permission_overrides ( + user_id VARCHAR(50) NOT NULL, -- Discord ID + permission_key VARCHAR(100) REFERENCES permissions(permission_key) ON DELETE CASCADE, + is_granted BOOLEAN NOT NULL, -- TRUE = Explicit Allow, FALSE = Explicit Deny + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + created_by VARCHAR(50), -- Discord ID of the admin who set the override + PRIMARY KEY (user_id, permission_key) +); + +-- Performance Indexes +CREATE INDEX idx_user_overrides ON user_permission_overrides(user_id); +CREATE INDEX idx_role_permissions ON role_permissions(role_id); +``` + +#### Permission Resolution Logic + +When a user tries to hit a protected route, resolve access using this strict hierarchy: + +1. **Check Overrides First:** Does this `user_id` have a row in `user_permission_overrides` for this `permission_key`? + - If yes, and `is_granted` is TRUE → **Access Granted** + - If yes, and `is_granted` is FALSE → **Access Denied** (Even if their role allows it, this acts as a hard block) + +2. **Check Role Second:** If there is no override, look up their `role_id` in `user_roles`. Does that `role_id` have the `permission_key` in `role_permissions`? + - If yes → **Access Granted** + +3. **Default to Deny:** If the permission is not in the overrides and not in the role → **Access Denied** + +#### Real-World Example + +- Meg has the `Community` role +- The `Community` role DOES NOT have the `financials.view` permission +- You insert a row into `user_permission_overrides` for Meg's Discord ID, setting `permission_key = 'financials.view'` and `is_granted = TRUE` +- **Result:** Meg can view the Financials module without needing a completely new "Community + Finance" role **Wildcards:** Support `module.*`. It makes your middleware much cleaner.