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.55.0"
version = "0.55.1"
description = "SAP Cloud SDK for Python"
readme = "README.md"
license = "Apache-2.0"
Expand Down
3 changes: 3 additions & 0 deletions src/sap_cloud_sdk/core/protocol/http/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
from oauthlib.oauth2 import BackendApplicationClient
from requests_oauthlib import OAuth2Session

from sap_cloud_sdk.core._tenant import _validate_tenant_subdomain

logger = logging.getLogger(__name__)

_TOKEN_EXPIRY_BUFFER_SECONDS = 60
Expand Down Expand Up @@ -109,6 +111,7 @@ def _fetch_token(self, tenant_subdomain: Optional[str]) -> OAuth2Session:
and identityzone is not None
and token_url is not None
):
_validate_tenant_subdomain(tenant_subdomain)
token_url = str(token_url).replace(str(identityzone), tenant_subdomain)

client = BackendApplicationClient(client_id=str(self._config.client_id))
Expand Down
67 changes: 67 additions & 0 deletions tests/core/unit/test_http_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,3 +217,70 @@ def test_close_no_auth_closes_plain_session(self):
client.close()
mock_session.close.assert_called_once()
assert client._plain_session is None


class TestXsuaaAuthProviderSubdomainValidation:

def _make_config(self, identityzone: str = "provider-zone") -> MagicMock:
cfg = MagicMock()
cfg.token_url = f"https://{identityzone}.authentication.region/oauth/token"
cfg.identityzone = identityzone
cfg.client_id = "cid"
cfg.client_secret = "csecret"
return cfg

def _make_provider(self, identityzone: str = "provider-zone") -> XsuaaAuthProvider:
cfg = self._make_config(identityzone)
factory = MagicMock(return_value=cfg)
factory.has_changed = MagicMock(return_value=False)
return XsuaaAuthProvider(factory)

@patch("sap_cloud_sdk.core.protocol.http.models._validate_tenant_subdomain")
def test_validator_called_with_tenant_subdomain(self, mock_validate):
provider = self._make_provider()
with patch.object(provider, "_fetch_token") as mock_fetch:
mock_fetch.return_value = MagicMock()
provider.get_session("tenant-123")
mock_fetch.assert_called_once_with("tenant-123")

def test_invalid_subdomain_raises_value_error_before_fetch_token(self):
provider = self._make_provider()
with patch("sap_cloud_sdk.core.protocol.http.models.OAuth2Session") as mock_oauth_cls:
with pytest.raises(ValueError, match="Invalid tenant_subdomain"):
provider._fetch_token("evil.example/oauth/token?ignore=")
mock_oauth_cls.return_value.fetch_token.assert_not_called()

@pytest.mark.parametrize("bad_subdomain", [
"evil.example/oauth/token?ignore=",
"has.dot",
"-leading-hyphen",
"trailing-hyphen-",
"has space",
"has/slash",
"a" * 64,
])
def test_delimiter_bearing_subdomains_rejected(self, bad_subdomain):
provider = self._make_provider()
with pytest.raises(ValueError, match="Invalid tenant_subdomain"):
provider._fetch_token(bad_subdomain)

def test_none_subdomain_skips_validation_and_uses_provider_url(self):
provider = self._make_provider()
cfg = provider._config
with patch("sap_cloud_sdk.core.protocol.http.models.OAuth2Session") as mock_oauth_cls:
mock_session = MagicMock()
mock_oauth_cls.return_value = mock_session
mock_session.fetch_token.return_value = {"access_token": "tok", "expires_in": 3600}
provider._fetch_token(None)
call_kwargs = mock_session.fetch_token.call_args[1]
assert call_kwargs["token_url"] == cfg.token_url

def test_valid_subdomain_replaces_identityzone_in_token_url(self):
provider = self._make_provider("provider-zone")
with patch("sap_cloud_sdk.core.protocol.http.models.OAuth2Session") as mock_oauth_cls:
mock_session = MagicMock()
mock_oauth_cls.return_value = mock_session
mock_session.fetch_token.return_value = {"access_token": "tok", "expires_in": 3600}
provider._fetch_token("tenant-123")
call_kwargs = mock_session.fetch_token.call_args[1]
assert call_kwargs["token_url"] == "https://tenant-123.authentication.region/oauth/token"
Loading