fix(oidc): reject an unrecognized boolean instead of guessing

_env_bool returned the default for anything outside {true,1,yes}, so
BAMBUDDY_OIDC_REQUIRE_EMAIL_VERIFIED=on silently read as OFF and
BAMBUDDY_OIDC_ENABLED=on silently disabled the provider -- the exact
opposite of what .env.example claimed. Unrecognized values now raise
EnvOIDCConfigError, caught in _apply_env_oidc_provider the same way a
bad DEFAULT_GROUP or a ValidationError already is: logged and left
running, never released on a typo.

Also promotes _env_bool to env_bool now that it has a call site in
auth.py, and corrects the boolean-parsing sentence in .env.example.
This commit is contained in:
Marian
2026-08-01 09:17:33 +00:00
parent c547505c64
commit 77c9bdd694
5 changed files with 128 additions and 16 deletions
@@ -173,6 +173,55 @@ async def test_a_rejected_config_never_logs_the_client_secret(db_session, monkey
assert "S3CR3T" not in caplog.text # not even a fragment of the value
# --- an unrecognized boolean is rejected, not guessed --------------------------
# `_env_bool` used to return the default for anything outside {true,1,yes}, so
# BAMBUDDY_OIDC_REQUIRE_EMAIL_VERIFIED=on silently read as OFF and
# BAMBUDDY_OIDC_ENABLED=on silently disabled the provider. Strict parsing
# refuses the config instead -- through the same clean path a bad
# DEFAULT_GROUP or a ValidationError already uses, so a typo never releases a
# provider that was running fine.
@pytest.mark.asyncio
async def test_an_unrecognized_require_email_verified_leaves_a_running_provider_intact(db_session, monkeypatch, caplog):
_configure(monkeypatch)
await apply_env_oidc_provider(db_session)
original = await _env_provider(db_session)
original_id, original_enabled = original.id, original.is_enabled
monkeypatch.setenv("BAMBUDDY_OIDC_REQUIRE_EMAIL_VERIFIED", "on")
with caplog.at_level(logging.ERROR):
await apply_env_oidc_provider(db_session)
provider = await _env_provider(db_session)
assert provider is not None, "a typo must not release the provider"
assert provider.id == original_id
assert provider.is_enabled == original_enabled
assert provider.is_env_managed is True
assert "rejected" in caplog.text
assert "BAMBUDDY_OIDC_REQUIRE_EMAIL_VERIFIED" in caplog.text
@pytest.mark.asyncio
async def test_an_unrecognized_enabled_leaves_a_running_provider_intact(db_session, monkeypatch, caplog):
_configure(monkeypatch)
await apply_env_oidc_provider(db_session)
original = await _env_provider(db_session)
original_id, original_enabled = original.id, original.is_enabled
monkeypatch.setenv("BAMBUDDY_OIDC_ENABLED", "on")
with caplog.at_level(logging.ERROR):
await apply_env_oidc_provider(db_session)
provider = await _env_provider(db_session)
assert provider is not None, "a typo must not release the provider"
assert provider.id == original_id
assert provider.is_enabled == original_enabled
assert provider.is_env_managed is True
assert "rejected" in caplog.text
assert "BAMBUDDY_OIDC_ENABLED" in caplog.text
@pytest.mark.asyncio
async def test_a_non_validation_error_is_survivable_and_leaks_nothing(db_session, monkeypatch, caplog):
"""The generic except branch handles anything that isn't a ValidationError
+46 -5
View File
@@ -10,7 +10,7 @@ from __future__ import annotations
import pytest
from backend.app.core.oidc_env import read_env_oidc_config
from backend.app.core.oidc_env import EnvOIDCConfigError, env_bool, read_env_oidc_config
REQUIRED = {
"BAMBUDDY_OIDC_NAME": "Keycloak",
@@ -93,21 +93,62 @@ def test_booleans_accept_the_project_truthy_spellings(monkeypatch, raw):
assert read_env_oidc_config()["auto_create_users"] is True
@pytest.mark.parametrize("raw", ["false", "0", "no", "", "off", "nonsense"])
def test_anything_else_is_false(monkeypatch, raw):
"""Only the three documented spellings enable a flag; an unrecognised value
must not silently turn on auto-create-users."""
@pytest.mark.parametrize("raw", ["false", "FALSE", "False", "0", "no", "NO"])
def test_falsy_values_are_false(monkeypatch, raw):
_set_required(monkeypatch)
monkeypatch.setenv("BAMBUDDY_OIDC_AUTO_CREATE_USERS", raw)
assert read_env_oidc_config()["auto_create_users"] is False
@pytest.mark.parametrize("raw", ["on", "enabled", "y", "nonsense"])
def test_an_unrecognized_boolean_is_rejected(monkeypatch, raw):
"""Only the documented spellings are accepted; an unrecognised value must
not silently turn a flag on or off -- it must refuse the whole config
instead of guessing (M-R4 strict boolean parsing)."""
_set_required(monkeypatch)
monkeypatch.setenv("BAMBUDDY_OIDC_AUTO_CREATE_USERS", raw)
with pytest.raises(EnvOIDCConfigError, match="BAMBUDDY_OIDC_AUTO_CREATE_USERS"):
read_env_oidc_config()
def test_a_boolean_default_of_true_can_be_turned_off(monkeypatch):
_set_required(monkeypatch)
monkeypatch.setenv("BAMBUDDY_OIDC_REQUIRE_EMAIL_VERIFIED", "false")
assert read_env_oidc_config()["require_email_verified"] is False
# --- env_bool, tested directly ------------------------------------------------
# The reader-level tests above pin the contract through read_env_oidc_config;
# these exercise the helper itself so its default/blank/reject behavior is
# proven independently of any particular BAMBUDDY_OIDC_* field.
@pytest.mark.parametrize("raw", ["false", "FALSE", "0", "no", "NO"])
def test_env_bool_falsy_values_are_false(monkeypatch, raw):
monkeypatch.setenv("SOME_FLAG", raw)
assert env_bool("SOME_FLAG", True) is False
@pytest.mark.parametrize("default", [True, False])
def test_env_bool_absent_is_the_given_default(monkeypatch, default):
monkeypatch.delenv("SOME_FLAG", raising=False)
assert env_bool("SOME_FLAG", default) is default
@pytest.mark.parametrize("raw", ["", " "])
@pytest.mark.parametrize("default", [True, False])
def test_env_bool_blank_is_the_given_default(monkeypatch, raw, default):
monkeypatch.setenv("SOME_FLAG", raw)
assert env_bool("SOME_FLAG", default) is default
@pytest.mark.parametrize("raw", ["on", "enabled", "y", "nonsense"])
def test_env_bool_rejects_an_unrecognized_value(monkeypatch, raw):
monkeypatch.setenv("SOME_FLAG", raw)
with pytest.raises(EnvOIDCConfigError, match="SOME_FLAG"):
env_bool("SOME_FLAG", True)
def test_optional_strings_override_their_defaults(monkeypatch):
_set_required(monkeypatch)
monkeypatch.setenv("BAMBUDDY_OIDC_SCOPES", "openid profile groups")