mirror of
https://github.com/greedychipmunk/agent-skills.git
synced 2026-09-14 20:46:42 +08:00
11 KiB
11 KiB
Row Level Security (RLS)
Overview
Row Level Security (RLS) is PostgreSQL's built-in mechanism for controlling access to rows in database tables. In Supabase, RLS is essential for securing your data when using the client library with the anon key.
Enabling RLS
-- Enable RLS on a table
alter table public.posts enable row level security;
-- Force RLS for table owner too (recommended for security)
alter table public.posts force row level security;
-- Disable RLS (not recommended for public tables)
alter table public.posts disable row level security;
Policy Basics
Policy Structure
create policy "policy_name"
on table_name
for [ALL | SELECT | INSERT | UPDATE | DELETE]
to [role_name | PUBLIC]
using (expression) -- For SELECT, UPDATE, DELETE
with check (expression); -- For INSERT, UPDATE
Policy Evaluation
USING: Filters which existing rows can be seen/modifiedWITH CHECK: Validates new/modified row data- Multiple policies: OR'd together (any matching policy allows access)
- No matching policy: Access denied
Common Patterns
Public Read Access
-- Anyone can read published posts
create policy "Public posts are viewable by everyone"
on public.posts
for select
using (published = true);
Authenticated Read Access
-- Only logged-in users can read
create policy "Authenticated users can read posts"
on public.posts
for select
to authenticated
using (true);
Owner-Based Access
-- Users can only see their own data
create policy "Users can view own data"
on public.profiles
for select
using (auth.uid() = id);
-- Users can only update their own data
create policy "Users can update own data"
on public.profiles
for update
using (auth.uid() = id)
with check (auth.uid() = id);
-- Users can only delete their own data
create policy "Users can delete own data"
on public.profiles
for delete
using (auth.uid() = id);
Insert Policies
-- Users can create posts for themselves
create policy "Users can create own posts"
on public.posts
for insert
to authenticated
with check (auth.uid() = author_id);
-- Prevent users from setting certain fields
create policy "Users cannot set admin-only fields"
on public.posts
for insert
to authenticated
with check (
auth.uid() = author_id
and featured is null -- Can't set featured
and approved = false -- Default to not approved
);
Combined Read/Write
-- Users can CRUD their own posts
create policy "Users can manage own posts"
on public.posts
for all
using (auth.uid() = author_id)
with check (auth.uid() = author_id);
Multi-Tenant Patterns
Organization-Based Access
-- Helper function to check membership
create or replace function is_org_member(org_id uuid)
returns boolean as $$
select exists (
select 1 from public.organization_members
where organization_id = org_id
and user_id = auth.uid()
);
$$ language sql security definer stable;
-- Organization members can read org data
create policy "Org members can read org data"
on public.projects
for select
using (is_org_member(organization_id));
-- Only org admins can modify
create or replace function is_org_admin(org_id uuid)
returns boolean as $$
select exists (
select 1 from public.organization_members
where organization_id = org_id
and user_id = auth.uid()
and role in ('owner', 'admin')
);
$$ language sql security definer stable;
create policy "Org admins can update"
on public.projects
for update
using (is_org_admin(organization_id))
with check (is_org_admin(organization_id));
Team-Based Access
-- Projects belong to teams
create table public.team_members (
team_id uuid references public.teams(id) on delete cascade,
user_id uuid references public.profiles(id) on delete cascade,
role text not null default 'member',
primary key (team_id, user_id)
);
-- Function to get user's teams
create or replace function get_user_teams()
returns setof uuid as $$
select team_id from public.team_members
where user_id = auth.uid();
$$ language sql security definer stable;
-- Team members can access team projects
create policy "Team members can read projects"
on public.projects
for select
using (team_id in (select get_user_teams()));
Role-Based Access Control (RBAC)
Custom Roles Table
create table public.user_roles (
user_id uuid references public.profiles(id) on delete cascade primary key,
role text not null default 'user' check (role in ('user', 'moderator', 'admin'))
);
-- Function to check role
create or replace function has_role(required_role text)
returns boolean as $$
select exists (
select 1 from public.user_roles
where user_id = auth.uid()
and role = required_role
);
$$ language sql security definer stable;
-- Admin-only access
create policy "Admins can do everything"
on public.posts
for all
using (has_role('admin'))
with check (has_role('admin'));
-- Moderators can update
create policy "Moderators can update"
on public.posts
for update
using (has_role('moderator') or has_role('admin'));
JWT-Based Roles
-- Using custom claims in JWT
create policy "Admin access via JWT"
on public.admin_settings
for all
using (
(auth.jwt() ->> 'role')::text = 'admin'
);
Hierarchical Data Access
Parent-Child Relationships
-- Comments belong to posts, inherit post access
create policy "Users can read comments on accessible posts"
on public.comments
for select
using (
exists (
select 1 from public.posts
where id = comments.post_id
and (published = true or author_id = auth.uid())
)
);
Recursive Access (Folders/Files)
-- Check access up the folder tree
create or replace function can_access_folder(folder_uuid uuid)
returns boolean as $$
with recursive folder_tree as (
select id, parent_id, owner_id
from public.folders
where id = folder_uuid
union all
select f.id, f.parent_id, f.owner_id
from public.folders f
join folder_tree ft on f.id = ft.parent_id
)
select exists (
select 1 from folder_tree
where owner_id = auth.uid()
);
$$ language sql security definer stable;
create policy "Access folder tree"
on public.folders
for select
using (can_access_folder(id));
Time-Based Access
-- Access only during business hours
create policy "Business hours only"
on public.time_sensitive_data
for select
using (
extract(hour from now() at time zone 'UTC') between 9 and 17
and extract(dow from now()) between 1 and 5
);
-- Temporary access
create policy "Time-limited access"
on public.temporary_data
for select
using (
created_at > now() - interval '24 hours'
);
Audit Logging with RLS
-- Audit table (no RLS, only service role can access)
create table private.audit_log (
id uuid default gen_random_uuid() primary key,
table_name text not null,
record_id uuid not null,
action text not null,
old_data jsonb,
new_data jsonb,
user_id uuid,
created_at timestamptz default now()
);
-- Don't enable RLS on audit table
-- Access only via service role or Edge Functions
Performance Optimization
Index for RLS Columns
-- Always index columns used in RLS policies
create index posts_author_id_idx on public.posts(author_id);
create index org_members_user_id_idx on public.organization_members(user_id);
create index org_members_org_id_idx on public.organization_members(organization_id);
Avoid Complex Subqueries
-- Bad: Complex subquery in every row check
create policy "Slow policy"
on public.posts
for select
using (
author_id in (
select user_id from public.follows
where follower_id = auth.uid()
)
);
-- Better: Use a function with caching hint
create or replace function get_followed_users()
returns setof uuid as $$
select user_id from public.follows
where follower_id = auth.uid();
$$ language sql security definer stable;
create policy "Faster policy"
on public.posts
for select
using (author_id in (select get_followed_users()));
Use Security Definer Functions
-- Security definer runs with creator's permissions
-- Bypasses RLS for the inner query, improving performance
create or replace function is_team_member(team_uuid uuid)
returns boolean as $$
select exists (
select 1 from public.team_members
where team_id = team_uuid
and user_id = auth.uid()
);
$$ language sql security definer stable;
Testing RLS Policies
Test as Different Users
-- Set role to authenticated user
set role authenticated;
set request.jwt.claim.sub = 'user-uuid-here';
-- Run queries and verify results
select * from public.posts;
-- Reset
reset role;
Using Supabase Dashboard
- Go to SQL Editor
- Use "Run as" dropdown to switch between roles
- Test queries with different user contexts
Automated Testing Script
// Test RLS policies
async function testRLS() {
const testUserId = 'test-user-uuid'
const otherUserId = 'other-user-uuid'
// Create test client with specific user context
const userClient = createClient(url, key, {
global: {
headers: {
Authorization: `Bearer ${userToken}`
}
}
})
// Should see own posts
const { data: ownPosts } = await userClient
.from('posts')
.select('*')
.eq('author_id', testUserId)
console.assert(ownPosts?.length > 0, 'Should see own posts')
// Should not see private posts from others
const { data: otherPosts } = await userClient
.from('posts')
.select('*')
.eq('author_id', otherUserId)
.eq('published', false)
console.assert(otherPosts?.length === 0, 'Should not see private posts')
}
Common Mistakes
1. Forgetting to Enable RLS
-- Always enable after creating public tables
create table public.sensitive_data (...);
alter table public.sensitive_data enable row level security;
2. Missing WITH CHECK
-- Wrong: User could insert data for other users
create policy "Bad insert policy"
on public.posts
for insert
using (true);
-- Correct: Validate the inserted data
create policy "Good insert policy"
on public.posts
for insert
with check (auth.uid() = author_id);
3. Not Handling NULL
-- auth.uid() returns NULL for anonymous users
-- This policy accidentally allows anonymous access
create policy "Broken policy"
on public.posts
for select
using (author_id = auth.uid() or published = true);
-- Fixed: Explicitly handle the null case
create policy "Fixed policy"
on public.posts
for select
using (
(auth.uid() is not null and author_id = auth.uid())
or published = true
);
4. Service Role Bypass
-- Service role bypasses RLS by default
-- If you need RLS on service role:
alter table public.posts force row level security;
Debugging RLS
-- Check existing policies
select * from pg_policies where tablename = 'posts';
-- See which policies apply
explain (analyze, verbose)
select * from public.posts;
-- Check auth context
select auth.uid(), auth.role(), auth.jwt();