User Access Roles In Firestore - firebase

I have Firestore setup as shown in the images below.
I am trying ensure that a user can only have access to projects of a specific company provided the user is authenticated (email and password), email-verified and is part of the users collection (by verifying with UID) for that company based on a specific role (eg. manager, dancer, choreographer).
I have created some rules as shown below but it is not working as expected. Thank you in advance for the contribution.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
//create user creates own data in users collection
match /companies/company_id/users{
allow create: if request.auth != null && request.auth.token.email_verified == true;
}
//user should be allowed to create or delete own data.
//Only managers should be allowed to delete data of others
match /companies/company_id/users/{user_id}{
allow read, delete: if request.auth != null && request.auth.uid == user_id;
}
match /companies/{company_id}/projects {
allow write: if isManager(get(/databases/$(database)/documents/companies/{company_id}/users/$(request.auth.uid))) || isChoreographer(get(/databases/$(database)/documents/companies/{company_id}/users/$(request.auth.uid)));
allow read: if isDancer(get(/databases/$(database)/documents/companies/{company_id}/users/$(request.auth.uid)));
}
match /companies/{company_id}/project/{project_id} {
allow read, write: if get(/databases/$(database)/documents/companies/{company_id}/users/$(request.auth.uid)) == request.auth.uid;
}
function isManager(database){
return database.data.role == "manager";
}
function isChoreographer(database){
return database.data.role == "choreographer";
}
function isDancer(database){
return database.data.role == "dancer";
}
}
}
Any ideas?
EDIT
I have included a curl script I used trying to access one of the firestore rules.
curl --location --request GET 'https://firestore.googleapis.com/v1/projects/project_somthin/databases/(default)/documents/companies/company_a/users/MSTJMcwggcGwrXgEgwl5' \
--header 'Authorization: Bearer token_id'
I go the error:
{
"error": {
"code": 403,
"message": "Missing or insufficient permissions.",
"status": "PERMISSION_DENIED"
} }

Related

How to write security rules for users setting documents that don't exist?

When a user registers, a document should be set in Firestore (database/users/${uid}). However, I keep getting a "Missing or insufficient permissions." error.
This is the most relevant security rule
match /users/{documents=**} {
allow read, create, update: if request.auth != null && request.auth.uid == resource.id
}
Another rules I tried implementing was
match /users/{uid=**} {
allow read, create, update: if request.auth != null && request.auth.uid == uid
}
and this is the code that registers the user and sets the document
createUserWithEmailAndPassword(auth, emailInput, passwordInput).then(
(UserCredential) => {
console.log(UserCredential);
uid = UserCredential.user.uid;
sendEmailVerification(auth.currentUser).then(() => {
toast.success(
"Account created and email verification sent! Please check your inbox/spam folder",
{
duration: 10000,
}
);
setDoc(doc(db, "users", uid), {
userSettings: ["example", 0],
});
router.push("/verify");
});
}
);
Why can't I set a document as an authorized user accessing my own user document?
The problem is request.auth.uid == resource.id. From the documentation,
The resource variable refers to the requested document, and resource.data is a map of all of the fields and values stored in the document.
But the document does not exists as user has just registered to your application.
Try the following rules instead:
match /users/{userId} {
allow read, create, update: if request.auth != null && request.auth.uid == userId;
}
This rule will ensure that a user can create/read/update a document with their user ID only.
Also do note that the match path is /users/{userId} and not match /users/{userId=**} as in your question. The value of userId would be /userID and not just userID if you use the recursive wilcard (=**) and rule will fail always.
If the rule must be applied for all nested collections, then use the recursive wildcard on the next path segment:
match /users/{userId}/{path=**} {
// ... can still read userId
}

Firestore: Prevent write/overwrite/update field once it is added

I have a collection in which I am storing user requests in documents having documents ID as user's email. In the document, I am creating fields the key for which is being generated at client side.
Now, the problem that I am facing is that user can overwrite the existing field/request in the document if the key matches which I don't want to happen.
What I tried was to use this rule which unfortunately does not work
resource.data.keys().hasAny(request.resource.data.key();
So how can I achieve this?
Below are the screen shot of the firestore data and the current security rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /roles/{userId}{
allow read: if isSignedIn() && hasId(userId);
}
match /requests/{email} {
allow read, update: if isSignedIn() && hasMail(email)
}
//functions//
function hasMail (email) {
return request.auth.token.email == email;
}
function hasId (userId) {
return request.auth.uid == userId;
}
function isSignedIn () {
return request.auth != null;
}
function getUserRole () {
return get(/databases/$(database)/documents/roles/$(request.auth.uid)).data.role
}
}
}
You can check if a resource already exists. Here an example:
allow write: if resource == null // Can create, not update
Use that to restrict any edit or update of the data. If you have additional rules you can granulate them to update, delete and create.

How to manage rights in firebase to allow differents users to read/upate/write?

I've a firestore database and I now need to add a new collection.
Each entry of this collection should contain:
Which userId is the owner(field admin)
Which userId has been allowed to edit this element(field writer)
Which userId has been allowed to only read(field reader).
I'm currently only at the first step, and already strugling:
I was hoping to be able to query my collection( /trips/) and get only the one that I'm allowed to access, but I get an error:
FirebaseError: Missing or insufficient permissions.
Here is my rules file:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
match /users/{userId} {
allow read, update, delete: if request.auth != null && request.auth.uid == userId;
allow create: if request.auth != null;
}
match /trips/{trip} {
allow read, update, delete: if request.auth != null && request.auth.uid == resource.data.admin;
allow create: if request.auth != null;
}
}
}
So my questions:
Is this the correct way of managing resource that must be acceeded by multiple people(meaning, I cannot just have the userId in the path since there are multiple users)
How should I query only the documents list that I'm allowed to see?
Thank you very much for your help
As you will read in the doc, "All match statements should point to documents, not collections".
With
service cloud.firestore {
match /databases/{database}/documents {
match /trips {
// ....
}
}
}
you don't point to a document. You should use a wildcard to point to any document in the specified path, as follows:
service cloud.firestore {
match /databases/{database}/documents {
match /trips/{trip} {
// ....
}
}
}
Therefore the following should correctly implement your requirements:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /trips/{trip} {
allow read: if request.auth != null &&
(request.auth.uid == resource.data.admin
|| request.auth.uid == resource.data.writer
|| request.auth.uid == resource.data.reader
);
allow update: if request.auth != null &&
(request.auth.uid == resource.data.admin
|| request.auth.uid == resource.data.writer
);
allow create: if request.auth != null;
}
}
}
Then, for the two questions:
Is this the correct way of managing resource that must be acceeded by multiple people (meaning, I cannot just have the userId in the path
since there are multiple users)
If the admin, writer and reader are specific for each document, yes this is the correct way. If those roles would be more global (e.g. all the trips to Europe can be edited by the same user), you could use a role based approach with Custom Claims.
How should I query only the documents list that I'm allowed to see?
It is important to note that rules are not filter. So your query for getting docs needs to be aligned with the rules. In your specific case, you could have an additional field of type Array which contains three values; the uids of the admin, writer and reader, and use the array-contains operator. Something like:
const user = firebase.auth().currentUser;
const query = db.collection("trips").where("authorizedReaders", "array-contains", user.uid);
match /{document=**} {
allow read, write: if false;
}
You don't need the above code as it will apply to all routes of the database, because of the above line you are getting the below error as it does not allow you to read and write to the database
FirebaseError: Missing or insufficient permissions.
Now, if you want to assign privileges to users then you should add the Role field to users collections which would have a value such as Admin, Editor, Reader
Then, you can check in routes something like below
match /users/{userId}/trips/{tripId} {
allow read, delete: if request.resource.data.role == "Admin";
allow create, update: if request.resource.data.role == "Admin || request.resource.data.role == "Editor";
}
If you want to know more about how to create a route check out this video for the best explanation

How to update Firestore Rules with custom Roles

How to configure RULES to control Reads/Writes and Deletes on a collection (RECORDS) based on custom user roles defined in another collection (USERS)?
---- User Collection ----
USERS: {
<RandomID1> : { uid: 234, email: "abc#xyz.com", role: "ENDUSER" },
<RandomID2> : { uid: 100, email: "def#xyz.com", role: "ADMIN" }
}
---- Records Collection ----
RECORDS: {
<RandomID1> : { uid: "234", name: "Record 123" },
<RandomID2> : { uid: "234", name: "Record 456" },
<RandomID3> : { uid: "999", name: "Record 999" } /* another user's record */
}
---- Current Rules ----
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if request.auth != null;
}
}
}
Assuming the user is logged in the web application and using client-side Firebase SDK, how to achieve the below cases?
IFF: USERS.RandomID1.role = 'ENDUSER'
How to restrict the user to READ only their records from RECORDS,
but not somebody else's?
How to restrict UPDATEs to only their
records in RECORDS?
How to restrict DELETEs on all of their records in RECORDS?
How to restrict all CRUD operations on the rest of collections(/COLLECTION**), except RECORDS?
IFF: USERS.RandomID1.role = 'ADMIN'
How to enable this (Admin) user to perform all CRUD operations in
RECORDS?
So, how to rewrite or update rules to control these operations? If not, are there better designs or alternatives?
Note: We need to handle these cases to block some users/hackers who may try to open browser console/inspect window, and execute firestore queries with or without any conditions.
I appreciate your help!
For 5 you will need to define custom claims for your admins, below I assume a field isadmin is set to true for admins.
The following rules should be a good start:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// 4: Will restrict all others access
match /records/{recordid} {
// 1+5(read): restrict reads
allow read: if request.auth != null
&& (resource.data.uid == request.auth.uid || request.auth.token.isadmin);
// 2+3+5(write): +create as a bonus
allow write: if request.auth != null
&& (request.resource.data.uid == request.auth.uid || request.auth.token.isadmin);
}
}
}
Here is my final answer:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
//Rule on a Collection Groups, instead of simple Collection
match /{NestedSubCollections=**}/<COMMON_COLLECTION-NAME>/{doc} {
allow create: if isSignedIn() && isAdmin();
allow read: if isSignedIn() && (isAdmin() || isThisUserRecord(resource.data.<CUST_USER-ID-FIELD>));
allow update: if isSignedIn() && (isAdmin() || isThisUserRecord(resource.data.<CUST_USER-ID-FIELD>));
allow delete: if isSignedIn() && isAdmin();
}
//Collection UserDB, to validate profile updates
match /UserDB/{userId=**} {
allow create: if isSignedIn() && isAdmin();
allow read: if isSignedIn() && (isAdmin() || hasUserProfile(resource.data.<CUST_USER-ID-FIELD>));
allow update: if isSignedIn() && (isAdmin() || hasUserProfile(resource.data.<CUST_USER-ID-FIELD>));
allow delete: if isSignedIn() && isAdmin();
}
//Checking if user is signed-in
function isSignedIn() {
return request.auth != null;
}
//Checking user's admin status, without the context of accessing collection, but directly
function isAdmin() {
return get(/databases/$(database)/documents/UserDB/$(request.auth.uid)).data.<CUST_ROLE-FIELD> == "<ADMIN-ROLE-NAME>";
}
//Check whether a current record belong to logged-in user; by <CUST_USER-ID-FIELD>, not oAuth-ID
function isThisUserRecord(custUserId) {
return get(/databases/$(database)/documents/UserDB/$(request.auth.uid)).data.<CUST_USER-ID-FIELD> == custUserId;
}
//Check whether a logged-in user has existing profile or not
function hasUserProfile(userOauthId) {
return userOauthId == request.auth.uid;
}
}
}
It is working perfectly fine in my production application and everything is secured without writing a single line of code in the application!
How to test it?
Updated a user record in UserDB with <CUST_ROLE_FIELD>, and <ADMIN_ROLE_NAME> as value
Then go to your frontend web application, go to browser Console, and trying to hit Firestore queries and validate the records and results
If it's a non-admin user, then he can only Read/Update as per our rules above, and the other queries will fail as expected.
Note, the create/delete is performed in the backend, via cloud function, for security reasons.
In sum, admin can perform any action on all records, but non-admin users can only touch his own records with limited Read/Update operations only.
You may have to tweak a bit based on other use cases. Let me know if anything.

How to set Firestore security rules? resource.data: Null value error

I need some help making my security rules for firestore work.
These are my firestore rules:
service cloud.firestore {
match /databases/{database}/documents {
match /orders/{orderID} {
allow read, update: if request.auth.uid == resource.data.buyerId || request.auth.uid == resource.data.sellerId;
}
}
}
my orders collection:
orders: {
sellerId: 'some-id',
createdAt: timestamp,
buyerId: 'some-id'
}
It should return all documents from orders collection which has either buyerId or sellerId equal to authorised user (request.auth.uid).
but the above rule is not working as expected.
firestore collections screenshot
firebase simulator output
That error message is suggesting that the requested document was not actually present in the database. You entered "orders/{orderId}", which looks like you put a wildcard in the Location field in the simulator. That's not going to work. You need to enter the path to an actual document that exists if you want to test your rule that uses its field values.
resource.data: Null - this error happens when you try to create a new entity.
Split write rule, on create and update.
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /user/{userId} {
allow read: if request.auth.uid == userId;
function authed() {
return request.auth.uid == userId;
}
allow create: if authed() && request.resource.data.keys().hasOnly(['name']);
allow update: if authed() && request.resource.data.diff(resource.data).changedKeys().hasOnly(['name']);
allow delete: if authed();
}
}
}

Resources