This repository has been archived on 2025-11-02. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
parandum/firestore.rules

190 lines
9.2 KiB
Plaintext

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 verifyUsersFieldTypes() {
let data = request.resource.data;
return data.get("admin",true) is bool;
}
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 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 verifyGroupsFieldTypes() {
let data = request.resource.data;
return data.get("display_name","") is string &&
data.get("sets",[]) is list;
}
function verifyGroupsStaticDataFieldTypes() {
let data = request.resource.data;
return data.get("join_code","") is string;
}
function verifySetsFieldTypes() {
let data = request.resource.data;
return data.get("public",true) is bool &&
data.get("title","") is string;
}
function verifySetsStaticDataFieldTypes() {
let data = request.resource.data;
return data.get("owner","") is string;
}
function verifySetsVocabFieldTypes() {
let data = request.resource.data;
return data.get("term","") is string &&
data.get("sound","") is string &&
data.get("definition","") 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;
}
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 verifyProgressTermsFieldTypes() {
let data = request.resource.data;
return data.get("term","") is string &&
data.get("sound","") is string;
}
function verifyProgressDefinitionsFieldTypes() {
let data = request.resource.data;
return data.get("definition","") is string;
}
}
}