| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476 |
- from __future__ import annotations
- import base64
- import hashlib
- from datetime import UTC, datetime, timedelta
- import jwt
- import pytest
- from cryptography.hazmat.primitives.asymmetric import rsa
- NOW = datetime(2026, 8, 2, 8, 0, tzinfo=UTC)
- def _b64int(value: int) -> str:
- size = (value.bit_length() + 7) // 8
- return base64.urlsafe_b64encode(value.to_bytes(size, "big")).rstrip(b"=").decode()
- def _key_material():
- private = rsa.generate_private_key(public_exponent=65537, key_size=2048)
- numbers = private.public_key().public_numbers()
- jwk = {"kty": "RSA", "kid": "test-key", "alg": "RS256", "use": "sig", "n": _b64int(numbers.n), "e": _b64int(numbers.e)}
- return private, jwk
- def test_idp_config_rejects_unsafe_urls_redirects_and_secret_values(monkeypatch):
- from app.core.system.enterprise_identity import IdentityPolicyError, IdpConfig
- valid = {
- "provider_uid": "idp-1", "version": 1, "issuer": "https://login.example.com/tenant",
- "client_id": "dataops", "secret_ref": "env:DATAOPS_OIDC_CLIENT_SECRET",
- "authorization_endpoint": "https://login.example.com/oauth2/authorize",
- "token_endpoint": "https://login.example.com/oauth2/token",
- "jwks_uri": "https://login.example.com/.well-known/jwks.json",
- "redirect_uris": ("https://dataops.example.com/api/system/identity/callback",),
- "post_login_redirect_uris": ("https://dataops.example.com/login/callback",),
- "algorithms": ("RS256",), "mapping_version": "mapping-v1",
- }
- config = IdpConfig(**valid)
- config.validate()
- monkeypatch.setenv("DATAOPS_OIDC_CLIENT_SECRET", "not-returned")
- assert config.resolve_secret() == "not-returned"
- assert "not-returned" not in repr(config)
- for update in (
- {"issuer": "http://login.example.com"},
- {"issuer": "https://127.0.0.1"},
- {"issuer": "https://localhost"},
- {"secret_ref": "plain-secret"},
- {"secret_ref": "env:AWS_SECRET_ACCESS_KEY"},
- {"secret_ref": "env:OIDC_TEST_SECRET"},
- {"algorithms": ("none",)},
- {"redirect_uris": ("https://dataops.example.com/callback#token",)},
- {"post_login_redirect_uris": ("javascript:alert(1)",)},
- {"post_login_redirect_uris": ("https://localhost/login/callback",)},
- ):
- payload = {**valid, **update}
- with pytest.raises(IdentityPolicyError):
- IdpConfig(**payload).validate()
- def test_oidc_pkce_state_nonce_signature_and_replay_are_fail_closed():
- from app.core.system.enterprise_identity import IdentityPolicyError, IdpConfig
- from app.core.system.identity_repository import MemoryIdentityRepository
- from app.core.system.oidc import OidcClient
- config = IdpConfig(
- provider_uid="idp-1", version=1, issuer="https://login.example.com/tenant",
- client_id="dataops", secret_ref="env:DATAOPS_OIDC_TEST_SECRET",
- authorization_endpoint="https://login.example.com/authorize",
- token_endpoint="https://login.example.com/token",
- jwks_uri="https://login.example.com/jwks", redirect_uris=("https://dataops.example.com/callback",),
- post_login_redirect_uris=("https://dataops.example.com/login/callback",),
- algorithms=("RS256",), mapping_version="mapping-v1",
- )
- private, jwk = _key_material()
- repo = MemoryIdentityRepository()
- client = OidcClient(repo, clock=lambda: NOW)
- started = client.begin(config, "https://dataops.example.com/callback")
- assert started.code_challenge_method == "S256"
- assert started.code_challenge == base64.urlsafe_b64encode(
- hashlib.sha256(started.code_verifier.encode()).digest()
- ).rstrip(b"=").decode()
- claims = {"iss": config.issuer, "aud": config.client_id, "sub": "employee-1", "nonce": started.nonce,
- "iat": int(NOW.timestamp()), "exp": int((NOW + timedelta(minutes=5)).timestamp()),
- "preferred_username": "alice", "name": "Alice", "department": "ops", "groups": ["stewards"]}
- token = jwt.encode(claims, private, algorithm="RS256", headers={"kid": "test-key"})
- verified = client.verify_callback(config, state=started.state, redirect_uri="https://dataops.example.com/callback",
- code_verifier=started.code_verifier, id_token=token, jwks={"keys": [jwk]})
- assert verified["sub"] == "employee-1"
- with pytest.raises(IdentityPolicyError, match="state"):
- client.verify_callback(config, state=started.state, redirect_uri="https://dataops.example.com/callback",
- code_verifier=started.code_verifier, id_token=token, jwks={"keys": [jwk]})
- forged = jwt.encode({**claims, "nonce": "wrong"}, "symmetric", algorithm="HS256", headers={"kid": "test-key"})
- second = client.begin(config, "https://dataops.example.com/callback")
- with pytest.raises(IdentityPolicyError):
- client.verify_callback(config, state=second.state, redirect_uri="https://dataops.example.com/callback",
- code_verifier=second.code_verifier, id_token=forged, jwks={"keys": [jwk]})
- def test_oidc_transport_rejects_private_dns_redirects_oversize_and_bad_json():
- from app.core.system.enterprise_identity import (
- IdentityPolicyError,
- IdentityUpstreamError,
- IdpConfig,
- )
- from app.core.system.oidc import OidcTransport
- config = IdpConfig(
- provider_uid="idp-1", version=1, issuer="https://login.example.com/tenant",
- client_id="dataops", secret_ref="env:DATAOPS_OIDC_TEST_SECRET",
- authorization_endpoint="https://login.example.com/authorize",
- token_endpoint="https://login.example.com/token", jwks_uri="https://login.example.com/jwks",
- redirect_uris=("https://dataops.example.com/callback",),
- post_login_redirect_uris=("https://dataops.example.com/login/callback",),
- algorithms=("RS256",), mapping_version="mapping-v1",
- )
- class Response:
- def __init__(self, status, body, content_type="application/json"):
- self.status_code = status
- self.body = body
- self.headers = {"Content-Type": content_type, "Content-Length": str(len(body))}
- def iter_content(self, chunk_size):
- del chunk_size
- yield self.body
- def close(self):
- return None
- class Http:
- def __init__(self, response):
- self.response = response
- def get(self, url, **kwargs):
- del url, kwargs
- return self.response
- def public_dns(*args, **kwargs):
- del args, kwargs
- return [(2, 1, 6, "", ("93.184.216.34", 443))]
- def private_dns(*args, **kwargs):
- del args, kwargs
- return [(2, 1, 6, "", ("10.0.0.8", 443))]
- with pytest.raises(IdentityPolicyError, match="non-public"):
- OidcTransport(http=Http(Response(200, b'{"keys":[]}')), resolver=private_dns).fetch_jwks(config)
- with pytest.raises(IdentityUpstreamError, match="redirect"):
- OidcTransport(http=Http(Response(302, b"{}")), resolver=public_dns).fetch_jwks(config)
- with pytest.raises(IdentityUpstreamError, match="too large"):
- OidcTransport(http=Http(Response(200, b"x" * 33)), resolver=public_dns,
- max_response_bytes=32).fetch_jwks(config)
- with pytest.raises(IdentityUpstreamError, match="invalid JSON"):
- OidcTransport(http=Http(Response(200, b"not-json")), resolver=public_dns).fetch_jwks(config)
- with pytest.raises(IdentityUpstreamError, match="must be JSON"):
- OidcTransport(http=Http(Response(200, b"{}", "text/plain")), resolver=public_dns).fetch_jwks(config)
- def test_oidc_production_transport_pins_validated_ip_and_preserves_tls_hostname():
- from app.core.system.enterprise_identity import IdpConfig
- from app.core.system.oidc import OidcTransport
- config = IdpConfig(
- provider_uid="idp-1", version=1, issuer="https://login.example.com/tenant",
- client_id="dataops", secret_ref="env:DATAOPS_OIDC_TEST_SECRET",
- authorization_endpoint="https://login.example.com/authorize",
- token_endpoint="https://login.example.com/token", jwks_uri="https://login.example.com/jwks?tenant=one",
- redirect_uris=("https://dataops.example.com/callback",),
- post_login_redirect_uris=("https://dataops.example.com/login/callback",),
- algorithms=("RS256",), mapping_version="mapping-v1",
- )
- observed = {"resolver_calls": 0, "pool": [], "request": []}
- def resolver(host, port, **kwargs):
- observed["resolver_calls"] += 1
- assert host == "login.example.com" and port == 443
- assert kwargs["type"]
- return [(2, 1, 6, "", ("93.184.216.34", 443))]
- class Response:
- status = 200
- headers = {"Content-Type": "application/json", "Content-Length": "11"}
- def stream(self, **kwargs):
- assert kwargs == {"amt": 64 * 1024, "decode_content": True}
- yield b'{"keys":[]}'
- def release_conn(self):
- return None
- class Pool:
- def __init__(self, **kwargs):
- observed["pool"].append(kwargs)
- def urlopen(self, method, target, **kwargs):
- observed["request"].append((method, target, kwargs))
- return Response()
- def close(self):
- return None
- payload = OidcTransport(resolver=resolver, pool_factory=Pool).fetch_jwks(config)
- assert payload == {"keys": []}
- assert observed["resolver_calls"] == 1
- pool = observed["pool"][0]
- assert pool["host"] == "93.184.216.34"
- assert pool["server_hostname"] == "login.example.com"
- assert pool["assert_hostname"] == "login.example.com"
- assert pool["cert_reqs"] == "CERT_REQUIRED" and pool["ca_certs"]
- method, target, request_options = observed["request"][0]
- assert method == "GET" and target == "/jwks?tenant=one"
- assert request_options["headers"]["Host"] == "login.example.com"
- assert request_options["redirect"] is False and request_options["retries"] is False
- def test_claim_mapping_is_deterministic_versioned_and_default_deny():
- from app.core.system.enterprise_identity import ClaimMapper, IdentityPolicyError
- mapper = ClaimMapper(
- version="mapping-v7",
- group_rules={"stewards": {"roles": ["editor"], "business_domain_uids": ["domain-b", "domain-a"],
- "object_types": ["dataset"], "environments": ["prod"],
- "data_scopes": ["plant-1"]}},
- )
- claims = {"sub": "s-1", "preferred_username": "alice", "name": "Alice", "department": "ops",
- "groups": ["stewards"]}
- result = mapper.map(claims)
- assert result.roles == ("editor",)
- assert result.business_domain_uids == ("domain-a", "domain-b")
- assert result.mapping_version == "mapping-v7"
- assert len(result.claims_digest) == 64
- assert "Alice" not in repr(result.evidence)
- with pytest.raises(IdentityPolicyError):
- mapper.map({**claims, "groups": ["unknown"]})
- for malformed_claims in (
- {**claims, "groups": "stewards"},
- {**claims, "groups": ["stewards", 1]},
- {**claims, "groups": []},
- {**claims, "sub": {"value": "s-1"}},
- {**claims, "preferred_username": ["alice"]},
- {**claims, "name": ""},
- {**claims, "department": "x" * 301},
- ):
- with pytest.raises(IdentityPolicyError):
- mapper.map(malformed_claims)
- with pytest.raises(IdentityPolicyError):
- ClaimMapper(version="x", group_rules={"bad": {"roles": ["superuser"]}})
- for version, rules in (
- ("", {"group": {"roles": ["viewer"]}}),
- ("v1", {}),
- ("v1", {"group": "viewer"}),
- ("v1", {"group": {"roles": "viewer"}}),
- ("v1", {"group": {"roles": ["viewer"], "unknown_scope": ["x"]}}),
- ("v1", {"group": {"roles": ["viewer"], "data_scopes": [1]}}),
- ):
- with pytest.raises(IdentityPolicyError):
- ClaimMapper(version=version, group_rules=rules)
- def test_directory_events_are_idempotent_conflict_safe_and_leaver_revokes():
- from app.core.system.enterprise_identity import (
- DirectorySynchronizer,
- IdentityPolicyError,
- )
- from app.core.system.identity_repository import MemoryIdentityRepository
- from app.core.system.identity_sessions import SessionManager
- repo = MemoryIdentityRepository()
- sessions = SessionManager(repo, secret="a-long-test-secret", clock=lambda: NOW)
- sync = DirectorySynchronizer(repo, sessions, clock=lambda: NOW)
- first = sync.apply(provider_uid="idp-a", source="scim", source_event_id="e1", cursor="10", cursor_sequence=10, event_type="JOINER",
- subject="s1", attributes={"username": "alice", "roles": ["viewer"]})
- sessions.create(provider_uid="idp-a", subject="s1", user_uid=first["user_uid"], roles=["viewer"], identity_source="oidc")
- sessions.create(provider_uid="idp-b", subject="s1", user_uid="other", roles=["viewer"], identity_source="oidc")
- assert sync.apply(provider_uid="idp-a", source="scim", source_event_id="e1", cursor="10", cursor_sequence=10, event_type="JOINER",
- subject="s1", attributes={"username": "alice", "roles": ["viewer"]})["idempotent"] is True
- with pytest.raises(IdentityPolicyError):
- sync.apply(provider_uid="idp-a", source="scim", source_event_id="e1", cursor="10", cursor_sequence=10, event_type="MOVER",
- subject="s1", attributes={"username": "alice", "roles": ["editor"]})
- with pytest.raises(IdentityPolicyError, match="monotonic"):
- sync.apply(provider_uid="idp-a", source="scim", source_event_id="old", cursor="9", cursor_sequence=9,
- event_type="MOVER", subject="s1", attributes={"username": "alice", "roles": ["viewer"]})
- assert len(repo.directory_conflicts) == 2
- sync.apply(provider_uid="idp-a", source="scim", source_event_id="e2", cursor="11", cursor_sequence=11, event_type="LEAVER",
- subject="s1", attributes={"username": "alice", "roles": []})
- assert all(item["status"] == "revoked" for item in repo.list_sessions("idp-a", "s1"))
- assert repo.list_sessions("idp-b", "s1")[0]["status"] == "active"
- def test_directory_organization_nodes_are_provider_scoped_and_atomic(monkeypatch):
- from app.core.system.enterprise_identity import (
- DirectorySynchronizer,
- IdentityPolicyError,
- )
- from app.core.system.identity_repository import MemoryIdentityRepository
- from app.core.system.identity_sessions import SessionManager
- repo = MemoryIdentityRepository()
- sync = DirectorySynchronizer(repo, SessionManager(repo, secret="a-long-test-secret"), clock=lambda: NOW)
- first = sync.apply(provider_uid="idp-a", source="directory", source_event_id="d1", cursor="1",
- cursor_sequence=1, event_type="DEPARTMENT",
- attributes={"external_id": "ops", "display_name": "Operations", "action": "UPSERT"})
- sync.apply(provider_uid="idp-a", source="directory", source_event_id="d2", cursor="2",
- cursor_sequence=2, event_type="DEPARTMENT",
- attributes={"external_id": "ops", "action": "DISABLE"})
- restored = sync.apply(provider_uid="idp-a", source="directory", source_event_id="d3", cursor="3",
- cursor_sequence=3, event_type="DEPARTMENT",
- attributes={"external_id": "ops", "display_name": "Operations", "action": "RESTORE"})
- assert first["node_uid"] == restored["node_uid"]
- assert repo.get_organization_node("idp-a", "department", "ops")["status"] == "active"
- sync.apply(provider_uid="idp-b", source="directory", source_event_id="d1", cursor="1",
- cursor_sequence=1, event_type="DEPARTMENT",
- attributes={"external_id": "ops", "display_name": "Other Operations", "action": "UPSERT"})
- assert repo.get_organization_node("idp-b", "department", "ops")["display_name"] == "Other Operations"
- with pytest.raises(IdentityPolicyError, match="idempotency"):
- sync.apply(provider_uid="idp-a", source="directory", source_event_id="d3", cursor="3",
- cursor_sequence=3, event_type="DEPARTMENT",
- attributes={"external_id": "ops", "display_name": "Changed", "action": "RESTORE"})
- original = repo.put_directory_event
- monkeypatch.setattr(repo, "put_directory_event", lambda event: (_ for _ in ()).throw(RuntimeError("event failure")))
- with pytest.raises(RuntimeError, match="event failure"):
- sync.apply(provider_uid="idp-a", source="rollback", source_event_id="g1", cursor="1",
- cursor_sequence=1, event_type="GROUP",
- attributes={"external_id": "stewards", "display_name": "Stewards", "action": "UPSERT"})
- monkeypatch.setattr(repo, "put_directory_event", original)
- assert repo.get_organization_node("idp-a", "group", "stewards") is None
- assert repo.get_directory_checkpoint("idp-a", "rollback") is None
- def test_refresh_rotation_reuse_concurrency_risk_and_token_version():
- from app.core.system.enterprise_identity import IdentityPolicyError
- from app.core.system.identity_repository import MemoryIdentityRepository
- from app.core.system.identity_sessions import SessionManager
- repo = MemoryIdentityRepository()
- manager = SessionManager(repo, secret="a-long-test-secret", clock=lambda: NOW, max_sessions=2)
- one = manager.create(provider_uid="idp-1", subject="s1", user_uid="u1", roles=["viewer"], identity_source="oidc")
- rotated = manager.refresh(one.refresh_token)
- with pytest.raises(IdentityPolicyError, match="reuse"):
- manager.refresh(one.refresh_token)
- assert repo.get_session(rotated.session_uid)["status"] == "revoked"
- manager.create(provider_uid="idp-1", subject="s1", user_uid="u1", roles=["viewer"], identity_source="oidc")
- latest = manager.create(provider_uid="idp-1", subject="s1", user_uid="u1", roles=["editor"], identity_source="oidc")
- manager.create(provider_uid="idp-1", subject="s1", user_uid="u1", roles=["viewer"], identity_source="oidc")
- assert len([x for x in repo.list_sessions("idp-1", "s1") if x["status"] == "active"]) <= 2
- manager.risk_revoke(latest.session_uid, reason="impossible_travel")
- assert repo.get_session(latest.session_uid)["risk_reason"] == "impossible_travel"
- def test_emergency_access_requires_two_distinct_non_self_approvers_and_window():
- from app.core.system.enterprise_identity import EmergencyAccess, IdentityPolicyError
- from app.core.system.identity_repository import MemoryIdentityRepository
- from app.core.system.identity_sessions import SessionManager
- repo = MemoryIdentityRepository()
- emergency = EmergencyAccess(repo, clock=lambda: NOW)
- with pytest.raises(IdentityPolicyError):
- emergency.request(requester_uid="requester", account_uid="breakglass", reason="IdP outage",
- expires_at=NOW + timedelta(minutes=30), account_is_local_active_admin=False)
- req = emergency.request(requester_uid="requester", account_uid="breakglass", reason="IdP outage",
- expires_at=NOW + timedelta(minutes=30), account_is_local_active_admin=True)
- for approver in ("requester", "breakglass"):
- with pytest.raises(IdentityPolicyError):
- emergency.approve(req["uid"], approver_uid=approver)
- emergency.approve(req["uid"], approver_uid="admin-a")
- with pytest.raises(IdentityPolicyError):
- emergency.activate(req["uid"])
- emergency.approve(req["uid"], approver_uid="admin-b")
- active = emergency.activate(req["uid"])
- assert active["status"] == "active"
- manager = SessionManager(repo, secret="a-long-test-secret", clock=lambda: NOW)
- credentials = manager.create(
- subject="breakglass", user_uid="breakglass", roles=["admin"], identity_source="emergency",
- emergency_request_uid=req["uid"], emergency_expires_at=req["expires_at"])
- rotated = manager.refresh(credentials.refresh_token)
- assert repo.get_session(rotated.session_uid)["expires_at"] == req["expires_at"]
- emergency.close(req["uid"], actor_uid="admin-a")
- with pytest.raises(IdentityPolicyError, match="window"):
- manager.refresh(rotated.refresh_token)
- reviewed = emergency.review(req["uid"], reviewer_uid="admin-b", outcome="approved")
- assert reviewed["status"] == "reviewed"
- def test_identity_audit_sanitizes_sensitive_fields_recursively():
- from app.core.system.enterprise_identity import sanitize_audit_detail
- safe = sanitize_audit_detail({"code": "secret", "token": "secret", "claims": {"sub": "s1"},
- "cookie": "secret", "password": "secret", "assertion": "secret",
- "ok": True, "nested": {"verifier": "secret", "event": "login"}})
- serialized = repr(safe)
- assert "secret" not in serialized and "s1" not in serialized
- assert safe["ok"] is True and safe["nested"]["event"] == "login"
- def test_public_identity_policy_is_method_exact_and_logout_is_viewer_safe():
- from app.core.system.permissions import (
- IDENTITY_READ,
- MANAGE_USERS,
- PUBLIC,
- permission_for_request,
- )
- assert permission_for_request("/api/system/identity/providers", "GET") == (PUBLIC,)
- assert permission_for_request("/api/system/identity/providers", "POST") != (PUBLIC,)
- assert permission_for_request("/api/system/identity/callback", "POST") != (PUBLIC,)
- assert permission_for_request("/api/system/auth/login", "GET") == (MANAGE_USERS,)
- assert permission_for_request("/api/system/identity/logout", "POST") == (IDENTITY_READ,)
- assert permission_for_request("/api/system/identity/idp-versions", "GET") == ("identity:manage",)
- assert permission_for_request("/api/system/identity/sessions", "GET") == ("identity:operate",)
- assert permission_for_request("/api/system/identity/audit", "GET") == ("identity:manage",)
- def test_identity_management_input_helpers_reject_malformed_values():
- from app.api.system.enterprise_identity import _positive_int, _timestamp, _uuid
- from app.core.system.enterprise_identity import IdentityPolicyError
- for value in (None, "not-a-uuid", 123):
- with pytest.raises(IdentityPolicyError):
- _uuid(value, "provider_uid")
- for value in (None, "bad", 0, -1, True, 501):
- with pytest.raises(IdentityPolicyError):
- _positive_int(value, "limit", maximum=500)
- for value in (None, "2026-08-02", "not-a-date"):
- with pytest.raises(IdentityPolicyError):
- _timestamp(value, "expires_at")
- def test_identity_management_api_returns_safe_4xx_and_upstream_502(monkeypatch):
- from app import create_app
- from app.api.system import enterprise_identity as identity_api
- from app.core.system.enterprise_identity import IdentityUpstreamError
- from app.core.system.oidc import OidcTransport
- monkeypatch.setattr(
- "app.core.system.auth.load_identity_from_token",
- lambda token, secret: {"id": "01900000-0000-7000-8000-000000000001", "username": "admin",
- "roles": ["admin"]} if token == "admin" else None,
- )
- monkeypatch.setattr(identity_api, "_audit", lambda *args, **kwargs: None)
- app = create_app()
- app.config.update(TESTING=True)
- client = app.test_client()
- headers = {"Authorization": "Bearer admin"}
- malformed = (
- client.post("/api/system/identity/idp-versions", json={"provider_uid": "bad"}, headers=headers),
- client.get("/api/system/identity/sessions?provider_uid=bad", headers=headers),
- client.get("/api/system/identity/audit?limit=not-an-int", headers=headers),
- client.post("/api/system/identity/directory/delta", json={"provider_uid": "bad"}, headers=headers),
- client.post("/api/system/identity/emergency/requests", json={"account_uid": "bad"}, headers=headers),
- )
- assert all(response.status_code == 400 for response in malformed)
- assert all("Traceback" not in response.get_data(as_text=True) for response in malformed)
- provider_uid = "01900000-0000-7000-8000-000000000099"
- monkeypatch.setattr(identity_api, "_load_config", lambda *args, **kwargs: {
- "provider_uid": provider_uid, "version": 1, "status": "draft",
- "issuer": "https://login.example.com/tenant", "client_id": "dataops",
- "secret_ref": "env:DATAOPS_OIDC_TEST_SECRET",
- "authorization_endpoint": "https://login.example.com/authorize",
- "token_endpoint": "https://login.example.com/token", "jwks_uri": "https://login.example.com/jwks",
- "redirect_uris": ["https://dataops.example.com/callback"], "algorithms": ["RS256"],
- "mapping_version": "v1", "config": {
- "post_login_redirect_uris": ["https://dataops.example.com/login/callback"],
- "group_rules": {"stewards": {"roles": ["viewer"]}},
- },
- })
- monkeypatch.setattr(OidcTransport, "fetch_jwks", lambda self, config: (_ for _ in ()).throw(
- IdentityUpstreamError("internal network detail must not leak")))
- response = client.post(f"/api/system/identity/idp-versions/{provider_uid}/1/connectivity",
- json={}, headers=headers)
- assert response.status_code == 502
- assert "internal network detail" not in response.get_data(as_text=True)
|