From ece083e2d90a5459205c9193f09fa4dd0db182d6 Mon Sep 17 00:00:00 2001 From: Matthew Grove Date: Wed, 1 Sep 2021 17:31:17 +0100 Subject: [PATCH] Update Firestore security rules & indexes --- firestore.indexes.json | 87 ++++++++++++++++++++++++++++++++++++++++++ firestore.rules | 64 +++++++++++++++++++------------ 2 files changed, 126 insertions(+), 25 deletions(-) create mode 100644 firestore.indexes.json diff --git a/firestore.indexes.json b/firestore.indexes.json new file mode 100644 index 0000000..a4a9dc5 --- /dev/null +++ b/firestore.indexes.json @@ -0,0 +1,87 @@ +{ + "indexes": [ + { + "collectionGroup": "sets", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "owner", + "order": "ASCENDING" + }, + { + "fieldPath": "title", + "order": "ASCENDING" + } + ] + }, + { + "collectionGroup": "sets", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "public", + "order": "ASCENDING" + }, + { + "fieldPath": "owner", + "order": "ASCENDING" + }, + { + "fieldPath": "title", + "order": "ASCENDING" + } + ] + }, + { + "collectionGroup": "progress", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "uid", + "order": "ASCENDING" + }, + { + "fieldPath": "start_time", + "order": "DESCENDING" + } + ] + }, + { + "collectionGroup": "progress", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "duration", + "order": "ASCENDING" + }, + { + "fieldPath": "uid", + "order": "ASCENDING" + }, + { + "fieldPath": "start_time", + "order": "DESCENDING" + } + ] + }, + { + "collectionGroup": "progress", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "setIds", + "order": "ASCENDING" + }, + { + "fieldPath": "uid", + "order": "ASCENDING" + }, + { + "fieldPath": "start_time", + "order": "ASCENDING" + } + ] + } + ], + "fieldOverrides": [] +} diff --git a/firestore.rules b/firestore.rules index 14d1fa6..fc8ee76 100644 --- a/firestore.rules +++ b/firestore.rules @@ -17,9 +17,10 @@ service cloud.firestore { 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 isSetOwnerOrIsPublic(setId) { + let data = get(/databases/$(database)/documents/sets/$(setId)).data; + return data.public || data.owner == request.auth.uid; } function verifyCreateFields(fields) { @@ -43,8 +44,8 @@ service cloud.firestore { return getRequestField(field, "") is string; } - function verifyNullType(field) { - return getRequestField(field, null) == null; + function verifyEmptyArrayType(field) { + return getRequestField(field, []) == []; } match /users/{userId} { @@ -54,7 +55,8 @@ service cloud.firestore { function verifyThemeValue() { let requestField = getRequestField("theme", "default"); - return requestField == "default"; + let themes = ["default", "red", "maroon", "green", "light-blue", "pink", "yellow", "orange"]; + return requestField in themes; } function verifyFieldTypes() { @@ -74,9 +76,9 @@ service cloud.firestore { match /groups/{groupId} { function verifyGroupFieldTypes() { - return getRequestField("role", "") == "member" || - getRequestField("role", "") == "contributor" || - getRequestField("role", "") == "owner"; + return getRequestField("role", "member") == "member" || + getRequestField("role", "contributor") == "contributor" || + getRequestField("role", "owner") == "owner"; } function getPossibleGroupFields() { @@ -88,7 +90,14 @@ service cloud.firestore { 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" || (isAdmin() && verifyGroupFieldTypes())) && verifyCreateFields(getPossibleGroupFields()); - allow update: if isSignedIn() && ((getGroupRole(groupId) == "owner" || isAdmin()) && verifyGroupFieldTypes()) && verifyUpdateFields(getPossibleGroupFields()); + allow update: if isSignedIn() && + (getGroupRole(groupId) == "owner" || isAdmin()) && + verifyGroupFieldTypes() && + !(isSignedInUser()) && verifyUpdateFields(getPossibleGroupFields()); + allow update: if isSignedIn() && + isAdmin() && + isSignedInUser() && + getRequestField("role", "") == "owner"; } } @@ -117,11 +126,16 @@ service cloud.firestore { match /sets/{setId} { + function verifyPublicField() { + return ((resource == null || resource.data == null || resource.data.groups == [] || resource.data.group == null) && verifyBoolType("public")) || + (resource.data.groups != [] && resource.data.groups is list && getRequestField("public", true) == true); + } + function verifyFieldTypes() { - return verifyBoolType("public") && + return verifyPublicField() && verifyStringType("title") && verifyStringType("owner") && - verifyNullType("groups"); + verifyEmptyArrayType("groups"); } function getPossibleFields() { @@ -142,20 +156,16 @@ service cloud.firestore { return [[], fields[0]]; } - allow read, delete: if isSignedIn() && request.auth.uid == resource.data.owner; - allow read: if isSignedIn() && resource.data.public; + allow read: if isSignedIn() && (request.auth.uid == resource.data.owner || resource.data.public == true); 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(); + allow delete: if isSignedIn() && request.auth.uid == resource.data.owner && (resource == null || resource.data == null || resource.data.groups == []); match /vocab/{vocabId} { - function verifySoundValue() { - return getRequestField("sound", vocabId) == vocabId; - } - function verifyVocabFieldTypes() { return verifyStringType("term") && verifyStringType("definition") && - verifySoundValue(); + verifyBoolType("sound"); } function getPossibleFields() { @@ -176,10 +186,10 @@ service cloud.firestore { return [[], fields[0]]; } - allow read, delete: if isSignedIn() && isSetOwner(setId); - allow read: if isSignedIn() && isPublicSet(setId); + allow read: if isSignedIn() && isSetOwnerOrIsPublic(setId); allow create: if isSignedIn() && isSetOwner(setId) && verifyCreateFields(getPossibleCreateFields()) && verifyVocabFieldTypes(); allow update: if isSignedIn() && isSetOwner(setId) && verifyUpdateFields(getPossibleUpdateFields()) && verifyVocabFieldTypes(); + allow delete: if isSignedIn() && isSetOwner(setId); } } @@ -196,16 +206,20 @@ service cloud.firestore { 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(); + allow read: if isSignedIn() && resource.data.uid == request.auth.uid; + allow delete: if isSignedIn() && resource.data.uid == request.auth.uid && isNotComplete(); match /terms/{vocabId} { - allow read: if isSignedIn() && isProgressUser() && !(isLanguageSwitched()); + allow read: if isSignedIn() && isProgressUser() && (!(isLanguageSwitched()) || !(isNotComplete())); } match /definitions/{vocabId} { - allow read: if isSignedIn() && isProgressUser() && isLanguageSwitched(); + allow read: if isSignedIn() && isProgressUser() && (isLanguageSwitched() || !(isNotComplete())); } } + + match /join_codes/{joinCode} { + allow get: if isSignedIn(); + } } }