5

I tested this security rules structure in the Firebase console for Firestore, and I think it does what I want, namely to keep a map of roles for users at the organization level and enforce permissions for organization subcollections by always referencing the parent organization using the get() method.

service cloud.firestore {
  match /databases/{database}/documents {

    // ORGANIZATIONS collection
    match /organizations/{organization} {

      function isSignedIn() {
        return request.auth != null;
      }
      function getRole(rsc) {
        // Read from the 'roles' map in the resource (rsc)
        return rsc.data.roles[request.auth.uid];
      }
      function isOneOfRoles(rsc, array) {
        // Determine if the user is one of an array of roles
        return isSignedIn() && (getRole(rsc) in array);
      }

      allow write: if isSignedIn();
      allow read: if isSignedIn() && isOneOfRoles(resource, ['owner']);

      // PROJECTS subcollection
      match /projects/{project} {

        allow write: if isSignedIn() && isOneOfRoles( get(/databases/$(database)/documents/organizations/$(organization)), ['owner']);
        allow read: if isSignedIn() && isOneOfRoles(get(/databases/$(database)/documents/organizations/$(organization)), ['owner', 'member']);
      }
    }
  }
}

Now, because queries must match security rules, I'm trying to understand how to make a query using the Javascript client that will match the above security rules.

I have a working query to get the organizations that looks like this:

db.collection('organizations').where(`roles.${currentUser.uuid}`, '==', 'owner').get()

which is based on a document structure like the following:

organizations (collection)
  {organization} (document)
    name: string,
    roles: { 
      <user_id_1>: "owner",
      <user_id_2>: "member"
    },
    projects (subcollection)

Is it possible to have a query (or even multiple queries) in the Javascript client that checks that user is a certain role at the organization level AND checks document properties within the projects subcollection, similar to what the security rules do?

EDIT: Ultimately, I think this is a question of authorization and whether or not Firestore supports my approach. It seems that my best bet at this point is the recursive wildcard syntax - match /organization/{document=**} - but because matching any condition allows access to all nested documents, this approach appears less secure.

Ideally, I could specify a collection path - e.g., organizations/{organization}/projects - in my client-side query and have a way to specify that the {organization} roles must be checked before granting access to the projects subcollection.

POTENTIAL SOLUTION: The following security rules structure leaves a lot to be desired, but it seems to be an improvement. Instead of paths to match specific subcollections, I'm just using recursive wildcard syntax. Does anyone know of a better solution?

service cloud.firestore {
  match /databases/{database}/documents {

    function isSignedIn() {
      return request.auth != null;
    }
    function getRole(rsc) {
      // Read from the 'roles' map in the resource (rsc)
      return rsc.data.roles[request.auth.uid];
    }
    function isOneOfRoles(rsc, array) {
      // Determine if the user is one of an array of roles
      return isSignedIn() && (getRole(rsc) in array);
    }

    // ORGANIZATIONS
    match /organizations/{organization} {

      allow write: if isOneOfRoles(resource, ['owner']);
      allow read: if isOneOfRoles(resource, ['owner', 'member']);

    }

    // Any subcollections under ORGANIZATION
    match /organizations/{organization}/{sub=**} {

      allow write: if isOneOfRoles(get(/databases/$(database)/documents/organizations/$(organization)), ['owner']);
      allow read: if isOneOfRoles(get(/databases/$(database)/documents/organizations/$(organization)), ['owner', 'member']);

    }
  }
}
Geoffrey
  • 61
  • 4
  • I think this would require an `OR` on multiple values in the same field, which isn't supported at the moment. See for example: https://stackoverflow.com/questions/46726673/firebase-firestore-or-query. – Frank van Puffelen Dec 12 '18 at 00:49
  • @FrankvanPuffelen I was imagining pseudocode looking something like: get `projects` where the parent `organization` has a `roles` map property containing a user property that matches the requestor's ID and having a value of 'owner' or 'member'. I'm not seeing where the `OR` comes in. Can you explain what you are thinking, please? Firestore recently introduced the `array-contains` operator that can be used for the last part, if that's what you're thinking. – Geoffrey Dec 12 '18 at 01:14
  • "having a value of 'owner' **or** 'member'" That's where an OR comes in. ;-) The `array-contains` allows you to check if an array of values contains a single specific value you're looking for. If you can map your use-case to a data model that fits, it's a great use of the feature. But I didn't see such a mappign. – Frank van Puffelen Dec 12 '18 at 02:12
  • Thanks, that's what I thought. Unfortunately, my question is more about how to access the organization information at all, not just checking the user's role in an array. – Geoffrey Dec 12 '18 at 02:50

0 Answers0