Firestore unwanted saving of repeated entries - firebase

I'm trying to store locations nearby me for a test. I ran the following code:
var macro_query = db.collection("cafes");
macro_query = macro_query.where("name", "==", place.name); // check whether name exists
console.log("Checking final query: ", macro_query); // check whether it exists
macro_query.get()
.then(function(querySnapshot) {
console.log(querySnapshot.empty); // returns true if empty, returns false if place.name is already in database
if (querySnapshot.empty) {
db.collection("cafes").add({ // store it since it doesnt exist
name: place.name,
});
console.log("Added into database: ", place.name);
} else {
console.log("Location already in database");
}
});
The logic of the code is basically to check the database if place.name already exist. If it does, the script does nothing, otherwise the place should be added to the database.
The code works fine after a couple of tests, to make sure that it never adds the same location twice. But after running it many many times, i noticed that in my firestore there could be more than 2 unique keys with the same location (meaning, 3 locations with the exact same name)
Is my code failing somewhere and I'm not matching it correctly?
Side note, I'm wondering if this is because of my security rules?
// Allow read/write access to all users under any conditions
// Warning: **NEVER** use this rule set in production; it allows
// anyone to overwrite your entire database.
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if true;
}
}
}

This has nothing to do with security rules.
Your code has a race condition. Since there is some brief period of time between the query and the update, that means you could end up with multiple concurrent queries each adding a store with the same name. Unfortunately, for Firestore web and mobile clients, there is no way to make this sort of query atomic in order to avoid the race condition. Firestore transactions won't help you here, since you can't do a query inside of a transaction.
If you want to atomically check-and-set, the thing to check must the document ID, not a field in the document. If you're looking for a specific document by ID, you can then use a transaction to only create the document if it doesn't already exist. This also implies either one of two things:
Your store names are also valid document IDs
Or you have a dedicated collection for storing encoded store names as document IDs, with parallel documents in another collection that actually contain the store data.
If you choose #2, you are in for quite a bit of work to make that happen smoothly.

Related

Firestore security rules get(). exists()

I have documents in Firestore like the following:
BID: "123"
From: "xxx"
Opn: true
I need to check if there are another document have BID == "123" and Opn == true before create the document, because it's not possible to have one more document where Opn is true with same BID.
I try to use get() and exists but it's not working with 2 data fields.
What I tried:
function checkIfThereOpenRoomForSameBBB(xxx) {
return !(
(get(/databases/$(database)/ChatRooms/$(ChatRoom)).data.BID == xxx) &&
(get(/databases/$(database)/ChatRooms/$(ChatRoom)).data.Opn == true)
);
}
is there any solution for this case?
Security rules can't search for data in the database, as that would be prohibitively slow and expensive. All they can do is check if a document exists at a specific path, or read a document as a specific path and check its contents.
This means that any time you want to check if something exists, you'll need to ensure that lives at a known path. So if the combination of BID and Opn=tru must be unique, you should create a collection where the key of each document consists of the BID value and Opn=true.
If this is a global requirement for your app, you could even use this key in your existing collection instead of the (likely auto-generated) key you currently use.
Also see:
Prevent duplicate entries in Firestore rules not working
firebase rule for unique property in firestore
I want to write a rule that will don't allow add same document second time

How to list all documents the user can read from a Firestore collection?

How do you get all documents in a collection, for which the current user has read permissions?
Trying to get all documents results in a permissions error, because it includes attempts to read documents where the user does not have permission (rather than returning the filtered list of documents).
Each user in this app can belong to multiple groups. Reads are locked down to the groups that they have been added to.
match groups/{group} {
allow read: if exists(/databases/$(database)/documents/groups/$(group)/users/$(request.auth.uid));
}
Here's how this would look with a hypothetical subcollection-contains-id operator.
firestore()
.collection("groups")
.where("users", "subcollection-contains-id", user.uid);
As a temporary workaround I've moved this logic to a cloud function. Here's a shorthand of how it works.
for (let group of firestore().collection("groups")) {
let user = firestore.doc(`groups/${group.id}/users/${uid}`);
if (user.exists) {
// Send this group id to the client
}
}
How can I keep these concerns together and move this logic to the client side without relaxing the security rules?
You could add owners field in the documents inside a collection
owners: ["uid1", "uid2"]
Then, you could get all the posts with uid by searching with array_contains
ref.where("owners", "array-contains", uid)
In rules, you could add sth like these:
allow read: if request.resource.data.owners.hasAny([request.auth.uid]) == true
allow update: if request.resource.data.owners.hasAny([request.auth.uid]) == true

Using Variables in a Firestore Security Rules "List" operation

I'm attempting to setup security rules that allow access to a collection, based on the value of a document field in a subcollection.
This works as expected when retrieving an individual document by id, which is a get operation. However, when querying main_collection (a list operation), this fails with a "permission denied" error. Since there is only a single document in the collection, this is not a case where I don't have permission to some of the documents being queried, such as on this question.
My database structure looks like the following. It contains the collection being listed (main_collection), which has a single document (some_doc), which has a single subcollection (sub_collection), which has a single document (another_doc).
/main_collection/some_doc/sub_collection/another_doc
another_doc has one string field someFieldValue.
For this example, my query is of the entire collection, which is the single document. In my actual application it only queries the documents it expects to have access to, but the end result here is the same because I cannot filter against a document's subcollection from the client library.
firestore.collection('main_collection').get()
These are my security rules.
service cloud.firestore {
match /databases/{database}/documents {
match /main_collection/{mainColDoc} {
// This operation works
allow get: if subCollectionDocumentHasField('someFieldValue');
// This operation fails with permission denied
allow list: if subCollectionDocumentHasField('someFieldValue');
// This checks for the existence of a field on the subcollection's document
function subCollectionDocumentHasField(fieldName) {
return get(/databases/$(database)/documents/main_collection/$(mainColDoc)/sub_collection/another_doc).data.keys().hasAny([fieldName]);
//return get(/databases/$(database)/documents/main_collection/some_doc/sub_collection/another_doc).data.keys().hasAny([fieldName]);
}
}
}
}
The subCollectionDocumentHasField function checks for the existence of the someFieldValue field on the document another_doc. In this function, if I replace the $(mainColDoc) variable with the hard-coded document id some_doc, the list operation is successful. Since the $(database) path variable can be used in this context, I would expect that others could be as well.
Is this a bug or expected behavior?
This is actually the expected behavior, you can't use Firebase's rules to filter the results of your query.
A typical scenario would be to have collection of messages, where each message refers to its creator.
You can't simply add a rule where reading is only allowed on messages for which creator is the authenticated user, to filter automatically the messages of the current authenticated user.
The only way to go is to query with filter on the client side (or through a Cloud function).
The documentation is very clear about this :
When writing queries to retrieve documents, keep in mind that security rules are not filters—queries are all or nothing. To save you time and resources, Cloud Firestore evaluates a query against its potential result set instead of the actual field values for all of your documents. If a query could potentially return documents that the client does not have permission to read, the entire request fails.
From Firebase's documentation
I opened a ticket with Google and confirmed effectively what #José inferred from usage, which is that a security rule "is only checked once per query".
For clarification, while a security rule on a list operation will typically not query the contents of a document (to avoid potenitally-poor performance), there is at least one condition when it will query the contents of a document. This is when the security rule is guaranteed to return only one document. When this guarantee is met, the single document's contents will be queried because high performance can be maintained; the same as on a get operation.
So, in the linked example in my question where the list operation's rule is referencing a parent document, this guarantee is met and the parent document's contents will get queried.
Also, in my example where the list operation's rule is referencing a hard-coded document id, this guarantee is met and the hard-coded document's contents will get queried.
For the sake of stating it explicitly, for a list operation, in any case where Firestore cannot guarantee that its rule will only query a single document, access will be automatically denied, by design.
To reiterate what the other answers say, but stated in a slightly different way: The query must be consistent with the security rules, before any query documents are looked at, or it will fail with permission denied.
For example, if all of the documents in a sub-collection happen to match the security rule (e.g., your create and list rules both require the owner field is "X"), the query still must match the security rules (e.g., the query must also filter on owner is "X") or it will fail with a permission denied error, independent of the actual content of the sub-collection.

Firestore permissions and rules for querying collections (list) that belong to the logged-in user

I'm building a contact manager where a user has a bunch of "contacts" in their address book. I only want the user who created the contact to be able to query that contact. I wrote a query below that says what I want it to do, but the query does not work and I do not know why.
All contacts are created with an owner_id field that corresponds to the uid of the user that created the contact.
service cloud.firestore {
match /databases/{database}/documents {
match /contacts/{contactId} {
// only allow read for contacts if the current user is the owner
allow read: if request.auth.uid == resource.data.owner_id // <-- this does not work
allow write: if request.auth.uid != null && request.resource.data.owner_id != null;
}
}
}
When I run the query, I get nothing back, and the simulator does not allow me to run queries on list queries for the entire collection, only get for a single document. The query is simply:
db.collections('contacts')
I've also tried limiting using a where clause:
db.collections('contacts').where('owner_id', '==', <hard-coded-owner-id>)
I should note that when I query for a single document, the syntax above does appear to work. It just appears to fail when I query a collection.
So my question is, how does one write a database rule such that I can list all items in the collection while only returning the items that are associated with the logged-in user?
https://firebase.google.com/docs/firestore/security/get-started
I would expect your first query to fail because it's essentially trying to access documents that it doesn't have permission to read. Your rules will not implicitly filter the results.
I'd expect your second query to work because it's only accessing documents that are allowed by permissions. However, it will only work when the effective UID as reported by Firebase Authentication is the same as the one you hard coded. That's what you're rule is verifying - that the logged in user is only trying to read documents where they are present in owner_id. If you're working in the console simulator, you will have to turn on Authentication and put the right UID in the form.

Firestore security rule to check if character username already exists

I have following character collection structure in my database (firestore)
/characters/{uid}
- username: string
- clan: string
- mana: number
- health: number
etc...
I am trying to figure out a security rule for /characters/{uid} with following logic
service cloud.firestore {
match /databases/{database}/documents {
// Characters
match /characters/{characterID} {
allow create: if isValidUsername();
}
}
}
here function isValidUsername checks for various things like length, special characters etc... but one thing I can't figure out is how to check following inside of the function
Make sure that request.resource.data.username is unique i.e. not present inside any other document of /characters collection.
TL;DR: Enforcing uniqueness is only possible by creating an extra collection.
In your current structure, to know if a username is unique, you will need to read each document. This is incredibly inefficient, and on top of that it isn't possible in security rules, since they can only read a few documents per rule.
The trick is to create an extra collection usernames, where you also have a document for each user, but now the key/ID of each document is the username. With such a collection, you can check for the existence of a certain document, which is a primitive operation in the security rules.
Also see:
Prevent duplicate entries in Firestore rules not working

Resources