I have a collection of users, and I have a separate collection of usernames. In my collection usernames I store different usernames as doc_ids. That is, under collection usernames I can have doc_ids as first, second, third, and so on. Under each doc_id I store the following info:
{
ownerId: id,
dateUpdated: someDate
}
When I change some user's username, I execute a batch query, where I first delete the oldUsername doc, and then insert the newUsername doc with the appropriate fields. My question is regarding one of the security rules, related to the usernames collection. Do I need to check, if I already have such username (that is such doc_id). Do I need the following rule:
match /usernames/{username} {
allow create: if !exists(/databases/$(database)/documents/usernames/$(username))
}
I think this rule, is redundant since I am enforcing the uniqueness of collection ids, but I already saw it on a few other posts, so I wanted to check other people's opinions.
Yup, that rule does nothing as the create will only be triggered when the document doesn't exist yet. If the document already exists, its .update will be triggered.
This type of check is common in a .write, but not needed when you're using the more granular .create.
Related
db.collectionGroup('private')
.where('members', 'array-contains', userId)
.get()
.then(...)
This query fetches documents successfully if the relevant security rule is set like:
match /{path=**}/private/{document} {
allow read: if request.auth.uid in resource.data.members;
}
However, the similar rule below prevents the same query unexpectedly.
match /{path=**}/private/allowed {
allow read: if request.auth.uid in resource.data.members;
}
In this database,
private subcollections exist only under documents in the rooms collection.
Every private has only a single document with the ID "allowed".
This means /rooms/xxxxxxxx/private/allowed is the only possible path existing, where xxxxxxxx is an auto-assigned document ID.
Therefore specifying the path as /{path=**}/private/allowed looks correct to me.
In fact, "get" queries work in simulations in the playground, so is it a restriction only for collection group queries, or am I doing anything wrong?
FYI, more detailed database structure is described in another question of mine here.
Yes, it is required.
When you perform a collection group query, it's not possible to call out a specific document id in the query (e.g. "allowed"). The query is explicitly asking to consider all of the documents in all of the subcollections of the given name ("private"). Therefore, the rules must allow for those documents to be considered by adding the trailing wildcard.
You can certainly add a filter to the query if you want to get only certain documents with certain field values, but that filter can't be enforced in the rules.
Based on this SO answer I came to know that firestore does not have collection level locking in a transaction.
In my case, I have to ensure that the username field in users collection is unique before I write to a collection.
For that, I write a transaction that does this:
Executes a query on users collection to check if a document exists where username=something
If it does exist, fail and return error from transaction
If it does not exist, just run the write operation for the userId I want to update/create.
Now the issue here is that if two clients simultaneously try to run this transaction, both might query the collection and since the collection is not locked, one client might insert/update a document in collection while other won't see it.
Is my assumption correct? And if yes, then how to deal with such scenarios?
What you're trying to do is actually not possible to do atomically, as it's not possible to transact safely on a document that you can't identify with an ID. The problem here is that a transaction is only "safe" if you can get() the specific document to add or modify. Since you can't get() a document using a field value in the document, you're at a loss.
If you want to ensure uniqueness of anything in Firestore, that uniqueness will need to be coded into the document ID itself. In the simplest case, you can use the username as the ID of a document in a new collection. If you do that, your transaction can simply get() the required document by username, check to see if it exists, then write the document if it doesn't. Else, the transaction can fail.
Bear in mind that because there are limitations to document IDs in Firestore, you might need to escape or encode that username if your usernames could possibly violate the rules.
An alternative to coding this data into the doc id is to use a separate collection as a sort of manual index. Security rules can then enforce uniqueness on the index. So something like this:
/docs/${documentId} => {uniqueField: "foo", ...}
/docmap/${uniqueField} => {docId: "doc2"}
The idea here is that one must first write the docmap entry containing the new doc id before they are allowed to writet he doc. Since the docmap is keyed on our unique field, it enforces uniqueness.
Security rules would look roughly like so:
function getPath(childPath) {
return path('/databases/'+database+'/documents/'+childPath)
}
// we can only write to our doc if the unique field exists in docmap/
// and matches our doc id
match /docs/{docid} {
let docMapPath = 'docmap/' + request.resource.data.uniqueField;
allow write: if getData(docMapPath).docId == docId;
//todo validate data schema
}
// It is only possible to add a uniqueField to the docmap
// if it doesn't already exist for another doc
// we also validate that the doc id matches our schema
match /docmap/{uniqueField} {
allow write: if resource.data.size() == 0 &&
request.resource.data.docId is string &&
request.resource.data.docId.size() < 100
}
And a write would look roughly like so:
const db = firebase.firestore();
db.doc('docmap/foo').set('doc2')
.then(() => db.doc('docs/doc2').set({uniqueField: 'foo'})
.then(doc => console.log("success"))
.catch(e => console.error(e));
You could also do this in a transaction or even a batch operation to make it atomic, but it's probably not necessary to add complexity to the process; the security rules will enforce the constraints.
I have few questions about collection group:
Is there way to execute request for collection group in Firestore simulator?
Can I add additional parameter for collection group rules for example the following rule is used for collection group
match /{prefix=**}/access/{email} {
allow read: if isSignedIn();
}
before access collection i have one more collection with user id, is is possible to add it as parameter to do some validations?
No, there currently is no way to simulate a collection group query in the Firestore console. (Actually there is no querying at all except individual document gets.)
There is no way, using security rules, to know any of the other path elements that come before access in the case you're showing. The prefix wildcard actually will not even contain any data at the time of execution.
I'm facing insufficient permissions for this firestore security check.
service cloud.firestore
{
match /databases/{database}/documents
{
match /events/{eventID}
{
allow read:
if get(/databases/$(database)/documents/events/$(eventID)/authorizations/$(request.auth.uid)).data.EVENT_READ == true;
}
}
}
the get document is hardcoded in the firestore database and the simulator returns true but for the actual query returns insufficient privileges.
I tested and moved the authorizations subcollection to the same level as users collection and it works. Am i missing out anything?
Additional testing: Reading the document directly does not result in insufficient privileges. I'm testing to see if it's an issue with listing but to my knowledge read should cover both get and list in firestore security rules.
Update: Listing seems to be the issue here. I tried to list the entire collection with only one document and it results in the insufficient privileges.
Works:
this.angularFirestore.collection('events').doc(eventID).valueChanges();
Doesn't work (updated):
this.angularFirestore.collection('events', query => query.where('admins', 'array-contains', auth.uid)).valueChanges()
My firestore database:
/events/event1_id
- field 1: some string
- field 2: some string
- admins: array of uid strings
/authorizations/<uid> #uid for $(request.auth.uid)
- EVENT_READ: true
Update 2: Updated the doesn't work query string which I tried out. It is intriguing that if i move the /authorizations sub collection out to be the same level as /events collections, the query will not fail.
Your first query works because it's accessing the events collection with a specific document. That specific document meets the rules criteria, because you've arranged for the get() to allow it.
Your second query doesn't work because it's attempting to get all of the documents in events collection. Your rule does not specifically allow that. It looks like you expect your rule to filter out the events that aren't allowed access based on the contents an unknown number of other documents. You need to be aware that security rules are not filters. Please click through to the documentation and read that section. The client must only request documents that are known to be readable according to rules. It can't depend on rules to filter documents that are not allowed.
If you want to be able to query for all events that the current user has access to, your current database structure will not work. You will need to put all the relevant information in the events collection itself. This means you should consider something like putting the UID of each user that's allowed to read the event in the document itself, then filter on that field. Or have some other collection that you can query in this way.
I have a collection of users with the following schema:
{
_id:ObjectId("123...."),
name:"user_name",
field1:"field1 value",
field2:"field2 value",
etc...
}
The users are looked up by the user.name, which must be unique. When a new user is added, I first perform a search and if no such user is found, I add the new user document to the collection. The operations of searching for the user and adding a new user, if not found, are not atomic, so it's possible, when multiple application servers are connect to the DB server, for two add_user requests to be received at the same time with the same user name, resulting in no such user being found for both add_user requests, which in turn results with two documents having the same "user.name". In fact this happened (due to a bug on the client) with just a single app server running NodeJS and using Async library.
I was thinking of using findAndModify, but that doesn't work, since I'm not simply updating a field (that exists or doesn't exist) of a document that already exists and can use upsert, but want to insert a new document only if the search criteria fails. I can't make the query to be not equal to "user.name", since it will find other users.
First of all, you should maintain a unique index on the name field of the users collection. This can be specified in the schema if you are using Mongoose or by using the statement:
collection.ensureIndex('name', {unique: true}, callback);
This will make sure that the name field remains unique and will solve the problem of concurrent requests as you have specified in your question. You do not require searching when this index is set.