How do rules cascade in Cloud Firestore? - firebase

I'm trying to learn to use Google Cloud Firestore for storing and securing some simple data, so I started writing some basic rules to verify that the data passed from the API is reasonable.
My initial thinking was that each rule would be evaluated and if any one failed it would fail the request, but I'm finding that requests and don't match a rule are still succeeding. Can someone explain how to create progressively stronger security rules for sub collections?
Here is my current ruleset:
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read;
allow write: if request.auth.uid != null;
}
match /projects {
allow write: if resource.data.keys().hasAll(['title', 'description']);
}
}
}

In Firestore Rules, if any allow grants the request, then the request is permitted. The allow statements applied to any given request are all match blocks that match the resource name.
Since the match /{document=**} pattern overlaps with the match /projects pattern, it will be possible to write to the projects document simply by being authenticated e.g. request.auth.uid != null. This was probably not what was intended.
The match /projects is fixed length match against /databases/*/documents/projects, whereas the match /{document=**} will match any document name that starts with /databases/*/documents. The presence of the ** indicates zero or more additional paths.
In general, it is good practice to avoid overlaps in match patterns. If you need to write a rule which matches most things but carves out an exception for a specific path, it would need to be as follows:
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read;
// allow writes to anything except the 'projects' document.
allow write: if request.auth.uid != null
&& /databases/$(database)/documents/$(document)
!= /databases/$(database)/documents/projects
}
match /projects {
// allow _authenticated_ writes to the projects document if they
// have the proper form.
allow write: if request.auth.uid != null
&& resource.data.keys().hasAll(['title', 'description']);
}
}
}

I implement rules as follows:
Generic or Admin role rules match and execute first and thus fail first (if user is not an admin)
More Specific or Public role rules match and execute
Only if both 1 and 2 are false does the rule fail.
Once a rule returns true, you can't fail it or make it false again because of another match.
Note: Place your matches in order of most likely to return true most of the times in order to save on unnecessary charges.
Also try to create functions that you can reuse.
Here is one of the best resources I have found.
https://www.fullstackfirebase.com/cloud-firestore/security-rules

Related

How to have different rule for a specific collection in firebase firestore

I have a firestore db, for all collections I want to have below rule
allow read : if request.auth.uid != null ;
allow write: if request.auth.uid != null ;
except for service-account collection, which I don't want anybody has access (only firebase functions since they are running under admin service account) so I updated my rules to
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read : if request.auth.uid != null ;
allow write: if request.auth.uid != null ;
}
match /service-account/{serviceAccount} {
allow read: if false;
allow write: if false;
}
}
}
Then I tried to test it via Rules Play Ground, rules are teken into account but result is not correct
So my rule correctly ban access, but the first rule which is going to be applied for all, seems to allow read. I changed the order and no diffrenece.
How can I fix this.
The problem is that this rule is unconditionally applied to every single document in your database:
match /{document=**} {
allow read : if request.auth.uid != null ;
allow write: if request.auth.uid != null ;
}
With this in place, all documents are readable and writable by all users. You cannot override this with another rule. Once a document is deemed readable by any rule, that can't be changed.
See the documentation for overlapping match statements.
What you will need to do instead is call out each individual collection by its name (except service-account), and apply the permissions to them individually.

How to only allow access if all matches are true in firestore

In firestore we can set our own security rules. I have added 2 security rules where i gave all crud operations access in 1st rule and deny delete access to all documents of a specific collection in 2nd rule. But due to the true condition of 1st rule, 2nd rule's condition is neglected as it is already mentioned in the document that for any true condition access is granted.
Is there any way that i can make in AND instead of OR.
Below are my rules for reference:
service cloud.firestore {
match /databases/{database}/documents {
function isAuthenticated(){
return request.auth.uid != null;
}
match /{collection}/{document = **}{
allow get, update, delete, create: if isAuthenticated()
}
match /records/{record} {
allow delete: if false
}
}
}
No all rules are OR so as soon it resolves to true on the the query, update etc it
will be processed.
It wouldn't be good practice to allow full crud to any user as they can just purge your database. Even if they are a good actor it can still accidentally happen with the wrong code.

Configuring rules for Firestore so user only gets records they own

This is a followup to this question
Firestore permissions
I'm trying to set rules on my firestore
service cloud.firestore {
match /databases/{database}/documents {
match /analysis/{analysis} {
allow read, write: if request.auth.uid == resource.data.owner_uid;
}
}
}
My goal is
a. When doing a list operation only those documents belonging to a user are returned
b. only documents a user owns can be read or written by that user.
With the above configuration b. is accomplished.
how do I do accomplish a. ?
Remember that firestore rules are not filters, they're a server-side validation of your queries.
You should always make your queries match your rules, or else you'll get permission errors.
In your case you already made the rule to enforce reading/listing on user owned documents. Now you simply have to make the corresponding query with the right filters :
const userId = firebase.auth().currentUser.uid
db.collection("analysis").where("owner_uid", "==", userId)
Another thing.
With your current rules, your users won't be able to create a new document, only edit an existing one, here are the updated rules to allow that :
allow read: if request.auth.uid == resource.data.owner_uid;
allow write: if request.auth.uid == resource.data.owner_uid
|| request.auth.uid == request.resource.data.owner_uid;

Firebase Firestore: custom admin access

In Firebase Firestore, I'm trying to allow only (custom-assigned) admins to write/update/delete resources, and for that I've got these security rules:
service cloud.firestore {
match /databases/{database}/documents {
match /resources {
allow read;
allow write, update, delete: if get(/users/$(request.auth.uid).isAdmin);
}
match /resources/{resource} {
allow read;
allow write, update, delete: if get(/users/$(request.auth.uid).isAdmin);
}
}
}
I'm signing in with the user that is marked as an admin in the users collection:
NfwIQAjfNdS85yDvd5yPVDyMTUj2 is the UID gotten from the Authentication pane:
However, for some reason (UPDATE: reasons identified; see answer), I'm getting PERMISSION_DENIED errors when writing to the resources collection after being absolutely sure I'm signed in with the admin user.
Perhaps it is possible to view request logs from Firestore? Then I could have a look at what request.auth.uid looks like to match it up with my collections and rules.
While writing my question, I made it work! I made two mistakes, both of which could have been avoided if I read the docs properly.
Firstly, all calls to the service-defined function get needs to prefix the path with /databases/$(database)/documents/. So that this rule:
allow write: if get(/users/$(request.auth.uid)).isAdmin;
becomes this:
allow write: if get(/databases/$(database)/documents/users/$(request.auth.uid)).isAdmin;
It's long, I know, but that's how it is. I'm not sure why Firestore isn't able to do that by itself, though, seeing as that same path prefix will stay the same across all calls to get, but perhaps this is for some future feature that isn't ready yet, like cross-database querying or something.
Second, the get function will return a resource, which in turn you'll need to call .data on to get the actual data that it contains. Thus, instead of doing this:
get(/path/to/user/).isAdmin
you'll need to do this:
get(/path/to/user/).data.isAdmin
Now I just wish I was able to extract that logic into a user-defined function:
function isAdmin() {
return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.isAdmin;
}
But doing so results in a PERMISSION_DENIED again, and without knowing what's actually going on in the function, I'm not sure if I'll spend more time trying to figure this out now.
UPDATE: #Hareesh pointed out that functions must be defined within the scope of a matcher, so it's possible to put the function in the default top-level matcher like this:
service cloud.firestore {
match /databases/{database}/documents {
function isAdmin() {
return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.isAdmin == true;
}
// ...
}
}
Some points i noticed
match /resources is pointing to a collection, that rules has no effect on its documents. here i am quoting from the doc
Rules for collections don't apply to documents within that collection. It's unusual (and probably an error) to have a security rule that is written at the collection level instead of the document level.
so you don't have to write rules for collections
Then in the rules allow write, update, delete: you can say either allow write: or specifically allow create, update, delete: any of the three options or combine them.
try this
service cloud.firestore {
match /databases/{database}/documents {
match /resources/{resource} {
function isAdmin() {
return get(/databases/$(database)/documents/users/$(request.auth.uid)).isAdmin ||
get(/databases/$(database)/documents/users/$(request.auth.uid)).data.isAdmin;
}
allow read;
allow create, update, delete: if isAdmin();
}
}
}

Recursive wildcards in Firestore security rules not working as expected

I have a data structure like this (Collections and Documents rather than JSON of course but you get the idea):
{
users: {
user1:{
name: Alice,
groups: {
groupA:{subbed:true},
groupB:{subbed:true}
}
},
user2:{
name: Bob,
groups: {
groupC:{subbed:true},
groupD:{subbed:true}
}
}
}
}
Basically this is registered users IDs and the group IDs that each user is subscribed to. I wanted to write a security rule allowing access to a users profile and sub-collections only if they are the current auth user and, based on my reading of the docs, I thought that a wildcard would achieve this...
match /users/{user=**}{
allow read,write: if user == request.auth.uid;
}
With this in place I can read the user document fine but I get a permissions error when I try and read the groups sub-collection. I can only make it work by matching the sub-collection explicitly...
match /appUsers/{user}{
allow read,write: if user == request.auth.uid;
match /groups/{group}{
allow read,write: if user == request.auth.uid;
}
}
...so my question is, what is the difference between the two examples and what am I misunderstanding about the recursive wildcards? I thought that the {user=**} part of the first example should grant access to the user document and all its sub-collections, sub-sub-collections etc etc ad infinitum (for the authorised user) and should remove the need to write rules specifically for data stored lower down as I have had to do in the second example.
I've only been messing around with Firestore for a short time so this could be a real dumb question :)
Thanks all
The firebase docs are a bit confusing when it comes to using the recursive while card. What I found in testing was that I needed to set two rules to give a user permission to write to the users document and all sub collections (and their sub documents) which is the most logical setup for managing user data.
You must set two rules.
Give user permission to the /users/{userId} document
Give user permission to all sub collections and their sub documents that begin at the /users/{userId} path.
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth.uid == userId;
}
match /users/{userId}/{document=**} {
allow read, write: if request.auth.uid == userId;
}
}
}
Rules
Sorry about including the images. I couldn't get SO to format them correctly.
I think the problem is that, while you are indeed using the subcollections wildcard =**, you are then allowing permissions only if user == request.auth.uid, so this is what happens (pseudocode):
(when accessing users/aHt3vGtyggD5fgGHJ)
user = 'aHt3vGtyggD5fgGHJ'
user == request.auth.uid? Yes
allow access
(when accessing users/aHt3vGtyggD5fgGHJ/groups/h1s5GDS53)
user = 'aHt3vGtyggD5fgGHJ/groups/h1s5GDS53'
user == request.auth.uid? No
deny access
You have two options: either you do as you've done and explicitly match the subcollection, or use this:
function checkAuthorization(usr) {
return usr.split('/')[0] == request.auth.uid;
}
match /users/{user=**}{
allow read,write: if checkAuthorization(user);
}
(the function must be inside your match /databases/{database}/documents, like your rule)
Let me know if this works :)
Security rules now has version 2.
match/cities/{city}/{document=**} matches documents in any
subcollections as well as documents in the cities collection.
You must opt-in to version 2 by adding rules_version = '2'; at the top
of your security rules.
Recursive wildcards (version 2).
This is what works for me:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Matches any document in the cities collection as well as any document
// in a subcollection.
match /cities/{city}/{document=**} {
allow read, write: if <condition>;
}
}
}

Resources