Implement Subscription Callbacks for a Multitenant Application
Implement the Subscribe/Unsubscribe callbacks of the SAP SaaS Provisioning Service (saas-registry) and use the information provided in the subscription/unsubscription event for your Multitenant Application in SAP BTP, Kyma runtime.
Overview
You will learn
- How to implement the Subscribe/Unsubscribe callbacks for the SAP SaaS Provisioning Service
- How to automatically create
APIRulefor tenant-specific URL in Kyma runtime
Prerequisites
Prerequisites
- You have finished the tutorial Secure a Multitenant Application with the Authorization and Trust Management Service (XSUAA)
Steps
To perform internal tenant onboarding activities, you must implement the subscription and unsubscription callbacks of the SAP Software-as-a-Service Provisioning service (saas-registry) and use the information provided in the subscription event. You can also implement the getDependencies callback to obtain the dependencies of any SAP reuse services by your application. For more details, please read SAP Help Portal: Develop the Multitenant Application.
When a consumer tenant creates or revokes a subscription to your multitenant application via SAP BTP cockpit, the SAP SaaS Provisioning service calls the multitenant application with subscription callbacks:
PUT – callback/v1.0/tenants/*
To inform the application that a new consumer tenant wants to subscribe to the application, implement the callback service with the PUT method. The callback must return a 200 response code and a string in the body, which is the access URL of the tenant to the application subscription (the URL contains the subdomain). This URL is generated by the multitenant application based on the approuter configuration (see Configure the approuter Application)
For example: https://customer-cb741ym5-approuter.c-xxxxxxx.kyma.ondemand.com
The URL must be in the format:
https://<subaccount-subdomain>-<approuter-application-host>.<domain>
DELETE – callback/v1.0/tenants/*
To inform the application that a tenant has revoked its subscription to the multitenant application and is no longer allowed to use it, implement the same callback with the DELETE method.
Payload of subscription PUT and DELETE methods:
{
"subscriptionAppId": "<value>",
# The application ID of the main subscribed application.
# Generated by Authorization and Trust Management service(XSUAA)-based on the xsappname.
"subscriptionAppName": "<value>"
# The application name of the main subscribed application.
"subscribedTenantId": "<value>"
# ID of the subscription tenant
"subscribedSubdomain": "<value>"
# The subdomain of the subscription tenant (hostname for the identityzone).
"globalAccountGUID": "<value>"
# ID of the global account
"subscribedLicenseType": "<value>"
# The license type of the subscription tenant
}GET – callback/v1.0/dependencies
[
{
"xsappname" : "<value>"
# The xsappname of the reuse service which the application consumes.
# Can be found in the environment variables of the application:
# VCAP_SERVICES.<service>.credentials.xsappname
}
]The callback must return a 200 response code and a JSON file with the dependent services
appNameandappId, or just thexsappname.The JSON response of the callback must be encoded as either UTF8, UTF16, or UTF32, otherwise an error is returned.
A tenant-specific URL should be provided to customers in the onboarding process and exposed to the Internet at the same time. Otherwise, customers cannot access the tenant. The URL is exposed in Kyma runtime through APIRule, which must be created dynamically during the onboarding/offboarding process using Kubernetes client for NodeJS.
In the kyma-multitenant-node/routes/index.js file, implement the subscription and unsubscription callbacks. Replace the placeholder with your cluster domain.
//******************************** API Callbacks for multitenancy ********************************
/**
* Request Method Type - PUT
* When a consumer subscribes to this application, SaaS Provisioning invokes this API.
* We return the SaaS application url for the subscribing tenant.
* This URL is unique per tenant and each tenant can access the application only through it's URL.
*/
router.put('/callback/v1.0/tenants/*', async function(req, res) {
//1. create tenant unique URL
var consumerSubdomain = req.body.subscribedSubdomain;
var tenantAppURL = "https://" + consumerSubdomain + "-approuter." + "<clusterdomain>"; // Replace <clusterdomain> with your actual Kyma cluster domain
//2. create apirules with subdomain
const kc = new k8s.KubeConfig();
kc.loadFromCluster();
const k8sApi = kc.makeApiClient(k8s.CustomObjectsApi);
const apiRuleTempl = createApiRule.createApiRule(
EF_SERVICE_NAME,
EF_SERVICE_PORT,
consumerSubdomain + "-approuter",
kyma_cluster);
try {
await k8sApi.getNamespacedCustomObject({
group: KYMA_APIRULE_GROUP,
version: KYMA_APIRULE_VERSION,
namespace: EF_APIRULE_DEFAULT_NAMESPACE,
plural: KYMA_APIRULE_PLURAL,
name: apiRuleTempl.metadata.name
});
// If found, respond and return early
console.log(apiRuleTempl.metadata.name + ' already exists.');
return res.status(200).send(tenantAppURL);
} catch (err) {
//create apirule if non-exist
console.warn(apiRuleTempl.metadata.name + ' does not exist, creating one...');
try {
const createResult = await k8sApi.createNamespacedCustomObject({
group: KYMA_APIRULE_GROUP,
version: KYMA_APIRULE_VERSION,
namespace: EF_APIRULE_DEFAULT_NAMESPACE,
plural: KYMA_APIRULE_PLURAL,
body: apiRuleTempl
});
console.log('APIRule creation result:', createResult);
if (createResult && createResult.kind === "APIRule") {
console.log("API Rule created!");
return res.status(200).send(tenantAppURL);
} else {
// fallback: always send 200 if no error thrown
return res.status(200).send(tenantAppURL);
}
} catch (err) {
console.log(err);
console.error("Fail to create APIRule");
return res.status(500).send("create APIRule error");
}
}
});
/**
* Request Method Type - DELETE
* When a consumer unsubscribes this application, SaaS Provisioning invokes this API.
* We delete the consumer entry in the SaaS Provisioning service.
*/
router.delete('/callback/v1.0/tenants/*', async function(req, res) {
const consumerSubdomain = req.body.subscribedSubdomain;
if (!consumerSubdomain) {
return res.status(400).send("Missing 'subscribedSubdomain' in request body");
}
const kc = new k8s.KubeConfig();
kc.loadFromCluster();
const k8sApi = kc.makeApiClient(k8s.CustomObjectsApi);
const apiRuleTempl = createApiRule.createApiRule(
EF_SERVICE_NAME,
EF_SERVICE_PORT,
consumerSubdomain + "-approuter",
kyma_cluster);
try {
await k8sApi.deleteNamespacedCustomObject({
group: KYMA_APIRULE_GROUP,
version: KYMA_APIRULE_VERSION,
namespace: EF_APIRULE_DEFAULT_NAMESPACE,
plural: KYMA_APIRULE_PLURAL,
name: apiRuleTempl.metadata.name
});
console.log('APIRule deleted:', apiRuleTempl.metadata.name);
return res.status(200).send("deleted");
} catch (err) {
if (err && err.statusCode === 404) {
// Not found: treat as success (idempotent delete)
console.warn('APIRule not found, treating as deleted:', apiRuleTempl.metadata.name);
return res.status(200).send("deleted");
}
console.error("API Rule deletion error", err);
return res.status(500).send("API Rule deletion error");
}
});
//************************************************************************************************
1. Add constant values and variables in the kyma-multitenant-node/routes/index.js, replace <namespace> with your own namespace name:
var express = require('express');
var router = express.Router();
const EF_SERVICE_NAME = 'kyma-multitenant-approuter-multitenancy';
const EF_SERVICE_PORT = 8080;
const EF_APIRULE_DEFAULT_NAMESPACE = '<namespace>'; // Replace <namespace> with your actual namespace
const KYMA_APIRULE_GROUP = 'gateway.kyma-project.io';
const KYMA_APIRULE_VERSION = 'v2';
const KYMA_APIRULE_PLURAL = 'apirules';
const k8s = require('@kubernetes/client-node');
const createApiRule = require('./createApiRule');
var kyma_cluster = process.env.CLUSTER_DOMAIN || "UNKNOWN";2. Create a new file named createApiRule.js in the directory kyma-multitenant-node/routes/ to provide APIRule object:
module.exports = {
createApiRule: createApiRule
}
function createApiRule(svcName, svcPort, host, clusterName) {
let forwardUrl = host + '.' + clusterName;
const supportedMethodsList = [
'GET',
'POST',
'PUT',
'PATCH',
'DELETE',
'HEAD',
];
const access_strategy = {
path: '/*',
methods: supportedMethodsList,
noAuth: true,
// mutators: [{
// handler: 'header',
// config: {
// headers: {
// "x-forwarded-host": forwardUrl,
// }
// },
// }],
accessStrategies: [{
handler: 'allow'
}],
};
const apiRuleTemplate = {
apiVersion: 'gateway.kyma-project.io/v2',
kind: 'APIRule',
metadata: {
name: host + '-apirule',
},
spec: {
gateway: 'kyma-system/kyma-gateway',
hosts: [host],
service: {
name: svcName,
port: svcPort,
},
rules: [access_strategy],
},
};
return apiRuleTemplate;
}3. Add dependency "@kubernetes/client-node" in the kyma-multitenant-node/package.json file, for example:
"dependencies": {
"@kubernetes/client-node": "~1.3.0",
...
}
}To automatically create APIRule from a pod, proper RoleBinding should be granted. Add a RoleBinding in the k8s-deployment-backend.yaml with the following content, replace <namespace> with your own namespace name:
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: broker-rolebinding
subjects:
- kind: ServiceAccount
name: default
namespace: <namespace>
roleRef:
kind: ClusterRole
name: api-gateway-manager-role
apiGroup: rbac.authorization.k8s.ioResources
Discussion
Share feedback on this tutorial or join the conversation in SAP Community.