mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-09-28 22:06:05 +01:00
Auth mode resolution fell back to "none" (anonymous full admin) whenever the configured method's prerequisites were missing: no local password admin for builtin/OIDC, no Calibre-Web database, a blank proxy header, an unrecognized AUTH_METHOD (including "OIDC" in uppercase), or any error while reading the config. Deleting or demoting the last local admin was allowed on purpose because of that fallback, which exposed OIDC instances publicly. - Only an explicit AUTH_METHOD=none disables authentication. A configured method stays active when its prerequisites are missing, so sign-in fails instead of opening up. - An unrecognized or unreadable AUTH_METHOD resolves to "unavailable", which still requires a session and accepts no login. Values are normalized, so AUTH_METHOD=OIDC works. - Restore the guard against deleting or demoting the last local password admin while builtin/OIDC is active (unless DISABLE_LOCAL_AUTH is set). - Require a local admin before enabling Local auth, as OIDC already did. - Log a recovery hint at startup when builtin/OIDC runs without a local admin, and document recovery via AUTH_METHOD=none. - Drop the "will fall back to No Authentication" UI toasts and hints. Fixes https://github.com/calibrain/shelfmark/issues/1387
163 lines
6.1 KiB
Python
163 lines
6.1 KiB
Python
"""Tests for auth mode and admin policy helpers used by OIDC integration."""
|
|
|
|
import pytest
|
|
|
|
from shelfmark.core.auth_modes import (
|
|
AUTH_MODE_UNAVAILABLE,
|
|
determine_auth_mode,
|
|
get_auth_check_admin_status,
|
|
get_settings_tab_from_path,
|
|
is_settings_or_onboarding_path,
|
|
load_active_auth_mode,
|
|
requires_admin_for_settings_access,
|
|
should_restrict_settings_to_admin,
|
|
)
|
|
|
|
|
|
class TestDetermineAuthMode:
|
|
@pytest.mark.parametrize("auth_method", ["builtin", "oidc", "proxy", "cwa", "none"])
|
|
def test_returns_configured_method(self, auth_method):
|
|
assert determine_auth_mode(auth_method) == auth_method
|
|
|
|
@pytest.mark.parametrize(
|
|
("auth_method", "expected"), [("OIDC", "oidc"), (" Builtin ", "builtin")]
|
|
)
|
|
def test_normalizes_case_and_whitespace(self, auth_method, expected):
|
|
assert determine_auth_mode(auth_method) == expected
|
|
|
|
@pytest.mark.parametrize("auth_method", [None, "", " "])
|
|
def test_unset_method_means_none(self, auth_method):
|
|
assert determine_auth_mode(auth_method) == "none"
|
|
|
|
@pytest.mark.parametrize("auth_method", ["local", "ldap", "openid", "no"])
|
|
def test_unrecognized_method_fails_closed(self, auth_method):
|
|
assert determine_auth_mode(auth_method) == AUTH_MODE_UNAVAILABLE
|
|
|
|
def test_load_active_auth_mode_fails_closed_when_config_unreadable(self, monkeypatch):
|
|
from shelfmark.core.config import config as app_config
|
|
|
|
def _boom(*_args, **_kwargs):
|
|
raise RuntimeError("boom")
|
|
|
|
monkeypatch.setattr(app_config, "get", _boom)
|
|
assert load_active_auth_mode() == AUTH_MODE_UNAVAILABLE
|
|
|
|
@pytest.mark.parametrize("auth_method", ["builtin", "oidc"])
|
|
def test_load_active_auth_mode_keeps_local_modes_without_local_admin(
|
|
self, monkeypatch, tmp_path, auth_method
|
|
):
|
|
"""Regression for #1387: no local admin must not turn authentication off."""
|
|
from shelfmark.core.config import config as app_config
|
|
|
|
monkeypatch.setenv("CONFIG_DIR", str(tmp_path))
|
|
monkeypatch.setenv("AUTH_METHOD", auth_method)
|
|
app_config.refresh(force=True)
|
|
|
|
try:
|
|
assert load_active_auth_mode() == auth_method
|
|
finally:
|
|
monkeypatch.delenv("AUTH_METHOD", raising=False)
|
|
app_config.refresh(force=True)
|
|
|
|
def test_load_active_auth_mode_keeps_cwa_without_database(self, monkeypatch, tmp_path):
|
|
from shelfmark.core.config import config as app_config
|
|
|
|
monkeypatch.setenv("CONFIG_DIR", str(tmp_path))
|
|
monkeypatch.setenv("AUTH_METHOD", "cwa")
|
|
app_config.refresh(force=True)
|
|
|
|
try:
|
|
assert load_active_auth_mode() == "cwa"
|
|
finally:
|
|
monkeypatch.delenv("AUTH_METHOD", raising=False)
|
|
app_config.refresh(force=True)
|
|
|
|
def test_load_active_auth_mode_reads_env_backed_proxy_setting(self, monkeypatch, tmp_path):
|
|
from shelfmark.core.config import config as app_config
|
|
|
|
monkeypatch.setenv("CONFIG_DIR", str(tmp_path))
|
|
monkeypatch.setenv("AUTH_METHOD", "proxy")
|
|
monkeypatch.setenv("PROXY_AUTH_USER_HEADER", "X-Forwarded-User")
|
|
app_config.refresh(force=True)
|
|
|
|
try:
|
|
assert load_active_auth_mode() == "proxy"
|
|
finally:
|
|
monkeypatch.delenv("AUTH_METHOD", raising=False)
|
|
monkeypatch.delenv("PROXY_AUTH_USER_HEADER", raising=False)
|
|
app_config.refresh(force=True)
|
|
|
|
|
|
class TestSettingsRestrictionPolicy:
|
|
def test_settings_path_detection(self):
|
|
assert is_settings_or_onboarding_path("/api/settings/downloads")
|
|
assert is_settings_or_onboarding_path("/api/onboarding")
|
|
assert not is_settings_or_onboarding_path("/api/releases")
|
|
|
|
def test_default_is_admin_restricted(self):
|
|
assert should_restrict_settings_to_admin({}) is True
|
|
|
|
def test_restriction_is_always_enabled(self):
|
|
assert should_restrict_settings_to_admin({"RESTRICT_SETTINGS_TO_ADMIN": True}) is True
|
|
assert should_restrict_settings_to_admin({"RESTRICT_SETTINGS_TO_ADMIN": False}) is True
|
|
|
|
def test_extracts_settings_tab_from_path(self):
|
|
assert get_settings_tab_from_path("/api/settings/security") == "security"
|
|
assert get_settings_tab_from_path("/api/settings/users/action/open_users_tab") == "users"
|
|
assert get_settings_tab_from_path("/api/settings") is None
|
|
|
|
def test_security_and_users_tabs_always_require_admin(self):
|
|
users_config = {"RESTRICT_SETTINGS_TO_ADMIN": False}
|
|
assert requires_admin_for_settings_access("/api/settings/security", users_config) is True
|
|
assert requires_admin_for_settings_access("/api/settings/users", users_config) is True
|
|
|
|
def test_other_tabs_also_require_admin(self):
|
|
assert (
|
|
requires_admin_for_settings_access(
|
|
"/api/settings/general",
|
|
{"RESTRICT_SETTINGS_TO_ADMIN": False},
|
|
)
|
|
is True
|
|
)
|
|
assert (
|
|
requires_admin_for_settings_access(
|
|
"/api/settings/general",
|
|
{"RESTRICT_SETTINGS_TO_ADMIN": True},
|
|
)
|
|
is True
|
|
)
|
|
|
|
|
|
class TestAuthCheckAdminStatus:
|
|
def test_authenticated_admin_when_restricted(self):
|
|
result = get_auth_check_admin_status(
|
|
"oidc",
|
|
{"RESTRICT_SETTINGS_TO_ADMIN": True},
|
|
{"user_id": "admin", "is_admin": True},
|
|
)
|
|
assert result is True
|
|
|
|
def test_authenticated_non_admin_when_restricted(self):
|
|
result = get_auth_check_admin_status(
|
|
"oidc",
|
|
{"RESTRICT_SETTINGS_TO_ADMIN": True},
|
|
{"user_id": "user", "is_admin": False},
|
|
)
|
|
assert result is False
|
|
|
|
def test_authenticated_non_admin_user_is_not_admin(self):
|
|
result = get_auth_check_admin_status(
|
|
"proxy",
|
|
{"RESTRICT_SETTINGS_TO_ADMIN": False},
|
|
{"user_id": "user", "is_admin": False},
|
|
)
|
|
assert result is False
|
|
|
|
def test_unauthenticated_is_never_admin(self):
|
|
result = get_auth_check_admin_status(
|
|
"builtin",
|
|
{"RESTRICT_SETTINGS_TO_ADMIN": False},
|
|
{"is_admin": True},
|
|
)
|
|
assert result is False
|