test_oidc_env_lock.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137
  1. """The env-managed provider is read-only through the API (#2593).
  2. Startup rewrites this row from BAMBUDDY_OIDC_* on every boot, so a UI edit
  3. would silently disappear at the next restart -- the operator would see their
  4. change accepted and then reverted, with nothing explaining why. Refusing the
  5. write is the honest answer.
  6. Locking it out is safe because BAMBUDDY_LOCAL_LOGIN (#1589) is the documented
  7. recovery path if the provider itself becomes unusable.
  8. """
  9. from __future__ import annotations
  10. import pytest
  11. from httpx import AsyncClient
  12. from backend.app.models.oidc_provider import OIDCProvider
  13. from backend.tests.integration.test_mfa_api import _auth_header, _setup_and_login
  14. async def _env_managed_provider(db_session) -> int:
  15. provider = OIDCProvider(
  16. name="Env Keycloak",
  17. issuer_url="https://sso.example.com/realms/main",
  18. client_id="bambuddy",
  19. icon_url="https://sso.example.com/logo.png",
  20. is_env_managed=True,
  21. )
  22. provider.client_secret = "s3cr3t"
  23. db_session.add(provider)
  24. await db_session.commit()
  25. await db_session.refresh(provider)
  26. return provider.id
  27. @pytest.mark.asyncio
  28. @pytest.mark.integration
  29. async def test_put_is_refused(async_client: AsyncClient, db_session):
  30. provider_id = await _env_managed_provider(db_session)
  31. token = await _setup_and_login(async_client, "envlockput", "envlockput123")
  32. response = await async_client.put(
  33. f"/api/v1/auth/oidc/providers/{provider_id}",
  34. json={"name": "hijacked"},
  35. headers=_auth_header(token),
  36. )
  37. assert response.status_code == 409
  38. @pytest.mark.asyncio
  39. @pytest.mark.integration
  40. async def test_delete_is_refused(async_client: AsyncClient, db_session):
  41. provider_id = await _env_managed_provider(db_session)
  42. token = await _setup_and_login(async_client, "envlockdel", "envlockdel123")
  43. response = await async_client.delete(
  44. f"/api/v1/auth/oidc/providers/{provider_id}",
  45. headers=_auth_header(token),
  46. )
  47. assert response.status_code == 409
  48. @pytest.mark.asyncio
  49. @pytest.mark.integration
  50. async def test_icon_delete_is_refused(async_client: AsyncClient, db_session):
  51. """The icon is part of the env config too -- BAMBUDDY_OIDC_ICON_URL."""
  52. provider_id = await _env_managed_provider(db_session)
  53. token = await _setup_and_login(async_client, "envlockicondel", "envlockicondel123")
  54. response = await async_client.delete(
  55. f"/api/v1/auth/oidc/providers/{provider_id}/icon",
  56. headers=_auth_header(token),
  57. )
  58. assert response.status_code == 409
  59. @pytest.mark.asyncio
  60. @pytest.mark.integration
  61. async def test_icon_refresh_is_refused(async_client: AsyncClient, db_session):
  62. provider_id = await _env_managed_provider(db_session)
  63. token = await _setup_and_login(async_client, "envlockiconref", "envlockiconref123")
  64. response = await async_client.post(
  65. f"/api/v1/auth/oidc/providers/{provider_id}/icon/refresh",
  66. headers=_auth_header(token),
  67. )
  68. assert response.status_code == 409
  69. @pytest.mark.asyncio
  70. @pytest.mark.integration
  71. async def test_a_ui_provider_is_still_editable(async_client: AsyncClient):
  72. """The lock must not leak onto providers the operator created themselves --
  73. they coexist with the env one and stay fully editable."""
  74. token = await _setup_and_login(async_client, "envlockui", "envlockui123")
  75. created = await async_client.post(
  76. "/api/v1/auth/oidc/providers",
  77. json={
  78. "name": "UI provider",
  79. "issuer_url": "https://other.example.com",
  80. "client_id": "ui",
  81. "client_secret": "ui-secret",
  82. "scopes": "openid",
  83. "is_enabled": True,
  84. "auto_create_users": False,
  85. },
  86. headers=_auth_header(token),
  87. )
  88. provider_id = created.json()["id"]
  89. response = await async_client.put(
  90. f"/api/v1/auth/oidc/providers/{provider_id}",
  91. json={"name": "Renamed"},
  92. headers=_auth_header(token),
  93. )
  94. assert response.status_code == 200
  95. assert response.json()["name"] == "Renamed"
  96. @pytest.mark.asyncio
  97. @pytest.mark.integration
  98. async def test_the_response_says_which_provider_is_env_managed(async_client: AsyncClient, db_session):
  99. """The frontend needs this to render the lock; without it the UI would show
  100. editable fields whose writes the API then refuses."""
  101. await _env_managed_provider(db_session)
  102. token = await _setup_and_login(async_client, "envlockflag", "envlockflag123")
  103. response = await async_client.get("/api/v1/auth/oidc/providers/all", headers=_auth_header(token))
  104. assert response.status_code == 200
  105. providers = response.json()
  106. assert any(p["is_env_managed"] for p in providers)