Skip to Content
Technical Articles
Author's profile photo Venu Ravipati

Multi-tenancy of destination service with cloud foundry applications

Introduction:

 

Hi Community,

The purpose of this blog is to share my learnings of cloud foundry destination service. I searched for blogs on multi-tenancy in cloud foundry destination service. I did not find one or I did not search enough?. So i did some further investigation and experimentation in this area and want to share my findings and learnings so that others looking for similar information can benefit out of it.

As we know, cloud foundry destination service is used to store service endpoints and credential details in a secure way that applications can use during runtime to connect to the services. Destination service is one of the service that inherently supports multi-tenancy. Let us understand multi-tenancy of destination service below.

 

xs-security.json:

 

Multitenancy logically starts at security as tenant level isolation is needed for access to the application. So in xs-security.json file “tenant-mode” is defined as “shared” and a scope “$XSAPPNAME.Callback” is defined for application to access saas-registry.

 

{
    "xsappname": "cfdestination",
    "tenant-mode": "shared",
    "description": "Security profile of cfdestination app",
    "scopes": [{
        "name": "$XSAPPNAME.Callback",
        "description": "With this scope set, the callbacks for tenant onboarding, offboarding and getDependencies can be called.",
        "grant-as-authority-to-apps": [
            "$XSAPPNAME(application,sap-provisioning,tenant-onboarding)"
        ]
    },
    {
        "name": "$XSAPPNAME.read",
        "description": "Read Scope"
    },
    {
        "name": "uaa.user",
        "description": "uaa.user"
    }
    ],
    "role-templates": [
        { 
            "name":"MultitenancyCallbackRoleTemplate",
            "description":"Call callback-services of applications",
            "scope-references":[ 
               "$XSAPPNAME.Callback"
            ]
        },
        {
            "name": "Viewer",
            "description": "Viewer Role Template",
            "scope-references": [
                "$XSAPPNAME.read",
                "uaa.user"
            ]
        }
    ],
    "role-collections": [
        {
            "name": "MyDestinationApplicationViewer",
            "description": "My Destination Application Viewer",
            "role-template-references": [
                "$XSAPPNAME.Viewer",
                "$XSAPPNAME.MultitenancyCallbackRoleTemplate"
            ]
        }
    ]
}

 

Implement rest endpoints for saas-registry:

 

Develop Node.js application with express and implement following endpoints for saas-registry

/callback/v1.0/tenants/*

For tenant onboarding and offboarding.

/callback/v1.0/dependencies

For dependencies

 

Endpoint implementations:

router.get('/callback/v1.0/dependencies', dependencyCallbackRoute);
router.put('/callback/v1.0/tenants/*', subscribeTenantRoute);
router.delete('/callback/v1.0/tenants/*', unsubscribeTenantRoute);

export async function dependencyCallbackRoute(req: Request, res: Response) {
        var dest = xsenv.getServices({ destination: { tag: 'destination' } }).destination;
        var xsappname = dest.xsappname;
        res.status(200).json([{'appId': xsappname, 'appName': 'destination'}]);
}

export async function subscribeTenantRoute(req: Request, res: Response) {
    var consumerSubdomain = req.params["0"];
    var tenantAppURL = "https:\/\/" + consumerSubdomain + "-cfdestinationui.cfapps.eu10.hana.ondemand.com";
    res.status(200).send(tenantAppURL);
}

export async function unsubscribeTenantRoute(req: Request, res: Response) {
    res.status(200);
}

 

Implement endpoint for retrieving destination details:

 

Implement a custom endpoint that provides the destination details for the given destination name. SAP Cloud SDK getDestination() is used to fetch the cloud foundry destinations.

 

import { getDestination, DestinationOptions, DestinationSelectionStrategies } from '@sap-cloud-sdk/core';

router.get("/api/cloudfoundry/destinations", getCFDestinationsRoute);

let options: DestinationOptions = {}

export async function getCFDestinationsRoute(req: Request, res: Response) {
    var paramdestination = req.query.destination;
    options.selectionStrategy = DestinationSelectionStrategies.subscriberFirst;
    options.userJwt = req.authInfo.getTokenInfo().getTokenValue();
    var destination = await getDestination(paramdestination, options);
    if (req.authInfo !== undefined && req.authInfo.checkLocalScope("read")) {
        res.status(200).send(destination.originalProperties);
    } else {
        res.type("text/plain").status(401).send(`ERROR: Not Authorized. Missing Necessary scope`)
    }
    
}

 

Implement Approuter:

 

Implement approuter that acts as single entry point to the application and takes care of user authentication.

 

{
  "name": "cfdestinationui",
  "version": "1.0.0",
  "description": "UI Module for Destination Application",
  "devDependencies": {
    "@sap/grunt-sapui5module-bestpractice-build": "^0.0.14"
  },
  "dependencies": {
    "@sap/approuter": "2.7.1"
  },
  "scripts": {
    "start": "node node_modules/@sap/approuter/approuter.js"
  }
}

 

Configure approuter in xs-app.json file as below:

 

{
    "welcomeFile": "/cfdestinationui/index.html",
    "authenticationMethod": "route",
    "logout": {
        "logoutEndpoint": "/logout"
    },
    "routes": [{
        "source": "^/cfdestinationui/(.*)$",
        "target": "$1",
        "localDir": "webapp"
    }, {
        "source": "^/cfdestinationbackend/(.*)$",
        "target": "$1",
        "csrfProtection": true,
        "authenticationType": "xsuaa",
        "destination": "cfdestinationbackend_api"
    }
]
}

 

Multi Target Application:

 

The application is built as multi target application with an mta.yaml file. It has two modules. cfdestinationbackend, a Node.js application and cfdestinationui, an approuter application. While other parts of mta.yaml is quite standard as with any mta deployments, I request your attention to  section TENANT_HOST_PATTERN which will help to identify the tenant information from the application url.

 

ID: cfdestinationapp
_schema-version: '2.1'
description: A multi-tenant enabled app for cf destination
version: 1.0.0

modules:

  - name: cfdestinationbackend
    type: nodejs
    path: cfdestinationbackend
    parameters:
      disk-quota: 512M
      memory: 512M
    provides:
      - name: cfdestinationbackend_api
        properties:
          backend_app_url: '${default-url}'
    requires:
      - name: cfdestination-uaa
      - name: cfdestination-destination
    properties:
      SAP_JWT_TRUST_ACL:
       - clientid: "*"
         identityzone: "*"

  - name: cfdestinationui
    type: html5
    path: cfdestinationui
    parameters:
      disk-quota: 512M
      memory: 512M
    build-parameters:
      builder: grunt
    requires:
      - name: cfdestination-uaa
      - name: cfdestinationbackend_api
        group: destinations
        properties:
          name: cfdestinationbackend_api
          url: '~{backend_app_url}'
          forwardAuthToken: true
    properties:
      TENANT_HOST_PATTERN: "^(.*)-cfdestinationui.cfapps.eu10.hana.ondemand.com"

resources:
  - name: cfdestination-uaa
    parameters:
      path: ./xs-security.json
      service-plan: application
    type: com.sap.xs.uaa

  - name: cfdestination-destination
    type: org.cloudfoundry.managed-service
    parameters:
      service: destination
      service-plan: lite

 

Deploy the Application to Cloud Foundry:

 

Once deployed, the below service instances created, and applications deployed.

 

Service Instances:

 

Applications:

 

SaaS registry:

 

Configuration file for saas-registry service instance creation is shown below:

 

{
    "appId": "cfdestination!t50911",
    "displayName": "CF Destination App",
    "description": "App to Demo CF Destination",
    "category": "Demo Provider",
    "appUrls": {
        "onSubscription": "https://13fa8802trial-dev-cfdestinationbackend.cfapps.eu10.hana.ondemand.com/callback/v1.0/tenants/{tenantId}",
        "getDependencies": "https://13fa8802trial-dev-cfdestinationbackend.cfapps.eu10.hana.ondemand.com/callback/v1.0/dependencies"
    }
}

 

Get XSAPPNAME from VCAP_SERVICES.xsuaa.credentials.xsappname (here cfdestination!t50911)

Get the onSubscription and getDependencies URL from backend application.

 

Create Saas registry service instance:

cf cs saas-registry application cf-destination-registry -c config.json

 

Bind the backend app to the SaaS registry service instance:

cf bs cfdestinationbackend cf-destination-registry

 

Re-stage the app by executing this command:

cf restage cfdestinationbackend

 

Testing in Provider Tenant:

 

Create destination named “mydestination” at subaccount level as shown

 

 

Calling the API with destination name (/api/cloudfoundry/destinations?destination=mydestination) retrieves the details of destination.

 

 

Now a destination with same name “mydestination” is created in the Destination service instance.

 

 

Calling the API (/api/cloudfoundry/destinations?destination=mydestination) retrieves the details of destination from destination service instance. The SAP Cloud SDK gives preference to the destination in service instance over subaccount and provides the destination details form service instance.

 

 

Onboard a subscriber tenant:

 

Create a Cloud Foundry subaccount for the application consumer (tenant).

 

 

Now additional subaccount just created is seen in the global account screen

 

 

Navigate to the new consumer subaccount and to the Subscriptions tab. The CF Destination App is seen under the Demo Provider category.

 

 

Subscribe to the Application:

 

 

Upon successful subscription, the status shows subscribed:

 

 

Create a new route for the newly subscribed tenant and assign to the approuter/ ui application:

 

 

 

Subscriber subaccount Test:

 

Access the application using subaccount specific route

https://13fa8802mytenant1-cfdestinationui.cfapps.eu10.hana.ondemand.com

Once login, call the api endpoint

https://13fa8802mytenant1-cfdestinationui.cfapps.eu10.hana.ondemand.com/cfdestinationbackend/api/cloudfoundry/destinations?destination=mydestination

Accessing the API will still give the destination details from the destination service instance.

 

 

 

Now create a destination in subscriber subaccount “mytenant1” with same name “mydestination”.

 

 

Calling the api again, will provide the destination details of “mytenant1” subaccount. The SAP Cloud SDK gives preference to subscriber subaccount destinations over provider subaccount destinations.

 

 

Conclusion:

Destination service of cloud foundry is a multitenant service and the multitenant capabilities can be leveraged with sap cloud sdk. The sample application shown in this blog is shared in github repo.

https://github.com/ravipativenu/cf-destinations

There are other blogs on working with destination service in CAP and Node.js and I provided some great references in the references section. I hope you got as much fun reading this blog as I did writing this. I would love to hear your comments and feedback.

Thank you and have a great day…

 

References:

Use SCP CF Destination service in NodeJs locally by Wouter Lemaire

https://blogs.sap.com/2020/05/28/use-the-destination-service-in-nodejs-locally/

CAP: Consume External Service – Part 1 by Jhodel Cailan

https://blogs.sap.com/2020/05/26/cap-consume-external-service-part-1/

Call SAP Cloud Platform destinations from your Node.js application by Maria Trinidad MARTINEZ GEA

https://blogs.sap.com/2018/10/16/call-sap-cloud-platform-cloud-foundry-destinations-from-your-node.js-application/

Consuming destinations in cloud foundry using axios in a nodejs application by Joachim Van Praet

https://blogs.sap.com/2019/11/13/consuming-destinations-in-cloud-foundry-using-axios-in-a-nodejs-application/

Multitenancy Architecture on SAP Cloud Platform, Cloud Foundry environment by Jan Rumig

https://blogs.sap.com/2018/09/26/multitenancy-architecture-on-sap-cloud-platform-cloud-foundry-environment/

Using SaaS Provisioning Service to develop Multitenant application on SAP Cloud Platform, Cloud Foundry Environment by SANDEEP TDS

https://blogs.sap.com/2018/10/25/using-saas-registry-to-develop-multitenant-application-on-sap-cloud-platform-cloud-foundry-environment/

Developing Multitenant Applications on SAP Cloud Platform, Cloud Foundry environment by Hariprasauth R

https://blogs.sap.com/2018/09/17/developing-multitenant-applications-on-sap-cloud-platform-cloud-foundry-environment/

SAP Cloud SDK for JS

https://github.com/SAP/cloud-sdk

 

 

 

 

Assigned Tags

      13 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Prakritidev Verma
      Prakritidev Verma

      Hey,

       

      Thanks for sharing this detailed blog, it really helped a lot I wanted to you are you aware of how can I use HANA HDI containers for achieving database level multitenancy ?

      Thanks,

      Author's profile photo Anil k
      Anil k

      Hi Prakritidev Verma ,

      I have the same question , were you able to figure it out ?

      Author's profile photo Prakritidev Verma
      Prakritidev Verma

      Hi Anil,

       

      The current approach is not very UX friendly, there is a playlist on this topic in sap hana academy youtube channel. There is a service called as service-manager which inject the HDI credentials into the incoming request.

      If you are using CDS with nodejs or something else then its easy, they have added documentation for service-manager.

      If you are not using CDS the for creating your services, then you can use a yoman template developed by saphanaacademy, they also have added a series on youtube for this whole process.

      One annoing thing is that the HDI container created by the service-manager will not be visible in hana explorer. You have to use a cli command for fetching the db details, and connect with it manually (apparently I was not able to connect with it manually, let me know if you can figure that out).

      The ecosystem is still immature I would say, but it works.

      Let me know if you are not clear.

       

      Thanks.

      Author's profile photo Anil k
      Anil k

      Hi Prakritidev,

      Thanks for the quick reply. I watched the playlist , i was not able to see the data from explorer as well , if the HDI container is created by service manager.

      I'm trying to build multitenant app using java backend (springboot) ,not using CDS , wondering if

      i should use HDI containers . If not HDI containers , not sure how  java app should multiple pick tenant specific  schema dynamically... do you have any reference for tenet aware java backend with hana db??

       

      Author's profile photo Prakritidev Verma
      Prakritidev Verma

      Well JavaScript is their main language now, I haven't seen any java implementation of service manager.

      what you can do is use the the saphanaacademy yo-man generator which will give you a boiler plate code, use that as a microservice for tenant registration, and tenant detection (this step is very similar to zuul api gateway. We can then inject db connection into the incoming request.)

      In your mta.yml forward all the request of this service to your springboot service. Your spring boot application will fetch the tenant db credentials from the request and you can connect to that tenant db. (you'll creating db connection for each incoming request).

      For db tables you can use cds or .hdbtable artifacts which will deploy at tenant onboarding time. I have use .hdbtable in my project I personally don't like cds approach.

      There might be a better way to do it but I'm not aware of it, nor I have found any thing that can guide how develop a tenant aware application.

      Let me know if you have any doubts.

      Thanks

      Author's profile photo Anil k
      Anil k

      Thanks for the idea. I do agree , I see most  examples from sap using javascript . I will try it.

      I will also try with Nodejs backend once.

      I saw your other post  on connection issues  with Node js boilerplate code generated by yo-man. What fix you had to make it to work?

      Any code snippets or do you have any github repo url with the fix?

       

      Also,  I was able to query the data from HDI container .

      Not sure, if it is the right way to do it. But I basically went to service instances in Cloud platform -> service manager instance -> under service manager, HDI- shared instance -> credentials

      used runtime user (*_RT)  id and password from hdi-shared credentials

      and Logged in to SQL explorer  with those credentials and was able to see data.    

       

      Thanks

       

      Author's profile photo Prakritidev Verma
      Prakritidev Verma

      I was using a wrapper developed by thomas jung https://github.com/SAP-samples/hana-hdbext-promisfied-example .

      In Nodejs we have to pass db credentials in the middleware of hdbext library. hdbext-promisified is a wrapper on top of that which helps in writing async await code.

      Apparently the middleware function was not working when I passed db credentials so I created my custom middleware which creates the connection for each incoming request.  It was a small change that took a lot of time to figureout.

      There is a small cf utility called as smsi that will also give the credentials. However I was getting "only secured connections are allowed" error.

      I'll try your steps I might be doing something wrong earlier.

      Author's profile photo Anil k
      Anil k

      Thank you.

      I tried with Nodejs backend  from yo-man template and boiler plate code  is working fine. Wondering if there is any good way to handle db operations like with connecting pooling, looks like code now trying to make new connection for every single request. Do you think if it will lead to any issues, especially when processing lot of requests?

      Author's profile photo Prakritidev Verma
      Prakritidev Verma

      I think this will be a bottleneck when processing a lot of request, at this moment I don't have any solution for this.

      That is going to be a whole new project on its own.

      Author's profile photo Anil k
      Anil k

      Thanks. Are you using this yo-man templateas a base in prod, faced any issues?

      I was able to run successfully run this app on CF, but not locally. I put in default-env.json in srv folder , used xsenv.loadEnv() in server.js.

      But i keep getting this error on business studio, if i do npm start. Any idea, if I'm missing anything? sorry,  kind of new to nodejs.

      /home/user/projects/appformt/srv/node_modules/@sap/xsenv/lib/xsservices.js:50
      throw new VError('No service matches %s', key);
      ^
      VError: No service matches uaa
      at Object.getServices (/home/user/projects/appformt/srv/node_modules/@sap/xsenv/lib/xsservices.js:50:15)

      Author's profile photo Prakritidev Verma
      Prakritidev Verma

      try putting the path of your default-env.json in  xsenv.loadEnv("filepath").

      I think is this some sort of bug I am not able to connect with hana on local; machine, I have to deploy it using cli.

      I wanted to ask have you imported a csv in HANA by any chance ?

      Author's profile photo Anil k
      Anil k

      it did not work for me in local even with setting path as you mentioned.

      I haven't tried importing csv in HANA. I will let you ,once I get there .

      are you also using mtx side car in your project, to push DB updates for tenant containers ?

      Author's profile photo Anil k
      Anil k

      Hey Venu,

      Thanks for sharing. I’m trying to implement the same in spring boot. Followed your repo for reference , I’m getting the provider destination information from both Provider and Tenant url . I have created destination with same name in both accounts. Not sure if i’m missing anything?

                  one thing i noticed ,as part of your  Node.js destination service , you are using JwtToken to get the destination. I don't see the option for passing jwt to java cloud sdk "DestinationAccessor " api.