/** * Core Philosophy: This ruleset enforces a strict user-ownership model. All data, * including documents and their generated summaries, is organized hierarchically * within a user's private data tree. There is no public or shared data access; * users can only access content they explicitly own. * * Data Structure: Data is structured hierarchically under a top-level `users` * collection: /users/{userId}/documents/{documentId}/summaries/{summaryId}. This * path-based ownership ensures that queries are naturally scoped to the * authenticated user's data. * * Key Security Decisions: * - Strict Ownership: All access is gated by checking if the authenticated * user's UID matches the {userId} in the path. * - No User Listing: Listing the top-level `/users` collection is disallowed to * protect user privacy and prevent enumeration attacks. * - Default Deny: Any operation not explicitly granted is denied. * - Authorization Independence: Documents and Summaries contain a denormalized * `userId` field. This allows rules to verify ownership directly from the * document being accessed, avoiding slow and costly `get()` calls to parent * documents and ensuring rules are performant and scalable. */ rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { // Helper Functions // -------------------------------- /** * Verifies that a user is signed into the application. */ function isSignedIn() { return request.auth != null; } /** * Verifies that the currently signed-in user's UID matches the given userId. * This is the primary function for enforcing user-ownership. */ function isOwner(userId) { return isSignedIn() && request.auth.uid == userId; } /** * Verifies ownership and ensures the document already exists. * CRITICAL: Use for all update and delete operations to prevent modifying * or deleting non-existent data. */ function isExistingOwner(userId) { return isOwner(userId) && resource != null; } /** * Validates required relational fields when creating a new User profile document. * Ensures the document's internal `id` matches the document's path ID (`userId`). */ function hasValidUserCreateData(userId) { let data = request.resource.data; return data.id == userId; } /** * Ensures critical relational fields are immutable on User profile updates. * The document's internal `id` cannot be changed after creation. */ function hasValidUserUpdateData() { let incomingData = request.resource.data; let existingData = resource.data; return incomingData.id == existingData.id; } /** * Validates required relational fields when creating a new Document. * Ensures the denormalized `userId` field correctly points to the owner. */ function hasValidDocumentCreateData(userId) { let data = request.resource.data; return data.userId == userId; } /** * Ensures critical relational fields are immutable on Document updates. * The `userId` linking the document to its owner cannot be changed. */ function hasValidDocumentUpdateData() { let incomingData = request.resource.data; let existingData = resource.data; return incomingData.userId == existingData.userId; } /** * Validates required relational fields when creating a new Summary. * Ensures denormalized `userId` and `documentId` are consistent with the path. */ function hasValidSummaryCreateData(userId, documentId) { let data = request.resource.data; return data.userId == userId && data.documentId == documentId; } /** * Ensures critical relational fields are immutable on Summary updates. * The `userId` and `documentId` cannot be changed after creation. */ function hasValidSummaryUpdateData() { let incomingData = request.resource.data; let existingData = resource.data; return incomingData.userId == existingData.userId && incomingData.documentId == existingData.documentId; } // Collection Rules // -------------------------------- /** * @description Manages user profile documents. Only the authenticated owner of the * profile can create, read, update, or delete their own data. * @path /users/{userId} * @allow (get) An authenticated user with UID 'user_abc' reads their own profile at /users/user_abc. * @deny (get) An authenticated user with UID 'user_xyz' tries to read /users/user_abc. * @principle Restricts access to a user's own data tree (Self-Creation & Ownership). */ match /users/{userId} { allow get: if isOwner(userId); allow list: if false; allow create: if isOwner(userId) && hasValidUserCreateData(userId); allow update: if isExistingOwner(userId) && hasValidUserUpdateData(); allow delete: if isExistingOwner(userId); } /** * @description Manages metadata for documents uploaded by a user. Access is * restricted to the owner of the parent user profile. * @path /users/{userId}/documents/{documentId} * @allow (create) An authenticated user 'user_abc' creates a new document at /users/user_abc/documents/doc_123. * @deny (list) An authenticated user 'user_xyz' tries to list documents at /users/user_abc/documents. * @principle Enforces document ownership for all operations and validates relational integrity on create/update. */ match /users/{userId}/documents/{documentId} { allow get: if isOwner(userId); allow list: if isOwner(userId); allow create: if isOwner(userId) && hasValidDocumentCreateData(userId); allow update: if isExistingOwner(userId) && hasValidDocumentUpdateData(); allow delete: if isExistingOwner(userId); } /** * @description Manages AI-generated summaries for a specific document. Access is * restricted to the owner of the parent user profile. * @path /users/{userId}/documents/{documentId}/summaries/{summaryId} * @allow (get) An authenticated user 'user_abc' reads a summary at /users/user_abc/documents/doc_123/summaries/sum_456. * @deny (update) An authenticated user 'user_xyz' tries to update a summary at /users/user_abc/documents/doc_123/summaries/sum_456. * @principle Enforces deep hierarchical ownership and validates relational integrity with both the user and the parent document. */ match /users/{userId}/documents/{documentId}/summaries/{summaryId} { allow get: if isOwner(userId); allow list: if isOwner(userId); allow create: if isOwner(userId) && hasValidSummaryCreateData(userId, documentId); allow update: if isExistingOwner(userId) && hasValidSummaryUpdateData(); allow delete: if isExistingOwner(userId); } } }