From 7687441fbe47b9e94b8237b6468036625270642c Mon Sep 17 00:00:00 2001 From: Hitesh Kumar Date: Sat, 9 May 2026 15:52:25 +0530 Subject: [PATCH] feat(rules): additive read-only guard for staging clone account MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a server-side enforcement that the staging clone's dedicated auth user cannot mutate prod data, even if all client-side guards in the staging app fail. The change is strictly additive: - One new helper: isStagingReadOnly() returns true iff the request is authenticated as staging-readonly@aadhat.local. - Every existing write/create/update/delete rule now ANDs in !isStagingReadOnly(). Reads are untouched. - The combined llow read, write: if isSignedIn(); rules are split into separate llow read (unchanged) and llow write (now also gated on !isStagingReadOnly()) so reads still work for the staging user. Worked example: - Before: allow read, write: if isSignedIn(); - After: allow read: if isSignedIn(); allow write: if isSignedIn() && !isStagingReadOnly(); Coverage: - 37 write rules now carry the !isStagingReadOnly() guard. - The read-only audit-log rule (llow update, delete: if false) is left unchanged - it's already strictly stricter. - The notifications collection's ead, delete: if isSignedIn() && isAdmin() is split similarly: reads stay open, deletes pick up the staging guard. Worst case if the regex/email is wrong: the staging user can write, but Layers 1 (read-only-guard.js) and 2 (service-worker fetch block) in the staging clone still block them at the client. PRE-DEPLOY VALIDATION (REQUIRED in Rules Playground): 1. uid=, path=purchases/{auto}, op=create ΓåÆ ALLOW 2. uid=, path=items/{auto}, op=create ΓåÆ ALLOW 3. uid=, path=items/{auto}, op=create ΓåÆ DENY 4. uid=, path=items/{any}, op=get ΓåÆ ALLOW 5. uid=, path=users/, op=update ΓåÆ ALLOW 6. uid=, path=users/, op=update ΓåÆ DENY 7. uid=, path=customers/{auto}, op=create ΓåÆ DENY 8. uid=, path=auditLogs/{any}, op=update ΓåÆ DENY (rule = false) DEPLOY: firebase deploy --only firestore:rules --project aadhat-management ROLLBACK: Revert this single commit and redeploy. Prod returns to exactly the pre-patch behaviour (no other rule changes). Reference: docs/STAGING_RULES_PATCH.md in the staging clone. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- firestore.rules | 115 +++++++++++++++++++++++++++++++++--------------- 1 file changed, 79 insertions(+), 36 deletions(-) diff --git a/firestore.rules b/firestore.rules index f55c24c..5af6c4c 100644 --- a/firestore.rules +++ b/firestore.rules @@ -5,6 +5,17 @@ service cloud.firestore { function isSignedIn() { return request.auth != null; } + // Staging clone uses one dedicated, throw-away Auth account + // (`staging-readonly@aadhat.local`). Every write rule below + // ANDs in `!isStagingReadOnly()` so that account is server-side + // read-only even if all client-side guards in the staging app fail. + // NOTE: this rule is server-side ONLY — production users are + // unaffected. Do not delete this helper unless the staging clone + // and its dedicated account have been retired. + function isStagingReadOnly() { + return request.auth != null + && request.auth.token.email == 'staging-readonly@aadhat.local'; + } // Helper function to check user role from their user document function getUserRole() { @@ -23,52 +34,62 @@ service cloud.firestore { // Purchases collection (renamed from bills) - all authenticated users can read/write match /purchases/{purchaseId} { - allow read, write: if isSignedIn(); + allow read: if isSignedIn(); + allow write: if isSignedIn() && !isStagingReadOnly(); } // Retail Sales collection - all authenticated users can read/write match /retailSales/{saleId} { - allow read, write: if isSignedIn(); + allow read: if isSignedIn(); + allow write: if isSignedIn() && !isStagingReadOnly(); } // Wholesale Sales collection - all authenticated users can read/write match /wholesaleSales/{saleId} { - allow read, write: if isSignedIn(); + allow read: if isSignedIn(); + allow write: if isSignedIn() && !isStagingReadOnly(); } // Items collection - all authenticated users can read/write match /items/{itemId} { - allow read, write: if isSignedIn(); + allow read: if isSignedIn(); + allow write: if isSignedIn() && !isStagingReadOnly(); } // Expenses collection - all authenticated users can read/write match /expenses/{expenseId} { - allow read, write: if isSignedIn(); + allow read: if isSignedIn(); + allow write: if isSignedIn() && !isStagingReadOnly(); } // Stock adjustments - all authenticated users can read/write match /stockAdjustments/{adjustmentId} { - allow read, write: if isSignedIn(); + allow read: if isSignedIn(); + allow write: if isSignedIn() && !isStagingReadOnly(); } // Withdrawals collection - all authenticated users can read/write match /withdrawals/{withdrawalId} { - allow read, write: if isSignedIn(); + allow read: if isSignedIn(); + allow write: if isSignedIn() && !isStagingReadOnly(); } // Cash management - all authenticated users can read/write match /cashManagement/{sessionId} { - allow read, write: if isSignedIn(); + allow read: if isSignedIn(); + allow write: if isSignedIn() && !isStagingReadOnly(); } // Cash sessions - all authenticated users can read/write match /cashSessions/{sessionId} { - allow read, write: if isSignedIn(); + allow read: if isSignedIn(); + allow write: if isSignedIn() && !isStagingReadOnly(); } // Settings collection - all authenticated users can read/write match /settings/{settingId} { - allow read, write: if isSignedIn(); + allow read: if isSignedIn(); + allow write: if isSignedIn() && !isStagingReadOnly(); } // ============================================ @@ -78,41 +99,46 @@ service cloud.firestore { // Users collection - all can read (for user lists), but only admins can modify others match /users/{userId} { allow read: if isSignedIn(); - allow create: if isSignedIn() && request.auth.uid == userId; - allow update, delete: if isSignedIn() && (request.auth.uid == userId || isAdmin()); + allow create: if isSignedIn() && request.auth.uid == userId && !isStagingReadOnly(); + allow update, delete: if isSignedIn() && (request.auth.uid == userId || isAdmin()) && !isStagingReadOnly(); } // Item frequency collection - all authenticated users can read/write (shared data) match /itemFrequency/{docId} { - allow read, write: if isSignedIn(); + allow read: if isSignedIn(); + allow write: if isSignedIn() && !isStagingReadOnly(); } // Notifications collection - all authenticated users can read/write match /notifications/{notificationId} { - allow read, write: if isSignedIn(); + allow read: if isSignedIn(); + allow write: if isSignedIn() && !isStagingReadOnly(); } // Auto-save drafts - users can only access their own match /autoSaves/{docId} { - allow read, write: if isSignedIn(); + allow read: if isSignedIn(); + allow write: if isSignedIn() && !isStagingReadOnly(); } // Bill drafts - users can only access their own match /drafts/{docId} { - allow read, write: if isSignedIn(); + allow read: if isSignedIn(); + allow write: if isSignedIn() && !isStagingReadOnly(); } // Audit logs - all can write (for logging), only owners can read match /auditLogs/{logId} { - allow create: if isSignedIn(); + allow create: if isSignedIn() && !isStagingReadOnly(); allow read: if isSignedIn() && isAdmin(); allow update, delete: if false; // Never allow modification or deletion } // Telemetry - all can write (for error logging), only owners can read/delete match /telemetry/{docId} { - allow create, update: if isSignedIn(); - allow read, delete: if isSignedIn() && isAdmin(); + allow create, update: if isSignedIn() && !isStagingReadOnly(); + allow read: if isSignedIn() && isAdmin(); + allow delete: if isSignedIn() && isAdmin() && !isStagingReadOnly(); } // ============================================ @@ -121,55 +147,72 @@ service cloud.firestore { // ============================================ match /dev_purchases/{docId} { - allow read, write: if isSignedIn(); + allow read: if isSignedIn(); + allow write: if isSignedIn() && !isStagingReadOnly(); } match /dev_retailSales/{docId} { - allow read, write: if isSignedIn(); + allow read: if isSignedIn(); + allow write: if isSignedIn() && !isStagingReadOnly(); } match /dev_wholesaleSales/{docId} { - allow read, write: if isSignedIn(); + allow read: if isSignedIn(); + allow write: if isSignedIn() && !isStagingReadOnly(); } match /dev_items/{docId} { - allow read, write: if isSignedIn(); + allow read: if isSignedIn(); + allow write: if isSignedIn() && !isStagingReadOnly(); } match /dev_expenses/{docId} { - allow read, write: if isSignedIn(); + allow read: if isSignedIn(); + allow write: if isSignedIn() && !isStagingReadOnly(); } match /dev_stockAdjustments/{docId} { - allow read, write: if isSignedIn(); + allow read: if isSignedIn(); + allow write: if isSignedIn() && !isStagingReadOnly(); } match /dev_withdrawals/{docId} { - allow read, write: if isSignedIn(); + allow read: if isSignedIn(); + allow write: if isSignedIn() && !isStagingReadOnly(); } match /dev_cashManagement/{docId} { - allow read, write: if isSignedIn(); + allow read: if isSignedIn(); + allow write: if isSignedIn() && !isStagingReadOnly(); } match /dev_cashSessions/{docId} { - allow read, write: if isSignedIn(); + allow read: if isSignedIn(); + allow write: if isSignedIn() && !isStagingReadOnly(); } match /dev_users/{docId} { - allow read, write: if isSignedIn(); + allow read: if isSignedIn(); + allow write: if isSignedIn() && !isStagingReadOnly(); } match /dev_itemFrequency/{docId} { - allow read, write: if isSignedIn(); + allow read: if isSignedIn(); + allow write: if isSignedIn() && !isStagingReadOnly(); } match /dev_notifications/{docId} { - allow read, write: if isSignedIn(); + allow read: if isSignedIn(); + allow write: if isSignedIn() && !isStagingReadOnly(); } match /dev_autoSaves/{docId} { - allow read, write: if isSignedIn(); + allow read: if isSignedIn(); + allow write: if isSignedIn() && !isStagingReadOnly(); } match /dev_drafts/{docId} { - allow read, write: if isSignedIn(); + allow read: if isSignedIn(); + allow write: if isSignedIn() && !isStagingReadOnly(); } match /dev_auditLogs/{docId} { - allow read, write: if isSignedIn(); + allow read: if isSignedIn(); + allow write: if isSignedIn() && !isStagingReadOnly(); } match /dev_settings/{docId} { - allow read, write: if isSignedIn(); + allow read: if isSignedIn(); + allow write: if isSignedIn() && !isStagingReadOnly(); } match /dev_telemetry/{docId} { - allow read, write: if isSignedIn(); + allow read: if isSignedIn(); + allow write: if isSignedIn() && !isStagingReadOnly(); } } }