Firebase cross-service Security Rules not working in application - firebase

I'm trying to use the new Firebase cross-service Security Rules (https://firebase.blog/posts/2022/09/announcing-cross-service-security-rules) but I having some problems with Storage Rules accessing to Firestore data.
The problem seems to be with userIsCreator() function
match /certification/{certificationId}/{fileId} {
function userIsCreator() {
let certification = firestore.get(/databases/(default)/documents/certifications/$(certificationId));
return firestore.get(certification.data.creatorRef).id == request.auth.uid;
}
allow read, write: if userIsCreator()
}
The content of the Firestore Document is:
{
"data": {
othersValues,
"creatorRef": "/databases/%28default%29/documents/users/CuutSAtFkDX2F9T8hlT4pjMUByS2"
}
"id": "3EhQakDrsKxlacUjdibs"
"__name__":
"/databases/%28default%29/documents/certifications/3EhQakDrsKxlacUjdibs"
}
The creatorRef variable is a reference to a Firestore Document to user. Inside Users collection, the doc id is the UID of an user, so I'm obtaining the creatorRef of an item and then checking if the id of that user collection referenced is the same UID that user logged in.
The same function is working for Firestore Rules to avoid updating certification document if not the creator, without any problem.
It seems to be a problem calling to firestore.get to creatorRef after obtaining it but it not make sense!
Tested:
If I use Firestore Storage Rules validator, it is not failing and it says I have access to that resource from the UID typed in the tester (for other UID is failing as expected). But in my app, even logged in with creator user is getting permission error.
If changing the function to only one call directly to the Users collection id (return firestore.get(/databases/(default)/documents/users/CuutSAtFkDX2F9T8hlT4pjMUByS2).id == request.auth.uid;), it is working in the tester and my app. But it isn't a solution because I need to get first the Users collection ref for the creator!
For original function in the tester It's getting the variables as expected and returning true if simulate the creator UID! But for any reason, in the real app access it is getting unauthorized if making both calls!

Firebaser here!
It looks like you've found a bug in our implementation of cross-service rules. With that said, your example will create two reads against Firestore but it's possible to simplify this to avoid the second read.
Removing the second read
From your post:
return firestore.get(certification.data.creatorRef).id == request.auth.uid;
This line is a bit redundant; the id field is already contained in the certification.data.creatorRef path. Assuming you are indeed using Firestore document references, the format of creatorRef will be /projects/<your-project-id>/databases/(default)/documents/users/<some-user-id>. You can therefore update your function to the following:
function userIsCreator() {
let certification = firestore.get(/databases/(default)/documents/certifications/$(certification));
let creatorRef = certification.data.creatorRef;
// Make sure to replace <your-project-id> with your project's actual ID
return creatorRef ==
/projects/<your-project-id>/databases/(default)/documents/users/$(request.auth.uid);
}
I've tested this out in the emulator and in production and it works as expected. The benefit of doing it this way is you only have to read from Firestore once, plus it works around the bug you've discovered.

Related

Firebase security rules wildcard in get function

I am struggling with an issue that is seemingly easy at first glance yet still does not work after trying dozens of different syntaxes. I've got a firebase database with a collection called users and every user has a subcollection 'posts'. What I want to achieve with implementing security rules is that one's posts can only be read by oneself if the account is private or by everyone if the account is public. Therefore, each user has got a boolean field isPublic. Sounds simple. This is my code:
match /users/{uid}{
function isPublic(){
return get(/databases/$(database)/documents/users/$(uid)).data.isPublic;
}
allow read, write: if request.auth.uid==uid
match /posts/{postid}{
allow read: if request.auth.uid==uid || isPublic();
}
The error is in line 3. If I hard code the uid (currently, I've got just 2 users) of user 2, who is public, and try to open his posts as user 1, it works fine. However, using the wildcard throws an exception every time.
return get(/databases/$(database)/documents/users/$(uid)).data.isPublic; //does not work
return get(/databases/$(database)/documents/users/uid).data.isPublic; //does not work
let userid = uid;
return get(/databases/$(database)/documents/users/$(userid)).data.isPublic; //does not work
let userid = string(uid);
return get(/databases/$(database)/documents/users/$(userid)).data.isPublic; //does not work
return get(/databases/$(database)/documents/users/ogfTdmsSG4dy2AHf8dp7LcPpd9B3).data.isPublic; //works
I would really appreciate I someone could tell me what the correct syntax is. Thank you very much in advance!
EDIT
The associated query that fails is:
_database.userCollection.document(idOfUser2).collection('posts').getDocuments();
It works fine when logged in as user 2 with security rules activated, and it works fine without security rules no matter of which user is making the request, but fails when user 1 is making the request with security rules activated.
Additionally, I should mention that if I use another function instead of get (for example, in line 5, like this:), it works fine as well.
allow read, write: if request.auth.uid==uid|| resource.data.isPublic
Thus, I came to the conclusion that the get command does not work. However, I need the get command as resource does not work in subcollections.

Prevent firebase function from overwriting existing data

I am moving the process of creating users in my application to a firebase function for a couple of reasons but I am running into an issue:
I have a /users ref and a /usernames, when a user is created I persist their info in users and usernames (which is publicly accessible to see if a username is available) as a transaction so the username is added immediately when a user is created and my security rules prevent overriding existing data.
However, with firebase functions these security rules are bypassed so there could be a case where 2 users signup with the same username and one person's data will be overriden by the other
is there a way to prevent overriding existing data from cloud functions? (ideally without having them go through the security rules)
I ran into a similar issue and the best solution i found was using the transaction method that firebase offers.
assuming you have a usernames ref you could do something like this:
db.ref('usernames').child(theUsername).transaction(function (usernameInfo) {
if (usernameInfo === null) {
return {
...anObjectOfUserData // or could just return true
}
}
// if the data is not null it means the username is used
// returning nothing makes the transaction do no updates
return
}, function (error, isComitted, snap) {
//
// Use isCommitted from the onComplete function to determine if data was commited
// DO NOT USE the onUpdate function to do this as it will almost certainly run a couple of times
//
})

Firestore default users data initialization

I am creating a game and I want to store completed game levels on firestore for each user.
Now my problem is that I will have to initalize this data once - I want to add a document for new user and a pojo that containts map of level ids and boolean for completed/uncompleted.
So I need to execute some kind of logic like "if document with this id doesnt exist, then add that document and add default data that means user hasnt completed any levels". Is there some way that would guarantee Id have to execute this logic only once? I want to avoid some kind of repeating/re-try if something fails and so on, thanks for your suggestion
That's what a transaction is for (definitely read the linked docs). In your transaction, you can read the document to find out if it exists, then write the document if it does not.
Alternatively, you may be able to get away with a set() with merge. A merged set operation will create the document if it doesn't exist, then update the document with the data you specify.
The typical approach to create-a-document-if-it-doesn't-exist-yet is to use a transaction. Based on the sample code in the documentation on transactions:
// Create a reference to the SF doc.
var sfDocRef = db.collection("cities").doc("SF");
return db.runTransaction(function(transaction) {
// This code may get re-run multiple times if there are conflicts.
return transaction.get(sfDocRef).then(function(sfDoc) {
if (!sfDoc.exists) {
transaction.set(sfDocRef, { count: 1 });
}
});
}).then(function() {
console.log("Transaction successfully committed!");
}).catch(function(error) {
console.log("Transaction failed: ", error);
});
Also see:
Firestore create document if it doesn't exist security rule, which shows security rules that allow a document to be create-but-not-updated.
Create a document only if it doesn't exist in Firebase Firestore, which shows how to allow a document only to be created by a UID identified in the data.
the documentation on transactions
the reference docs for the Transaction class

Firestore Access Rules that Rely on a Document Reference

Firestore has a DocumentReference type, which is a "pointer" to another firestore document. Using the firebase JavaScript client, you can access properties (e.g. document "id"), directly on the reference.
For example, if there is a document with a docRef property that is a firestore DocumentReference:
const retrievedDoc = await getFirestoreDocument();
console.log(retrievedDoc.docRef.id); // "jRmSeMYDMKiOPGsmkdaZ"
I am trying to accomplish the same thing within firestore rules. There is a custom function named isOwner. It uses the firestore rules get call on a document path, and then attempts to access the docRef.id just as if it were the JavaScript client above.
get(/databases/$(database)/documents/path/to/$(id)).data.docRef.id
The value of the document's id is compared against the current user's. But when I test this using the simulator and in real code, it fails. I feel like this should work, but it doesn't.
What does work is to store and use the id value directly as a string (e.g. get(/path/id).docId) instead of a DocumentReference.
Should I be able to access the id value of a DocumentReference within the firestore rules? Am I doing something wrong?
I want to avoid doing a second document get within the rule as described in this SO answer. That's a second "read" for each trigger of this rule. And I don't think the document id (which is what I need) will be available on the get call anyway.
Based on documentation:
https://firebase.google.com/docs/reference/rules/rules.firestore#.get
https://firebase.google.com/docs/reference/rules/rules.firestore.Resource
get() method is supposed to returns a Resource object which is supposed to contains a .id property (as well as .data).
For example, to restrict write access to an authenticated user which is the author of a book document (authors documents are identified with the user uid), you would do:
service cloud.firestore {
match /databases/{database}/documents {
match /books/{document=**} {
allow write: if get(resource.data.authorReference).id == request.auth.uid;
}
}
}
Yet I'm always having the error property id is undefined on object on trying.
.data is accessible so I suppose there is an issue in the api.
Update
Actually, a reference is a Path object in Firestore rules as documented here. So you access the id by the index of the part of the path you need.
In this example I use the incoming document's data which has a reference object to lookup a property on another document from a get()
match /databases{database}/documents {
match /contacts/{contact} {
allow create: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.relatedRules[request.resource.data.relation.path[4]].canBeRelated
// the [4] assumes the path to be `databases/$(database)/documents/contacts/contactId`
// your exact index would vary for your data structure
}
}
First Answer
This only works in the Firestore dashboard rules simulator, it is not a working example for either the local emulation or production Firestore.
This is a year old but I encountered this same puzzling issue, but not on the data from a get(), just on the data of the request.resource.data. I'm not sure what ought to be available (not even __name__ is available) in the rules but if you're accessing a resource reference on the data and you have a predictable id size (say, 20 characters) you could simply get the range of the path on the resource to check against
match /databases{database}/documents {
match /contacts/{contact} {
allow create: if get(/databases/$(database)/documents/users/$(request.auth.uid)).data.relatedRules[request.resource.data.relation.path[9:29]].canBeRelated
// the [9:29] assumes the path to be `/contacts/20characterLongIdStr`
// your exact range would vary for your data structure
}
}
Feels like a resource reference object should have at least the id since the path is there. It appears Firestore won't support this for whatever reason.

Firestore security rules based on map values

I want to store if a user is permitted to read a document in the document itself, based on the user's email address. Multiple users should have access to the same document.
According to the documentation Firestore does not allow querying array members. That'S why I'm storing the users email addresses in a String-Bool Map with the email address as a key.
For the following example I'm not using emails as map keys, because it already doesn't work with basic strings.
The database structure looks like that:
lists
list_1
id: String
name: String
owner: E-Mail
type: String
shared:
test: true
All security rules are listed here:
service cloud.firestore {
match /databases/{database}/documents {
match /lists/{listId=**} {
allow read: if resource.data.shared.test == true
}
}
}
Edit: It also doesn't work if I use match /lists/{listId} instead of match /lists/{listId=**}
How I understand it, this security rules should allow reading access to everyone if the value in the map shared[test] is true.
For completness sake: This is the query I'm using (Kotlin on Android):
collection.whereEqualTo("shared.test", true).get()
.addOnCompleteListener(activity, { task ->
if (task.isSuccessful) {
Log.i("FIRESTORE", "Query was successful")
} else {
Log.e("FIRESTORE", "Failed to query existing from Firestore. Error ${task.exception}")
}
})
I'm guessing that I cannot access map values from the security rules. So what would be an alternative solution to my problem?
In the Firestore rules reference it's written that maps can be accessed like that resource.data.property == 'property' so, what am I doing wrong?
Edit: This issue should be fixed now. If you're still seeing it (and are sure it's a bug with the rules evaluator), let me know in the comments.
I've chatted with some folks here about the problem you're encountering, and it appears to be an issue with the security rules itself. Essentially, the problem seems to be specific to evaluating nested fields in queries, like what you're doing.
So, basically, what you're doing should work fine, and you'll need to wait for an update from the Firestore team to make this query work. I'll try to remember to update this answer when that happens. Sorry 'bout that!
Whenever you have (optional) nested properties you should make sure the property exists before continuing to check its' value eg.
allow read: if role in request.auth.token && request.auth.token[role] == true
in your case:
allow read: if test in resource.data.shared && resource.data.shared.test == true
, I was struggling a long time with roles until I realized that on non-admin users the admin field is undefined and firestore rules just crashes and doesn't continue checking other possible matches.
For a user without token.admin, this will always crash no matter if you have other matches that are true eg:
function userHasRole(role) {
return isSignedIn() && request.auth.token[role] == true
}

Resources