Skip to content

Commit a7dbf3a

Browse files
committed
docs(spec): document organization parameter in platform service specification
Add AIGNOSTICS_ORGANIZATION to the platform service specification: - Add organization parameter to Configuration Parameters (section 6.1) - Add AIGNOSTICS_ORGANIZATION environment variable (section 6.2) - Update PKCE Flow description to document organization parameter usage
1 parent 3a3adc8 commit a7dbf3a

4 files changed

Lines changed: 49 additions & 80 deletions

File tree

specifications/SPEC_PLATFORM_SERVICE.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -409,14 +409,15 @@ The Platform module provides foundational services but does not directly expose
409409

410410
### 6.1 Configuration Parameters
411411

412-
| Parameter | Type | Default | Description | Required |
413-
| ------------------------------- | ---- | --------------------------------- | --------------------------- | -------- |
414-
| `api_root` | str | `https://platform.aignostics.com` | Base URL of Aignostics API | Yes |
415-
| `audience` | str | Environment-specific | OAuth audience claim | Yes |
416-
| `scope` | str | `offline_access` | OAuth scopes required | Yes |
417-
| `cache_dir` | str | User cache directory | Directory for token storage | No |
418-
| `request_timeout_seconds` | int | 30 | API request timeout | No |
419-
| `authorization_backoff_seconds` | int | 3 | Retry backoff time | No |
412+
| Parameter | Type | Default | Description | Required |
413+
| ------------------------------- | ---- | --------------------------------- | ---------------------------- | -------- |
414+
| `api_root` | str | `https://platform.aignostics.com` | Base URL of Aignostics API | Yes |
415+
| `audience` | str | Environment-specific | OAuth audience claim | Yes |
416+
| `scope` | str | `offline_access` | OAuth scopes required | Yes |
417+
| `organization` | str | None | Auth0 organization for OAuth | No |
418+
| `cache_dir` | str | User cache directory | Directory for token storage | No |
419+
| `request_timeout_seconds` | int | 30 | API request timeout | No |
420+
| `authorization_backoff_seconds` | int | 3 | Retry backoff time | No |
420421

421422
### 6.2 Environment Variables
422423

@@ -432,6 +433,7 @@ The Platform module provides foundational services but does not directly expose
432433
| `AIGNOSTICS_DEVICE_URL` | Custom device authorization URL | `https://custom.auth0.com/oauth/device/code` |
433434
| `AIGNOSTICS_JWS_JSON_URL` | Custom JWS key set URL | `https://custom.auth0.com/.well-known/jwks.json` |
434435
| `AIGNOSTICS_CLIENT_ID_INTERACTIVE` | Interactive flow client ID | `interactive_client_123` |
436+
| `AIGNOSTICS_ORGANIZATION_ID` | Auth0 organization for OAuth | `my-organization` |
435437
| `AIGNOSTICS_REFRESH_TOKEN` | Long-lived refresh token | `refresh_token_value` |
436438
| `AIGNOSTICS_CACHE_DIR` | Custom cache directory | `/custom/cache/path` |
437439
| `AIGNOSTICS_REQUEST_TIMEOUT_SECONDS` | API request timeout | `60` |
@@ -492,7 +494,7 @@ The Platform module provides foundational services but does not directly expose
492494

493495
### 9.1 Key Algorithms and Business Logic
494496

495-
- **PKCE Flow**: OAuth 2.0 Authorization Code flow with Proof Key for Code Exchange for enhanced security in public clients
497+
- **PKCE Flow**: OAuth 2.0 Authorization Code flow with Proof Key for Code Exchange for enhanced security in public clients. Supports optional `AIGNOSTICS_ORGANIZATION` parameter for Auth0 organization-specific authentication flows
496498
- **Token Caching**: File-based token persistence with expiration tracking and automatic cleanup
497499
- **Health Monitoring**: Multi-layer health checks including public endpoint availability and authenticated API access
498500

src/aignostics/platform/_authentication.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -337,10 +337,10 @@ def _perform_authorization_code_with_pkce_flow() -> str: # noqa: C901
337337
"access_type": "offline",
338338
"audience": settings().audience,
339339
}
340-
organization = settings().organization
340+
organization_id = settings().organization_id
341341

342-
if organization:
343-
auth_params["organization"] = organization
342+
if organization_id:
343+
auth_params["organization"] = organization_id
344344

345345
authorization_url, _ = session.authorization_url(
346346
settings().authorization_base_url,

src/aignostics/platform/_settings.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,9 @@ def profile_edit_url(self) -> str:
206206
]
207207
client_id_interactive: Annotated[str, Field(description="OAuth client ID for interactive flows")]
208208

209-
organization: Annotated[str | None, Field(description="Optional organization for OAuth redirect")] = None
209+
organization_id: Annotated[
210+
str | None, Field(description="Optional Auth0 organization ID parameter for the /authorize OAuth endpoint")
211+
] = None
210212

211213
@computed_field # type: ignore[prop-decorator]
212214
@property

tests/aignostics/platform/authentication_test.py

Lines changed: 32 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ def mock_settings() -> MagicMock:
6161
settings.auth_retry_attempts = 3
6262
settings.auth_jwk_set_cache_ttl = 300
6363
settings.refresh_token = None
64+
settings.organization_id = None
6465
mock_settings.return_value = settings
6566
yield mock_settings
6667

@@ -339,25 +340,22 @@ def test_can_open_browser_false(record_property) -> None:
339340
class TestAuthorizationCodeFlow:
340341
"""Test cases for the authorization code flow with PKCE."""
341342

342-
@pytest.mark.unit
343343
@staticmethod
344-
def test_perform_authorization_code_flow_success(record_property, mock_settings) -> None:
345-
"""Test successful authorization code flow with PKCE."""
346-
record_property("tested-item-id", "SPEC-PLATFORM-SERVICE")
347-
# Mock OAuth session
344+
def _run_pkce_flow() -> tuple[str | None, MagicMock]:
345+
"""Set up common PKCE flow mocks, run the flow, and return (token, mock_session).
346+
347+
Returns:
348+
tuple: The returned token and the OAuth2Session mock for further assertions.
349+
"""
348350
mock_session = MagicMock(spec=OAuth2Session)
349351
mock_session.authorization_url.return_value = ("https://test.auth/authorize?code_challenge=abc", None)
350352
mock_session.fetch_token.return_value = {"access_token": "pkce.token"}
351353

352-
# Mock HTTP server
353354
mock_server = MagicMock()
354-
355-
# Setup mocks for the redirect URI parsing
356355
mock_redirect_parsed = MagicMock()
357356
mock_redirect_parsed.hostname = "localhost"
358357
mock_redirect_parsed.port = 8000
359358

360-
# Create a custom HTTPServer mock implementation that simulates a callback
361359
class MockHTTPServer:
362360
def __init__(self, *args, **kwargs) -> None:
363361
pass
@@ -368,7 +366,6 @@ def __enter__(self) -> MagicMock:
368366
def __exit__(self, *args) -> None:
369367
pass
370368

371-
# Create a mock for the auth result
372369
mock_auth_result = MagicMock()
373370
mock_auth_result.token = "pkce.token" # noqa: S105 - Test credential
374371
mock_auth_result.error = None
@@ -379,20 +376,19 @@ def __exit__(self, *args) -> None:
379376
patch("urllib.parse.urlparse", return_value=mock_redirect_parsed),
380377
patch("aignostics.platform._authentication.AuthenticationResult", return_value=mock_auth_result),
381378
):
382-
# Simulate a successful server response by making handle_request set the token
383-
def handle_request_side_effect():
384-
# This simulates what the HTTP handler would do on success
385-
mock_auth_result.token = "pkce.token" # noqa: S105 - Test credential
386-
387-
mock_server.handle_request.side_effect = handle_request_side_effect
388-
389-
# Call the function under test
379+
mock_server.handle_request.side_effect = lambda: None
390380
token = _perform_authorization_code_with_pkce_flow()
391381

392-
# Assertions
393-
assert token == "pkce.token" # noqa: S105 - Test credential
394-
mock_server.handle_request.assert_called_once()
395-
mock_session.authorization_url.assert_called_once()
382+
return token, mock_session
383+
384+
@pytest.mark.unit
385+
@staticmethod
386+
def test_perform_authorization_code_flow_success(record_property, mock_settings) -> None:
387+
"""Test successful authorization code flow with PKCE."""
388+
record_property("tested-item-id", "SPEC-PLATFORM-SERVICE")
389+
token, mock_session = TestAuthorizationCodeFlow._run_pkce_flow()
390+
assert token == "pkce.token" # noqa: S105 - Test credential
391+
mock_session.authorization_url.assert_called_once()
396392

397393
@pytest.mark.unit
398394
@staticmethod
@@ -471,52 +467,21 @@ def handle_request_side_effect():
471467
def test_perform_authorization_code_flow_with_organization(record_property, mock_settings) -> None:
472468
"""Test authorization code flow includes organization parameter when set."""
473469
record_property("tested-item-id", "SPEC-PLATFORM-SERVICE")
474-
mock_settings_instance = mock_settings.return_value
475-
mock_settings_instance.organization = "test-org"
476-
477-
# Mock OAuth session
478-
mock_session = MagicMock(spec=OAuth2Session)
479-
mock_session.authorization_url.return_value = ("https://test.auth/authorize?code_challenge=abc", None)
480-
mock_session.fetch_token.return_value = {"access_token": "pkce.token"}
481-
482-
# Mock HTTP server
483-
mock_server = MagicMock()
484-
mock_redirect_parsed = MagicMock()
485-
mock_redirect_parsed.hostname = "localhost"
486-
mock_redirect_parsed.port = 8000
487-
488-
class MockHTTPServer:
489-
def __init__(self, *args, **kwargs) -> None:
490-
pass
491-
492-
def __enter__(self) -> MagicMock:
493-
return mock_server
494-
495-
def __exit__(self, *args) -> None:
496-
pass
470+
mock_settings.return_value.organization_id = "test-org"
471+
token, mock_session = TestAuthorizationCodeFlow._run_pkce_flow()
472+
assert token == "pkce.token" # noqa: S105
473+
assert mock_session.authorization_url.call_args[1].get("organization") == "test-org"
497474

498-
mock_auth_result = MagicMock()
499-
mock_auth_result.token = "pkce.token" # noqa: S105
500-
mock_auth_result.error = None
501-
502-
with (
503-
patch("aignostics.platform._authentication.OAuth2Session", return_value=mock_session),
504-
patch("aignostics.platform._authentication.HTTPServer", MockHTTPServer),
505-
patch("urllib.parse.urlparse", return_value=mock_redirect_parsed),
506-
patch("aignostics.platform._authentication.AuthenticationResult", return_value=mock_auth_result),
507-
):
508-
509-
def handle_request_side_effect():
510-
mock_auth_result.token = "pkce.token" # noqa: S105
511-
512-
mock_server.handle_request.side_effect = handle_request_side_effect
513-
token = _perform_authorization_code_with_pkce_flow()
514-
515-
# Assertions
516-
assert token == "pkce.token" # noqa: S105
517-
# Verify organization was passed to authorization_url
518-
call_kwargs = mock_session.authorization_url.call_args[1]
519-
assert call_kwargs.get("organization") == "test-org"
475+
@pytest.mark.unit
476+
@staticmethod
477+
def test_perform_authorization_code_flow_without_organization(record_property, mock_settings) -> None:
478+
"""Test authorization code flow omits organization parameter when unset."""
479+
record_property("tested-item-id", "SPEC-PLATFORM-SERVICE")
480+
# organization_id is None by default in mock_settings fixture
481+
assert mock_settings.return_value.organization_id is None
482+
token, mock_session = TestAuthorizationCodeFlow._run_pkce_flow()
483+
assert token == "pkce.token" # noqa: S105
484+
assert "organization" not in mock_session.authorization_url.call_args[1]
520485

521486

522487
class TestDeviceFlow:

0 commit comments

Comments
 (0)