Skip to content

Commit 36fc13e

Browse files
committed
feat(utils): extract RH identity context to shared utility
Move _get_rh_identity_context() and AUTH_DISABLED from rlsapi_v1.py to src/utils/rh_identity.py for reuse by the responses endpoint. Update rlsapi imports to use the shared module. Add unit tests. Signed-off-by: Major Hayden <major@redhat.com>
1 parent 52c5453 commit 36fc13e

4 files changed

Lines changed: 116 additions & 28 deletions

File tree

src/app/endpoints/rlsapi_v1.py

Lines changed: 5 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import metrics
2121
from authentication import get_auth_dependency
2222
from authentication.interface import AuthTuple
23-
from authentication.rh_identity import RHIdentityData
2423
from authorization.middleware import authorize
2524
from client import AsyncLlamaStackClientHolder
2625
from configuration import configuration
@@ -54,6 +53,7 @@
5453
extract_token_usage,
5554
get_mcp_tools,
5655
)
56+
from utils.rh_identity import AUTH_DISABLED, get_rh_identity_context
5757
from utils.shields import run_shield_moderation
5858
from utils.suid import get_suid
5959

@@ -65,8 +65,6 @@ class TemplateRenderError(Exception):
6565
"""Raised when the system prompt Jinja2 template cannot be compiled."""
6666

6767

68-
# Default values when RH Identity auth is not configured
69-
AUTH_DISABLED = "auth_disabled"
7068
# Keep this tuple centralized so infer_endpoint can catch all expected backend
7169
# failures in one place while preserving a single telemetry/error-mapping path.
7270
_INFER_HANDLED_EXCEPTIONS = (
@@ -79,29 +77,8 @@ class TemplateRenderError(Exception):
7977
)
8078

8179

82-
def _get_rh_identity_context(request: Request) -> tuple[str, str]:
83-
"""Extract org_id and system_id from RH Identity request state.
84-
85-
When RH Identity authentication is configured, the auth dependency stores
86-
the RHIdentityData object in request.state.rh_identity_data. This function
87-
extracts the org_id and system_id for telemetry purposes.
88-
89-
Args:
90-
request: The FastAPI request object.
91-
92-
Returns:
93-
Tuple of (org_id, system_id). Returns ("auth_disabled", "auth_disabled")
94-
when RH Identity auth is not configured or data is unavailable.
95-
"""
96-
rh_identity: Optional[RHIdentityData] = getattr(
97-
request.state, "rh_identity_data", None
98-
)
99-
if rh_identity is None:
100-
return AUTH_DISABLED, AUTH_DISABLED
101-
102-
org_id = rh_identity.get_org_id() or AUTH_DISABLED
103-
system_id = rh_identity.get_user_id() or AUTH_DISABLED
104-
return org_id, system_id
80+
# Backward-compatible alias so existing test imports continue to work.
81+
_get_rh_identity_context = get_rh_identity_context
10582

10683

10784
infer_responses: dict[int | str, dict[str, Any]] = {
@@ -369,7 +346,7 @@ def _queue_splunk_event( # pylint: disable=too-many-arguments,too-many-position
369346
input_tokens: Number of prompt tokens consumed by the LLM call.
370347
output_tokens: Number of completion tokens produced by the LLM call.
371348
"""
372-
org_id, system_id = _get_rh_identity_context(request)
349+
org_id, system_id = get_rh_identity_context(request)
373350
systeminfo = infer_request.context.systeminfo
374351

375352
event_data = InferenceEventData(
@@ -530,7 +507,7 @@ def _resolve_quota_subject(request: Request, auth: AuthTuple) -> str | None:
530507
if quota_subject == "user_id":
531508
return user_id
532509

533-
org_id, system_id = _get_rh_identity_context(request)
510+
org_id, system_id = get_rh_identity_context(request)
534511

535512
if quota_subject == "org_id":
536513
if org_id == AUTH_DISABLED:

src/utils/rh_identity.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""Utility functions for extracting RH Identity context for telemetry.
2+
3+
This module provides functions to extract organization and system identifiers
4+
from Red Hat Identity request state for telemetry and logging purposes.
5+
"""
6+
7+
from typing import Final, Optional
8+
9+
from starlette.requests import Request
10+
11+
from authentication.rh_identity import RHIdentityData
12+
13+
# Default value when RH Identity auth is not configured
14+
AUTH_DISABLED: Final[str] = "auth_disabled"
15+
16+
17+
def get_rh_identity_context(request: Request) -> tuple[str, str]:
18+
"""Extract org_id and system_id from RH Identity request state.
19+
20+
When RH Identity authentication is configured, the auth dependency stores
21+
the RHIdentityData object in request.state.rh_identity_data. This function
22+
extracts the org_id and system_id for telemetry purposes.
23+
24+
Args:
25+
request: The FastAPI request object.
26+
27+
Returns:
28+
Tuple of (org_id, system_id). Returns ("auth_disabled", "auth_disabled")
29+
when RH Identity auth is not configured or data is unavailable.
30+
"""
31+
rh_identity: Optional[RHIdentityData] = getattr(
32+
request.state, "rh_identity_data", None
33+
)
34+
if rh_identity is None:
35+
return AUTH_DISABLED, AUTH_DISABLED
36+
37+
org_id = rh_identity.get_org_id() or AUTH_DISABLED
38+
system_id = rh_identity.get_user_id() or AUTH_DISABLED
39+
return org_id, system_id

tests/unit/utils/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ Unit tests for utils/query.py functions.
3939
## [test_responses.py](test_responses.py)
4040
Unit tests for utils/responses.py functions.
4141

42+
## [test_rh_identity.py](test_rh_identity.py)
43+
Unit tests for utils/rh_identity module.
44+
4245
## [test_shields.py](test_shields.py)
4346
Unit tests for utils/shields.py functions.
4447

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
"""Unit tests for utils/rh_identity module."""
2+
3+
import pytest
4+
from pytest_mock import MockerFixture
5+
6+
from authentication.rh_identity import RHIdentityData
7+
from utils.rh_identity import AUTH_DISABLED, get_rh_identity_context
8+
9+
10+
def test_auth_disabled_constant() -> None:
11+
"""Verify AUTH_DISABLED constant value."""
12+
assert AUTH_DISABLED == "auth_disabled"
13+
14+
15+
@pytest.mark.parametrize(
16+
("rh_identity_setup", "expected_org_id", "expected_system_id"),
17+
[
18+
pytest.param(
19+
{"org_id": "org123", "user_id": "sys456"},
20+
"org123",
21+
"sys456",
22+
id="identity_present",
23+
),
24+
pytest.param(
25+
None,
26+
AUTH_DISABLED,
27+
AUTH_DISABLED,
28+
id="identity_absent",
29+
),
30+
pytest.param(
31+
{"org_id": "", "user_id": "sys456"},
32+
AUTH_DISABLED,
33+
"sys456",
34+
id="empty_org_id",
35+
),
36+
pytest.param(
37+
{"org_id": "org123", "user_id": ""},
38+
"org123",
39+
AUTH_DISABLED,
40+
id="empty_user_id",
41+
),
42+
],
43+
)
44+
def test_get_rh_identity_context(
45+
mocker: MockerFixture,
46+
rh_identity_setup: dict[str, str] | None,
47+
expected_org_id: str,
48+
expected_system_id: str,
49+
) -> None:
50+
"""Test get_rh_identity_context extracts or defaults org/system IDs."""
51+
mock_request = mocker.Mock()
52+
53+
if rh_identity_setup is not None:
54+
mock_rh_identity = mocker.Mock(spec=RHIdentityData)
55+
mock_rh_identity.get_org_id.return_value = rh_identity_setup["org_id"]
56+
mock_rh_identity.get_user_id.return_value = rh_identity_setup["user_id"]
57+
mock_request.state = mocker.Mock()
58+
mock_request.state.rh_identity_data = mock_rh_identity
59+
else:
60+
mock_request.state = mocker.Mock(spec=[])
61+
62+
org_id, system_id = get_rh_identity_context(mock_request)
63+
64+
assert org_id == expected_org_id
65+
assert system_id == expected_system_id
66+
67+
if rh_identity_setup is not None:
68+
mock_rh_identity.get_org_id.assert_called_once()
69+
mock_rh_identity.get_user_id.assert_called_once()

0 commit comments

Comments
 (0)