From 8b73a6132a0dcd63b76ed1c6dbed21e2c8d1d71f Mon Sep 17 00:00:00 2001 From: Major Hayden Date: Thu, 22 Jan 2026 16:47:35 -0600 Subject: [PATCH 1/5] test(e2e): add rh-identity auth configuration files Add configuration files for e2e testing with rh-identity authentication module enabled for both server-mode and library-mode deployments. Both configs require the 'rhel' entitlement for validation testing. Signed-off-by: Major Hayden --- .../lightspeed-stack-auth-rh-identity.yaml | 24 ++++++++++++++++++ .../lightspeed-stack-auth-rh-identity.yaml | 25 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 tests/e2e/configuration/library-mode/lightspeed-stack-auth-rh-identity.yaml create mode 100644 tests/e2e/configuration/server-mode/lightspeed-stack-auth-rh-identity.yaml diff --git a/tests/e2e/configuration/library-mode/lightspeed-stack-auth-rh-identity.yaml b/tests/e2e/configuration/library-mode/lightspeed-stack-auth-rh-identity.yaml new file mode 100644 index 00000000..ba2c5036 --- /dev/null +++ b/tests/e2e/configuration/library-mode/lightspeed-stack-auth-rh-identity.yaml @@ -0,0 +1,24 @@ +name: Lightspeed Core Service (LCS) - RH Identity Auth +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: true + workers: 1 + color_log: true + access_log: true +llama_stack: + use_as_library_client: true + library_client_config_path: run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +conversation_cache: + type: "sqlite" + sqlite: + db_path: "/tmp/data/conversation-cache.db" +authentication: + module: "rh-identity" + rh_identity_config: + required_entitlements: ["rhel"] diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-auth-rh-identity.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-auth-rh-identity.yaml new file mode 100644 index 00000000..1fc83fa4 --- /dev/null +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-auth-rh-identity.yaml @@ -0,0 +1,25 @@ +name: Lightspeed Core Service (LCS) - RH Identity Auth +service: + host: 0.0.0.0 + port: 8080 + auth_enabled: true + workers: 1 + color_log: true + access_log: true +llama_stack: + use_as_library_client: false + url: http://llama-stack:8321 + api_key: xyzzy +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +conversation_cache: + type: "sqlite" + sqlite: + db_path: "/tmp/data/conversation-cache.db" +authentication: + module: "rh-identity" + rh_identity_config: + required_entitlements: ["rhel"] From 6fa152363cb2e20f22b73b677ec95a9d1ea308ee Mon Sep 17 00:00:00 2001 From: Major Hayden Date: Thu, 22 Jan 2026 16:47:41 -0600 Subject: [PATCH 2/5] test(e2e): add step definitions for rh-identity header handling Add step definitions to set x-rh-identity headers in various formats: - Raw string values (for invalid base64 testing) - Base64-encoded raw strings (for invalid JSON testing) - Base64-encoded JSON objects - Valid User identity with configurable fields - Valid System identity with configurable fields Includes helper function to encode identity data to base64. Signed-off-by: Major Hayden --- tests/e2e/features/steps/auth.py | 112 +++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/tests/e2e/features/steps/auth.py b/tests/e2e/features/steps/auth.py index e9360f51..e31a49ba 100644 --- a/tests/e2e/features/steps/auth.py +++ b/tests/e2e/features/steps/auth.py @@ -1,11 +1,27 @@ """Implementation of common test steps.""" +import base64 +import json + import requests from behave import given, when # pyright: ignore[reportAttributeAccessIssue] from behave.runner import Context from tests.e2e.utils.utils import normalize_endpoint +def _encode_rh_identity(identity_data: dict) -> str: + """Encode identity dict to base64 for x-rh-identity header. + + Args: + identity_data: JSON-serializable identity payload to encode. + + Returns: + Base64-encoded UTF-8 string representation of the JSON payload. + """ + json_str = json.dumps(identity_data) + return base64.b64encode(json_str.encode("utf-8")).decode("utf-8") + + @given("I set the Authorization header to {header_value}") def set_authorization_header_custom(context: Context, header_value: str) -> None: """Set a custom Authorization header value. @@ -73,3 +89,99 @@ def access_rest_api_endpoint_post_without_param( context.response = requests.post( url, json="", headers=context.auth_headers, timeout=10 ) + + +@given('I set the x-rh-identity header to raw value "{header_value}"') +def set_rh_identity_header_raw(context: Context, header_value: str) -> None: + """Set x-rh-identity header with a raw string value for testing invalid base64.""" + if not hasattr(context, "auth_headers"): + context.auth_headers = {} + context.auth_headers["x-rh-identity"] = header_value + print(f"Set x-rh-identity header to raw value: {header_value[:50]}...") + + +@given('I set the x-rh-identity header with base64 encoded value "{raw_value}"') +def set_rh_identity_header_base64_raw(context: Context, raw_value: str) -> None: + """Set x-rh-identity header with base64-encoded raw string for testing invalid JSON.""" + if not hasattr(context, "auth_headers"): + context.auth_headers = {} + encoded = base64.b64encode(raw_value.encode("utf-8")).decode("utf-8") + context.auth_headers["x-rh-identity"] = encoded + print(f"Set x-rh-identity header with base64-encoded: {raw_value}") + + +@given("I set the x-rh-identity header with JSON") +def set_rh_identity_header_json(context: Context) -> None: + """Set x-rh-identity header with base64-encoded JSON from context.text.""" + if not hasattr(context, "auth_headers"): + context.auth_headers = {} + assert context.text is not None, "JSON payload required" + identity_data = json.loads(context.text) + context.auth_headers["x-rh-identity"] = _encode_rh_identity(identity_data) + print(f"Set x-rh-identity header with JSON: {identity_data}") + + +@given("I set the x-rh-identity header with valid User identity") +def set_rh_identity_user(context: Context) -> None: + """Set x-rh-identity header with User identity from table.""" + if not hasattr(context, "auth_headers"): + context.auth_headers = {} + + assert context.table is not None, "Table with identity fields required" + + fields = {row["field"]: row["value"] for row in context.table} + + entitlements = {} + if "entitlements" in fields: + for ent in fields["entitlements"].split(","): + ent = ent.strip() + entitlements[ent] = {"is_entitled": True, "is_trial": False} + + identity_data = { + "identity": { + "account_number": fields.get("account_number", "123"), + "org_id": fields.get("org_id", "321"), + "type": "User", + "user": { + "user_id": fields.get("user_id", "test-user"), + "username": fields.get("username", "test@redhat.com"), + "is_org_admin": fields.get("is_org_admin", "false").lower() == "true", + }, + }, + "entitlements": entitlements, + } + + context.auth_headers["x-rh-identity"] = _encode_rh_identity(identity_data) + print(f"Set x-rh-identity header with User identity: {fields.get('user_id')}") + + +@given("I set the x-rh-identity header with valid System identity") +def set_rh_identity_system(context: Context) -> None: + """Set x-rh-identity header with System identity from table.""" + if not hasattr(context, "auth_headers"): + context.auth_headers = {} + + assert context.table is not None, "Table with identity fields required" + + fields = {row["field"]: row["value"] for row in context.table} + + entitlements = {} + if "entitlements" in fields: + for ent in fields["entitlements"].split(","): + ent = ent.strip() + entitlements[ent] = {"is_entitled": True, "is_trial": False} + + identity_data = { + "identity": { + "account_number": fields.get("account_number", "123"), + "org_id": fields.get("org_id", "321"), + "type": "System", + "system": { + "cn": fields.get("cn", "default-cn-uuid"), + }, + }, + "entitlements": entitlements, + } + + context.auth_headers["x-rh-identity"] = _encode_rh_identity(identity_data) + print(f"Set x-rh-identity header with System identity: {fields.get('cn')}") From baffd2e3d5e81b613d2fd01905a1dbc3dc76b443 Mon Sep 17 00:00:00 2001 From: Major Hayden Date: Thu, 22 Jan 2026 16:47:48 -0600 Subject: [PATCH 3/5] test(e2e): add RHIdentity feature tag handling Register @RHIdentity tag in before_feature and after_feature hooks to switch configuration to rh-identity auth mode during feature execution and restore the original configuration afterwards. Signed-off-by: Major Hayden --- tests/e2e/features/environment.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/e2e/features/environment.py b/tests/e2e/features/environment.py index ce40a395..3df842f6 100644 --- a/tests/e2e/features/environment.py +++ b/tests/e2e/features/environment.py @@ -251,6 +251,15 @@ def before_feature(context: Context, feature: Feature) -> None: switch_config(context.feature_config) restart_container("lightspeed-stack") + if "RHIdentity" in feature.tags: + mode_dir = "library-mode" if context.is_library_mode else "server-mode" + context.feature_config = ( + f"tests/e2e/configuration/{mode_dir}/lightspeed-stack-auth-rh-identity.yaml" + ) + context.default_config_backup = create_config_backup("lightspeed-stack.yaml") + switch_config(context.feature_config) + restart_container("lightspeed-stack") + if "Feedback" in feature.tags: context.hostname = os.getenv("E2E_LSC_HOSTNAME", "localhost") context.port = os.getenv("E2E_LSC_PORT", "8080") @@ -273,6 +282,11 @@ def after_feature(context: Context, feature: Feature) -> None: restart_container("lightspeed-stack") remove_config_backup(context.default_config_backup) + if "RHIdentity" in feature.tags: + switch_config(context.default_config_backup) + restart_container("lightspeed-stack") + remove_config_backup(context.default_config_backup) + if "Feedback" in feature.tags: for conversation_id in context.feedback_conversations: url = f"http://{context.hostname}:{context.port}/v1/conversations/{conversation_id}" From a2c726432fc131ff9806940977e633642efcc39f Mon Sep 17 00:00:00 2001 From: Major Hayden Date: Thu, 22 Jan 2026 16:47:57 -0600 Subject: [PATCH 4/5] test(e2e): add rh-identity authentication test scenarios Add comprehensive e2e test scenarios covering all validation paths in the rh-identity authentication module: - Missing x-rh-identity header (401) - Invalid base64 encoding (400) - Invalid JSON content (400) - Missing/null identity field (400) - Missing identity type field (400) - Unsupported identity type (400) - User identity: missing user field (400) - User identity: missing user_id (400) - User identity: missing username (400) - System identity: missing system field (400) - System identity: missing cn (400) - System identity: missing account_number (400) - Missing required entitlements (403) - Empty entitlements (403) - Entitlement with is_entitled=false (403) - Valid User identity with entitlements (200) - Valid System identity with entitlements (200) Signed-off-by: Major Hayden --- .../features/authorized_rh_identity.feature | 293 ++++++++++++++++++ tests/e2e/test_list.txt | 1 + 2 files changed, 294 insertions(+) create mode 100644 tests/e2e/features/authorized_rh_identity.feature diff --git a/tests/e2e/features/authorized_rh_identity.feature b/tests/e2e/features/authorized_rh_identity.feature new file mode 100644 index 00000000..6e44e020 --- /dev/null +++ b/tests/e2e/features/authorized_rh_identity.feature @@ -0,0 +1,293 @@ +@RHIdentity +Feature: Authorized endpoint API tests for the rh-identity authentication module + + Background: + Given The service is started locally + And REST API service prefix is /v1 + + Scenario: Request fails when x-rh-identity header is missing + Given The system is in default state + When I access endpoint "authorized" using HTTP POST method + """ + {"placeholder":"abc"} + """ + Then The status code of the response is 401 + And The body of the response is the following + """ + {"detail": "Missing x-rh-identity header"} + """ + + Scenario: Request fails when x-rh-identity header has invalid base64 + Given The system is in default state + And I set the x-rh-identity header to raw value "not-valid-base64!!!" + When I access endpoint "authorized" using HTTP POST method + """ + {"placeholder":"abc"} + """ + Then The status code of the response is 400 + And The body of the response contains "Invalid base64 encoding" + + Scenario: Request fails when x-rh-identity header has invalid JSON + Given The system is in default state + And I set the x-rh-identity header with base64 encoded value "{not valid json" + When I access endpoint "authorized" using HTTP POST method + """ + {"placeholder":"abc"} + """ + Then The status code of the response is 400 + And The body of the response contains "Invalid JSON" + + Scenario: Request fails when identity field is missing + Given The system is in default state + And I set the x-rh-identity header with JSON + """ + {"entitlements": {"rhel": {"is_entitled": true}}} + """ + When I access endpoint "authorized" using HTTP POST method + """ + {"placeholder":"abc"} + """ + Then The status code of the response is 400 + And The body of the response contains "Missing 'identity' field" + + Scenario: Request fails when identity field is null + Given The system is in default state + And I set the x-rh-identity header with JSON + """ + {"identity": null, "entitlements": {"rhel": {"is_entitled": true}}} + """ + When I access endpoint "authorized" using HTTP POST method + """ + {"placeholder":"abc"} + """ + Then The status code of the response is 400 + And The body of the response contains "Missing 'identity' field" + + Scenario: Request fails when identity type field is missing + Given The system is in default state + And I set the x-rh-identity header with JSON + """ + {"identity": {"org_id": "321"}, "entitlements": {"rhel": {"is_entitled": true}}} + """ + When I access endpoint "authorized" using HTTP POST method + """ + {"placeholder":"abc"} + """ + Then The status code of the response is 400 + And The body of the response contains "Missing identity 'type' field" + + Scenario: Request fails with unsupported identity type + Given The system is in default state + And I set the x-rh-identity header with JSON + """ + {"identity": {"type": "Unknown", "org_id": "123"}} + """ + When I access endpoint "authorized" using HTTP POST method + """ + {"placeholder":"abc"} + """ + Then The status code of the response is 400 + And The body of the response contains "Unsupported identity type" + + Scenario: Request succeeds with valid User identity and required entitlements + Given The system is in default state + And I set the x-rh-identity header with valid User identity + | field | value | + | user_id | test-user-123 | + | username | testuser@redhat.com | + | org_id | 321 | + | entitlements | rhel | + When I access endpoint "authorized" using HTTP POST method + """ + {"placeholder":"abc"} + """ + Then The status code of the response is 200 + + Scenario: Request succeeds with valid System identity and required entitlements + Given The system is in default state + And I set the x-rh-identity header with valid System identity + | field | value | + | cn | c87dcb4c-8af1-40dd-878e-60c744edddd0 | + | account_number | 456 | + | org_id | 654 | + | entitlements | rhel | + When I access endpoint "authorized" using HTTP POST method + """ + {"placeholder":"abc"} + """ + Then The status code of the response is 200 + + Scenario: Request fails when required entitlement is missing + Given The system is in default state + And I set the x-rh-identity header with valid User identity + | field | value | + | user_id | test-user-123 | + | username | testuser@redhat.com | + | org_id | 321 | + | entitlements | ansible | + When I access endpoint "authorized" using HTTP POST method + """ + {"placeholder":"abc"} + """ + Then The status code of the response is 403 + And The body of the response contains "Missing required entitlement" + + Scenario: Request fails when user has no entitlements + Given The system is in default state + And I set the x-rh-identity header with JSON + """ + { + "identity": { + "type": "User", + "org_id": "321", + "user": {"user_id": "test-user-123", "username": "testuser@redhat.com"} + }, + "entitlements": {} + } + """ + When I access endpoint "authorized" using HTTP POST method + """ + {"placeholder":"abc"} + """ + Then The status code of the response is 403 + And The body of the response contains "Missing required entitlement" + + Scenario: Request fails when entitlement exists but is_entitled is false + Given The system is in default state + And I set the x-rh-identity header with JSON + """ + { + "identity": { + "type": "User", + "org_id": "321", + "user": {"user_id": "test-user-123", "username": "testuser@redhat.com"} + }, + "entitlements": {"rhel": {"is_entitled": false, "is_trial": true}} + } + """ + When I access endpoint "authorized" using HTTP POST method + """ + {"placeholder":"abc"} + """ + Then The status code of the response is 403 + And The body of the response contains "Missing required entitlement" + + Scenario: Request fails when User identity is missing user field + Given The system is in default state + And I set the x-rh-identity header with JSON + """ + { + "identity": { + "type": "User", + "org_id": "321" + }, + "entitlements": {"rhel": {"is_entitled": true}} + } + """ + When I access endpoint "authorized" using HTTP POST method + """ + {"placeholder":"abc"} + """ + Then The status code of the response is 400 + And The body of the response contains "Missing 'user' field for User type" + + Scenario: Request fails when User identity is missing user_id + Given The system is in default state + And I set the x-rh-identity header with JSON + """ + { + "identity": { + "type": "User", + "org_id": "321", + "user": {"username": "testuser@redhat.com"} + }, + "entitlements": {"rhel": {"is_entitled": true}} + } + """ + When I access endpoint "authorized" using HTTP POST method + """ + {"placeholder":"abc"} + """ + Then The status code of the response is 400 + And The body of the response contains "Missing 'user_id' in user data" + + Scenario: Request fails when User identity is missing username + Given The system is in default state + And I set the x-rh-identity header with JSON + """ + { + "identity": { + "type": "User", + "org_id": "321", + "user": {"user_id": "test-user-123"} + }, + "entitlements": {"rhel": {"is_entitled": true}} + } + """ + When I access endpoint "authorized" using HTTP POST method + """ + {"placeholder":"abc"} + """ + Then The status code of the response is 400 + And The body of the response contains "Missing 'username' in user data" + + Scenario: Request fails when System identity is missing system field + Given The system is in default state + And I set the x-rh-identity header with JSON + """ + { + "identity": { + "type": "System", + "account_number": "456", + "org_id": "654" + }, + "entitlements": {"rhel": {"is_entitled": true}} + } + """ + When I access endpoint "authorized" using HTTP POST method + """ + {"placeholder":"abc"} + """ + Then The status code of the response is 400 + And The body of the response contains "Missing 'system' field for System type" + + Scenario: Request fails when System identity is missing cn + Given The system is in default state + And I set the x-rh-identity header with JSON + """ + { + "identity": { + "type": "System", + "account_number": "456", + "org_id": "654", + "system": {} + }, + "entitlements": {"rhel": {"is_entitled": true}} + } + """ + When I access endpoint "authorized" using HTTP POST method + """ + {"placeholder":"abc"} + """ + Then The status code of the response is 400 + And The body of the response contains "Missing 'cn' in system data" + + Scenario: Request fails when System identity is missing account_number + Given The system is in default state + And I set the x-rh-identity header with JSON + """ + { + "identity": { + "type": "System", + "org_id": "654", + "system": {"cn": "c87dcb4c-8af1-40dd-878e-60c744edddd0"} + }, + "entitlements": {"rhel": {"is_entitled": true}} + } + """ + When I access endpoint "authorized" using HTTP POST method + """ + {"placeholder":"abc"} + """ + Then The status code of the response is 400 + And The body of the response contains "Missing 'account_number' for System type" diff --git a/tests/e2e/test_list.txt b/tests/e2e/test_list.txt index ebd39760..804e180c 100644 --- a/tests/e2e/test_list.txt +++ b/tests/e2e/test_list.txt @@ -2,6 +2,7 @@ features/faiss.feature features/smoketests.feature features/authorized_noop.feature features/authorized_noop_token.feature +features/authorized_rh_identity.feature features/rbac.feature features/conversations.feature features/conversation_cache_v2.feature From 686d53b70a629d3bbdfed946bb97c1fd5a36d259 Mon Sep 17 00:00:00 2001 From: Major Hayden Date: Thu, 22 Jan 2026 17:22:47 -0600 Subject: [PATCH 5/5] fix(e2e): remove extra quotes from substring assertions in rh-identity tests The behave step 'body of the response contains {substring}' captures the literal text after 'contains', including any quotes. The test assertions were failing because they searched for quoted substrings like '"Invalid base64 encoding"' instead of unquoted text that actually exists in the JSON response body. Fixes 6 failing test scenarios: - Invalid base64 encoding detection - Invalid JSON detection - Unsupported identity type detection - Missing required entitlement detection (3 scenarios) Signed-off-by: Major Hayden --- tests/e2e/features/authorized_rh_identity.feature | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/e2e/features/authorized_rh_identity.feature b/tests/e2e/features/authorized_rh_identity.feature index 6e44e020..957f4147 100644 --- a/tests/e2e/features/authorized_rh_identity.feature +++ b/tests/e2e/features/authorized_rh_identity.feature @@ -25,7 +25,7 @@ Feature: Authorized endpoint API tests for the rh-identity authentication module {"placeholder":"abc"} """ Then The status code of the response is 400 - And The body of the response contains "Invalid base64 encoding" + And The body of the response contains Invalid base64 encoding Scenario: Request fails when x-rh-identity header has invalid JSON Given The system is in default state @@ -35,7 +35,7 @@ Feature: Authorized endpoint API tests for the rh-identity authentication module {"placeholder":"abc"} """ Then The status code of the response is 400 - And The body of the response contains "Invalid JSON" + And The body of the response contains Invalid JSON Scenario: Request fails when identity field is missing Given The system is in default state @@ -87,7 +87,7 @@ Feature: Authorized endpoint API tests for the rh-identity authentication module {"placeholder":"abc"} """ Then The status code of the response is 400 - And The body of the response contains "Unsupported identity type" + And The body of the response contains Unsupported identity type Scenario: Request succeeds with valid User identity and required entitlements Given The system is in default state @@ -130,7 +130,7 @@ Feature: Authorized endpoint API tests for the rh-identity authentication module {"placeholder":"abc"} """ Then The status code of the response is 403 - And The body of the response contains "Missing required entitlement" + And The body of the response contains Missing required entitlement Scenario: Request fails when user has no entitlements Given The system is in default state @@ -150,7 +150,7 @@ Feature: Authorized endpoint API tests for the rh-identity authentication module {"placeholder":"abc"} """ Then The status code of the response is 403 - And The body of the response contains "Missing required entitlement" + And The body of the response contains Missing required entitlement Scenario: Request fails when entitlement exists but is_entitled is false Given The system is in default state @@ -170,7 +170,7 @@ Feature: Authorized endpoint API tests for the rh-identity authentication module {"placeholder":"abc"} """ Then The status code of the response is 403 - And The body of the response contains "Missing required entitlement" + And The body of the response contains Missing required entitlement Scenario: Request fails when User identity is missing user field Given The system is in default state