How to update Firestore Rules with custom Roles - firebase

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.

Related

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

Firebase security rules syntax with arrays/list property

Within a Firebase Firestore collection with path 'organizations' each document contains a list of string userID's of users who can update or delete that document.
export interface Organization{
name?: string,
owners: string[]
}
I would like to create a Firebase security rule that ensures that only a logged in user with a uid that is in this list can edit or delete the object. Unsure of the appropriate syntax.
service cloud.firestore {
match /databases/{database}/documents {
match /organizations/{organization} {
allow read: if true;
allow create: if request.auth != null;
/// What should be the syntax here?
allow update, delete: if request.auth != null && (request.auth.uid in resource.data.owners); // <--------- What should be the syntax for this line?
}
Ok, answering my own question here in case it's useful for anyone else.
It looks like the 'in' syntax above actually works even-though it was a complete guess and I wasn't able to find any documentation for it in the firebase security roles documentation.
Final code:
service cloud.firestore {
match /databases/{database}/documents {
match /organizations/{organization} {
allow read: if true;
allow create: if request.auth != null;
allow update, delete: if (request.auth != null) && (request.auth.uid in resource.data.owners);
}

Firebase - Cloud Firestore, Problems creating rule for admin

Im having a problem with creating firebase rules, I created a field "role" in my 'employees' collection, so there, I want to distinguish each type of users allowed to read/write, etc in the DB. My problem is that for some reason I cant verify if the user trying to read others information is really the admin, in the "isAdministrator" function I cant make the get's to work:
service cloud.firestore {
// Function: Check if an authenticated employee is admin
function isAdministrator(){
//return get(/databases/$(database)/documents/employees/$(request.auth.uid)).data.role == 'admin'
// || get(/databases/$(database)/documents/employees/$(request.auth.uid)).role == 'admin';
return false;
// (data.child('role').val() == 'admin')
}
// Function: Check if an employee is authenticated
// RULES
match /databases/{database}/documents {
// Allows only admins and 'owner' to acess his document
match /employees/{document=**} {
allow read, write, update: if request.auth.uid == resource.data.uid || isAdministrator();
allow create, delete, list: if isAdministrator();
}
}
}
This is how my collection looks like:
match /databases/{database}/documents must wrap everything, including the function, like this:
service cloud.firestore {
match /databases/{database}/documents {
// FUNCTIONS
// Function: Check if an authenticated employee is admin
function isAdministrator(){
return get(/databases/$(database)/documents/employees/$(request.auth.uid)).data.role == 'admin'
|| get(/databases/$(database)/documents/employees/$(request.auth.uid)).role == 'admin';
}
// RULES
// Allows only admins and 'owner' to acess his document
match /employees/{document=**} {
allow read, write, update: if request.auth.uid == resource.data.uid || isAdministrator();
allow create, delete, list: if isAdministrator();
}
}
}

Resources