Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Not able to call getDestinationFromDestinationService in multitenant CAP scenario #3067

Open
ybbus opened this issue Nov 17, 2022 · 11 comments
Labels
bug Something isn't working CAP Issues related to CAP

Comments

@ybbus
Copy link

ybbus commented Nov 17, 2022

I am not able to call serviceB from serviceA (both in a provider account) using CAP with multitenancy enabled.

Scenario:
Subscriber Account A -- calls --> ProviderServiceA -- calls (using destination) --> ProviderServiceB.

Internally getDestinationFromDestinationService gets called with the option "alwaysProvider".

The problem here seems to be, that even when "alwaysProvider" is set, the method getDestinationFromDestinationService will still try to fetch a token for the subscriber, using the following information:

  • auth endpoint: subscriber auth endpoint (fetched from user token coming from subscriber)
  • credentials of destination-service: fetched from provider destination service binding

So it tries to authenticate using the provider destination credentials against the subscriber auth endpoint. This of course does not work and the method fails with an error.

I would expect, if "alwaysProvider" is set, that it does not even try to get a subscriber token first, since this will always fail.

Am I missing something here, or is this maybe a bug?

@ybbus ybbus added the question Further information is requested label Nov 17, 2022
@fwilhe fwilhe added the CAP Issues related to CAP label Nov 17, 2022
@fwilhe
Copy link
Contributor

fwilhe commented Nov 17, 2022

Hi @ybbus

Thanks for raising this issue.

I currently can't tell you if this is supposed to work, we have an issue in our backlog to clarify this.

We'll come back to you.

Best,
Florian

@fwilhe
Copy link
Contributor

fwilhe commented Nov 18, 2022

Hi @ybbus

after some more research, I have a few follow up questions to better understand the issue:

It looks like the error message thrown by the SDK is truncated. This might be done by CAP. Can you give us the full error message?

This might be available when you can create a minimal example SDK application (without CAP) where you replace ProviderServiceA with the minimal example app which then does the get Destination call.

If you can reproduce your issue with a minimal sample this would help us a lot.

One possible error cause might be a missing destination service binding for your provider account. Can you double check that binding is in place?

Best,
Florian

@ybbus
Copy link
Author

ybbus commented Nov 18, 2022

Hi @fwilhe,

the destination service is bound to the application and it is also correctly recognized (shown in the logs).

As far as I can see when debugging:

This method DestinationFromServiceRetriever.getSubscriberToken(options); gets called even so alwaysProvider is set.

The problem then is that this call fails because of the following reason:

  • it correctly retrieves the (provider) destination service credentials
  • it uses these credentials to fetch a token from the subscriber
    --> this of course fails with 401 what is the internal error message and stops the whole getDestinationFromDestinationService call.

image

Here you can see the request that fails:
A token request to the subscriber account auth endpoint using the credentials of the provider destination service.

I don't have the exact error message here, but it was just something like 401 of the request from above.

@FrankEssenberger
Copy link
Contributor

Hi @ybbus,

thanks for the detailed debugging. Let me also give my 5 cents :-).

The name of the function getSubscriberToken is not optimal because it has to cover multiple cases. The JWT determines:

  • The tenant you want to fetch for
  • The user (if property is there)

If you see in the implementation it will do nothing if no jwt (or iss which is a subset) is passed. If you pass a jwt it is considered. We called this userSpecificToken also in the past, but this is not always correct because the JWT is not enforced to have a user property. Also the JWT could be from the provider account and containing the user in this case we would need it even if alwaysProvideris set. So in the end we sticked to subscriberToken because in most cases this is correct when users pass a JWT along.

The problem now is, that a JWT for the subscriber account is passed - otherwise the method would do nothing. This indicates that you want to fetch a destination on behalf of the subscriber account (You set always provider but this is considered later for the actual which accounts are called). This is not allowed unless you specify the destination service and xsuaa as dependency of your service via the endpoint. If you add this you should not get the 401 anymore. However, this would involve an unnecessary call to get the service token.

Alternatively you could try not to pass a subscriber JWT to the call. Since you are using CAP you are not in full control of the arguments, but perhaps you can try to pass {jwt: undefined} as option. This will perhaps overwrite jwt and make it undefined. In this case you are in the alwaysProvider automatically because the JWT is determining the tenant and provider is the fallback.

I hope this helps.

Best
Frank

@ybbus
Copy link
Author

ybbus commented Nov 25, 2022

@FrankEssenberger

Thanks for the explanation. The problem is that I am using standard CAP here. And as far as I am not missing anything in my configuration, I cannot see how to change the behavior.

All I do is adding an external service in the package.json:

      "MyService": {
        "kind": "odata",
        "model": "srv/external/MyService",
        "credentials": {
          "destination": "my-service",
          "path": "/api/v1",
          "forwardAuthToken": true
        },
         "destinationOptions": {
          "selectionStrategy": "alwaysProvider"
        }
      }

and then try to call it from the application:

const myService= await cds.connect.to('MyService')
const resp = await myService.send('myAction', {...})

I followed the documentation here: https://cap.cloud.sap/docs/guides/using-services?q=destination#use-destinations-with-nodejs

So is this some wrong behavior in CAP?

@FrankEssenberger
Copy link
Contributor

No it is not wrong behavior in CAP - simply that you have no control over the api. We assume that account (identified by a JWT) is allowed to call the destination service. Otherwise do not pass it. Now CAP passes it automatically which is right in general but not in your case. Anywho, did you try to add:

 "destinationOptions": {
  "jwt": undefined //could also try empty string
}

to the config? Perhaps CAP just spreads the options: {...default,...userOptions} in a generic way. Then this would overwrite the subscriber JWT which should not be passed if you want provider. Also you could add the destination service as dependency.

Besides that I think about wrapping the toke in some object with state (a class or so) so that we can use some lazy init. This would make the code more efficient and also circumvent your problem because you will not use the subscriber service token and JWt.

Best
Frank

@ybbus
Copy link
Author

ybbus commented Nov 25, 2022

Unfortunately this did not work. It seems that jwt is explicitly set in the options:

destination = { destinationName, ...(resolveDestinationOptions(destinationOptions, jwt) || {}) }

What do you mean with setting the destination service as dependency?
Do you mean as service-binding? Because the application already is bound to the destination-service.

@FrankEssenberger
Copy link
Contributor

No I mean via the dependencies endpoint. The application is allowed to call a service via the service binding. So XSUAA knows: I can give you a token. But the subscriber account does not have a application where you can bind something. Hence, you implement a getDependencies endpoint in your application listing all services the application will use on behalf of the subscriber account. The moment you create the subscription, BTP makes a call behind the scenes to this getDependencies endpoint and say: XSUAA you can alos give this tenant a token.

However, as I understood your case you do not have destinations in the subscriber accounts and for you it is fine to make the call as the provider account, which is allowed via the binding. The problem is that CAP sets the subscriber JWT which makes the SDK believe you want to make a call as the subscriber. I will create a BLI to make our implementation more efficient avoiding the problem you have.

Unfortunately my first idea of a workaround did not work. I have a second idea which should work. We offer a registerDestination method which stores a destination in memory. If something is found here no call to the destination service is made. So the idea would be to get the destination and register it:

 const jwt =  retrieveJwt(request) //get the JWT from the request (SDK helper method)

  const dest = await getDestinationFromDestinationService({destinationName: 'YourName'}) //no JWT given so provides
  registerDestination(dest as DestinationWithName, {jwt}) //register it with the JWT so CAP finds it
  
  //Make your normal CAP call

The register method considers the lifetime of a destination given by the authentication token. So you could optimize this to check, do I get something from the cache and only make the getDestinationFromDestinationService but for a first test the above snippet should be good enough :-).

@ybbus
Copy link
Author

ybbus commented Nov 28, 2022

@FrankEssenberger

I guess for now it is ok for us to use the application provided destination (in the package.json) and wait for some improvement here, to be able to get destinations in the context of a tenant when alwaysProvider is set.

Thanks for your support and the explanation.

@FrankEssenberger
Copy link
Contributor

Ok, then I would close the issue for now since we have a BLI to keep track on this one?

@jjtang1985
Copy link
Contributor

I'll keep this ticket open, so for others it's clear, this is an known issue.

@jjtang1985 jjtang1985 reopened this Feb 21, 2023
@jjtang1985 jjtang1985 added bug Something isn't working and removed question Further information is requested labels Feb 21, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working CAP Issues related to CAP
Projects
None yet
Development

No branches or pull requests

4 participants