Claude-skill-registry firestore-security-rules-generation
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/firestore-security-rules-generation" ~/.claude/skills/majiayu000-claude-skill-registry-firestore-security-rules-generation && rm -rf "$T"
manifest:
skills/data/firestore-security-rules-generation/SKILL.mdsource content
Firestore Security Rules Generation
Overview
Firestore Security Rules define who can access what data and under what conditions. Rules are enforced at the database level, providing a critical security layer.
Rules Syntax Fundamentals
Basic Structure
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { // Rules go here } }
Match Patterns
// Exact match match /users/alice { } // Wildcard (single document) match /users/{userId} { } // Recursive wildcard (all documents in subcollections) match /users/{userId}/{document=**} { }
Allow Operations
allow read; // get() and list() operations allow write; // create(), update(), delete() operations // Or be specific: allow get; // Read single document allow list; // Query/list documents allow create; // Create document allow update; // Update document allow delete; // Delete document
Authentication Patterns
User-Scoped Access
Pattern: Users can only access their own data
match /users/{userId} { allow read, write: if request.auth.uid == userId; } match /profiles/{profileId} { allow read: if true; // Public read allow write: if request.auth.uid == resource.data.ownerId; }
Authenticated Only
match /posts/{postId} { allow read: if request.auth != null; allow create: if request.auth != null; }
Public Read, Authenticated Write
match /posts/{postId} { allow read: if true; // Anyone can read allow create: if request.auth != null; // Must be logged in to create allow update, delete: if request.auth.uid == resource.data.authorId; }
Role-Based Access Control (RBAC)
Custom Claims Pattern
Set Claims (Server-Side):
await adminAuth.setCustomUserClaims(uid, { role: 'admin' });
Rules:
// Helper functions function isAuthenticated() { return request.auth != null; } function hasRole(role) { return isAuthenticated() && request.auth.token.role == role; } function isAdmin() { return hasRole('admin'); } // Admin-only collection match /admin-data/{document} { allow read, write: if isAdmin(); } // Role-based read access match /posts/{postId} { allow read: if resource.data.visibility == 'public' || isAdmin() || hasRole('moderator'); }
Multi-Role Rules
function hasAnyRole(roles) { return isAuthenticated() && request.auth.token.role in roles; } match /moderation/{docId} { allow read, write: if hasAnyRole(['admin', 'moderator']); }
Multi-Tenant Security
Pattern 1: Tenant ID in Document
Data Structure:
{ id: "post1", tenantId: "tenant_abc", content: "...", }
Set Tenant Claim (Server-Side):
await adminAuth.setCustomUserClaims(uid, { tenantId: 'tenant_abc' });
Rules:
function belongsToTenant(tenantId) { return isAuthenticated() && request.auth.token.tenantId == tenantId; } match /posts/{postId} { allow read, write: if belongsToTenant(resource.data.tenantId); } // For creates (resource.data doesn't exist yet) match /posts/{postId} { allow create: if isAuthenticated() && request.resource.data.tenantId == request.auth.token.tenantId; }
Pattern 2: Subcollection Per Tenant
tenants/tenant_abc/posts/post1 tenants/tenant_abc/users/user1
Rules:
match /tenants/{tenantId}/{document=**} { allow read, write: if request.auth.token.tenantId == tenantId; }
Field Validation
Required Fields
function hasRequiredFields(fields) { return request.resource.data.keys().hasAll(fields); } match /posts/{postId} { allow create: if hasRequiredFields(['title', 'content', 'authorId', 'createdAt']); }
Data Type Validation
match /posts/{postId} { allow create: if request.resource.data.title is string && request.resource.data.title.size() > 0 && request.resource.data.title.size() <= 200 && request.resource.data.viewCount is int && request.resource.data.viewCount >= 0; }
Enum Validation
match /posts/{postId} { allow create, update: if request.resource.data.status in ['draft', 'published', 'archived']; }
Immutable Fields
function isImmutable(field) { return request.resource.data[field] == resource.data[field]; } match /posts/{postId} { allow update: if isImmutable('authorId') && isImmutable('createdAt'); }
Field Length Limits
match /posts/{postId} { allow create: if request.resource.data.title.size() >= 1 && request.resource.data.title.size() <= 200 && request.resource.data.content.size() >= 10 && request.resource.data.content.size() <= 10000; }
Relationship Validation
Reference Exists
match /comments/{commentId} { allow create: if exists(/databases/$(database)/documents/posts/$(request.resource.data.postId)); }
Parent-Child Relationship
match /posts/{postId}/comments/{commentId} { allow read: if exists(/databases/$(database)/documents/posts/$(postId)); allow create: if request.auth != null && exists(/databases/$(database)/documents/posts/$(postId)); }
Advanced Patterns
Conditional Access Based on Document State
match /posts/{postId} { // Anyone can read published posts allow read: if resource.data.status == 'published'; // Author can read their own posts (any status) allow read: if request.auth.uid == resource.data.authorId; // Admins can read all allow read: if isAdmin(); }
Rate Limiting (Approximate)
match /posts/{postId} { allow create: if request.auth != null && request.time < resource.data.lastPostTime + duration.value(1, 'm'); // 1 minute cooldown }
Note: Firestore rules don't have built-in rate limiting. Use Cloud Functions for accurate rate limiting.
Soft Delete Pattern
match /posts/{postId} { // Prevent actual deletion allow delete: if false; // Only allow marking as deleted allow update: if request.auth.uid == resource.data.authorId && request.resource.data.diff(resource.data).affectedKeys().hasOnly(['deletedAt']); }
Complete Example
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { // ======================================================================== // HELPER FUNCTIONS // ======================================================================== function isAuthenticated() { return request.auth != null; } function isOwner(userId) { return isAuthenticated() && request.auth.uid == userId; } function hasRole(role) { return isAuthenticated() && request.auth.token.role == role; } function isAdmin() { return hasRole('admin'); } function hasRequiredFields(fields) { return request.resource.data.keys().hasAll(fields); } function isImmutable(field) { return request.resource.data[field] == resource.data[field]; } // ======================================================================== // USERS COLLECTION // ======================================================================== match /users/{userId} { allow read: if isOwner(userId) || isAdmin(); allow create: if isOwner(userId) && hasRequiredFields(['email', 'displayName', 'createdAt']); allow update: if isOwner(userId) && isImmutable('createdAt') && (!('role' in request.resource.data) || isImmutable('role')); allow delete: if isAdmin(); } // ======================================================================== // POSTS COLLECTION // ======================================================================== match /posts/{postId} { allow read: if resource.data.status == 'published' || isOwner(resource.data.authorId) || isAdmin(); allow create: if isAuthenticated() && request.resource.data.authorId == request.auth.uid && hasRequiredFields(['title', 'content', 'authorId', 'status', 'createdAt']) && request.resource.data.status in ['draft', 'published', 'archived'] && request.resource.data.title is string && request.resource.data.title.size() > 0 && request.resource.data.title.size() <= 200; allow update: if (isOwner(resource.data.authorId) || isAdmin()) && isImmutable('authorId') && isImmutable('createdAt'); allow delete: if isOwner(resource.data.authorId) || isAdmin(); } // ======================================================================== // COMMENTS COLLECTION // ======================================================================== match /comments/{commentId} { allow read: if true; allow create: if isAuthenticated() && request.resource.data.authorId == request.auth.uid && exists(/databases/$(database)/documents/posts/$(request.resource.data.postId)) && request.resource.data.content.size() > 0 && request.resource.data.content.size() <= 1000; allow update: if isOwner(resource.data.authorId) && isImmutable('authorId') && isImmutable('postId'); allow delete: if isOwner(resource.data.authorId) || isAdmin(); } } }
Testing Rules
Local Testing with Emulator
// firestore.rules.spec.ts import { assertFails, assertSucceeds } from '@firebase/rules-unit-testing'; import { initializeTestEnvironment } from '@firebase/rules-unit-testing'; let testEnv; beforeAll(async () => { testEnv = await initializeTestEnvironment({ projectId: 'test-project', firestore: { rules: fs.readFileSync('firestore.rules', 'utf8'), }, }); }); test('User can read their own profile', async () => { const alice = testEnv.authenticatedContext('alice'); await assertSucceeds(alice.firestore().collection('users').doc('alice').get()); }); test('User cannot read other profiles', async () => { const alice = testEnv.authenticatedContext('alice'); await assertFails(alice.firestore().collection('users').doc('bob').get()); });
Deployment
# Deploy rules only firebase deploy --only firestore:rules # Validate rules before deploying firebase firestore:rules:release
Best Practices
✅ Do:
- Default deny, explicitly allow
- Validate all user input (types, sizes, ranges)
- Use helper functions for reusable logic
- Test rules locally before deploying
- Document complex rules with comments
- Use custom claims for RBAC (not Firestore lookups)
- Enforce immutable fields (createdAt, authorId)
❌ Don't:
- Use
in production (except truly public data)allow read, write: if true - Perform Firestore lookups in rules (slow, limited to 10 per request)
- Store sensitive data in custom claims (1000 byte limit)
- Skip field validation
- Forget to handle create vs update differences
- Use rules for rate limiting (use Cloud Functions)
Common Pitfalls
Issue:
resource.data is null on create
Solution: Use request.resource.data for creates
Issue: "Property is undefined" error Solution: Check field exists first:
'field' in resource.data
Issue: Rules too slow Solution: Avoid
exists() and get() calls, use custom claims instead
Related Skills:
firebase-authentication-patterns, firebase-admin-sdk-server-integration
Token Estimate: ~1,400 tokens