I tested this security rules structure in the Firebase console for Firestore, and I think it does what I want, namely to keep a map of roles for users at the organization level and enforce permissions for organization subcollections by always referencing the parent organization using the get()
method.
service cloud.firestore {
match /databases/{database}/documents {
// ORGANIZATIONS collection
match /organizations/{organization} {
function isSignedIn() {
return request.auth != null;
}
function getRole(rsc) {
// Read from the 'roles' map in the resource (rsc)
return rsc.data.roles[request.auth.uid];
}
function isOneOfRoles(rsc, array) {
// Determine if the user is one of an array of roles
return isSignedIn() && (getRole(rsc) in array);
}
allow write: if isSignedIn();
allow read: if isSignedIn() && isOneOfRoles(resource, ['owner']);
// PROJECTS subcollection
match /projects/{project} {
allow write: if isSignedIn() && isOneOfRoles( get(/databases/$(database)/documents/organizations/$(organization)), ['owner']);
allow read: if isSignedIn() && isOneOfRoles(get(/databases/$(database)/documents/organizations/$(organization)), ['owner', 'member']);
}
}
}
}
Now, because queries must match security rules, I'm trying to understand how to make a query using the Javascript client that will match the above security rules.
I have a working query to get the organizations that looks like this:
db.collection('organizations').where(`roles.${currentUser.uuid}`, '==', 'owner').get()
which is based on a document structure like the following:
organizations (collection)
{organization} (document)
name: string,
roles: {
<user_id_1>: "owner",
<user_id_2>: "member"
},
projects (subcollection)
Is it possible to have a query (or even multiple queries) in the Javascript client that checks that user is a certain role at the organization
level AND checks document properties within the projects
subcollection, similar to what the security rules do?
EDIT: Ultimately, I think this is a question of authorization
and whether or not Firestore supports my approach. It seems that my best bet at this point is the recursive wildcard syntax - match /organization/{document=**}
- but because matching any condition allows access to all nested documents, this approach appears less secure.
Ideally, I could specify a collection path - e.g., organizations/{organization}/projects
- in my client-side query and have a way to specify that the {organization}
roles must be checked before granting access to the projects
subcollection.
POTENTIAL SOLUTION: The following security rules structure leaves a lot to be desired, but it seems to be an improvement. Instead of paths to match specific subcollections, I'm just using recursive wildcard syntax. Does anyone know of a better solution?
service cloud.firestore {
match /databases/{database}/documents {
function isSignedIn() {
return request.auth != null;
}
function getRole(rsc) {
// Read from the 'roles' map in the resource (rsc)
return rsc.data.roles[request.auth.uid];
}
function isOneOfRoles(rsc, array) {
// Determine if the user is one of an array of roles
return isSignedIn() && (getRole(rsc) in array);
}
// ORGANIZATIONS
match /organizations/{organization} {
allow write: if isOneOfRoles(resource, ['owner']);
allow read: if isOneOfRoles(resource, ['owner', 'member']);
}
// Any subcollections under ORGANIZATION
match /organizations/{organization}/{sub=**} {
allow write: if isOneOfRoles(get(/databases/$(database)/documents/organizations/$(organization)), ['owner']);
allow read: if isOneOfRoles(get(/databases/$(database)/documents/organizations/$(organization)), ['owner', 'member']);
}
}
}