Firebase firestore security rule for collectionGroup query - firebase

I am trying to query and filter a collectionGroup from the client doing this:
const document = doc(db, 'forums/foo');
const posts = await getDocs(
query(
collectionGroup(db, 'posts'),
orderBy(documentId()),
startAt(document.path),
endAt(document.path + '\uf8ff')
)
);
My auth custom user claims looks like this:
{ forumIds: ['foo'] }
The documentation tells me to add the following security rule:
match /{path=**}/posts/{post} {
allow read: if request.auth != null;
}
But this is a security breach as it means that anyone can read all of the posts collections. I only want the user to read the posts in its forums. Is there no better way to secure a collectionGroup query?
(1) I have tried:
match /{path=**}/posts/{post} {
allow read: if path[1] in request.auth.token.forumIds;
}
but I get this error: Variable is not bound in path template. for 'list' # L49.
(2) I have also tried:
match /{path=**}/posts/{post} {
allow read: if resource.__name__[4] in request.auth.token.forumIds;
}
but I get this error: Property __name__ is undefined on object. for 'list' # L49.
I have also tried debugging the two previous security rules with debug and both of them return true.

Based on your stated requirements, you don't want a collection group query at all. A collection group query intends to fetch all of the documents in all of the named collections. You can only filter the results based on the contents of the document like you would any other query.
Since you have a list of forums that the user should be able to read, you should just query them each individually and combine the results in the app. Security rules are not going to be able to filter them out for you because security rules are not filters.
See also:
https://medium.com/firebase-developers/what-does-it-mean-that-firestore-security-rules-are-not-filters-68ec14f3d003
https://firebase.google.com/docs/firestore/security/rules-query#rules_are_not_filters

Related

Firebase rules variable not matching as string

so I am trying to match the user email with the collection name like below in my Firestore rules:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userEmail} {
allow read: if request.auth.token.email.matches(userEmail);
}
}
}
I am aware its not good practice to set collection ID's as emails, but please assume it to be any string here. The above does not work. however, if I replace request.auth.token.email.matches(userEmail) with request.auth.token.email.matches("myemail#gmail.com") it works fine.
Above I have a single document in my users collection with id = myemail#gmail.com, so why is it not matching when I use the userEmail variable but will match if I use "myemail#gmail.com" string?
Additional Info:
Request to /getAccountInfo you can see myemail#gmail.com as email
App code
I used Vuexfire for firestore binding.
store/index.js
bindUsers: firestoreAction(({bindFirestoreRef}) => {
return bindFirestoreRef("users", db.collection("users")
.where('email', '==', 'myemail#gmail.com');
}),
App.vue
async mounted() {
if (firebase.auth.currentUser) {
// Bind Vuexfire after if/when user exists to capture Firestore changes
await this.$store.dispatch("bindUsers");
}
}
Your query is filtering on a document property called email (not its ID):
return bindFirestoreRef("users", db.collection("users")
.where('email', '==', 'myemail#gmail.com');
This has nothing to do with the email token in the user's Firebase Auth account. You haven't shown that you have an email property in the document at all - all you have is a document with an ID that contains an email address.
Your query ultimately needs to match the rule that limits the query. This means that you need some way of explicitly filtering on the client in a way that matches the constraints of the rule. This means you're going to have to use a get() type query for the specific document with an ID, not a collection query that requires filtering with a where clause.
I could be wrong, but it looks like you are writing your rule more like a filter than as a security rule.
#DougStevenson will know much better than me, but if you hard-code a string value then Firestore can determine explicitly if that rule will succeed or fail. But if you use a variable, then I believe that Firestore determines whether the rule will return true or false in general - not specific runtime cases. In this case, the rule should return false since there will be rows that fail the test.
It almost looks like you are trying to use your rule to filter out rows. Firestore Rules don't work that way.
As Doug suggests, you should show us some client-side code you are using for accessing that collection so we can determine if the code is falling into the "rule trying to be a filter" trap.

Firestore Security Rules for Query with Array Contains

I have a Flutter app in which users can make posts and tag the post as belonging to a group. Posts are stored in a global collection and each has a Post.groupId field:
/posts/{postId}
Based on my Firestore security rules and queries, users are only allow to read posts if they are in the group for which the post is tagged (i.e the posts's groupId field). Approved group users are stored in:
/groups/{groupId}/users/{userId}
I could query the posts from a particular user's group like:
_firestore.collection('posts').where('groupId', isEqualTo: 'groupA')...
This above was all working properly.
I am attempting to make an improvement in which a post can be tagged in multiple groups instead of just one, so I am replacing the single Post.groupId field with a Post.groupIds array. A user should be able to read a post if he/she is a member of ANY of the groups from Post.groupIds. I attempt to read all posts tagged with a particular group with the following query from my Flutter app:
_firestore.collection('posts').where('groupIds', arrayContains: 'groupA')...
I keep receiving the following exception Missing or insufficient permissions with these security rules:
match /posts/{postId} {
allow read: if canActiveUserReadAnyGroupId(resource.data.groupIds);
}
function isSignedIn() {
return request.auth != null;
}
function getActiveUserId() {
return request.auth.uid;
}
function isActiveUserGroupMember(groupId) {
return isSignedIn() &&
exists(/databases/$(database)/documents/groups/$(groupId)/users/$(getActiveUserId()));
}
function canActiveUserReadAnyGroupId(groupIds) {
return groupIds != null && (
(groupIds.size() >= 1 && isActiveUserGroupMember(groupIds[0])) ||
(groupIds.size() >= 2 && isActiveUserGroupMember(groupIds[1])) ||
(groupIds.size() >= 3 && isActiveUserGroupMember(groupIds[2])) ||
(groupIds.size() >= 4 && isActiveUserGroupMember(groupIds[3])) ||
(groupIds.size() >= 5 && isActiveUserGroupMember(groupIds[4]))
);
}
With these security rules I can read a single post but I cannot make the above query. Is it possible to have security rules which allow me to make this query?
UPDATE 1
Added isSignedIn() and getActiveUserId() security rules functions for completeness.
UPDATE 2
Here is the error I am receiving when I attempt to execute this query with the Firestore Emulator locally:
FirebaseError:
Function not found error: Name: [size]. for 'list' # L215
Line 215 corresponds to the allow read line within this rule:
match /posts/{postId} {
allow read: if canActiveUserReadAnyGroupId(resource.data.groupIds);
}
It appears Firestore does not currently support security rules for this scenario at the moment (thanks for your help tracking this down Doug Stevenson). I have come up with a mechanism to work around the limitation and wanted to share in case someone else is dealing with this issue. It requires an extra query but keeps me from having to create a Web API using the Admin SDK just to get around the security rules.
Posts are stored as follows (simplified):
/posts/{postId}
- userId
- timestamp
- groupIds[]
- message
- photo
Now I am adding an additional post references collection which just stores pointer information:
/postRefs/{postId}
- userId
- timestamp
- groupIds[]
The posts collection will have security rules which does all the validation to ensure the user is in at least one of the groups in which the post is tagged. Firestore is able to handle this properly for simple get requests, just not list requests at the moment.
Since the postRefs collection stores only ID's, and not sensitive information which may be in the post, its security rules can be relaxed such that I only verify a user is logged in. So, the user will perform post queries on the postRefs collection to retrieve a list of ordered postId's to be lazily loaded from the posts collection.
Clients add/delete posts to/from the normal posts collection and then there is a Cloud Function which copies the ID information over to the postRefs collection.
As per this blog post, if you can maintain an index of member IDs for a given post (based on group assignments), then you can secure post read access storing member IDs in an array data type and matching against the member IDs with the "array-contains" clause in your ruleset. It looks like this in your Firebase rules:
service cloud.firestore {
match /databases/{database}/documents {
match /posts/{postId} {
allow read: if request.auth.uid in resource.data.members
allow write: if request.auth.uid == resource.data.owner
}
}
}
If I had to guess, I'd say that groupIds isn't actually a List type object, which means that the field from the document is also not an array. If it's a string, this code won't work, since strings don't have a method called size() in the rules language.
If you aren't 100% certain what the type of field is going to be, you will need to check the type in the rule and determine what to do with it. You can use the is operator to check the type. For example, groupIds is list will be boolean true if you're actually working with one.
In your rules, you can use the debug() function to dump the value of some expression to the log. It will return the same value. So, you can say debug(groupIds) != null to both print the value and check it for null.

Firestore security rules get field/id of reference

I have two collections - tenancies and users.
A tenancy doc has a field called "landlordID" and is of type REFERENCE (not String).
Now in my Firestore Security Rules I want to allow a tenancy to be updated ONLY IF the landlordID field of that tenancy matches with the uid of the user making the request, namely request.auth.uid.
Read it as " allow a tenancy document to be updated if the user making the user is authenticated, hence request.auth.uid != null, and the landlordID field's ID should be equal to that of the request.auth.uid.
Hence the code should me something like this:
service cloud.firestore {
match /databases/{database}/documents {
match /tenancies/{tenancyID}{
allow update: if request.auth.uid != null &&
request.auth.uid == get(resource.data.landlordID).id
}
}
I have also tried get(/databases/$(database)/documents/users/$(resource.data.landlordID)).data.id
Supporting screenshot of my database
This should be very simple but get() simply does not work. Firebase Docs, scroll to "Access other documents" was not helpful at all for my situation and I am not sure how to get it working.
It would be a shame if references can't be used like this as they are just like any other field of a document.
Here is a function I made that works for me. I guess you have a user collection with users having the same id as their auth.uid
function isUserRef(field) {
return field in resource.data
&& resource.data[field] == /databases/$(database)/documents/users/$(request.auth.uid)
}
Adjusting to your use case you'd call the function so: isUserRef('landlordID') although the ID at the end of it is a bit misleading as this field is in fact a reference.
I see a couple of issues here. A first problem is that the get() function expects a fully specified ducument path, something like:
get(/databases/$(database)/documents/users/$(resource.data.landlordID)).data.id
A second problem is that you are trying to use the reference type in your rules, I do not think that is possible unfortunately.
The reference type in Firestore is not very helpfull (yet), I think you should store the landlordID as a string, then you can simply do something like:
service cloud.firestore {
match /databases/{database}/documents {
match /tenancies/{tenancyID}{
allow update: if request.auth.uid != resource.data.landlordID;
}
}
I had the same issue I needed an answer for. See this Google-thread with the answer from someone from google. To quote it:
You can get an id out of a path using the "index" operator:
some_document_ref should look like /databases/(default)/documents/foo/bar
which has 5 segments: ["databases", "(default)", ...]
some_document_ref[4] should be "bar"
allow create: if request.resource.data.some_document_ref[4] == "bar";
You can also use the normal get and exists functions on them.
A few difficult aspects of this that you may run into:
There's no way to retrieve the number of segments in a path at the moment (we're adding this soon), so you'll need to know some information about the reference ahead of time
There's not great support for writing references using the simulator in the Firebase Console. I used the Firestore emulator to test out this behavior (gist1, gist2)
might be too late, but I was able to piece together (despite a lack of docs) that a document reference is just a path, and complete path can be created with
/databases/$(database)/documents/users/$(request.auth.uid)
Then I have an array/list in firestore of references, called reads that I can grab with:
get(/databases/$(database)/documents/users/$(userId)/userinfo/granted_users).data.reads
Leaving me able to create a bool, and a rule with:
/databases/$(database)/documents/users/$(request.auth.uid) in get(/databases/$(database)/documents/users/$(userId)/userinfo/granted_users).data.reads
obviously your data structure will vary, but knowing the ref is a path is the important part here.
I had to experiment a little to get this working. Here the function that worked for me
function isUserRef(database, userId) {
return 'user' in resource.data
&& resource.data.user == /databases/$(database)/documents/users/$(userId);
}
And I call it like:
match /answers/{answer} {
allow read:
if isUserRef(database, request.auth.uid);
}
As mentioned by some other answers, a reference has a path property that is just a string that will look something like users/randomuserid123. You can split that into an array and match it against the user making the update request.
...
match /tenancies/{tenancyID}{
allow update: if request.auth.uid != null &&
resource.data.landlordID.path.split('/') == ['users', request.auth.uid]
}
...
Also had a trouble handling this problem, but in my case I needed to allow the user to add a message into a chat only if they're the owner of that chat room. There are 2 "tables" - chats and chat_messages, and chat_messages relate to a specific chat through chatId field. chats objects have ownerId field.
The rule I've used goes like this:
// Allow adding messages into a chat if the user is an owner of the chat room
match /chat_messages/{itemId} {
function isOwner() {
return get(/databases/$(database)/documents/chats/$(request.resource.data.chatId)).data.ownerId == request.auth.uid;
}
allow read: if true;
allow create: if isOwner();
}

Firestore: Query a collection for all documents user has access to

Running into a situation that I'm unclear has a clean solution.
In Firestore, I have a collection in which user are only allowed to access certain documents. Users can be assigned to one or more accounts, and Accounts can have one or more user. The general models and rules work as expected:
USER: {
id : abc123,
accounts : [ xyz789, ... ]
}
ACCOUNT: {
id : xyz789,
users : [ abc123, ... ]
}
service cloud.firestore {
match /databases/{database}/documents {
match /accounts/{accountID} {
allow read, write: if accountID in get(/databases/$(database)/documents/users/$(request.auth.uid)).data.accounts;
}
}
}
From what I can tell with the Firebase Rule Simulator, the above rule is working correctly (I can read/update the accounts that list my userID, but not the ones that don't).
The issue is that if I want to get those same accounts via a Query operator, I get an error. The error does go away when I relax the ruleset, but that's not ideal.
firestore.collection('accounts').where('users', 'array-contains', userID)
ERROR: Missing or insufficient permissions
Given that the ruleset and the query seem to refer to the same records, is there a way to get them to work in conjunction or am I forced to relax the rules in order to get this to work?
I had a similar problem before and I found that Firebase doesn't check the fetched data to validate the rules, but it compare the query code with the rules, and depending on that it throws the exception
So what I found is that the if condition should have a where filter in the code
This if condition is missing a where
allow read, write: if accountID in ...
To make your code work, it would need to add a where filter that refers to accountID
firestore().collection('accounts')
.where(firestore.FieldPath.documentId(), 'in', accounts) //accounts: ['xyz789']
.where('users', 'array-contains', userID)

Firestore; security rule simulation passes but actually fails (uid as map key)

I can't understand why this Firestore security rule fails in web.
I'm using #angular/fire latest with a query on a collection and with firebase latest, but it works in simulation.
service cloud.firestore {
match /databases/{database}/documents {
// Match any document in the 'cities' collection
match /events/{event} {
allow read: if (resource.data.access.code == 'public' || (resource.data.access.code == 'protected' && resource.data.objname.objfield == "A"));
}
}
}
Here's the data:
If access.code is "protected", then we look at objname.objfield == "A".
Checking access.code == "protected" lets me access the data, but the second part objname.objfield == "A" doesn't.
I don't understand why.
I've made sure this property exists in all objects of the collection. (Firestore security rules based on map values)
I tried several different ways, they all pass in Simulation, because it's a single document query.
An actual query at collection level does not go through permissions with the actual web call.
This is the query I'm making, which passes when there's no security rule in place
const pathAccessCode: firebase.firestore.FieldPath = new firebase.firestore.FieldPath('access', 'code');
const eventsUser: AngularFirestoreCollection<any> = this.firestore.collection('events', ref => ref.where(pathAccessCode, '==', 'protected'));
const eventsUser$: Observable<any> = eventsUser.valueChanges();
eventsUser$.subscribe(data => console.log(data) );
As you can see; some properties are recognised by the rule (access.code) but not others (objname.objfield).
The only think I can think of at this stage, is that access.code has been created before I started playing with the rules.
objname.objfield however, was created after I started using rules.
Can this be a thing?
Thanks for your help
After asking Firebase support, I was told that in the context of queries, the security rules have to match the query themselves.
That is; if I want to query documents with filters on fieldA and fieldB, then only fieldA & fieldB should be in the security rule.
This is not what I want to achieve here however.
Hope that helps anyone with the same issue.

Resources