Skip to main content

Pentaho+ documentation has moved!

The new product documentation portal is here. Check it out now at


Hitachi Vantara Lumada and Pentaho Documentation


Pentaho Enterprise software is designed to work as a stand-alone, multi-tenant solution or embedded as part of a multi-tenant service. Pentaho is flexible enough to support a variety of multi-tenancy approaches. Taking advantage of Pentaho’s multi-tenancy capabilities can provide sophisticated analytics while reducing complexity and cost.

This article is designed for architects and developers who are familiar with multi-tenancy and ready to consider applying their business requirements to parts of their Pentaho system.

What is multi-tenancy?

Multi-tenancy is an architecture in which individuals or groups share the same instance of a software application, but have separate data and content. These multiple individuals or groups are referred to as tenants. In a multi-tenancy architecture, customers share infrastructures, applications, or databases to gain performance advantages while reducing overhead. A provider defines the rules for the tenants within the system. Each tenant can be restricted to see only her own secure data and content while using the same software. Note that multi-tenancy differs from multi-instance architecture which is based on maintaining separate copies of the software to serves separate clients.

The focus of this article is to explain how multi-tenancy is achieved using Pentaho Enterprise Business Analytics software.

Multi-tenancy in Pentaho

Pentaho has three categories of multi-tenancy:

  • Data multi-tenancy

    allows developers and integrators to apply custom security and business rules to control access to data.

  • Content multi-tenancy

    separates content, such as reports and folders, among tenants.

  • UI multi-tenancy

    presents different styles of the user interface for each tenant.

There are two required components to make multi-tenancy work. Users need to be associated with tenants via roles, tenant IDs, or other identifiers which indicate what content and data users see. Similarly, there must be something in the data that can be used to restrict access. The combination of user information and data make the multi-tenancy approaches described here possible. Since these approaches are data model and data-driven, they are very flexible.

Preparing for multi-tenancy in Pentaho

Before you can apply multi-tenancy to your Pentaho system, you need to associate users to a tenant. The most likely methods are to assign a specific role to users who belong to the same tenant or to designate a session variable which identifies the tenant ID. Other approaches include associating users with some data, such as geographic region or business unit. The association of a user with tenant identifiers is accomplished through one of the following approaches:

  • The user information can be set via single sign-on if it is used. This approach has the advantage of requiring a single point to set user ID, roles, and tenant info. However, if users will be scheduling their own content, this approach will not work because the SSO filter is not called by the scheduler.
  • A session startup action that is run when a user session is created. The advantage to this approach is that the action is called by the scheduler. The downside is that an action sequence is required, which means understanding a new technology.

You can use an action sequence to add an indicator to the user's session which identifies the user as a member of the tenant. The following document illustrates techniques for controlling access to data used in action sequences in the Pentaho BI Platform. You should be familiar with the Pentaho BI Platform, creating action sequences and SQL database queries.

For more information, see:

Data multi-tenancy and supported methods

The most common category of multi-tenancy is data multi-tenancy. Data multi-tenancy allows developers to apply their own custom data access rules at run time. For example, each tenant might only be allowed to see data which is associated with their tenant ID. Here are the most common methods for data multi-tenancy in Pentaho Business Analytics.

  • Sharding

    Each tenant has its own database or schema. This approach has the advantage of controlling per database and ensuring data is separated. Note that with this approach, multiple databases and servers will need to be managed.

  • Striping

    Tenants share a database, but the tables have a tenant ID column to indicate which tenant can see the specified data. This approach has the advantage of managing only a single database. Note that with this approach, databases can become very large.

  • Data Models

    Tenancy is controlled at the data level where different tenants (or sub-tenants) are only able to see certain data. This approach is very flexible, but the data to restrict on must usually be known in advance.

  • Hybrid

    Combinations of sharding, striping, and data model. Each of the approaches above can be combined into a single, flexible solution to data multi-tenancy.

Each database implementation has advantages and disadvantages, based on factors of performance management, maintenance, security, efficiency, available resources, and database vendor features. For best practices in database architecture, Pentaho recommends working with an expert in your chosen database and data modeling.

Sharding data multi-tenancy

Sharding involves separating data by database or by database connection properties. The Pentaho Business Analytics platform manages access to database connections through a central mechanism called a data source. The platform delegates to the IDBDatasource object whenever a connection is requested to a given data source. Through Java code and Pentaho configuration files, developers can plug in their own object to apply rules at run time as shown in the following example.

IDBDatasourceService implementation example

An IDBDatasourceService implementation uses two methods to modify the data source name: getDataSource() and clearDataSource(). The following code sample adds a tenant ID to the data source name. Adding the tenant ID allows the system to publish report definitions with a common data source name. At run time, the platform will use the dynamically created data source name.

public class MyTenantedDatasourceService extends PooledOrJndiDatasourceService {
    public DataSource getDataSource(String dsName) throws DBDatasourceServiceException {
        return super.getDataSource(modifyDsNameForTenancy(dsName));
    public void clearDataSource(String dsName) {
    private String modifyDsNameForTenancy(String dsName){
        logger.debug("Original DSName is "+dsName);
        IPentahoSession session = PentahoSessionHolder.getSession();
        if (session == null) {
            logger.warn("Session is null; no modifications made to the JNDI dsName.");
            return dsName;
        String tenantId = (String)session.getAttribute("tenantId");
        if (StringUtils.isEmpty(tenantId)){
            logger.warn("ID not found; no modifications made to the JNDI dsName.");
            return dsName;
        String dsname = tenantId.concat("_").concat(dsName);
        logger.debug("New DSName is "+dsname);
        return dsname;

Configuring IDBDatasourceService

You must configure the class you want to use.


  1. Place your compiled class or JAR file in the webapps/pentaho/WEB-INF/lib folder.

  2. Modify pentahoObjects.spring.xml to point to your class name.

    <bean id="IDBDatasourceService" class="org.myorganization.MyTenantedDatasourceService" scope="singleton"/>
  3. Restart the server for the configuration change to take effect.

Using the data source

The following steps describe how the data source in this example is used at run time.


  1. A report is published to Pentaho which refers to a JNDI data source called 'MyDataSource'.

  2. A user with the tenant ID of 'CompanyA' runs a report.

  3. The IDBDatasourceService appends the tenant ID to the data source name, resulting in 'MyDataSource_CompanyA', and returns the connection.

  4. The report is run using data from the 'MyDataSource_CompanyA' connection.


The IDBDatasourceService interface can be used any time a connection uses either JNDI or Pentaho database-pooled connections. This approach works for reports generated with Report Designer, Analyzer, dashboards, data model-based reports, and action sequences using named data sources. Note that the IDBDatasource is implemented on the Pentaho Server and is not directly available to BA design tools, such asReport Designer and Schema Workbench.

CautionIf you are using only the IDBDatasourceService for multi-tenancy and also using Pentaho Analyzer, it is recommended that you have a DSP (see below) which will modify the Analyzer schema in a benign way, such as modifying the description. This is because the schema is used to determine caching. If only the IDBDatasourceService is used, it is possible for data from one tenant to be exposed to another tenant.

Striping and data model multi-tenancy

Striping involves filtering tenant data within a single database connection. To allow a user to interactively query or navigate this data, access rules must exist between the user actions and the data. Data models provide a place to embed these access rules. Pentaho Business Analytics natively supports two types of data models: Pentaho Metadata and Analyzer schemas.

  • Pentaho Metadata allows you to define business abstractions of relational database models. The Pentaho Metadata Editor and the Data Source Wizard are used to build and set up data models. Pentaho Metadata is used by Pentaho Interactive Reports report files (.prpti) and can be used by Pentaho Report Designer report files (.prpt), dashboards, Pentaho Data Integration, and CTools dashboards.
  • Analysis schemas define multi-dimensional data models. These schemas are created by Pentaho Schema Workbench, Data Source Wizard, and other parts of Pentaho software. Analyzer schemas can be used by Analyzer, Pentaho Report Designer reports (.prpt) and CTools dashboards.

Pentaho metadata security

The easiest way to implement multi-tenancy is to use a global security constraint in the data model. The advantage to this approach is that there is no code to maintain, making it simple for non-developers to implement. However, there are several disadvantages to this approach, such as that complex constraints can be difficult to model and not all constraints can be modeled. This approach also applies to all queries which may result in unnecessary joins. Finally, each model has to be constrained, whereas the SQL Generator can be applied to all models with one piece of code.

Set the global constraint

To set the global constraint:


  1. Edit the data model.

  2. Set a constraint on the business model to be constrained using available formulas.

  3. Repeat for all tables and models as necessary.

Next steps

More information on setting Pentaho metadata security, see Edit the Properties File for Metadata Editor.

SQL generator

The SQL Generator is a special class that is called when Pentaho metadata queries the database. There are two methods that can be overwritten: preprocessQueryModel() and processGeneratedSql().

  • preprocessQueryModel()

    The recommended method to overwrite. It is called before the SQL is generated. To overwrite, add an AND condition to the query to restrict data.

  • processGeneratedSql()

    The method called after the SQL is generated. While it is possible to modify this query, it would involve parsing and modifying the string query.

The following example shows the preprocessQueryModel() for a class that extends the SqlGenerator class. The first loop gathers all of the columns to see which ones need to be constrained to avoid duplicate checks. The second loop adds a WHERE clause to the query. Only AND conditions can be used. Because you cannot add parentheses, using AND conditions can cause problems if the model already has an OR condition in the WHERE clause.

protected void preprocessQueryModel(SQLQueryModel query, List<Selection> selections, Map<LogicalTable, String> tableAliases, DatabaseMeta databaseMeta) {
    Set<LogicalTable> selectedLogicalTables = new HashSet<LogicalTable>();
    // Get the users productline from the session
    IPentahoSession session = PentahoSessionHolder.getSession();
    String productline = (String)session.getAttribute("productline");
    //Object territory = "NA" ;
    // Figure out and gather up the selected logical tables. We need to
    // know if the CUSTOMER_W_TER table is included in the
    // query...
    if(selections!=null && !selections.isEmpty()) {
        for(Selection selection:selections) {
            LogicalColumn column = selection.getLogicalColumn();
            LogicalTable table = column.getLogicalTable();
    // We now find the column to constrain, and add our where
    // clause, using a dialect specific where clause...
    for(LogicalTable table:selectedLogicalTables) {
        List<LogicalColumn> logicalColumns=table.getLogicalColumns();
        for(LogicalColumn logicalColumn:logicalColumns) {
            if(logicalColumn.getId().equalsIgnoreCase("BC_PRODUCTS_PRODUCTLINE")) {
                String tableAlias = tableAliases.get(table);
                String columnName = (String) logicalColumn.getPhysicalColumn().
                query.addWhereFormula(tableAlias + "." +
                columnName + " = '" + productline + "', AND");

Once the code has been compiled and placed in a JAR file, it should be deployed to webapps/pentaho/WEB-INF/lib. Then two files need to be modified:

  • pentaho-solutions/system/pentahoObjects.spring.xml

    should be modified to change the class defined for the sqlGenerator bean. The following shows an example of the bean mapping:

    <bean id="sqlGenerator" class="org.myorganization.MySqlGenerator" scope="prototype"/>
  • webapps/pentaho/WEB-INF/classes/

    should be modified by defining a new parameter:


One way to debug SQLGenerator functionality is to view the generated SQL statements. A common method is to use logging statements within processGeneratedSql and increase the log level of SQLGenerator in log4j.xml file under webapps/pentaho/WEB-INF/classes.

<category name="org.pentaho.metadata.query.impl.sql.SqlGenerator">
    <priority value="DEBUG"/>
  <category name="org.myorganization.MySqlGenerator">
    <priority value="DEBUG"/>

Analyzer data multi-tenancy

The two primary ways that multi-tenancy is applied for analysis reports include Dynamic Schema Processor and Delegate Roles. This section will describe when and how to use each.

Dynamic schema processor

A Dynamic Schema Processor (DSP) is a special class that can intercept the schema when it is loaded. Since the class has access to the user session information, it can modify the schema based on details about the user, such as user ID, roles, or other session attributes set when the user logged in, such as tenant ID.

A common implementation approach with Mondrian 3.x is as follows:


  1. Modify the Mondrian schema to include SQL statements that will limit the data returned from the dimension and fact tables.

    SQL statements on tables will be added to the WHERE clause when the SQL query is generated by Mondrian.
    . . .
    <Table name="my_facts">
      <SQL dialect="generic">my_column = %my_tenant_id%</SQL>
    . . .
  2. Populate the user’s session to include the variable value, such as my_tenant_id = 12.

    This step is usually completed at session startup by creating a session startup action or startup rule. It can also be set via SSO by passing additional parameters which are set in the SSO filter.
  3. Create a DynamicSchemaProcessor class to modify the schema to replace the variable with the value from the user’s session.

    This class should implement the mondrian.spi.DynamicSchemaProcessor interface. It is common to extend the FilterDynamicSchemaProcessor or the LocalizingDynamicSchemaProcessor.
  4. Publish the Mondrian schema on the Pentaho Server.

  5. Configure each desired Pentaho Analyzer data source to use the DSP by adding parameters to the data source properties:

    • UseContentChecksum=true
    • DynamicSchemaProcessor=org.myorganization.MyDynamicSchemaProcessor
    Please note that DSPs typically involve a separate cache to be maintained for each tenant, which can cause performance concerns. Therefore, DSPs are generally recommended where multiple users share the generated schema. For cases where individuals each have his or her own rules within the schema, the delegate role technique is recommended.

    To maintain internationalization functionality, the LocalizingDynamicSchema processor is typically extended and the filter method overridden. The following listing shows a simple DynamicSchemaProcessor which replaces all of the tenant-ID variables in the schema with the tenant-ID of the user:

Next steps

See the following references for further details:

Delegate roles

Delegate roles allow the data to be restricted using roles at run time. You can implement individual roles for each user in the schema, but this can be unmanageable for thousands of users, each with a unique role. In the scenario where many users will have fine-grained control, delegate roles solve the problem with a few classes.

The standard approach is as follows.

  1. Populate the user session with information which can be used to restrict the data.
  2. Add a role with data restrictions to some members of the hierarchy to be controlled.
  3. Configure a role mapper in pentahoObjects.spring.xml. A one-to-one mapping is usually recommended. Without this configuration, roles are not enforced.
  4. Create a custom role object.
  5. Create a custom connection object which will set the new delegate role.
  6. Change the configuration to use your custom connection and role objects. Open pentahoObjects.spring.xml in an editor, and modify the connection-MDX bean.

Populate the user session

The typical approach is to populate the session with the specific data members which a user will be able to see. For example, if a sales manager is restricted to the northwestern United States, then that manager might have “ID,” “WA,” and “OR” in a session variable. This data will be used by the delegate role described to restrict access.

Add a role to the schema

To restrict on a role, Mondrian will need to have a role defined. A common approach is as follows.


  1. Create the Authenticated role or some other role that all users of the schema will have. However, you can use any role which the user might have.

  2. Within the role, a MemberGrant should be defined.

    The following listing shows a role for Sales Managers. In this case the Sales Manager will only have access to facts associated with the state of WA (Washington).
    <Role name="Sales Manager">
      <SchemaGrant access="none">
        <CubeGrant cube="Product Sales" access="all">
          <HierarchyGrant hierarchy="[Location]" access="custom">
            <MemberGrant member="[Location].[Country].[USA]" access="none"/>
            <MemberGrant member="[Location].[State].[WA] access="all"/>

Next steps

There are a couple of important considerations.
  • A member grant must exist. If there is no member grant on a hierarchy, Mondrian will not check to see if the user has access.
  • The member must exist in the data. For example, the member could not be [Location].[State].[NoWhere] unless NoWhere is a legitimate member. It is an option to have fake members in the dimension table with no facts and use that as the default.
  • If a dimension needs to be restricted, you must restrict it separately. Restricting a hierarchy does not restrict other hierarchies, so if there is not a measure or restricted member, Analyzer will show you all members of the dimension.

Configure the role mapper

A role mapper tells Mondrian how to map from a Pentaho role to a role defined in the Mondrian schema. By default, Pentaho does not have the role mapper defined. If there is no role mapper, then Mondrian will not apply any security access and all users will have access to all data.

To configure the role mapper, modify pentahoObjects.spring.xml in the pentaho-solutions/system folder. There are several options with descriptions already defined in the configuration file. The most common is to use the MondrianOneToOneUserRoleListMapper. This mapper will map directly from a Pentaho role to a Mondrian role.

Create a custom connection

You will need a custom connection to set the delegate role mapper. This is because only the connection can be configured in Pentaho. The custom class will typically extend the default MDX connection and extend the init() method. The following example shows how to set the delegating role for Authenticated:

public class CustomMDXOlap4jConnection extends MDXOlap4jConnection {

  public boolean connect( Properties props ) {
    boolean result = super.connect( props );
    if ( result && this.connection != null ) {
      try {
        RolapConnection rolapConnection = this.connection.unwrap( RolapConnection.class );
        Role delegateRole = rolapConnection.getSchema().lookupRole( "Authenticated" );

        if ( delegateRole != null ) {
          CustomSessionAssistedRole customRole = new CustomSessionAssistedRole( delegateRole );
          rolapConnection.setRole( customRole );
      } catch ( Exception e ) {
        throw new RuntimeException( e );

    return result;

To configure the custom connection, modify pentahoObjects.spring.xml and set the value of the connection-MDX bean.

<bean id="connection-MDXOlap4j" class="org.myorganization.CustomMDXOlap4jConnection" scope="prototype">
  <property name="useExtendedColumnNames" value="true" />

Create a delegating role class

The custom delegating role will be called whenever access needs to be determined, such as if a user is attempting to see data about a particular state. You must extend the DelegatingRole class and override the getAccess() methods, depending on which one is to be checked (for example, Hierarchy or Member). Only the logic for determining access to a member is shown below. Note that a complete class will have a number of other methods:

public Access getAccess(Member member) {
  // state previously defined by reading from PentahoSession
  if (null == state) {
    return Access.NONE;  // prevent access to everything if no session variable defined
  // Only check the Location dimension.
  if (member.getHierarchy().getName().contains("Location")) {
    // Check ancestors as well for restriction.
    List<Member> members = member.getAncestorMembers();
    for (Member mem : members) {
      if (state.equalsIgnoreCase(mem.getName())) {
        return Access.ALL;
    // Check the member itself; is this the state that's allowed?
    if (state.equalsIgnoreCase(member.getName())) {
      return Access.ALL;
    return Access.NONE;
  return role.getAccess(member);

public Role.HierarchyAccess getAccessDetails(Hierarchy hierarchy) {
  Role.HierarchyAccess ha = super.getAccessDetails(hierarchy);
  return (ha == null ? null : new CustomHierarchyAccess(ha));

public boolean canAccess(OlapElement olapElement) {
  Util.assertPrecondition(olapElement != null, "olapElement != null");
  if (olapElement instanceof Member) {
    return getAccess((Member) olapElement) != Access.NONE;
  } else {
    return super.canAccess(olapElement);

protected class CustomHierarchyAccess extends RoleImpl.DelegatingHierarchyAccess {
  public CustomHierarchyAccess(Role.HierarchyAccess ha) {
  public Access getAccess( Member member) {
    return CustomDelegateRole.this.getAccess(member);
NoteIf you are using a custom role class, you must override these methods: DelegatingRole.getAccess(), DelegatingRole.getAccessDetails(), and DelegatingRole.canAccess().

Solution multi-tenancy

The following topics discuss solution multi-tenancy:

  • Roles and Access Control Lists (ACLs)
  • Multi-Tenancy at the content level
  • Custom voters
  • Managing ACLs via a web service

Roles and Access Control Lists (ACLs)

Pentaho uses roles and user IDs to enforce authorization to content in the solution repository. For multi-tenancy, roles are typically used instead of user IDs. A role for a user should be thought of as a piece of metadata about the user and is simply a group which the user belongs to. User roles are set as part of the authentication and identification process when a user logs in. For example, a user might be in the 'West and Sales Managers group'.

Access Control Lists (ACLs) are used to define which users or roles have access to content in the solution repository. As with roles, ACLs should be thought of as metadata about the repository content. An ACL might be that Sales Managers can read and edit content in the Sales Managers folder, but not delete it.

When a user attempts to perform a task with content, an ACL voter will determine if the user can perform the action. The voter compares the ACL against the user’s ID and roles to determine the user's access permissions. For example, if Sam is assigned the role of 'Sales Manager' and the Sales Manager role can read files in the Sales Managers folder, then Sam can see the content in that folder. However, Sam is not assigned the HR role, and since the HR folder is only viewable by users assigned the HR role, Sam cannot see the content in the HR folder.

For more information on roles and ACLs, see Use Pentaho Security.

This feature can be used for multi-tenancy as well. Each user is assigned a role which associates that user with a tenant-specific folder which only that user can see. There also may be folders which are viewable by all tenants. When a user logs on to the system, that user can only see shared folders and the folders for their tenant.

The approach described works for many cases, but does not accommodate complex access rules. For this reason, a better approach is often to use custom access voters. The next sections will describe how to create custom voters and how to manage ACLs without using the User Console.

Multi-tenancy at the content level

You can use the ACL system to manage access to content by tenant. If content needs to be restricted by more complex rules such as by tenant and then by role within the tenant, you can create a custom access voter by implementing IRepositoryAccessVoter. The only method that needs to be implemented is the hasAccess() method.

Custom voters

Pentaho, through the Spring-based bean injection, allows you to apply a different AccessVoter, which can apply custom logic to determine access for users. For example, you may have common reports that all users can access based on roles, as well as solution folders for specific tenants or business units. The ACL can allow a user to see all the common reports, but only the tenant folders for which that user has access. Additional fine-grained access control rules can be added as needed. Therefore, it is possible to have more than one AccessVoter and to have them chained. All instances of AccessVoters are managed by AccessVoterManager. When implementing a new AccessVoter, consider the following:

  • All users with the role of Administrator will bypass the AccessVoter logic.
  • AccessVoters are chained, and their order is specified in the repository.spring.xml for the repositoryAccessVoterManager bean:
    <bean class="org.pentaho.platform.repository2.unified.RepositoryAccessVoterManager" id="repositoryAccessVoterManager">
      <constructor-arg ref="tenantedAccessVoter/>
      <constructor-arg ref="authorizationPolicy"/>
      <constructor-arg ref="repositoryAdminUsername"/>
  • A user will have access to a resource if all AccessVoters in the chain allow access. The first AccessVoter in the chain that rejects access will break the chain, and the user will not have access to the resource.

To implement a new AccessVoter, you need to write a java class which implements IRepositoryAccessVoter. This interface is for all resources stored in Pentaho’s repository. The only method that needs to be implemented is the hasAccess() method, which is called for each object the user tries to access. In the following listing, the tenantid is retrieved from the session and then compared to the folder name. All tenants will have an individualized directory under /public/tenants/${tenantid}. If the folder name matches the tenantid, the user is granted access; otherwise, access is denied.

  public boolean hasAccess(RepositoryFile file, RepositoryFilePermission operation, 
                              RepositoryFileAcl acl, IPentahoSession session) {
     String tenantid = (String)session.getAttribute("tenantid");
     String [] topLevelDirs = getTopLevelDirNames(file);
     // a dir pattern name of /public/tenants/tenantid
     if ((topLevelDirs != null ) && (topLevelDirs.length > 3)) {  
        if ( "public".equals(topLevelDirs[1])) {
           if ("tenants".equals(topLevelDirs[2])) {
              return tenantid.equals(topLevelDirs[3]);
     return true; // leave it up to the server level ACL 

  protected String [] getTopLevelDirNames(RepositoryFile f){
      String fullpath = f.getPath();
      if (fullpath != null){
          String [] dirs = fullpath.split("/");
          return dirs;
      } else {
          return null;

Managing ACLs via a web service

As a part of multi-tenant solution, there is often a need create and manage files and folders in an unattended fashion. Pentaho has SolutionRepositoryService with a RESTful interface that allows third party scripts and applications to create new folders as well as set ACLs. Detailed information on this service can be found in the Pentaho SDK package, which can be downloaded from the Pentaho support site.

UI multi-tenancy

Each of the major components within the Pentaho Platform have a theme engine which allows you to control the presentation style through CSS and other methods. The reporting tools also have support for templates which allow you to apply structure to the report.

  • Pentaho User Console (PUC) includes themes.
  • Pentaho Analyzer (PAZ) includes themes.
  • Pentaho Report Designer includes templates.
  • Pentaho Interactive Reports includes themes and templates.
  • Pentaho Dashboard Designer includes themes and templates.


When you integrate content for users, you may want to make it look like the application into which it is being integrated. Furthermore, in multi-tenanted environments, you may want a different theme for each tenant. How to create additional themes is covered in the Make custom User Console themes article. This section focuses on setting the theme for different tenants.

Setting the theme per tenant

The Pentaho User Console will present the specified theme if the pentaho-user-theme session variable is present. The other components with the user console will default to the same theme name which the user console is presenting. Set the pentaho-user-theme attribute in the user session using one of the techniques described in the "Preparing for Multi-Tenancy" section. The value of the attribute should be the name of the theme you have configured.