rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { // Helper functions function inOrg(orgId) { return request.auth != null && request.auth.token.orgId == orgId; } function canWriteOrg(orgId) { return inOrg(orgId) && (request.auth.token.role in ['superadmin', 'orgAdmin']); } function territoryOK(resOrgId, resTerritoryId) { return inOrg(resOrgId) && ( request.auth.token.role in ['superadmin', 'orgAdmin'] || (request.auth.token.role == 'territoryManager' && (resTerritoryId in request.auth.token.territoryIds)) || request.auth.token.role == 'staff' // staff sees entire org; can narrow later ); } function canReadTerritory(resOrgId, resTerritoryId) { return inOrg(resOrgId) && ( request.auth.token.role in ['superadmin', 'orgAdmin'] || (request.auth.token.role == 'territoryManager' && (resTerritoryId in request.auth.token.territoryIds)) || request.auth.token.role == 'staff' ); } // Organizations collection match /orgs/{orgId} { allow read, write: if inOrg(orgId); allow create: if request.auth != null; } // Users collection for organization membership tracking match /users/{userId} { // Users can read their own document, or admins can read within their org allow read: if (request.auth != null && request.auth.uid == userId) || (request.auth != null && inOrg(resource.data.orgId)); // Only orgAdmins and superadmins can write user documents allow write: if request.auth != null && canWriteOrg(request.resource.data.orgId); } // Territories collection match /territories/{territoryId} { allow read: if inOrg(resource.data.orgId); allow write: if canWriteOrg(request.resource.data.orgId); } // Events collection with territory scoping match /events/{eventId} { allow read: if canReadTerritory(resource.data.orgId, resource.data.territoryId); allow write: if territoryOK(request.resource.data.orgId, request.resource.data.territoryId); } // Ticket types collection with territory inheritance match /ticket_types/{ticketTypeId} { allow read: if inOrg(resource.data.orgId); allow write: if territoryOK(request.resource.data.orgId, request.resource.data.territoryId); } // Tickets collection with territory inheritance match /tickets/{ticketId} { // Scanning/reporting needs org-wide reads; can narrow if required allow read: if inOrg(resource.data.orgId); allow write: if territoryOK(request.resource.data.orgId, request.resource.data.territoryId); } // Scans collection - append-only audit trail for ticket scanning match /scans/{scanId} { // Staff and above can read scans within their org for reporting/analytics allow read: if inOrg(resource.data.orgId) && request.auth.token.role in ['staff', 'territoryManager', 'orgAdmin', 'superadmin']; // Only create operations allowed - append-only pattern // Staff and above can create scan records within their org allow create: if inOrg(request.resource.data.orgId) && request.auth.token.role in ['staff', 'territoryManager', 'orgAdmin', 'superadmin']; // Explicitly deny updates and deletes to enforce append-only pattern allow update, delete: if false; } // Legacy support for old organization membership model function isOrgMember(orgId) { return request.auth != null && exists(/databases/$(database)/documents/users/$(request.auth.uid)) && get(/databases/$(database)/documents/users/$(request.auth.uid)).data.orgs[orgId] != null; } } }