I'm trying to setup role based authentication in Cloud Firestore. The intent is to have it set up so that only users within an organization can read the organization or associated events. It is working when the client requests a single document (get), but not a query of documents (list).
My collection called organizations looks like this, so far only containing a single document with ID devtest:
{
"name": "Dev Test"
"users": [
"STFg0EvGemTaD2r9jyby0UMSt6O2"
]
"orgid": "devtest"
}
I also have a collection of events (so far also only containing one):
{
"name": "foo",
"orgid": "devtest",
...
}
And my rules look like this:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function isSignedIn() {
return request.auth != null;
}
function userIsUser() {
return isSignedIn() &&
request.auth.uid in get(/databases/$(database)/documents/organizations/$(resource.data.orgid)).data.users;
}
match /organizations/{org} {
allow read: if userIsUser();
allow write: if false;
}
match /events/{event} {
allow read: if userIsUser();
allow write: if userIsUser();
}
}
}
What I'm confused about is why I can do a get and it succeeds, for example:
firebase.firestore().collection('organizations').doc('devtest').get()
but when I try a query it fails, for example:
firebase.firestore().collection('organizations').where('users', 'array-contains', firebase.auth().currentUser ? firebase.auth().currentUser.uid : false)
responds with permission denied. I know that a security rule is not a filter, there is only the one document in the collection right now.
Any suggestions on what I might be missing?
Thanks!
According to this documentation, using authentication objects directly causes issues, since they can be in an intermediary state. It’s recommended you also create an authentication listener that waits for the state to change.
By using an observer, you ensure that the Auth object isn't in an intermediate state—such as initialization—when you get the current user. When you use signInWithRedirect, the onAuthStateChanged observer waits until getRedirectResult resolves before triggering.
If this is happening in your query, it might explain why it gives you the permissions error, as the object can be still initializing and resolving to false instead of true.
Related
I am having an issue where I am trying to query a subquery by a field, and I have tried the emulator as well as firebase support (still on going, but not solutions yet).
When running the following query:
const queryByNumber = query(
collectionGroup(this.firestore, 'residents'),
where('cellNumber', '==', number)
);
return from(getDocs(queryByNumber));
This query has the error of:
Property cellNumber is undefined on object. for 'list' # L6
This is what the rules look like at the moment
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{path=**}/residents/{residentDoc} {
allow write: if resource.data.cellNumber == request.auth.token.phone_number;
allow read: if request.auth.token.phone_number == resource.data.cellNumber;
//allow read; //temp permission till support fix
}
match /Units/{unitNumber} {
allow read: if request.auth != null;
function residentDoc(residentId) {
return get(/databases/$(database)/documents/Units/$(unitNumber)/residents/$(residentId))
}
match /pets/{petId} {
allow write: if residentDoc(request.resource.data.residentId).data.cellNumber == request.auth.token.phone_number;
allow read: if request.auth != null;
}
}
}
}
And this is the firestore data structure at the moment:
I have tried to change my query to have the array-contains in the where clause, but that doesn't change much.
Note: the allow read without the if check allows the data to be retrieved, so the query does work, just the rules to secure it are not`
The problem is in this condition in your rules:
request.auth.token.phone_number in resource.data.cellNumber
The in operation in Firestore security rules is only defined for maps and lists/arrays, but your cellNumber field is a simple string value, which does not define an in operation as you can see here.
If you want to check whether the phone number in the token is the same as the value of the field, use == instead of in. If you want to test whether it is in the field, consider using the matches method on the string.
Also see:
Firebase firestore security rules allow if document ID contains string
Using contains() with firebase rules
The issue was another query trying to get all resident data after the initial one was fired, which caused it to fail the rule because the second one wasn't querying by cellNumber anymore. I have changed the rules to validate by UID since that will always be provided
I have a firestore rule for a collection "orders" and i use the simulator tool to est my rules and they definitely pass for all reads, yet when i attempt to display the orders in my console i get an error
Rules
match /orders/{document=**} {
allow read;
allow write: if isAdmin();
allow create: if isOrderOwner();
}
function isAdmin() {
return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.userinfo.isAdmin;
}
function isOrderOwner() {
return get(/databases/$(database)/documents/orders/$(request.auth.uid)) == request.auth.uid
}
On my app, all i'm doing is console.log(orders) and i get this error:
Uncaught Error in snapshot listener: FirebaseError: Missing or insufficient permissions.
In my front end (react)
db.collectionGroup('orders').onSnapshot(snap => {
let ordersArr = []
snap.forEach(doc => {
if(doc.data().orderid)
ordersArr.push(doc.data())
})
setAllOrders(ordersArr)
console.log(ordersArr) // -> this console causes the insufficient permissions error
})
I can't figure it out, i very simply have allow read; for all documents in my orders collection.
Note: the orders are in a subcollection of the orders collection (i don' think that is the problem though)
You are using collectionGroup so if I assume orders is a sub-collection for each document in parent collection, let's say users or customers, the rules should ideally be something like this:
rules_version = '2';
service cloud.firestore {
match /{path=**}/orders/{orderId} {
allow read;
allow write: if false; // <- any rules you have for writes
}
}
I ran the following query and the read operation was allowed:
const snapshot = await firebase.firestore().collectionGroup("orders").get();
console.log(snapshot.size, "docs")
The {path=**} is a recursive wildcard as mentioned in the documentation.
The documentation says,
In your security rules, you must explicitly allow collection group
queries by writing a rule for the collection group:
Make sure rules_version = '2'; is the first line of your ruleset. Collection group queries require the new recursive wildcard {name=**}
behavior of security rules version 2.
Write a rule for you collection group using match /{path=**}/[COLLECTION_ID]/{doc}.
Because both isAdmin() and isOrderOwner() use a get() call, you are likely exceeding the limit for your query. You will need to apply a limit() to your collection group query and paginate through the results. As each rule uses two get() calls, you will only be able to retrieve up to 10 documents per query request.
Paraphrased from the Cloud Firestore Security Rules Documentation:
A limit of 10 exists(), get(), and getAfter() calls applies to single-document requests and query requests.
A limit of 20 exists(), get(), and getAfter() calls per request applies to multi-document reads, transactions, and batched writes.
For example, imagine you create a batched write request with 3 write operations and that your security rules use 2 document access calls to validate each write. In this case, each write uses 2 of its 10 access calls and the batched write request uses 6 of its 20 access calls.
Exceeding either limit results in a permission denied error.
I am using Firebase Firestore to store a list of transactions in a collection called transactions .
I use react to get the transaction from the collection, using a URL with the id of the transaction document: http://myurl.com/h1kj54h2jk35h
const id = match.params.id;
firebase.firestore().collection('transactions').doc(id).get()
The same way I create a new doc. I have not firebase authentication.
How can I secure my Firestore rules:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /transactions/{id} {
allow read, write;
}
}
}
If I don't allow write, I can't create new transactions.
If I don't allow read, I can't read the transaction, but I don't want to allow the read of all transactions. Only the once when the id from the URL is valid.
Thus, I am looking for a way to protect my database agains unwanted document creations and unwanted document reads.
I think you're looking for what are called granular operations. From that link:
In some situations, it's useful to break down read and write into more granular operations. For example, your app may want to enforce different conditions on document creation than on document deletion. Or you may want to allow single document reads but deny large queries.
A read rule can be broken into get and list, while a write rule can be broken into create, update, and delete.
So if you want to only allow creation of new documents, and getting of document for which a user must know the ID, that'd be:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /transactions/{id} {
allow create, get;
}
}
}
I have a doc like this (all of this simplified) ...
myCollection -> myDoc: {
...
roles: { "xyz123" : "reader" } // associate UIDs with privileges
}
and a rule like this...
match /myCollection/{doc=**} {
allow list: if request.auth.uid != null;
allow read: if canRead(resource);
where canRead is defined as...
function canRead(res) {
return request.auth.uid != null &&
request.auth.uid in res.data.roles &&
res.data.roles[request.auth.uid] == 'reader';
}
Using the simulator, simulating an authenticated user with UID="abcdef" (not present in the roles object), I see that this rule prevents a get. When the simulated UID="xyz123" (which is present in roles), the get is allowed.
But, in my app, performing the same experiment, reading succeeds with a UID that should not be permitted. In other words...
console.log(firebase.auth().currentUser.uid) // logs 'abcdef', which should not be permitted
db.collection('myCollection').onSnapshot(snapshot => {
console.log(snapshot.docs) // logs several docs, including myDoc
It's my understanding that the collection snapshot should not be permitted because one of the results cannot be read. Furthermore, the returned docs actually contain the doc that should be excluded.
Can you think of any circumstances where both of these things can be true: A simulated get fails as expected, but a real get from a client succeeds despite a rule preventing it?
The reason why the query is allowed is because of this line:
allow list: if request.auth.uid != null;
list is essentially queries, but not individual document gets. read is both. Since you allow list to all authenticated users, your query will work for all auth'd users. The get from the console simulator was not affected by this line.
(Note that read access == get + list)
I have an app that is designed so authenticated users via Google only have access to their own data with no "social" features. I want to know the security rules for the below criteria.
Let's say I have 5 collections and one of them is called "todos" and the data mirrors the other collections in that it has a field for the authenticated users uid. The typical document looks something like this:
Todos
todo:{
title:"some titled",
body:"we are the world , we are the children",
uid:"2378y4c2378rdt2387btyc23r7y"
}
Some other collection
thing:{
name:"some name",
content:"Some content",
whatever:"whu-eva",
uid:"2378y4c2378rdt2387btyc23r7y"
}
I want the authenticated Google user to be able to CRUD any data that has said users uid in the uid field. I want all other data to be inaccessible to the logged in user.
I want to know how to create rules for this scenario.
I'm mulling through the documentation now but I figure I might be able to save some time by asking.
I do not have specific roles for the app.
https://firebase.google.com/docs/firestore/solutions/role-based-access
As a side note, is their a feature in Firebase to automatically bind an authenticated Google users uid to documents created while they are logged in? (I am assuming the answer is no and I was planning on manually grabbing the uid in my app and setting it on the client prior to document creation).
Thank you.
Update
I tried using the code that Klugjo posted below.
When I try to test it in the simulator I get an error.
Here is my collection and a screenshot of the error.
Here is something else I tried:
Based on everything I've read it seems like the following code should work - but it doesn't. I've supplemented the key "userId" in place of " uid" that is written in the object data at the top of this post. I changed the key to distinguish it from the uid.
service cloud.firestore {
match /databases/{database}/documents {
match /todos/{id} {
allow read: if request.auth.uid == request.resource.data.userId;
allow create, update, delete:
if request.resource.data.userId == request.auth.uid;
}
}
}
I've created a video where I try to GET and CREATE a document.
I don't think I am using the testing feature correctly.
Video
https://www.youtube.com/watch?v=W7GZNxmBCBo&feature=youtu.be
EDIT
I have it working when I test with a hard-coded request.auth.uid.
In the image below I hardcoded "test" as the request.auth.uid.
My problem now is that I would really like to know how to test it in the rules editor without hard-coding this information.
Edit
Here is a video demo of the problem using a real app.
https://www.youtube.com/watch?v=J8qctcpKd4Y&feature=youtu.be
Here is a sample secure rule set for your requirements.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{id}/{u=**} {
allow read, write: if (isSignedIn() && isUser(id));
}
match /todos/{id}/{t=**} {
allow read, write: if (isSignedIn() && isUserOwner());
}
match /{document=**} {
allow read, write: if false;
}
function isSignedIn() {
return request.auth != null;
}
function isUser(uid) {
return uid == request.auth.uid;
}
function isUserOwner() {
return getResourceData().uid == request.auth.uid;
}
function getResourceData() {
return resource == null ? request.resource.data : resource.data
}
}
}
All documents are publicly inaccessible.
The rests will be decided based on the data already saved in DB and / or the data being sent by the user. The key point is resource only exists when reading from DB and request.resource only exists when writing to DB (reading from the user).
Documents under todos can be read and written only if they have a saved uid which is the same as the sent request's uid.
Documents under users can be read and written only if their document id is the same as the sent request's uid.
isSignedIn() function checks if request is authorised.
isUser(id) function checks if id matches the authorised request's uid.
isUserOwner() function checks if document's uid matches the authorised request's uid.
I think what you are looking for is the "resource" parameter in the security rules: https://firebase.google.com/docs/firestore/security/rules-conditions#data_validation
Try something like:
service cloud.firestore {
match /databases/{database}/documents {
match /todos/{id} {
allow read, write: if request.auth.uid == resource.data.userId;
}
}
}
EDIT:
Subcollection strategy
If you change your DB to look like the following:
/users/{userId}/todos/**
then you could allow users to read/write anything under their own document with the following rule:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{uid}/{doc=**} {
allow read, write: if request.auth != null && request.auth.uid == uid;
}
}
}
This would have the advantage of not needing to introspect the contents of the data which I believe might count against your read quota.
You are looking for something like this
service.cloud.firestore {
match /databases/{database}/documents {
match /todos/{userId} {
allow read, update, delete: if request.auth.uid == userId;
allow create: if request.auth.uid != null;
}
}
}
match /todos/{userId} makes the userId variable available in the rule condition
request.auth.uid matches the auth'd user uid