Add Firestore rules
This commit is contained in:
372
firestore.rules
372
firestore.rules
@@ -2,188 +2,228 @@ rules_version = '2';
|
||||
|
||||
service cloud.firestore {
|
||||
match /databases/{database}/documents {
|
||||
|
||||
match /users/{userID} {
|
||||
allow read, delete: if isSignedIn() && (isSignedInUser(userID) || getUserData(request.auth.uid).admin); // is current user's data or is admin
|
||||
allow update: if isSignedIn() && getUserData(request.auth.uid).admin && verifyFields([],["admin"]) && verifyUsersFieldTypes();
|
||||
// NOTE: create disallowed to ensure users can't define themselves as admins. Handled in Cloud Functions instead.
|
||||
|
||||
match /private/settings {
|
||||
allow read: if isSignedIn() && isSignedInUser(userID); // is current user's data
|
||||
allow delete: if isSignedIn() && (isSignedInUser(userID) || getUserData(request.auth.uid).admin);
|
||||
allow update: if isSignedIn() && isSignedInUser(userID) && verifyFields([],["display_name", "sound", "theme"]) && verifyUsersPrivateSettingsFieldTypes();
|
||||
allow create: if isSignedIn() && isSignedInUser(userID) && verifyFields(["display_name", "sound", "theme"],[]) && verifyUsersPrivateSettingsFieldTypes();
|
||||
}
|
||||
match /groups/{groupID} {
|
||||
allow read, delete: if isSignedIn() && (isSignedInUser(userID) || getUserGroupData(request.auth.uid, groupID).role == "owner" || getUserData(request.auth.uid).admin); // is current user's data or is owner of group or is admin
|
||||
allow create: if isSignedIn() && (isSignedInUser(userID) || getUserGroupData(request.auth.uid, groupID).role == "owner" || getUserData(request.auth.uid).admin) && verifyFields(["role"],[]) && verifyUsersGroupsFieldTypes();
|
||||
allow update: if isSignedIn() && (isSignedInUser(userID) || getUserGroupData(request.auth.uid, groupID).role == "owner" || getUserData(request.auth.uid).admin) && verifyFields([],["role"]) && verifyUsersGroupsFieldTypes();
|
||||
}
|
||||
}
|
||||
|
||||
match /groups/{groupID} {
|
||||
match /sets/{setID} {
|
||||
allow delete: if isSignedIn() && (getUserGroupData(groupID).role == "owner" || getUserData(request.auth.uid).admin);
|
||||
allow read: if isSignedIn() && (getUserGroupData(groupID).role || getUserData(request.auth.uid).admin);
|
||||
allow create: if isSignedIn() && isSetIDValid(setID) && (getUserGroupData(groupID).role == "contributor" || getUserGroupData(groupID).role == "owner" || getUserData(request.auth.uid).admin) && verifyFields(["sets"],[]) && verifyGroupsFieldTypes();
|
||||
}
|
||||
|
||||
match /static/data {
|
||||
allow read, delete: if isSignedIn() && (getUserGroupData(groupID).role == "owner" || getUserData(request.auth.uid).admin);
|
||||
allow create: if isSignedIn() && (getUserGroupData(groupID).role == "owner" || getUserData(request.auth.uid).admin) && verifyFields(["join_code"],[]) && verifyGroupsStaticDataFieldTypes();
|
||||
}
|
||||
|
||||
match /restricted/data {
|
||||
allow delete: if isSignedIn() && (getUserGroupData(groupID).role == "owner" || getUserData(request.auth.uid).admin);
|
||||
allow read: if isSignedIn() && (getUserGroupData(groupID).role || getUserData(request.auth.uid).admin);
|
||||
allow create: if isSignedIn() && (getUserGroupData(groupID).role == "owner" || getUserData(request.auth.uid).admin) && verifyFields(["display_name"],[]) && verifyGroupsFieldTypes();
|
||||
allow update: if isSignedIn() && (getUserGroupData(groupID).role == "owner" || getUserData(request.auth.uid).admin) && verifyFields([],["display_name"]) && verifyGroupsFieldTypes();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
match /sets/{setID} {
|
||||
allow read, delete: if isSignedIn() && request.auth.uid == getSetOwner(setID);
|
||||
allow read: if isSignedIn() && resource.data.public;
|
||||
allow create: if isSignedIn() && request.auth.uid == getSetOwner(setID) && verifyFields(["public", "title"],[]) && verifySetsFieldTypes();
|
||||
allow update: if isSignedIn() && request.auth.uid == getSetOwner(setID) && verifyFields([],["public", "title"]) && verifySetsFieldTypes();
|
||||
|
||||
match /static/data {
|
||||
allow read, delete: if isSignedIn() && request.auth.uid == resource.data.owner;
|
||||
allow read: if isSignedIn() && getSetData(setID).public;
|
||||
allow create: if isSignedIn() && request.auth.uid == request.resource.data.owner && verifyFields(["owner"],[]) && verifySetsStaticDataFieldTypes();
|
||||
}
|
||||
|
||||
match /vocab/{vocabID} {
|
||||
allow read, delete: if isSignedIn() && request.auth.uid == getSetOwner(setID);
|
||||
allow read: if isSignedIn() && (getSetData(setID).public || request.auth.uid == getSetOwner(setID));
|
||||
allow create: if isSignedIn() && request.auth.uid == getSetOwner(setID) && verifyFields(["term", "sound", "definition"],[]) && verifySetsVocabFieldTypes();
|
||||
allow update: if isSignedIn() && request.auth.uid == getSetOwner(setID) && verifyFields([],["term", "sound", "definition"]) && verifySetsVocabFieldTypes();
|
||||
}
|
||||
}
|
||||
|
||||
match /progress/{progressID} {
|
||||
allow read: if isSignedIn() && checkProgressUser(progressID);
|
||||
allow delete: if isSignedIn() && checkProgressUser(progressID) && checkNotComplete(progressID);
|
||||
// NOTE: update and create disallowed as these are handled by Cloud Functions to ensure sound file IDs aren't altered to illegally access other files
|
||||
|
||||
match /static/data {
|
||||
allow read: if isSignedIn() && checkProgressUser(progressID);
|
||||
// NOTE: create handled by Cloud Functions
|
||||
allow delete: if isSignedIn() && checkProgressUser(progressID) && checkNotComplete(progressID);
|
||||
}
|
||||
|
||||
match /terms/{vocabID} {
|
||||
allow read: if isSignedIn() && checkProgressUser(progressID) && !(isLanguageSwitched(progressID));
|
||||
// NOTE: create handled by Cloud Functions
|
||||
allow delete: if isSignedIn() && checkProgressUser(progressID) && checkNotComplete(progressID);
|
||||
}
|
||||
|
||||
match /definitions/{vocabID} {
|
||||
allow read: if isSignedIn() && checkProgressUser(progressID) && isLanguageSwitched(progressID);
|
||||
// NOTE: create handled by Cloud Functions
|
||||
allow delete: if isSignedIn() && checkProgressUser(progressID) && checkNotComplete(progressID);
|
||||
}
|
||||
}
|
||||
|
||||
function isSignedIn() {
|
||||
return request.auth != null;
|
||||
}
|
||||
function isSignedInUser(userID) {
|
||||
return request.auth.uid == userID;
|
||||
}
|
||||
function getUserData(userID) {
|
||||
return get(/databases/$(database)/documents/users/$(userID)).data;
|
||||
}
|
||||
function getUserGroupData(userID, groupID) {
|
||||
return get(/databases/$(database)/documents/users/$(userID)/groups/$(groupID)).data;
|
||||
}
|
||||
function checkProgressUser(progressID) {
|
||||
return get(/databases/$(database)/documents/progress/$(progressID)/static/data).data.uid == request.auth.uid;
|
||||
}
|
||||
function checkNotComplete(progressID) {
|
||||
return get(/databases/$(database)/documents/progress/$(progressID)).data.progress < get(/databases/$(database)/documents/progress/$(progressID)).data.questions.length;
|
||||
}
|
||||
function isLanguageSwitched(progressID) {
|
||||
return get(/databases/$(database)/documents/progress/$(progressID)/static/data).data;
|
||||
}
|
||||
function getSetData(setID) {
|
||||
return get(/databases/$(database)/documents/sets/$(setID)).data;
|
||||
}
|
||||
function getSetOwner(setID) {
|
||||
return get(/databases/$(database)/documents/sets/$(setID)/static/data).data.owner;
|
||||
}
|
||||
function verifyFields(required, optional) {
|
||||
return request.resource.data.keys().hasAll(required) && request.resource.data.keys().hasOnly(required.concat(optional));
|
||||
}
|
||||
function isSetIDValid(setID) {
|
||||
return getSetData(setID).public || getSetOwner(setID) == request.auth.uid;
|
||||
|
||||
function isAdmin() {
|
||||
return request.auth.token.admin;
|
||||
}
|
||||
|
||||
function verifyUsersFieldTypes() {
|
||||
let data = request.resource.data;
|
||||
return data.get("admin",true) is bool;
|
||||
function getGroupRole(groupId) {
|
||||
return get(/databases/$(database)/documents/users/$(request.auth.uid)/groups/$(groupId)).data.role;
|
||||
}
|
||||
function verifyUsersPrivateSettingsFieldTypes() {
|
||||
let data = request.resource.data;
|
||||
return data.get("display_name",true) is bool &&
|
||||
data.get("sound",true) is bool &&
|
||||
data.get("theme","") is string;
|
||||
|
||||
function isSetOwner(setId) {
|
||||
return get(/databases/$(database)/documents/sets/$(setId)).data.owner == request.auth.uid;
|
||||
}
|
||||
function verifyUsersGroupsFieldTypes() {
|
||||
let data = request.resource.data;
|
||||
return data.get("role","") is string &&
|
||||
(data.get("role","") == "member" ||
|
||||
data.get("role","") == "contributor" ||
|
||||
data.get("role","") == "owner");
|
||||
|
||||
function isPublicSet(setId) {
|
||||
return get(/databases/$(database)/documents/sets/$(setId)).data.public;
|
||||
}
|
||||
function verifyGroupsFieldTypes() {
|
||||
let data = request.resource.data;
|
||||
return data.get("display_name","") is string &&
|
||||
data.get("sets",[]) is list;
|
||||
|
||||
function verifyCreateFields(fields) {
|
||||
return request.resource.data.keys().hasAll(fields[0]) && request.resource.data.keys().hasOnly(fields[1]);
|
||||
}
|
||||
function verifyGroupsStaticDataFieldTypes() {
|
||||
let data = request.resource.data;
|
||||
return data.get("join_code","") is string;
|
||||
|
||||
function verifyUpdateFields(fields) {
|
||||
let affectedKeys = request.resource.data.diff(resource.data).affectedKeys();
|
||||
return affectedKeys.hasAll([]) && affectedKeys.hasOnly(fields[1]);
|
||||
}
|
||||
function verifySetsFieldTypes() {
|
||||
let data = request.resource.data;
|
||||
return data.get("public",true) is bool &&
|
||||
data.get("title","") is string;
|
||||
|
||||
function getRequestField(field, default_value) {
|
||||
return request.resource.data.get(field, default_value);
|
||||
}
|
||||
function verifySetsStaticDataFieldTypes() {
|
||||
let data = request.resource.data;
|
||||
return data.get("owner","") is string;
|
||||
|
||||
function verifyBoolType(field) {
|
||||
return getRequestField(field, true) is bool;
|
||||
}
|
||||
function verifySetsVocabFieldTypes() {
|
||||
let data = request.resource.data;
|
||||
return data.get("term","") is string &&
|
||||
data.get("sound","") is string &&
|
||||
data.get("definition","") is string;
|
||||
|
||||
function verifyStringType(field) {
|
||||
return getRequestField(field, "") is string;
|
||||
}
|
||||
function verifyProgressFieldTypes() {
|
||||
let data = request.resource.data;
|
||||
return data.get("correct",[]) is list &&
|
||||
data.get("incorrect",[]) is list &&
|
||||
data.get("questions",[]) is list &&
|
||||
data.get("mark",0) is number &&
|
||||
data.get("progress",0) is number;
|
||||
|
||||
match /users/{userId} {
|
||||
function isSignedInUser() {
|
||||
return request.auth.uid == userId;
|
||||
}
|
||||
function verifyProgressStaticDataFieldTypes() {
|
||||
let data = request.resource.data;
|
||||
return data.get("set_title","") is string &&
|
||||
data.get("start_time",timestamp.date(2021,1,1)) is timestamp &&
|
||||
data.get("switch_language",true) is bool &&
|
||||
data.get("uid","") is string;
|
||||
|
||||
function verifyFieldTypes() {
|
||||
return verifyStringType("display_name") &&
|
||||
verifyBoolType("sound") &&
|
||||
verifyStringType("theme");
|
||||
}
|
||||
function verifyProgressTermsFieldTypes() {
|
||||
let data = request.resource.data;
|
||||
return data.get("term","") is string &&
|
||||
data.get("sound","") is string;
|
||||
|
||||
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();
|
||||
}
|
||||
function verifyProgressDefinitionsFieldTypes() {
|
||||
let data = request.resource.data;
|
||||
return data.get("definition","") is string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user