Cloud Firestore custom claims with query - javascript

I have a firebase collection notes where each document has a user-id array that contains some user ids.
I've set a custom claim on my user access token that is in the format of nid=true (where nid is the note document id they should have access to) however when I try and query the collection for any relevant documents I receive a permission error.
My query is written as follows:
const notesRef = collection(db, "notes");
const allNotesQuery = query(
notesRef,
where("user_ids", "array-contains", user.uid)
);
const noteDocs = await getDocs(allNotesQuery);
my security rules are:
match /notes/{nid} {
allow read, write: if request.auth != null && request.auth.token[(nid)] == true
}
I've also tried nid without the brackets around it like so, but it still doesn't work
match /notes/{nid} {
allow read, write: if request.auth != null && request.auth.token[nid] == true
}
Can anyone see anything obviously wrong with this? If I inspect my token I see the custom claim set correctly with the correct nid value. Unfortunately I can't find a way to test custom claims in the developer console.
*** edit ***
As a follow up to my comments on the first posted answer. The following doesn't work.
allow read, write, list: if request.auth != null && request.auth.token[nid] in resource.data.user_ids
but this does:
allow read, write, list: if request.auth != null && request.auth.uid in resource.data.user_ids

Firestore security rules don't filter the data. Instead they merely ensure that your app is not requesting more data than it's permitted to. For more on this, see the Firebase documentation on rules are not filters.
Since your code is not in any way filtering on the nid token (as far as I can tell), the rules reject the operation. So modify the query to only request only note IDs that the user has access to, and then the rule you wrote can validate it.

Related

Firestore rules: Property is undefined on object. for 'list' # L6

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

Missing or insufficient permissions firestore although it works on rules play ground

I have added some security rules to my project and tested them in rules playground everything seems to be working perfect but when I try to retrieve the same doc using same UID from my website it throws error: "FirebaseError: Missing or insufficient permissions".
Refrence to collection:
dbRef = db.collection("organization").doc(organization).collection(course).doc(courseId).collection("meetings");
This are the snapshot of my code:
Note: I have already tested the security rules in rules playground with different uids and paths. And it worked perfectly.
dbRef = db.collection("organization").doc(organization).collection(course).doc(courseId).collection("meetings");
This query attempts to fetch all documents under meeting collection.
There is a possibility that the above query will fetch documents where designation != 'Teacher' && courseId != course;
Therefore it will fail.
From firestore rules docs
if you have this rule:
match /stories/{storyid} {
// Only the authenticated user who authored the document can read or write
allow read, write: if request.auth != null && request.auth.uid == resource.data.author;
}
and this request db.collection("stories").get().
The query fails even if the current user actually is the author of every story document. The reason for this behavior is that when Cloud Firestore applies your security rules, it evaluates the query against its potential result set, not against the actual properties of documents in your database. If a query could potentially include documents that violate your security rules, the query will fail.
the following query succeeds, because it includes the same constraint on the author field as the security rules:
var user = firebase.auth().currentUser;
db.collection("stories").where("author", "==", user.uid).get()
NB: playground can only test for document gets (not list collection).
Possible solution.
You can write your query in such a way that it can never return the wrong document.
Something like this. When a student is enrolled to take a meeting (or course), add his uid to that meeting's document.
doc = {student_ids = [uidofstudent1, uidOfStudent2, ...]}.
Then you can query like this:
db
.collection("organization")
.doc(organization)
.collection(course)
.doc(courseId)
.collection("meetings")
.where('student_ids', arrayContains: studentUid);
Your firestore rules can be something like this:
match /organisation/{org}/{course}/{courseId}/meetings/{meet=**} {
allow read: if request.auth.uid in resource.data.student_ids ||
isTeacher(request.auth.uid);
}

Firestore Security Rule not allowing two uids to access a doc

TLDR; I want restrict reading a firestore doc to the two users that are mentioned in that doc. Firestore security rules are allowing one to access but failing for other.
In my ecommerce app, when customer A places an order in Shop B, an order document is added in orders collection. I want to setup security rules so that this order can only be read by the customer A or the shop B.
This is what the rules look like :
match /orders/{orderId} {
allow read: if request.auth.uid == resource.data.cust.uid ||
request.auth.uid == resource.data.shop.ownerUid;
allow write: if < Some Condition >;
}
Now while fetching orders for a customer I am querying like this, and it works OK. Security Rules pass it through.
let query = dbFirestore.collection('orders')
.where("cust.uid", "==", firebase.auth().currentUser.uid)
.orderBy('statusHistory.New', 'desc')
.startAfter(lastVisible)
.limit(5);
query.get();
However, when i query for the shop (like below), I get permission denied error (by Security rules)
let query = dbFirestore.collection('orders')
.where("shop.ownerUid", "==", firebase.auth().currentUser.uid)
.orderBy('statusHistory.New', 'desc')
.startAfter(lastVisible)
.limit(5);
query.get();
Both queries are similar, yet one passes and the other fails !!!
Another fact that i want to mention is that a customer can own a shop too, so below is one possible case :
request.auth.uid = shop.ownerUid = cust.uid = firebase.auth().currentUser.uid
I am having a feeling that it may have something to do with the Rules error, but I am not on any conclusion.
I also tried with adding shopId in query and rules, but it still fails.
let query = dbFirestore.collection('orders')
.where("shop.ownerUid", "==", firebase.auth().currentUser.uid)
.where("shop.shopId", "==", shopId_of_this_shop) // <---- This
.orderBy('statusHistory.New', 'desc')
.startAfter(lastVisible)
.limit(5);
Rules :
match /orders/{orderId} {
allow read: if request.auth.uid == resource.data.cust.uid ||
(request.auth.uid == resource.data.shop.ownerUid &&
request.resource.data.shop.shopId == resource.data.shop.shopId);
}
For reference, below is the data from order document :
I appreciate any help on this.
It was due to index not existing.
I hadn't generated index for "shop.ownerUid", and since the query was failing with the "Permission Error" and not the "Index is Required". I kept focussing on Security Rules.
Query on "cust.uid" was passing because it had the index.
Once i realised and generated the index on "shop.ownerUid" and the status field of query, it worked.

Why would a security rule prevent a read in the simulator, but not the equivalent read from a JS client?

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)

Setting Firebase Firestore security rules so only users can CRUD their own data and all else is ignored

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

Categories

Resources