mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-09-30 03:01:21 +02:00
Startup rewrites this row from BAMBUDDY_OIDC_* on every boot, so an edit through the UI would be accepted and then silently reverted at the next restart -- the operator would watch their change vanish with nothing explaining why. A 409 says so instead. Covers all four mutating routes, including the two icon ones: the icon comes from BAMBUDDY_OIDC_ICON_URL and would be restored the same way. Extracted as one helper rather than four copies of the same check, so a fifth route cannot be added with the guard silently missing. Locking it is safe because BAMBUDDY_LOCAL_LOGIN (#1589) remains the documented recovery path if the provider itself becomes unusable. A test pins that UI-created providers stay editable -- the lock must not leak onto them. Refs #2593
138 lines
4.6 KiB
Python
138 lines
4.6 KiB
Python
"""The env-managed provider is read-only through the API (#2593).
|
|
|
|
Startup rewrites this row from BAMBUDDY_OIDC_* on every boot, so a UI edit
|
|
would silently disappear at the next restart -- the operator would see their
|
|
change accepted and then reverted, with nothing explaining why. Refusing the
|
|
write is the honest answer.
|
|
|
|
Locking it out is safe because BAMBUDDY_LOCAL_LOGIN (#1589) is the documented
|
|
recovery path if the provider itself becomes unusable.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from httpx import AsyncClient
|
|
|
|
from backend.app.models.oidc_provider import OIDCProvider
|
|
from backend.tests.integration.test_mfa_api import _auth_header, _setup_and_login
|
|
|
|
|
|
async def _env_managed_provider(db_session) -> int:
|
|
provider = OIDCProvider(
|
|
name="Env Keycloak",
|
|
issuer_url="https://sso.example.com/realms/main",
|
|
client_id="bambuddy",
|
|
icon_url="https://sso.example.com/logo.png",
|
|
is_env_managed=True,
|
|
)
|
|
provider.client_secret = "s3cr3t"
|
|
db_session.add(provider)
|
|
await db_session.commit()
|
|
await db_session.refresh(provider)
|
|
return provider.id
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.integration
|
|
async def test_put_is_refused(async_client: AsyncClient, db_session):
|
|
provider_id = await _env_managed_provider(db_session)
|
|
token = await _setup_and_login(async_client, "envlockput", "envlockput123")
|
|
|
|
response = await async_client.put(
|
|
f"/api/v1/auth/oidc/providers/{provider_id}",
|
|
json={"name": "hijacked"},
|
|
headers=_auth_header(token),
|
|
)
|
|
|
|
assert response.status_code == 409
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.integration
|
|
async def test_delete_is_refused(async_client: AsyncClient, db_session):
|
|
provider_id = await _env_managed_provider(db_session)
|
|
token = await _setup_and_login(async_client, "envlockdel", "envlockdel123")
|
|
|
|
response = await async_client.delete(
|
|
f"/api/v1/auth/oidc/providers/{provider_id}",
|
|
headers=_auth_header(token),
|
|
)
|
|
|
|
assert response.status_code == 409
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.integration
|
|
async def test_icon_delete_is_refused(async_client: AsyncClient, db_session):
|
|
"""The icon is part of the env config too -- BAMBUDDY_OIDC_ICON_URL."""
|
|
provider_id = await _env_managed_provider(db_session)
|
|
token = await _setup_and_login(async_client, "envlockicondel", "envlockicondel123")
|
|
|
|
response = await async_client.delete(
|
|
f"/api/v1/auth/oidc/providers/{provider_id}/icon",
|
|
headers=_auth_header(token),
|
|
)
|
|
|
|
assert response.status_code == 409
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.integration
|
|
async def test_icon_refresh_is_refused(async_client: AsyncClient, db_session):
|
|
provider_id = await _env_managed_provider(db_session)
|
|
token = await _setup_and_login(async_client, "envlockiconref", "envlockiconref123")
|
|
|
|
response = await async_client.post(
|
|
f"/api/v1/auth/oidc/providers/{provider_id}/icon/refresh",
|
|
headers=_auth_header(token),
|
|
)
|
|
|
|
assert response.status_code == 409
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.integration
|
|
async def test_a_ui_provider_is_still_editable(async_client: AsyncClient):
|
|
"""The lock must not leak onto providers the operator created themselves --
|
|
they coexist with the env one and stay fully editable."""
|
|
token = await _setup_and_login(async_client, "envlockui", "envlockui123")
|
|
created = await async_client.post(
|
|
"/api/v1/auth/oidc/providers",
|
|
json={
|
|
"name": "UI provider",
|
|
"issuer_url": "https://other.example.com",
|
|
"client_id": "ui",
|
|
"client_secret": "ui-secret",
|
|
"scopes": "openid",
|
|
"is_enabled": True,
|
|
"auto_create_users": False,
|
|
},
|
|
headers=_auth_header(token),
|
|
)
|
|
provider_id = created.json()["id"]
|
|
|
|
response = await async_client.put(
|
|
f"/api/v1/auth/oidc/providers/{provider_id}",
|
|
json={"name": "Renamed"},
|
|
headers=_auth_header(token),
|
|
)
|
|
|
|
assert response.status_code == 200
|
|
assert response.json()["name"] == "Renamed"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.integration
|
|
async def test_the_response_says_which_provider_is_env_managed(async_client: AsyncClient, db_session):
|
|
"""The frontend needs this to render the lock; without it the UI would show
|
|
editable fields whose writes the API then refuses."""
|
|
await _env_managed_provider(db_session)
|
|
token = await _setup_and_login(async_client, "envlockflag", "envlockflag123")
|
|
|
|
response = await async_client.get("/api/v1/auth/oidc/providers/all", headers=_auth_header(token))
|
|
|
|
assert response.status_code == 200
|
|
providers = response.json()
|
|
assert any(p["is_env_managed"] for p in providers)
|