| /** | |
| * 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); | |
| } | |
| } | |
| } |