diff --git a/tests/model_serving/maas_billing/maas_subscription/conftest.py b/tests/model_serving/maas_billing/maas_subscription/conftest.py index 00bc0bf6e..ba3b77e56 100644 --- a/tests/model_serving/maas_billing/maas_subscription/conftest.py +++ b/tests/model_serving/maas_billing/maas_subscription/conftest.py @@ -203,6 +203,133 @@ def maas_headers_for_actor_api_key(maas_api_key_for_actor: str) -> dict[str, str return build_maas_headers(token=maas_api_key_for_actor) +@pytest.fixture(scope="function") +def extra_subscription_with_api_key( + request_session_http: requests.Session, + base_url: str, + ocp_token_for_actor: str, + admin_client: DynamicClient, + maas_free_group: str, + maas_model_tinyllama_free: MaaSModelRef, + maas_subscription_namespace: Namespace, + maas_subscription_tinyllama_free: MaaSSubscription, + maas_subscription_controller_enabled_latest: None, + maas_gateway_api: None, + maas_api_gateway_reachable: None, +) -> Generator[str, Any, Any]: + """ + Creates an extra subscription (for nonexistent-group, priority=1) and an API key + bound to the original free subscription. Verifies the user's key still works even + with a second subscription present (OR-logic fix). Revokes key on teardown. + """ + with create_maas_subscription( + admin_client=admin_client, + subscription_namespace=maas_subscription_namespace.name, + subscription_name="extra-subscription", + owner_group_name="nonexistent-group-xyz", + model_name=maas_model_tinyllama_free.name, + model_namespace=maas_model_tinyllama_free.namespace, + tokens_per_minute=999, + window="1m", + priority=1, + teardown=True, + wait_for_resource=True, + ) as extra_subscription: + extra_subscription.wait_for_condition(condition="Ready", status="True", timeout=300) + _, body = create_api_key( + base_url=base_url, + ocp_user_token=ocp_token_for_actor, + request_session_http=request_session_http, + api_key_name=f"e2e-one-of-two-{generate_random_name()}", + subscription=maas_subscription_tinyllama_free.name, + ) + yield body["key"] + revoke_api_key( + request_session_http=request_session_http, + base_url=base_url, + key_id=body["id"], + ocp_user_token=ocp_token_for_actor, + ) + + +@pytest.fixture(scope="function") +def high_tier_subscription_with_api_key( + request_session_http: requests.Session, + base_url: str, + ocp_token_for_actor: str, + admin_client: DynamicClient, + maas_free_group: str, + maas_model_tinyllama_free: MaaSModelRef, + maas_subscription_namespace: Namespace, + maas_subscription_tinyllama_free: MaaSSubscription, + maas_subscription_controller_enabled_latest: None, + maas_gateway_api: None, + maas_api_gateway_reachable: None, +) -> Generator[str, Any, Any]: + """ + Creates a high-priority subscription (priority=10) for the free group and an API key + bound to it. Returns the API key. Revokes key and cleans up subscription on teardown. + """ + with create_maas_subscription( + admin_client=admin_client, + subscription_namespace=maas_subscription_namespace.name, + subscription_name="high-tier-subscription", + owner_group_name=maas_free_group, + model_name=maas_model_tinyllama_free.name, + model_namespace=maas_model_tinyllama_free.namespace, + tokens_per_minute=9999, + window="1m", + priority=10, + teardown=True, + wait_for_resource=True, + ) as high_tier_subscription: + high_tier_subscription.wait_for_condition(condition="Ready", status="True", timeout=300) + _, body = create_api_key( + base_url=base_url, + ocp_user_token=ocp_token_for_actor, + request_session_http=request_session_http, + api_key_name=f"e2e-high-tier-{generate_random_name()}", + subscription=high_tier_subscription.name, + ) + yield body["key"] + revoke_api_key( + request_session_http=request_session_http, + base_url=base_url, + key_id=body["id"], + ocp_user_token=ocp_token_for_actor, + ) + + +@pytest.fixture(scope="function") +def api_key_bound_to_system_auth_subscription( + request_session_http: requests.Session, + base_url: str, + ocp_token_for_actor: str, + premium_system_authenticated_access: dict, + maas_subscription_controller_enabled_latest: None, + maas_gateway_api: None, + maas_api_gateway_reachable: None, +) -> Generator[str, Any, Any]: + """ + API key bound to the system:authenticated subscription on the premium model. + Used for tests that verify OR-logic auth policy access. Revoked on teardown. + """ + _, body = create_api_key( + base_url=base_url, + ocp_user_token=ocp_token_for_actor, + request_session_http=request_session_http, + api_key_name=f"e2e-system-auth-{generate_random_name()}", + subscription=premium_system_authenticated_access["subscription"].name, + ) + yield body["key"] + revoke_api_key( + request_session_http=request_session_http, + base_url=base_url, + key_id=body["id"], + ocp_user_token=ocp_token_for_actor, + ) + + @pytest.fixture(scope="class") def api_key_bound_to_free_subscription( request_session_http: requests.Session, @@ -369,7 +496,7 @@ def premium_system_authenticated_access( model_namespace=maas_model_tinyllama_premium.namespace, tokens_per_minute=100, window="1m", - priority=0, + priority=1, teardown=True, wait_for_resource=True, ) as system_authenticated_subscription, diff --git a/tests/model_serving/maas_billing/maas_subscription/test_multiple_auth_policies_per_model.py b/tests/model_serving/maas_billing/maas_subscription/test_multiple_auth_policies_per_model.py index dc172a0b9..2ed7fccfd 100644 --- a/tests/model_serving/maas_billing/maas_subscription/test_multiple_auth_policies_per_model.py +++ b/tests/model_serving/maas_billing/maas_subscription/test_multiple_auth_policies_per_model.py @@ -8,11 +8,10 @@ chat_payload_for_url, poll_expected_status, ) +from tests.model_serving.maas_billing.utils import build_maas_headers LOGGER = structlog.get_logger(name=__name__) -MAAS_SUBSCRIPTION_HEADER = "x-maas-subscription" - @pytest.mark.usefixtures( "maas_unprivileged_model_namespace", @@ -31,21 +30,18 @@ def test_premium_model_denies_free_actor_by_default( self, request_session_http: requests.Session, model_url_tinyllama_premium: str, - maas_subscription_tinyllama_premium, maas_headers_for_actor_api_key: dict[str, str], ) -> None: """ Verify FREE actor is denied by default on the premium model. + The API key is not bound to the premium subscription, and auth policy denies the free group. """ - - baseline_headers = dict(maas_headers_for_actor_api_key) - baseline_headers[MAAS_SUBSCRIPTION_HEADER] = maas_subscription_tinyllama_premium.name baseline_payload = chat_payload_for_url(model_url=model_url_tinyllama_premium) baseline_response = poll_expected_status( request_session_http=request_session_http, model_url=model_url_tinyllama_premium, - headers=baseline_headers, + headers=maas_headers_for_actor_api_key, payload=baseline_payload, expected_statuses={403}, ) @@ -61,18 +57,14 @@ def test_two_auth_policies_or_logic_allows_access( self, request_session_http: requests.Session, model_url_tinyllama_premium: str, - maas_headers_for_actor_api_key: dict[str, str], premium_system_authenticated_access, + api_key_bound_to_system_auth_subscription: str, ) -> None: """ Verify FREE actor can access the premium model when an extra AuthPolicy and matching subscription for system:authenticated exist. + API key is minted and bound to the system:authenticated subscription at creation time. """ - - payload = chat_payload_for_url(model_url=model_url_tinyllama_premium) - explicit_headers = dict(maas_headers_for_actor_api_key) - explicit_headers[MAAS_SUBSCRIPTION_HEADER] = premium_system_authenticated_access["subscription"].name - LOGGER.info( f"Polling for 200 on premium model with OR auth policy: " f"auth_policy={premium_system_authenticated_access['auth_policy'].name}, " @@ -82,11 +74,10 @@ def test_two_auth_policies_or_logic_allows_access( response = poll_expected_status( request_session_http=request_session_http, model_url=model_url_tinyllama_premium, - headers=explicit_headers, - payload=payload, + headers=build_maas_headers(token=api_key_bound_to_system_auth_subscription), + payload=chat_payload_for_url(model_url=model_url_tinyllama_premium), expected_statuses={200}, ) - assert response.status_code == 200, ( f"Expected 200 with second AuthPolicy (OR logic), got {response.status_code}: {(response.text or '')[:200]}" ) @@ -97,21 +88,20 @@ def test_delete_extra_auth_policy_denies_access_on_premium_model( self, request_session_http: requests.Session, model_url_tinyllama_premium: str, - maas_headers_for_actor_api_key: dict[str, str], premium_system_authenticated_access, + api_key_bound_to_system_auth_subscription: str, ) -> None: """ Verify FREE actor loses access again after the extra AuthPolicy is deleted. + API key is minted and bound to the system:authenticated subscription at creation time. """ - + headers = build_maas_headers(token=api_key_bound_to_system_auth_subscription) payload = chat_payload_for_url(model_url=model_url_tinyllama_premium) - explicit_headers = dict(maas_headers_for_actor_api_key) - explicit_headers[MAAS_SUBSCRIPTION_HEADER] = premium_system_authenticated_access["subscription"].name poll_expected_status( request_session_http=request_session_http, model_url=model_url_tinyllama_premium, - headers=explicit_headers, + headers=headers, payload=payload, expected_statuses={200}, ) @@ -121,7 +111,7 @@ def test_delete_extra_auth_policy_denies_access_on_premium_model( response = poll_expected_status( request_session_http=request_session_http, model_url=model_url_tinyllama_premium, - headers=explicit_headers, + headers=headers, payload=payload, expected_statuses={403}, ) diff --git a/tests/model_serving/maas_billing/maas_subscription/test_multiple_subscriptions_per_model.py b/tests/model_serving/maas_billing/maas_subscription/test_multiple_subscriptions_per_model.py index 02e445ee4..881df1c00 100644 --- a/tests/model_serving/maas_billing/maas_subscription/test_multiple_subscriptions_per_model.py +++ b/tests/model_serving/maas_billing/maas_subscription/test_multiple_subscriptions_per_model.py @@ -17,8 +17,6 @@ LOGGER = structlog.get_logger(name=__name__) -MAAS_SUBSCRIPTION_HEADER = "x-maas-subscription" - @pytest.mark.usefixtures( "maas_unprivileged_model_namespace", @@ -39,109 +37,53 @@ class TestMultipleSubscriptionsPerModel: def test_user_in_one_of_two_subscriptions_can_access_model( self, request_session_http: requests.Session, - admin_client: DynamicClient, - maas_free_group: str, - maas_model_tinyllama_free, model_url_tinyllama_free: str, - maas_subscription_tinyllama_free, - maas_headers_for_actor_api_key: dict[str, str], - maas_subscription_namespace, + extra_subscription_with_api_key: str, ) -> None: """ Create a second subscription for a different group the user is NOT in. - User should still get 200 when explicitly selecting the correct subscription. + User should still get 200 — API key is bound to the original free subscription, + verifying OR-logic: a second subscription does not block access. """ - assert maas_free_group, "maas_free_group fixture returned empty group name" - - with create_maas_subscription( - admin_client=admin_client, - subscription_namespace=maas_subscription_namespace.name, - subscription_name="extra-subscription", - owner_group_name="nonexistent-group-xyz", - model_name=maas_model_tinyllama_free.name, - model_namespace=maas_model_tinyllama_free.namespace, - tokens_per_minute=999, - window="1m", - priority=0, - teardown=True, - wait_for_resource=True, - ) as extra_subscription: - extra_subscription.wait_for_condition(condition="Ready", status="True", timeout=300) + LOGGER.info("Polling for 200 — user's API key bound to original subscription despite a second one existing") - payload = chat_payload_for_url(model_url=model_url_tinyllama_free) - explicit_headers = dict(maas_headers_for_actor_api_key) - explicit_headers[MAAS_SUBSCRIPTION_HEADER] = maas_subscription_tinyllama_free.name + response = poll_expected_status( + request_session_http=request_session_http, + model_url=model_url_tinyllama_free, + headers=build_maas_headers(token=extra_subscription_with_api_key), + payload=chat_payload_for_url(model_url=model_url_tinyllama_free), + expected_statuses={200}, + ) - LOGGER.info( - "Polling for 200 with explicit subscription selection: " - f"subscription={maas_subscription_tinyllama_free.name}" - ) - - response = poll_expected_status( - request_session_http=request_session_http, - model_url=model_url_tinyllama_free, - headers=explicit_headers, - payload=payload, - expected_statuses={200}, - ) - - assert response.status_code == 200, ( - f"Expected 200 after adding second subscription, got {response.status_code}: " - f"{(response.text or '')[:200]}" - ) + assert response.status_code == 200, ( + f"Expected 200 after adding second subscription, got {response.status_code}: {(response.text or '')[:200]}" + ) @pytest.mark.smoke @pytest.mark.parametrize("ocp_token_for_actor", [{"type": "free"}], indirect=True) def test_high_priority_subscription_allows_access_when_explicitly_selected( self, request_session_http: requests.Session, - admin_client: DynamicClient, - maas_free_group: str, - maas_model_tinyllama_free, model_url_tinyllama_free: str, - maas_subscription_tinyllama_free, - maas_headers_for_actor_api_key: dict[str, str], - maas_subscription_namespace, + high_tier_subscription_with_api_key: str, ) -> None: """ Create a second (higher priority) subscription for the same group + model. - User should get 200 when explicitly selecting the high-priority subscription. + User should get 200 — API key is minted and bound to the high-priority subscription + at creation time. """ - assert maas_free_group, "maas_free_group fixture returned empty group name" - _ = maas_subscription_tinyllama_free - - with create_maas_subscription( - admin_client=admin_client, - subscription_namespace=maas_subscription_namespace.name, - subscription_name="high-tier-subscription", - owner_group_name=maas_free_group, - model_name=maas_model_tinyllama_free.name, - model_namespace=maas_model_tinyllama_free.namespace, - tokens_per_minute=9999, - window="1m", - priority=10, - teardown=True, - wait_for_resource=True, - ) as high_tier_subscription: - high_tier_subscription.wait_for_condition(condition="Ready", status="True", timeout=300) - - payload = chat_payload_for_url(model_url=model_url_tinyllama_free) - - explicit_headers = dict(maas_headers_for_actor_api_key) - explicit_headers[MAAS_SUBSCRIPTION_HEADER] = high_tier_subscription.name - - response = poll_expected_status( - request_session_http=request_session_http, - model_url=model_url_tinyllama_free, - headers=explicit_headers, - payload=payload, - expected_statuses={200}, - ) - - assert response.status_code == 200, ( - f"Expected 200 when selecting high-priority subscription '{high_tier_subscription.name}', " - f"got {response.status_code}: {(response.text or '')[:200]}" - ) + response = poll_expected_status( + request_session_http=request_session_http, + model_url=model_url_tinyllama_free, + headers=build_maas_headers(token=high_tier_subscription_with_api_key), + payload=chat_payload_for_url(model_url=model_url_tinyllama_free), + expected_statuses={200}, + ) + + assert response.status_code == 200, ( + f"Expected 200 when using API key bound to high-priority subscription, " + f"got {response.status_code}: {(response.text or '')[:200]}" + ) @pytest.mark.smoke def test_service_account_cannot_use_subscription_it_does_not_belong_to( @@ -192,7 +134,7 @@ def test_service_account_cannot_use_subscription_it_does_not_belong_to( model_namespace=maas_model_tinyllama_free.namespace, tokens_per_minute=500, window="1m", - priority=0, + priority=2, teardown=True, wait_for_resource=True, ) as premium_subscription, @@ -208,8 +150,8 @@ def test_service_account_cannot_use_subscription_it_does_not_belong_to( service_account.wait(timeout=60) service_account_token = create_inference_token(model_service_account=service_account) + # SA token is not a valid MaaS API key — request is rejected (401/403/429) headers = build_maas_headers(token=service_account_token) - headers[MAAS_SUBSCRIPTION_HEADER] = premium_subscription.name payload = chat_payload_for_url(model_url=model_url_tinyllama_free) @@ -218,10 +160,10 @@ def test_service_account_cannot_use_subscription_it_does_not_belong_to( model_url=model_url_tinyllama_free, headers=headers, payload=payload, - expected_statuses={403, 429}, + expected_statuses={401, 403, 429}, ) - assert response.status_code in {403, 429}, ( - f"Expected 403/429 when service account selects a subscription it doesn't belong to, " + assert response.status_code in {401, 403, 429}, ( + f"Expected 401/403/429 when service account token has no valid MaaS subscription, " f"got {response.status_code}: {(response.text or '')[:200]}" )