Initial commit
This commit is contained in:
12
.claude-plugin/plugin.json
Normal file
12
.claude-plugin/plugin.json
Normal 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
3
README.md
Normal 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
61
plugin.lock.json
Normal 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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
246
skills/supabase-rls-policy-generator/SKILL.md
Normal file
246
skills/supabase-rls-policy-generator/SKILL.md
Normal 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
|
||||||
@@ -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 |
|
||||||
|
| ... | ... | ... |
|
||||||
396
skills/supabase-rls-policy-generator/assets/policy-templates.sql
Normal file
396
skills/supabase-rls-policy-generator/assets/policy-templates.sql
Normal 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;
|
||||||
393
skills/supabase-rls-policy-generator/references/rls-patterns.md
Normal file
393
skills/supabase-rls-policy-generator/references/rls-patterns.md
Normal 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
|
||||||
303
skills/supabase-rls-policy-generator/references/supabase-auth.md
Normal file
303
skills/supabase-rls-policy-generator/references/supabase-auth.md
Normal 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
|
||||||
Reference in New Issue
Block a user