Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 18:46:56 +08:00
commit 9cf462a7c1
8 changed files with 1666 additions and 0 deletions

View File

@@ -0,0 +1,12 @@
{
"name": "supabase-rls-policy-generator",
"description": "This skill should be used when the user requests to generate, create, or add Row-Level Security (RLS) policies for Supabase databases in multi-tenant or role-based applications. It generates comprehensive RLS policies using auth.uid(), auth.jwt() claims, and role-based access patterns. Trigger terms include RLS, row level security, supabase security, generate policies, auth policies, multi-tenant security, role-based access, database security policies, supabase permissions, tenant isolation.",
"version": "1.0.0",
"author": {
"name": "Hope Overture",
"email": "support@worldbuilding-app-skills.dev"
},
"skills": [
"./skills"
]
}

3
README.md Normal file
View File

@@ -0,0 +1,3 @@
# supabase-rls-policy-generator
This skill should be used when the user requests to generate, create, or add Row-Level Security (RLS) policies for Supabase databases in multi-tenant or role-based applications. It generates comprehensive RLS policies using auth.uid(), auth.jwt() claims, and role-based access patterns. Trigger terms include RLS, row level security, supabase security, generate policies, auth policies, multi-tenant security, role-based access, database security policies, supabase permissions, tenant isolation.

61
plugin.lock.json Normal file
View File

@@ -0,0 +1,61 @@
{
"$schema": "internal://schemas/plugin.lock.v1.json",
"pluginId": "gh:hopeoverture/worldbuilding-app-skills:plugins/supabase-rls-policy-generator",
"normalized": {
"repo": null,
"ref": "refs/tags/v20251128.0",
"commit": "9dc70ff85f9cc02392469b553fc7f43956562474",
"treeHash": "c9851f2b5d70ed6a0e59bfa9ba5703a9ee0e747f5fc4cd0d3da28ee11578491b",
"generatedAt": "2025-11-28T10:17:30.411244Z",
"toolVersion": "publish_plugins.py@0.2.0"
},
"origin": {
"remote": "git@github.com:zhongweili/42plugin-data.git",
"branch": "master",
"commit": "aa1497ed0949fd50e99e70d6324a29c5b34f9390",
"repoRoot": "/Users/zhongweili/projects/openmind/42plugin-data"
},
"manifest": {
"name": "supabase-rls-policy-generator",
"description": "This skill should be used when the user requests to generate, create, or add Row-Level Security (RLS) policies for Supabase databases in multi-tenant or role-based applications. It generates comprehensive RLS policies using auth.uid(), auth.jwt() claims, and role-based access patterns. Trigger terms include RLS, row level security, supabase security, generate policies, auth policies, multi-tenant security, role-based access, database security policies, supabase permissions, tenant isolation.",
"version": "1.0.0"
},
"content": {
"files": [
{
"path": "README.md",
"sha256": "d2f2b90edb4f7e9963ab139bb5d8c8010e03fb0fa3603de6d036192704663462"
},
{
"path": ".claude-plugin/plugin.json",
"sha256": "8a9d5f4bb8a2752dc20d7128d0aadd29a9044e7dcecc0c9ee6d60d0348ce96e5"
},
{
"path": "skills/supabase-rls-policy-generator/SKILL.md",
"sha256": "f404dd551c98507aeab7bde8453cb4c10cf25268b6d6f3470379b41741ad4859"
},
{
"path": "skills/supabase-rls-policy-generator/references/supabase-auth.md",
"sha256": "ed0080224fe24da2a27cf79d8e0237aa28e9787569c2dede65a2bde58046b4c8"
},
{
"path": "skills/supabase-rls-policy-generator/references/rls-patterns.md",
"sha256": "8b9db4aaec6c21d9c3edf72d9d029d5d0ec597d0c890ee420ffee2cf04ed7c23"
},
{
"path": "skills/supabase-rls-policy-generator/assets/policy-templates.sql",
"sha256": "6a2f3a70dcbaddba304c9c339e7d68541460ab664be0e258778b297051cbd64c"
},
{
"path": "skills/supabase-rls-policy-generator/assets/policy-documentation-template.md",
"sha256": "d5df28d015c20d3a4fc96e0fd6f3a3be55439dc094bb419540b65850bd4f046a"
}
],
"dirSha256": "c9851f2b5d70ed6a0e59bfa9ba5703a9ee0e747f5fc4cd0d3da28ee11578491b"
},
"security": {
"scannedAt": null,
"scannerVersion": null,
"flags": []
}
}

View File

@@ -0,0 +1,246 @@
---
name: supabase-rls-policy-generator
description: This skill should be used when the user requests to generate, create, or add Row-Level Security (RLS) policies for Supabase databases in multi-tenant or role-based applications. It generates comprehensive RLS policies using auth.uid(), auth.jwt() claims, and role-based access patterns. Trigger terms include RLS, row level security, supabase security, generate policies, auth policies, multi-tenant security, role-based access, database security policies, supabase permissions, tenant isolation.
---
# Supabase RLS Policy Generator
To generate comprehensive Row-Level Security policies for Supabase databases, follow these steps systematically.
## Step 1: Analyze Current Schema
Before generating policies:
1. Ask user for the database schema file path or table names
2. Read the schema to understand table structures, foreign keys, and relationships
3. Identify tables that need RLS protection
4. Determine the security model: multi-tenant, role-based, or hybrid
## Step 2: Identify Security Requirements
Determine access patterns by asking:
- Is this a multi-tenant application? (tenant_id isolation)
- What roles exist in the system? (admin, user, viewer, etc.)
- Are there public vs private resources?
- Do users need to share resources across accounts?
- Are there hierarchical permissions? (organization > team > user)
Consult `references/rls-patterns.md` for common security patterns.
## Step 3: Generate RLS Policies
For each table requiring protection, generate policies following this structure:
### Enable RLS
```sql
ALTER TABLE table_name ENABLE ROW LEVEL SECURITY;
```
### Policy Types to Generate
**SELECT Policies** - Control read access:
- User can view their own records
- User can view records in their tenant
- Role-based viewing (admins see all)
- Public records accessible to all authenticated users
**INSERT Policies** - Control creation:
- User can create records with their own user_id
- User can create records in their tenant
- Role-based creation restrictions
**UPDATE Policies** - Control modifications:
- User can update their own records
- Admins can update all records
- Tenant-scoped updates
**DELETE Policies** - Control deletion:
- User can delete their own records
- Admin-only deletion
- Tenant-scoped deletion
### Policy Templates
Use templates from `assets/policy-templates.sql`:
**Basic User Ownership**:
```sql
CREATE POLICY "Users can view own records"
ON table_name FOR SELECT
USING (auth.uid() = user_id);
```
**Multi-Tenant Isolation**:
```sql
CREATE POLICY "Tenant isolation"
ON table_name FOR ALL
USING (
tenant_id IN (
SELECT tenant_id FROM user_tenants
WHERE user_id = auth.uid()
)
);
```
**Role-Based Access**:
```sql
CREATE POLICY "Admins have full access"
ON table_name FOR ALL
USING (
auth.jwt() ->> 'role' = 'admin'
);
```
**JWT Claims**:
```sql
CREATE POLICY "Organization access"
ON table_name FOR SELECT
USING (
organization_id = (auth.jwt() -> 'app_metadata' ->> 'organization_id')::uuid
);
```
## Step 4: Generate Helper Functions
Create PostgreSQL functions to support complex policies:
```sql
-- Function to check user role
CREATE OR REPLACE FUNCTION auth.user_has_role(required_role TEXT)
RETURNS BOOLEAN AS $$
BEGIN
RETURN (auth.jwt() ->> 'role') = required_role;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Function to check tenant membership
CREATE OR REPLACE FUNCTION auth.user_in_tenant(target_tenant_id UUID)
RETURNS BOOLEAN AS $$
BEGIN
RETURN EXISTS (
SELECT 1 FROM user_tenants
WHERE user_id = auth.uid()
AND tenant_id = target_tenant_id
);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
```
## Step 5: Generate Testing Queries
Create test queries to verify policies work correctly:
```sql
-- Test as authenticated user
SET request.jwt.claim.sub = 'user-uuid';
SELECT * FROM table_name; -- Should see only accessible records
-- Test as admin
SET request.jwt.claim.role = 'admin';
SELECT * FROM table_name; -- Should see all records
-- Test as different tenant
SET request.jwt.claim.sub = 'other-user-uuid';
SELECT * FROM table_name; -- Should see different tenant's records
```
## Step 6: Create Migration File
Generate a migration file with proper structure:
```sql
-- Migration: Add RLS policies
-- Created: [timestamp]
-- Enable RLS on tables
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
ALTER TABLE items ENABLE ROW LEVEL SECURITY;
-- Drop existing policies if any
DROP POLICY IF EXISTS "policy_name" ON table_name;
-- Create new policies
[Generated policies here]
-- Create helper functions
[Generated functions here]
-- Grant necessary permissions
GRANT SELECT, INSERT, UPDATE, DELETE ON table_name TO authenticated;
GRANT SELECT ON table_name TO anon; -- If public read needed
```
## Step 7: Document Generated Policies
Create documentation explaining:
- What each policy does
- Which users/roles have what access
- Any special cases or exceptions
- How to test the policies
- Common troubleshooting tips
Use template from `assets/policy-documentation-template.md`.
## Implementation Guidelines
### Security Best Practices
- Always enable RLS on tables with user data
- Use auth.uid() for user-owned records
- Use JWT claims for role-based access
- Prefer SECURITY DEFINER functions for complex logic
- Test policies with different user roles
- Use USING clause for read access, WITH CHECK for write validation
### Performance Considerations
- Add indexes on columns used in policies (user_id, tenant_id, role)
- Keep policy logic simple for better performance
- Use helper functions for reusable complex logic
- Avoid subqueries in policies when possible
### Common Patterns
Consult `references/rls-patterns.md` for detailed examples of:
- Multi-tenant isolation
- Role-based access control (RBAC)
- Attribute-based access control (ABAC)
- Hierarchical permissions
- Public/private resource splitting
- Shared resource access
## Output Format
Generate files in the following structure:
```
migrations/
[timestamp]_add_rls_policies.sql
docs/
rls-policies.md (documentation)
tests/
rls_tests.sql (test queries)
```
## Verification Checklist
Before completing:
- [ ] RLS enabled on all sensitive tables
- [ ] Policies cover all operations (SELECT, INSERT, UPDATE, DELETE)
- [ ] Policies tested with different user roles
- [ ] Indexes added for policy columns
- [ ] Helper functions created for complex logic
- [ ] Documentation generated
- [ ] Test queries provided
- [ ] No policies accidentally grant excessive access
## Consulting References
Throughout generation:
- Consult `references/rls-patterns.md` for security patterns
- Consult `references/supabase-auth.md` for auth.uid() and JWT structure
- Use templates from `assets/policy-templates.sql`
## Completion
When finished:
1. Display the generated migration file
2. Summarize the policies created
3. Provide testing instructions
4. Offer to generate additional policies or modify existing ones

View File

@@ -0,0 +1,252 @@
# RLS Policies Documentation: [Table Name]
**Generated**: [Date]
**Database**: [Database Name]
**Schema**: public
## Overview
This document describes the Row-Level Security (RLS) policies implemented for the `[table_name]` table.
**Security Model**: [Multi-tenant / Role-based / Hybrid / etc.]
## Table Schema
```sql
CREATE TABLE [table_name] (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES auth.users(id),
tenant_id UUID REFERENCES tenants(id),
[other columns...]
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
```
**Security-relevant columns**:
- `user_id`: Owner of the record
- `tenant_id`: Tenant/organization identifier
- [other relevant columns...]
## RLS Status
```sql
-- RLS is ENABLED on this table
ALTER TABLE [table_name] ENABLE ROW LEVEL SECURITY;
```
## Policies
### Policy 1: [Policy Name]
**Operation**: SELECT | INSERT | UPDATE | DELETE | ALL
**Policy Name**: `[policy_name]`
**Purpose**: [What this policy does]
**SQL**:
```sql
CREATE POLICY "[policy_name]"
ON [table_name] FOR [OPERATION]
USING ([condition])
WITH CHECK ([condition]);
```
**Access Rules**:
- [Who] can [do what] when [condition]
- Example: "Users can view records where they are the owner"
- Example: "Admins can view all records"
**Testing**:
```sql
-- Test as user [user_id]
SET request.jwt.claim.sub = '[user-uuid]';
SELECT * FROM [table_name]; -- Expected: [description]
-- Test as admin
SET request.jwt.claim.role = 'admin';
SELECT * FROM [table_name]; -- Expected: [description]
```
---
### Policy 2: [Policy Name]
[Repeat structure for each policy...]
## Access Matrix
| Role / Context | SELECT | INSERT | UPDATE | DELETE |
|---------------|--------|--------|--------|--------|
| Anonymous | No | No | No | No |
| Authenticated User | Own records | Own records | Own records | Own records |
| Team Member | Team records | Team records | Team records | No |
| Admin | All records | All records | All records | All records |
## Helper Functions
### Function: [function_name]
**Purpose**: [What this function does]
**SQL**:
```sql
CREATE OR REPLACE FUNCTION [function_name]([params])
RETURNS [type] AS $$
BEGIN
[implementation]
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
```
**Usage in Policies**:
```sql
USING ([function_name](param))
```
## Indexes
The following indexes support RLS policy performance:
```sql
CREATE INDEX idx_[table]_user_id ON [table_name](user_id);
CREATE INDEX idx_[table]_tenant_id ON [table_name](tenant_id);
-- [other indexes...]
```
## Permissions
```sql
-- Authenticated users can perform CRUD operations (filtered by RLS)
GRANT SELECT, INSERT, UPDATE, DELETE ON [table_name] TO authenticated;
-- Anonymous users can read public records (filtered by RLS)
GRANT SELECT ON [table_name] TO anon;
-- Sequence usage
GRANT USAGE, SELECT ON SEQUENCE [table_name]_id_seq TO authenticated;
```
## Testing Scenarios
### Scenario 1: User Access Own Records
**Setup**:
```sql
-- Create test user
INSERT INTO auth.users (id, email) VALUES
('user-1-uuid', 'user1@example.com');
-- Create test records
INSERT INTO [table_name] (user_id, [data])
VALUES ('user-1-uuid', 'data1');
```
**Test**:
```sql
-- Set auth context
SET request.jwt.claim.sub = 'user-1-uuid';
-- User should see own record
SELECT * FROM [table_name];
-- Expected: 1 row
-- User should not see other users' records
SELECT * FROM [table_name] WHERE user_id != 'user-1-uuid';
-- Expected: 0 rows
```
### Scenario 2: [Other scenarios...]
[Add more test scenarios as needed]
## Security Considerations
### Enforced Restrictions
- [x] Users cannot access other users' private data
- [x] Users cannot modify tenant_id after creation
- [x] Anonymous users have read-only access to public data
- [x] [Other restrictions...]
### Known Limitations
- [Any limitations or edge cases]
- [Performance considerations]
- [Future improvements needed]
## Common Issues and Troubleshooting
### Issue 1: "Permission denied for relation [table_name]"
**Cause**: RLS is enabled but no policies match the user's context.
**Solution**:
1. Verify user is authenticated: `SELECT auth.uid();`
2. Check user's role: `SELECT auth.jwt() ->> 'role';`
3. Verify policy conditions match user context
### Issue 2: Policies not applying correctly
**Cause**: Testing as superuser, which bypasses RLS.
**Solution**:
```sql
-- Test as non-superuser
SET ROLE non_superuser_role;
-- Or use Supabase client with actual auth token
```
### Issue 3: Performance degradation
**Cause**: Missing indexes on columns used in policies.
**Solution**:
```sql
-- Add indexes
CREATE INDEX idx_[table]_[column] ON [table_name]([column]);
-- Analyze table
ANALYZE [table_name];
```
## Migration History
### Initial RLS Setup
- **Date**: [Date]
- **Migration**: `[timestamp]_add_rls_policies.sql`
- **Changes**: Enabled RLS and created initial policies
### [Future updates...]
## Maintenance
### Regular Tasks
- [ ] Review policy effectiveness quarterly
- [ ] Monitor query performance
- [ ] Audit access logs for anomalies
- [ ] Update policies when roles/permissions change
### Performance Monitoring
```sql
-- Check policy execution time
EXPLAIN ANALYZE
SELECT * FROM [table_name]
WHERE [typical_condition];
-- Check index usage
SELECT schemaname, tablename, indexname, idx_scan
FROM pg_stat_user_indexes
WHERE tablename = '[table_name]';
```
## References
- Supabase RLS Documentation: https://supabase.com/docs/guides/auth/row-level-security
- PostgreSQL RLS Policies: https://www.postgresql.org/docs/current/ddl-rowsecurity.html
- Auth Functions: https://supabase.com/docs/guides/database/postgres/row-level-security
## Change Log
| Date | Author | Change |
|------|--------|--------|
| [Date] | [Name] | Initial policies created |
| [Date] | [Name] | Added admin access policy |
| ... | ... | ... |

View File

@@ -0,0 +1,396 @@
-- Supabase RLS Policy Templates
-- Copy and adapt these templates for your tables
-- ============================================================================
-- TEMPLATE 1: User Ownership (Basic)
-- ============================================================================
-- Use when: Users can only access their own records
-- Tables: user_profiles, user_settings, user_preferences
ALTER TABLE table_name ENABLE ROW LEVEL SECURITY;
CREATE POLICY "users_select_own"
ON table_name FOR SELECT
USING (auth.uid() = user_id);
CREATE POLICY "users_insert_own"
ON table_name FOR INSERT
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "users_update_own"
ON table_name FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "users_delete_own"
ON table_name FOR DELETE
USING (auth.uid() = user_id);
-- ============================================================================
-- TEMPLATE 2: Multi-Tenant Isolation
-- ============================================================================
-- Use when: Multiple tenants/organizations sharing the database
-- Tables: documents, projects, team_data
ALTER TABLE table_name ENABLE ROW LEVEL SECURITY;
-- Assumes a user_tenants junction table
CREATE POLICY "tenant_isolation"
ON table_name FOR ALL
USING (
tenant_id IN (
SELECT tenant_id FROM user_tenants
WHERE user_id = auth.uid()
)
);
-- Alternative: Using JWT claim for tenant ID
CREATE POLICY "tenant_isolation_jwt"
ON table_name FOR ALL
USING (
tenant_id = (auth.jwt() -> 'app_metadata' ->> 'tenant_id')::uuid
);
-- ============================================================================
-- TEMPLATE 3: Role-Based Access Control (Simple)
-- ============================================================================
-- Use when: Different access based on user roles
-- Tables: admin_settings, moderation_queue
ALTER TABLE table_name ENABLE ROW LEVEL SECURITY;
-- Admins can do everything
CREATE POLICY "admins_all_access"
ON table_name FOR ALL
USING (auth.jwt() ->> 'role' = 'admin');
-- Regular users can only read
CREATE POLICY "users_read_only"
ON table_name FOR SELECT
USING (auth.jwt() ->> 'role' = 'authenticated');
-- ============================================================================
-- TEMPLATE 4: Role-Based with Ownership Fallback
-- ============================================================================
-- Use when: Users can access own records, admins can access all
-- Tables: posts, comments, submissions
ALTER TABLE table_name ENABLE ROW LEVEL SECURITY;
CREATE POLICY "select_own_or_admin"
ON table_name FOR SELECT
USING (
auth.uid() = user_id OR
auth.jwt() ->> 'role' = 'admin'
);
CREATE POLICY "insert_own"
ON table_name FOR INSERT
WITH CHECK (auth.uid() = user_id);
CREATE POLICY "update_own_or_admin"
ON table_name FOR UPDATE
USING (
auth.uid() = user_id OR
auth.jwt() ->> 'role' = 'admin'
);
CREATE POLICY "delete_own_or_admin"
ON table_name FOR DELETE
USING (
auth.uid() = user_id OR
auth.jwt() ->> 'role' = 'admin'
);
-- ============================================================================
-- TEMPLATE 5: Public/Private Resources
-- ============================================================================
-- Use when: Some records are public, others are private
-- Tables: articles, galleries, portfolios
ALTER TABLE table_name ENABLE ROW LEVEL SECURITY;
-- Everyone (including anon) can view public records
CREATE POLICY "public_records_viewable"
ON table_name FOR SELECT
USING (is_public = true);
-- Authenticated users can view own private records
CREATE POLICY "private_records_owner"
ON table_name FOR SELECT
USING (
is_public = false AND
auth.uid() = user_id
);
-- Only owner can modify
CREATE POLICY "owner_can_modify"
ON table_name FOR UPDATE
USING (auth.uid() = user_id);
CREATE POLICY "owner_can_delete"
ON table_name FOR DELETE
USING (auth.uid() = user_id);
-- ============================================================================
-- TEMPLATE 6: Hierarchical Permissions (Organization/Team)
-- ============================================================================
-- Use when: Organization > Team > User hierarchy
-- Tables: projects, resources, team_documents
ALTER TABLE table_name ENABLE ROW LEVEL SECURITY;
-- Organization admins see all
CREATE POLICY "org_admin_all"
ON table_name FOR ALL
USING (
organization_id IN (
SELECT organization_id FROM user_organizations
WHERE user_id = auth.uid() AND role = 'admin'
)
);
-- Team members see team records
CREATE POLICY "team_member_access"
ON table_name FOR SELECT
USING (
team_id IN (
SELECT team_id FROM user_teams
WHERE user_id = auth.uid()
)
);
-- Owner always has access
CREATE POLICY "owner_access"
ON table_name FOR ALL
USING (auth.uid() = owner_id);
-- ============================================================================
-- TEMPLATE 7: Shared Resources
-- ============================================================================
-- Use when: Resources can be shared with specific users
-- Tables: files, documents, notes
ALTER TABLE table_name ENABLE ROW LEVEL SECURITY;
-- Owner can do everything
CREATE POLICY "owner_full_access"
ON table_name FOR ALL
USING (auth.uid() = owner_id);
-- Shared users can read (and maybe write)
CREATE POLICY "shared_users_read"
ON table_name FOR SELECT
USING (
EXISTS (
SELECT 1 FROM table_name_shares
WHERE resource_id = table_name.id
AND user_id = auth.uid()
AND (expires_at IS NULL OR expires_at > NOW())
)
);
-- Shared users with write permission can update
CREATE POLICY "shared_users_write"
ON table_name FOR UPDATE
USING (
EXISTS (
SELECT 1 FROM table_name_shares
WHERE resource_id = table_name.id
AND user_id = auth.uid()
AND can_write = true
AND (expires_at IS NULL OR expires_at > NOW())
)
);
-- ============================================================================
-- TEMPLATE 8: Time-Based Access
-- ============================================================================
-- Use when: Content has publish/expire dates
-- Tables: scheduled_posts, campaigns, events
ALTER TABLE table_name ENABLE ROW LEVEL SECURITY;
-- Published content is viewable
CREATE POLICY "view_published"
ON table_name FOR SELECT
USING (
status = 'published' AND
(published_at IS NULL OR published_at <= NOW()) AND
(expires_at IS NULL OR expires_at > NOW())
);
-- Authors can view all states
CREATE POLICY "author_view_all"
ON table_name FOR SELECT
USING (auth.uid() = author_id);
-- Authors can update before published
CREATE POLICY "author_update_unpublished"
ON table_name FOR UPDATE
USING (
auth.uid() = author_id AND
(status = 'draft' OR published_at > NOW())
);
-- ============================================================================
-- TEMPLATE 9: Cascading Permissions (Parent/Child)
-- ============================================================================
-- Use when: Child records inherit permissions from parent
-- Tables: comments (child of posts), task_items (child of tasks)
-- Parent table
ALTER TABLE parent_table ENABLE ROW LEVEL SECURITY;
CREATE POLICY "parent_access"
ON parent_table FOR SELECT
USING (
auth.uid() = owner_id OR
is_public = true
);
-- Child table inherits from parent
ALTER TABLE child_table ENABLE ROW LEVEL SECURITY;
CREATE POLICY "child_inherit_from_parent"
ON child_table FOR SELECT
USING (
parent_id IN (
SELECT id FROM parent_table
WHERE auth.uid() = owner_id OR is_public = true
)
);
-- ============================================================================
-- TEMPLATE 10: Moderation/Approval Workflow
-- ============================================================================
-- Use when: Content needs approval before being visible
-- Tables: user_submissions, comments, reviews
ALTER TABLE table_name ENABLE ROW LEVEL SECURITY;
-- Users can view approved content
CREATE POLICY "view_approved"
ON table_name FOR SELECT
USING (status = 'approved');
-- Users can view own content in any state
CREATE POLICY "view_own"
ON table_name FOR SELECT
USING (auth.uid() = author_id);
-- Users can create content (starts as pending)
CREATE POLICY "create_as_pending"
ON table_name FOR INSERT
WITH CHECK (
auth.uid() = author_id AND
status = 'pending'
);
-- Users can edit own pending content
CREATE POLICY "update_own_pending"
ON table_name FOR UPDATE
USING (
auth.uid() = author_id AND
status = 'pending'
);
-- Moderators can update status
CREATE POLICY "moderators_approve"
ON table_name FOR UPDATE
USING (auth.jwt() ->> 'role' IN ('moderator', 'admin'))
WITH CHECK (status IN ('approved', 'rejected'));
-- ============================================================================
-- HELPER FUNCTIONS
-- ============================================================================
-- Check if user has specific role
CREATE OR REPLACE FUNCTION auth.user_has_role(required_role TEXT)
RETURNS BOOLEAN AS $$
BEGIN
RETURN (auth.jwt() ->> 'role') = required_role;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Check if user is in tenant
CREATE OR REPLACE FUNCTION auth.user_in_tenant(target_tenant_id UUID)
RETURNS BOOLEAN AS $$
BEGIN
RETURN EXISTS (
SELECT 1 FROM user_tenants
WHERE user_id = auth.uid() AND tenant_id = target_tenant_id
);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Check team membership
CREATE OR REPLACE FUNCTION auth.user_in_team(target_team_id UUID)
RETURNS BOOLEAN AS $$
BEGIN
RETURN EXISTS (
SELECT 1 FROM user_teams
WHERE user_id = auth.uid() AND team_id = target_team_id
);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Check organization role
CREATE OR REPLACE FUNCTION auth.user_org_role(org_id UUID)
RETURNS TEXT AS $$
DECLARE
user_role TEXT;
BEGIN
SELECT role INTO user_role
FROM user_organizations
WHERE user_id = auth.uid() AND organization_id = org_id;
RETURN user_role;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Check if user can access resource
CREATE OR REPLACE FUNCTION auth.can_access_resource(
resource_owner_id UUID,
resource_is_public BOOLEAN DEFAULT false
)
RETURNS BOOLEAN AS $$
BEGIN
RETURN (
auth.uid() = resource_owner_id OR
resource_is_public = true OR
auth.jwt() ->> 'role' = 'admin'
);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- ============================================================================
-- INDEXES FOR PERFORMANCE
-- ============================================================================
-- Always index columns used in RLS policies
CREATE INDEX IF NOT EXISTS idx_table_user_id ON table_name(user_id);
CREATE INDEX IF NOT EXISTS idx_table_tenant_id ON table_name(tenant_id);
CREATE INDEX IF NOT EXISTS idx_table_owner_id ON table_name(owner_id);
CREATE INDEX IF NOT EXISTS idx_table_is_public ON table_name(is_public);
CREATE INDEX IF NOT EXISTS idx_table_status ON table_name(status);
CREATE INDEX IF NOT EXISTS idx_table_published_at ON table_name(published_at);
-- Composite indexes for common queries
CREATE INDEX IF NOT EXISTS idx_table_user_status
ON table_name(user_id, status);
CREATE INDEX IF NOT EXISTS idx_table_tenant_created
ON table_name(tenant_id, created_at DESC);
-- ============================================================================
-- GRANT PERMISSIONS
-- ============================================================================
-- Grant table access to authenticated users
GRANT SELECT, INSERT, UPDATE, DELETE ON table_name TO authenticated;
-- Grant read-only to anonymous for public content
GRANT SELECT ON table_name TO anon;
-- Grant usage on sequences
GRANT USAGE, SELECT ON SEQUENCE table_name_id_seq TO authenticated;

View File

@@ -0,0 +1,393 @@
# RLS Policy Patterns
This reference document provides common Row-Level Security patterns for Supabase applications.
## Pattern 1: User Ownership
Simple pattern where users can only access their own records.
```sql
-- Enable RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- User can view own posts
CREATE POLICY "Users view own posts"
ON posts FOR SELECT
USING (auth.uid() = user_id);
-- User can insert posts with their ID
CREATE POLICY "Users create own posts"
ON posts FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- User can update own posts
CREATE POLICY "Users update own posts"
ON posts FOR UPDATE
USING (auth.uid() = user_id)
WITH CHECK (auth.uid() = user_id);
-- User can delete own posts
CREATE POLICY "Users delete own posts"
ON posts FOR DELETE
USING (auth.uid() = user_id);
```
## Pattern 2: Multi-Tenant Isolation
Users can access all records within their tenant.
```sql
-- Enable RLS
ALTER TABLE documents ENABLE ROW LEVEL SECURITY;
-- Users can only see documents in their tenant
CREATE POLICY "Tenant isolation"
ON documents FOR ALL
USING (
tenant_id IN (
SELECT tenant_id FROM user_tenants
WHERE user_id = auth.uid()
)
);
```
## Pattern 3: Role-Based Access Control (RBAC)
Different access levels based on user roles.
```sql
-- Enable RLS
ALTER TABLE sensitive_data ENABLE ROW LEVEL SECURITY;
-- Admins can view all records
CREATE POLICY "Admins view all"
ON sensitive_data FOR SELECT
USING (auth.jwt() ->> 'role' = 'admin');
-- Managers can view team records
CREATE POLICY "Managers view team"
ON sensitive_data FOR SELECT
USING (
auth.jwt() ->> 'role' = 'manager' AND
team_id IN (
SELECT team_id FROM user_teams
WHERE user_id = auth.uid() AND role = 'manager'
)
);
-- Regular users can view own records
CREATE POLICY "Users view own"
ON sensitive_data FOR SELECT
USING (auth.uid() = user_id);
```
## Pattern 4: Public/Private Resources
Some records are public, others are private.
```sql
-- Enable RLS
ALTER TABLE articles ENABLE ROW LEVEL SECURITY;
-- Everyone can view public articles
CREATE POLICY "Public articles readable"
ON articles FOR SELECT
USING (is_public = true);
-- Authenticated users can view their private articles
CREATE POLICY "Private articles owner only"
ON articles FOR SELECT
USING (is_public = false AND auth.uid() = user_id);
-- Only author can update
CREATE POLICY "Author can update"
ON articles FOR UPDATE
USING (auth.uid() = user_id);
```
## Pattern 5: Hierarchical Permissions
Organization > Team > User hierarchy.
```sql
-- Enable RLS
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
-- Organization admins see all organization projects
CREATE POLICY "Org admins view all"
ON projects FOR SELECT
USING (
organization_id IN (
SELECT organization_id FROM user_organizations
WHERE user_id = auth.uid() AND role = 'admin'
)
);
-- Team members see team projects
CREATE POLICY "Team members view team projects"
ON projects FOR SELECT
USING (
team_id IN (
SELECT team_id FROM user_teams
WHERE user_id = auth.uid()
)
);
-- Project owner can always view
CREATE POLICY "Owner can view"
ON projects FOR SELECT
USING (auth.uid() = owner_id);
```
## Pattern 6: Shared Resources
Resources shared with specific users.
```sql
-- Enable RLS
ALTER TABLE files ENABLE ROW LEVEL SECURITY;
-- Owner can view
CREATE POLICY "Owner can view files"
ON files FOR SELECT
USING (auth.uid() = owner_id);
-- Shared users can view
CREATE POLICY "Shared users can view"
ON files FOR SELECT
USING (
EXISTS (
SELECT 1 FROM file_shares
WHERE file_id = files.id
AND user_id = auth.uid()
AND (expires_at IS NULL OR expires_at > NOW())
)
);
-- Public files viewable by all authenticated
CREATE POLICY "Public files viewable"
ON files FOR SELECT
USING (is_public = true);
```
## Pattern 7: JWT Claims-Based Access
Access based on custom JWT claims.
```sql
-- Enable RLS
ALTER TABLE premium_content ENABLE ROW LEVEL SECURITY;
-- Premium users can view premium content
CREATE POLICY "Premium users access"
ON premium_content FOR SELECT
USING (
(auth.jwt() -> 'app_metadata' ->> 'subscription_tier') IN ('premium', 'enterprise')
);
-- Organization-scoped access
CREATE POLICY "Organization scoped"
ON premium_content FOR SELECT
USING (
organization_id = (auth.jwt() -> 'app_metadata' ->> 'organization_id')::uuid
);
```
## Pattern 8: Time-Based Access
Access that expires or becomes available at certain times.
```sql
-- Enable RLS
ALTER TABLE scheduled_content ENABLE ROW LEVEL SECURITY;
-- Content available after publish date
CREATE POLICY "Published content viewable"
ON scheduled_content FOR SELECT
USING (
published_at IS NOT NULL AND
published_at <= NOW() AND
(expires_at IS NULL OR expires_at > NOW())
);
-- Authors can always view
CREATE POLICY "Authors view all states"
ON scheduled_content FOR SELECT
USING (auth.uid() = author_id);
```
## Pattern 9: Cascading Permissions
Permissions inherited from parent resources.
```sql
-- Enable RLS on both tables
ALTER TABLE folders ENABLE ROW LEVEL SECURITY;
ALTER TABLE folder_items ENABLE ROW LEVEL SECURITY;
-- Folder access based on ownership or sharing
CREATE POLICY "Folder access"
ON folders FOR SELECT
USING (
auth.uid() = owner_id OR
EXISTS (
SELECT 1 FROM folder_permissions
WHERE folder_id = folders.id AND user_id = auth.uid()
)
);
-- Item access inherits from folder
CREATE POLICY "Item access via folder"
ON folder_items FOR SELECT
USING (
folder_id IN (
SELECT id FROM folders
WHERE auth.uid() = owner_id OR
EXISTS (
SELECT 1 FROM folder_permissions
WHERE folder_id = folders.id AND user_id = auth.uid()
)
)
);
```
## Pattern 10: Conditional Write Restrictions
Different rules for reading vs writing.
```sql
-- Enable RLS
ALTER TABLE moderated_content ENABLE ROW LEVEL SECURITY;
-- Anyone authenticated can view approved content
CREATE POLICY "View approved content"
ON moderated_content FOR SELECT
USING (status = 'approved');
-- Users can insert content (starts as pending)
CREATE POLICY "Users can create content"
ON moderated_content FOR INSERT
WITH CHECK (
auth.uid() = author_id AND
status = 'pending'
);
-- Only moderators can approve
CREATE POLICY "Moderators can approve"
ON moderated_content FOR UPDATE
USING (auth.jwt() ->> 'role' = 'moderator')
WITH CHECK (
status IN ('approved', 'rejected') AND
(OLD.status != 'approved' OR auth.jwt() ->> 'role' = 'admin')
);
```
## Helper Functions
Reusable functions for complex RLS logic.
```sql
-- Check if user has specific role
CREATE OR REPLACE FUNCTION auth.user_has_role(required_role TEXT)
RETURNS BOOLEAN AS $$
BEGIN
RETURN (auth.jwt() ->> 'role') = required_role;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Check tenant membership
CREATE OR REPLACE FUNCTION auth.user_in_tenant(target_tenant_id UUID)
RETURNS BOOLEAN AS $$
BEGIN
RETURN EXISTS (
SELECT 1 FROM user_tenants
WHERE user_id = auth.uid() AND tenant_id = target_tenant_id
);
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
-- Check organization permission
CREATE OR REPLACE FUNCTION auth.user_can_access_org(org_id UUID, min_role TEXT DEFAULT 'member')
RETURNS BOOLEAN AS $$
DECLARE
user_role TEXT;
role_hierarchy TEXT[] := ARRAY['member', 'manager', 'admin', 'owner'];
min_role_level INT;
user_role_level INT;
BEGIN
SELECT role INTO user_role
FROM user_organizations
WHERE user_id = auth.uid() AND organization_id = org_id;
IF user_role IS NULL THEN
RETURN FALSE;
END IF;
min_role_level := array_position(role_hierarchy, min_role);
user_role_level := array_position(role_hierarchy, user_role);
RETURN user_role_level >= min_role_level;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;
```
## Performance Tips
1. **Index Policy Columns**: Add indexes on columns used in policies
```sql
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE INDEX idx_documents_tenant_id ON documents(tenant_id);
CREATE INDEX idx_files_owner_id ON files(owner_id);
```
2. **Avoid Subqueries**: Use helper functions for complex logic
```sql
-- Slow
USING (
user_id IN (
SELECT user_id FROM complex_query WHERE ...
)
)
-- Faster
USING (auth.check_user_access(user_id))
```
3. **Materialized Views**: For complex hierarchical permissions
```sql
CREATE MATERIALIZED VIEW user_accessible_resources AS
SELECT user_id, resource_id, access_level
FROM ... -- complex joins
-- Refresh periodically
REFRESH MATERIALIZED VIEW user_accessible_resources;
```
## Testing RLS Policies
Always test policies with different user contexts:
```sql
-- Test as specific user
SET request.jwt.claim.sub = 'user-uuid-here';
SELECT * FROM table_name;
-- Test as specific role
SET request.jwt.claim.role = 'admin';
SELECT * FROM table_name;
-- Test insert
INSERT INTO table_name (user_id, content) VALUES (auth.uid(), 'test');
-- Reset
RESET ALL;
```
## Common Pitfalls
1. **Forgetting WITH CHECK**: INSERT/UPDATE policies need WITH CHECK clause
2. **Not enabling RLS**: ALTER TABLE ... ENABLE ROW LEVEL SECURITY is required
3. **Overly complex policies**: Keep policies simple, use helper functions
4. **Missing indexes**: Policy columns need indexes for performance
5. **Testing as superuser**: Superusers bypass RLS, test as regular users
6. **Not considering anon vs authenticated**: Different policies for public access

View File

@@ -0,0 +1,303 @@
# Supabase Auth Reference
This document provides reference information for Supabase authentication functions and JWT structure.
## Core Auth Functions
### auth.uid()
Returns the UUID of the currently authenticated user.
```sql
-- Usage in RLS policy
CREATE POLICY "User access own records"
ON table_name FOR SELECT
USING (auth.uid() = user_id);
```
**Returns**: UUID or NULL if unauthenticated
**Common Uses**:
- User ownership checks
- Filtering user-specific data
- Inserting records with current user ID
### auth.jwt()
Returns the full JSON Web Token for the authenticated user.
```sql
-- Access top-level claims
auth.jwt() ->> 'role' -- Returns: 'authenticated', 'anon', custom role
-- Access nested app_metadata
auth.jwt() -> 'app_metadata' ->> 'subscription_tier'
auth.jwt() -> 'app_metadata' ->> 'organization_id'
-- Access user_metadata
auth.jwt() -> 'user_metadata' ->> 'display_name'
```
**Returns**: JSONB object or NULL if unauthenticated
## JWT Structure
Standard Supabase JWT contains:
```json
{
"aud": "authenticated",
"exp": 1234567890,
"sub": "user-uuid",
"email": "user@example.com",
"phone": "",
"app_metadata": {
"provider": "email",
"providers": ["email"]
},
"user_metadata": {
"display_name": "John Doe",
"avatar_url": "https://..."
},
"role": "authenticated",
"aal": "aal1",
"amr": [
{
"method": "password",
"timestamp": 1234567890
}
],
"session_id": "session-uuid"
}
```
### Custom Claims
Add custom claims via database functions or auth hooks:
```sql
-- Example: Add custom role from database
CREATE OR REPLACE FUNCTION auth.custom_access_token_hook(event jsonb)
RETURNS jsonb
LANGUAGE plpgsql
AS $$
DECLARE
user_role TEXT;
org_id UUID;
BEGIN
-- Get user's role and organization
SELECT role, organization_id INTO user_role, org_id
FROM user_profiles
WHERE id = (event->>'user_id')::uuid;
-- Add custom claims
event := jsonb_set(event, '{claims,role}', to_jsonb(user_role));
event := jsonb_set(event, '{claims,organization_id}', to_jsonb(org_id::text));
RETURN event;
END;
$$;
```
Access custom claims in policies:
```sql
-- Check custom role
CREATE POLICY "Admin access"
ON table_name FOR ALL
USING (auth.jwt() ->> 'role' = 'admin');
-- Check custom organization
CREATE POLICY "Organization access"
ON table_name FOR SELECT
USING (
organization_id = (auth.jwt() ->> 'organization_id')::uuid
);
```
## Common Patterns
### Check Authentication Status
```sql
-- Require authenticated user
USING (auth.uid() IS NOT NULL)
-- Allow both authenticated and anonymous
USING (true)
-- Authenticated users only
USING (auth.jwt() ->> 'role' = 'authenticated')
```
### Role-Based Access
```sql
-- Simple role check
USING (auth.jwt() ->> 'role' = 'admin')
-- Multiple roles
USING (
auth.jwt() ->> 'role' IN ('admin', 'moderator')
)
-- Custom role from app_metadata
USING (
auth.jwt() -> 'app_metadata' ->> 'custom_role' = 'premium_user'
)
```
### Email Verification
```sql
-- Require verified email
USING (
(auth.jwt() -> 'email_confirmed_at') IS NOT NULL
)
```
### Multi-Factor Authentication
```sql
-- Require MFA
USING (
auth.jwt() ->> 'aal' = 'aal2'
)
```
## Session Management
### Session ID
Access current session:
```sql
-- Filter by session
USING (
session_id = (auth.jwt() ->> 'session_id')::uuid
)
```
### Authentication Method
Check how user authenticated:
```sql
-- Check for password authentication
USING (
EXISTS (
SELECT 1 FROM jsonb_array_elements(auth.jwt() -> 'amr') AS method
WHERE method ->> 'method' = 'password'
)
)
-- Check for OAuth
USING (
EXISTS (
SELECT 1 FROM jsonb_array_elements(auth.jwt() -> 'amr') AS method
WHERE method ->> 'method' = 'oauth'
)
)
```
## User Metadata vs App Metadata
### User Metadata
- User-controlled data
- Can be updated by user
- Store preferences, profile info
- Access: `auth.jwt() -> 'user_metadata'`
### App Metadata
- Admin-controlled data
- Cannot be updated by user
- Store roles, permissions, org IDs
- Access: `auth.jwt() -> 'app_metadata'`
## Security Considerations
### Always Validate User ID
```sql
-- GOOD: Enforce user ID on insert
CREATE POLICY "Insert own records"
ON table_name FOR INSERT
WITH CHECK (auth.uid() = user_id);
-- BAD: Allow any user_id
CREATE POLICY "Insert records"
ON table_name FOR INSERT
WITH CHECK (true);
```
### Don't Trust Client-Provided IDs
```sql
-- GOOD: Use auth.uid() directly
INSERT INTO posts (user_id, content)
VALUES (auth.uid(), 'content');
-- BAD: Trust client-provided user_id
INSERT INTO posts (user_id, content)
VALUES ($1, $2); -- Client could pass any user_id
```
### Use SECURITY DEFINER Carefully
```sql
-- Mark function as SECURITY DEFINER only when necessary
CREATE FUNCTION check_permission()
RETURNS BOOLEAN
SECURITY DEFINER -- Runs with function owner's privileges
AS $$
-- Be very careful here - this bypasses RLS
$$;
```
## Testing Auth Context
Test policies with different auth states:
```sql
-- Set user ID
SET request.jwt.claim.sub = 'user-uuid';
-- Set role
SET request.jwt.claim.role = 'admin';
-- Set custom claims
SET request.jwt.claims = '{"role": "admin", "organization_id": "org-uuid"}';
-- Reset
RESET request.jwt.claim.sub;
RESET request.jwt.claim.role;
RESET request.jwt.claims;
```
## Common Errors
### "function auth.uid() does not exist"
- RLS is not enabled on the database
- Not using Supabase's auth schema
### NULL when expecting user ID
- User is not authenticated
- Token has expired
- Policy executed outside auth context
### "permission denied for relation"
- RLS is enabled but no policies match
- User doesn't have base GRANT permissions
## Performance Tips
1. **Cache JWT lookups**: Use helper functions to avoid repeated JWT parsing
2. **Index foreign keys**: Always index user_id, tenant_id, etc.
3. **Avoid complex JWT parsing**: Move complex logic to helper functions
4. **Use materialized views**: For complex role hierarchies
## Additional Resources
- Supabase Auth Documentation: https://supabase.com/docs/guides/auth
- Row Level Security Guide: https://supabase.com/docs/guides/auth/row-level-security
- JWT Customization: https://supabase.com/docs/guides/auth/custom-claims-and-role-based-access-control-rbac