Implement multi tenancy through access groups

hi guys,

as reading the docs of the security subsystem, i imagined that it should be possible, to do something like multi tenancy through the feature of access group especially with constraints. So i tried a little to accomplish something like you described with Creating Local Administrators. But the restriction shown here are just in the context of the Users / Roles / Groups Entites.

So i tried the following: Every tenant should only see the whole application (which means, all entities) in the scope of its current tenant. Like: User2_1 sees all Products / Orders that User2_1 or User2_2 created, but not the one that User 1_1 created.

User Groups:

  • tenants
    – tenant1
    — User 1_1
    – tenant2
    — User 2_1
    — User 2_2

Entities:

  • Products
  • Orders

I followed the example given in the docs and did something like this (constraint of the “tenants” access group for the entity “products” :


{E}.createdBy in (
  select u.name from sec$User u where u.group.id = :session$userGroupId
)




The result is, that i didn’t work out :slight_smile: There are no results for “User 2_1” when “User 2_2” created a product. This is true as well for “User 1_1” and his products.

The problem, i would guess is in the JPQL part, because i do not fully understand the query language and so there are some tricky portions of it. Additionally i don’t think it is that performant to do a subselect in every query that will hit the db, to get the tenancy right (am i wrong here?).

The question that i have is more, how would the general idea to accomplish something like this be (although i would be happy, if anyone can give me a hint on the solution above :slight_smile: ). I thought of another possible solution, to add session attributes. What i saw as well is the “where (deletedTs = null)” that gets appended to handle the soft deletion stuff. Would that be also possible to hijack something like this to add multi tenancy?

Or would you say, that doing multi tenancy should be done trough multiple database(-schemas)?

Perhaps we could have a little discussion about this one. Thanks in advance.

Bye,
Mario

3 Likes

Hi Mario,

thanks for a great question!

I think that implementing multi-tenancy only by means of runtime security settings is not optimal (if achievable at all). It’s better to incorporate some facilities to the application on the development stage.

I’ve made a quick prototype of one possible way:

  • First, I’ve created a MappedSuperclass named MultiTenancySupportEntity. It has only one attribute - tenant of type Integer, and it is marked as mandatory.

  • Each entity which is isolated for a tenant must be inherited from MultiTenancySupportEntity. I’ve created one such entity - Order.

  • To automatically fill the tenant attribute, I’ve created the MultiTenancySupportEntityListener, which gets a value of a special session attribute and sets it to the entity:


@ManagedBean("mt_MultiTenancySupportEntityListener")
public class MultiTenancySupportEntityListener implements BeforeInsertEntityListener<MultiTenancySupportEntity> {

    @Inject
    protected UserSessionSource userSessionSource;

    @Override
    public void onBeforeInsert(MultiTenancySupportEntity entity) {
        entity.setTenant(userSessionSource.getUserSession().<Integer>getAttribute("tenantId"));
    }
}

This listener is specified for the Order entity.

  • Now about the security configuration. The access groups structure is as you suggested:
  • Company
    ** Tenants
    *** Tenant1
    *** Tenant2

Tenant’s users belong to “Tenant1”, “Tenant2” groups. “Tenants” group has constraints for each entity:


{E}.tenant = :session$tenantId

And “Tenant1”, “Tenant2” groups define “tenantId” session attribute with values 1 and 2 respectively.

  • That’s all. Users see only Orders created by their tenants.
    This way you can easily mix shared and isolated entities in one application and database.

The cost at the development stage is not high: you have to inherit all isolated entities from MultiTenancySupportEntity and add MultiTenancySupportEntityListener to each of them. And you have to add the constraint for the entity to the “Tenants” group at runtime.

Resulting queries to select isolated entities will include just one additional trivial condition.

I’ve attached the prototype project together with the database, so you could run it right after importing to Studio (do not execute “Create database”).

I welcome your comments.

multi-tenancy.zip (28.3K)

hi,

that solutions seems to work great. It is something like i thought of, as i said in the sentence: “I thought of another possible solution, to add session attributes.” but in a much more advanced way :slight_smile:

Is there any particular reason, why you use Integer for the tenantId instead of a string (to store the name of the access group / the tenant) or a UUID (to store the UUID of the access group / the tenant).

Would it be possible to automatically add the session attribute “tenant” to a new created access group? I would imagine that another EntityListener (afterInsert) on sec$Group could do the trick.
Another way i think of would be to create the session attribute with a dynamic value in the parent (“tenants”) access group. Something like this: “name: tenantId, datatype: Integer, value: ${actual_tenant.id}”? So that the value is not a hardcoded string anymore, Like you did with the constraint that was defined in the “tenants” access group: “{E}.tenant = :session$tenantId”? I’m not sure, if this is even possible for the value of the session attribute.

Is there any particular reason, why you use Integer for the tenantId
No reason except it is the smallest suitable data type for this task.
Actually you are right about using UUID and storing the access group ID as a tenantId. This way you can avoid using session attributes, and the entity listener will look like this:

entity.setTenant(userSessionSource.getUserSession().getUser().getGroup().getId());

But there is one drawback - you will not be able to create access groups below TenantN group (by obvious reason). In contrast, session attributes are propagated from top to bottom.
If you go with integer (or string) tenant identifiers and session attributes, you will have to automate creation of groups and attributes somehow. Attributes can be created together with groups by an appropriate entity listener. You can add an entity listener to the platform entity (e.g. Group) using this mechanism.

Hello Konstantin,

The link provided about the mechanism to have listeners working on platform entities is broken.

Is there a way to do it in CUBA 6.5?

Thank you

The link is now fixed, thank you.

A post was split to a new topic: Multitenancy with access groups questions