Firebase query, rule for collectiongroup query getting in the way - firebase

Just when I thought I had the hang of it, the query rules throws me a curve ball :(
I have this query rule:
// Needed for collection group (Member) query
// https://firebase.googleblog.com/2019/06/understanding-collection-group-queries.html
match /{rootPath=**}/Members/{member} {
allow read: if request.auth != null;
}
It's pretty basic, only needs an authorized user. collectiongroup query works perfectly as expected.
Now, I want to have another query just to get member documents:
Firebase.firestore.collection("Companies\\$companyID\\Members").get().await()
The query returns an error (PERMISSION_DENIED).
I also tried adding a rule just for members like this:
match /Companies/{companyID} {
allow read: if request.auth != null &&
isMember(database, companyID, request.auth.uid)
match /Members/{member} {
allow read: if request.auth != null
}
}
Still, the same error.
This is the document path:
I looked at a few resources, but I didn't see anything to suggest a solution:
Understanding Collection Group Queries in Cloud Firestore
Recursive wildcards

I am posting this as an answer, as it is too long for comment.
Have you tried the following example rule:
service cloud.firestore {
match /databases/{database}/documents {
match /Companies/{companyID}/Members/{member} {
allow read, write: if <condition>;
}
}
}
as mentioned earlier in the documentation you shared based on structuring rules with hierarchical data?

I would recommend that you have a look at the following documentation where you can find some examples:
service cloud.firestore {
match /databases/{database}/documents {
match /Companies/{companyID}/Members/{memberID} {
// Only authenticated users can read
allow read: if request.auth != null;
}
}
}
Using the above security rules, any authenticated user can retrieve the members of any single company:
db.collection("Companies/{companyID}/Members").get()
Now , if you would like to have the same security rules applied to collection group queries, you must use version 2:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Authenticated users can query the posts collection group
// Applies to collection queries, collection group queries, and
// single document retrievals
match /{path=**}/Members/{memberID} {
allow read: if request.auth != null;
}
}
}
Any authenticated user can retrieve the members of any single company:
But what if you want to show a certain user their members across all companies? You can use a collection group query to retrieve results from all members collections:
var user = firebase.auth().currentUser;
db.collectionGroup("members").where("author", "==", user.uid).get()
Note: This query requires will require a composite index for the members collection. If you haven't enabled this index, the query will return an error link you can follow to create the required index.

You can try using "match /{path=**}/Members/{member}" instead of rootPath. I have not used the latter but the former worked for me in other projects.

Related

Firestore security rule to cover parent documents and nested collection based on array field

I've got a top-level collection of documents, each of which has an array field containing the email addresses of the users that are permitted to view the document in question (there's a reason in this case that it's the email addresses, not the UIDs).
Additionally, I have a custom claim set up on some users that mark them as admin who should have read/write access to everything.
The following security rule works well for the top level:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /accounts/{account} {
function allowRead() {
return resource.data.users.hasAny([request.auth.token.email]);
}
allow read: if allowRead();
allow read, write: if request.auth.token.admin == true;
}
}
}
I want the rule to check the 'users' array field when requesting documents in a collectionGroup query across nested collections called 'projects' and block access if a nested document is requested for which there is no match to the 'users' array field in the parent.
I get that the allowRead() funtion won't work as is, even if I add a recursive wildcard after match /accounts/{account} like /accounts/{account}/{document=**} since the definition of resource.data will be different at that level and won't return true for the allowRead() function.
I know that Firestore rules don't really support an operation like 'check foo in parent' when querying, so I was hoping for some advice on the best way to approach this requirement.
The collectionGroup query to the nested collections is
this.firestore.collectionGroup('projects', (ref) => ref.where('accountId','in',accountIds)).get()
in which 'accountIds' is a given array of the ids of the parent account, which is stored in each document in the projects collection for ease of querying with collectionGroup.
I don't really want to move the nested collection to top level because then I'd have to store the users array in each document as well as the reference to the account to which it belongs, but perhaps that's the best way to do this?
Or am I thinking about this whole thing wrong? I'm fairly new to Firebase.
I've solved this with the following:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /accounts/{account} {
function allowRead() {
return resource.data.users.hasAny([request.auth.token.email]);
}
allow read,write: if request.auth.token.admin == true;
allow read: if allowRead();
}
match /{path=**}/projects/{project} {
allow read,write: if request.auth.token.admin == true;
allow read: if get(/databases/$(database)/documents/accounts/$(resource.data.accountId)).data.users.hasAny([request.auth.token.email]);
}
}
}
The only issue is that as far as I can tell, this is going to double the amount of billable queries because there's an extra 'get' made any time the nested collection is queried.
Any thoughts from the community on better ways to do this would be welcome!

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.

Firestore security rules for specific document

I am trying to apply the following situation :
all authenticated users have read and write access to the database except for admin document.
Admin document is accessible only for him for read and write.
My rules:
service cloud.firestore {
match /databases/{database}/documents {
//Functions
function isAuthenticated(){
return request.auth != null;
}
function isAdministrator(){
return request.auth != null && request.auth.token.name == resource.data.oid;
}
//Administrator Identity Check Point
match /admin/identity {
allow read, write: if isAdministrator();
}
//Allow Reads and Writes for All Authenticated Users
match /{document=**}{
allow read, write: if isAuthenticated();
}
}//databases/{database}/documents
}//cloud.firestore
Is there any way i can achieve this, actually when testing these rules, the tests succeed because only isAuthenticated() is being called because of the tag /{document=**}. I also tried /{document!=/admin/identity} but it does not work.
How can I write a security rule that follow this model ?
Maybe on your default user rule you could check if the collection isn't admin, something like this:
//Allow Reads and Writes for All Authenticated Users
match /{collection}/{document=**}{
allow read, write: if (isAuthenticated() && collection != "admin") || isAdministrator();
}
Since June 17, Firebase has provided new improvements to Firestore Security Rules.
Firebase blog - 2020/06 - New Firestore Security Rules features
New Map methods
We'll use Map.get() to get the "roleToEdit" field. If the document doesn't have the field, it will default to the "admin" role. Then we'll compare that to the role that's on the user's custom claims:
allow update, delete: if resource.data.get("roleToEdit", "admin") == request.auth.token.role;
Local variables
Say you're commonly checking that a user meets the same three conditions before granting access: that they're an owner of the product or an admin user.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
function privilegedAccess(uid, product) {
let adminDatabasePath = /databases/$(database)/documents/admins/$(uid);
let userDatabasePath = /databases/$(database)/documents/users/$(uid);
let ownerDatabasePath = /databases/$(database)/documents/$(product)/owner/$(uid);
let isOwnerOrAdmin = exists(adminDatabasePath) || exists(ownerDatabasePath);
let meetsChallenge = get(userDatabasePath).data.get("passChallenge", false) == true;
let meetsKarmaThreshold = get(userDatabasePath).get("karma", 1) > 5;
return isOwnerOrAdmin && meetsChallenge && meetsKarmaThreshold;
}
match /products/{product} {
allow read: if true;
allow write: if privilegedAccess();
}
match /categories/{category} {
allow read: if true;
allow write: if privilegedAccess();
}
match /brands/{brand} {
allow read, write: if privilegedAccess();
}
}
}
The same conditions grant access to write to documents in the three different collections.
Ternary operator
This is the first time we've introduced an if/else control flow, and we hope it will make rules smoother and more powerful.
Here's an example of using a ternary operator to specify complex conditions for a write.
A user can update a document in two cases: first, if they're an admin user, they need to either set the field overrideReason or approvedBy. Second, if they're not an admin user, then the update must include all the required fields:
allow update: if isAdminUser(request.auth.uid) ?
request.resource.data.keys().toSet().hasAny(["overrideReason", "approvedBy"]) :
request.resource.data.keys().toSet().hasAll(["all", "the", "required", "fields"])
It was possible to express this before the ternary, but this is a much more concise expression. ;)

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

Firestore rules not working for userId

I'm having a simple collection users, document id is using the uid with name string
I'm using angularfire2 to connect to the collection
this.users = afs.collection<User>('users').valueChanges();
I'm using firebase authentication. When displaying the user id, I get 4NxoeUeB4OXverhHhiw86eKl0Sj1 which match with the user document.
this.afAuth.authState.subscribe(auth => console.log(auth.uid));
I'm trying to add rules for the collection. If I use the rules below, I get error Missing or insufficient permissions.
`service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth.uid == userId;
}
}
}`
If I change the rule to request.auth != null the data is shown.
`service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, write: if request.auth != null;
}
}
}`
What could I have done wrong? I'm also trying a few different rules eg. resource.data.name != null. It is also not working.
I am testing the rule. The end result I want a readers array field and writers array field so I can try something like request.auth.uid in resource.data.readers to control access to the document
You are trying to get /users/* but you only have access to /users/{yourid}.
You should only retrieve data you have access to, so you need to filter your query even if you actually only have one document.
// either by adding a field in your doc
afs.collection('users', ref => ref.where('id', '==', auth.uid))
// or by getting the document directly
afs.collection('users').doc(auth.uid)
When performing a rules check on a query, Cloud Firestore Security
Rules will check to ensure that the user has access to all results
before executing the query. If a query could return results a user
doesn't have access to, the entire query fails and Firestore returns
an error.
Source : https://cloud.google.com/firestore/docs/security/secure-data#shallow_queries

Resources