Firestore security rules with reference fields - firebase

I am a bit stuck here as there is no way to debug those rules. I'd appreciate help with below rules.
I want to access:
/modules/module-id/sessions/session-id/parts/
The comparison with null in the first part of hasCompletedPrerequisiteSession() works well, the second part doesn't!
The path /modules/moduleId/sessions/sessionId/prerequisite points to a reference field.
service cloud.firestore {
match /databases/{database}/documents {
function hasCompletedPrerequisiteSession(moduleId,sessionId) {
// this part works well
return getPrerequisiteSession(moduleId,sessionId) == null ||
// !!! this part does not work !!!
hasCompleted(getPrerequisiteSession(moduleId,sessionId).id);
}
function getPrerequisiteSession(moduleId,sessionId) {
return get(/databases/$(database)/documents/modules/$(moduleId)/sessions/$(sessionId)).data.prerequisite;
}
function hasCompleted(sessionId) {
return exists(/databases/$(database)/documents/progress/$(request.auth.uid)/sessions/$(sessionId));
}
match /modules/{moduleId}/sessions/{sessionId}/parts/{partId} {
allow read: if hasCompletedPrerequisiteSession(moduleId,sessionId);
}
}
}
(If I store the session ID as a string instead of a reference to the session, it works fine.)
Edit
Questions
Reference field in security rules. Assuming modules/moduleId/owner points to a field of the type reference. What is the proper way to get the id of the referenced document?get(../modules/moduleId).data.owner.data.id or get(../modules/moduleId).data.owner or something else?

From Firebase support:
It seems that in your use case, you want to get the document name (sessionId) from the value of your reference field (prerequisite), unfortunately, this is not currently supported by Firestore security rules. I would suggest that you store only the sessionId as String on your prerequisite field, or you can also add String field for the sessionId. Keep in mind that the exists() and get() functions only allow you to check if a document exists, or retrieve the document at the given path.

It might be that around getPrerequisiteSession, after using get to pull the object by ref path, you had to use .data first before referencing the id field. Of course, id field needs to be stored as an object field.
For example, in my case I needed to allow 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:
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();
}

Related

What variables are available for use as "wildcards" in a Firestore Rules Match Statement?

The syntax for specifying the syntax of a rule that you might want to declare in order to protect documents in a Firestore collection are specified at https://firebase.google.com/docs/reference/security/storage. But this describes the wider context of Google Cloud storage and consequently, when you're trying to use these specifically for Firestore applications, the documentation seems a bit opaque.
The match statement used to specify the subset of documents in a collection myCollection to which the rule is to be applied typically takes the form:
match myCollection/{wildcard}
My question is "what variables are available for use as wildcards?". According to the Google document referenced above "A wildcard variable is declared in a path by wrapping a variable in curly braces: {variable}. This variable is accessible within the match statement as a string."
I now understand that if myCollection contains a field called email for example, a match statement of the form
match myCollection/{email}
will allow me to specify a rule to be applied to a document whose documentId is the value specified by the email field. In this case, a rule to allow only logged-in users to read their own record in myCollection would take the form:
match myCollection/{email}
allow read : if request.auth != null && request.auth.token.email == email;
where email is the user's email address filed in myCollection
But supposing that email isn't being used as the unique key to identify a myCollection record - ie that other fields identify documents and a user may have many records tagged with their email within myCollection? This is surely the more likely case but the Google documentation doesn't seem to give any clues to how you would specify a match to cover this situation.
A previous stackoverflow question at How write Firestore rule to match email field in document has directed me towards the keyword doc. This has allowed me to specify a more general rule that can be applied to all the records in a collection with a particular email value:
match /myCollection/{doc} {
allow read : if request.auth != null &&
request.auth.token.email == resource.data.email;
}
There are two concepts here that are new to me (at least in the context of Firestore rules): first the keyword doc and second the resource.data.email construct that gives my rule access to document fields. Can anybody tell me where these constructs are documented, and also if there are any more "wrinkles" to the "wildcard" concept (I already know about the documents=** one).
match myCollection/{wildcard} - This rule tells firebase to match all documents in the myCollection collection. Essentially, it says match all documents that match this path myCollection/*
wildcard above is simply a variable that holds the document path (the asterisks path). This variable can be anything (apart from the reserved strings). If this is your path:
'myCollection/tests', then wildcard variable is holds 'tests';
'myCollection/test#email.com', then wildcard variable holds 'test#email.com'
wildcard is simply a place holder for anything that matches this path: myCollection/*. Instead of wildcard, you can use anything e.g email (myCollection/{email} and email here becomes the variable that holds the document path).
resource.data is the data being fetched. So if your data has an 'email' field, then resource.data.email always holds the email field data (regardless of what the wildcard variable is). Do not use reserved strings e.g 'resource' as the wildcard variable.
what variables are available for use as wildcards?
Any variable you put in curly braces becomes the wildcard variable. It simply holds the document path. It is not a special string.
UPDATE #MartinJ.
For autogenerated ids. Lets assume this is your doc path myCollection/1s6SUqi8M6sC6DZdTKjt, (1s6SUqi8M6sC6DZdTKjt is autogenerated)
match /myCollection/{email} {
allow read : if email == '1s6SUqi8M6sC6DZdTKjt'; // true because
// email variable holds the value '1s6SUqi8M6sC6DZdTKjt'
}
match /myCollection/{doc} {
allow read : if doc == '1s6SUqi8M6sC6DZdTKjt'; // true because
// doc variable holds the value '1s6SUqi8M6sC6DZdTKjt'
}
match /myCollection/{stackoverflow} {
allow read : if stackoverflow == '1s6SUqi8M6sC6DZdTKjt'; // true
// because stackoverflow variable holds the value '1s6SUqi8M6sC6DZdTKjt'
}
// It does not matter what we use as wildcard variable (we can even
// use MartinJ), it will always hold the value of '1s6SUqi8M6sC6DZdTKjt'
// (doc id if the path matches).
// As #Frank said, these are simply variables, not keywords. You
// can call it anything and it will have the value of doc path.
}
if this is your document path sales/2K5CwGU9ZWwNoO9efyuv/payments/40ciObsicEntBZParkon (autogenerated document ids)
match /sales/{doc=**} {
allow write: if doc == '2K5CwGU9ZWwNoO9efyuv/payments/40ciObsicEntBZParkon';
// this will be true because doc=** (**with asterisks**) matches
// everything after sales
}
match /sales/{saleId} {
match /payment/{paymentId} {
allow write: if saleId == '2K5CwGU9ZWwNoO9efyuv' && paymentId == '40ciObsicEntBZParkon';
// this will be true because saleId holds the value
// '2K5CwGU9ZWwNoO9efyuv' and paymentId holds this value '40ciObsicEntBZParkon'
}
}

Firebase query returns "PERMISSION_DENIED" but rules work in test console

I have the following Firebase rule:
match /PendingInvites/{inviteID} {
allow read: if request.auth != null &&
isInviteForUser(database, inviteID);
}
and the following functions:
function isInviteForUser(database, inviteID) {
let dataItem = get(/databases/$(database)/documents/PendingInvites/$(inviteID)).data;
return (dataItem.userPhone == request.auth.token.phone_number) ||
(dataItem.userEmail == request.auth.token.email);
}
The testing against the collection using the online role test works, I've verifies using real documents and with both userPhone and userEmail values (both matching and non-matching). All work as expected, denying mismatch values and allowing matched values.
This is where is gets strange. When I run this (Android) query:
val companyMemberDocuments = Firebase.firestore.collection("PendingInvites").whereEqualTo("userPhone", firebaseAuth.currentUser!!.phoneNumber).whereEqualTo("userEmail", firebaseAuth.currentUser!!.email).get().await()
I get "PERMISSION_DENIED: Missing or insufficient permissions". As near as I understand rules, I think it should work. It works in the online console, I'm specifically querying for userPhone (or userEmail) but it doesn't work.
I've tried removing the userEmail and only testing for userPhone, but that also appears to not work.
Any ideas how I can correct the rules (or query)?
Thanks
Document:
Auth token used in testing:
{
"uid": "",
"token": {
"sub": "",
"aud": "certifly-global",
"phone_number": "+16505551234",
"firebase": {
"sign_in_provider": "phone"
}
}
}
Note how the document includes BOTH, userPhone and userEmail
Your rules are rejecting the query every time because Firestore security rules are not filters. Be sure to read that documentation and this post.
Your query is asking for all documents in PendingInvites where userPhone == +16505551234. However, your rules do not allow this for two reasons:
Your query doesn't match the constraints of rule (that both userPhone and userEmail are set to specific values). The query is just specifying userPhone.
The system will not perform a get() for every single document that could match. While this will work for individual document gets from the client, it will not work for queries that could return any number of documents.
So you will have to resolve both problems.
Your query will need to filter using both userPhone and userEmail, as required by your rules. This means you will have to add another whereEqualTo on userEmail that matches the requirement of the rules. In other words, the client app needs to pass the user's email in that filter.
You don't need to use a get() at all in the rules. You can refer to fields in documents in the current collection via resource.data.
The rules will need too something more like this:
match /PendingInvites/{inviteID} {
allow read: if
request.auth != null &&
resource.data.userPhone == request.auth.token.phone_number &&
resource.data.userEmail == request.auth.token.email;
}

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 Rules - Validate if data exists in other collection

I'm currently working on my firestore rules and I need to validate the incoming data. Besides what I already have, I also need to validate if the incoming origin and tag fields exist in the collection origins and tags. I've found how to do so when using references but I'm using embedded data so I'm unsure how to exactly do it.
function incomingData() {
return request.resource.data
}
function validTicket() {
return incomingData().email is string &&
incomingData().description is string &&
incomingData().address is string &&
incomingData().location is string &&
incomingData().postCode.matches('^[0-9][0-9][0-9][0-9]-[0-9][0-9][0-9]') &&
incomingData().size() == 5 &&
incomingData().hasAll(['email','description', 'address', 'location', 'postCode']) &&
isSecretary()
}
In the tags collection, every document has a single value with the tag name. The same applies to the origins.
I'm sorry my answer will be partial, i need you to post your current firestore rules, and the name of the ticket collection...
anyway, for tags, you won't be able to search them for their value, and nor inside the rules, so you should save them as keys. that mean, that the key for the document of sports, should be sports, and not 8VCCvq7qnvjyT98r95pu.
next, you will have to use the function exists, in the follow way:
function isTagExists(tag) {
return exists(/databases/$(database)/documents/tags/$(tag));
}
let me know if you updated the question or you need more help with my solution.
also you can read more at:
https://firebase.google.com/docs/firestore/security/rules-conditions

Resources