Reputation: 501
I've got a problem with structuring my security rules for Firestore. Specifically:
db.collection("boards").whereEqualTo("roles.${me.email}", "admin")
always fails with PERMISSION_DENIED
error.
What's weird, getting single documents works like a charm. From what I understand this query will not violate any of my security rules. It should always return a subcollection of a isAnyRole(resource)
function.
My security rules:
service cloud.firestore {
match /databases/{database}/documents {
function isSignedIn() {
return request.auth != null;
}
match /boards/{board} {
//Board has owner and 3 possible roles:
//* admin: can do everything, like an owner, but can be removed
//* idea_reader: has read only access to the board and its ideas
//* idea_editor: har write access to ideas and read access to the board itself
function roles() {
return ['admin', 'idea_reader', 'idea_editor'];
}
function isOwner(rsc) {
return request.auth.uid == rsc.data.ownerId;
}
function getRole(rsc) {
return rsc.data.roles[request.auth.token.email];
}
function isOneOfRoles(rsc, array) {
// Determine if the user is one of an array of roles
return isSignedIn() && (getRole(rsc) in array);
}
function isAnyRole(rsc) {
//Determine if user is any role or owner.
return isOwner(rsc) || isOneOfRoles(rsc, roles());
}
function isValidNewBoard() {
// Valid if story does not exist and data is set correctly
return resource == null
&& request.resource.data.ownerId == request.auth.uid
&& request.resource.data.name != null;
}
function isValidBoardUpdate() {
// Valid if ownerId didn't change and called by owner of admin
return (isOwner(resource) || isOneOfRoles(resource, ['admin']))
&& resource.data.ownerId == request.resource.data.ownerId
&& request.resource.data.name != null;
}
// Owner and admin can edit. Owner can delete.
allow write: if isValidNewBoard() || isValidBoardUpdate();
allow delete: if isOwner(resource);
// Owner and any role can read
allow read: if isAnyRole(resource);
match /ideas/{idea} {
// Any role can read ideas
allow read: if isAnyRole(get(/databases/$(database)/documents/boards/$(board)));
//Owner, admin and idea_editor can edit ideas
allow write: if isOwner(get(/databases/$(database)/documents/boards/$(board)))
|| isOneOfRoles(get(/databases/$(database)/documents/boards/$(board)), ['admin', 'idea_editor']);
}
}
match /{document=**} {
allow read, write: if false;
}
}
}
Upvotes: 1
Views: 1489
Reputation: 501
The problem was with the query, not security rules. It's a small tricky thing that is not well documented. So let's have a look at my query:
db.collection("boards").whereEqualTo("roles.${me.email}", "admin")
and let's assume email is [email protected]
. Then the query looks like this:
db.collection("boards").whereEqualTo("[email protected]", "admin")
So the Firestore first tries to access object roles
, then object n
inside of the roles
and so on and so forth. That's why the query was denied - it was not compliant with the defined security rules. If it worked I could have query boards that I shouldn't be able to access.
To fix that issue I had to modify query using FieldPath
:
db.collection("boards").whereEqualTo(FieldPath.of("roles", me.email), ROLE_ADMIN)
Upvotes: 4
Reputation: 145
Shouldn't the template literals be
db.collection("boards").whereEqualTo(`roles.${me.email}`, "admin")
instead of
db.collection("boards").whereEqualTo("roles.${me.email}", "admin")
So, as you are trying to query for documents that are not covered by the security rules (me.email !== "${me.email}"; me.email === `${me.email}`), you will always get a permission denied error.
Upvotes: 1