diff --git a/cogsol/core/api.py b/cogsol/core/api.py index eda7098..67343f2 100644 --- a/cogsol/core/api.py +++ b/cogsol/core/api.py @@ -14,6 +14,7 @@ from jwt import decode from cogsol.core.constants import ( + get_auth_scope_id, get_cognitive_api_base_url, get_content_api_base_url, ) @@ -133,6 +134,8 @@ def _refresh_bearer_token(self) -> None: client_id = os.environ.get("COGSOL_AUTH_CLIENT_ID") client_secret = os.environ.get("COGSOL_AUTH_SECRET") + scope_id = get_auth_scope_id() + if not client_secret: raise CogSolAPIError( "Missing authentication configuration: COGSOL_AUTH_SECRET is not set.\n" @@ -141,7 +144,7 @@ def _refresh_bearer_token(self) -> None: ) authority = "https://pyxiscognitivesweden.b2clogin.com/pyxiscognitivesweden.onmicrosoft.com/B2C_1A_CS_signup_signin_Sweden_MigrationOIDC" - scopes = [f"https://pyxiscognitivesweden.onmicrosoft.com/{client_id}/.default"] + scopes = [f"https://pyxiscognitivesweden.onmicrosoft.com/{scope_id}/.default"] app = msal.ConfidentialClientApplication( client_id, diff --git a/cogsol/core/constants.py b/cogsol/core/constants.py index 3496684..f0f44c8 100644 --- a/cogsol/core/constants.py +++ b/cogsol/core/constants.py @@ -8,6 +8,7 @@ COGSOL_ENV_VAR: Final = "COGSOL_ENV" COGSOL_API_BASE_VAR: Final = "COGSOL_API_BASE" COGSOL_CONTENT_API_BASE_VAR: Final = "COGSOL_CONTENT_API_BASE" +COGSOL_AUTH_SCOPE_ID_VAR: Final = "COGSOL_AUTH_SCOPE_ID" if os.environ.get("COGSOL_AUTH_CLIENT_ID"): _implantation_cognitive_api_url = "https://apis-imp.cogsol.ai/cognitive" @@ -63,6 +64,24 @@ def get_content_api_base_url() -> str: return _get_env_var(COGSOL_CONTENT_API_BASE_VAR) or get_default_content_api_base_url() +AUTH_SCOPE_IDS: Final[dict[str, str]] = { + "implantation": "9efa4bc6-2b2b-4208-8c88-7a218c7061d6", + "production": "92c0d1cc-127b-4ec5-9be4-960c13c7aecc", +} + + +def get_default_auth_scope_id() -> str: + """Return the default OAuth scope ID based on COGSOL_ENV.""" + if get_cogsol_env() == "production": + return AUTH_SCOPE_IDS["production"] + return AUTH_SCOPE_IDS["implantation"] + + +def get_auth_scope_id() -> str: + """Resolve the OAuth scope ID using env overrides when present.""" + return _get_env_var(COGSOL_AUTH_SCOPE_ID_VAR) or get_default_auth_scope_id() + + __all__ = [ "COGSOL_ENV_VAR", "COGSOL_API_BASE_VAR", @@ -76,4 +95,7 @@ def get_content_api_base_url() -> str: "get_default_content_api_base_url", "get_cognitive_api_base_url", "get_content_api_base_url", + "AUTH_SCOPE_IDS", + "get_default_auth_scope_id", + "get_auth_scope_id", ] diff --git a/tests/test_api_key_errors.py b/tests/test_api_key_errors.py index 461f3ec..0f1ccce 100644 --- a/tests/test_api_key_errors.py +++ b/tests/test_api_key_errors.py @@ -2,6 +2,7 @@ Tests for API key and credential error messages (branch: csp-1666-api-key-error-msg). Covers: +- get_auth_scope_id: scope resolved from COGSOL_ENV, overridable via COGSOL_AUTH_SCOPE_ID. - CogSolClient._refresh_bearer_token: detailed error when COGSOL_AUTH_SECRET is missing. - migrate Command: no-credentials check shows helpful message and returns 1. - importagent Command: no-credentials check shows helpful message and returns 1. @@ -38,12 +39,51 @@ def _bare_client() -> CogSolClient: # --------------------------------------------------------------------------- +class TestAuthScopeIdResolution: + """get_auth_scope_id must return the correct scope based on COGSOL_ENV, + defaulting to the implantation scope for missing or unknown values. + COGSOL_AUTH_SCOPE_ID overrides the derived value when set.""" + + def test_returns_implantation_scope_when_env_not_set(self, monkeypatch): + monkeypatch.delenv("COGSOL_ENV", raising=False) + monkeypatch.delenv("COGSOL_AUTH_SCOPE_ID", raising=False) + + from cogsol.core.constants import AUTH_SCOPE_IDS, get_auth_scope_id + + assert get_auth_scope_id() == AUTH_SCOPE_IDS["implantation"] + + def test_returns_implantation_scope_when_env_is_unknown(self, monkeypatch): + monkeypatch.setenv("COGSOL_ENV", "development") + monkeypatch.delenv("COGSOL_AUTH_SCOPE_ID", raising=False) + + from cogsol.core.constants import AUTH_SCOPE_IDS, get_auth_scope_id + + assert get_auth_scope_id() == AUTH_SCOPE_IDS["implantation"] + + def test_returns_production_scope_when_env_is_production(self, monkeypatch): + monkeypatch.setenv("COGSOL_ENV", "production") + monkeypatch.delenv("COGSOL_AUTH_SCOPE_ID", raising=False) + + from cogsol.core.constants import AUTH_SCOPE_IDS, get_auth_scope_id + + assert get_auth_scope_id() == AUTH_SCOPE_IDS["production"] + + def test_scope_id_env_var_overrides_derived_value(self, monkeypatch): + monkeypatch.setenv("COGSOL_AUTH_SCOPE_ID", "custom-scope-id-override") + monkeypatch.setenv("COGSOL_ENV", "production") + + from cogsol.core.constants import get_auth_scope_id + + assert get_auth_scope_id() == "custom-scope-id-override" + + class TestMissingAuthSecret: """_refresh_bearer_token must raise CogSolAPIError with a helpful message when COGSOL_AUTH_CLIENT_ID is set but COGSOL_AUTH_SECRET is missing.""" def test_raises_cogsol_api_error(self, monkeypatch): monkeypatch.setenv("COGSOL_AUTH_CLIENT_ID", "test-client-id") + monkeypatch.setenv("COGSOL_ENV", "development") monkeypatch.delenv("COGSOL_AUTH_SECRET", raising=False) with pytest.raises(CogSolAPIError): @@ -51,6 +91,7 @@ def test_raises_cogsol_api_error(self, monkeypatch): def test_error_mentions_missing_secret_var(self, monkeypatch): monkeypatch.setenv("COGSOL_AUTH_CLIENT_ID", "test-client-id") + monkeypatch.setenv("COGSOL_ENV", "development") monkeypatch.delenv("COGSOL_AUTH_SECRET", raising=False) with pytest.raises(CogSolAPIError) as exc_info: @@ -60,6 +101,7 @@ def test_error_mentions_missing_secret_var(self, monkeypatch): def test_error_includes_onboarding_url(self, monkeypatch): monkeypatch.setenv("COGSOL_AUTH_CLIENT_ID", "test-client-id") + monkeypatch.setenv("COGSOL_ENV", "development") monkeypatch.delenv("COGSOL_AUTH_SECRET", raising=False) with pytest.raises(CogSolAPIError) as exc_info: @@ -69,6 +111,7 @@ def test_error_includes_onboarding_url(self, monkeypatch): def test_error_mentions_implantation_portal(self, monkeypatch): monkeypatch.setenv("COGSOL_AUTH_CLIENT_ID", "test-client-id") + monkeypatch.setenv("COGSOL_ENV", "development") monkeypatch.delenv("COGSOL_AUTH_SECRET", raising=False) with pytest.raises(CogSolAPIError) as exc_info: @@ -80,6 +123,7 @@ def test_no_error_when_secret_is_present(self, monkeypatch): """With a valid secret the error must NOT be raised (msal call may fail, but that is a different error path).""" monkeypatch.setenv("COGSOL_AUTH_CLIENT_ID", "test-client-id") + monkeypatch.setenv("COGSOL_ENV", "development") monkeypatch.setenv("COGSOL_AUTH_SECRET", "some-secret") # The error about missing secret should not be raised;