diff --git a/docker-compose-library.yaml b/docker-compose-library.yaml index f38fb195..d9880bfa 100644 --- a/docker-compose-library.yaml +++ b/docker-compose-library.yaml @@ -44,6 +44,8 @@ services: - WATSONX_API_KEY=${WATSONX_API_KEY:-} # Enable debug logging if needed - LLAMA_STACK_LOGGING=${LLAMA_STACK_LOGGING:-} + networks: + - lightspeednet healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/liveness"] interval: 10s # how often to run the check @@ -51,3 +53,23 @@ services: retries: 3 # how many times to retry before marking as unhealthy start_period: 15s # time to wait before starting checks (increased for library initialization) + # Mock JWKS server for RBAC E2E tests + mock-jwks: + build: + context: ./tests/e2e/mock_jwks_server + dockerfile: Dockerfile + container_name: mock-jwks + ports: + - "8000:8000" + networks: + - lightspeednet + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"] + interval: 5s + timeout: 3s + retries: 3 + start_period: 2s + +networks: + lightspeednet: + driver: bridge \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 86c7dc4d..55a20555 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -9,10 +9,11 @@ services: ports: - "8321:8321" # Expose llama-stack on 8321 (adjust if needed) volumes: - - ./run.yaml:/opt/app-root/run.yaml:Z + - ./run.yaml:/opt/app-root/run.yaml:z - ${GCP_KEYS_PATH:-./tmp/.gcp-keys-dummy}:/opt/app-root/.gcp-keys:ro - - ./tests/e2e/rag:/opt/app-root/src/.llama/storage/rag:Z - - ./lightspeed-stack.yaml:/opt/app-root/lightspeed-stack.yaml:z + - ./lightspeed-stack.yaml:/opt/app-root/lightspeed-stack.yaml:ro + - llama-storage:/opt/app-root/src/.llama/storage + - ./tests/e2e/rag:/opt/app-root/src/.llama/storage/rag:z environment: - BRAVE_SEARCH_API_KEY=${BRAVE_SEARCH_API_KEY:-} - TAVILY_SEARCH_API_KEY=${TAVILY_SEARCH_API_KEY:-} @@ -78,6 +79,26 @@ services: retries: 3 # how many times to retry before marking as unhealthy start_period: 5s # time to wait before starting checks + # Mock JWKS server for RBAC E2E tests + mock-jwks: + build: + context: ./tests/e2e/mock_jwks_server + dockerfile: Dockerfile + container_name: mock-jwks + ports: + - "8000:8000" + networks: + - lightspeednet + healthcheck: + test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"] + interval: 5s + timeout: 3s + retries: 3 + start_period: 2s + +volumes: + llama-storage: + networks: lightspeednet: driver: bridge diff --git a/tests/e2e/configuration/library-mode/lightspeed-stack-rbac.yaml b/tests/e2e/configuration/library-mode/lightspeed-stack-rbac.yaml new file mode 100644 index 00000000..b8aacaf1 --- /dev/null +++ b/tests/e2e/configuration/library-mode/lightspeed-stack-rbac.yaml @@ -0,0 +1,94 @@ +name: Lightspeed Core Service (RBAC E2E Tests - Library Mode) +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 for storing Q&A history +conversation_cache: + type: "sqlite" + sqlite: + db_path: "/tmp/data/conversation-cache.db" + +# JWK token authentication with role extraction +authentication: + module: "jwk-token" + jwk_config: + url: "http://mock-jwks:8000/.well-known/jwks.json" + jwt_configuration: + user_id_claim: "sub" + username_claim: "name" + # Role rules: extract roles from JWT claims + role_rules: + # Grant 'admin' role to users with admin=true in JWT + - jsonpath: "$.admin" + operator: "equals" + value: [true] + roles: ["admin"] + # Grant 'user' role to users with role=user in JWT + - jsonpath: "$.role" + operator: "equals" + value: ["user"] + roles: ["user"] + # Grant 'viewer' role to users with role=viewer in JWT + - jsonpath: "$.role" + operator: "equals" + value: ["viewer"] + roles: ["viewer"] + # Grant 'query_only' role based on permissions array containing 'query' + - jsonpath: "$.permissions[*]" + operator: "contains" + value: "query" + roles: ["query_only"] + +# Authorization: map roles to actions +authorization: + access_rules: + # Admin role gets full access + - role: "admin" + actions: ["admin"] + # User role can query, access conversations, and provide feedback + - role: "user" + actions: + - "query" + - "streaming_query" + - "get_conversation" + - "list_conversations" + - "delete_conversation" + - "update_conversation" + - "feedback" + - "get_models" + - "get_tools" + - "info" + - "model_override" + # Viewer role can only read (no mutations) + - role: "viewer" + actions: + - "get_conversation" + - "list_conversations" + - "get_models" + - "get_tools" + - "info" + # Query-only role can only query (no model_override - must use defaults) + - role: "query_only" + actions: + - "query" + - "streaming_query" + # Everyone (*) role gets basic info access + - role: "*" + actions: + - "info" + diff --git a/tests/e2e/configuration/server-mode/lightspeed-stack-rbac.yaml b/tests/e2e/configuration/server-mode/lightspeed-stack-rbac.yaml new file mode 100644 index 00000000..7fd95395 --- /dev/null +++ b/tests/e2e/configuration/server-mode/lightspeed-stack-rbac.yaml @@ -0,0 +1,95 @@ +name: Lightspeed Core Service (RBAC E2E Tests) +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 for storing Q&A history +conversation_cache: + type: "sqlite" + sqlite: + db_path: "/tmp/data/conversation-cache.db" + +# JWK token authentication with role extraction +authentication: + module: "jwk-token" + jwk_config: + url: "http://mock-jwks:8000/.well-known/jwks.json" + jwt_configuration: + user_id_claim: "sub" + username_claim: "name" + # Role rules: extract roles from JWT claims + role_rules: + # Grant 'admin' role to users with admin=true in JWT + - jsonpath: "$.admin" + operator: "equals" + value: [true] + roles: ["admin"] + # Grant 'user' role to users with role=user in JWT + - jsonpath: "$.role" + operator: "equals" + value: ["user"] + roles: ["user"] + # Grant 'viewer' role to users with role=viewer in JWT + - jsonpath: "$.role" + operator: "equals" + value: ["viewer"] + roles: ["viewer"] + # Grant 'query_only' role based on permissions array containing 'query' + - jsonpath: "$.permissions[*]" + operator: "contains" + value: "query" + roles: ["query_only"] + +# Authorization: map roles to actions +authorization: + access_rules: + # Admin role gets full access + - role: "admin" + actions: ["admin"] + # User role can query, access conversations, and provide feedback + - role: "user" + actions: + - "query" + - "streaming_query" + - "get_conversation" + - "list_conversations" + - "delete_conversation" + - "update_conversation" + - "feedback" + - "get_models" + - "get_tools" + - "info" + - "model_override" + # Viewer role can only read (no mutations) + - role: "viewer" + actions: + - "get_conversation" + - "list_conversations" + - "get_models" + - "get_tools" + - "info" + # Query-only role can only query (no model_override - must use defaults) + - role: "query_only" + actions: + - "query" + - "streaming_query" + # Everyone (*) role gets basic info access + - role: "*" + actions: + - "info" + diff --git a/tests/e2e/features/environment.py b/tests/e2e/features/environment.py index 9ad5d71b..ce40a395 100644 --- a/tests/e2e/features/environment.py +++ b/tests/e2e/features/environment.py @@ -242,6 +242,15 @@ def before_feature(context: Context, feature: Feature) -> None: switch_config(context.feature_config) restart_container("lightspeed-stack") + if "RBAC" 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-rbac.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") @@ -259,6 +268,11 @@ def after_feature(context: Context, feature: Feature) -> None: restart_container("lightspeed-stack") remove_config_backup(context.default_config_backup) + if "RBAC" 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/info.feature b/tests/e2e/features/info.feature index df7e30de..0f35f3f6 100644 --- a/tests/e2e/features/info.feature +++ b/tests/e2e/features/info.feature @@ -65,6 +65,8 @@ Feature: Info tests {"detail": {"response": "Unable to connect to Llama Stack", "cause": "Connection error."}} """ + #https://issues.redhat.com/browse/LCORE-1211 + @skip Scenario: Check if tools endpoint is working Given The system is in default state When I access REST API endpoint "tools" using HTTP GET method diff --git a/tests/e2e/features/query.feature b/tests/e2e/features/query.feature index fead7abc..179f317c 100644 --- a/tests/e2e/features/query.feature +++ b/tests/e2e/features/query.feature @@ -29,6 +29,8 @@ Feature: Query endpoint API tests | Fragments in LLM response | | checkout | + #enable on demand + @skip Scenario: Check if LLM ignores new system prompt in same conversation Given The system is in default state And I set the Authorization header to Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva diff --git a/tests/e2e/features/rbac.feature b/tests/e2e/features/rbac.feature new file mode 100644 index 00000000..b63ec69c --- /dev/null +++ b/tests/e2e/features/rbac.feature @@ -0,0 +1,155 @@ +@RBAC +Feature: Role-Based Access Control (RBAC) + + Comprehensive tests for role-based access control to ensure + authentication and authorization work correctly. + + Background: + Given The service is started locally + And REST API service prefix is /v1 + + # ============================================ + # Authentication - Token Validation + # ============================================ + + #https://issues.redhat.com/browse/LCORE-1210 + @skip + Scenario: Request without token returns 401 + Given The system is in default state + And I remove the auth header + When I access REST API endpoint "models" using HTTP GET method + Then The status code of the response is 401 + And The body of the response contains Missing or invalid credentials + + Scenario: Request with malformed Authorization header returns 401 + Given The system is in default state + And I set the Authorization header to NotBearer sometoken + When I access REST API endpoint "models" using HTTP GET method + Then The status code of the response is 401 + + # ============================================ + # Admin Role - Full Access + # ============================================ + + Scenario: Admin can access query endpoint + Given The system is in default state + And I authenticate as "admin" user + When I use "query" to ask question with authorization header + """ + {"query": "Say hi", "model": "{MODEL}", "provider": "{PROVIDER}"} + """ + Then The status code of the response is 200 + + Scenario: Admin can access models endpoint + Given The system is in default state + And I authenticate as "admin" user + When I access REST API endpoint "models" using HTTP GET method + Then The status code of the response is 200 + + Scenario: Admin can list conversations + Given The system is in default state + And I authenticate as "admin" user + When I access REST API endpoint "conversations" using HTTP GET method + Then The status code of the response is 200 + + # ============================================ + # User Role - Standard Access + # ============================================ + + Scenario: User can access query endpoint + Given The system is in default state + And I authenticate as "user" user + When I use "query" to ask question with authorization header + """ + {"query": "Say hi", "model": "{MODEL}", "provider": "{PROVIDER}"} + """ + Then The status code of the response is 200 + + Scenario: User can list conversations + Given The system is in default state + And I authenticate as "user" user + When I access REST API endpoint "conversations" using HTTP GET method + Then The status code of the response is 200 + + # ============================================ + # Viewer Role - Read Only + # ============================================ + + Scenario: Viewer can list conversations + Given The system is in default state + And I authenticate as "viewer" user + When I access REST API endpoint "conversations" using HTTP GET method + Then The status code of the response is 200 + + Scenario: Viewer can access info endpoint + Given The system is in default state + And I authenticate as "viewer" user + When I access REST API endpoint "info" using HTTP GET method + Then The status code of the response is 200 + + Scenario: Viewer cannot query - returns 403 + Given The system is in default state + And I authenticate as "viewer" user + When I use "query" to ask question with authorization header + """ + {"query": "Say hi", "model": "{MODEL}", "provider": "{PROVIDER}"} + """ + Then The status code of the response is 403 + And The body of the response contains does not have permission + + # ============================================ + # Query-Only Role - Limited Access (no model_override) + # ============================================ + + Scenario: Query-only user can query without specifying model + Given The system is in default state + And I authenticate as "query_only" user + When I use "query" to ask question with authorization header + """ + {"query": "Say hi"} + """ + Then The status code of the response is 200 + + Scenario: Query-only user cannot override model - returns 403 + Given The system is in default state + And I authenticate as "query_only" user + When I use "query" to ask question with authorization header + """ + {"query": "Say hi", "model": "{MODEL}", "provider": "{PROVIDER}"} + """ + Then The status code of the response is 403 + And The body of the response contains model_override + + Scenario: Query-only user cannot list conversations - returns 403 + Given The system is in default state + And I authenticate as "query_only" user + When I access REST API endpoint "conversations" using HTTP GET method + Then The status code of the response is 403 + And The body of the response contains does not have permission + + # ============================================ + # No Role - Minimal Access (everyone role only) + # ============================================ + + Scenario: No-role user can access info endpoint (everyone role) + Given The system is in default state + And I authenticate as "no_role" user + When I access REST API endpoint "info" using HTTP GET method + Then The status code of the response is 200 + + Scenario: No-role user cannot query - returns 403 + Given The system is in default state + And I authenticate as "no_role" user + When I use "query" to ask question with authorization header + """ + {"query": "Say hi", "model": "{MODEL}", "provider": "{PROVIDER}"} + """ + Then The status code of the response is 403 + And The body of the response contains does not have permission + + Scenario: No-role user cannot list conversations - returns 403 + Given The system is in default state + And I authenticate as "no_role" user + When I access REST API endpoint "conversations" using HTTP GET method + Then The status code of the response is 403 + And The body of the response contains does not have permission diff --git a/tests/e2e/features/steps/rbac.py b/tests/e2e/features/steps/rbac.py new file mode 100644 index 00000000..babaa0be --- /dev/null +++ b/tests/e2e/features/steps/rbac.py @@ -0,0 +1,40 @@ +"""Step definitions for RBAC E2E tests.""" + +import os +import requests +from behave import given # pyright: ignore[reportAttributeAccessIssue] +from behave.runner import Context + + +def get_test_tokens() -> dict[str, str]: + """Fetch test tokens from the mock JWKS server.""" + jwks_host = os.getenv("E2E_JWKS_HOSTNAME", "localhost") + jwks_port = os.getenv("E2E_JWKS_PORT", "8000") + tokens_url = f"http://{jwks_host}:{jwks_port}/tokens" + + response = requests.get(tokens_url, timeout=5) + response.raise_for_status() + return response.json() + + +@given('I authenticate as "{role}" user') +def authenticate_as_role(context: Context, role: str) -> None: + """Set the Authorization header with a token for the specified role. + + Fetches pre-generated test tokens from the mock JWKS server + and sets the appropriate Authorization header for the given role. + + Available roles: admin, user, viewer, query_only, no_role + """ + tokens = get_test_tokens() + + if role not in tokens: + raise ValueError( + f"Unknown role '{role}'. Available roles: {list(tokens.keys())}" + ) + + if not hasattr(context, "auth_headers"): + context.auth_headers = {} + + context.auth_headers["Authorization"] = f"Bearer {tokens[role]}" + print(f"🔑 Authenticated as '{role}' user") diff --git a/tests/e2e/features/streaming_query.feature b/tests/e2e/features/streaming_query.feature index 58bd3ff7..fe2bcf61 100644 --- a/tests/e2e/features/streaming_query.feature +++ b/tests/e2e/features/streaming_query.feature @@ -43,6 +43,8 @@ Feature: streaming_query endpoint API tests | Fragments in LLM response | | checkout | + #enable on demand + @skip Scenario: Check if LLM ignores new system prompt in same conversation Given The system is in default state And I set the Authorization header to Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6Ikpva diff --git a/tests/e2e/mock_jwks_server/Dockerfile b/tests/e2e/mock_jwks_server/Dockerfile new file mode 100644 index 00000000..ee2bef7d --- /dev/null +++ b/tests/e2e/mock_jwks_server/Dockerfile @@ -0,0 +1,5 @@ +FROM python:3.12-slim +WORKDIR /app +COPY server.py . +EXPOSE 8000 +CMD ["python", "server.py"] diff --git a/tests/e2e/mock_jwks_server/generate_tokens.py b/tests/e2e/mock_jwks_server/generate_tokens.py new file mode 100644 index 00000000..f373cad9 --- /dev/null +++ b/tests/e2e/mock_jwks_server/generate_tokens.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +"""One-time script to generate JWKS and test tokens. + +Run this once to generate the static values for server.py. +Requires: pip install cryptography PyJWT +""" + +import base64 +import json +import time + +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.backends import default_backend +import jwt + +# Generate RSA key pair +private_key = rsa.generate_private_key( + public_exponent=65537, key_size=2048, backend=default_backend() +) +public_key = private_key.public_key() + +# Convert to PEM for JWT signing +private_pem = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption(), +) + +# Create JWK from public key +public_numbers = public_key.public_numbers() + + +def int_to_base64url(n: int, length: int) -> str: + """Convert an integer to base64url-encoded string.""" + return base64.urlsafe_b64encode(n.to_bytes(length, "big")).rstrip(b"=").decode() + + +jwk = { + "kty": "RSA", + "kid": "test-key-1", + "use": "sig", + "alg": "RS256", + "n": int_to_base64url(public_numbers.n, 256), + "e": int_to_base64url(public_numbers.e, 3), +} + +# Token claims for each role +now = int(time.time()) +exp = now + (10 * 365 * 24 * 3600) # 10 years + +roles = { + "admin": { + "sub": "admin-user-id", + "name": "Admin User", + "admin": True, + "role": "admin", + }, + "user": {"sub": "user-id", "name": "Regular User", "admin": False, "role": "user"}, + "viewer": { + "sub": "viewer-id", + "name": "Viewer User", + "admin": False, + "role": "viewer", + }, + "query_only": { + "sub": "query-id", + "name": "Query User", + "admin": False, + "permissions": ["query"], + }, + "no_role": {"sub": "norole-id", "name": "No Role User", "admin": False}, +} + +tokens = {} +for role, claims in roles.items(): + payload = {"iat": now, "exp": exp, **claims} + tokens[role] = jwt.encode( + payload, private_pem, algorithm="RS256", headers={"kid": "test-key-1"} + ) + +print("=== JWKS ===") +print(json.dumps({"keys": [jwk]}, indent=2)) +print("\n=== TOKENS ===") +print(json.dumps(tokens, indent=2)) diff --git a/tests/e2e/mock_jwks_server/server.py b/tests/e2e/mock_jwks_server/server.py new file mode 100644 index 00000000..a9d27a94 --- /dev/null +++ b/tests/e2e/mock_jwks_server/server.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Simple mock JWKS server for E2E RBAC tests. + +Serves static pre-generated JWKS and test tokens. +No external dependencies - uses only Python stdlib. +""" + +import json +from http.server import HTTPServer, BaseHTTPRequestHandler + +# Static JWKS - pre-generated RSA public key +JWKS = { + "keys": [ + { + "kty": "RSA", + "kid": "test-key-1", + "use": "sig", + "alg": "RS256", + "n": "oYVHa2Map44Cbd32Ai_37P0CHnRqDU3U3MKNdHIBkkI9nl3VV1K-4GqyKmTHl6CfSDUh5_JrKJJblyY-u7MOB9kzrPn-7it2FBfmhnc8RNBRvvF2ti3_IC-an3-2t_qYP30ZtkTx4EtgbBhd6iCJFjDU6Rjl9fxtYG-jZR_91UDOyJSQnVCV9-1oRWhkA_5y6l1gNKu-Kc92Kmu39fhxOs4U8399MPI-RkGcJkGRP86xg9lNx1Linz7UzEENGvYhPf2peaUvCZSElSZcgy_EFI3Tag9-nSTDCZPmxv1ugAohMGIgtQtmBI-K30_1Mek_RPwMOXh2EX5ThVhvIbXXmw", + "e": "AQAB", + } + ] +} + +# Pre-generated test tokens (valid for 10 years from Jan 2026) +TOKENS = { + "admin": "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LTEiLCJ0eXAiOiJKV1QifQ.eyJpYXQiOjE3NjgzNzkzMDcsImV4cCI6MjA4MzczOTMwNywic3ViIjoiYWRtaW4tdXNlci1pZCIsIm5hbWUiOiJBZG1pbiBVc2VyIiwiYWRtaW4iOnRydWUsInJvbGUiOiJhZG1pbiJ9.BFVQDG6Io59q3gYwt54c2NJEI5q3MUIXwRIlPhu3v1F9inrZOPtLKBUbjgkF6OpU5xe5ck09BsKwvuNX0gBS8iVHb4vetkd2hwqDljk8wHEOs_E8X4_3Yqoz5NFgs1Mx3fd66xuWy2TtwLaIZ3Mwx6aGERZBXBvY_5yP7HI2oUQ4jVHe6TZL4qa927YFXtNZv11DBq9FkrZRaFtACt6iikEA-UD-v4N1szWlBvn_JCsmB9gQc8txN8FfNH_h01qTJWfuqBbK-6pSpgjr9pS4dG3AuFpBucp-eaBDCGlC7kz085_I10hnZhGCoB7XD1VOTILtwdMvjB_6VFd4f-0EiQ", + "user": "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LTEiLCJ0eXAiOiJKV1QifQ.eyJpYXQiOjE3NjgzNzkzMDcsImV4cCI6MjA4MzczOTMwNywic3ViIjoidXNlci1pZCIsIm5hbWUiOiJSZWd1bGFyIFVzZXIiLCJhZG1pbiI6ZmFsc2UsInJvbGUiOiJ1c2VyIn0.eocDRnf8Cbw1wEee3mmZGDyPlUGAFN8-dH9LmEChAYSSQ6g94vRhL4yoQCiDJA76Vuzmt9CJKGxHNlvmqZh82rEPezLDq0H_a3qgPZq_9uS_dzl3c-ityojbI0YBE1DWm_29vhEv9lfVaJc9EalSObN5xttq32GJ8-1kFWATgP--n5SP3omoljLxAmVMlQlU2gjB7trH7OyLLHp4-DqsUzUUXsNg1pj-BmWT7pkw36QjRfintX-GEcSMbHABX0g2CXUKuLAWsqbbyLPPtDPlPFQh6HmZna74-riWJqOYg6pL4XSUwl_DKxafjZ_wCysSULUjR_i2E6XlgBlIRAZC5A", + "viewer": "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LTEiLCJ0eXAiOiJKV1QifQ.eyJpYXQiOjE3NjgzNzkzMDcsImV4cCI6MjA4MzczOTMwNywic3ViIjoidmlld2VyLWlkIiwibmFtZSI6IlZpZXdlciBVc2VyIiwiYWRtaW4iOmZhbHNlLCJyb2xlIjoidmlld2VyIn0.a_6FLiAw9cg-hUNNtdv1WyQtwkMJCmMnXXB1fOcGNyjgYSL-z3-bW12FOGH86MTxdcXKxsvfaw5FrUqOZVUitlo3AjqFdZJaZkKJO23-eMvWwaCME90wPkM6nW0L95nygkko8SkX4WWoccPBqqDRG3QxzsBxq6Lu7NdSnpz2iGlZcYwmCZdIhmBqgxuQbUPeMQlxJtoiv6AUXA8lMJbHAcftrwoQ2oWVKIRwjK4VHn-s8G5HzK3ezlDKz31kNxg74rQo4jZzlRkWVHQ2wByabyaRGCysoM7KrNuCJwjs4W_tShb9nM50zTc_jrcjeur3LbtDt3XOPNyKpVxElpAgYw", + "query_only": "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LTEiLCJ0eXAiOiJKV1QifQ.eyJpYXQiOjE3NjgzNzkzMDcsImV4cCI6MjA4MzczOTMwNywic3ViIjoicXVlcnktaWQiLCJuYW1lIjoiUXVlcnkgVXNlciIsImFkbWluIjpmYWxzZSwicGVybWlzc2lvbnMiOlsicXVlcnkiXX0.fOEEnWhVajeBSGxxMhzmcHPJ1ZWoDrz-JgFGngoanbEA8NGoQcNnbZvnDGg_Jn6_4YtFwQ5NnVb50lZSw046HapLPRfbQsz2yxCzW1FaX2Jvc8-d8kciZPh_aWwxv2foAEii_8hG9ZisRvUIDoBUHmtJdxGcRcilgXywIc4BS15Cxi-Ib7RPkqsKN56vIy30-vTeV0bwcAXVjmpPiekIrFqZX-rLpFptjouSdBTF8PEvh_K1pmFteMfe1QJzonDYYNdMTOsQRy-c0KH9fX7oWhw9xJvvTlh0pDZbh1zAk6EYeiSCavq6myxRGyImNT0wQ7IuzWywsBUmLauRxf6W5Q", + "no_role": "eyJhbGciOiJSUzI1NiIsImtpZCI6InRlc3Qta2V5LTEiLCJ0eXAiOiJKV1QifQ.eyJpYXQiOjE3NjgzNzkzMDcsImV4cCI6MjA4MzczOTMwNywic3ViIjoibm9yb2xlLWlkIiwibmFtZSI6Ik5vIFJvbGUgVXNlciIsImFkbWluIjpmYWxzZX0.jBpNj3HKfSwMNED8J-o3A847aJg7LBDiHJeEB_tRUYJZhd4U6wMv2iun7fpdkns6b-70qtVqOd8xd-BUOsiXNpldjVWI8GaXsqh0q63X622ZYGItMWX0BGgwg2LoQgmN2G1k0xQIs1unCQn0wDmSB6ZFBAMDDSYLpZ0KOLNknh5NUX4GJyMXYgz3FZj6my0ypxWOnmOmC4iL5HGUszq6GB-K7nu75TMOuMZh4FxhbxIvWoT59y-NVKzoTxrkU4w6s0_gfcbqjieJd0sJbp-T4xm3qap7PF4yuFjwkptfbT_hiwAgbOsguTE1LbZQXOz0tdzuORQq7J9skyt2LCjV7w", +} + + +class Handler(BaseHTTPRequestHandler): + """Simple HTTP handler for JWKS and tokens.""" + + def do_GET(self) -> None: + """Handle GET requests.""" + if self.path == "/.well-known/jwks.json": + self._json_response(JWKS) + elif self.path == "/tokens": + self._json_response(TOKENS) + elif self.path == "/health": + self._json_response({"status": "ok"}) + else: + self.send_error(404) + + def _json_response(self, data: dict) -> None: + """Send JSON response.""" + body = json.dumps(data).encode() + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + + def log_message(self, format: str, *args) -> None: + """Suppress request logging.""" + + +if __name__ == "__main__": + server = HTTPServer(("0.0.0.0", 8000), Handler) + print("Mock JWKS server on :8000") + server.serve_forever() diff --git a/tests/e2e/test_list.txt b/tests/e2e/test_list.txt index b61f186a..ebd39760 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/rbac.feature features/conversations.feature features/conversation_cache_v2.feature features/feedback.feature