Parse Roles nested hierarchy either not working, not possible or misleading docs

Issue Description

I have a complex role based design suitable for a SaaS using latest parse SDK/Server

Multiple organisation’s (parent role) which have sub roles (admin, editor, viewer), the sub roles should have permissions only to interact and operate within the context of the organisation. The code which I use to create these base roles is as follows but also maybe i am doing this wrong, lets add in Apple as an organisation, so Microsoft, Apple, admin, editor and viewer are all created in a for loop calling the below function (later I assign each of these roles to be sub roles, correct ?).

        try {
            const { roleName } = request.params
    
            let role_name = slugify(roleName) 

            let roleExists: any = await new Parse.Query(Parse.Role).equalTo("name", roleName).first(masterKey)  
            if(!roleExists) {
                let perms = new Parse.Role(role_name, new Parse.ACL());
                let result = await perms.save(null, masterKey)                 
                resolve(result)
            }   
            resolve(true)
            // reject(`role ${roleName} already exists`)
        } catch (error) {
            console.log(error)
            reject(error)
        }   

So now I have “Microsoft” and “Apple” roles - these are the parent level role to which all other i.e admin, editor, viewer shall be sub roles. I have the logic in place to add sub roles to each Parent role:

        try {    
            let child: any = await new Parse.Query(Parse.Role).equalTo("name", childRole).first(masterKey)  
            let parent: any = await new Parse.Query(Parse.Role).equalTo("name", parentRole).first(masterKey)              
    
            let query = new Parse.Query(Parse.Role);
                query.equalTo("name", parentRole);
            let role =  <any>await query.first(masterKey)
                role.relation("roles").add(child);        
            await role.save(null, masterKey);        
    
            let nestedRoles = await parent.getRoles().query().find(masterKey)
                console.log("nestedRoles "+JSON.stringify(nestedRoles))        
            resolve(nestedRoles)            
    
        } catch (error) {
            console.log(error)
            reject(error)
        }

So now our Role structure is like this:

Microsoft => admin => editor => viewer
Apple => admin => editor => viewer

So now I call each parent role, I query for its direct descendant sub roles and I add myself as a user for each of these roles

    return new Promise( async (resolve, reject) => { 
     
        try {
            const user = request.user
            const { parentRoleName } = request.params
            
            let parentRole: any = await new Parse.Query(Parse.Role).equalTo("name", parentRoleName).first(masterKey)    
            

            let nestedRoles = await parentRole.getRoles().query().find(masterKey)

            if(nestedRoles.length) {
                nestedRoles.forEach( async (role: any) => {
                    let childRole: any = await new Parse.Query(Parse.Role).equalTo("name", role.get('name') ).first(masterKey)                       
                        childRole.getUsers().add(user)
                    let saveit = await childRole.save(null, masterKey)
                    console.log(saveit)
                });
                resolve( JSON.parse(JSON.stringify(nestedRoles)))                
            } else {
                reject(`${parentRoleName} has no sub/child roles under it.`)
            }
           
        } catch (error) {
            console.log(error)
            reject(error)
        }          
    })

In the parse dashboard, I see all roles, each of the above on the same hierarcy so:

Apple
editor
viewer
admin
Microsoft

if I click on Apple/Microsoft then inside each of these, on the role relations I see as expected editor, viewer and admin.

however if i go back outside to the main 1st level list and click on admin, editor, viewer I see my user object

So if i add editor, viewer, admin to any user then have this feature across both Apple and Microsoft, I get the logic of adding users to roles, adding roles to other roles but how do you lock it down in such a way that each user is assigned to an organization and they have roles within just that organization.

right now my org acl is looking like:

role:admin {
    w: true
    r: true    
}

role:super {
    w: true
    r: true    
}

role:microsoft {
    w: true
    r: true    
}

but i would have thought that because using nested Roles it would first be:

role:super {
    w: true
    r: true
    role:microsoft {
        w: true
        r: true  
        role:admin {
            w: true
            r: true                 
        }            
    }      
}

Role based docs are really lacking, id be happy to try flesh this out because parse security out of the box is not so good, its vital that people understand Roles, ACL proper use of masterkey etc

I guess im curious have I implemented this the way you guys have imagined it should be used, if not how can i refactor to do it by best practice as well as achieve this kind of nested role hierarchy based on organizations and special roles within each organization

Before doing all this job of populating the roles, I’d first manually create some roles, users, and objects to check how they will work all together using the REST API Console.

When you have a “child” role inside a “parent” role, it means that any user assigned to the “child” role will have the “child” accesses, and any user assigned to the “parent” role will have both “parent” and “child” accesses. When setting ACL to an object, you can either assign “parent” OR “child” role. You can’t assign “parent” AND “child”, unless you use custom rules via cloud code.

With that said, I’d go with a much simpler model. For each company, I’d create two roles: CompanyAReader, and CompanyAWriter. I’d add CompanyAReader as a “child” of CompanyAWriter. Then, when creating an object that belongs to CompanyA, I’d set read to CompanyAReader and write to CompanyAWriter.

That would be fine if it was just 1 role in the company, but each company has different modules, so someone can have role to read/write users who are assigned to that company and another module for events (same again certain users can either read or write) and other modules.

So I need some kind of fine grained Role ACL system.

I am using cloud code for all of these interactions i.e returning items based on the users role, this isnt done client side.

so it sounds like the role hierarchy can only really go 1 level deep

Also I already have everything built but refactoring it to allow for this fine grained roles, right now its just flat hierarchy for 1 company

So I’d go with:
CompanyAModule1Reader
CompanyAModule1Writer (owns CompanyAModule1Reader)
CompanyAModule2Reader
CompanyAModule2Writer (owns CompanyAModule2Reader)

Then, if you need, you can also create CompanyAReader, owning all company a modules readers, and CompanyAWriter, owning all company a modules writers.

But I will end up with an exponential number of different roles, it could be a nightmare to maintain as well as perform lookups for individual users and their roles.

is this the best approach, is it how something like slack would work backend wise ?

also while i have you :slight_smile: how would you work in the Company role, should I even have a parent role for company which each user is assigned to ? maybe via pointer or acl

I understand that you wanted to have CompanyA, Module1, and Reader roles. Then, for a certain object, you wanted to set that it needs to have CompanyA AND Module1 AND Reader roles in order to read it. Unfortunately, that’s not how the role system works in Parse by default. You can create cloud code custom validation for that though. I’d still go with the easier approach.

What would be the Company role? Access to common functionalities? Or Access to all functionalities?

so company role would be that other members of that company could see only other users within that company and not the ones from another company.

When you say you would use custom code validation do you mean lookup users roles and see if he has companyA role and then also has module1read + module1write role ?

I’d code it a module. Let’s say CompanyAUsersReader. Then all users with this role could see the other users of Company A.

Custom validation could be via beforeFind triggers or via cloud code functions.

1 Like

@davimacedo So I started to implement this on a company by company basis i.e companyName_editor role etc etc but now I have another problem, If i want to assign protectedFields I must specify at server start time the names of the roles which are going to be able to access these things however if these companyName_* roles are all dynamic id need to add them all manually and start the server, is there some trick around this ? thanks again in advance

parseConfig.protectedFields = {
    _User: {
      "*": ["email", "sin"],
      "role:moderator": ["sin"],
      "role:admin": [] 
    }
  }

After looking at these other ACL libraries I think it would expand Parse flexibility so much, things like CASL and CASBIN are such fully fledged libraries that if they were included rather than parse default acl/roles which is so restrictive when you need to try build something more complex like multi tenant/multi-domain and they are so much easier to understand too, plus the parse documentation is so bad I have to pull relevant info from multiple sources. it would be so cool if there was a way to incorporate either of these into parse core

You can set protected fields also in runtime by updating the schema. Take a look at this test case as an example: https://github.com/parse-community/parse-server/blob/4c29d4d23b67e4abaf25803fe71cae47ce1b5957/spec/ProtectedFields.spec.js#L303