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
Related
In a Firestore security rule, I'm trying to check if the user attempting to create/delete a document in one collection is also the owner of that object, as marked in another collection. Basically, my data looks something like this:
In the users collection, each user has a document like this:
{
name: 'john',
userItems: [
{
id: 'random-id',
...
},
...
],
...
}
In the items collection (which I am writing the rule for), all of the items from all of the users of the platform are there, and have Firestore IDs which correspond to the id keys in the elements of the items list of their owners. So if john created an item with the id random-id, his user document would look like the above, and there would be a new document in the items collection with the Firestore ID of random-id.
What I am trying to achieve is to create a security rule wherein a document in the items collection can only be updated if the user document of the currently authed user, which I can access with get(/databases/$(database)/documents/users/$(request.auth.uid)), has an element in their userItems list which has the id key equal to request.resource.id. If this were normal JS, I'd probably do something like:
match /items/{item} {
allow write: if get(/databases/$(database)/documents/users/$(request.auth.uid))
.data
.userItems
.some(userItem =>
userItem.id === request.resource.id
)
}
However, the Firestore List interface is very lacklustre, and doesn't support fancy operations like .some, or even basic/manual looping (as far as I'm aware). I've been trying to come up with some clever way around this, but even .joining the list and .matching the resulting string with some fancy RegExp wouldn't work, since I'm pretty sure that maps would parse as '[object Object]' instead of stringifying properly.
Is there any way to do this using standard Firestore rules, without reconfiguring my DB structure?
What you're trying to do isn't possible with security rules. You will need to either change that way you represent your data (which I recommend, as a list is probably not the best representation), or add more data to satisfy your requirements.
If the the random-id is unique in the list, you should consider using a map instead of a list to represent it, so that you can do simple lookups on the Map that becomes available in rules. If your userItems field was a map indexed by that ID, you could instead say:
allow write: if get(...).data.userItems.keys().hasAny([request.resource.id]);
If for some reason you can't change the field, you will need to duplicate the IDs into a new list field and check it like this:
allow write: if get(...).data.userItemIds.hasAny([request.resource.id]);
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.
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
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.
I'm trying to implement something similar to "Working with Arrays, Lists, and Sets" example in Firestore's document, but for user access control list.
In a document, there'll be a modified field of a timestamp, and a users field of a map, containing user's UID as a key and the modified timestamp as value (so that I can sort using this field).
{
modified: 2018-01-01T17:05:00Z,
users: {
someUID: 2018-01-01T17:05:00Z,
otherUID: 2018-01-01T17:05:00Z
/* ... */
}
}
It's obvious that I have to keep the values inside users map in sync with the modified field. As I don't want to pay a cloud function call and a document write whenever the document is updated, I plan to do the update on the client, at the same time the document itself is updated.
The question is, how can I enforce, using Firestore security rules, the values of users map so that it'll always be in sync with the modified field? The keys of this map isn't known in advance, and the size of this map can be variable, too.
You can use writeFields for update rules if map is variable or keys are unknown while updating.
allow update: if request.auth != null
&& request.resource.data.users is map
&& request.writeFields.size() == 1
&& (('users.' + request.auth.uid) in request.writeFields)
&& request.resource.data.users[request.auth.uid] <= request.time.time();