287 lines
7.0 KiB
Markdown
287 lines
7.0 KiB
Markdown
---
|
|
name: row-level-security
|
|
description: RLS policies for multi-tenant data isolation and access control
|
|
parent-skill: moai-platform-supabase
|
|
version: 1.0.0
|
|
updated: 2026-01-06
|
|
---
|
|
|
|
# Row-Level Security (RLS) Module
|
|
|
|
## Overview
|
|
|
|
Row-Level Security provides automatic data isolation at the database level, ensuring users can only access data they are authorized to see.
|
|
|
|
## Basic Setup
|
|
|
|
Enable RLS on a table:
|
|
|
|
```sql
|
|
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
|
|
```
|
|
|
|
## Policy Types
|
|
|
|
RLS policies can be created for specific operations:
|
|
|
|
- SELECT: Controls read access
|
|
- INSERT: Controls creation
|
|
- UPDATE: Controls modification
|
|
- DELETE: Controls removal
|
|
- ALL: Applies to all operations
|
|
|
|
## Basic Tenant Isolation
|
|
|
|
### JWT-Based Tenant Isolation
|
|
|
|
Extract tenant ID from JWT claims:
|
|
|
|
```sql
|
|
CREATE POLICY "tenant_isolation" ON projects FOR ALL
|
|
USING (tenant_id = (auth.jwt() ->> 'tenant_id')::UUID);
|
|
```
|
|
|
|
### Owner-Based Access
|
|
|
|
Restrict access to resource owners:
|
|
|
|
```sql
|
|
CREATE POLICY "owner_access" ON projects FOR ALL
|
|
USING (owner_id = auth.uid());
|
|
```
|
|
|
|
## Hierarchical Access Patterns
|
|
|
|
### Organization Membership
|
|
|
|
Allow access based on organization membership:
|
|
|
|
```sql
|
|
CREATE POLICY "org_member_select" ON organizations FOR SELECT
|
|
USING (id IN (SELECT org_id FROM org_members WHERE user_id = auth.uid()));
|
|
```
|
|
|
|
### Role-Based Modification
|
|
|
|
Restrict modifications to specific roles:
|
|
|
|
```sql
|
|
CREATE POLICY "org_admin_modify" ON organizations FOR UPDATE
|
|
USING (id IN (
|
|
SELECT org_id FROM org_members
|
|
WHERE user_id = auth.uid() AND role IN ('owner', 'admin')
|
|
));
|
|
```
|
|
|
|
### Cascading Project Access
|
|
|
|
Grant project access through organization membership:
|
|
|
|
```sql
|
|
CREATE POLICY "project_access" ON projects FOR ALL
|
|
USING (org_id IN (SELECT org_id FROM org_members WHERE user_id = auth.uid()));
|
|
```
|
|
|
|
## Service Role Bypass
|
|
|
|
Allow service role to bypass RLS for server-side operations:
|
|
|
|
```sql
|
|
CREATE POLICY "service_bypass" ON organizations FOR ALL TO service_role USING (true);
|
|
```
|
|
|
|
## Multi-Tenant SaaS Schema
|
|
|
|
### Complete Schema Setup
|
|
|
|
```sql
|
|
-- Organizations (tenants)
|
|
CREATE TABLE organizations (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
name TEXT NOT NULL,
|
|
slug TEXT UNIQUE NOT NULL,
|
|
plan TEXT DEFAULT 'free' CHECK (plan IN ('free', 'pro', 'enterprise')),
|
|
settings JSONB DEFAULT '{}',
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
|
|
-- Organization members with roles
|
|
CREATE TABLE organization_members (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
|
|
user_id UUID NOT NULL,
|
|
role TEXT NOT NULL CHECK (role IN ('owner', 'admin', 'member', 'viewer')),
|
|
joined_at TIMESTAMPTZ DEFAULT NOW(),
|
|
UNIQUE(organization_id, user_id)
|
|
);
|
|
|
|
-- Projects within organizations
|
|
CREATE TABLE projects (
|
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
|
organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE,
|
|
name TEXT NOT NULL,
|
|
owner_id UUID NOT NULL,
|
|
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
);
|
|
```
|
|
|
|
### Enable RLS on All Tables
|
|
|
|
```sql
|
|
ALTER TABLE organizations ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE organization_members ENABLE ROW LEVEL SECURITY;
|
|
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
|
|
```
|
|
|
|
### Comprehensive RLS Policies
|
|
|
|
```sql
|
|
-- Organization read access
|
|
CREATE POLICY "org_member_select" ON organizations FOR SELECT
|
|
USING (id IN (SELECT organization_id FROM organization_members WHERE user_id = auth.uid()));
|
|
|
|
-- Organization admin update
|
|
CREATE POLICY "org_admin_update" ON organizations FOR UPDATE
|
|
USING (id IN (SELECT organization_id FROM organization_members
|
|
WHERE user_id = auth.uid() AND role IN ('owner', 'admin')));
|
|
|
|
-- Project member access
|
|
CREATE POLICY "project_member_access" ON projects FOR ALL
|
|
USING (organization_id IN (SELECT organization_id FROM organization_members WHERE user_id = auth.uid()));
|
|
|
|
-- Member management (admin only)
|
|
CREATE POLICY "member_admin_manage" ON organization_members FOR ALL
|
|
USING (organization_id IN (SELECT organization_id FROM organization_members
|
|
WHERE user_id = auth.uid() AND role IN ('owner', 'admin')));
|
|
```
|
|
|
|
## Helper Functions
|
|
|
|
### Check Organization Membership
|
|
|
|
```sql
|
|
CREATE OR REPLACE FUNCTION is_org_member(org_id UUID)
|
|
RETURNS BOOLEAN AS $$
|
|
BEGIN
|
|
RETURN EXISTS (
|
|
SELECT 1 FROM organization_members
|
|
WHERE organization_id = org_id AND user_id = auth.uid()
|
|
);
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
```
|
|
|
|
### Check Organization Role
|
|
|
|
```sql
|
|
CREATE OR REPLACE FUNCTION has_org_role(org_id UUID, required_roles TEXT[])
|
|
RETURNS BOOLEAN AS $$
|
|
BEGIN
|
|
RETURN EXISTS (
|
|
SELECT 1 FROM organization_members
|
|
WHERE organization_id = org_id
|
|
AND user_id = auth.uid()
|
|
AND role = ANY(required_roles)
|
|
);
|
|
END;
|
|
$$ LANGUAGE plpgsql SECURITY DEFINER;
|
|
```
|
|
|
|
### Usage in Policies
|
|
|
|
```sql
|
|
CREATE POLICY "project_admin_delete" ON projects FOR DELETE
|
|
USING (has_org_role(organization_id, ARRAY['owner', 'admin']));
|
|
```
|
|
|
|
## Performance Optimization
|
|
|
|
### Index for RLS Queries
|
|
|
|
Create indexes on foreign keys used in RLS policies:
|
|
|
|
```sql
|
|
CREATE INDEX idx_org_members_user ON organization_members(user_id);
|
|
CREATE INDEX idx_org_members_org ON organization_members(organization_id);
|
|
CREATE INDEX idx_projects_org ON projects(organization_id);
|
|
```
|
|
|
|
### Materialized View for Complex Policies
|
|
|
|
For complex permission checks, use materialized views:
|
|
|
|
```sql
|
|
CREATE MATERIALIZED VIEW user_accessible_projects AS
|
|
SELECT p.id as project_id, om.user_id, om.role
|
|
FROM projects p
|
|
JOIN organization_members om ON p.organization_id = om.organization_id;
|
|
|
|
CREATE INDEX idx_uap_user ON user_accessible_projects(user_id);
|
|
|
|
REFRESH MATERIALIZED VIEW CONCURRENTLY user_accessible_projects;
|
|
```
|
|
|
|
## Testing RLS Policies
|
|
|
|
### Test as Authenticated User
|
|
|
|
```sql
|
|
SET request.jwt.claim.sub = 'user-uuid-here';
|
|
SET request.jwt.claims = '{"role": "authenticated"}';
|
|
|
|
SELECT * FROM projects; -- Returns only accessible projects
|
|
```
|
|
|
|
### Verify Policy Restrictions
|
|
|
|
```sql
|
|
-- Should fail if not a member
|
|
INSERT INTO projects (organization_id, name, owner_id)
|
|
VALUES ('non-member-org-id', 'Test', auth.uid());
|
|
```
|
|
|
|
## Common Patterns
|
|
|
|
### Public Read, Owner Write
|
|
|
|
```sql
|
|
CREATE POLICY "public_read" ON posts FOR SELECT USING (true);
|
|
CREATE POLICY "owner_write" ON posts FOR INSERT WITH CHECK (author_id = auth.uid());
|
|
CREATE POLICY "owner_update" ON posts FOR UPDATE USING (author_id = auth.uid());
|
|
CREATE POLICY "owner_delete" ON posts FOR DELETE USING (author_id = auth.uid());
|
|
```
|
|
|
|
### Draft vs Published
|
|
|
|
```sql
|
|
CREATE POLICY "published_read" ON articles FOR SELECT
|
|
USING (status = 'published' OR author_id = auth.uid());
|
|
```
|
|
|
|
### Time-Based Access
|
|
|
|
```sql
|
|
CREATE POLICY "active_subscription" ON premium_content FOR SELECT
|
|
USING (
|
|
EXISTS (
|
|
SELECT 1 FROM subscriptions
|
|
WHERE user_id = auth.uid()
|
|
AND expires_at > NOW()
|
|
)
|
|
);
|
|
```
|
|
|
|
## Context7 Query Examples
|
|
|
|
For latest RLS documentation:
|
|
|
|
Topic: "row level security policies supabase"
|
|
Topic: "auth.uid auth.jwt functions"
|
|
Topic: "rls performance optimization"
|
|
|
|
---
|
|
|
|
Related Modules:
|
|
- auth-integration.md - Authentication patterns
|
|
- typescript-patterns.md - Client-side access patterns
|