Permission System
The permission system controls which users can perform which actions on each entity, integrating role-based control with Row-Level Security (RLS) in PostgreSQL.
Security Levels
The system implements security in 3 layers:
- Application Level: Validation in TypeScript code
- API Level: Verification at endpoints
- Database Level: Row-Level Security (RLS) in PostgreSQL
Permission Configuration
In EntityConfig
{
permissions: {
read: ['admin', 'colaborator', 'member'],
create: ['admin', 'colaborator', 'member'],
update: ['admin', 'colaborator', 'member'],
delete: ['admin', 'colaborator']
}
}
Available Roles
type UserRole = 'admin' | 'colaborator' | 'member' | 'user'
| Role | Description | Level | Typical Use |
|---|---|---|---|
admin |
Administrator | 4 | Full system access |
colaborator |
Collaborator | 3 | Content management, broad access |
member |
Member | 2 | Standard use, medium access |
user |
User | 1 | Basic access, limited |
CRUD Operations
read - View/List
Who can view entity records.
{
permissions: {
read: ['admin', 'colaborator', 'member', 'user'] // Everyone can view
}
}
create - Create
Who can create new records.
{
permissions: {
create: ['admin', 'colaborator', 'member'] // Basic user CANNOT create
}
}
update - Update
Who can edit existing records.
{
permissions: {
update: ['admin', 'colaborator'] // Only admin and collaborator
}
}
delete - Delete
Who can delete records.
{
permissions: {
delete: ['admin'] // Only admin can delete
}
}
Row-Level Security (RLS)
RLS is the security layer in PostgreSQL that ensures users only see their own data.
RLS Types
1. Data per User (shared: false)
Each user sees only their records.
-- In migration
ALTER TABLE "tasks" ENABLE ROW LEVEL SECURITY;
-- Policy: SELECT (read)
CREATE POLICY "tasks_select_own" ON "tasks"
FOR SELECT
USING ("userId" = auth.uid());
-- Policy: INSERT (create)
CREATE POLICY "tasks_insert_own" ON "tasks"
FOR INSERT
WITH CHECK ("userId" = auth.uid());
-- Policy: UPDATE (update)
CREATE POLICY "tasks_update_own" ON "tasks"
FOR UPDATE
USING ("userId" = auth.uid())
WITH CHECK ("userId" = auth.uid());
-- Policy: DELETE (delete)
CREATE POLICY "tasks_delete_own" ON "tasks"
FOR DELETE
USING ("userId" = auth.uid());
Behavior:
- User A only sees their tasks
- User B only sees their tasks
- Automatic in all queries
2. Shared Data (shared: true)
All authenticated users see all records.
ALTER TABLE "categories" ENABLE ROW LEVEL SECURITY;
-- Policy: Any authenticated user can view
CREATE POLICY "categories_select_authenticated" ON "categories"
FOR SELECT
USING (auth.role() = 'authenticated');
-- Policy: Only admin can modify
CREATE POLICY "categories_modify_admin" ON "categories"
FOR ALL
USING (
auth.role() = 'authenticated'
AND (SELECT "role" FROM "user" WHERE "id" = auth.uid()) = 'admin'
);
3. Public Data (public: true)
Anonymous users can read.
ALTER TABLE "blog_posts" ENABLE ROW LEVEL SECURITY;
-- Policy: Everyone can read (including anon)
CREATE POLICY "posts_select_public" ON "blog_posts"
FOR SELECT
USING (true);
-- Policy: Only authenticated can create
CREATE POLICY "posts_insert_authenticated" ON "blog_posts"
FOR INSERT
TO authenticated
WITH CHECK ("userId" = auth.uid());
Validation in Code
In APIs
// app/api/v1/tasks/route.ts
export async function POST(request: Request) {
const user = await getUser(request)
// Check create permission
const canCreate = entityRegistry.checkEntityPermission(
'tasks',
'create',
user.role
)
if (!canCreate) {
return NextResponse.json(
{ error: 'Insufficient permissions' },
{ status: 403 }
)
}
// Continue with creation...
}
In Components
'use client'
import { useEntityPermissions } from '@/core/hooks/useEntityPermissions'
export function TaskActions({ task }) {
const { canUpdate, canDelete } = useEntityPermissions('tasks')
return (
<div>
{canUpdate && (
<Button onClick={handleEdit}>Edit</Button>
)}
{canDelete && (
<Button onClick={handleDelete} variant="destructive">Delete</Button>
)}
</div>
)
}
Ownership
Concept of record "owner".
Owner Validation
// Check if user is owner of the record
async function checkOwnership(entityName: string, recordId: string, userId: string): Promise<boolean> {
const record = await getEntity(entityName, recordId)
return record.userId === userId
}
// In API
if (!await checkOwnership('tasks', taskId, user.id)) {
return NextResponse.json(
{ error: 'You do not own this task' },
{ status: 403 }
)
}
RLS with Ownership
-- Only owner can update
CREATE POLICY "tasks_update_owner" ON "tasks"
FOR UPDATE
USING ("userId" = auth.uid())
WITH CHECK ("userId" = auth.uid());
Role-based Permissions in RLS
Helper Function
-- Function to get user role
CREATE OR REPLACE FUNCTION auth.user_role()
RETURNS TEXT AS $$
SELECT "role" FROM "user" WHERE "id" = auth.uid()
$$ LANGUAGE SQL STABLE;
Policies with Roles
-- Only admin can delete
CREATE POLICY "tasks_delete_admin" ON "tasks"
FOR DELETE
USING (auth.user_role() = 'admin');
-- Admin and collaborator can update
CREATE POLICY "tasks_update_admin_colaborator" ON "tasks"
FOR UPDATE
USING (auth.user_role() IN ('admin', 'colaborator'))
WITH CHECK (auth.user_role() IN ('admin', 'colaborator'));
Permission Scenarios
Scenario 1: Personal Entity
Personal tasks - each user sees only theirs.
{
access: { shared: false },
permissions: {
read: ['admin', 'colaborator', 'member'],
create: ['admin', 'colaborator', 'member'],
update: ['admin', 'colaborator', 'member'],
delete: ['admin', 'colaborator', 'member']
}
}
Scenario 2: Shared Workspace Entity
Shared projects - everyone in workspace sees them.
{
access: { shared: true },
permissions: {
read: ['admin', 'colaborator', 'member'],
create: ['admin', 'colaborator'],
update: ['admin', 'colaborator'],
delete: ['admin']
}
}
Scenario 3: Read-Only Data
Global categories - everyone views, only admin edits.
{
access: { shared: true },
permissions: {
read: ['admin', 'colaborator', 'member', 'user'],
create: ['admin'],
update: ['admin'],
delete: ['admin']
}
}
Scenario 4: Public Content
Blog posts - anonymous read, authenticated create.
{
access: { public: true, shared: true },
permissions: {
read: ['admin', 'colaborator', 'member', 'user'], // + anon via RLS
create: ['admin', 'colaborator'],
update: ['admin', 'colaborator'],
delete: ['admin']
}
}
Child Entities
Child entity permissions can be configured independently:
{
childEntities: {
'comments': {
permissions: {
read: ['admin', 'colaborator', 'member'],
create: ['admin', 'colaborator', 'member'], // Everyone can comment
update: ['admin'], // Only admin edits comments
delete: ['admin'] // Only admin deletes
}
}
}
}
If not specified, they inherit from the parent.
Testing Permissions
// test/permissions/tasks.test.ts
describe('Task Permissions', () => {
test('member can create tasks', async () => {
const user = { role: 'member', id: 'user-1' }
const canCreate = checkPermission('tasks', 'create', user.role)
expect(canCreate).toBe(true)
})
test('member cannot delete tasks', async () => {
const user = { role: 'member', id: 'user-1' }
const canDelete = checkPermission('tasks', 'delete', user.role)
expect(canDelete).toBe(false)
})
test('RLS prevents seeing other users tasks', async () => {
const userA = { id: 'user-a' }
const userB = { id: 'user-b' }
// User A creates task
const task = await createTask({ title: 'A Task' }, userA.id)
// User B tries to read
const tasks = await listTasks(userB.id)
expect(tasks).not.toContainEqual(task)
})
})
Debugging Permissions
Logs
// In development, log permission decisions
if (process.env.NODE_ENV === 'development') {
console.log(`Permission check: ${entityName}.${operation} for ${userRole}`)
console.log(`Result: ${allowed ? 'ALLOWED' : 'DENIED'}`)
}
Verify RLS
-- View active policies
SELECT * FROM pg_policies WHERE tablename = 'tasks';
-- Test policy as user
SET ROLE authenticated;
SET request.jwt.claim.sub = 'user-id-here';
SELECT * FROM tasks; -- Should show only your tasks
RESET ROLE;
Best Practices
- Always enable RLS: Never trust only code validation
- Principle of least privilege: Grant minimum necessary permissions
- Test exhaustively: Especially RLS
- Use appropriate roles: Don't give admin to normal users
- Document policies: Comments in SQL
- Review regularly: Audit permissions periodically
Next Steps
- Validation - Validation system
- Advanced Patterns - Advanced security patterns
- Examples - Complete examples with permissions
💡 Tip: RLS is your last line of defense. While you validate in code, RLS guarantees security at the database level. ALWAYS implement both layers.