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"] diff --git a/tests/e2e/features/authorized_rh_identity.feature b/tests/e2e/features/authorized_rh_identity.feature new file mode 100644 index 00000000..2bc97c27 --- /dev/null +++ b/tests/e2e/features/authorized_rh_identity.feature @@ -0,0 +1,163 @@ +@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 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 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 + And The body of the response is the following + """ + {"user_id": "test-user-123", "username": "testuser@redhat.com", "skip_userid_check": false} + """ + + 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 + And The body of the response is the following + """ + {"user_id": "c87dcb4c-8af1-40dd-878e-60c744edddd0", "username": "456", "skip_userid_check": false} + """ + + 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 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_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 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 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}" diff --git a/tests/e2e/features/steps/auth.py b/tests/e2e/features/steps/auth.py index e9360f51..fcd54395 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,103 @@ 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() + if not ent: + continue + 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() + if not ent: + continue + 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')}") 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