|
14 | 14 | from fabric_cli.errors import ErrorMessages |
15 | 15 |
|
16 | 16 |
|
17 | | -def _make_jwt(tid: str = "test-tenant", oid: str = "test-oid", **extra_claims) -> str: |
| 17 | +def _make_jwt(tid: str = "test-tenant", oid: str = "test-oid", |
| 18 | + iss: str = "https://sts.windows.net/test-tenant/", **extra_claims) -> str: |
18 | 19 | """Create a fake JWT with specified claims (no signature validation needed).""" |
19 | 20 | header = base64.urlsafe_b64encode(b'{"alg":"none"}').rstrip(b"=").decode() |
20 | | - claims = {"tid": tid, "oid": oid, **extra_claims} |
| 21 | + claims = {"tid": tid, "oid": oid, "iss": iss, **extra_claims} |
21 | 22 | payload = base64.urlsafe_b64encode(_json.dumps(claims).encode()).rstrip(b"=").decode() |
22 | 23 | return f"{header}.{payload}.fakesig" |
23 | 24 |
|
24 | 25 |
|
25 | | -def _mock_credential_with_jwt(mock_class, tid="test-tenant", oid="test-oid", **extra): |
| 26 | +def _mock_credential_with_jwt(mock_class, tid="test-tenant", oid="test-oid", |
| 27 | + iss="https://sts.windows.net/test-tenant/", **extra): |
26 | 28 | """Set up a mock AzureCliCredential that returns a JWT with given claims.""" |
27 | | - token_str = _make_jwt(tid=tid, oid=oid, **extra) |
| 29 | + token_str = _make_jwt(tid=tid, oid=oid, iss=iss, **extra) |
28 | 30 | mock_token = MagicMock() |
29 | 31 | mock_token.token = token_str |
30 | 32 | mock_token.expires_on = int(time.time()) + 3600 |
@@ -280,6 +282,53 @@ def test_tenant_match_allows_token_acquisition( |
280 | 282 | assert "access_token" in result |
281 | 283 |
|
282 | 284 |
|
| 285 | +class TestAzureCliEnvironmentDrift: |
| 286 | + """Test cloud environment drift detection via JWT iss claim.""" |
| 287 | + |
| 288 | + @patch("fabric_cli.core.fab_auth.AzureCliCredential") |
| 289 | + def test_environment_drift_blocks_token_acquisition( |
| 290 | + self, mock_credential_class, temp_dir_fixture |
| 291 | + ): |
| 292 | + """Should block when token issuer differs from stored environment.""" |
| 293 | + # Login in Azure Public |
| 294 | + _mock_credential_with_jwt( |
| 295 | + mock_credential_class, tid="t1", oid="u1", |
| 296 | + iss="https://sts.windows.net/t1/" |
| 297 | + ) |
| 298 | + auth = FabAuth() |
| 299 | + auth.set_access_mode("azure_cli") |
| 300 | + auth.set_azure_cli() |
| 301 | + auth._azure_cli_credential = None |
| 302 | + |
| 303 | + # Now credential returns token from Azure Government |
| 304 | + _mock_credential_with_jwt( |
| 305 | + mock_credential_class, tid="t1", oid="u1", |
| 306 | + iss="https://sts.microsoftonline.us/t1/" |
| 307 | + ) |
| 308 | + |
| 309 | + with pytest.raises(FabricCLIError) as exc_info: |
| 310 | + auth._acquire_token_from_azure_cli(con.SCOPE_FABRIC_DEFAULT) |
| 311 | + |
| 312 | + assert "environment has changed" in str(exc_info.value) |
| 313 | + |
| 314 | + @patch("fabric_cli.core.fab_auth.AzureCliCredential") |
| 315 | + def test_same_environment_allows_token_acquisition( |
| 316 | + self, mock_credential_class, temp_dir_fixture |
| 317 | + ): |
| 318 | + """Should allow when token issuer matches stored environment.""" |
| 319 | + _mock_credential_with_jwt( |
| 320 | + mock_credential_class, tid="t1", oid="u1", |
| 321 | + iss="https://sts.windows.net/t1/" |
| 322 | + ) |
| 323 | + auth = FabAuth() |
| 324 | + auth.set_access_mode("azure_cli") |
| 325 | + auth.set_azure_cli() |
| 326 | + auth._azure_cli_credential = None |
| 327 | + |
| 328 | + result = auth._acquire_token_from_azure_cli(con.SCOPE_FABRIC_DEFAULT) |
| 329 | + assert "access_token" in result |
| 330 | + |
| 331 | + |
283 | 332 | class TestAzureCliPrincipalDrift: |
284 | 333 | """Test principal (identity) drift detection via JWT OID claims.""" |
285 | 334 |
|
@@ -462,14 +511,16 @@ def test_login_discovers_tenant_from_jwt(self, mock_credential_class, temp_dir_f |
462 | 511 | assert auth.get_tenant_id() == "discovered-tenant" |
463 | 512 |
|
464 | 513 | @patch("fabric_cli.core.fab_auth.AzureCliCredential") |
465 | | - def test_login_stores_oid_for_drift_detection(self, mock_credential_class, temp_dir_fixture): |
466 | | - """set_azure_cli should store OID from JWT for drift detection.""" |
467 | | - _mock_credential_with_jwt(mock_credential_class, tid="t1", oid="user-oid-123") |
| 514 | + def test_login_stores_oid_and_issuer_for_drift_detection(self, mock_credential_class, temp_dir_fixture): |
| 515 | + """set_azure_cli should store OID and issuer from JWT for drift detection.""" |
| 516 | + _mock_credential_with_jwt(mock_credential_class, tid="t1", oid="user-oid-123", |
| 517 | + iss="https://sts.windows.net/t1/") |
468 | 518 |
|
469 | 519 | auth = FabAuth() |
470 | 520 | auth.set_access_mode("azure_cli") |
471 | 521 | auth.set_azure_cli() |
472 | 522 | assert auth._auth_info.get(con.FAB_AZURE_CLI_PRINCIPAL_ID) == "user-oid-123" |
| 523 | + assert auth._auth_info.get(con.FAB_AZURE_CLI_ISSUER) == "https://sts.windows.net/t1/" |
473 | 524 |
|
474 | 525 | @patch("fabric_cli.core.fab_auth.AzureCliCredential") |
475 | 526 | def test_re_login_updates_tenant_and_oid(self, mock_credential_class, temp_dir_fixture): |
|
0 commit comments