can't read path variable in firestore rules - google-cloud-firestore

I have some rules like:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /teams/{teamId} {
allow read, write, delete, list:
if debug(teamId) in debug(request.auth.token.teams)
allow create:
if true;
}
}
}
I am using claims to control access. Each user has an array of teams in their token. That part is working fine.
Basically, teamId (coming from the path) doesn't exist. Nothing is printed in the log for this variable. I can't figure out whats going on. Is there some different way to access that teamId variable?
by doing some logs, it seems that for accessing /teams/teamXXXX I'm getting multiple rule hits. First on /database/default/documents/teams and then again on /database/default/documents/teams/teamXXX The first rule pass is failing because {teamId} is not defined on that path. I need to somehow allow access to the collection while limiting access to the child documents
It looks like the way I'm accessing teams may be causing a problem. I'm getting teams by doing a query like:
instance.collection("teams").where('owners',
arrayContains: FirebaseAuth.instance.currentUser!.uid);
This must be triggering the rule check where no {teamId} in the path. I thought about wrapping my match statement like:
match /teams/{document=**}
allow read:
if (request.auth.uid != "")
match /teams/{teamId} {
allow read, write, delete, list:
if debug(teamId) in debug(request.auth.token.teams)
allow create:
if true;
}
}
I'm worried that this will just allow all documents. I'm stuck.

answering my own question.
When doing a search / list on a collection, it still matches the rule of /teams/{teamId} even though you are not specifically querying by teamId.
Meaning a search like instance.collections("teams").where(...) will still match /teams/{teamId}
In the match, {teamId} will be blank. Instead, you must look at the input paramters coming in via the "resource" variable. The items you have in the "where" clauses will appear in the resource variable. You must use the resource data to resolve your rules.
So I had to separate the "list" rule form the rest of the rules.

Related

How to detect forward slashes in Firestore document IDs with security rules?

I have a usernames collection. Each of its documents has an id equal to the username of an app user, and a single field userId, which is a document ID in a parallel users collection. If a user changes his name to John52, I create a new document with an ID John52, delete the old username document, and update the username field in several other documents in other collections using a batched write.
I would like to prevent the creation of certain usernames, and I use the following security rule to achieve that:
function isSignedIn() {
return request.auth.uid != null
}
function existingData() {
return resource.data
}
match /usernames/{username} {
allow get: if isSignedIn();
allow create: if username.size() >= 5 && username.size() <= 12 &&
username.matches("[[:alpha:]]*") == true &&
username.lower().matches(".*duck.*") == false;
allow delete: if isSignedIn() && request.auth.uid == existingData().userId;
}
It is supposed to allow the creation of a new document only if its ID is: (1) between 5-12 characters in length, (2) alphanumeric, and (3) not a bad word.
The problem is that this rule does not prevent usernames with forward slashes at the beginning or the end of the otherwise valid text, e.g., usernames /John52 and John52/ are both allowed. The document ID that I can see in the Firebase console is still John52 (without slashes), so I suspect that perhaps Firebase somehow treats those slashes in a special way, or drops them, and the {username} in the security rule has no slashes anymore.
I know that document IDs should not contain forward slashes. However, if the ID gets set from the client, what is the best way to prevent it, and is it possible to do with the security rules?
However, if the ID gets set from the client, what is the best way to prevent it, and is it possible to do with the security rules?
First, the Firebase client SDK will remove the slashes from the document ID before that create/update document request is sent so value of username will never contain /.
If someone tries to use the Firestore REST API and bypass the SDK, they'll get an error says:
{
"error": {
"code": 400,
"message": "Resource id \"/test\" is invalid because it contains \"/\".",
"status": "INVALID_ARGUMENT"
}
}
The best you can do is replace / with any other character (e.g. =) but render it as a slash on frontend (i.e. .replace('=', '/')).
Alternatively, you can store all the username in an array but a document only has 1 MB max size limit and won't scale well. Routing write requests through a Cloud Function would be another option where you can encode the username into a base64 string and use it as document ID. You can then decode it while rendering.
The client SDK will remove only trailing or leading slashes.
const docRef = doc(db, 'books', '/test/')
console.log(docRef.id) // <-- 'test'
await setDoc(docRef, { ... })
If you have multiple forward slashes and create odd number of path segments, then it'll throw an error as the path would represent a collection and not a document.

Flutter Firestore Security Rules

I've been trying to get the Firestore rules to play nice for a while now and every time I think I get them right, another portion stops working for some reason.
I'm trying to have some simple rules, if you made the document that document and any child documents or collections, you can create, edit and delete them. I thought this was pretty simple but alas I keep getting permission denied errors.
Rules:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read, update, delete: if request.auth != null && request.auth.uid == userId;
allow create: if request.auth != null;
}
match /users/{userId}/{document=**} {
allow read, update, delete: if request.auth != null && request.auth.uid == userId;
allow create: if request.auth != null;
}
}
}
When doing just the match /users/{userId} I was able at one time able to create user documents but I couldn't create child documents or colletions.
When doing just the match /users/{userId}/{document=**} I could no longer create users but any existing users I could add child documents and collections and do everything expected.
This combination of both rules doesn't seem to work either.
I keep getting [cloud_firestore/permission-denied] The caller does not have permission to execute the specified operation. when I try to create a user with this statement:
await FirebaseFirestore.instance.collection('users').doc(googleUser.uid).set(
{
'created': now,
'lastLogin': now,
'name': name,
'email': email,
},
);
Now nothing works. I deleted all my authentication accounts and my Firestore data and wanted to start over but it simply will not create the data in Firestore.
Any suggestions would be greatly appreciated as I'm going in circles and nothing is working anymore which is extremely frustrating as it did at one point but no longer does.
edit All of my testing is being done on a real Android phone.
After walking away from my computer and thinking more, I figured out what it was. My App Check debug token changed somehow.
Once I added the new value from the debug console everything started working again.
I'll leave this answer here in case this saves anyone else some headaches in the future!
Edit: Additionally, ones App Check debug token will change anytime you clear storage on your app on the device. Which is why mine was changing.

User can't write to Firestore database even though he is authorized

I created a rule in Cloud Firestore to read/write based on wether the user is signed in through Firebase Auth or not.
From my understanding based on what I read in the official documentation, the following code should allow the signed in user the correspodent permissions to the userID document inside the data collection.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/document {
match/data/{userId}{
allow read, write: if isSignedIn();
}
}
function isSignedIn(){
return request.auth != null;
}
}
Database image:
The idea is that after the user logs in, the code I wrote should verify if there is a document called ReservedID in data/userID/ReservedID, and if there isn't, create one for him, however, this collection is never created.
It does work if I remove the security rules.
Image of the error that shows in Android Studio:
However, after signing in using mAuth.signInWithEmailAndPassword, the user still can't write or read from the database. The Android Studio Logcat provides this message:
PERMISSION_DENIED: Missing or insufficient permissions.
Did I misunderstood how to properly set these rules in my database? Or could it have something to do with the code itself?
It looks like there are some issues with the code.
The match statement should specify the path to the collection and whole documents, rather than just one document. i.e you are using the path for single document match /databases/{database}/document instead of below path:
match /databases/{database}/documents {
match/data/{userId}{
}
With the above changes, the code will look like this:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match/data/{userId}{
allow read, write: if isSignedIn();
}
}
function isSignedIn() {
return request.auth != null;
}
}
You can verify this on playground

How do i write a Firebase Storage security rule that only allows write file permissions for auth users to a nested directory and none its parents?

I have the following security rule set for a Firebase Storage bucket:
rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
match /avatars/{userId}/{profileId}/{allPaths=**} {
allow write: if request.auth != null && request.auth.uid == userId
}
}
}
This works assuming the dynamic subdirectory userId matches the request.auth.uid.
However, this rule also allows that same user to write a file to the subdirectory {userId} and to write additional directories into profileId.
Id like to allow the authenticated user to generate both of the wildcard subdirectories as i have specified, but ONLY allow FILES to be uploaded into the last directory: profileId.
So...
attempting to upload a file into /avatars/${userId}/{profileId}/file.jpg should succeed.
attempting to write a file into /avatars/${userId}/file.jpg should fail.
attempting to create a directory in /avatars/${userId}/{profileId}/directoryName should fail.
attempting to upload a file into /avatars/${userId}/{profileId}/directoryName/file.jpg should fail.
Is this not possible?
Firstly, it's important to understand that Cloud Storage does not have "directories". There is no operation to create a "directory". There are only objects with paths that can have / separators to make it easier for you to organize content.
The reason why users can write to nested paths under profileId is because you are using a trailing recursive wildcard match {allPaths=**}, which allows writes under any possible path under profileId. I suggest reviewing the documentation on wildcards to better understand the behavior. It sounds like you don't want a recursive wildcard match at all, and instead just a single path segment match, e.g. {imageId}.

Meeting permission error on my Firestore rule accessing other user's path

I'm testing permission for accessing user A's document from login user B.
User B already saved uid_A on it's path /userData/uid_B/subscriber/uid_A.
In this situation, I'd like to access A's document by checking subscriber in Firestore rule.
service cloud.firestore {
..
match /databases/{database}/documents {
match /userData/{uid}/{document=**} {
allow create, read, update, delete: if exists(/databases/{database}/documents/userData/$(request.auth.uid)/subscriber/$(uid));
}
}
}
But following Flutter code fails with this message:
6.33.0 - [Firebase/Firestore][I-FST000001] Listen for query at userData/uid_A failed: Missing or insufficient permissions.
await db.collection('userData').doc('uid_A').get();
The reason user B want's to access user A's document is to add uid_B as user A's provider, so user A can easily list up his accessible documents on screen.
I wonder what's causing this issue and how to resolve this kind of problem.
Thanks for advance.
Your syntax for the document to check with exists() isn't correct. The full path includes more, as shown in the documentation:
exists(/databases/$(database)/documents/userData/$(request.auth.uid)/subscriber/$(uid));