Skip to content

Commit ba2030c

Browse files
authored
Merge pull request #1045 from major/e2e-rh-identity-auth-tests
test(e2e): add comprehensive e2e tests for rh-identity authentication
2 parents 596591c + 8ca8553 commit ba2030c

6 files changed

Lines changed: 343 additions & 0 deletions

File tree

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
name: Lightspeed Core Service (LCS) - RH Identity Auth
2+
service:
3+
host: 0.0.0.0
4+
port: 8080
5+
auth_enabled: true
6+
workers: 1
7+
color_log: true
8+
access_log: true
9+
llama_stack:
10+
use_as_library_client: true
11+
library_client_config_path: run.yaml
12+
user_data_collection:
13+
feedback_enabled: true
14+
feedback_storage: "/tmp/data/feedback"
15+
transcripts_enabled: true
16+
transcripts_storage: "/tmp/data/transcripts"
17+
conversation_cache:
18+
type: "sqlite"
19+
sqlite:
20+
db_path: "/tmp/data/conversation-cache.db"
21+
authentication:
22+
module: "rh-identity"
23+
rh_identity_config:
24+
required_entitlements: ["rhel"]
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: Lightspeed Core Service (LCS) - RH Identity Auth
2+
service:
3+
host: 0.0.0.0
4+
port: 8080
5+
auth_enabled: true
6+
workers: 1
7+
color_log: true
8+
access_log: true
9+
llama_stack:
10+
use_as_library_client: false
11+
url: http://llama-stack:8321
12+
api_key: xyzzy
13+
user_data_collection:
14+
feedback_enabled: true
15+
feedback_storage: "/tmp/data/feedback"
16+
transcripts_enabled: true
17+
transcripts_storage: "/tmp/data/transcripts"
18+
conversation_cache:
19+
type: "sqlite"
20+
sqlite:
21+
db_path: "/tmp/data/conversation-cache.db"
22+
authentication:
23+
module: "rh-identity"
24+
rh_identity_config:
25+
required_entitlements: ["rhel"]
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
@RHIdentity
2+
Feature: Authorized endpoint API tests for the rh-identity authentication module
3+
4+
Background:
5+
Given The service is started locally
6+
And REST API service prefix is /v1
7+
8+
Scenario: Request fails when x-rh-identity header is missing
9+
Given The system is in default state
10+
When I access endpoint "authorized" using HTTP POST method
11+
"""
12+
{"placeholder":"abc"}
13+
"""
14+
Then The status code of the response is 401
15+
And The body of the response is the following
16+
"""
17+
{"detail": "Missing x-rh-identity header"}
18+
"""
19+
20+
Scenario: Request fails when identity field is missing
21+
Given The system is in default state
22+
And I set the x-rh-identity header with JSON
23+
"""
24+
{"entitlements": {"rhel": {"is_entitled": true}}}
25+
"""
26+
When I access endpoint "authorized" using HTTP POST method
27+
"""
28+
{"placeholder":"abc"}
29+
"""
30+
Then The status code of the response is 400
31+
And The body of the response contains Missing 'identity' field
32+
33+
Scenario: Request succeeds with valid User identity and required entitlements
34+
Given The system is in default state
35+
And I set the x-rh-identity header with valid User identity
36+
| field | value |
37+
| user_id | test-user-123 |
38+
| username | testuser@redhat.com |
39+
| org_id | 321 |
40+
| entitlements | rhel |
41+
When I access endpoint "authorized" using HTTP POST method
42+
"""
43+
{"placeholder":"abc"}
44+
"""
45+
Then The status code of the response is 200
46+
And The body of the response is the following
47+
"""
48+
{"user_id": "test-user-123", "username": "testuser@redhat.com", "skip_userid_check": false}
49+
"""
50+
51+
Scenario: Request succeeds with valid System identity and required entitlements
52+
Given The system is in default state
53+
And I set the x-rh-identity header with valid System identity
54+
| field | value |
55+
| cn | c87dcb4c-8af1-40dd-878e-60c744edddd0 |
56+
| account_number | 456 |
57+
| org_id | 654 |
58+
| entitlements | rhel |
59+
When I access endpoint "authorized" using HTTP POST method
60+
"""
61+
{"placeholder":"abc"}
62+
"""
63+
Then The status code of the response is 200
64+
And The body of the response is the following
65+
"""
66+
{"user_id": "c87dcb4c-8af1-40dd-878e-60c744edddd0", "username": "456", "skip_userid_check": false}
67+
"""
68+
69+
Scenario: Request fails when required entitlement is missing
70+
Given The system is in default state
71+
And I set the x-rh-identity header with valid User identity
72+
| field | value |
73+
| user_id | test-user-123 |
74+
| username | testuser@redhat.com |
75+
| org_id | 321 |
76+
| entitlements | ansible |
77+
When I access endpoint "authorized" using HTTP POST method
78+
"""
79+
{"placeholder":"abc"}
80+
"""
81+
Then The status code of the response is 403
82+
And The body of the response contains Missing required entitlement
83+
84+
Scenario: Request fails when entitlement exists but is_entitled is false
85+
Given The system is in default state
86+
And I set the x-rh-identity header with JSON
87+
"""
88+
{
89+
"identity": {
90+
"type": "User",
91+
"org_id": "321",
92+
"user": {"user_id": "test-user-123", "username": "testuser@redhat.com"}
93+
},
94+
"entitlements": {"rhel": {"is_entitled": false, "is_trial": true}}
95+
}
96+
"""
97+
When I access endpoint "authorized" using HTTP POST method
98+
"""
99+
{"placeholder":"abc"}
100+
"""
101+
Then The status code of the response is 403
102+
And The body of the response contains Missing required entitlement
103+
104+
Scenario: Request fails when User identity is missing user_id
105+
Given The system is in default state
106+
And I set the x-rh-identity header with JSON
107+
"""
108+
{
109+
"identity": {
110+
"type": "User",
111+
"org_id": "321",
112+
"user": {"username": "testuser@redhat.com"}
113+
},
114+
"entitlements": {"rhel": {"is_entitled": true}}
115+
}
116+
"""
117+
When I access endpoint "authorized" using HTTP POST method
118+
"""
119+
{"placeholder":"abc"}
120+
"""
121+
Then The status code of the response is 400
122+
And The body of the response contains Missing 'user_id' in user data
123+
124+
Scenario: Request fails when User identity is missing username
125+
Given The system is in default state
126+
And I set the x-rh-identity header with JSON
127+
"""
128+
{
129+
"identity": {
130+
"type": "User",
131+
"org_id": "321",
132+
"user": {"user_id": "test-user-123"}
133+
},
134+
"entitlements": {"rhel": {"is_entitled": true}}
135+
}
136+
"""
137+
When I access endpoint "authorized" using HTTP POST method
138+
"""
139+
{"placeholder":"abc"}
140+
"""
141+
Then The status code of the response is 400
142+
And The body of the response contains Missing 'username' in user data
143+
144+
Scenario: Request fails when System identity is missing cn
145+
Given The system is in default state
146+
And I set the x-rh-identity header with JSON
147+
"""
148+
{
149+
"identity": {
150+
"type": "System",
151+
"account_number": "456",
152+
"org_id": "654",
153+
"system": {}
154+
},
155+
"entitlements": {"rhel": {"is_entitled": true}}
156+
}
157+
"""
158+
When I access endpoint "authorized" using HTTP POST method
159+
"""
160+
{"placeholder":"abc"}
161+
"""
162+
Then The status code of the response is 400
163+
And The body of the response contains Missing 'cn' in system data

tests/e2e/features/environment.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,15 @@ def before_feature(context: Context, feature: Feature) -> None:
251251
switch_config(context.feature_config)
252252
restart_container("lightspeed-stack")
253253

254+
if "RHIdentity" in feature.tags:
255+
mode_dir = "library-mode" if context.is_library_mode else "server-mode"
256+
context.feature_config = (
257+
f"tests/e2e/configuration/{mode_dir}/lightspeed-stack-auth-rh-identity.yaml"
258+
)
259+
context.default_config_backup = create_config_backup("lightspeed-stack.yaml")
260+
switch_config(context.feature_config)
261+
restart_container("lightspeed-stack")
262+
254263
if "Feedback" in feature.tags:
255264
context.hostname = os.getenv("E2E_LSC_HOSTNAME", "localhost")
256265
context.port = os.getenv("E2E_LSC_PORT", "8080")
@@ -273,6 +282,11 @@ def after_feature(context: Context, feature: Feature) -> None:
273282
restart_container("lightspeed-stack")
274283
remove_config_backup(context.default_config_backup)
275284

285+
if "RHIdentity" in feature.tags:
286+
switch_config(context.default_config_backup)
287+
restart_container("lightspeed-stack")
288+
remove_config_backup(context.default_config_backup)
289+
276290
if "Feedback" in feature.tags:
277291
for conversation_id in context.feedback_conversations:
278292
url = f"http://{context.hostname}:{context.port}/v1/conversations/{conversation_id}"

tests/e2e/features/steps/auth.py

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,27 @@
11
"""Implementation of common test steps."""
22

3+
import base64
4+
import json
5+
36
import requests
47
from behave import given, when # pyright: ignore[reportAttributeAccessIssue]
58
from behave.runner import Context
69
from tests.e2e.utils.utils import normalize_endpoint
710

811

12+
def _encode_rh_identity(identity_data: dict) -> str:
13+
"""Encode identity dict to base64 for x-rh-identity header.
14+
15+
Args:
16+
identity_data: JSON-serializable identity payload to encode.
17+
18+
Returns:
19+
Base64-encoded UTF-8 string representation of the JSON payload.
20+
"""
21+
json_str = json.dumps(identity_data)
22+
return base64.b64encode(json_str.encode("utf-8")).decode("utf-8")
23+
24+
925
@given("I set the Authorization header to {header_value}")
1026
def set_authorization_header_custom(context: Context, header_value: str) -> None:
1127
"""Set a custom Authorization header value.
@@ -73,3 +89,103 @@ def access_rest_api_endpoint_post_without_param(
7389
context.response = requests.post(
7490
url, json="", headers=context.auth_headers, timeout=10
7591
)
92+
93+
94+
@given('I set the x-rh-identity header to raw value "{header_value}"')
95+
def set_rh_identity_header_raw(context: Context, header_value: str) -> None:
96+
"""Set x-rh-identity header with a raw string value for testing invalid base64."""
97+
if not hasattr(context, "auth_headers"):
98+
context.auth_headers = {}
99+
context.auth_headers["x-rh-identity"] = header_value
100+
print(f"Set x-rh-identity header to raw value: {header_value[:50]}...")
101+
102+
103+
@given('I set the x-rh-identity header with base64 encoded value "{raw_value}"')
104+
def set_rh_identity_header_base64_raw(context: Context, raw_value: str) -> None:
105+
"""Set x-rh-identity header with base64-encoded raw string for testing invalid JSON."""
106+
if not hasattr(context, "auth_headers"):
107+
context.auth_headers = {}
108+
encoded = base64.b64encode(raw_value.encode("utf-8")).decode("utf-8")
109+
context.auth_headers["x-rh-identity"] = encoded
110+
print(f"Set x-rh-identity header with base64-encoded: {raw_value}")
111+
112+
113+
@given("I set the x-rh-identity header with JSON")
114+
def set_rh_identity_header_json(context: Context) -> None:
115+
"""Set x-rh-identity header with base64-encoded JSON from context.text."""
116+
if not hasattr(context, "auth_headers"):
117+
context.auth_headers = {}
118+
assert context.text is not None, "JSON payload required"
119+
identity_data = json.loads(context.text)
120+
context.auth_headers["x-rh-identity"] = _encode_rh_identity(identity_data)
121+
print(f"Set x-rh-identity header with JSON: {identity_data}")
122+
123+
124+
@given("I set the x-rh-identity header with valid User identity")
125+
def set_rh_identity_user(context: Context) -> None:
126+
"""Set x-rh-identity header with User identity from table."""
127+
if not hasattr(context, "auth_headers"):
128+
context.auth_headers = {}
129+
130+
assert context.table is not None, "Table with identity fields required"
131+
132+
fields = {row["field"]: row["value"] for row in context.table}
133+
134+
entitlements = {}
135+
if "entitlements" in fields:
136+
for ent in fields["entitlements"].split(","):
137+
ent = ent.strip()
138+
if not ent:
139+
continue
140+
entitlements[ent] = {"is_entitled": True, "is_trial": False}
141+
142+
identity_data = {
143+
"identity": {
144+
"account_number": fields.get("account_number", "123"),
145+
"org_id": fields.get("org_id", "321"),
146+
"type": "User",
147+
"user": {
148+
"user_id": fields.get("user_id", "test-user"),
149+
"username": fields.get("username", "test@redhat.com"),
150+
"is_org_admin": fields.get("is_org_admin", "false").lower() == "true",
151+
},
152+
},
153+
"entitlements": entitlements,
154+
}
155+
156+
context.auth_headers["x-rh-identity"] = _encode_rh_identity(identity_data)
157+
print(f"Set x-rh-identity header with User identity: {fields.get('user_id')}")
158+
159+
160+
@given("I set the x-rh-identity header with valid System identity")
161+
def set_rh_identity_system(context: Context) -> None:
162+
"""Set x-rh-identity header with System identity from table."""
163+
if not hasattr(context, "auth_headers"):
164+
context.auth_headers = {}
165+
166+
assert context.table is not None, "Table with identity fields required"
167+
168+
fields = {row["field"]: row["value"] for row in context.table}
169+
170+
entitlements = {}
171+
if "entitlements" in fields:
172+
for ent in fields["entitlements"].split(","):
173+
ent = ent.strip()
174+
if not ent:
175+
continue
176+
entitlements[ent] = {"is_entitled": True, "is_trial": False}
177+
178+
identity_data = {
179+
"identity": {
180+
"account_number": fields.get("account_number", "123"),
181+
"org_id": fields.get("org_id", "321"),
182+
"type": "System",
183+
"system": {
184+
"cn": fields.get("cn", "default-cn-uuid"),
185+
},
186+
},
187+
"entitlements": entitlements,
188+
}
189+
190+
context.auth_headers["x-rh-identity"] = _encode_rh_identity(identity_data)
191+
print(f"Set x-rh-identity header with System identity: {fields.get('cn')}")

tests/e2e/test_list.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ features/faiss.feature
22
features/smoketests.feature
33
features/authorized_noop.feature
44
features/authorized_noop_token.feature
5+
features/authorized_rh_identity.feature
56
features/rbac.feature
67
features/conversations.feature
78
features/conversation_cache_v2.feature

0 commit comments

Comments
 (0)