@rip-user/rls-debugger-mcp
Version:
AI-powered MCP server for debugging Supabase Row Level Security policies with Claude structured outputs
274 lines (211 loc) • 8.22 kB
Markdown
---
name: RLS Policy Debugger
description: Debug Supabase Row Level Security policies using AI-powered analysis
tags: [supabase, rls, debugging, postgres, security]
requires:
mcp_servers: [rls-debugger]
env_vars: [SUPABASE_URL, SUPABASE_SERVICE_KEY, ANTHROPIC_API_KEY]
---
# RLS Policy Debugger Skill
Systematically debug Supabase Row Level Security (RLS) policies using the rls-debugger MCP server.
## When to Use This Skill
- User reports access denied errors
- Policies not behaving as expected
- Multiple policies creating complex interactions
- Need to understand policy decision flow
- Debugging multi-tenant access patterns
## Available MCP Tools
1. **analyze_rls_policies** - AI-powered policy analysis with structured output
2. **compile_policy_logic** - Generate decision trees showing policy combinations
3. **list_all_policies** - View all RLS policies in the database
4. **save_policy_knowledge** - Remember architectural patterns and decisions
5. **get_policy_knowledge** - Retrieve saved insights from previous sessions
## Debugging Workflow
### Step 1: Gather Context
Start by understanding the scenario:
- What is the user trying to do?
- Which table are they accessing?
- What operation (SELECT, INSERT, UPDATE, DELETE)?
- User's role and permissions
### Step 2: List and Analyze Policies
Use `list_all_policies` to see all RLS policies, then `analyze_rls_policies` with the specific scenario.
### Step 3: Review Decision Logic
Use `compile_policy_logic` to understand how PERMISSIVE (OR) and RESTRICTIVE (AND) policies combine.
### Step 4: Identify Root Cause
The analysis will show:
- Which policies apply to the scenario
- Why access is granted or denied
- Missing relationships or conditions
- Verification SQL queries to check data
### Step 5: Save Learnings
Use `save_policy_knowledge` to remember architectural decisions and known patterns for future debugging.
## RLS Policy Fundamentals
### PERMISSIVE Policies (Default)
- Use OR logic - ANY one policy grants access
- Most common type
- Example: "User owns record OR record is public"
### RESTRICTIVE Policies
- Use AND logic - ALL policies must pass
- Applied on top of PERMISSIVE
- Example: "Record must be active AND user subscribed"
### How They Combine
```
FINAL ACCESS = (Any PERMISSIVE passes) AND (All RESTRICTIVE pass)
```
## Example Debugging Session
**Scenario:** User can't see their orders
\`\`\`
User: "User abc-123 can't access their orders"
Step 1 - Analyze:
"Using rls-debugger, analyze why user abc-123 can't access the orders table"
Step 2 - Review logic:
"Compile the policy logic for orders to see how policies combine"
Step 3 - Check relationships:
Review the verification queries to see if required relationships exist
Step 4 - Save findings:
"Save to memory: orders table uses tenant_id isolation pattern"
\`\`\`
## Common RLS Patterns
### Pattern 1: Multi-tenant Isolation
\`\`\`sql
CREATE POLICY "tenant_isolation" ON orders
FOR ALL USING (tenant_id = auth.uid());
\`\`\`
Access granted if: user's ID matches the tenant_id column
### Pattern 2: Role-based Access
\`\`\`sql
CREATE POLICY "admin_access" ON orders
FOR ALL USING (
EXISTS (
SELECT 1 FROM user_roles
WHERE user_id = auth.uid() AND role = 'admin'
)
);
\`\`\`
Access granted if: user has admin role in user_roles table
### Pattern 3: Hierarchical Access
\`\`\`sql
CREATE POLICY "org_access" ON documents
FOR SELECT USING (
org_id IN (
SELECT org_id FROM user_orgs
WHERE user_id = auth.uid()
)
);
\`\`\`
Access granted if: user belongs to the organization
### Pattern 4: Ownership + Public
\`\`\`sql
-- Policy 1: Own records (PERMISSIVE)
CREATE POLICY "own_posts" ON posts
FOR SELECT USING (user_id = auth.uid());
-- Policy 2: Public records (PERMISSIVE)
CREATE POLICY "public_posts" ON posts
FOR SELECT USING (is_public = true);
\`\`\`
Access granted if: user owns it OR it's public (OR logic)
### Pattern 5: Base Access + Restrictions
\`\`\`sql
-- Base access (PERMISSIVE)
CREATE POLICY "team_members" ON projects
FOR SELECT USING (
EXISTS (
SELECT 1 FROM team_members
WHERE user_id = auth.uid() AND team_id = projects.team_id
)
);
-- Must be active (RESTRICTIVE)
CREATE POLICY "active_only" ON projects
FOR SELECT AS RESTRICTIVE
USING (status = 'active');
\`\`\`
Access granted if: (user is team member) AND (project is active)
## Best Practices
1. **Always check saved knowledge first** - Load context from previous sessions
2. **Verify relationships exist** - Run the provided SQL verification queries
3. **Consider performance** - Index foreign keys used in policy clauses
4. **Save architectural decisions** - Help future debugging sessions
5. **Test policy changes carefully** - Use `risk_level` in recommendations
## Common Issues Detected
### Issue 1: Missing Junction Table Records
**Symptom:** Policy checks EXISTS but record doesn't exist
**Fix:** Insert the missing relationship record
**Example:** User exists but team_members entry is missing
### Issue 2: Policy Type Mismatch
**Symptom:** Multiple conditions that should ALL pass, but defined as PERMISSIVE
**Fix:** Change to RESTRICTIVE policy
**Example:** "active AND subscribed" should be RESTRICTIVE, not PERMISSIVE
### Issue 3: Overly Complex Conditions
**Symptom:** Slow queries, multiple JOINs in policy
**Fix:** Use materialized views or simplify logic
**Example:** Policy with 4-level deep JOIN
### Issue 4: Missing WITH CHECK Clause
**Symptom:** INSERT/UPDATE allowed but creates inaccessible rows
**Fix:** Add WITH CHECK clause matching USING clause
**Example:** User can insert but can't read what they inserted
### Issue 5: Conflicting Policies
**Symptom:** Multiple PERMISSIVE policies that never both pass
**Fix:** Combine into single policy with OR conditions
**Example:** Two policies checking different roles but should be one
## Performance Tips
### Index Foreign Keys
Always index columns used in policy clauses:
\`\`\`sql
CREATE INDEX idx_orders_tenant_id ON orders(tenant_id);
CREATE INDEX idx_team_members_user_id ON team_members(user_id);
\`\`\`
### Avoid Complex JOINs
Use materialized views for complex relationships:
\`\`\`sql
-- Instead of complex EXISTS in policy:
CREATE MATERIALIZED VIEW user_accessible_orgs AS
SELECT user_id, org_id FROM ...complex query...;
-- Simple policy:
CREATE POLICY "org_access" ON documents
FOR SELECT USING (
org_id IN (
SELECT org_id FROM user_accessible_orgs
WHERE user_id = auth.uid()
)
);
\`\`\`
### Use Specific Operations
Don't use `FOR ALL` if you only need SELECT:
\`\`\`sql
-- Good - specific operation
CREATE POLICY "view_orders" ON orders
FOR SELECT USING (...);
-- Less optimal - applies to all operations
CREATE POLICY "orders_access" ON orders
FOR ALL USING (...);
\`\`\`
## Verification Workflow
After analyzing and fixing:
1. **Run verification queries** provided in the analysis
2. **Test the access** with the actual user
3. **Save the knowledge** for future reference
4. **Monitor performance** if policy is complex
## Example Complete Workflow
\`\`\`
1. User: "User abc-123 with admin role can't see document xyz-456"
2. Analyze:
"Using rls-debugger, analyze why user abc-123 can't see document xyz-456"
3. Results show:
- Policy requires team_members record
- User has admin role but no team_members entry
- Verification query: SELECT * FROM team_members WHERE user_id = 'abc-123'
4. Fix:
INSERT INTO team_members (user_id, org_id, role)
VALUES ('abc-123', 'org-789', 'admin');
5. Verify:
SELECT * FROM documents WHERE id = 'xyz-456'; -- Now accessible!
6. Save:
"Save to memory: documents table requires team_members record for access.
Fixed by ensuring all admins have team_members entries."
\`\`\`
## Tips for Effective Usage
- **Be specific in scenarios** - Include user IDs, table names, and operations
- **Start with policy logic** - Understand the structure before diving into analysis
- **Use saved knowledge** - Build a knowledge base over time
- **Verify relationships** - Always run the verification SQL queries
- **Consider security** - Review `risk_level` before applying recommendations