22# SPDX-License-Identifier: Apache-2.0
33
44import uuid
5+ from collections .abc import Callable
56from pathlib import Path
7+ from time import monotonic , sleep
68from unittest .mock import patch
79
10+ import pytest
811from cryptography .hazmat .primitives import serialization
912from cryptography .hazmat .primitives .asymmetric import rsa
1013from fastapi .testclient import TestClient
14+ from httpx import Response
1115from nmp .common .auth .access_keys import public_jwk_from_private_key_pem , validate_access_key_token
1216from nmp .common .auth .jwt import TokenClaims
1317from nmp .common .config import AuthConfig
1418from nmp .common .config .base import AccessKeyConfig , TokenSigningConfig
1519from nmp .core .auth .config import AuthServiceConfig
1620from nmp .testing .client import create_test_client
1721
22+ # Keep this test in its own xdist group while embedded PDP uses process-wide
23+ # wasmtime state. This limits cross-test scheduling noise under --dist loadgroup.
24+ pytestmark = pytest .mark .xdist_group ("auth_scoped_access_keys" )
25+
1826ACCESS_KEYS_PATH = "/apis/auth/v2/access-keys"
1927IAM_ROLE_BINDINGS_PATH = "/apis/auth/v2/iam/role-bindings"
2028WORKSPACES_PATH = "/apis/entities/v2/workspaces"
2129SERVICE_HEADERS = {"X-NMP-Principal-Id" : "service:integration-test" }
30+ AUTHZ_PROPAGATION_TIMEOUT_SECONDS = 5.0
31+ AUTHZ_PROPAGATION_POLL_INTERVAL_SECONDS = 0.05
32+
33+
34+ def _wait_for_authorization_response (
35+ request : Callable [[], Response ],
36+ * ,
37+ expected_status_code : int ,
38+ ) -> Response :
39+ deadline = monotonic () + AUTHZ_PROPAGATION_TIMEOUT_SECONDS
40+ response = request ()
41+ while response .status_code != expected_status_code and monotonic () < deadline :
42+ sleep (AUTHZ_PROPAGATION_POLL_INTERVAL_SECONDS )
43+ response = request ()
44+
45+ if response .status_code != expected_status_code :
46+ raise AssertionError (
47+ f"Timed out waiting for authorization response { expected_status_code } ; "
48+ f"last response { response .status_code } : { response .text } "
49+ )
50+ return response
2251
2352
2453def _tamper_jwt (token : str ) -> str :
@@ -57,8 +86,8 @@ def _auth_configs(private_key_file: str) -> tuple[AuthConfig, AuthServiceConfig]
5786 )
5887 service_config = AuthServiceConfig (
5988 ** shared_config .model_dump (),
60- policy_data_refresh_interval = 0.05 ,
61- bundle_cache_seconds = 0 ,
89+ policy_data_refresh_interval = 0.2 ,
90+ bundle_cache_seconds = 0.1 ,
6291 admin_email = "admin@example.com" ,
6392 )
6493 return shared_config , service_config
@@ -117,35 +146,37 @@ def test_scoped_access_key_created_by_auth_service_authenticates_platform_reques
117146 async def validate_with_local_jwks (config : AuthConfig , token : str ) -> TokenClaims | None :
118147 return await validate_access_key_token (config , token , jwks_override = jwks )
119148
120- with patch ("nmp.common.auth.access_keys.validate_access_key_token" , validate_with_local_jwks ):
121- response = client .get (
122- f"{ WORKSPACES_PATH } /{ workspace } " ,
123- headers = {"Authorization" : f"Bearer { access_key } " },
124- )
149+ def get_workspace_with_access_key (token : str ) -> Response :
150+ with patch ("nmp.common.auth.access_keys.validate_access_key_token" , validate_with_local_jwks ):
151+ return client .get (
152+ f"{ WORKSPACES_PATH } /{ workspace } " ,
153+ headers = {"Authorization" : f"Bearer { token } " },
154+ )
155+
156+ def authenticate_with_access_key (token : str ) -> Response :
157+ with patch ("nmp.common.auth.access_keys.validate_access_key_token" , validate_with_local_jwks ):
158+ return client .get (
159+ "/apis/auth/authenticate" ,
160+ headers = {"Authorization" : f"Bearer { token } " },
161+ )
162+
163+ response = _wait_for_authorization_response (
164+ lambda : get_workspace_with_access_key (access_key ),
165+ expected_status_code = 200 ,
166+ )
125167
126168 assert response .status_code == 200 , response .text
127169 assert response .json ()["name" ] == workspace
128170
129- with patch ("nmp.common.auth.access_keys.validate_access_key_token" , validate_with_local_jwks ):
130- authenticate_response = client .get (
131- "/apis/auth/authenticate" ,
132- headers = {"Authorization" : f"Bearer { access_key } " },
133- )
134- invalid_authenticate_response = client .get (
135- "/apis/auth/authenticate" ,
136- headers = {"Authorization" : f"Bearer { _tamper_jwt (access_key )} " },
137- )
171+ authenticate_response = authenticate_with_access_key (access_key )
172+ invalid_authenticate_response = authenticate_with_access_key (_tamper_jwt (access_key ))
138173
139174 assert authenticate_response .status_code == 200 , authenticate_response .text
140175 assert authenticate_response .json ()["principal" ] == user
141176 assert authenticate_response .json ()["token_kind" ] == "access_key"
142177 assert invalid_authenticate_response .status_code == 401 , invalid_authenticate_response .text
143178
144- with patch ("nmp.common.auth.access_keys.validate_access_key_token" , validate_with_local_jwks ):
145- invalid_workspace_response = client .get (
146- f"{ WORKSPACES_PATH } /{ workspace } " ,
147- headers = {"Authorization" : f"Bearer { _tamper_jwt (access_key )} " },
148- )
179+ invalid_workspace_response = get_workspace_with_access_key (_tamper_jwt (access_key ))
149180
150181 assert invalid_workspace_response .status_code == 401 , invalid_workspace_response .text
151182
@@ -155,15 +186,14 @@ async def validate_with_local_jwks(config: AuthConfig, token: str) -> TokenClaim
155186 assert len (revoked_keys ) == 1
156187 assert revoked_keys [0 ]["status" ] == "REVOKED"
157188
158- with patch ("nmp.common.auth.access_keys.validate_access_key_token" , validate_with_local_jwks ):
159- revoked_authenticate_response = client .get (
160- "/apis/auth/authenticate" ,
161- headers = {"Authorization" : f"Bearer { access_key } " },
162- )
163- revoked_workspace_response = client .get (
164- f"{ WORKSPACES_PATH } /{ workspace } " ,
165- headers = {"Authorization" : f"Bearer { access_key } " },
166- )
189+ revoked_authenticate_response = _wait_for_authorization_response (
190+ lambda : authenticate_with_access_key (access_key ),
191+ expected_status_code = 401 ,
192+ )
193+ revoked_workspace_response = _wait_for_authorization_response (
194+ lambda : get_workspace_with_access_key (access_key ),
195+ expected_status_code = 401 ,
196+ )
167197
168198 assert revoked_authenticate_response .status_code == 401 , revoked_authenticate_response .text
169199 assert revoked_workspace_response .status_code == 401 , revoked_workspace_response .text
0 commit comments