Multitenancy

Multitenancy was first introduced in Totara 13. Tenants introduce the ability to group and segregate users and content, and to delegate responsibilities for tenant management.

A diagram outlining the structure of a Totara site with multiple tenants.

In Totara 13 you can:

  • Enable tenants
  • Configure whether tenants are isolated or not
  • Create tenants
  • Create users within a tenant - making them a tenant member
  • Move system users into a tenant - making them a tenant member
  • Upload users, creating them within a tenant - making them a tenant member
  • Associate a system user with a tenant - making them a tenant participant (non-member)
  • Create content within a tenant category - courses and containers
  • Assign roles to user against a tenant - delegating responsibilities
  • Report on tenants, tenant members and tenant participants

The architecture extends the context system by specifically injecting a new context level between systems and users for users who are in a tenant, and by using existing category contexts in which all sub-content (courses, sub categories, activities, containers etc) is considered to belong to a tenant.
All components, plugins, and sub-plugins operating at the user context, or course category context and that correctly check capabilities will automatically work with the new multitenancy architecture.
Components, plugins and sub-plugins checking capabilities at the system context, or that do not use capabilities to control access will require modification in order to work with multitenancy.

This document aims to explain how that modification should be approached.

What multitenancy is not

Multitenancy is not the complete carving of the site into multiple sites within one database and on one code base.
It is an extension of the context system, and as such integration with multitenancy architecture is best achieved by utilising the context and capability system where possible.


How it works

Multitenancy is an advanced feature which must be enabled at the system level. Once enabled you can optionally enable the tenant isolation setting.

Once multitenancy has been enabled, a Tenants option will appear in the quick-access menu, through which tenants can be added.

For more information see the What is multitenancy? help documentation.

The structures that make up a tenant

Each tenant within system has the following:

  • A tenant record
  • A tenant context
  • A tenant category
  • A tenant audience
  • A tenant dashboard

The tenant record

A description of each individual tenant.

The tenant context

Added in Totara 13.0 the tenant context has a context level of 15. It can only have one parent, the system context.
It can only have one type of child context, user contexts.
Prior to Totara 13 the user context could only use the system context as a parent. Now, user contexts can use either the system context or a tenant context as the parent. A user whose context uses the system context is considered a "system user". A user whose context uses a tenant context as a parent is considered a "tenant member".
The purpose of the tenant context is to enable delegation of user management responsibilities over a tenants members.

The tenant category

The context structure for categories, courses and activities was not changed. In order to allow for delegation of tenant content responsibilities support was added for system managed categories, and when a new tenant is created a brand new top level category is created at the same time. The new category is marked as a system category belonging to the tenant and cannot be edited or moved in the same way that normal categories can.

The tenant category is always a top level category, and its context uses the system context as a parent. The existing category context is used.
When looking at a tenant category the administration block shows tenant management options as well.

Users (typically tenant members) can then be given roles against the tenant category allowing delegation of tenant content responsibilities.

The tenant audience

When a tenant is first created a new audience is also created. We have introduced the ability to have system managed audiences that cannot be edited in the same way that regular audiences can be. The tenant audience is a system managed audience, that is owned by the tenant.
All tenant members (users whose context uses the tenant context as a parent) are automatically added to this audience.
When adding tenant participant (non-member) through the product interfaces you are in fact adding a user to this audience.

The purpose of the audience is to allow easy selection and use of users associated with a tenant.

Fundamental architecture

Multitenancy was principally an extension of the context hierarchy structure within Totara.

When enabled, capability checks now take into account the tenant a user is a member of, and include a check against the tenant of the context the capability check is being made against. The explanation of behaviour below outlines how this additional check behaves.

By wiring it directly into the context and capability systems much of the Totara code automatically and inherently follows the behaviour rules brought in by multitenancy. This is then supplemented by the addition of the tenant category and audience, which make should make it easy for developers to update their code in places where permissions are not being used to control visibility or access by providing a path to restrict both users and content to a tenant. Primarily the tenant that the current user belongs to

Tenant members vs tenant participants

A user who belongs to a tenant is referred to as a tenant member. A user can only be a member of a single tenant. Tenant members cannot participate in any other tenant. In the context tree the users context will have the tenant context as its parent. Capabilities are therefore inherited from the tenant, and changes at the tenant level will have an effect on the user context if there are not overrides at the user context.

A user who is not a member of a tenant can be associated with a tenant as a participant. A user can be a tenant participant in multiple tenants. When a user is added as a tenant participant they will be added to the tenants audience. The user context uses the system context as its parent and therefore does not inherit from the tenant capability as tenant members do.

When undertaking actions for a tenant or its content through which you see or select users the list of users that you see will be restricted to just tenant members and tenant participants.
This includes actions such as delegating responsibility within the tenant, associating managers and appraisers with a tenant members job, or enrolling in a course within the tenant.


Developing for multitenancy

When developing for multitenancy your very first consideration is the context level at which you functionality is implemented. It [multitenancy] was implemented in such a way that for those who are creating, or updating functionality that woks at the category, course, activity, and user levels access control and visibility via capabilities will work out of the box.
Where your functionality is implemented at a context level with lineage back to category, or user context then the following is true:

  • Providing your functionality has properly hooked into the navigation structure then the navigation blocks, navbar, site administration areas and other features that consume the navigation structure for a site will automatically adjust as designed when required.
  • Core API's for working with existing structures such as users, courses, and activities have already been updated to work with multitenancy - custom code already using there API's will not require updating in these areas.

If however your functionality has been implemented at the system context level, or has extended beyond these conveniences through the likes of report build reports, custom selection API's for users and content then work will be required to make it compatible with multitenancy, if desired.

For those using the system context level the first consideration should be whether there is a more appropriate context level that could be used, such as a course, category, or activity context. If so then making this change will be beneficial in a number of ways, including automatic adoption of permission and access control checks.

Regardless, the following sections try to outline some of the commonly encountered challenges a developer will face, and attempt to provide useful code snippets to aid you in resolving this quickly.


Getting a tenant

A tenant record can be fetched easily using the following method.

$tenant = \core\record\tenant::fetch($id);

The tenant record is rarely of interest outside of tenant management functionality. It is used within this document more for illustration purposes. In nearly all situations you will be interested in the tenantid only.


Getting a user's tenant

There are a couple of ways to get a user's tenant, depending upon which information you have readily accessible to you.

The user table has a tenantid column that records the ID of the tenant the user is a member of. If the user is not a member of a tenant the value will be null.

$USER->tenantid

The context table also has a tenantid field that gets set for all contexts within the a tenant. The user context therefore can be used to get the users tenant also.

\context_user::instance($USER->id)->tenantid

The following function would get the tenant a user belongs to, or null if they are not a tenant member:

function core_user_get_tenant_by_userid(int $userid): ?\core\record\tenant {
    global $DB;
    $tenantid = $DB->get_field('user', 'tenantid', ['id' => $userid], MUST_EXIST);
    if (empty($tenantid)) {
        return null;
    }
    return \core\record\tenant::fetch($tenantid);
}

Getting the tenants a member is a participant of

The following snippet of SQL illustrates how to get all of the tenants a member is a participant of.

SELECT t.id
  FROM {tenant} t
  JOIN {cohort_members} cm ON cm.cohortid = t.cohortid
 WHERE cm.userid = :userid

Getting the tenant a context belongs to

The context table also has a tenantid field that gets set for all contexts within the a tenant. The context therefore can be used to get the tenant easily.

function core_user_get_tenant_by_context(\context $context): ?\core\record\tenant {
    if (empty($context->tenantid)) {
        return null;
    }
    return \core\record\tenant::fetch($context->tenantid);
}

As above, its unlikely the tenant record is going to be of much use. This example just illustrates logic around the content→tenantid field.


Restricting a list of users, courses, or contexts to a tenant by context join in SQL

Restricting a list of users by tenant is extremely simple as the user table has a tenantid column.

Restricting users
SELECT u.id
  FROM {user} u
 WHERE u.tenantid = :tenantid

For courses and other things in the context hierarchy joining to the context table and restricting based upon that is the best path

Restricting based upon context
SELECT c.id
  FROM {course} c
  JOIN {context} ctx ON ctx.instanceid = c.id AND ctx.contextlevel = 50
 WHERE ctx.tenantid = :tenantid

Adding restrictions to a tenant where a capability check is made at the system context

Because the system context is being used capability checks for a user will fail when isolation mode is enabled regardless of whether the user has been granted the capability or not. It is rejected simply because isolation mode is on, the content exists at the system context, and the user is a tenant member.
If you require access control checks that differ from this behaviour then your only option is to implement your own check here, using the login that you wish to implement.
If you must use the capability system then your only option is to move the content into a context level that will parent (either directly or indirectly) the tenant context, or the tenant category context.


Tenant API

There is currently no publicly accessible tenant API.
Internally \totara_tenant\local\util serves as the tenant API for the totara_tenant plugin that manages tenants.
In the future we are very likely to build out GraphQL API's that expose multitenancy API.