cancel
Showing results for 
Search instead for 
Did you mean: 

Destination not found in my multitenant CAP app

marcmaurí
Participant
0 Kudos

Hello,

In my multitenant CAP app, I got an error when accessing an S/4HANA Cloud via Cloud SDK from my consumer subaccount. It works fine when accessing from my provider. My scenario is the following:

Perhaps it may seem a little weird the own s/4hana cloud system vs the provider subaccount, it is here just for test purposes.

When accessing https://provider-myapp-aa.cfapps.eu10.hana.ondemand.com/mobile/getData(cardTemplate=''), the own s4hc is reached successfully and data is retrieved but doing so from the consumer, https://consumer-myapp-aa.cfapps.eu10.hana.ondemand.com/mobile/getData(cardTemplate=''), I got the following error in the provider app:

2020-04-15T09:46:27.639+0000 [APP/PROC/WEB/0] OUT error calling api Error: Could not find a destination with name "s4n103C1"! Unable to execute request.
2020-04-15T09:46:27.639+0000 [APP/PROC/WEB/0] OUT     at Object.errorWithCause (/home/vcap/app/node_modules/@sap/cloud-sdk-core/node_modules/@sap/cloud-sdk-util/dist/error.js:14:20)
2020-04-15T09:46:27.639+0000 [APP/PROC/WEB/0] OUT     at /home/vcap/app/node_modules/@sap/cloud-sdk-core/dist/request-builder/request-builder-base.js:125:78
2020-04-15T09:46:27.639+0000 [APP/PROC/WEB/0] OUT     at process._tickCallback (internal/process/next_tick.js:68:7)
2020-04-15T09:46:27.639+0000 [APP/PROC/WEB/0] OUT Caused by:
2020-04-15T09:46:27.639+0000 [APP/PROC/WEB/0] OUT Error: FetchTokenError: Client credentials Grant failed! Request failed with status code 401
2020-04-15T09:46:27.639+0000 [APP/PROC/WEB/0] OUT     at Object.errorWithCause (/home/vcap/app/node_modules/@sap/cloud-sdk-core/node_modules/@sap/cloud-sdk-util/dist/error.js:14:20)
2020-04-15T09:46:27.639+0000 [APP/PROC/WEB/0] OUT     at accessTokenError (/home/vcap/app/node_modules/@sap/cloud-sdk-core/dist/scp-cf/xsuaa-service.js:166:29)
2020-04-15T09:46:27.639+0000 [APP/PROC/WEB/0] OUT     at /home/vcap/app/node_modules/@sap/cloud-sdk-core/dist/scp-cf/xsuaa-service.js:43:57
2020-04-15T09:46:27.639+0000 [APP/PROC/WEB/0] OUT     at process._tickCallback (internal/process/next_tick.js:68:7)
2020-04-15T09:46:27.639+0000 [APP/PROC/WEB/0] OUT Caused by:
2020-04-15T09:46:27.639+0000 [APP/PROC/WEB/0] OUT Error: Request failed with status code 401
2020-04-15T09:46:27.639+0000 [APP/PROC/WEB/0] OUT     at createError (/home/vcap/app/node_modules/axios/lib/core/createError.js:16:15)
2020-04-15T09:46:27.639+0000 [APP/PROC/WEB/0] OUT     at settle (/home/vcap/app/node_modules/axios/lib/core/settle.js:17:12)
2020-04-15T09:46:27.639+0000 [APP/PROC/WEB/0] OUT     at IncomingMessage.handleStreamEnd (/home/vcap/app/node_modules/axios/lib/adapters/http.js:237:11)
2020-04-15T09:46:27.639+0000 [APP/PROC/WEB/0] OUT     at IncomingMessage.emit (events.js:203:15)
2020-04-15T09:46:27.639+0000 [APP/PROC/WEB/0] OUT     at endReadableNT (_stream_readable.js:1145:12)
2020-04-15T09:46:27.639+0000 [APP/PROC/WEB/0] OUT     at process._tickCallback (internal/process/next_tick.js:63:19)

The destination s4n103C1 is created in the provider subaccount and I have made sure that the fields are well filled out. I tried creating the destination in the consumer subaccount and I got the same error.

Let me here remark that accessing other CAP services from the consumer subaccount (i.e https://consumer-myapp-aa.cfapps.eu10.hana.ondemand.com/mobile/books) works fine, the error occurs when accessing backend destinations via Cloud SDK.

The source code that raises the error is:

    var cloud_sdk_core_1 = require("@sap/cloud-sdk-core");
    const { serializeEntity } = require("@sap/cloud-sdk-core");
    
    // building the select fields array
    ....
     
    var jwt = retrieveJwt(req._.req)

    // call api
    const salesOrder = await SalesOrder
      .requestBuilder()
      .getAll()
      .select(...selectFields)        
      .filter(filters)
      .top(top)
      .execute({destinationName: destination, jwt: jwt}) 
      .then(result => result.map(so => serializeEntity(so, SalesOrder)))
      .catch(reason => {
          console.log('error calling api ', reason)
      })

This is my mta.yaml file:

_schema-version: 2.0.0
ID: myapp
version: 1.0.0
modules:
  - name: myapp-db
    type: hdb
    path: db
    parameters:
      memory: 256M
      disk-quota: 256M
    requires:
      - name: myapp-db-hdi-container
  - name: myapp-srv
    type: nodejs
    path: srv
    parameters:
      memory: 512M
      disk-quota: 512M
    provides:
      - name: srv_api
        properties:
          url: ${default-url}
    requires:
      - name: myapp-db-hdi-container
      - name: myapp-uaa    
      - name: myapp_destination
      - name: myapp_connectivity   
    properties:
      SAP_JWT_TRUST_ACL:
       - clientid: "*"
         identityzone: "*"     
      DEBUG : "false"     
  - name: myapp-ui
    type: nodejs
    path: app
    parameters:
      memory: 256M
      disk-quota: 256M
    requires:
      - name: myapp-uaa
      - name: srv_api
        group: destinations
        properties:
          forwardAuthToken: true
          strictSSL: true
          name: srv_api
          url: ~{url}
    properties:
      SAP_JWT_TRUST_ACL:
       - clientid: "*"
         identityzone: "*"  
      TENANT_HOST_PATTERN: "^(.*)-myapp-ui.cfapps.eu10.hana.ondemand.com"            
resources:
  - name: myapp-db-hdi-container
    type: com.sap.xs.hdi-container
    properties:
      hdi-container-name: ${service-name}
  - name: myapp-uaa
    type: org.cloudfoundry.managed-service
    parameters:
      service-plan: application
      service: xsuaa
      path: ./xs-security.json
      shared: true
      config:
        xsappname: myapp-${space}
  - name: myapp_destination
    type: org.cloudfoundry.managed-service
    parameters:
      service-plan: lite
      service: destination
  - name: myapp_connectivity
    type: org.cloudfoundry.managed-service
    parameters:
      service-plan: lite
      service: connectivity      

Any clues with this issue?

Thanks in advance.

Best regards,

Marc

gregorw
Active Contributor

Looking forward to see someone from SAP jumping in here to show us a solution.

Marc please note that the correct spelling of "S4/HANA" is "S/4HANA"

dhem
Active Participant
0 Kudos

Hi Marc, are you sure that a) the subscription is setup correctly and b) the JWT is parsed correctly in your code? The error message indicates that your app is unauthorized to fetch a token on behalf of the caller (i.e. the subscriber tenant).

marcmaurí
Participant
0 Kudos

Hi dhem,

thanks for answering. I've been checking and doing some stuff on my project but I didn't solve it.

Regarding b, I think my JWT is parsed correctly. Here a comparison between the JWT sent to CloudSDK when calling from the consumer and from the provider:

.execute({destinationName:destination, jwt: jwt}) 

As shown in "origin" attribute, there are different IAS services, but roles and scopes seems to be correct. SAP_JWT_TRUST_ACL property is set.

As for the subscription settings, I summarize what I did following this blog.

- In xs-security.json, I set tenant-mode to shared.
- In xs-security.json, I added the callback scope:

{
  "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)"
  ]
}


- In my express server file server.js, I added the subscription callbacks:

// Callback - 1: On subscribing, this method constructs and returns the app url for the subscriber.
  await app.put('/callback/v1.0/tenants/*', function (req, res) {
     var consumerSubdomain = req.body.subscribedSubdomain;
     var tenantAppURL = "https:\/\/" + consumerSubdomain + "-s4XXXXXXXXXX-ui." + "cfapps.eu10.hana.ondemand.com";
     res.status(200).send(tenantAppURL);
  });
   
  // Callback - 2: On unsubscribing, this method deletes the subscription.
  await app.delete('/callback/v1.0/tenants/*', function (req, res) {
      res.status(200);
  });

I think this part works correctly because I can successfully subscribe from the consumer.


- In mta.yaml, I added the TENANT_HOST_PATTERN property.

- Then I created a new saas-registry service, I binded it to the app and ran the re-stage of the app.

- From the consumer, I subscribed to the provider and it is now Subscribed:

- I assigned the role templates to the consumer user.

- I created routes for the consumer and provider to the app.

Thanks for you time.

Best regards,

Marc

dhem
Active Participant
0 Kudos

Hi Marc,

thanks for providing such exhaustive details! We do have an idea of where the error comes from and are currently trying to validate it. Do you by any chance remember when you created the subaccount that acts as subscriber in this scenario? We will let you know as soon as we validated our hypothesis successfully.

Best regards,
Dennis

dhem
Active Participant
0 Kudos

Also can you check whether there's some error like "invalid signature" or "JWT invalid" in your logs?

marcmaurí
Participant
0 Kudos

Hi Dennis,

The subscriber subaccount was created between April 2-7.

Regarding your second question, there are no messages like "invalid signature" or "invalid JWT" in my application logs. The error message stack is the one posted in my question. If there is some way to activate an enhanced log for the xsuaa service, please let me know.

Best regards,

Marc

dhem
Active Participant
0 Kudos
marcmaurí
Participant
0 Kudos

Hi dhem,
No, I didn't. While I was testing my getDependencies method (implementation has not been as easy as I expected because the documentation is quite limited), I received sperstorfer's answer.
My deployment is working fine and now my multitenant app is reading data from the customer's s/4 hana cloud backend successfully.

Anyway, I'm going to check the solution proposed by Simon.

Thank you very much for your support!

Regards,

Marc

former_member194549
Contributor

The getDependencies callback also took me two days.

I would also suggest to improve the documentation, what the purpose of the callback is (I didn't know before that it is essential for the destinations or the portal service) and what has to be returned.

And it is also important to mention that the AppRouter has this subscription middleware, which can be used to handle the getDependencies or onSubscription callback.

It is also worth mentioning that requests that go to the URIs stored in the SaaS Registry Service for getDependencies and onSubscription are not forwarded by the AppRouter.

Regards
Simon

Accepted Solutions (1)

Accepted Solutions (1)

former_member194549
Contributor
0 Kudos

Hi Marc

as dhem already described, did you implement the getDependencies endpoint?

Here we had problems in our solution when we had not implemented this endpoint.

You can create an AppRouter module (@sap/approuter) and execute the getDependencies callback against this module. The AppRouter includes a middleware (lib/middleware/subscription-middleware.js) which can answer these requests.
For the AppRouter to answer the request correctly, you must bind the service instance of the destination service (maybe also the service instance of the connectivity service, but I'm not sure if this is necessary) to the AppRouter. The AppRouter reads the bound services and uses them to create the payload that is returned.

When the getDependencies callback is executed against the AppRouter, it returns an array containing information about the bound service instance of the destination service (and the connectivity service instance if it is bound).

Following a screenshot of the response of our getDependencies Callback handled by a AppRouter module.

Regards
Simon

marcmaurí
Participant
0 Kudos

Hi Simon,

if I understood correctly, only with an Approuter module (I already have it) and without coding any additional handler to process the request, I can execute the onboarding and getDependencies callbacks against this module. Did I get it right?

But, how did you configure the route in xs-app.json? What do you set in destination/localDir/service parameters for the route? One of these is mandatory.

        {
            "source": "^/callback/(.*)$",
            "target": "/callback/$1",
            "authenticationType": "xsuaa",
            "destination": ...
        }

I have not found any documentation on how to implement it, just some additional mention that it is possible to do so.

Thanks in advance.

Regards,

Marc

former_member194549
Contributor
0 Kudos

Hi Marc,

you got that right and you don't have to define a route.

All you have to do is create a service instance of the saas-registry service, set the URIs for onSubscription and getDependencies (in many documentation you will find the paths callback/v1.0/tenants/{tenantId} and callback/v1.0/dependencies here, but they can be different) and bind the service instance to your router.

The XSUAA service must be configured correctly and bound to the AppRouter (tenant-mode, grant-as-authority-to-apps, Scope $XSAPPNAME.Callback (Link)) and the AppRouter also needs the TENANT_HOST_PATTERN (Link).

You do not need to configure any routes for the subscription.

Note
The AppRouter does not forward the requests where the path matches the path of the URIs in the saas registry.

Take a look at node_modules/@sap/approuter/lib/middleware/subscription-middleware.js of your AppRouter module. There you will find the middleware.

Regards
Simon

marcmaurí
Participant

Hi Simon,
thanks for sharing such exhaustive details. I guess I forgot to recreate some service in my first try because the configuration was as you explain. Now it works like a charm.

So, in summary, both approaches works fine, but this one based on Approuter doesn't need any coding and is easier to implement.
Thanks for your support sperstorfer and dhem!
Best regards,
Marc

Answers (0)