rules_version = '2'; service cloud.firestore { match /databases/{databaseId}/documents { function valueExists(object, property) { return (property in object) && object[property] != null; } function isEqualOptional(object0, property0, object1, property1) { return valueExists(object0, property0) ? (valueExists(object1, property1) && object0[property0] == object1[property1]) : !valueExists(object1, property1); } function isAuthenticated() { return request.auth != null && ('type' in request.auth.token) && ( !valueExists(request.auth.token, 'disabled') || request.auth.token.disabled == false ); } function isAdmin() { return isAuthenticated() && request.auth.token.type == 'admin'; } function isPartOf(organizationId) { return ('organization' in request.auth.token) && request.auth.token.organization == organizationId; } function isOwnerOf(organizationId) { return organizationId != null && isAuthenticated() && request.auth.token.type == 'owner' && isPartOf(organizationId); } function isOwnerOrClinicianOf(organizationId) { return organizationId != null && isAuthenticated() && request.auth.token.type in ['owner', 'clinician'] && isPartOf(organizationId); } function isUser(userId) { return isAuthenticated() && request.auth.uid == userId; } function getUser(userId) { return get(/databases/$(databaseId)/documents/users/$(userId)); } function getInvitation(invitationId) { return get(/databases/$(databaseId)/documents/invitations/$(invitationId)); } match /invitations/{invitationId} { function securityRelatedFieldsDidNotChange() { return isEqualOptional(request.resource.data.user, 'type', resource.data.user, 'type') && isEqualOptional(request.resource.data.user, 'organization', resource.data.user, 'organization') } allow read, delete: if isAdmin() || isOwnerOrClinicianOf(resource.data.user.organization); allow create: if isAdmin(); allow update: if isAdmin() || (securityRelatedFieldsDidNotChange() && isOwnerOrClinicianOf(resource.data.user.organization)); } match /invitations/{invitationId}/{collectionName}/{documentId} { function isOwnerOrClinicianOfSameOrganization() { let invitation = getInvitation(invitationId); return invitation != null && isOwnerOrClinicianOf(invitation.data.user.organization); } function isPatientWritableCollectionName() { return collectionName.matches('^[A-Za-z]+Observations$') || collectionName in ['questionnaireResponses'] } function isClinicianWritableCollectionName() { return isPatientWritableCollectionName() || collectionName in ['allergyIntolerances', 'appointments', 'medicationRequests']; } allow read: if isAdmin() || isOwnerOrClinicianOfSameOrganization(); allow write: if isAdmin() || (isOwnerOrClinicianOfSameOrganization() && isClinicianWritableCollectionName()); } match /medicationClasses/{medicationClassId} { allow read: if isAuthenticated(); allow write: if isAdmin(); } match /medications/{documents=**} { allow read: if isAuthenticated(); allow write: if isAdmin(); } match /organizations/{organizationId} { allow read: if isAdmin() || isPartOf(organizationId); allow update: if isAdmin() || isOwnerOf(organizationId); allow create, delete: if isAdmin(); } match /questionnaires/{questionnaireId} { allow read: if isAuthenticated(); allow write: if isAdmin(); } match /users/{userId} { function securityRelatedFieldsDidNotChange() { return isEqualOptional(request.resource.data, 'type', resource.data, 'type') && isEqualOptional(request.resource.data, 'disabled', resource.data, 'disabled') && isEqualOptional(request.resource.data, 'organization', resource.data, 'organization'); } function isAllowedUpdateWithinOrganization() { return valueExists(resource.data, 'type') ? ( resource.data.type in ['patient'] ? isOwnerOf(resource.data.organization) : isOwnerOrClinicianOf(resource.data.organization) && resource.data.type in ['clinician'] ) : false; } allow read: if isAdmin() || (request.auth != null && request.auth.uid == userId) || (resource == null && isAuthenticated()) || (resource != null && valueExists(resource.data, 'organization') && isOwnerOrClinicianOf(resource.data.organization)); allow create: if isAdmin() || (isUser(userId) && !valueExists(request.resource.data, 'organization') && !valueExists(request.resource.data, 'type')); allow update: if isAdmin() || (securityRelatedFieldsDidNotChange() && (isUser(userId) || isAllowedUpdateWithinOrganization())); allow delete: if isAdmin(); } match /users/{userId}/{collectionName}/{documentId} { function isOwnerOrClinicianOfSameOrganization() { let userDoc = getUser(userId); return (userDoc.data != null) && ('organization' in userDoc.data) && isOwnerOrClinicianOf(userDoc.data.organization); } function isPatientWritableCollectionName() { return collectionName.matches('^[A-Za-z]+Observations$') || collectionName in ['questionnaireResponses'] } function isClinicianWritableCollectionName() { return isPatientWritableCollectionName() || collectionName in ['allergyIntolerances', 'appointments', 'medicationRequests']; } allow read: if isAdmin() || (request.auth != null && request.auth.uid == userId) || isOwnerOrClinicianOfSameOrganization(); allow write: if isAdmin() || (isUser(userId) && isPatientWritableCollectionName()) || (isOwnerOrClinicianOfSameOrganization() && isClinicianWritableCollectionName()); } match /videoSections/{documents=**} { allow read: if isAuthenticated(); allow write: if isAdmin(); } } }