Claude-skill-registry Firestore Security Rules
Write and validate Firestore Security Rules following the project's multi-tenancy Blueprint pattern. Use this skill when implementing collection-level security, Blueprint membership validation, role-based permissions, and data access controls. Ensures rules validate BlueprintMember status, check permissions array, enforce data isolation, and integrate with the three-layer architecture where Security Rules are the first line of defense.
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" ~/.claude/skills/majiayu000-claude-skill-registry-firestore-security-rules && rm -rf "$T"
manifest:
skills/data/firestore-security-rules/SKILL.mdsource content
Firestore Security Rules Skill
This skill helps create secure Firestore Security Rules following the project's multi-tenancy architecture.
Core Principles
Security-First Architecture
- Security Rules are First Defense: Never trust client-side checks alone
- Multi-Tenancy: Blueprint-based data isolation
- Permission System: Role + permissions array validation
- Server-Side Validation: All access validated at database level
Blueprint Multi-Tenancy Model
User → Organization → Blueprint → Resources ↓ Team / Partner
- Blueprint defines permission boundaries (not data boundaries)
- BlueprintMember is dedicated collection for membership
- All resources belong to a Blueprint
- Access controlled via BlueprintMember role + permissions
Core Helper Functions
Global Authentication
rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { /** * Check if user is authenticated */ function isAuthenticated() { return request.auth != null; } /** * Get current user ID */ function getCurrentUserId() { return request.auth.uid; } /** * Check if user is system admin */ function isSystemAdmin() { return isAuthenticated() && get(/databases/$(database)/documents/users/$(getCurrentUserId())).data.role == 'admin'; } } }
Blueprint Membership Functions
/** * Check if user is Blueprint member */ function isBlueprintMember(blueprintId) { let memberId = getCurrentUserId() + '_' + blueprintId; return exists(/databases/$(database)/documents/blueprintMembers/$(memberId)); } /** * Get user's role in Blueprint */ function getBlueprintMemberRole(blueprintId) { let memberId = getCurrentUserId() + '_' + blueprintId; let member = get(/databases/$(database)/documents/blueprintMembers/$(memberId)); return member.data.role; } /** * Check if user is Owner or Admin */ function isBlueprintOwnerOrAdmin(blueprintId) { let role = getBlueprintMemberRole(blueprintId); return role in ['owner', 'admin']; } /** * Check if user has specific permission */ function hasPermission(blueprintId, permission) { let memberId = getCurrentUserId() + '_' + blueprintId; let member = get(/databases/$(database)/documents/blueprintMembers/$(memberId)); return permission in member.data.permissions; } /** * Check if member is active */ function isMemberActive(blueprintId) { let memberId = getCurrentUserId() + '_' + blueprintId; let member = get(/databases/$(database)/documents/blueprintMembers/$(memberId)); return member.data.status == 'active'; }
Data Validation Functions
/** * Validate task data structure */ function validateTaskData(data) { return data.keys().hasAll(['blueprintId', 'title', 'status']) && data.title is string && data.title.size() > 0 && data.title.size() <= 200 && data.status in ['pending', 'in-progress', 'completed', 'archived']; }
Collection Rules Pattern
Resource Collection (with Blueprint)
match /tasks/{taskId} { // Blueprint members can read tasks allow read: if isAuthenticated() && isBlueprintMember(resource.data.blueprintId) && isMemberActive(resource.data.blueprintId); // Members with task:create permission can create allow create: if isAuthenticated() && isBlueprintMember(request.resource.data.blueprintId) && isMemberActive(request.resource.data.blueprintId) && hasPermission(request.resource.data.blueprintId, 'task:create') && validateTaskData(request.resource.data); // Members with task:update permission or assignee can update allow update: if isAuthenticated() && isBlueprintMember(resource.data.blueprintId) && isMemberActive(resource.data.blueprintId) && (hasPermission(resource.data.blueprintId, 'task:update') || resource.data.assignedTo == getCurrentUserId()) && validateTaskData(request.resource.data); // Only members with task:delete permission can delete allow delete: if isAuthenticated() && isBlueprintMember(resource.data.blueprintId) && isMemberActive(resource.data.blueprintId) && hasPermission(resource.data.blueprintId, 'task:delete'); }
Nested Subcollections
match /blueprints/{blueprintId} { allow read: if isAuthenticated() && isBlueprintMember(blueprintId); // Nested tasks subcollection match /tasks/{taskId} { allow read: if isAuthenticated() && isBlueprintMember(blueprintId) && isMemberActive(blueprintId); allow create: if isAuthenticated() && isBlueprintMember(blueprintId) && isMemberActive(blueprintId) && hasPermission(blueprintId, 'task:create') && validateTaskData(request.resource.data) && request.resource.data.blueprintId == blueprintId; } }
Membership Collection
match /blueprintMembers/{memberId} { // Blueprint members can read member list allow read: if isAuthenticated() && isBlueprintMember(resource.data.blueprintId); // Only Owner/Admin can add/modify/remove members allow create, update: if isAuthenticated() && isBlueprintOwnerOrAdmin(resource.data.blueprintId); // Only Owner can delete members (soft delete recommended) allow delete: if isAuthenticated() && isBlueprintOwnerOrAdmin(resource.data.blueprintId); }
User Collection
match /users/{userId} { // Users can only read their own data allow read: if isAuthenticated() && getCurrentUserId() == userId; // Users can update their own data (but not role) allow update: if isAuthenticated() && getCurrentUserId() == userId && !request.resource.data.diff(resource.data).affectedKeys().hasAny(['role']); // Only system admin can create/delete users allow create, delete: if isSystemAdmin(); }
Audit Logs (Write-Only)
match /auditLogs/{logId} { // Only system admin can read logs allow read: if isSystemAdmin(); // Any authenticated user can write logs allow create: if isAuthenticated(); // Logs cannot be modified or deleted allow update, delete: if false; }
Performance Optimization
Minimize get() Calls
// ❌ BAD: Multiple get() calls function hasMultiplePermissions(blueprintId) { let memberId = getCurrentUserId() + '_' + blueprintId; let member = get(/databases/$(database)/documents/blueprintMembers/$(memberId)); let canCreate = 'task:create' in member.data.permissions; let member2 = get(/databases/$(database)/documents/blueprintMembers/$(memberId)); // Duplicate! let canUpdate = 'task:update' in member2.data.permissions; return canCreate && canUpdate; } // ✅ GOOD: Single get() call function hasMultiplePermissions(blueprintId) { let memberId = getCurrentUserId() + '_' + blueprintId; let member = get(/databases/$(database)/documents/blueprintMembers/$(memberId)); return 'task:create' in member.data.permissions && 'task:update' in member.data.permissions; }
Use exists() When Possible
// ✅ GOOD: exists() is faster than get() function isBlueprintMember(blueprintId) { let memberId = getCurrentUserId() + '_' + blueprintId; return exists(/databases/$(database)/documents/blueprintMembers/$(memberId)); } // Only use get() when you need data function getBlueprintMemberRole(blueprintId) { let memberId = getCurrentUserId() + '_' + blueprintId; let member = get(/databases/$(database)/documents/blueprintMembers/$(memberId)); return member.data.role; }
Testing Security Rules
Using Firebase Emulator
# Start emulator firebase emulators:start # Run security rules tests npm run test:rules
Test Template
describe('Task Collection Security Rules', () => { it('should allow authenticated blueprint member to read tasks', async () => { const db = testEnv.authenticatedContext('user1').firestore(); // Setup: Create member await testEnv.withSecurityRulesDisabled(async (context) => { await context.firestore().doc('blueprintMembers/user1_blueprint1').set({ blueprintId: 'blueprint1', userId: 'user1', role: 'member', status: 'active', permissions: ['task:read'] }); }); // Test: Read should succeed await assertSucceeds(db.doc('tasks/task1').get()); }); it('should deny unauthenticated access', async () => { const db = testEnv.unauthenticatedContext().firestore(); await assertFails(db.doc('tasks/task1').get()); }); });
Checklist
When writing Security Rules:
- All collections have security rules
- Multi-tenancy data isolation implemented
- BlueprintMember membership checked
- Permissions array validated
- Member status checked (active/suspended)
- Data validation functions complete
- Minimize get() calls for performance
- Use exists() when only checking existence
- Audit logs are write-only
- Cross-blueprint access prevented
- Tests cover all scenarios
- Both positive and negative tests
Integration with Architecture
Three-Layer Security
1. UI Layer: Permission checks for UX (can hide buttons) 2. Service Layer: Business logic validation 3. Security Rules: Server-side enforcement (MUST)
Repository + Security Rules
// Repository - No permission checks async deleteTask(taskId: string): Promise<void> { await this.taskRepository.delete(taskId); // Security Rules will validate permissions }
// Security Rules - Permission enforcement match /tasks/{taskId} { allow delete: if isAuthenticated() && isBlueprintMember(resource.data.blueprintId) && hasPermission(resource.data.blueprintId, 'task:delete'); }