0
votes

I know that there are a lot of threads available about creating a role system on firestore using security rules, but I still am unable to do it. I want to be able to have three different roles, with three different access levels.

  • Administrator: Can read and write anything within the organisation
  • Manager: Can read anything within the organisation, but not write unless it is their own document
  • Employee: Can read and update their own data

I have found the documentation by google on this topic(https://firebase.google.com/docs/firestore/solutions/role-based-access), but I don't want every manager or administrator at every user and estabishment, because that is way too much duplicated data. Also, the roles should apply to anywhere within the organisation, it should not be different per subcollection.

My database:

organisations{
    organisation1{
    <data about organisation>
       establishments{
           establishment1{
              <data about establishment>
           }
           establishment2{
               <data about establishment>
           }
       }
       people{
           user1{
               <data about user>
               userId: <UID from authentication>
               accountType: <Administrator, Manager or Employee>
           }
           user2{
               <data about user>
               userId: <UID from authentication>
               accountType: <Administrator, Manager or Employee>
           }
       }
    }
    organisation2{
    <data about organisation>
       establishments{
           establishment1{
              <data about establishment>
           }
           establishment2{
               <data about establishment>
           }
       }
       people{
           user1{
               <data about user>
               userId: <UID from authentication>
               accountType: <Administrator, Manager or Employee>
               documents{
                   some documents
               }
           }
           user2{
               <data about user>
               userId: <UID from authentication>
               accountType: <Administrator, Manager or Employee>
           }
       }
   }
}

I know it is quite a lot, but it might help someone understand my problem a bit better. An administrator(someone with an accountType of Administrator) should be able to read and write anywhere within the organisation(organisation1 or organisation2). A manager should only be able to read everything, but not write unless it is their own data(So they can read anything but have the same write permissions as an employee, they can only write documents and anywhere in subcollections of those documents that have their request.auth.uid as the userId field, like user1 in organisation2 should be able to write the document of user1, as well as every document in any subcollection)

I currently have the following, and I think I need to get something like this:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
  //Access levels:
  //Medewerker: Can only see their own user document and all subcollections
  //Beheerder: Can see any document within the organisation
  //Administrator: Can see and do everything

    function getAccountType(orgId, userId) {
      return get(/databases/$(database)/documents/organisations/$(orgId)/people/$(userId)).data.accountType;
    }
    function isAdmin(orgID){
      return getAccountType(orgID, request.auth.uid) == "Administrator";
    }
    function isBeheerder(orgID){
      return getAccountType(orgID, request.auth.uid) == "Beheerder";
    }
    function isOwnData(userId){
      return request.auth.uid == userId;
    }
    function authenticated(){
      return request.auth != null;
    }
    match /organisations/{orgID} {
      //Administrator rule goes here
      allow write: if authenticated() && (isAdmin(resource.data.organisationId));
      
      //Beheerder rule goes here
      allow read: if authenticated() && (isAdmin(resource.data.organisationId) || isBeheerder(resource.data.organisationId));

      match /people/{userID} {
        //Medewerker rule goes here
        allow read: if authenticated() && (isAdmin(resource.data.organisationId) || isBeheerder(resource.data.organisationId) || isOwnData(resource.data.userId));
        allow update: if authenticated() && (isAdmin(resource.data.organisationId) || isOwnData(resource.data.userId));
        allow create, delete: if authenticated() && (isAdmin(resource.data.organisationId));
      }
    }
  }
}

The code that I have now doesn't give errors, but it doesn't behave the way I had expected. When I log in with an Administrator account, I first see only my own user record. If I log in with a Beheerder or Medewerker account, I don't get any records at all, not even my own. Also, if I change my account back to Administrator, I don't get my own record back. Is there any way to fix this/what is wrong with my code? Also, do I need an organisation ID in every document, or is there a workaround for it? My current code did have that. Thanks for your help!

1

1 Answers

2
votes

As this question is fairly large, here is two things:

1. A sample project with 3 level access roles (made for this question)

The full open source sample is here, feel free to clone/fork & contribute if needed.

Firestore model

  • organizations (collection)
    • organization1 (doc)
      • organizationName (field)
      • createdAt (field)
      • establishments (collection)
        • establisment 1 (doc)
        • establisment 2 (doc)
      • peoples (collection)
        • userId (doc)
          • uid: user id (field)
          • accountType: "admin" || "moderator" || "peon" (field)
          • documents (collection)
            • docId (doc)

Access level details:

Admin

  • read: the whole organization he is in (include establishments, peoples documents, organization)
  • write: the whole organization he is in (include establishments, peoples documents, organization)

Moderator

  • read: the whole organization he is in (include establishments, peoples documents, organization)
  • write: only his own documents (organizations/orgId/peoples/userId/documents/*)

Peon

  • read: only his own documents (organizations/orgId/peoples/userId/documents/*), not the organization or establishment
  • write: only his own documents (organizations/orgId/peoples/userId/documents/*)

firestore.rules (explanation)

function getAccountType(orgId, userId) {
    return get(/databases/$(database)/documents/organizations/$(orgId)/peoples/$(userId)).data.accountType;
}

This query the roles of the given user in parameters for a given organization in parameter. Having this in parameter make it easy to either use it with the request.data or with the id from a match rule.

function isAdmin(orgID){
    return getAccountType(orgID, request.auth.uid) == "admin";
}

Check if the current logged in user request.auth.uid has "admin" roles for the given organization in parameter.

match /organizations/{orgID} {
    allow read: if authenticated() && (isAdmin(orgID) || isModerator(orgID));

Only allow read if the user is logged in AND is either an admin or a moderator.

Full rules available here


2. General guidance:

Go with clean rules:

isAdmin()
isManager()
isPeon()

That you can easily use and read later on:

match /reminders/{reminderId} {
    allow read: if authenticated() && (isAdmin(resource.data.organizationId) || isManager(...) || isPeon(...));
    allow write: if authenticated() && (isAdmin(request.resource.data.organizationId) || isManager(...));
    allow delete: if authenticated() && isAdmin(resource.data.organizationId);
}

Some firestore.rules example: this one and another one.

Setup Unit Test (⚠️ important) for your rules, so it's simpler to iterate over it and check that everything works as expected. Example of test here, run it using jest.