Improving the RBAC to accommodate a wider range of popular use-cases

Additional fields issue is definitely valid. Could you report to https://github.com/yiisoft/rbac/issues? I think it may be implemented in base auth manager level.

Created an issue with regard the Permission/Role additional fields. You want me to include the $userId in there too or perhaps we discuss it further here and then decide if an entry is to be filed?

it may be implemented in base auth manager level.

Same thought here also. Most of the implementation belongs to the manager. You might want to consider giving the control to the objects themselves Item (inversion of control) for instantiation and storage (where applicable)

1 Like

User ID is a tricky issue. As @rob006 mentioned, accepting an interface which only method returns a string instead of accepting a string only shifts the responsibility of forming a string to another place: from where the method is called to the entity itself. In this case I do not see practical need in making RBAC public interface more complex.

Regrettable but understandable.

The title of this mentions a “wider range” of popular use-cases, but seems to mainly be about multi-tenant management of roles. Using the current Yii2 rbac library (I haven’t studied the Yii3 yet) this would have to be handled via Rules - and gets quite complicated.

My understanding of RBAC is that there is another level that tends to get ‘ignored’ on websites. That is the concept of Session. In RBAC the authorization of user (Assigning Roles to a Subject) is considered a single Session. RBAC allows multiple Sessions simultaneously for any given Subject. This is not the same as having multiple Roles per user (Subject) but multiple different unique assignments of Roles.

On a website we think in terms of a single SESSION because that’s how websites handle state. And it seems that most RBAC libraries use that SESSION as their session layer. So they end up limiting the number of Sessions a user (Subject) can have at any one time.

I would recommend adding in that Session layer and not have it replaced by the http SESSION, but rather stored in it. So that way a User (Subject) could have multiple Sessions each with its own Roles/Permission and Rules. A Session could then represent a different Tenant if needed.

It would also allow implementation of another “popular use-case” and that of Temporary Permissions. Kind of like sudo… Consider a scenario where a service person is Viewing a customer’s account. They want to edit it, so they sign in (again or maybe just have a button or something) to create a temporary Session that allows the edit. That Session could stay active until they sign out from it, or based on a timeout or something similar. So the normal access level would be only to view, to prevent accidental editing. To edit would require that additional step. Again, like the whole purpose of having sudo.

By implementing a Session layer we expand RBAC to its full capabilities or at least add in more capabilities.

In my previous post I talk about RBAC Sessions. That’s what they are called but it might help to think of them as something very different from HTTP Sessions. Kind of like pre-defined sessions or Groups or Tenants.

Currently RBAC has a User set and a Role set (whether stored in database or file) and a set of links between the two sets.
Users <= UserRole Assignments => Roles

With RBAC Sessions, you would add in a Session set between the User set and the Role set.
Users <= UserSession Assignments => Sessions <= SessionRole Assignments => Roles

The Sessions would have names just like Roles have names.

The UserSession Assignments could store whether they are assigned during an initial login or not. Or another way of saying whether they are temporary or not. And if temporary then store what’s required to close them: logout, timeout, some callback. It would also store which Session Assignment is the default Session for that User.

Then calls to RBAC for can() could specify a Session by name and return whether it’s enabled, available temporarily, or not available for that user. If no Session name is passed RBAC would use the default session for the user.

Yii::$app->user->session(‘bigSite’)->can(‘createPost’)
Yii::$app->user->session()->can(‘createPost’) // default session
Yii::$app->user->can(‘createPost’) // if sessions are not used or default if they are used

Of course, it would probably help to have Sessions as an option for RBAC that can be turned on or off as needed for a site.

What is the advantage of having a user in multiple accounts, wouldnt different roles solve that already?

@mathis
I’m assuming you mean multi-tenant? However this could also apply the “Session” layer I discussed.

This is a contrived example: App supports two different stores.

A Customer Service user is given the role of managing the Reports for both stores. The stores use shared/common code for the reports so you’d only want to a single role for that even though there are two stores.

Now that same user actually works at one of the stores, so they are given a Management role, but only for one Store. However, the stores also use shared/common code for management.

So how would you prevent that user from being a manager of the other store? Simple: different tenants.

The user is assigned to two different tenants (stores) and for one they have Reports role, for the other they have Reports and Manager roles.

Of course, you can solve most problems in multiple ways and with the tools you are given. So, yes, you could probably do it with just Roles and Rules.

1 Like

By the way. Regarding my idea about using the RBAC Session layer, I think the term “Session” is confusing in a web app because a web app often uses the HTTP SESSION. I think a better name for the Session layer might something like “Realm” but I’m sure someone can come up with something better if this idea is ever used.

Yii::$app->user->realm(‘bigSite’)->can(‘createPost’)
Yii::$app->user->realm()->can(‘createPost’) // default realm
Yii::$app->user->can(‘createPost’) // if realms are not used or default realm if they are used

I’m assuming you mean multi-tenant

No, I’m talking multi-account which is not the same as multi-tenant.
Take for example AWS or DO (or any of the plenty SaaS platforms).
It’s very common that you can have 1 user that can have access to multiple accounts and different privileges per account.

Yii::$app->user->realm(‘bigSite’)->can(‘createPost’)

How would you pass the bigSite down to RBAC to the permission code?

Yii::$app->user->can(‘createPost’)

What if you need to check permissions for more than 1 user? Realm was set for first user, not set for next user so prev. realm assumed. Sounds like a lot of prone-to-error cases. And considering it’s around permissions, they’re pretty critical. Imagine giving access to proprietary content to the wrong user.

Having a unique identifier that represents the combination of user-account is foolproof. Serialising-deserialising (string<->object) an identifier is far safer and would prefer to sacrifice performance over such delicate matters.

It’s pretty much a choice between KISS (string) or improved performance/debugging (object). I still find the Object approach more suitable but I understand that the added complexity might hurt the adoption rate of the framework.

I think this solution could help with any multiple account rbac. But it really would depend greatly on how the webuser is authenticated for each account. This would take some thought - I don’t know if there is any standard way to do that and so building an RBAC that can handle that might not be that easy.

The realm() method is part of the RBAC plugin just the same way that the can() method is. They both would ‘apply’ their functionality to the webuser they are attached to. Whereas the can() method returns a bool, the realm() method returns the webuser, but with the realm property set appropriately so that the call to can() has something to work with.

Seriously, my ideas about incorporating a Session/Realm layer were just theoretical at this point. I just started looking into the yii3 project and haven’t really had time to look over the details of the code.

My whole interest was because I’m in the middle of converting a huge ria from Yii1 to Yii2. The ria grew over time and so is not the most elegant - I used AR in a lot of places and now find it a chore to convert. When I saw the work on Yii3 and Hisite projects, I got thrilled. This is the way to go.

The ria has a rather extensive RBAC setup and I had been toying with code to implement a temporary access system, so this discussion is important to me. I would really like to see something along that line incorporated into the RBAC plugin or at least be an extension to it.

I’ve never done anything like that so I don’t know how you would wire that up. I guess you would be passing in an array of User ids? It wouldn’t take much to instead pass in an associative array of [user_id => realm]. However I would imagine that any call for access would probably apply to a single realm, so you could simply pass in another parameter to determine the realm.

What do you get as the result of that check? I’m thinking it would have to be an associative array [user_id => access_bool]. But you are wanting to manage the access for multiple accounts - so that info would have to be passed back as well?

It’s kind of hard to plan out something that would likely be better managed differently for each project. My hope was that by adding in the extra session/realm layer it would be easier to apply RBAC to more scenarios and it would still part of the standard definition of RBAC. I thought that sticking to a standard was kind of important with plugable code.

Seriously, my ideas about incorporating a Session/Realm layer were just theoretical at this point. I just started looking into the yii3 project and haven’t really had time to look over the details of the code.

The Realm functionality you can encounter in other systems as Context or SecurityContext (see ApolloGraphQL, Spring, Symfony, etc). This is usually a simple object that carries stuff (vector) like all relevant identifiers. In this particular use-case, it would carry the user-id, the account-id, Security Resolver (similar to SecurityManager but not singleton), etc.

The realm() method is part of the RBAC plugin

I think emulating this concept in Yii2 the easiest approach would be to see WebUser as a SecurityContext on steroids.

Thanks for that info. I’d like to look into those and how they do it. And the name Context sounds a little more normal than Realm, which I pulled from Token usage.

You may be right. Speaking of Users and Security, I had to create a ConsoleUser to closely emulate a WebUser so commands could run code that was expecting a WebUser, mainly for security tracking purposes, but it could apply to access as well.

I do have some concerns with extending WebUser to implement RBAC - just some thoughts.

[ ] It pushes a part of the Authorization into the Authenticate code. I’d prefer to keep them separate.
[ ] It doesn’t help much with temporary access.
[ ] To me it kind of strays from basic RBAC, in that the Subject would now carry authentication logic. I realize that there are no laws governing this… but it might confuse things.

It seems from your brief description of SecurityContext, it was implemented more along the line of Rules rather than Subjects, following a more standard RBAC implementation, but did not really use the RBAC Session Layer directly - of course, I say this only from your description. Again I would like to look into how they did it.

By the way your question on this usage was right. I don’t think this would be how to implement realms. To do it this way would probably require an extended WebUser, ha.

Currently for RBAC we have a WebUser (Subject), the desired Permission, and any required Context for Rules. With a Realm layer we would have to add in an additional desired Realm. Sounds like more work, but that’s why it would be nice to also have some way to have default Realms for more normal scenarios.

The way I envisioned this to handle the multiple accounts is that for each User each Account would have the Role assignments in a different Realm. It feels logical to me. It isolates the Account from the Subject and keeps it in the authorization code, otherwise you’re back to a contrived id or moving authorization into the Subject which should be governing authentication.

Reading some of this. I think it’s kind of 6 of one, half dozen of the other. The Subject could refer to a WebUser or to an Entity of some sort that represents a User / Account combination and is Authenticated as such. But it needs to be a single Entity. There are no Teams, Groups, Mobs or anything like that in RBAC. All of that has to be handled separately, it’s not supposed to be a part of the RBAC lookup.

Mobs would add an interesting aspect to RBAC - the more users the more access! :wink:

I think the idea in RBAC is that the Subject is identified by a simple id and that id is used to pull up the various Roles. From there the Roles can be searched for the Role itself or it’s Permissions; and then any Rules (Context) can be applied. The point being that it’s a simple id that’s used to retrieve the roles.

That said, RBAC does also support the idea of Sessions. Kind of like applying the Context to determine which Roles are retrieved. But the sessions are unique to a single id. There are no cross-sessions, shared sessions or groups.

Reading some of this. I think it’s kind of 6 of one, half dozen of the other. The Subject could refer to a WebUser or to an Entity of some sort that represents a User / Account combination and is Authenticated as such. But it needs to be a single Entity. There are no Teams, Groups, Mobs or anything like that in RBAC. All of that has to be handled separately, it’s not supposed to be a part of the RBAC lookup.

Not really. It’s about

  1. adding custom columns/attributes to the Items
  2. the format of the ID when calling the AccessChecker.

At the end of the day it all comes down to the id representing the user. Consider the following

Yii::$app->user->can(sprintf('%s-%s', Yii::$app->user->getId(), $accountId));

// will produce results such as
Yii::$app->user->can('1-', ....);
Yii::$app->user->can('1-1', ....);
Yii::$app->user->can('1-2', ....);

As you see the ID for RBAC is not the user id but the combination of userId and account id.
To perform the actual checks in permissions you need to explode($id). So you always need to compose and decompose the id. This can be done much safer and simpler with the proposed Object approach.

Now in RBAC we can of course directly store the composite IDs in the assignments table and voila, right? Not as simple as that.

You want to

  1. store the userId and accountID in separate columns to maintain integrity (account is deleted, assignments are deleted).
  2. easily perform searches such as - find all the users who have assigned access to this account.

Now, imagine how much more complex it can get if you say, multi-account and multi-tenant (with shared database - not a good practice but business is business right?). Your RBAC access checker id there would be userId-accountId-tenantId.

So in synopsis, all practically need is

  1. Custom columns support
  2. Composite ID support (string will convenience the single id systems, object conveniences the composite-id systems - but both solutions do not hinder development)

@ptheofan, I just want to preface this with an apology. I’m kind of old and stubborn. I think the official term is “Ornery”. :wink: I’m guessing we might never agree. Not sure I work well with teams…

I agree with this wholly

These should not be done in or by the RBAC plugin. That would break separation of concerns. RBAC is concerned only with finding proper permissions for a given identity in a given context. Managing Users, Accounts and Tenants is outside of that domain and should be managed separately.

If you want to make sure corresponding Role assignments are deleted when an account (or anything similar) is deleted then that would handled by the identity used to assign the Roles in the first place, not some partial-identity. My guess is that when you delete an account you would be managing the deletion of the user and account link as well, which should not be stored in RBAC, so as you’re doing this remove the role assignment.

If you really really want the RBAC to help manage Accounts, I believe that would need to be handled as Context, not as part of the identity. The context can be managed with Rules.

This is actually the reason for not passing in an object. There is no reason for RBAC itself to manage that complexity.

I think this thread brings up a similar point about separation of concerns:

RBAC library design

The only reason I can think of for why this is wrong is separation of concerns.

Personally I think assigning permissions (or rather Roles) directly to users makes a lot of sense and makes coding way “easier”. Every user should have roles assigned to them, right? However it tends to cripple RBAC in that it limits how you can use it. So it should only be used where it would be a benefit and not a limitation. You would want to make sure you have a good reason for crippling RBAC.

So I agree that it should not be a part of an RBAC plugin because it breaks separation of concerns.

To me it is wrong to assign permission directly to a user, somehow is an old heritage of monolithic apps

It is bad practice to assign permissions directly to users. I don’t recall ever implying anything different.
The direction should be Users -> Roles -> Permissions -> Rules.

Considering the RBAC requirements of a high-performance high traffic SaaS IoT platform

Typically at first, I do a cheap role level check then load resources and perform a real deeper permission level check. A typical action looks like

$ctx = Yii::$app->request->getSecurityContext();
Yii::$app->user->can($ctx) // on fail throw 403
// load relevant resources - if missing throw 404
// attach resources to context
Yii::$app->user->can($ctx) // on fail throw 403

SecurityContext
Constructs itself based on request data. It can create itself either from the session cookie or JWT token. At a minimum, at all times it has identity-id and account-id.

  1. Identity-id the API Access Token the request is using (a user can have multiple Identities - typical SaaS use-case)
  2. account-id the account the request is referring to

The 2 different checks might seem unintuitive at first but there’s a very good (measurable) reason.

  1. Cheap Check: Checking against identityId-accountId makes this lightning-fast (caching) as it’s a basic check - does this identity for that account have link to that permission via some assigned role?

  2. Expensive Check: Now that we have loaded the resources, go deeper and check if this user can access the particular resource (this utilises the loaded objects and most times ends up to some rule). For performance reasons, context response is temporarily cached (with dependency).

When it comes to Identities and permissions (RBAC), my favourite source of inspiration is AWS IAM :slight_smile:

I’m sorry. I was only using that as an example of separation of concerns.

I wouldn’t call AWS IAM an RBAC implementation. It includes a lot of Authentication, including multi-factor and federated access. It even handles Credit Cards with PCI Compliance! It might encapsulate RBAC, but it’s not strict RBAC.

It even handles Credit Cards with PCI Compliance

Are you sure? AWS itself describes it as the following.

AWS Identity and Access Management (IAM) enables you to manage access to AWS services and resources securely. Using IAM, you can create and manage AWS users and groups, and use permissions to allow and deny their access to AWS resources

I’ve always been using it for Users -> Groups -> Policies + Configurations (those little wonderful json snippets).

In any case, the level of detail and customization that IAM offers is awesome - if you don’t get overwhelmed by the countless policies :smiley: