Firestore security rule to check if character username already exists - firebase

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

Related

How to make certain data fields readable in a Firestore document

My Question
Is it possible to create firebase rules such that the user can read certain data fields in a document but not read other rules in the same document?
I'm asking this question because I want to enable users to query data from documents but only certain fields in the document.
For example
Assume that you have the following data in a story-document:
/stories/{storyid}
{
title: "A Great Story",
content: "Once upon a time...",
author: "some_auth_id",
published: false
}
In the example below I have attempted to create a rule such that users can read/query the author of a document (but not other fields of a document - for example title).
Notice that I have added /stories/{storyid}/{author}
service cloud.firestore {
match /databases/{database}/documents {
match /stories/{storyid}/{author} {
allow read: if request.auth != null;
}
}
}
How would I go about to create such a rule?
I guess that you mean "Is it possible to create firebase rules such that the user can read certain data fields in a document but not read other fields in the same document?"
This is actually not possible: Firestore Security Rules apply at the level of the document.
One classical solution is to duplicate the documents in another collection with only the subset of the fields you want to make readable.

Filter/some on list of objects in a Firestore security rule

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]);

Firestore unwanted saving of repeated entries

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.

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.

firebase rule for unique property in firestore

I've found that question relatively often asked here, but i still cant figure out how to manage a rule for unique properties. I have following document datamodel:
users/{usereId}/Object
users/usernames/Object
The first Object contains basic information about the user, like:
{
email: "example#hotmail.edu"
photoURL: null
providerId: null
role: "admin"
username:"hello_world"
}
meanwhile the usernames objects only contains the username as the property key and the uid as the value, for instance:
{
hello_world:"F3YAm8ynF1fXaurezxaQTg8RzMB3"
}
I set it up this way, because I want that every user has a unique username. And its less time consuming iterating through the second object than through the first ones.
But back to my issue. I need that hello_world is unique within the write operation. But my rules so far does not work. I have:
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth.uid != null
}
match /users/{userID} {
allow create: if !exists(/databases/$(database)/documents/users/$(request.resource.data.username)) <== does not apply
}
}
}
The second match is, what should apply the unique property rule. Has anyone an idea how to set the rule correctly?
In the console the object model looks as follows
You created a document called usernames in your users collection, to track the names that are in use. But your rules are trying to find a document named after the current user's name, which will never exist in this structure.
In your current structure, you will need something like this:
allow create: if get(/databases/$(database)/documents/users/usernames).data[$(request.resource.data.username)] == request.auth.uid
So the above gets the document where you keep all the user names, and then checks if the current user name is in their for the current UID.
An alternative approach is to keep an additional colllection of all user names, and then make each document in there map a single user names to a UID. In this scenario your /usernames collection would be top-level, since it's shared between all users. The rules for this would be closer to what you currently have:
allow create: if !exists(/databases/$(database)/documents/usernames/$(request.resource.data.username))
Since your usersnames have to be unique, wouldn't it be an option to use their names as the document key?

Resources