Skip to content

Commit 3599511

Browse files
committed
refactor credentials detection
1 parent 8f9c3d3 commit 3599511

4 files changed

Lines changed: 154 additions & 187 deletions

File tree

src/sap_cloud_sdk/agentgateway/_customer.py

Lines changed: 65 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -114,18 +114,76 @@ def detect_customer_agent_credentials(
114114
return None
115115

116116

117-
def load_customer_credentials(path: str) -> CustomerCredentials:
118-
"""Load and parse customer credentials from file.
117+
def load_customer_credentials(
118+
path: str | None,
119+
tls_mode: TlsMode = TlsMode.STANDARD,
120+
) -> CustomerCredentials:
121+
"""Load customer credentials from a file (standard mode) or environment variables (transparent mode).
122+
123+
In transparent mode (``tls_mode=TlsMode.TRANSPARENT``) the OpenShell Gateway
124+
injects the mTLS client certificate at the TLS layer. Credentials are read
125+
from environment variables and ``certificate``/``private_key`` are set to
126+
``None``.
127+
128+
In standard mode ``path`` must point to a valid JSON credentials file.
119129
120130
Args:
121-
path: Path to the credentials JSON file.
131+
path: Path to the credentials JSON file. Required in standard mode,
132+
ignored in transparent mode.
133+
tls_mode: TLS handling mode. When TRANSPARENT, loads from env vars.
122134
123135
Returns:
124136
Parsed CustomerCredentials.
125137
126138
Raises:
127-
AgentGatewaySDKError: If file cannot be read or is missing required fields.
139+
AgentGatewaySDKError: If credentials cannot be loaded or required fields
140+
are missing.
128141
"""
142+
if tls_mode == TlsMode.TRANSPARENT:
143+
logger.debug("TLS_MODE=transparent: loading credentials from environment variables")
144+
required = [_ENV_CLIENT_ID, _ENV_TOKEN_SERVICE_URL, _ENV_GATEWAY_URL]
145+
missing = [v for v in required if not os.environ.get(v)]
146+
if missing:
147+
raise AgentGatewaySDKError(
148+
f"TLS_MODE=transparent requires environment variables: {missing}"
149+
)
150+
151+
raw_deps = os.environ.get(_ENV_INTEGRATION_DEPENDENCIES, "[]")
152+
try:
153+
deps_data = json.loads(raw_deps)
154+
integration_dependencies = [
155+
IntegrationDependency(
156+
ord_id=dep[_CredentialFields.ORD_ID],
157+
global_tenant_id=dep[_CredentialFields.GLOBAL_TENANT_ID],
158+
)
159+
for dep in deps_data
160+
]
161+
except (json.JSONDecodeError, KeyError, TypeError) as e:
162+
raise AgentGatewaySDKError(
163+
f"Failed to parse INTEGRATION_DEPENDENCIES: {e}. "
164+
'Expected format: [{"ordId": "...", "globalTenantId": "..."}]'
165+
)
166+
167+
logger.debug(
168+
"Loaded %d integration dependencies from environment",
169+
len(integration_dependencies),
170+
)
171+
172+
return CustomerCredentials(
173+
token_service_url=os.environ[_ENV_TOKEN_SERVICE_URL],
174+
client_id=os.environ[_ENV_CLIENT_ID],
175+
certificate=None,
176+
private_key=None,
177+
gateway_url=os.environ[_ENV_GATEWAY_URL].rstrip("/"),
178+
integration_dependencies=integration_dependencies,
179+
)
180+
181+
# Standard mode: file-based credentials
182+
if not path:
183+
raise AgentGatewaySDKError(
184+
"Credentials file path is required in standard TLS mode."
185+
)
186+
129187
logger.debug("Loading customer credentials from: %s", path)
130188

131189
try:
@@ -134,8 +192,6 @@ def load_customer_credentials(path: str) -> CustomerCredentials:
134192
except (OSError, json.JSONDecodeError) as e:
135193
raise AgentGatewaySDKError(f"Failed to load credentials from '{path}': {e}")
136194

137-
# Map credential file keys to dataclass fields
138-
# Credential file uses camelCase, we use snake_case
139195
required_fields = {
140196
_CredentialFields.TOKEN_SERVICE_URL: "token_service_url",
141197
_CredentialFields.CLIENT_ID: "client_id",
@@ -144,13 +200,12 @@ def load_customer_credentials(path: str) -> CustomerCredentials:
144200
_CredentialFields.GATEWAY_URL: "gateway_url",
145201
}
146202

147-
missing = [k for k in required_fields if k not in data]
148-
if missing:
203+
missing_fields = [k for k in required_fields if k not in data]
204+
if missing_fields:
149205
raise AgentGatewaySDKError(
150-
f"Credentials file missing required fields: {missing}"
206+
f"Credentials file missing required fields: {missing_fields}"
151207
)
152208

153-
# Parse integrationDependencies (required)
154209
if _CredentialFields.INTEGRATION_DEPENDENCIES not in data:
155210
raise AgentGatewaySDKError(
156211
"Credentials file missing required field: integrationDependencies. "
@@ -187,64 +242,6 @@ def load_customer_credentials(path: str) -> CustomerCredentials:
187242
)
188243

189244

190-
def load_customer_credentials_from_env() -> CustomerCredentials:
191-
"""Load customer credentials from environment variables (transparent mode).
192-
193-
Used when TlsMode.TRANSPARENT is active and the OpenShell Gateway handles
194-
mTLS. Certificate and private key are not required — they are injected at
195-
the TLS layer by the gateway.
196-
197-
Environment variables:
198-
CLIENT_ID: IAS client ID (may be a gateway-resolved placeholder at runtime)
199-
TOKEN_SERVICE_URL: IAS token service endpoint
200-
GATEWAY_URL: Agent Gateway base URL
201-
INTEGRATION_DEPENDENCIES: JSON array of {ordId, globalTenantId} objects
202-
203-
Returns:
204-
CustomerCredentials with certificate and private_key set to None.
205-
206-
Raises:
207-
AgentGatewaySDKError: If required environment variables are missing or
208-
INTEGRATION_DEPENDENCIES cannot be parsed.
209-
"""
210-
required = [_ENV_CLIENT_ID, _ENV_TOKEN_SERVICE_URL, _ENV_GATEWAY_URL]
211-
missing = [v for v in required if not os.environ.get(v)]
212-
if missing:
213-
raise AgentGatewaySDKError(
214-
f"TLS_MODE=transparent requires environment variables: {missing}"
215-
)
216-
217-
raw_deps = os.environ.get(_ENV_INTEGRATION_DEPENDENCIES, "[]")
218-
try:
219-
deps_data = json.loads(raw_deps)
220-
integration_dependencies = [
221-
IntegrationDependency(
222-
ord_id=dep[_CredentialFields.ORD_ID],
223-
global_tenant_id=dep[_CredentialFields.GLOBAL_TENANT_ID],
224-
)
225-
for dep in deps_data
226-
]
227-
except (json.JSONDecodeError, KeyError, TypeError) as e:
228-
raise AgentGatewaySDKError(
229-
f"Failed to parse INTEGRATION_DEPENDENCIES: {e}. "
230-
'Expected format: [{"ordId": "...", "globalTenantId": "..."}]'
231-
)
232-
233-
logger.debug(
234-
"Loaded %d integration dependencies from environment",
235-
len(integration_dependencies),
236-
)
237-
238-
return CustomerCredentials(
239-
token_service_url=os.environ[_ENV_TOKEN_SERVICE_URL],
240-
client_id=os.environ[_ENV_CLIENT_ID],
241-
certificate=None,
242-
private_key=None,
243-
gateway_url=os.environ[_ENV_GATEWAY_URL].rstrip("/"),
244-
integration_dependencies=integration_dependencies,
245-
)
246-
247-
248245
def _create_ssl_context(certificate: str, private_key: str) -> ssl.SSLContext:
249246
"""Create SSL context for mTLS from in-memory certificate and key.
250247

0 commit comments

Comments
 (0)