Calvin
Calvin

Reputation: 377

Cloud Firestore security rules not working

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

}

working in playground

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

Upvotes: 3

Views: 746

Answers (1)

Frank van Puffelen
Frank van Puffelen

Reputation: 600006

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.

Upvotes: 2

Related Questions