Firebase Firestore: custom admin access - firebase

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();
}
}
}

Related

Firebase - Rule always denies requests

I am starting to restrict the access to my Firebase Database. So far I have three collections where I simply want to return true for testing purposes.
I did it like this
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /devices/{device} {
allow read, write: if true;
}
match /users/{user} {
allow read, write: if true;
}
match /groups/{group} {
allow read, write: if true;
}
}
}
When I try to test this, I can't access the data no matter what. It gets always denied.
Your rules are correct, I see the issue is how you are using the playground,
In the textbox just enter something like
/devices/yourdeviceId
When you use the console simulator to test your rules, the field for the document to test should only contain the path of the document to get, using only the names of specific collections and document IDs. If you're trying to test document "foo" in collection "devices", then your path should be "/devices/foo". Don't enter the full match path, and don't use any wildcards - you should refere to an actual document in the same way that you would refer to it in your app code.

Firestore Security rules match array [duplicate]

First, sorry for my terrible English, it is not my native language...
I am building a simple app in Firebase, using the Firestore database. In my app, users are members of small groups. They have access to other users' data.
In order not to query too many documents (one per user, in a subcollection of the group's document), I have chosen to add the users' data in an array inside the group's document.
Here is my group's document:
{
"name":"fefefefe",
"days":[false,false,false,false,true],
"members":[
{"email":"eee#ff.com","id":"aaaaaaaa","name":"Mavireck"},
{"email":"eee2#ff.com","id":"bbbbbbbb","name":"Mavireck2"},
],
}
How can I check with the security rules if a user is in a group ?
Should I use an object instead ?
I'd really prefer not use a subcollection for users, because I would reach the free quota's limits too quickly...
Thank you for your time !
EDIT:
Thanks for the answer. I will change it to an object :
"Members": { uid1 : {}, uid2 : {} }
In general, you need to write a rule like the following:
service cloud.firestore {
match /databases/{database}/documents {
match /collection/{documentId} {
// works if `members` = [uid1, uid2, uid3]
// no way to iterate over a collection and check members
allow read: if request.auth.uid in resource.data.members;
// you could also have `members` = {uid1: {}, uid2: {}}
allow read: if resource.data.members[request.auth.uid] != null;
}
}
}
You could also use subcollections:
service cloud.firestore {
match /databases/{database}/documents {
// Allow a user to read a message if the user is in the room
match /rooms/{roomId} {
match /documents/{documentId} {
allow read: if exists(/databases/$(database)/documents/documents/$(documentId)/users/$(request.auth.uid));
}
match /users/{userId} {
// rules to allow users to operate on a document
}
}
}
}
I made it happen with this code
Allow some user to read/write some document of a collection if this same user is present into an array of another collection
service cloud.firestore {
match /databases/{database}/documents {
match /repositories/{accountId} {
allow read, write: if request.auth.uid in get(/databases/$(database)/documents/accounts/$(accountId)).data.users
}
}
}
Just offering an alternative solution. In my case I store two separate fields. In your case it would be:
"membersSummary":[
{"email":"eee#ff.com","id":"aaaaaaaa","name":"Mavireck"},
{"email":"eee2#ff.com","id":"bbbbbbbb","name":"Mavireck2"},
],
"members": ["aaaaaaaa", "bbbbbbbb"]
I'm aware that this is not necessarily optimal but as we're using firebase I assume we're ok with using denormalised data in our documents.
I'd use the members field for collection queries and firestore rules (allow read: if request.auth.uid in resource.data.members; as per Mike's answer above), and the membersSummary for rendering the info in the UI or using the additional fields for other types of processing.
If you use uids as keys then if you wanted to query a collection and list all the documents for which that user is a member, and order them by name, then firebase would need a separate composite index for each uid, which unless you have a fixed set of users (highly unlikely) would basically result in your app breaking.
I really don't like the idea of extra document reads just for access control but if you prefer that approach to tracking two separate related fields then do that. There's no perfect solution - just offering another possibility with its own pros and cons.

How do rules cascade in Cloud Firestore?

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

Blocking overwriting of objects in Firestore

In Firestore I have a collection with several documents, with structure like
users: [
1234: {
foo: 'bar'
}
]
In old version of our app (that we've since fixed, but there are still many users using it), there's a piece of code executing
firestoreInstance.collection('users').doc(1234).set({});
because of an old bug.
It's overwriting the whole object, so it becomes
users: [
1234: {
}
]
I've tried blocking this specific overwrite with following security rules:
service cloud.firestore {
match /databases/{database}/documents {
function objectIsEmpty() {
return request.resource.data.size() == 0;
}
match /users/{userId=**} {
allow read;
allow write: if !objectIsNotEmpty();
}
}
}
but the overwrites are still happening. I've tried the same thing with
allow write: if false; and it didn't help.
According to Customize Cloud Firestore Security Rules:
There are three operations for writes in Cloud Firestore: create, update, and delete. These correspond to the set(), add(), update(), remove(), and transaction() methods in the client libraries. For your convenience the write operation allows all of these.
I imagined not allowing write with empty object (by disallowing writes with request.resource.data.size() == 0) would not allow the set({}) code from being executed.
What's curious, same piece of code, only with update instead of set is blocked properly and the object isn't overwritten.
Is there something wrong with my rule? Is there another way to block any write operation with an empty object?

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