6

In the documentation it's stated that you can query by comparing string to another string value e.g:

citiesRef.where('name', '>=', 'San Francisco');
citiesRef.where('state', '>=', 'CA').where('state', '<=', 'IN');

Then there is this roles part in the documentation that shows how to apply roles in Firestore documents. However it's not shown how to query this.. But as shown in above example, this should work like following:

citiesRef.where(`roles.${user.uid}`, '>', '');

so this query above, should return all documents where is any value bigger than empty string, right?

In my code I have organizations collection with one document:

{
  "name": "MyAmazingCompany",
  "roles": {
    "my-user-uid": "owner"
  }
}

And if I try to query organizations where I have some role like this:

organizationsRef.where(`roles.${user.uid}`, '>', '');

I'll just get Uncaught Error in onSnapshot: Error: Missing or insufficient permissions. in my browser console (using firebase npm package version 5.1.0 and tried also 5.0.3).

Just to make sure that I should have access to that document, following query is tested, it works and it returns that one organization.

organizationsRef.where(`roles.${user.uid}`, '==', 'owner');

So what is wrong?

Also here is someone claiming it should work: Firestore select where is not null

And here are my rules:

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

    match /organizations/{organizationId} {
      allow read: if isOneOfRoles(resource, ['owner']);
      allow write: if isOneOfRoles(resource, ['owner']);
    }
  }
}

Like I said, it works if I compare if the role is owner, but I want to get results if user's uid exists in the roles array, no matter what role she is having.

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
lehtu
  • 868
  • 1
  • 12
  • 27

2 Answers2

3

I've also found this part of the documentation confusing (or rather, lacking in quite essential details). Firestore security requires that you follow very specific design patterns regarding you data structure. This comes from the fact that Firebase Security Rules language has less capabilities when expressing rules than simple and compound query expressions you can run against your data.

When trying to secure my database, I gradually had to give up my original data abstraction ideas, and start again from the limitations that the security language imposes. Instead of the proposed roles representation in Firestore documentation (https://firebase.google.com/docs/firestore/solutions/role-based-access), I started to apply the following pattern. Code adopted to your use case.

{
  "name": "MyAmazingCompany",
  "roles": {
    "anyRole": ["user-id-1", "user-id-2", "user-id-3", "user-id-4"],
    "owners": ["user-id-1"],
    "editors": ["user-id-2"],
    "readers": ["user-id-3", "user-id-4"],
  }
}

So, in essence, different role levels are the properties of the role object, and users belonging to a given role stored as an array of ids under the role property. There is an additional meta role called anyRole, which serves as a convenience collection to make queries easier.

Firebase security rules are as follows:

rules_version = '2';

service cloud.firestore {
  match /databases/{database}/documents {
        match /organizations/{organization} {
    
      function hasReadPermission(res) {
        let anyRole = request.auth.uid in res.data.accessors.anyRole;
        return anyRole;
      }
      
      function hasWritePermission(res) {
        let owner = request.auth.uid in res.data.accessors.owners;
        let editor = request.auth.uid in res.data.accessors.editors;
        return owner || editor;
      }

      allow read: if hasReadPermission(resource);
      allow write: if hasWritePermission(resource);
    }
  }
}

Finally, querying data is quite simple with array-contains on roles.anyRole

Get all documents the user has access to:

organizationsRef.where(`roles.anyRole`, 'array-contains', user.uid);

Get all documents owned by the user:

organizationsRef.where(`roles.owner`, 'array-contains', user.uid);
András Szepesházi
  • 6,483
  • 5
  • 45
  • 59
1

One problem with your rules may be how you are checking the roles map. It will throw an exception if you try to access a map key that does not exist.

function getRole(rsc) {
  // Read from the "roles" map in the resource (rsc).
  return rsc.data.roles[request.auth.uid]; // this will throw an exception if the uid isn't in the map
}

In the rules language, maps have a get("key", "default-value") method that helps make it safe to do what you are doing. So this might help:

function getRole(rsc) {
  // Read from the "roles" map in the resource (rsc).
  return rsc.data.roles.get(request.auth.uid, null);
}

function isOneOfRoles(rsc, array) {
  // Determine if the user is one of an array of roles
  return isSignedIn() && getRole(rsc) != null && (getRole(rsc) in array);
}

I haven't run your code in firebase to verify that there are no other issues, but this is at least one issue you may be hitting.

Grahambo
  • 467
  • 5
  • 11