I was writing a rule for my "users" collection that looks like this:
match /users/{userId} {
// Returns true if auth's email is empty and user's
// insensitiveEmail is empty or non-existent
function emailDoesNotExist(resource) {
return request.auth.token.email.size() == 0 &&
(!fieldExists(resource, "insensitiveEmail") || resource.data.insensitiveEmail.size() == 0);
}
// Returns true if auth's email matches user's email
function emailMatches(resource) {
return request.auth.token.email == resource.data.insensitiveEmail;
}
// Returns true if the auth uid matches the resource's uid
function uidMatchesResource(resource) {
return request.auth.uid == resource.data.uid && request.auth.uid == resource.id;
}
allow get: if request.auth != null
&& request.auth.uid == userId
&& uidMatchesResource(resource)
&& (emailDoesNotExist(resource) || emailMatches(resource))
I'm getting [firestore/permission-denied] The caller does not have permission to execute the specified operation.
errors in a case where the user id being requested does not exist in the database.
Is that because I'm trying to do comparisons against resource
and resource
doesn't exist?
And if that's so, is there a way I can validate data in returned documents without preventing requests that result in 0 returned documents?
To allow your query to access documents that don't exist, perform a null
check against resource
and short-circuit the result before the other checks are resolved.
Additionally, your email check can be greatly simplified with the use of Map#get()
.
match /users/{userId} {
// ...
// Returns true if auth's email matches the stored
// insensitiveEmail value (if any).
// - has an email that matches stored value? true
// - has no email and stored value is missing? true
// - has no email and stored value is ""? true
// - otherwise? false
// note: while request.auth.token.get("email", "") could also be used,
// the token should probably always have the "email" claim and
// throw an error when it doesn't.
function emailMatches(resource) {
return request.auth.token.email == resource.data.get("insensitiveEmail", "")
}
// Returns true if the auth uid matches the resource's uid
function uidMatchesResource(resource) {
return request.auth.uid == resource.data.uid
&& request.auth.uid == resource.id; // <- this line could be removed because of request.auth.uid == userId check
}
// note spread out for clarity, will need flattening
allow get: if request.auth != null
&& request.auth.uid == userId
&& (
resource == null // permit read when document does not exist
|| (
uidMatchesResource(resource) // or when the uid & email match in existing document
&& emailMatches(resource)
)
)
}