Search

Multitenancy

CAP applications can be run as “software as a service” (SaaS). That means, different customers (so called tenants) can access the application while the data of different tenants is securely isolated. The CAP Java runtime routes user requests that access data automatically to their tenant-specific database container. This chapter explains how to configure multitenancy for the CAP Java runtime.

Content

Overview

For a general overview on this topic, see Cookbook > Multitenancy. In CAP Java, the Node.js based cds-mtx module is reused to handle tenant provisioning. This reuse has the following implications:

The following figure describes the basic setup:

Maven Dependencies

Multitenancy support is available as a so called optional application feature of the CAP Java runtime. It’s already included when you use the cds-starter-cloudfoundry dependency. Otherwise, you can add the following Maven dependency to apply the feature:

<dependency>
    <groupId>com.sap.cds</groupId>
    <artifactId>cds-feature-mt</artifactId>
</dependency>

Note: When you add this dependency to your project, it becomes active when certain conditions are fulfilled, for example, when your application is deployed to SAP Cloud Platform. This condition check lets you test your application locally without multitenancy turned on.

Tenant Subscription Events

The SaaS Provisioning service (saas-registry) in SAP Cloud Platform sends specific requests to applications when tenants are subscribed or unsubscribed. For these requests, the CAP Java runtime internally generates CAP events on the technical service MtSubscriptionService.

For a general introduction to CAP events, see Service Provisioning API.

Register event handlers for the following CAP events to add custom logic for requests sent by the SaaS Provisioning service. Each event passes a special type of EventContext object to the event handler method and provides event-specific information:

Event Name Event Context Use Case
EVENT_SUBSCRIBE MtSubscribeEventContext Add a tenant
EVENT_UNSUBSCRIBE MtUnsubscribeEventContext Remove a tenant
EVENT_GET_DEPENDENCIES MtGetDependenciesEventContext Dependencies

You only need to register event handlers to override the default behavior.

Default behaviors:

  • A new tenant-specific database container is created through the Service Manager during subscription
  • A tenant-specific database container is not deleted during unsubscription

The following sections describe how to register to these events in more detail.

Subscribe Tenant

Subscription events are generated when a new tenant is added. By default, subscription creates a new database container for a newly subscribed tenant.

Synchronous Tenant Subscription

By default an EVENT_SUBSCRIBE event is sent when a tenant is added.

The following example shows how to register to this event:

package com.sap.cds.demo.spring.handler;

import org.springframework.stereotype.Component;

import com.sap.cds.services.handler.EventHandler;
import com.sap.cds.services.handler.annotations.Before;
import com.sap.cds.services.handler.annotations.ServiceName;
import com.sap.cds.services.mt.MtSubscriptionService;
import com.sap.cds.services.mt.MtUnsubscribeEventContext;

@Component
@ServiceName(MtSubscriptionService.DEFAULT_NAME)
public class SubscriptionHandler implements EventHandler {

  @Before(event = MtSubscriptionService.EVENT_SUBSCRIBE)
  public void beforeSubscription(MtSubscribeEventContext context) {
      // Activities before tenant database container is created
  }

}

To send notifications when a subscription was successful, you could register an @After handler:

@After(event = MtSubscriptionService.EVENT_SUBSCRIBE)
public void afterSubscription(MtSubscribeEventContext context) {
    // For example, send notification, ...
}

Returning a Database ID

When you’ve registered exactly one SAP HANA instance in your SAP Cloud Platform space, a new tenant-specific database container is created automatically. However, if you’ve registered more than one SAP HANA instances in your SAP Cloud Platform space, you have to pass the target database ID for the new database container in a customer handler, as illustrated in the following example:

@Before(event = MtSubscriptionService.EVENT_SUBSCRIBE)
public void beforeSubscription(MtSubscribeEventContext context) {
    context.setInstanceCreationOptions(
      new InstanceCreationOptions().withProvisioningParameters(
        Collections.singletonMap("database_id", "<database ID>")));
}

Returning a Custom Application URL

The following example shows how to return a custom application URL that is shown in SAP Cloud Cockpit:

@After(event = MtSubscriptionService.EVENT_SUBSCRIBE)
public void afterSubscribe(MtSubscribeEventContext context) {
    if (context.getResult() == null) {
        context.setResult(
          "https://" + 
          context.getSubscriptionPayload().subscribedSubdomain + 
          ".myapp.com");
    }
}

By default, the application URL is constructed by configuration as described in Wiring It Up.

Returning Dependencies

The event EVENT_GET_DEPENDENCIES fires when the SaaS Provisioning calls the getDependencies callback. Hence, if your application consumes any reuse services provided by SAP, you must implement the EVENT_GET_DEPENDENCIES to return the service dependencies of the application. The callback must return a 200 response code and a JSON file with the dependent services’ appName and appId, or just the xsappname.

The xsappname of an SAP reuse service that is bound to your application can be found as part of the VCAP_SERVICES JSON structure under the path VCAP_SERVICES.<service>.credentials.xsappname.

The following example shows this in more detail:

import com.sap.cloud.mt.subscription.json.ApplicationDependency;

@Value("${vcap.services.<my-service-instance>.credentials.xsappname}")
private String xsappname;

@On(event = MtSubscriptionService.EVENT_GET_DEPENDENCIES)
public void onGetDependencies(MtGetDependenciesEventContext context) {
    ApplicationDependency dependency = new ApplicationDependency();
    dependency.xsappname = xsappname;
    context.setResult(Arrays.asList(dependency));
}

Unsubscribe Tenant

Unsubscription events are generated, when a tenant is off-boarded. By default, the tenant-specific database container is not deleted during off-boarding. You can change this behavior by registering a custom event handler as illustrated in the following examples:

Synchronous Tenant Unsubscription

By default an EVENT_UNSUBSCRIBE is sent when a tenant is removed. The following example shows how to add custom logic for this event:

@Before(event = MtSubscriptionService.EVENT_UNSUBSCRIBE)
public void beforeUnsubscribe(MtUnsubscribeEventContext context) {
    // Activities before off-boarding
}

You can also register an @After handler, for example to notify when removal is finished:

@After(event = MtSubscriptionService.EVENT_UNSUBSCRIBE)
public void afterUnsubscribe(MtUnsubscribeEventContext context) {
    // Notify off-boarding finished
}

Deleting Tenant Containers During Tenant Unsubscription

By default, tenant-specific database containers aren’t deleted during removal. However, you can register a customer handler change this behavior. For example:

@Before(event = MtSubscriptionService.EVENT_UNSUBSCRIBE)
public void beforeUnsubscribe(MtUnsubscribeEventContext context) {
    // Trigger deletion of database container of off-boarded tenant
    context.setDelete(true);
}

Configuring the Required Services

To enable multitenancy on the SAP Cloud Platform, three services are involved:

  • XSUAA
  • Service Manager
  • SaaS Provisioning service (saas-registry)

Only when these services are bound to your application, the multitenancy feature is turned on. You can either create and configure these services manually, see also Developing Multitenant Applications in the Cloud Foundry Environment. The following sections describe how to configure and bind these services by means of an mta.yaml file.

XSUAA

A special configuration of an XSUAA service instance is required to enable authorization between the SaaS Provisioning service, CAP Java application, and MTX sidecar.

The service can be configured in the mta.yaml by adding an xsuaa resource as follows:

resources:
  [...]
  - name: xsuaa
    type: com.sap.xs.uaa
    parameters:
      service-plan: broker
      path: ./security.json
      config:
        xsappname: <appname>

Choose a value for property xsappname that is unique globally.

Also, you have to create an Application Security Descriptor (security.json) file, which must include two scopes:

  • mtcallback
  • mtdeployment

You can also use custom scope names by configuring them. Use the following application configuration properties:

  • mtcallback: cds.multitenancy.security.subscription-scope
  • mtdeployment: cds.multitenancy.security.deployment-scope

The mtcallback scope is required by the onboarding process. The mtdeployment scope is required to redeploy database artifacts at runtime.

An example security.json file looks like this:

{
    "xsappname": "<appname>",
    "tenant-mode": "shared",
    "scopes": [
        {
            "name": "$XSAPPNAME.mtcallback",
            "description": "Multi Tenancy Callback Access",
            "grant-as-authority-to-apps": [
                "$XSAPPNAME(application, sap-provisioning, tenant-onboarding)"
            ]
        },
        {
            "name": "$XSAPPNAME.mtdeployment",
            "description": "Scope to trigger a re-deployment of the database artifacts"
        }
    ],
    "authorities": [
        "$XSAPPNAME.mtdeployment"
    ]
}

In this example, the grant-as-authority-to-apps section is used to grant the mtcallback scope to the applications sap-provisioning and tenant-onboarding. These are services provided by SAP Cloud Platform involved in the onboarding process.

It isn’t necessary to have the security configuration in a separate file. It can also be added to the mta.yaml file directly.

WARNING: The mtcallback and mtdeployment scopes must not be exposed in a role template because otherwise customers could delete tenants of other customers and trigger deployments of database artifacts. Instead, the mtdeployment scope is exposed in the authorities section. This enables getting the scope through client credential flow. If you implement a service broker, you also need to specify "authorities-inheritance": false to prevent that the authorities will be contained in cloned service instances.

Service Manager

A service-manager instance is required that the CAP Java runtime can create database containers per tenant at application runtime. It doesn’t require special parameters and can be added as a resource in mta.yaml as follows:

resources:
  [...]
  - name: service-manager
    type: org.cloudfoundry.managed-service
    parameters:
      service: service-manager
      service-plan: container

SaaS Provisioning Service (saas-registry)

A saas-registry service instance is required to make your application known to the SAP Cloud Platform Provisioning service and to register the endpoints that should be called when tenants are added or removed. The service can be configured as a resource in mta.yaml as follows (see also Register the Multitenant Application to the SaaS Provisioning Service):

resources:
  [...]
  - name: saas-registry
    type: org.cloudfoundry.managed-service
    parameters:
      service: saas-registry
      service-plan: application
      config:
        appName: <app display name>
        xsappname: <appname>
        appUrls:
          getDependencies: ~{srv/srv-url}/mt/v1.0/subscriptions/dependencies
          onSubscription: ~{srv/srv-url}/mt/v1.0/subscriptions/tenants/{tenantId}
    requires:
      - name: srv

It’s required to configure the parameters:

  • appName: Choose an appropriate application display name.
  • xsappname: Use the value for xsappname you configured at your UAA service instance.
  • appUrls: Configure the callback URLs used by the SaaS Provisioning service to get the dependencies of the application and to trigger a subscription. In the above example, the property ~{srv/srv-url} that is provided by the srv module is used (see also Wiring It Up). If you use different module and property names for your CAP Java backend module, you have to adapt these properties here accordingly.

Adding the MTX Sidecar Application

This section describes how to use the cds-mtx Node.js module and add the MTX sidecar microservice to the mta.yaml file.

In a dedicated project subfolder named mtx-sidecar, create a Node.js start script in a file named server.js to bootstrap the cds-mtx library:

const app = require('express')();
const cds = require('@sap/cds');

const main = async () => {
	app.use(require('express').json());

	await cds.connect.to('db');
	const PORT = process.env.PORT || 4004;
	const scope = process.env.CDS_MULTITENANCY_SECURITY_SUBSCRIPTIONSCOPE || 'mtcallback';
	await cds.mtx.in(app);
	
	const provisioning = await cds.connect.to('ProvisioningService');
	provisioning.before(['UPDATE', 'DELETE', 'READ'], 'tenant', async (req) => {
		// Check for the scope of the SaaS Provisioning Service
		if (!req.user.is(scope)) {
			// Reject request
			const e = new Error('Forbidden');
			e.code = 403;
			return req.reject(e);
		}
	});
			
	app.listen(PORT);
}

main();

Note: By default, this script implements authorization and checks for the scope mtcallback. If you use a custom scope name for requests issued by the SaaS Provisioning Service in your application security descriptor (security.json), you have to configure the custom scope name at the MTX sidecar as well. Use the environment variable CDS_MULTITENANCY_SECURITY_SUBSCRIPTIONSCOPE, for example, by specifying it in the mta.yaml file.

To define the dependencies and start command, also create a file package.json like this:

{
    "name": "deploy",
    "dependencies": {
        "@sap/cds": "^4.1.7",
        "@sap/cds-mtx": "^1.0.17",
        "@sap/hdi-deploy": "^3.11.11",
        "@sap/instance-manager": "^2.2.0",
        "@sap/xssec": "^3",
        "@sap/hana-client": "^2.5.105",
        "express": "^4.17.1",
        "passport": "^0.4.1"
    },
    "scripts": {
        "start": "node server.js"
    }
}

Because the MTX sidecar will build the CDS model, you need to configure the build by means of two .cdsrc files:

The first .cdsrc file goes into the root folder of your project and specifies from which location the CDS files should be collected. The following example demonstrates this:

{
    "build": {
        "target": ".",
        "tasks": [
            {
                "for": "java-cf",
                "src": "srv",
                "options": {
                    "model": [
                        "db",
                        "srv",
                        "app"
                    ]
                }
            },
            {
                "for": "mtx",
                "src": ".",
                "dest": "mtx-sidecar",
                "options": {
                    "model": [
                        "db",
                        "srv",
                        "app"
                    ]
                }
            },
            {
                "for": "hana",
                "src": "db",
                "options": {
                    "model": [
                        "db",
                        "srv",
                        "app"
                    ]
                }
            }
        ]
    },
    "hana": {
        "deploy-format": "hdbtable"
    },
    "requires": {
        "db": {
            "kind": "sql"
        }
    },
    "odata": {
        "version": "v4"
    }
}

You only need to change this configuration if you named your project folders, app, db, srv, and mtx-sidecar differently.

A detailed description of this configuration file can be found in Build Configuration. In the following, you find a short summary of this example:

The build section defines the build tasks that should be executed. Three build tasks are defined in this example:

Task Description
java-cf Generates csn.json and edmx files
mtx Collects .cds files to copy to mtx-sidecar directory, generates i18n.json
hana Generates Hana artifacts

In the previous example, the options section specifies the source directories for each build task.

Note: The hana build task is optional because the SAP HANA artifacts are also generated by the mtx-sidecar directly. However, the generated SAP HANA artifacts enable you to test your application in a single tenant scenario.

The second .cdsrc file goes into the mtx-sidecar directory. This could look, for example, like:

{
    "hana": {
        "deploy-format": "hdbtable"
    },
    "build": {
        "tasks": [
            {
                "for": "hana",
                "src": "db",
                "options": {
                    "model": [
                        "db",
                        "srv",
                        "app"
                    ]
                }
            },
            {
                "for": "java-cf",
                "src": "srv",
                "options": {
                    "model": [
                        "db",
                        "srv",
                        "app"
                    ]
                }
            }
        ]
    },
    "odata": {
        "version": "v4"
    },
    "requires": {
        "db": {
            "kind": "hana",
            "multiTenant": true,
            "vcap": {
				"label": "service-manager"
            }
        },
        "uaa": {
            "kind": "xsuaa"
        }
    }
}

You only need to change this configuration if you named your project folders, app, db, srv, and mtx-sidecar differently.

In this file, the requires section configures the service instances that should be used by the mtx-sidecar. In this case, it’s an instance of the UAA Service, to enable authentication and authorization, as well as the Service Manager, that enables multitenancy.

Now, add the mtx-sidecar module to your mta.yaml file:

modules:
  [...]
  - name: mtx-sidecar
    type: nodejs
    path: mtx-sidecar
    parameters:
      memory: 256M
      disk-quota: 512M
    requires:
      - name: xsuaa
      - name: service-manager
    provides:
      - name: mtx-sidecar
        properties:
          url: ${default-url}

The mtx-sidecar module requires the XSUAA and Service Manager services. Also you need to provide its URL to be able to configure the URL in the service module as shown in the previous mta.yaml. The authentication works through token validation.

Wiring It Up

To bind the previously mentioned services and the MTX sidecar to your CAP Java application, you could use the following example of the srv module in the mta.yaml file:

modules:
  [...]
  - name: srv
    type: java
    path: srv
    parameters:
      [...]
    requires:
      - name: service-manager
      - name: xsuaa
      - name: mtx-sidecar
        properties:
          CDS_MULTITENANCY_SIDECAR_URL: ~{url}
      - name: app
        properties:
          CDS_MULTITENANCY_APPUI_URL: ~{url}
          CDS_MULTITENANCY_APPUI_TENANTSEPARATOR: "."
    provides:
      - name: srv
        properties:
          srv-url: '${default-url}'

The environment variable CDS_MULTITENANCY_APPUI_URL configures the URL that is shown in the SAP Cloud Platform Cockpit. Usually it’s pointing to the app providing the UI (which is the module app in this example).

As value for CDS_MULTITENANCY_APPUI_TENANTSEPARATOR only "." is supported at the moment. The actual URL shown in the SAP Cloud Platform Cockpit is then composed of:

https://<subaccount subdomain>.<CDS_MULTITENANCY_APPUI_URL>

Database Schema Update

When shipping a new application version with an updated CDS model, the database schema for each subscribed tenant needs an update. The database schema update needs to be triggered explicitly (see following sections). The following events are sent when a new version of the CDS model is deployed. You can add custom logic by registering event handlers for these events. By default, the CAP Java runtime notifies the MTX sidecar to perform any schema upgrade if necessary.

Event Name Event Context
EVENT_ASYNC_DEPLOY MtAsyncDeployEventContext
EVENT_ASYNC_DEPLOY_STATUS MtAsyncDeployStatusEventContext

Furthermore, it’s often desired to update the whole service in a zero downtime manner. However, this section doesn’t deal with the details about updating a service productively, but describes tool support the CAP Java SDK offers to update database schemas.

The following sections describe how to trigger the database schema upgrade for tenants.

Deploy Endpoint

When multitenancy is configured, the CAP Java runtime exposes a REST endpoint to update database schemata:

You must use the scope mtdeployment for the following requests!

Deployment Request

Send this request when a new version of your application with an updated database schema was deployed. This call triggers updating the persistence of each tenant.

Route
POST /mt/v1.0/subscriptions/deploy/async

Note: This is the default endpoint. One or more endpoints might differ if you configure different endpoints through properties.

Body

The POST request must contain the following body:

{
  "tenants": [ "all" ]
}

Alternatively, you can also update single tenants:

{
  "tenants": ["<tenant-id-1>", "<tenant-id-2>", ...]
}
Response

The deploy endpoint is asynchronous, so it returns immediately with status code 202 and JSON structure containing a jobID value:

{
  "jobID": "<jobID>"
}

Job Status Request

You can use this jobID to check the progress of the operation by means of the following REST endpoint:

Route
GET /mt/v1.0/subscriptions/deploy/async/status/<jobID>
Response

The server responds with status code 200. During processing, the response looks like:

{
  "error": null,
  "status": "RUNNING",
  "result": null
}

Once a job is finished, the collective status is reported like this:

{
  "error": null,
  "status": "FINISHED",
  "result": {
      "tenants": {
          "<tenantId1>": {
              "status": "SUCCESS",
              "message": "",
              "buildLogs": "<build logs>"
          },
          "<tenantId2>": {
              "status": "FAILURE",
              "message": "<error log output>",
              "buildLogs": "<build logs>"
          }
      }
  }
}

Logs are persisted for a period of 30 minutes before they get deleted automatically. If you’re requesting the job status after the 30-minute period expired, you get a 404 Not Found response.

Deploy Main Class

As an alternative to calling the deploy REST endpoints, the CAP Java runtime also offers a main class that can be called from the command line while the CAP Java application is still stopped:

com.sap.cds.framework.spring.utils.Deploy

This way, you can synchronize updating the database schema for tenants with running the CAP Java application. This prevents Java code to access database artifacts that aren’t yet deployed to database. This synchronization can also be automated, for example using Cloud Foundry Tasks on SAP Cloud Platform.

The main class takes a list of tenant-ids as input parameters. If tenant-ids are specified, only these tenants are updated. If no input parameters are specified, all tenants are updated. The class waits until all deployments are finished and prints the result afterwards.

The start command to run this class is:

java -cp <jar-file> -Dloader.main=com.sap.cds.framework.spring.utils.Deploy org.springframework.boot.loader.PropertiesLauncher

However, in the Cloud Foundry environment it can be tricky to construct such a command. The reason is, that the JAR file is extracted by the Java Buildpack, the place of the Java executable isn’t easy to determine and also differs for different Java versions. Therefore, it’s easier to just adapt the start command, that is already generated by the buildpack and run the adapted command:

sed -i 's/org.springframework.boot.loader.JarLauncher/-Dloader.main=com.sap.cds.framework.spring.utils.Deploy org.springframework.boot.loader.PropertiesLauncher/g' /home/vcap/staging_info.yml && jq -r .start_command /home/vcap/staging_info.yml | bash

Development Scenario

As stated previously, local testing with SQLite works as usual. In this case, the feature is disabled.

If you want to test using a hybrid scenario, meaning to access cloud services locally, it’s also possible. You can decide whether you want to access just one fixed SAP HANA service binding or access all available SAP HANA service bindings that were created through the Service Manager binding.

Static SAP HANA Binding

For the static case, just copy the credentials of the SAP HANA service binding you want to use into the default-env.json. You can, for example, see all application-managed service instances in the SAP Cloud Platform Cockpit. The app behaves like in the single tenant case.

You can use the SAP Cloud Business Application Tools for Eclipse to deploy your local database artifacts.

It’s also possible to update the database artifacts using cds deploy -2 hana. The needed credentials are contained in default-env.json.

Service Manager Binding

If you want to test multitenancy locally, just copy the complete Service Manager binding into the default-env.json. If you have extensibility enabled, you also need to set the property cds.multitenancy.sidecar.url to the URL of the deployed MTX sidecar app. Now you can access the data of different tenants locally, if user information is set for the requests to your locally running server.

You can locally authenticate at your app either through mock users or the UAA.

The configuration of mock users is described in section Security. For a mock user, you can also set the tenant property. The value needs to be the subaccount ID, which can be found in SAP Cloud Platform Cockpit in the subaccount details. You can then authenticate at your app using basic authentication. If you already secured your services, the browser asks you automatically for credentials. Otherwise, you can also set username and password explicitly, for example, in Postman.

If you want to authenticate using the XSUAA, just copy the XSUAA service binding into the default-env.json. You then need to have a valid token for the tenant to authenticate. This can be obtained through client-credential-flow, for example, using Postman.

Important: Requests without user information fail!

Currently you need to push the changes to Cloud Foundry, to update the database artifacts. If you’re working on the data model, it’s recommended to use a static SAP HANA binding.

Properties

A number of settings can be configured through properties. They can be found in the following table. The prefix for multitenancy-related settings is cds.multitenancy.

Name Description Default
servlet.path Path of the subscription and deployment endpoints /mt/v1.0/subscriptions
servlet.enabled Flag to deactivate the endpoints true
datasource.pool Pool to use for the tenant-dependent data source. Possible values are: hikari, tomcat, atomikos hikari
datasource.<pool>.<property> Pool-specific properties Default depends on the pool and property
datasource.combinePools.enabled Only one data source pool per database is created. See Combine SAP HANA Data Source Pools false
datasource.hanaDatabaseIds List of SAP HANA database IDs, needed, for example, with oneDataSourcePerDb = true Not set by default
instancemanager.timeout Timeout for requests to the Service Manager in seconds 3600
security.subscription-scope Scope necessary to call the subscription endpoints mtcallback
security.deployment-scope Scope necessary to call the deployment endpoints mtdeployment
sidecar.url URL of the MTX sidecar Not set by default
sidecar.reuse-token Switch to reuse the request token for requests to the MTX sidecar false
sidecar.cache.maxsize Maximum size of model and metadata cache 20
sidecar.cache.expiration-time Lifetime of an entry in seconds after the entry’s creation, the most recent replacement of its value, or its last access 600
sidecar.cache.refresh-time Time after that a cached entry is refreshed 60
app-ui.url URL of the application UI Not set by default
app-ui.tenant-separator Separator of the tenant in the URL, for example, “.” or “-“ Not set by default
oneDataSourcePerDb Only one data source pool per database is created. false
hanaDatabaseIds List of SAP HANA database IDs, needed, for example, with oneDataSourcePerDb = true Not set by default

Combine SAP HANA Data Source Pools

By default, one data source pool is created for each tenant that accesses the application. If you maintain a large number of tenants, this architecture can lead to resource problems. To solve this problem, a new mode that creates only one database pool per database, is introduced. Activate it by setting the property datasource.combinePools.enabled = true. When using this mode, it’s required to set the property datasource.hanaDatabaseIds, where the IDs of all used SAP HANA databases are specified. The new mode incurs an additional latency and is therefore not activated by default.