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

One of the Apps I built has multiple company accounts. A user is bound to one or more accounts while can also be restricted to particular properties within the assigned accounts.

Adjusting RBAC to support multiple accounts was quite a lot of work whilst I had to modify some function signatures to accommodate for all the required changes.
Although It can be done in a fast and dirty way by formulating ids (like, “%s-%s-%s” and deconstructing on permission check level) but this hinders the queries performed in the db (quickly with a single query check if user is eligible). So I had to actually restructure a lot of stuff to add additional fields in the RBAC schema.

The problem in the existing RBAC is that almost all queries had to be reworked and additional functions introduced to handle the data loaded from the DB to properly populate the Objects.

It would be great if the RBAC in Yii3 is more extensible allowing us to easily accommodate additional data changes by simply extending the base classes. It would also be great if the functions performing RBAC checks supported by default structures (starting from User::can(…) all the way down to permissions).

By structures I mean

  • UserId is an Object (extending some base class will allow us to extend the data used to identify a user or resource in the system)
  • Permissions and etc. can be data-vectors (carry all kinds of additional custom data that might be needed accessible in a magic getter-setter or with concrete functions built in classes extending the base class)
  • Allow be default the use of Contexts that identify under which application contexts an RBAC item is valid (especially useful in GUI permissions assignment - think Admin, Frontend, Api, etc). Can be emulated similar to messages (translations) but it would be nicer to be cleaner and separate for improved db performance.

What do you think? Is it worth adding this type of extensibility in the RBAC system of Yii3?

2 Likes

What you describe sounds a lot like it could be solved with using RBAC rules.

https://www.yiiframework.com/doc/guide/2.0/en/security-authorization#using-rules

Are you using rules in your setup?

1 Like

Rules cannot solve multi-account setups (and yes, I am using them extensively).

Imagine a platform that supports multiple accounts. Each user can be assigned to multiple accounts or none. To solve this you need to create complex ids in auth_assignment.
For instance “accountId-userId”. By doing that you add not only complexity to the queries but also a lot of additional computational cost not to mention you loose the data relational integrity guarantee of SQL.
This can be easily (in theory) solved by adding another column there, account_id.
To do that in the existing RBAC is quite painful as it’s tightly coupled in many parts of the code to the initial schema.

Then imagine you also need a “form” where roles hierarchies and rules can be edited and attached.
Bad luck again as there’s no clear indication in the db schema for contexts. Yes, you can have different sets of tables per context (admin, frontend, api, etc) but this just adds a lot of complexity which can be very easily be solved by adding another column called “context”.

So, what I ended up doing is extending the Auth manager, pretty much overloaded most functions to eliminate the tight schema coupling. I also modified the User::can function signature to accommodate the additional account_id information.

This is why I suggested using a more abstract approach when it comes to

  • Auth Manager and db schema

  • Objects to represent an a user id in RBAC.

1 Like

I am currently working on a system where a user can be assigned to multiple “accounts” with different roles in them. I am quite busy right now, but I might come back here and explain how I made that work with current Yii rbac later.

1 Like

Hey @CeBe,

i am also interested in your solution, since we want to implement a kind of “multi-tenant” RBAC in the future, too. I would love to share the experiences we make here.

1 Like

We do this in multiple projects of ours with Yii2 by extending the auth-assignment table with a foreign key column to point to the “object” the user is getting a role assigned to. For example:

In a system that has multiple stores, our auth assignment is not the default (item_name, user_id) but it is (item_name, user_id, store_id) because the user account of a store employee should get a role assigned not across the whole application, but only for one or more specific stores. The auth assignment only applies to the store with that ID. Consequence is that we generate an AuthAssignment model, controller & views and custom rule extensions (for can() statements), because we need to bypass a lot of the generic RBAC functionality.

Would be nice if RBAC supported this by default. Also, would be nice if auth_item would add an item_id INT primary key column and just make item_name unique. Strings are a lot more annoying to deal with when defining foreign key relationships in models as ActiveRecord and/or in joins.

1 Like

Agree with @toMeloos. RBAC is the same since yii 1, nothing has been added. I would like a more coppled solution, just like spring have for row level security.

2 Likes

We now have an interface for access check. RBAC itself is currently in the nearly same form: https://github.com/yiisoft/rbac. Feel free to propose concrete changes.

UserID

As a start, I’d recommend we change the $userId from string/int to some object so it can easily be extended.

Proposed changes
ManagerInterface (and of course multiple related points such as IdentiyInterface etc)

Problem
In the current implementation, limiting this to a string/int means that this ID has to be composed into a string and then later on decomposed back to its components (really bad resource-wise). Of course, there are workarounds and hacks to get around it. Using an interface in-place defeats the need for hacks.

Why an Object?
Can easily be modified by anyone to incorporate any criteria he needs to represent a unique user identifier instance. From account-specific information (for multi-account systems) to whatever else they might want/need (think GraphQL, think oAuth, think JWT).

Example

// instead of string $userId we use an object Actor (naming idea came from UML diagrams)
interface ActorInterface
{
    // This will return the unique string representing this actor (user)
    // Handy to be used as a key for Hashmaps $cash[$this-guid-here] = $this-object
    public function getGuid(): string;
}

// Example of simple implementation accomodating userId + accountId
class Actor implements ActorInterface
{
    // @var int
    private $id;

    // @var int
    private $accountId;

    // This will return the unique string representing this actor (user)
    public function getGuid(): string
    {
        return (string)$this->id . '-' . $this->accountId;
    }

    // getters + setters
}

RBAC - SQL

Currently RBAC sql implementation (not in the repo so I assume we’ll have a separate repo for that? :smiley:) needs decoupling.

Scenario
Due to project needs, someone needs to add a couple of extra attributes in the permission (or role). According to the RBAC implementation I would extend Permission and add my additional attributes.
The SQL implementation does not respect that and is tightly coupled to the predefined SQL table structure.

Problem
When loading/saving a Permission/Role the additional defined attributes are not loaded/saved.

Solution
Let the Permission/Role instantiate itself from the sql fetched row (IOC) - and let it provide an array of values when saving.

1 Like

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