SAP-samples / btp-cap-multitenant-saas

Sample project that demonstrates how to setup a multitenant application for a Software-as-a-Service scenario, leveraging the Kyma and Cloud Foundry Runtimes of the SAP Business Technology Platform. Developers learn how to implement their own CAP (mtxs) based SaaS app including an SaaS API and integration with various essential SAP BTP service of...
Apache License 2.0
76 stars 41 forks source link

How to run background jobs which require Principal Propagation to SAP Backend? #58

Open codeyogi911 opened 1 week ago

codeyogi911 commented 1 week ago

I have a use case where the background job needs to access the backend system via destinations in a multi tenant app. How can i achieve this? Basically i want to run a background job with a technical user

alperdedeoglu commented 1 week ago

Hi @codeyogi911,

cds.spawn should help you with your use case as described here.

cds.spawn ({ tenant:'t0', every: 1000 /* ms */ }, async (tx) => {
  const mails = await SELECT.from('Outbox')
  await MailServer.send(mails)
  await DELETE.from('Outbox').where (`ID in ${mails.map(m => m.ID)}`)
})
codeyogi911 commented 1 week ago

@alperdedeoglu I am aware about how to use cds.spawn, what I want to find out is how to run with a jwt in the background?

alperdedeoglu commented 1 week ago

@codeyogi911 What kind of SAP Backend are we talking about? Can you explain the exact use case you have? If that is S/4HANA Private Cloud or on-premise, you need to get the token for destination service on behalf of your tenant. You can do this each time when your job gets executed. I am having hard times understanding what is the actual problem you are having.

codeyogi911 commented 1 week ago

I have an SAP Onprem system connected to our multi-tenant app. We are running in the background a Auto Sales Order job. When connecting to the backend API it fails because there is no jwt. Is there a way to configure and use technical user for each tenant? What i did currently is get the jwt using password_grant (which is not correct) and created a cds.user using this token and @sap/xssec. I am then using this user in the cds.spawn. I feel like its a work around and there should be a way to configure and use background users.

alperdedeoglu commented 1 week ago

Where exactly do you get your JWT from? Do you mean the XSUAA? By Backend API do you mean CAP API or S/4 API?

codeyogi911 commented 1 week ago

Right now as a workaround i get the jwt from the bound XSUAA using a password_grant. Backend API means S4 API using Destinations and Cloud Connector.

alperdedeoglu commented 1 week ago

Right now as a workaround i get the jwt from the bound XSUAA using a password_grant. Backend API means S4 API using Destinations and Cloud Connector.

And are you using Cloud SDK or something to connect to the backend? Or have you defined a destination? How do you call the on-premise backend? Which library do you use?

codeyogi911 commented 1 week ago

I have my function to create sales order defined like:

async function createSalesOrder(orderData) {
  LOGGER.info("Creating sales order: ", cds.context.user)
  const salesSrv = await cds.connect.to("API_SALES_ORDER_SRV", {
    destinationOptions: {
      jwt: cds.context.user.tokenInfo.getTokenValue(),
    }
  })
  const result = await salesSrv.create("A_SalesOrder").entries(orderData)
  LOGGER.info("Sales order created", result.SalesOrder)
  return result
}

earlier i had it defined like:

async function createSalesOrder(orderData) {
  let salesSrv
  if (cds.context.isPrivilegedSpawn) {
    const subDomain = await getTenantSubdomain(cds.context.tenant)
    LOGGER.info("Subdomain for tenant", subDomain)
    LOGGER.info("Creating sales order for tenant using basic authentication", cds.context.tenant)
    salesSrv = await cds.connect.to("API_SALES_ORDER_SRV", {
      credentials: {
        destination: "onpremise-basic",
        path: "/sap/opu/odata/sap/API_SALES_ORDER_SRV",
      },
      destinationOptions: {
        selectionStrategy: "alwaysSubscriber",
        iss: `https://${subDomain}.authentication.eu10.hana.ondemand.com/oauth/token`,
        useCache: true,
      },
    })
  } else {
    salesSrv = await cds.connect.to("API_SALES_ORDER_SRV")
  }
  const result = await salesSrv.create("A_SalesOrder").entries(orderData)
  LOGGER.info("Sales order created", result.SalesOrder)
  return result
}

so this stopped working after the upgrading to CDS8, it somehow now gives me 401 when using onpremise-basic and even if i try in the foreground then also it gives me 401, even though it should use the onpremise in foreground. Somehow the destination is not getting cached even after setting

"API_SALES_ORDER_SRV": {
        "kind": "odata-v2",
        "model": "srv/external/API_SALES_ORDER_SRV",
        "csrf": true,
        "csrfInBatch": true,
        "[production]": {
          "credentials": {
            "destination": "onpremise",
            "path": "/sap/opu/odata/sap/API_SALES_ORDER_SRV"
          },
          "destinationOptions": {
            "selectionStrategy": "alwaysSubscriber",
            "useCache": false
          }
        }
      },
alperdedeoglu commented 2 days ago

Ok got it, can you just give the jwt with retrieving a token via client_credentials and let me know how it goes?