Skip to content

Commit b1763fe

Browse files
authored
feat: add headers for destination consumption (#56)
1 parent 239233a commit b1763fe

File tree

6 files changed

+408
-31
lines changed

6 files changed

+408
-31
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "sap-cloud-sdk"
3-
version = "0.7.1"
3+
version = "0.8.0"
44
description = "SAP Cloud SDK for Python"
55
readme = "README.md"
66
license = "Apache-2.0"

src/sap_cloud_sdk/destination/_models.py

Lines changed: 79 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -405,35 +405,102 @@ def from_dict(cls, obj: Dict[str, Any]) -> "AuthToken":
405405
class ConsumptionOptions:
406406
"""Options for consuming a destination via the v2 runtime API.
407407
408-
This class encapsulates optional parameters for destination consumption.
408+
Each field maps directly to an HTTP request header sent to the Destination Service.
409409
410410
Fields:
411-
fragment_name: Optional fragment name for property merging via X-fragment-name header
412-
tenant: Optional subscriber tenant subdomain for user token exchange
411+
fragment_name: Name of the destination fragment used to override/extend destination
412+
properties (X-fragment-name). In case of overlapping properties, fragment values
413+
take priority.
414+
fragment_optional: When True, if the fragment specified by fragment_name does not
415+
exist the destination is returned without it. When False (default), a missing
416+
fragment causes an error (X-fragment-optional).
417+
tenant: Subdomain of the tenant on behalf of which to fetch an access token
418+
(X-tenant). Required when tokenServiceURLType is Common. Takes precedence over
419+
user_token for tenant determination.
420+
user_token: Encoded user JWT token (RFC 7519) for authentication types that require
421+
user information: OAuth2UserTokenExchange, OAuth2JWTBearer,
422+
OAuth2SAMLBearerAssertion (X-user-token). Takes priority over the Authorization
423+
header for token exchange.
424+
subject_token: Subject token for OAuth2TokenExchange destinations (X-subject-token).
425+
Used as the subject_token parameter in the token exchange request (RFC 8693).
426+
Must be used together with subject_token_type.
427+
subject_token_type: Format of the subject token as defined by the authorization
428+
server (X-subject-token-type), e.g.
429+
"urn:ietf:params:oauth:token-type:access_token". Required with subject_token.
430+
actor_token: Actor token for OAuth2TokenExchange destinations (X-actor-token).
431+
Used as the actor_token parameter in the token exchange request (RFC 8693).
432+
Should be used together with actor_token_type.
433+
actor_token_type: Format of the actor token as defined by the authorization server
434+
(X-actor-token-type), e.g. "urn:ietf:params:oauth:token-type:access_token".
435+
saml_assertion: Client-provided SAML assertion for destinations with authentication
436+
type OAuth2SAMLBearerAssertion and SAMLAssertionProvider=ClientProvided
437+
(X-samlAssertion). If applicable but not provided, token retrieval will fail.
438+
refresh_token: Refresh token for OAuth2RefreshToken destinations (X-refresh-token).
439+
Mandatory for that authentication type. The service uses it to fetch new access
440+
and refresh tokens from the configured tokenServiceURL.
441+
code: Authorization code for OAuth2AuthorizationCode destinations (X-code).
442+
Mandatory for that authentication type. Exchanged for an access token at the
443+
configured tokenServiceURL.
444+
redirect_uri: URL-encoded redirect URI for OAuth2AuthorizationCode destinations
445+
(X-redirect-uri). Required when the same redirect URI was registered during the
446+
authorization code grant; must match the registered value.
447+
code_verifier: PKCE code verifier for OAuth2AuthorizationCode destinations
448+
(X-code-verifier). Required when a code challenge was provided during the
449+
authorization code grant.
450+
chain_name: Name of a predefined destination chain, enabling multiple Destination
451+
Service interactions in a single request (X-chain-name).
452+
chain_vars: Key-value pairs for destination chain variables (X-chain-var-<name>).
453+
Each entry is sent as a separate "X-chain-var-<key>" header. Only applicable
454+
when chain_name is provided.
413455
414456
Example:
415457
```python
416458
from sap_cloud_sdk.destination import create_client, ConsumptionOptions
417459
418460
client = create_client()
419461
420-
# Simple consumption
421-
dest = client.get_destination("my-api")
462+
# Fragment merging
463+
dest = client.get_destination("my-api", options=ConsumptionOptions(fragment_name="prod"))
422464
423-
# With options
424-
options = ConsumptionOptions(fragment_name="production", tenant="tenant-1")
425-
dest = client.get_destination("my-api", options=options)
465+
# User token exchange
466+
opts = ConsumptionOptions(user_token="<jwt>", tenant="tenant-1")
467+
dest = client.get_destination("my-api", options=opts)
426468
427-
# Or inline
428-
dest = client.get_destination(
429-
"my-api",
430-
options=ConsumptionOptions(fragment_name="prod")
469+
# OAuth2TokenExchange
470+
opts = ConsumptionOptions(
471+
subject_token="<token>",
472+
subject_token_type="urn:ietf:params:oauth:token-type:access_token",
431473
)
474+
dest = client.get_destination("my-api", options=opts)
475+
476+
# OAuth2AuthorizationCode
477+
opts = ConsumptionOptions(code="<auth-code>", redirect_uri="https://app/callback")
478+
dest = client.get_destination("my-api", options=opts)
479+
480+
# Destination chain
481+
opts = ConsumptionOptions(
482+
chain_name="my-chain",
483+
chain_vars={"subject_token": "<token>", "subject_token_type": "access_token"},
484+
)
485+
dest = client.get_destination("my-api", options=opts)
432486
```
433487
"""
434488

435489
fragment_name: Optional[str] = None
490+
fragment_optional: Optional[bool] = None
436491
tenant: Optional[str] = None
492+
user_token: Optional[str] = None
493+
subject_token: Optional[str] = None
494+
subject_token_type: Optional[str] = None
495+
actor_token: Optional[str] = None
496+
actor_token_type: Optional[str] = None
497+
saml_assertion: Optional[str] = None
498+
refresh_token: Optional[str] = None
499+
code: Optional[str] = None
500+
redirect_uri: Optional[str] = None
501+
code_verifier: Optional[str] = None
502+
chain_name: Optional[str] = None
503+
chain_vars: Optional[dict] = None
437504

438505

439506
@dataclass

src/sap_cloud_sdk/destination/client.py

Lines changed: 51 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -289,10 +289,13 @@ def get_destination(
289289
290290
Args:
291291
name: Destination name.
292-
level: Optional level hint (subaccount or instance) to optimize lookup. If not provided, the API will search on instance level.
293-
options: Optional ConsumptionOptions for fragment merging and tenant context.
294-
proxy_enabled: Whether to route the request through a transparent proxy (if configured).
295-
If None, uses the client's default proxy_enabled setting.
292+
level: Optional level hint (subaccount or instance) to optimize lookup. If not
293+
provided, the API will search on instance level.
294+
options: Optional ConsumptionOptions controlling request headers sent to the
295+
Destination Service. See ConsumptionOptions for the full list of supported
296+
headers (fragment merging, token exchange, SAML, OAuth2 flows, chains, etc.).
297+
proxy_enabled: Whether to route the request through a transparent proxy (if
298+
configured). If None, uses the client's default proxy_enabled setting.
296299
297300
Returns:
298301
Destination with auth_tokens and certificates populated from v2 API,
@@ -310,23 +313,27 @@ def get_destination(
310313
311314
# Simple consumption
312315
dest = client.get_destination("my-api")
313-
print(dest.url)
314-
print(dest.auth_tokens)
315316
316317
# With level hint
317318
dest = client.get_destination("my-api", level=Level.SERVICE_INSTANCE)
318319
319-
# With options - fragment merging
320-
opts = ConsumptionOptions(fragment_name="production")
321-
dest = client.get_destination("my-api", options=opts)
320+
# Fragment merging
321+
dest = client.get_destination("my-api", options=ConsumptionOptions(fragment_name="prod"))
322322
323-
# With tenant context for user token exchange
324-
opts = ConsumptionOptions(tenant="tenant-subdomain")
325-
dest = client.get_destination("my-api", options=opts)
323+
# Optional fragment (no error if fragment not found)
324+
dest = client.get_destination(
325+
"my-api",
326+
options=ConsumptionOptions(fragment_name="prod", fragment_optional=True),
327+
)
326328
327-
# Both fragment and tenant
328-
opts = ConsumptionOptions(fragment_name="prod", tenant="tenant-1")
329-
dest = client.get_destination("my-api", options=opts)
329+
# Tenant context
330+
dest = client.get_destination("my-api", options=ConsumptionOptions(tenant="tenant-1"))
331+
332+
# User token exchange (OAuth2UserTokenExchange / OAuth2JWTBearer)
333+
dest = client.get_destination(
334+
"my-api",
335+
options=ConsumptionOptions(user_token="<jwt>", tenant="tenant-1"),
336+
)
330337
331338
# With transparent proxy enabled
332339
dest = client.get_destination("my-api", proxy_enabled=True)
@@ -343,8 +350,37 @@ def get_destination(
343350
if options:
344351
if options.fragment_name:
345352
headers["X-fragment-name"] = options.fragment_name
353+
if options.fragment_optional is not None:
354+
headers["X-fragment-optional"] = str(
355+
options.fragment_optional
356+
).lower()
346357
if options.tenant:
347358
headers["X-tenant"] = options.tenant
359+
if options.user_token:
360+
headers["X-user-token"] = options.user_token
361+
if options.subject_token:
362+
headers["X-subject-token"] = options.subject_token
363+
if options.subject_token_type:
364+
headers["X-subject-token-type"] = options.subject_token_type
365+
if options.actor_token:
366+
headers["X-actor-token"] = options.actor_token
367+
if options.actor_token_type:
368+
headers["X-actor-token-type"] = options.actor_token_type
369+
if options.saml_assertion:
370+
headers["X-samlAssertion"] = options.saml_assertion
371+
if options.refresh_token:
372+
headers["X-refresh-token"] = options.refresh_token
373+
if options.code:
374+
headers["X-code"] = options.code
375+
if options.redirect_uri:
376+
headers["X-redirect-uri"] = options.redirect_uri
377+
if options.code_verifier:
378+
headers["X-code-verifier"] = options.code_verifier
379+
if options.chain_name:
380+
headers["X-chain-name"] = options.chain_name
381+
if options.chain_vars:
382+
for var_name, var_value in options.chain_vars.items():
383+
headers[f"X-chain-var-{var_name}"] = var_value
348384

349385
# Build path with optional level hint
350386
if level:

src/sap_cloud_sdk/destination/user-guide.md

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,22 @@ class CertificateClient:
107107

108108
- `Destination(name: str, type: str, url?: str, proxy_type?: str, authentication?: str, description?: str, properties?: dict[str, str], auth_tokens?: list[AuthToken], certificates?: list[Certificate])`
109109
- `auth_tokens` and `certificates` are populated by the v2 consumption API
110-
- `ConsumptionOptions(fragment_name?: str, tenant?: str)` - Options for v2 destination consumption
110+
- `ConsumptionOptions` - Options for v2 destination consumption, controls HTTP headers sent to the Destination Service:
111+
- `fragment_name?: str` - Fragment to merge into the destination (`X-fragment-name`)
112+
- `fragment_optional?: bool` - If `True`, a missing fragment does not cause an error (`X-fragment-optional`)
113+
- `tenant?: str` - Tenant subdomain for token retrieval (`X-tenant`)
114+
- `user_token?: str` - User JWT for OAuth2UserTokenExchange / OAuth2JWTBearer / OAuth2SAMLBearerAssertion (`X-user-token`)
115+
- `subject_token?: str` - Subject token for OAuth2TokenExchange (`X-subject-token`)
116+
- `subject_token_type?: str` - Format of the subject token (`X-subject-token-type`), e.g. `"urn:ietf:params:oauth:token-type:access_token"`
117+
- `actor_token?: str` - Actor token for OAuth2TokenExchange (`X-actor-token`)
118+
- `actor_token_type?: str` - Format of the actor token (`X-actor-token-type`)
119+
- `saml_assertion?: str` - Client-provided SAML assertion for OAuth2SAMLBearerAssertion with `SAMLAssertionProvider=ClientProvided` (`X-samlAssertion`)
120+
- `refresh_token?: str` - Refresh token for OAuth2RefreshToken destinations (`X-refresh-token`)
121+
- `code?: str` - Authorization code for OAuth2AuthorizationCode destinations (`X-code`)
122+
- `redirect_uri?: str` - Redirect URI for OAuth2AuthorizationCode destinations (`X-redirect-uri`)
123+
- `code_verifier?: str` - PKCE code verifier for OAuth2AuthorizationCode destinations (`X-code-verifier`)
124+
- `chain_name?: str` - Name of a predefined destination chain (`X-chain-name`)
125+
- `chain_vars?: dict[str, str]` - Variables for the destination chain; each entry is sent as `X-chain-var-<key>`
111126
- `AuthToken(type: str, value: str, http_header: dict, expires_in?: str, error?: str, scope?: str, refresh_token?: str)` - Authentication token from v2 API
112127
- `Fragment(name: str, properties: dict[str, str])`
113128
- `Certificate(name: str, content: str, type: str)`
@@ -247,6 +262,46 @@ dest = client.get_destination("my-api")
247262
# Example 9: Combine level with options
248263
options = ConsumptionOptions(fragment_name="production", tenant="tenant-1")
249264
dest = client.get_destination("my-api", level=Level.SUB_ACCOUNT, options=options)
265+
266+
# Example 10: Optional fragment (no error if fragment does not exist)
267+
options = ConsumptionOptions(fragment_name="maybe-exists", fragment_optional=True)
268+
dest = client.get_destination("my-api", options=options)
269+
270+
# Example 11: User token exchange (OAuth2UserTokenExchange / OAuth2JWTBearer)
271+
options = ConsumptionOptions(user_token="<encoded-jwt>", tenant="tenant-1")
272+
dest = client.get_destination("my-api", options=options)
273+
274+
# Example 12: OAuth2TokenExchange with subject and actor tokens
275+
options = ConsumptionOptions(
276+
subject_token="<subject-token>",
277+
subject_token_type="urn:ietf:params:oauth:token-type:access_token",
278+
actor_token="<actor-token>",
279+
actor_token_type="urn:ietf:params:oauth:token-type:access_token",
280+
)
281+
dest = client.get_destination("my-api", options=options)
282+
283+
# Example 13: Client-provided SAML assertion (SAMLAssertionProvider=ClientProvided)
284+
options = ConsumptionOptions(saml_assertion="<base64-encoded-saml>")
285+
dest = client.get_destination("my-api", options=options)
286+
287+
# Example 14: OAuth2RefreshToken
288+
options = ConsumptionOptions(refresh_token="<refresh-token>")
289+
dest = client.get_destination("my-api", options=options)
290+
291+
# Example 15: OAuth2AuthorizationCode with PKCE
292+
options = ConsumptionOptions(
293+
code="<authorization-code>",
294+
redirect_uri="https://myapp/callback",
295+
code_verifier="<pkce-code-verifier>",
296+
)
297+
dest = client.get_destination("my-api", options=options)
298+
299+
# Example 16: Destination chain with chain variables
300+
options = ConsumptionOptions(
301+
chain_name="my-predefined-chain",
302+
chain_vars={"subject_token": "<token>", "subject_token_type": "access_token"},
303+
)
304+
dest = client.get_destination("my-api", options=options)
250305
```
251306

252307
### Return Type

0 commit comments

Comments
 (0)