Using the user's email in Firestore Rules? - firebase

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?

Related

Creating security rule depend on doc data

I just added new value to my users collection appVersion
My user collection like:
documents/users/5kwgNgGi3sY6oCbUAg9v
so i just added value versionCode to collection. if user download new app it will be update
My rule now:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth !=null
}
}
}
what i need as a if else statement
if(request.auth!=null){
firebase().collection('users').where('UID','==',request.auth.uid).get().then(x=>{
if(x.docs[0].versionCode==4){
allow
}
else{
deny
}})
else{
deny
}
There is no way to perform a query in your security rules, as that would never scale on the backend.
Instead, what you'll want to do is store the user documents with the UID as the document ID, so that the document you show is at path documents/users/xHzKoXbrZqbHZY8DI3wkXnMShF92. Then you can use get() in your rules to load the document, as shown in the documentation on accesing other documents in your rules.
I'd also recommend checking out the documentation on attribute-based and Role-based access, as it covers a pretty similar scenario.

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);
}
}

where cluase not working after security rule in google cloud

this is my security rule for firebase, it throwing permission for query :
app.firestore().collection('users').where('email', '==' ,'test#gmail.com').get().then(snap => console.log(snap.docs[0].data())).catch(err => console.log("err",err));
the above query is throwing permission error!!
service cloud.firestore {
match /databases/{database}/documents {
// Make sure the uid of the requesting user matches name of the user
// document. The wildcard expression {userId} makes the userId variable
// available in rules.
match /users/{userId} {
allow read, update, delete: if request.auth.uid == userId;
allow create: if request.auth.uid != null;
}
}
}
why ?
Your security rule is saying that a user can only read the document in users with an id that's the same as their authenticated UID. But your query is trying to read any document where the email field has some value. Since that query could possibly try to read documents other than the one that matches the UID, the query will fail every time. With the rules you have now, the client is only allowed to get() their own specific document, with no other queries allowed.
You have to remember that security rules are not filters.
Example:
my uid: 123123
doc1:
email: test#gmail.com, id: 123123
The securtiy rule will pass for doc1.
doc2: email: test#gmail.com, id: 321321
The security rule will fail for doc2.
That is because your security rule is saying that if ALL documents have the uid in then it should pass, but because it is not, it fails.
To fix this, you would need to provide some further information on how your db is setup and what you are querying. Ie, you are finding all emails which match a criteria, or something else.

In Firebase storage, can I use db "get" rules?

I'm trying to secure my Firebase (google cloud storage) files based on user data. In firestore, I use a rule based on getting database content (look up the uid in users table and match a field) and that's working fine. I'm trying to use the same kind of rules in firebase storage but in the simulator I get Error: simulator.rules line [12], column [17]. Function not found error: Name: [get].; Error: Invalid argument provided to call. Function: [get], Argument: ["||invalid_argument||"]. My rules look like this:
match /b/{bucket}/o {
function isAuth() {
return request.auth != null && request.auth.uid != null
}
function isAdmin() {
return isAuth() &&
"admin" in get(/databases/$(database)/documents/users/$(request.auth.uid)).data.roles;
}
function clientMatch(client) { // expects user's "client" field to be ID of client
return isAuth() &&
client == get(/databases/$(database)/documents/users/$(request.auth.uid)).data.client;
}
match /P/Clients/{client}/{allPaths=**} {
allow read, write: if isAdmin() || clientMatch(client);
}
}
}
Line 12 is the one beginning client == get, in clientMatch().
I haven't been able to tell whether these functions are only supported for Firestore (db) rules, or whether they should work for storage as well.
If this doesn't work, what are my options? How do people look up user data for Firebase storage security?
You currently can't reference Firestore documents in Storage rules. If you would like to see that as a feature of Storage rules, please file a feature request.
Consider instead using a Cloud Functions storage trigger to perform some additional checks after the file is uploaded, and remove the file if you find that it's not valid.
This is now possible with cross-service security rules. You have to use firestore. namespace before the get() and exists() functions as shown below:
function isAdmin() {
return isAuth() && "admin" in firestore.get(/databases/$(database)/documents/users/$(request.auth.uid)).data.roles;
}
Do note that you'll be charged for read operations just like in Firestore security rules.

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.

Resources