Firestore Security Rules - how to prevent modification of a certain field - firebase

Let's assume we have a Firestore collection called todos, where each todo will look something like this:
{
name: "Buy milk",
completed: false,
user: "eYtGDHdfgSERewfqwEFfweE" // some user's uid
}
Now, I want to prevent modification of the user field during updates (in other words - make this field read-only).
Believe me, I've done my research. My update rule looks like this:
allow update: if request.auth.uid == resource.data.user
//&& !request.writeFields.hasAny(["user"]);
//&& !(request.writeFields.hasAny(["user"]));
//&& !request.resource.data.keys().hasAny(["user"]);
//&& !('user' in request.resource.data);
//&& ('user' in request.resource.data) == false;
//&& !('user' in request.writeFields);
None of the above (commented out) work. They all result in an error: Error: Missing or insufficient permissions..
But...
It gets even more interesting! Because if we compare some of the above rules for positive outcome (aka true) they will work!
For example, this one works perfectly (assuming we include user field in our request):
allow update: if request.resource.data.keys().hasAny(["user"]) == true;
BUT this one doesn't work (assuming we do NOT include the user field in the request):
allow update: if request.resource.data.keys().hasAny(["user"]) == false;
Can anyone explain me what is going on here? Is it possible this is actually a bug in Firestore?

At "Writing conditions for Cloud Firestore Security Rules" section "Data validation" example #2
service cloud.firestore {
match /databases/{database}/documents {
// Make sure all cities have a positive population and
// the name is not changed
match /cities/{city} {
allow update: if request.resource.data.population > 0
&& request.resource.data.name == resource.data.name;
}
}
}
So request.resource.data.user == resource.data.user should work for you? CMIIW
Ref: https://firebase.google.com/docs/firestore/security/rules-conditions#data_validation

When you update a document the security rules compare the document that results from the update with the conditions of the security rules. This means that if a field exists in the document on the server but not in the data you push in the update, the security rules will see that field from the old data in the update. For example:
In the document store on the server:
{
reviewerID: sam123,
title: "It sucks",
description: "Because it does",
rating: 1
}
and then your update is:
{
description: "but actually it's not so bad",
rating: 3
}
Then what the security rules see in request.resource.data is:
{
reviewerID: sam123,
title: "It sucks",
description: "but actually it's not so bad",
rating: 3
}
Which means that even though the update didn't push a change to reviewerID or title those fields do exist in the data you're evaluating.
To make sure that the data is unchanged you need to compare the new data with the old data:
request.resource.data.reviewerID == resource.data.reviewerID
the line:
!request.resource.data.keys().hasAny(["user"]);
Would be correct in preventing the data from existing in the document. It is not something that would allow it to exist but make it immutable.
See:
service cloud.firestore {
match /databases/{database}/documents {
// Make sure all cities have a positive population and
// the name is not changed
match /cities/{city} {
allow update: if request.resource.data.population > 0
&& request.resource.data.name == resource.data.name;
}
}
}
From:
https://firebase.google.com/docs/firestore/security/rules-conditions#data_validation
and:
service cloud.firestore {
match /databases/{database}/documents {
// Allow the user to create a document only if that document does *not*
// contain an average_score or rating_count field.
match /restaurant/{restId} {
allow create: if (!request.resource.data.keys().hasAny(
['average_score', 'rating_count']));
}
}
}
From:
https://firebase.google.com/docs/firestore/security/rules-fields#forbidding_specific_fields_in_new_documents

Related

Firebase Firestore: Nested Security rules for global read access

This is a follow up on this post: Firebase Firestore security rules - read all nested docs based on resource (but it's null)
I'm having a hard time figuring how Firestore works!
So let's say I have this Bases collections:
{
name: "Base 1",
roles : {
user_1: true
}
},
{
name: "Base 2",
roles : {
user_2: true
}
}
I want to fetch all Base where user_1 is in resource.roles. I do :
db.collection('bases').where('roles.user_1', '==', true).get()
And it works great. All server side so no one can try to alter the filter: All good.
But, I want to enforce this filter with a security rule in firestore. Just to be sure! So I've wrote:
function hasRole(){
return request.auth.uid in resource.data.roles
}
match /bases/{document=**}{
allow read: if true; // signedIn() && hasRole();
}
match /bases/{baseId}{
// allow read: if signedIn() && hasRole();
allow update, delete: if false && signedIn()
allow create: if signedIn()
}
And it works when I try to access a specific resource in the rule simulator but now my .get() method receive a not-enough permission. Funny enough, When I uncomment the read into /bases/{baseId} it works perfectly (if I comment the /bases/{document=**} of course).
So I can make it work but I'd still like to understand why the {document=**} rule work in simulator and not when I'm trying to fetch from a server.
Thanks a lot for your help :)
P.S: I didn't find a way to simulate a nodejs .get() in the simulator which is not super convenient to learn this stuff. Any resource on that will be read and helpful.

Using the user's email in Firestore Rules?

In a firestore database, I am using email only as authentication. The database has the following structure:
companies
jobs
users
For sake of readability (and for customer's peace of mind), I am using the email address as the Document ID for the users collection. A user document looks like this:
document id: tbogard#gmail.com
fields
name_first: Terry
name_last: Bogard
jobs_read: ["job_A"]
jobs_readwrite: ["job_B, job_C"]
When I try to grab the request token in the rules, it gives me errors (search for ***):
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Helper functions
function userExists(){
// *** Function not found error: Name: [exists]. ***
return exists(/databases/$(database)/documents/users/$(request.auth.token.email));
}
function userData(){
// *** Function [get] called with malformed path: /databases/(default)/documents/users/ ***
return get(/databases/$(database)/documents/users/$(request.auth.token.email)).data;
}
// For now, let's keep it simple, and enforce the User read/readwrite rules on Jobs.
match /jobs/{jobId}{
allow read: if userExists() && (jobId in userData().jobs_read || jobId in userData().jobs_readwrite);
allow write: if userExists() && jobId in userData().jobs_readwrite;
}
// Only allow Users to see their own profile and companies they belong to.
match /companies/{companyId}{
allow read: if userExists() && userData().email in resource.data.employees;
allow write: if false;
}
match /users/{userId}{
allow read: if userExists() && userData().email == resource.data.email;
allow write: if false;
}
}
}
I'm guessing request.auth.token.email is returning something like an optional? I can't find anything in the documentation explaining how the functions get/exists, which require a path, handle this. Is there a way that I could make the Firestore UID for each user the email address instead of the random string, or can I fix these rules some way?

How to debug firestore.rules variables and functions?

I am having difficulty trying to diagnose a particular rule in my firestore.rules file. See that question here for context.
Is there a way to debug the firestore.rules file and/or functions? I'm using unit testing and the emulators to test my rules, but I would really love to see exactly what values are being evaluated by the rules engine.
For instance, here is my firestore.rules file:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /organizations/{orgId} {
allow read: if isAdmin();
allow create, update: if isAdmin();
match /classes/{classId} {
allow read: if request.auth.uid != null;
allow create, update: if isAdmin();
match /students/{studentId} {
allow read: if isAdmin() || belongsToCurrentClass();
allow create, update: if isAdmin();
}
}
}
}
}
function isAdmin() {
// removed for security
}
function belongsToCurrentClass() {
// retuns true if the authenticated user is the teacher of the requested class
return get(/databases/$(database)/documents/organizations/$(orgId)/classes/$(classId)).data.teacherUid == request.auth.uid;
}
What I'd love to do is set breakpoints or step through the code. When attempting CRUD operations on a organizations/{orgId}/classes/{classId}/students/{studentId} path I'd love to inspect exactly what values the orgId, classId, and studentId variables are holding, as well as the resource and request parameters. I'd love to inspect exactly which document (if any) is returned by the get request in belongsToCurrentClass and what the return value is.
Does anyone know of any way to do this? I think I'd answer my question referred to above in 10 seconds if I could just see the data being evaluated.
There is a local emulator for Cloud Firestore security rules. This is your best (and really only) tool for digging into security rule execution. There is no step-through debugging, but you can see a lot of debug output in the console.
https://firebase.google.com/docs/rules/emulator-setup
We can add the built-in debug function to rules. As noted in a comment, you'll see an unhelpful message like this in the browser:
Received: [path] Expected: [bool]. for 'list' # L6
On the plus side, we won't forget to remove debug messages. Tail the log file to see the output: tail -f firestore-debug.log
For example, to see which paths are being called:
match /databases/{database}/documents {
match /{document=**} {
allow create, read, update, delete: if debug(request.path);
}
}

Firebase Firestore Security Rules Variable not used

Writing rules for Firestore it seems that custom variables are not working.
Did anyone know why or have seen similar behaviour?
Using the below I got access denied although the uid is in the array of admin.
service cloud.firestore {
match /databases/{database}/documents {
match /conferences/{confid} {
allow read,write: if request.auth.uid in get(/databases/$(database)/documents/conferences/$(confid)).data.admin;
}
}
}
Simulator is giving the below error:
Function [get] called with path to nonexistent resource: /databases/%28default%29/documents/conferences/%7Bconfid%7D
Also testing this on a real devices I got access denied.
If however I use the ID of the document like below it works and access is granted.
service cloud.firestore {
match /databases/{database}/documents {
match /conferences/{confid} {
allow read,write: if request.auth.uid in get(/databases/$(database)/documents/conferences/ySWLb8NSTj9sur6n2CbS).data.admin;
}
}
}
Obviously I can't hardcode this for each and every ID.
UPDATE
Apart from logging the case with support I have done some further testing.
On the below the simulator is now granting access.
service cloud.firestore {
match /databases/{database}/documents {
match /conferences/{confID}{
allow read, write: if request.auth.uid in get(/databases/$(database)/documents/conferences/$(confID)/permissions/permission).data.users;
}
}
}
For reference I use the below to query from my web-application:
db.collection("conferences")
.get()
.then(query => {
console.log("SUCCESS!!!")
query.forEach(function(doc) {
// doc.data() is never undefined for query doc snapshots
console.log(doc.id, " => ", doc.data());
});
}).catch((e) => {
console.log(e)
})
This is the log from the browser:
FirebaseError: Missing or insufficient permissions.
at new FirestoreError (webpack-internal:///./node_modules/#firebase/firestore/dist/index.cjs.js:352:28)
at JsonProtoSerializer.fromRpcStatus (webpack-internal:///./node_modules/#firebase/firestore/dist/index.cjs.js:5649:16)
at JsonProtoSerializer.fromWatchChange (webpack-internal:///./node_modules/#firebase/firestore/dist/index.cjs.js:6146:44)
at PersistentListenStream.onMessage (webpack-internal:///./node_modules/#firebase/firestore/dist/index.cjs.js:14350:43)
at eval (webpack-internal:///./node_modules/#firebase/firestore/dist/index.cjs.js:14279:30)
at eval (webpack-internal:///./node_modules/#firebase/firestore/dist/index.cjs.js:14319:28)
at eval (webpack-internal:///./node_modules/#firebase/firestore/dist/index.cjs.js:7411:20)
I am using the latest Firebase package 5.8.3.
If I change the above rule to something simple like below it got access as long as I am logged in with a user:
service cloud.firestore {
match /databases/{database}/documents {
match /conferences/{confID}{
allow read, write: if request.auth.uid != null
}
}
}
This even confuses me more. Is this because the rule is more complex and it takes too long to get this verified and gives back access denied?
Update-2
Quickly tested this in a mobile app via Flutter. Same result. Access denied with this ruleset.
service cloud.firestore {
match /databases/{database}/documents {
match /conferences/{confID}{
allow read, write: if request.auth.uid in get(/databases/$(database)/documents/conferences/$(confID)/permissions/permission).data.users;
}
}
}
I think my problem was the query's don't match security rules. If you would only access a single specific document it would work but if you query multiple documents in a collection you got blocked by the security rules.
I had two options. Restructure my data so that a single document will hold all the data I need or redesign security rules to match query's.
In the end I have attached to each document an indentifier like the UID to make sure query's match the security rules.
One solution would be to put the users with permissions into an array in the conference document instead,
so request.resource.data.permissions
So, instead of this:
get(/databases/$(database)/documents/conferences/$(confID)/permissions/permission).data.users
use this:
request.resource.data.permissions
This wouldn't solve the get() problem, but it would eliminate the need for a get() call, which could save you 15% or more on your quota.

Firestore .setData is blocked by update rule not create [duplicate]

I posted a question about this yesterday but I'm creating a new one with more details.
Firestore .setData is blocked by update rule not create
I've run the simulator and the rules work there. Also when I create the document and change setData in the swift code to update the code works. It appears to only fail when creating the document. But the catch is that when I remove the update rule or simply change it to allow update: if false; the setData (or seen as create by the rules) executes properly. I have no clue whats going on nor do I know of any tools for getting a better insight.
match /users_real/{userID} {
allow create: if true;
allow read: if isOwner(userID);
allow update: if (request.writeFields.size() == 1);
}
set data:
self.docRef.collection("users_real").document("adfadsf").setData(post) { (error) in
if let error = error {
print("He dead!: \(error.localizedDescription)")
}
else {
print("it worked, for now")
}
}
Firebase Support confirms that there is a bug related to the evaluation of request.writeFields.size(). No estimate was given of when it will be fixed.
The existence of the bug can be demonstrated with the following rules:
service cloud.firestore {
match /databases/{database}/documents {
match /cities/{city} {
// This should always evaluate to true, but does not.
allow create: if (request.writeFields.size() == 1) || (request.writeFields.size() != 1);
allow update: if true;
}
}
}
Although the create rule should always evaluate to true, an attempt to create a city fails with Permission Denied. It seems that the problem with request.writeFields affects not only the rule in which it appears, but also other rules for the path. For the rules shown above, an attempt to update an existing city also fails with Permission Denied.

Resources