I am trying to improve my Firestore database security, but I am getting desperate with this. I have tried editing it for hours to try if it would work, but it keeps erroring or denying access. The rules playground indicates it should work.. but it doesn't.
I am trying to access /environment/STABLE/route_sets/w3TBJrovQbgFPr2GFfug with user /environment/STABLE/users/RMwv3mmP4RIOVqPOsa77. User has a field called db_role containing the string ADMIN. The document in route_sets contains an array called _permissions_read with the values TEST_NOTHING and ADMIN.
Testing it with rules playground works, but when I try it via my Angular app, I get "ERROR FirebaseError: Missing or insufficient permissions.". For authentication, I am using Firebase Authentication with a custom provider, uid is document ID in users table.
The info on the tab "monitor rules" on Cloud Firestore -> Rules seems to indicate that the current rules (as below) are generating an error when trying to access a route_set.
rules_version = "2";
service cloud.firestore {
match /databases/{database}/documents {
match /environment/{environment}/{collectionName}/{documentId} {
allow read, write: if request.auth != null && collectionName == "route_sets" && checkPermissionRead(environment, collectionName, documentId);
allow read: if request.auth != null && collectionName != "route_sets";
allow write, delete: if request.auth != null;
function checkPermissionRead(environment, collectionName, documentId) {
return get(/databases/$(database)/documents/environment/STABLE/users/$(request.auth.uid)).data.db_role in get(/databases/$(database)/documents/environment/$(environment)/$(collectionName)/$(documentId)).data._permissions_read;
}
}
match /environment/STABLE/licenseholders/{document=**} {
allow read: if true
allow read, write: if request.auth != null;
}
match /environment/STABLE/users/{document=**} {
allow read: if true
allow read, write: if request.auth != null;
}
}
}
Angular app code:
import {Component, OnDestroy, OnInit} from '@angular/core';
import {AngularFirestore} from '@angular/fire/firestore';
import {AngularFireAuth} from '@angular/fire/auth';
@Component({
selector: 'app-example-security-rules',
template: `<span *ngFor="let set of routeSets"></span>`,
styles: [``],
providers: [ ]
})
export class ExampleSecurityRulesComponent implements OnInit {
routeSets = [];
constructor(private db: AngularFirestore,
public afAuth: AngularFireAuth) {
}
ngOnInit() {
this.afAuth.onAuthStateChanged(async (user) => {
if (user) {
this.db.collection<any>('environment/STABLE/route_sets').get().subscribe((d) => {
d.forEach(docT => {
this.routeSets.push({__document__key: docT.id, ...docT.data()});
});
});
}
});
}
}
NEW code & security rules working:
Security rules
rules_version = "2";
service cloud.firestore {
match /databases/{database}/documents {
match /environment/{environment}/{collectionName}/{documentId} {
allow read: if request.auth != null && collectionName != "route_sets";
allow write, delete: if request.auth != null;
}
match /environment/{environment}/route_sets/{document=**} {
allow read, write: if request.auth != null && checkPermissionRead(environment);
function checkPermissionRead(environment) {
let user = get(/databases/$(database)/documents/environment/$(environment)/users/$(request.auth.uid)).data;
return user.db_role in resource.data._permissions_read && user.licenseholder_id == resource.data.licenseholder_id;
}
}
match /environment/STABLE/licenseholders/{document=**} {
allow read: if true
allow read, write: if request.auth != null;
}
match /environment/STABLE/users/{document=**} {
allow read: if true
allow read, write: if request.auth != null;
}
}
}
Angular
this.db.collection<any>('environment/STABLE/route_sets').ref.where('licenseholder_id', '==', environment.licenseholder_id)
.where('_permissions_read', 'array-contains', DatabaseRoles.ADMIN).get().then((d) => {
d.forEach(docT => {
this.routeSets.push({__document__key: docT.id, ...docT.data()});
});
});
If I understand the user-case correctly, you are trying to read the entire collection and then filter the individual documents in your rules.
That is not going to work, as rules are not filters. Instead rules merely check whether the read operation is allowed at the moment the query starts, without checking individual documents - as that would not scale in both performance and cost.
For that reason the get()
calls in your rules will only be effective when you're reading a single document, not when you're requesting a range of documents (known as a list
operation in rules).
If you want to securely read a range of documents, you'll have to build the correct query in your code and then secure that query using rules. Unfortunately, there's no way to have the equivalent of your get()
operations from the rules in a query.
The only way to securely perform this type of operation is to duplicate the permission data under each route_sets
document, so that your rules can check it there - and so that they can validate that you're passing the right conditions to the query to only request documents you're authorized for.