rules_version = '2'; service cloud.firestore { match /databases/{database}/documents { function isSignedIn() { return request.auth != null; } function isAdmin() { return request.auth.token.admin; } function getGroupRole(groupId) { return get(/databases/$(database)/documents/users/$(request.auth.uid)/groups/$(groupId)).data.role; } function isSetOwner(setId) { return get(/databases/$(database)/documents/sets/$(setId)).data.owner == request.auth.uid; } function isPublicSet(setId) { return get(/databases/$(database)/documents/sets/$(setId)).data.public; } function verifyCreateFields(fields) { return request.resource.data.keys().hasAll(fields[0]) && request.resource.data.keys().hasOnly(fields[1]); } function verifyUpdateFields(fields) { let affectedKeys = request.resource.data.diff(resource.data).affectedKeys(); return affectedKeys.hasAll([]) && affectedKeys.hasOnly(fields[1]); } function getRequestField(field, default_value) { return request.resource.data.get(field, default_value); } function verifyBoolType(field) { return getRequestField(field, true) is bool; } function verifyStringType(field) { return getRequestField(field, "") is string; } match /users/{userId} { function isSignedInUser() { return request.auth.uid == userId; } function verifyFieldTypes() { return verifyStringType("display_name") && verifyBoolType("sound") && verifyStringType("theme"); } function getPossibleFields() { let requiredFields = ["display_name", "sound", "theme"]; let optionalFields = []; let allFields = requiredFields.concat(optionalFields); return [requiredFields, allFields]; } allow read: if isSignedIn() && isSignedInUser(); // is current user's data allow update: if isSignedIn() && isSignedInUser() && verifyUpdateFields(getPossibleFields()) && verifyFieldTypes(); allow delete: if isSignedIn() && (isSignedInUser() || isAdmin()); match /groups/{groupId} { function verifyGroupFieldTypes() { return getRequestField("role", "") == "member" || getRequestField("role", "") == "contributor" || getRequestField("role", "") == "owner"; } function getPossibleGroupFields() { let requiredFields = ["role"]; let optionalFields = []; let allFields = requiredFields.concat(optionalFields); return [requiredFields, allFields]; } allow read, delete: if isSignedIn() && (isSignedInUser() || getGroupRole(groupId) == "owner" || isAdmin()); // is current user's data or is owner of group or is admin allow create: if isSignedIn() && ((isSignedInUser() && getRequestField("role", "") == "member") || ((getGroupRole(groupId) == "owner" || isAdmin()) && verifyGroupFieldTypes())) && verifyCreateFields(getPossibleGroupFields()); allow update: if isSignedIn() && ((isSignedInUser() && getRequestField("role", "") == "member") || ((getGroupRole(groupId) == "owner" || isAdmin()) && verifyGroupFieldTypes())) && verifyUpdateFields(getPossibleGroupFields()); } } match /groups/{groupId} { function verifyFieldTypes() { return verifyStringType("display_name"); } function getPossibleFields() { let requiredFields = ["display_name"]; let optionalFields = []; let allFields = requiredFields.concat(optionalFields); return [requiredFields, allFields]; } allow read: if isSignedIn() && (getGroupRole(groupId) != null || isAdmin()); allow create: if isSignedIn() && getGroupRole(groupId) == "owner" && verifyCreateFields(getPossibleFields()) && verifyFieldTypes(); allow update: if isSignedIn() && (getGroupRole(groupId) == "owner" || isAdmin()) && verifyUpdateFields(getPossibleFields()) && verifyFieldTypes(); allow delete: if isSignedIn() && (getGroupRole(groupId) == "owner" || isAdmin()); match /sets/{setId} { function isSetVisibleToUser(setId) { return (isPublicSet(setId) || isSetOwner(setId)); } function verifySetFieldTypes() { return getRequestField("exists", true); } function getPossibleSetFields() { let requiredFields = ["exists"]; let optionalFields = []; let allFields = requiredFields.concat(optionalFields); return [requiredFields, allFields]; } allow read: if isSignedIn() && ((getGroupRole(groupId) != null) || isAdmin()); allow create: if isSignedIn() && isSetVisibleToUser(setId) && (getGroupRole(groupId) == "contributor" || getGroupRole(groupId) == "owner" || isAdmin()) && verifyCreateFields(getPossibleSetFields()) && verifySetFieldTypes(); allow delete: if isSignedIn() && (getGroupRole(groupId) == "owner" || isAdmin()); } match /static/data { function verifyStaticFieldTypes() { return verifyStringType("join_code"); } function getPossibleStaticFields() { let requiredFields = ["join_code"]; let optionalFields = []; let allFields = requiredFields.concat(optionalFields); return [requiredFields, allFields]; } allow read, delete: if isSignedIn() && (getGroupRole(groupId) == "owner" || isAdmin()); allow create: if isSignedIn() && (getGroupRole(groupId) == "owner" || isAdmin()) && verifyCreateFields(getPossibleStaticFields()) && verifyStaticFieldTypes(); } } match /sets/{setId} { function verifyFieldTypes() { return verifyBoolType("public") && verifyStringType("title") && verifyStringType("owner"); } function getPossibleFields() { let nonStaticFields = ["public", "title"]; let staticFields = ["owner"]; let requiredFields = staticFields.concat(nonStaticFields); let optionalFields = []; let allFields = requiredFields.concat(optionalFields); return [requiredFields, allFields, nonStaticFields]; } function getPossibleCreateFields() { let fields = getPossibleFields(); return [fields[0], fields[1]]; } function getPossibleUpdateFields() { let fields = getPossibleFields(); return [[], fields[2]]; } allow read, delete: if isSignedIn() && request.auth.uid == resource.data.owner; allow read: if isSignedIn() && resource.data.public; allow create: if isSignedIn() && request.auth.uid == request.resource.data.owner && verifyCreateFields(getPossibleCreateFields()) && verifyFieldTypes(); allow update: if isSignedIn() && request.auth.uid == resource.data.owner && verifyUpdateFields(getPossibleUpdateFields()) && verifyFieldTypes(); match /vocab/{vocabId} { function verifyVocabFieldTypes() { return verifyStringType("term") && verifyStringType("sound") && verifyStringType("definition"); } function getPossibleVocabFields() { let requiredFields = ["term", "sound", "definition"]; let optionalFields = []; let allFields = requiredFields.concat(optionalFields); return [requiredFields, allFields]; } allow read, delete: if isSignedIn() && isSetOwner(setId); allow read: if isSignedIn() && isPublicSet(setId); allow create: if isSignedIn() && isSetOwner(setId) && verifyCreateFields(getPossibleVocabFields()) && verifyVocabFieldTypes(); allow update: if isSignedIn() && isSetOwner(setId) && verifyUpdateFields(getPossibleVocabFields()) && verifyVocabFieldTypes(); } } match /progress/{progressId} { function isProgressUser() { return get(/databases/$(database)/documents/progress/$(progressId)).data.uid == request.auth.uid; } function isLanguageSwitched() { return get(/databases/$(database)/documents/progress/$(progressId)).data.switch_language; } function isNotComplete() { return get(/databases/$(database)/documents/progress/$(progressId)).data.progress < get(/databases/$(database)/documents/progress/$(progressId)).data.questions.size(); } allow read: if isSignedIn() && isProgressUser(); allow delete: if isSignedIn() && isProgressUser() && isNotComplete(); // TODO: update and create disallowed as these are handled by Cloud Functions to ensure sound file Ids aren't altered to illegally access other files // TODO: disallow start_time update match /terms/{vocabId} { allow read: if isSignedIn() && isProgressUser() && !(isLanguageSwitched()); // TODO: create handled by Cloud Functions allow delete: if isSignedIn() && isProgressUser() && isNotComplete(); } match /definitions/{vocabId} { allow read: if isSignedIn() && isProgressUser() && isLanguageSwitched(); // TODO: create handled by Cloud Functions allow delete: if isSignedIn() && isProgressUser() && isNotComplete(); } } } }