Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "sap-cloud-sdk"
version = "0.7.1"
version = "0.8.0"
description = "SAP Cloud SDK for Python"
readme = "README.md"
license = "Apache-2.0"
Expand Down
91 changes: 79 additions & 12 deletions src/sap_cloud_sdk/destination/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,35 +405,102 @@ def from_dict(cls, obj: Dict[str, Any]) -> "AuthToken":
class ConsumptionOptions:
"""Options for consuming a destination via the v2 runtime API.

This class encapsulates optional parameters for destination consumption.
Each field maps directly to an HTTP request header sent to the Destination Service.

Fields:
fragment_name: Optional fragment name for property merging via X-fragment-name header
tenant: Optional subscriber tenant subdomain for user token exchange
fragment_name: Name of the destination fragment used to override/extend destination
properties (X-fragment-name). In case of overlapping properties, fragment values
take priority.
fragment_optional: When True, if the fragment specified by fragment_name does not
exist the destination is returned without it. When False (default), a missing
fragment causes an error (X-fragment-optional).
tenant: Subdomain of the tenant on behalf of which to fetch an access token
(X-tenant). Required when tokenServiceURLType is Common. Takes precedence over
user_token for tenant determination.
user_token: Encoded user JWT token (RFC 7519) for authentication types that require
user information: OAuth2UserTokenExchange, OAuth2JWTBearer,
OAuth2SAMLBearerAssertion (X-user-token). Takes priority over the Authorization
header for token exchange.
subject_token: Subject token for OAuth2TokenExchange destinations (X-subject-token).
Used as the subject_token parameter in the token exchange request (RFC 8693).
Must be used together with subject_token_type.
subject_token_type: Format of the subject token as defined by the authorization
server (X-subject-token-type), e.g.
"urn:ietf:params:oauth:token-type:access_token". Required with subject_token.
actor_token: Actor token for OAuth2TokenExchange destinations (X-actor-token).
Used as the actor_token parameter in the token exchange request (RFC 8693).
Should be used together with actor_token_type.
actor_token_type: Format of the actor token as defined by the authorization server
(X-actor-token-type), e.g. "urn:ietf:params:oauth:token-type:access_token".
saml_assertion: Client-provided SAML assertion for destinations with authentication
type OAuth2SAMLBearerAssertion and SAMLAssertionProvider=ClientProvided
(X-samlAssertion). If applicable but not provided, token retrieval will fail.
refresh_token: Refresh token for OAuth2RefreshToken destinations (X-refresh-token).
Mandatory for that authentication type. The service uses it to fetch new access
and refresh tokens from the configured tokenServiceURL.
code: Authorization code for OAuth2AuthorizationCode destinations (X-code).
Mandatory for that authentication type. Exchanged for an access token at the
configured tokenServiceURL.
redirect_uri: URL-encoded redirect URI for OAuth2AuthorizationCode destinations
(X-redirect-uri). Required when the same redirect URI was registered during the
authorization code grant; must match the registered value.
code_verifier: PKCE code verifier for OAuth2AuthorizationCode destinations
(X-code-verifier). Required when a code challenge was provided during the
authorization code grant.
chain_name: Name of a predefined destination chain, enabling multiple Destination
Service interactions in a single request (X-chain-name).
chain_vars: Key-value pairs for destination chain variables (X-chain-var-<name>).
Each entry is sent as a separate "X-chain-var-<key>" header. Only applicable
when chain_name is provided.

Example:
```python
from sap_cloud_sdk.destination import create_client, ConsumptionOptions

client = create_client()

# Simple consumption
dest = client.get_destination("my-api")
# Fragment merging
dest = client.get_destination("my-api", options=ConsumptionOptions(fragment_name="prod"))

# With options
options = ConsumptionOptions(fragment_name="production", tenant="tenant-1")
dest = client.get_destination("my-api", options=options)
# User token exchange
opts = ConsumptionOptions(user_token="<jwt>", tenant="tenant-1")
dest = client.get_destination("my-api", options=opts)

# Or inline
dest = client.get_destination(
"my-api",
options=ConsumptionOptions(fragment_name="prod")
# OAuth2TokenExchange
opts = ConsumptionOptions(
subject_token="<token>",
subject_token_type="urn:ietf:params:oauth:token-type:access_token",
)
dest = client.get_destination("my-api", options=opts)

# OAuth2AuthorizationCode
opts = ConsumptionOptions(code="<auth-code>", redirect_uri="https://app/callback")
dest = client.get_destination("my-api", options=opts)

# Destination chain
opts = ConsumptionOptions(
chain_name="my-chain",
chain_vars={"subject_token": "<token>", "subject_token_type": "access_token"},
)
dest = client.get_destination("my-api", options=opts)
```
"""

fragment_name: Optional[str] = None
fragment_optional: Optional[bool] = None
tenant: Optional[str] = None
user_token: Optional[str] = None
subject_token: Optional[str] = None
subject_token_type: Optional[str] = None
actor_token: Optional[str] = None
actor_token_type: Optional[str] = None
saml_assertion: Optional[str] = None
refresh_token: Optional[str] = None
code: Optional[str] = None
redirect_uri: Optional[str] = None
code_verifier: Optional[str] = None
chain_name: Optional[str] = None
chain_vars: Optional[dict] = None


@dataclass
Expand Down
66 changes: 51 additions & 15 deletions src/sap_cloud_sdk/destination/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,10 +289,13 @@ def get_destination(

Args:
name: Destination name.
level: Optional level hint (subaccount or instance) to optimize lookup. If not provided, the API will search on instance level.
options: Optional ConsumptionOptions for fragment merging and tenant context.
proxy_enabled: Whether to route the request through a transparent proxy (if configured).
If None, uses the client's default proxy_enabled setting.
level: Optional level hint (subaccount or instance) to optimize lookup. If not
provided, the API will search on instance level.
options: Optional ConsumptionOptions controlling request headers sent to the
Destination Service. See ConsumptionOptions for the full list of supported
headers (fragment merging, token exchange, SAML, OAuth2 flows, chains, etc.).
proxy_enabled: Whether to route the request through a transparent proxy (if
configured). If None, uses the client's default proxy_enabled setting.

Returns:
Destination with auth_tokens and certificates populated from v2 API,
Expand All @@ -310,23 +313,27 @@ def get_destination(

# Simple consumption
dest = client.get_destination("my-api")
print(dest.url)
print(dest.auth_tokens)

# With level hint
dest = client.get_destination("my-api", level=Level.SERVICE_INSTANCE)

# With options - fragment merging
opts = ConsumptionOptions(fragment_name="production")
dest = client.get_destination("my-api", options=opts)
# Fragment merging
dest = client.get_destination("my-api", options=ConsumptionOptions(fragment_name="prod"))

# With tenant context for user token exchange
opts = ConsumptionOptions(tenant="tenant-subdomain")
dest = client.get_destination("my-api", options=opts)
# Optional fragment (no error if fragment not found)
dest = client.get_destination(
"my-api",
options=ConsumptionOptions(fragment_name="prod", fragment_optional=True),
)

# Both fragment and tenant
opts = ConsumptionOptions(fragment_name="prod", tenant="tenant-1")
dest = client.get_destination("my-api", options=opts)
# Tenant context
dest = client.get_destination("my-api", options=ConsumptionOptions(tenant="tenant-1"))

# User token exchange (OAuth2UserTokenExchange / OAuth2JWTBearer)
dest = client.get_destination(
"my-api",
options=ConsumptionOptions(user_token="<jwt>", tenant="tenant-1"),
)

# With transparent proxy enabled
dest = client.get_destination("my-api", proxy_enabled=True)
Expand All @@ -343,8 +350,37 @@ def get_destination(
if options:
if options.fragment_name:
headers["X-fragment-name"] = options.fragment_name
if options.fragment_optional is not None:
headers["X-fragment-optional"] = str(
options.fragment_optional
).lower()
if options.tenant:
headers["X-tenant"] = options.tenant
if options.user_token:
headers["X-user-token"] = options.user_token
if options.subject_token:
headers["X-subject-token"] = options.subject_token
if options.subject_token_type:
headers["X-subject-token-type"] = options.subject_token_type
if options.actor_token:
headers["X-actor-token"] = options.actor_token
if options.actor_token_type:
headers["X-actor-token-type"] = options.actor_token_type
if options.saml_assertion:
headers["X-samlAssertion"] = options.saml_assertion
if options.refresh_token:
headers["X-refresh-token"] = options.refresh_token
if options.code:
headers["X-code"] = options.code
if options.redirect_uri:
headers["X-redirect-uri"] = options.redirect_uri
if options.code_verifier:
headers["X-code-verifier"] = options.code_verifier
if options.chain_name:
headers["X-chain-name"] = options.chain_name
if options.chain_vars:
for var_name, var_value in options.chain_vars.items():
headers[f"X-chain-var-{var_name}"] = var_value

# Build path with optional level hint
if level:
Expand Down
57 changes: 56 additions & 1 deletion src/sap_cloud_sdk/destination/user-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,22 @@ class CertificateClient:

- `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])`
- `auth_tokens` and `certificates` are populated by the v2 consumption API
- `ConsumptionOptions(fragment_name?: str, tenant?: str)` - Options for v2 destination consumption
- `ConsumptionOptions` - Options for v2 destination consumption, controls HTTP headers sent to the Destination Service:
- `fragment_name?: str` - Fragment to merge into the destination (`X-fragment-name`)
- `fragment_optional?: bool` - If `True`, a missing fragment does not cause an error (`X-fragment-optional`)
- `tenant?: str` - Tenant subdomain for token retrieval (`X-tenant`)
- `user_token?: str` - User JWT for OAuth2UserTokenExchange / OAuth2JWTBearer / OAuth2SAMLBearerAssertion (`X-user-token`)
- `subject_token?: str` - Subject token for OAuth2TokenExchange (`X-subject-token`)
- `subject_token_type?: str` - Format of the subject token (`X-subject-token-type`), e.g. `"urn:ietf:params:oauth:token-type:access_token"`
- `actor_token?: str` - Actor token for OAuth2TokenExchange (`X-actor-token`)
- `actor_token_type?: str` - Format of the actor token (`X-actor-token-type`)
- `saml_assertion?: str` - Client-provided SAML assertion for OAuth2SAMLBearerAssertion with `SAMLAssertionProvider=ClientProvided` (`X-samlAssertion`)
- `refresh_token?: str` - Refresh token for OAuth2RefreshToken destinations (`X-refresh-token`)
- `code?: str` - Authorization code for OAuth2AuthorizationCode destinations (`X-code`)
- `redirect_uri?: str` - Redirect URI for OAuth2AuthorizationCode destinations (`X-redirect-uri`)
- `code_verifier?: str` - PKCE code verifier for OAuth2AuthorizationCode destinations (`X-code-verifier`)
- `chain_name?: str` - Name of a predefined destination chain (`X-chain-name`)
- `chain_vars?: dict[str, str]` - Variables for the destination chain; each entry is sent as `X-chain-var-<key>`
- `AuthToken(type: str, value: str, http_header: dict, expires_in?: str, error?: str, scope?: str, refresh_token?: str)` - Authentication token from v2 API
- `Fragment(name: str, properties: dict[str, str])`
- `Certificate(name: str, content: str, type: str)`
Expand Down Expand Up @@ -247,6 +262,46 @@ dest = client.get_destination("my-api")
# Example 9: Combine level with options
options = ConsumptionOptions(fragment_name="production", tenant="tenant-1")
dest = client.get_destination("my-api", level=Level.SUB_ACCOUNT, options=options)

# Example 10: Optional fragment (no error if fragment does not exist)
options = ConsumptionOptions(fragment_name="maybe-exists", fragment_optional=True)
dest = client.get_destination("my-api", options=options)

# Example 11: User token exchange (OAuth2UserTokenExchange / OAuth2JWTBearer)
options = ConsumptionOptions(user_token="<encoded-jwt>", tenant="tenant-1")
dest = client.get_destination("my-api", options=options)

# Example 12: OAuth2TokenExchange with subject and actor tokens
options = ConsumptionOptions(
subject_token="<subject-token>",
subject_token_type="urn:ietf:params:oauth:token-type:access_token",
actor_token="<actor-token>",
actor_token_type="urn:ietf:params:oauth:token-type:access_token",
)
dest = client.get_destination("my-api", options=options)

# Example 13: Client-provided SAML assertion (SAMLAssertionProvider=ClientProvided)
options = ConsumptionOptions(saml_assertion="<base64-encoded-saml>")
dest = client.get_destination("my-api", options=options)

# Example 14: OAuth2RefreshToken
options = ConsumptionOptions(refresh_token="<refresh-token>")
dest = client.get_destination("my-api", options=options)

# Example 15: OAuth2AuthorizationCode with PKCE
options = ConsumptionOptions(
code="<authorization-code>",
redirect_uri="https://myapp/callback",
code_verifier="<pkce-code-verifier>",
)
dest = client.get_destination("my-api", options=options)

# Example 16: Destination chain with chain variables
options = ConsumptionOptions(
chain_name="my-predefined-chain",
chain_vars={"subject_token": "<token>", "subject_token_type": "access_token"},
)
dest = client.get_destination("my-api", options=options)
```

### Return Type
Expand Down
Loading
Loading