Allowing access based on mapped key - firebase

It's a simple and common use case of security rule, but cannot make it work.
I have a document orgs/fooOrg on my Firestore(not RTDB), and it contains an object
{
"members": {
"fooUser": true
}
}
and the rule applied is
service cloud.firestore {
match /databases/{database}/documents {
match /orgs/{orgId} {
allow read: if "fooUser" in resource.data.members;
}
}
}
I expect all the document in orgs collection should be able to be read; however, the server says Error: Missing or insufficient permissions as a result of running
firebase.firestore().doc('orgs/fooOrg').get()
on a browser (using v4.5.0 and v4.5.1). Even
allow read: if resource.data.members["fooUser"] == true;
fails, too. What went wrong?
In my understanding, this should work according to this document
https://firebase.google.com/docs/firestore/security/secure-data#evaluating_documents_currently_in_the_database
I believe that it was working like a week ago. All the sudden, my working code started to generate the error, so I wrote this MCVE and tested on several different projects.
In addition, I found similar issues below, but a bit different from them, so not sure if it's the same reason (a bug on Firestore)
Firestore security rules based on map values
(My case, even getting a simple document fails)
Firestore read rules with self condition
(This case uses a value of a map. My case, a key is used)

Now seems that the issue is solved without changing code. No announcement, but seems that something is fixed by Firestore side.

Related

Firebase security rules not working - Can't figure out why

is there some one that knows how security rules for firestore works?
I'm trying to do something like this but it doesn't work (I don't get access to data).
match /contents/{contentID} {
allow read: if get(/databases/$(database)/documents/users/$(request.auth.uid)/reserved/permissions).data.contents.hasAny([contentID])
allow create, update, delete : if false
}
It seems the problem is contentID since if I do this
match /contents/{contentID} {
allow read: if get(/databases/$(database)/documents/users/$(request.auth.uid)/reserved/permissions).data.contents.hasAny(["3"])
allow create, update, delete : if false
}
and update the document located in user/reserved/permission adding "3" to contents (that is an array field of the document ) it works. It's like contentID is not converted right.
Can someone explains why?
UPDATE
The client code is just
firestore()
.collection('contents')
.onSnapshot((querySnapshot) => {
console.log(querySnapsho)
})
and it return always null. If I change the rule in
match /contents/{contentID} {
allow read: true
allow create, update, delete : if false
}
it works. So the problem is with the rule
The problem is that security rules are not filters. I strongly suggest reading that documentation to understand how the system work.
Your query attempts to get all documents in the collection. The rules deny that query because it's not certain if the client actually has read access to each any every document. It will not evaluate a get() for each possible document - that simply doesn't scale (and it would be very expensive for you for large collections).
Your client app should be able to get() any individual document where the rule evaluates to true, but you won't be able to perform queries against the collection like this.
The rule is well written. The problem is that the generic query get() on the entire collection "contents" it's not allowed after this kind of rules are activated (and it makes sense since this behaviour is designed to reduce the resources needed for a query)
Read this to understand the logic
https://medium.com/firebase-developers/what-does-it-mean-that-firestore-security-rules-are-not-filters-68ec14f3d003

Super basic firestore security rule won't work

I can't get the literal simplest firestore security rule I can write to work in the play ground. Just for testing, I've made a Cloud Firestore database with a collection named users. It has one field stuff. In the playground, these are my rules:
service cloud.firestore {
match /databases/{database}/documents {
match /users/{user} {
allow read, write: if true;
}
match /{document=**} {
allow read, write: if false;
}
}
}
I'm simulating a get on location: /databases/(default)/documents/users, but it always fails due to the document=** match, and never matches /users/{user}. Why is this! Feels like I'm following the most basic examples from the docs.
Added a couple screenshots for clarify.
In the "Rules playground", in the location field, you don't need to enter /databases/(default)/documents/. This part of the path is already taken into account, as it is shown above the editable field with the pale grey (or greyed out) /databases/(default)/documents string.
So, by just entering users/C8YDk... it will work, since your rule allows reading the doc, due to an overlapping matching statement.
More info on how to use the playground is to be found here.

Firestore rules for a survey web app and a shared link

I'm trying to build a survey web app with firebase and have a certain user flow in my mind but I don't know if it can be done:
In this app you would signup/login and build your survey, which will be stored like this:
collection("creator").doc("creatorID").collection("surveyData").
After storing the data, you would get a link (e.g. webapp.com/forUser/secret/surveyName) you then can share with your friends. This link opens the same web app (from another entry point) but with anonymous authentication, hydrating and displaying only the content of this one survey from this one user.
There are two main problems here:
Pointing the user's web app to the right data via the link and only being able to access this one survey.
Writing the user answers in the creator's files
For 1) Can I pass a secret via the link (could I use the creatorID for this or is this not safe/secure?), which points the user to this one survey data (without granting any other access)? If so, how to do this without hard coding the secret into the authentication rules?
For 2) I could use a two-step process:
The user answers the survey questions and the results are stored in something like:
collection("user").doc("secret").collection("surveyData").
When the creator opens the app later on (or done via cloud functions), the app fetches all the answers from the shared secret doc. Can this be done in one step?
I hope this makes sense somehow. Maybe my ideas are way too complicated and there is an easy way to do this or are there some best practices in this regard?
Thanks for your help!
A while ago I built a hashing package into Firebase Rules. It might be possible to use that?
Example rules:
rules_version="2"
function hashUserCreds(auth) {
let combinationOfCreds = string(auth.uid) /* + string(auth.other.stuff) */;
// hashing.sha256 will work on strings as well, but it's more readable to
// make this explicit.
return hashing.sha256(combinationOfCreds.toUtf8()).toBase64();
}
service cloud.firestore {
match /databases/{database}/documents {
match /{ownerHash}/{surveyId} {
function isOwner() {
return hashUserCreds(request.auth) == ownerHash;
}
allow read;
allow write: if isOwner();
match /{responseId} {
function isResponder() {
return hashUserCreds(request.auth) == responseId;
}
allow update: if isResponder();
allow read, create: if isOwner();
}
}
}
}
If you want open-responses then you change the final two permissions, but it sounds like you want the survey creator to choose who can respond which I've tried to express there.
Using hashes instead of names will provide better anonymity, obscure PII, and force the restriction that a user can only edit their data.

How to make complex Firestore rules work fine with maps and lists?

I am currently having a lot of trouble setting up complex Firestore rules, but nothing worked so far and I would love if someone could help.
This is my root database structure:
groups
user_access
meetings
Where user_access has email address as key, and an object/list/value (more on that later) with the groupId he can access followed by the level of permission.
Every meeting has a groupId (where it belong).
So, I would like a rule to: check if the current groupId value from meetings is in the document at user_access with the current email address as key.
I thought about doing this rule (simplified below):
service cloud.firestore {
match /databases/{database}/documents {
match /meetings/{meetingId} {
function correctUser() {
return get(/databases/$(database)/documents/access/$(request.auth.token.email)).data.obj[get(/databases/$(database)/documents/meetings/$(meetingId)).data.groupId] == "leader"
}
allow read: if correctUser();
allow write: if correctUser();
}
}
But it doesn't work.. And I'm not sure why.
I tried making a list:
function correctUser() {
return get(/databases/$(database)/documents/meetings/$(meetingId)).data.groupId in get(/databases/$(database)/documents/access/$(request.auth.token.email)).data.list
}
But it also didn't work and I'm not sure why.
The best case scenario would be using a list of objects (a map), key(id),value(permission). Is that possible? Worst case scenario I can use a list for each different permission, or even put all the ids as value (I'll probably never reach the 20k fields limit).
So, I have two questions:
First, how can I make my rule(s) work?
Second, how do I call values from wildcards from inside fields? For example, on the example above with {meetingId}, how would I use this meetingId as a key? (...).data.meetingId? (...).data[$(meetingId)]? I found it very confusing and bad documented. What about on maps? Same thing?
Thanks!
It took me weeks to find out, but what I ultimately wanted and worked was:
function isLeader() {
return get(/databases/$(database)/documents/access/$(request.auth.token.email)).data[request.resource.data.groupId] == "leader"
|| get(/databases/$(database)/documents/access/$(request.auth.token.email)).data[resource.data.groupId] == "leader"
}
There is difference between request.resource.data.groupId and resource.data.groupId which I didn't know and was killing my requests, sometimes read, sometimes write. Glad it works now.

Firestore security rules based on map values

I want to store if a user is permitted to read a document in the document itself, based on the user's email address. Multiple users should have access to the same document.
According to the documentation Firestore does not allow querying array members. That'S why I'm storing the users email addresses in a String-Bool Map with the email address as a key.
For the following example I'm not using emails as map keys, because it already doesn't work with basic strings.
The database structure looks like that:
lists
list_1
id: String
name: String
owner: E-Mail
type: String
shared:
test: true
All security rules are listed here:
service cloud.firestore {
match /databases/{database}/documents {
match /lists/{listId=**} {
allow read: if resource.data.shared.test == true
}
}
}
Edit: It also doesn't work if I use match /lists/{listId} instead of match /lists/{listId=**}
How I understand it, this security rules should allow reading access to everyone if the value in the map shared[test] is true.
For completness sake: This is the query I'm using (Kotlin on Android):
collection.whereEqualTo("shared.test", true).get()
.addOnCompleteListener(activity, { task ->
if (task.isSuccessful) {
Log.i("FIRESTORE", "Query was successful")
} else {
Log.e("FIRESTORE", "Failed to query existing from Firestore. Error ${task.exception}")
}
})
I'm guessing that I cannot access map values from the security rules. So what would be an alternative solution to my problem?
In the Firestore rules reference it's written that maps can be accessed like that resource.data.property == 'property' so, what am I doing wrong?
Edit: This issue should be fixed now. If you're still seeing it (and are sure it's a bug with the rules evaluator), let me know in the comments.
I've chatted with some folks here about the problem you're encountering, and it appears to be an issue with the security rules itself. Essentially, the problem seems to be specific to evaluating nested fields in queries, like what you're doing.
So, basically, what you're doing should work fine, and you'll need to wait for an update from the Firestore team to make this query work. I'll try to remember to update this answer when that happens. Sorry 'bout that!
Whenever you have (optional) nested properties you should make sure the property exists before continuing to check its' value eg.
allow read: if role in request.auth.token && request.auth.token[role] == true
in your case:
allow read: if test in resource.data.shared && resource.data.shared.test == true
, I was struggling a long time with roles until I realized that on non-admin users the admin field is undefined and firestore rules just crashes and doesn't continue checking other possible matches.
For a user without token.admin, this will always crash no matter if you have other matches that are true eg:
function userHasRole(role) {
return isSignedIn() && request.auth.token[role] == true
}

Resources