mirror of
https://github.com/calibrain/shelfmark.git
synced 2026-09-29 22:10:51 +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
593 lines
24 KiB
Python
593 lines
24 KiB
Python
"""
|
|
Tests for security configuration and migration.
|
|
|
|
Tests the security settings registration, migration from old settings,
|
|
and current on-save guard behavior.
|
|
"""
|
|
|
|
import json
|
|
import tempfile
|
|
from pathlib import Path
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from shelfmark.core.user_db import UserDB
|
|
|
|
|
|
def _set_config_dir(monkeypatch, config_dir: Path) -> None:
|
|
"""Point config helpers at a test-local config directory."""
|
|
monkeypatch.setenv("CONFIG_DIR", str(config_dir))
|
|
monkeypatch.setattr("shelfmark.config.env.CONFIG_DIR", config_dir)
|
|
|
|
|
|
@pytest.fixture
|
|
def temp_config_dir():
|
|
"""Create a temporary config directory for tests."""
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
config_dir = Path(tmpdir)
|
|
security_dir = config_dir / "security"
|
|
security_dir.mkdir(parents=True, exist_ok=True)
|
|
yield security_dir
|
|
|
|
|
|
@pytest.fixture
|
|
def mock_logger():
|
|
"""Mock logger to capture log messages."""
|
|
return MagicMock()
|
|
|
|
|
|
class TestSecurityMigration:
|
|
"""Tests for migrating legacy security settings."""
|
|
|
|
def test_migrate_use_cwa_auth_true_syncs_legacy_admin(
|
|
self, temp_config_dir, mock_logger, monkeypatch
|
|
):
|
|
"""USE_CWA_AUTH=True migrates to cwa and keeps legacy creds synced to users DB."""
|
|
config_root = temp_config_dir.parent
|
|
monkeypatch.setenv("CONFIG_DIR", str(config_root))
|
|
|
|
config_file = temp_config_dir / "config.json"
|
|
legacy_config = {
|
|
"USE_CWA_AUTH": True,
|
|
"BUILTIN_USERNAME": "admin",
|
|
"BUILTIN_PASSWORD_HASH": "hashed_password",
|
|
}
|
|
config_file.write_text(json.dumps(legacy_config, indent=2))
|
|
|
|
with patch("shelfmark.config.security.load_config_file", return_value=legacy_config.copy()):
|
|
with patch(
|
|
"shelfmark.core.settings_registry._get_config_file_path",
|
|
return_value=str(config_file),
|
|
):
|
|
with patch("shelfmark.core.settings_registry._ensure_config_dir"):
|
|
with patch("shelfmark.config.security.logger", mock_logger):
|
|
from shelfmark.config.security import _migrate_security_settings
|
|
|
|
_migrate_security_settings()
|
|
|
|
migrated = json.loads(config_file.read_text())
|
|
assert migrated["AUTH_METHOD"] == "cwa"
|
|
assert "USE_CWA_AUTH" not in migrated
|
|
assert migrated["BUILTIN_USERNAME"] == "admin"
|
|
assert migrated["BUILTIN_PASSWORD_HASH"] == "hashed_password"
|
|
|
|
user_db = UserDB(str(config_root / "users.db"))
|
|
user_db.initialize()
|
|
user = user_db.get_user(username="admin")
|
|
assert user is not None
|
|
assert user["role"] == "admin"
|
|
assert user["auth_source"] == "builtin"
|
|
assert user["password_hash"] == "hashed_password"
|
|
|
|
def test_migrate_use_cwa_auth_false_with_credentials(
|
|
self, temp_config_dir, mock_logger, monkeypatch
|
|
):
|
|
"""USE_CWA_AUTH=False with creds migrates to builtin and syncs users DB."""
|
|
config_root = temp_config_dir.parent
|
|
monkeypatch.setenv("CONFIG_DIR", str(config_root))
|
|
|
|
config_file = temp_config_dir / "config.json"
|
|
legacy_config = {
|
|
"USE_CWA_AUTH": False,
|
|
"BUILTIN_USERNAME": "admin",
|
|
"BUILTIN_PASSWORD_HASH": "hashed_password",
|
|
}
|
|
config_file.write_text(json.dumps(legacy_config, indent=2))
|
|
|
|
with patch("shelfmark.config.security.load_config_file", return_value=legacy_config.copy()):
|
|
with patch(
|
|
"shelfmark.core.settings_registry._get_config_file_path",
|
|
return_value=str(config_file),
|
|
):
|
|
with patch("shelfmark.core.settings_registry._ensure_config_dir"):
|
|
with patch("shelfmark.config.security.logger", mock_logger):
|
|
from shelfmark.config.security import _migrate_security_settings
|
|
|
|
_migrate_security_settings()
|
|
|
|
migrated = json.loads(config_file.read_text())
|
|
assert migrated["AUTH_METHOD"] == "builtin"
|
|
assert "USE_CWA_AUTH" not in migrated
|
|
assert migrated["BUILTIN_USERNAME"] == "admin"
|
|
assert migrated["BUILTIN_PASSWORD_HASH"] == "hashed_password"
|
|
|
|
user_db = UserDB(str(config_root / "users.db"))
|
|
user_db.initialize()
|
|
user = user_db.get_user(username="admin")
|
|
assert user is not None
|
|
assert user["role"] == "admin"
|
|
|
|
def test_migrate_use_cwa_auth_false_without_credentials(self, temp_config_dir, mock_logger):
|
|
"""USE_CWA_AUTH=False without creds migrates to none."""
|
|
config_file = temp_config_dir / "config.json"
|
|
legacy_config = {"USE_CWA_AUTH": False}
|
|
config_file.write_text(json.dumps(legacy_config, indent=2))
|
|
|
|
with patch("shelfmark.config.security.load_config_file", return_value=legacy_config.copy()):
|
|
with patch(
|
|
"shelfmark.core.settings_registry._get_config_file_path",
|
|
return_value=str(config_file),
|
|
):
|
|
with patch("shelfmark.core.settings_registry._ensure_config_dir"):
|
|
with patch("shelfmark.config.security.logger", mock_logger):
|
|
from shelfmark.config.security import _migrate_security_settings
|
|
|
|
_migrate_security_settings()
|
|
|
|
migrated = json.loads(config_file.read_text())
|
|
assert migrated["AUTH_METHOD"] == "none"
|
|
assert "USE_CWA_AUTH" not in migrated
|
|
|
|
def test_migrate_restrict_settings_to_admin(self, temp_config_dir, mock_logger):
|
|
"""Legacy settings restriction should migrate to users tab global toggle."""
|
|
config_file = temp_config_dir / "config.json"
|
|
legacy_config = {
|
|
"AUTH_METHOD": "cwa",
|
|
"RESTRICT_SETTINGS_TO_ADMIN": True,
|
|
}
|
|
config_file.write_text(json.dumps(legacy_config, indent=2))
|
|
|
|
def _load_config(tab_name: str):
|
|
if tab_name == "security":
|
|
return legacy_config.copy()
|
|
if tab_name == "users":
|
|
return {}
|
|
return {}
|
|
|
|
with patch("shelfmark.config.security.load_config_file", side_effect=_load_config):
|
|
with patch(
|
|
"shelfmark.core.settings_registry._get_config_file_path",
|
|
return_value=str(config_file),
|
|
):
|
|
with patch("shelfmark.core.settings_registry._ensure_config_dir"):
|
|
with patch(
|
|
"shelfmark.core.settings_registry.save_config_file"
|
|
) as mock_save_config:
|
|
with patch("shelfmark.config.security.logger", mock_logger):
|
|
from shelfmark.config.security import _migrate_security_settings
|
|
|
|
_migrate_security_settings()
|
|
|
|
migrated = json.loads(config_file.read_text())
|
|
assert "RESTRICT_SETTINGS_TO_ADMIN" not in migrated
|
|
mock_save_config.assert_called_with("users", {"RESTRICT_SETTINGS_TO_ADMIN": True})
|
|
|
|
def test_migrate_proxy_restriction_to_users_global(self, temp_config_dir, mock_logger):
|
|
"""Proxy-specific restriction should migrate to users.RESTRICT_SETTINGS_TO_ADMIN."""
|
|
config_file = temp_config_dir / "config.json"
|
|
legacy_config = {
|
|
"AUTH_METHOD": "proxy",
|
|
"PROXY_AUTH_RESTRICT_SETTINGS_TO_ADMIN": False,
|
|
}
|
|
config_file.write_text(json.dumps(legacy_config, indent=2))
|
|
|
|
def _load_config(tab_name: str):
|
|
if tab_name == "security":
|
|
return legacy_config.copy()
|
|
if tab_name == "users":
|
|
return {}
|
|
return {}
|
|
|
|
with patch("shelfmark.config.security.load_config_file", side_effect=_load_config):
|
|
with patch(
|
|
"shelfmark.core.settings_registry._get_config_file_path",
|
|
return_value=str(config_file),
|
|
):
|
|
with patch("shelfmark.core.settings_registry._ensure_config_dir"):
|
|
with patch(
|
|
"shelfmark.core.settings_registry.save_config_file"
|
|
) as mock_save_config:
|
|
with patch("shelfmark.config.security.logger", mock_logger):
|
|
from shelfmark.config.security import _migrate_security_settings
|
|
|
|
_migrate_security_settings()
|
|
|
|
migrated = json.loads(config_file.read_text())
|
|
assert "PROXY_AUTH_RESTRICT_SETTINGS_TO_ADMIN" not in migrated
|
|
mock_save_config.assert_called_with("users", {"RESTRICT_SETTINGS_TO_ADMIN": False})
|
|
|
|
def test_migrate_preserves_existing_auth_method(self, temp_config_dir, mock_logger):
|
|
"""Existing AUTH_METHOD should not be overwritten."""
|
|
config_file = temp_config_dir / "config.json"
|
|
legacy_config = {
|
|
"USE_CWA_AUTH": True,
|
|
"AUTH_METHOD": "proxy",
|
|
}
|
|
config_file.write_text(json.dumps(legacy_config, indent=2))
|
|
|
|
with patch("shelfmark.config.security.load_config_file", return_value=legacy_config.copy()):
|
|
with patch(
|
|
"shelfmark.core.settings_registry._get_config_file_path",
|
|
return_value=str(config_file),
|
|
):
|
|
with patch("shelfmark.core.settings_registry._ensure_config_dir"):
|
|
with patch("shelfmark.config.security.logger", mock_logger):
|
|
from shelfmark.config.security import _migrate_security_settings
|
|
|
|
_migrate_security_settings()
|
|
|
|
migrated = json.loads(config_file.read_text())
|
|
assert migrated["AUTH_METHOD"] == "proxy"
|
|
assert "USE_CWA_AUTH" not in migrated
|
|
|
|
def test_migrate_backfills_auth_method_from_legacy_builtin_credentials(
|
|
self, temp_config_dir, mock_logger, monkeypatch
|
|
):
|
|
"""Configs with BUILTIN creds but no AUTH_METHOD should be backfilled to builtin."""
|
|
config_root = temp_config_dir.parent
|
|
monkeypatch.setenv("CONFIG_DIR", str(config_root))
|
|
|
|
config_file = temp_config_dir / "config.json"
|
|
legacy_config = {
|
|
"BUILTIN_USERNAME": "admin",
|
|
"BUILTIN_PASSWORD_HASH": "hashed_password",
|
|
}
|
|
config_file.write_text(json.dumps(legacy_config, indent=2))
|
|
|
|
with patch("shelfmark.config.security.load_config_file", return_value=legacy_config.copy()):
|
|
with patch(
|
|
"shelfmark.core.settings_registry._get_config_file_path",
|
|
return_value=str(config_file),
|
|
):
|
|
with patch("shelfmark.core.settings_registry._ensure_config_dir"):
|
|
with patch("shelfmark.config.security.logger", mock_logger):
|
|
from shelfmark.config.security import _migrate_security_settings
|
|
|
|
_migrate_security_settings()
|
|
|
|
migrated = json.loads(config_file.read_text())
|
|
assert migrated["AUTH_METHOD"] == "builtin"
|
|
assert migrated["BUILTIN_USERNAME"] == "admin"
|
|
assert migrated["BUILTIN_PASSWORD_HASH"] == "hashed_password"
|
|
|
|
user_db = UserDB(str(config_root / "users.db"))
|
|
user_db.initialize()
|
|
user = user_db.get_user(username="admin")
|
|
assert user is not None
|
|
assert user["role"] == "admin"
|
|
|
|
def test_migrate_handles_missing_config_file(self, mock_logger):
|
|
"""Missing config file should be handled gracefully."""
|
|
with patch("shelfmark.config.security.load_config_file", side_effect=FileNotFoundError()):
|
|
with patch("shelfmark.config.security.logger", mock_logger):
|
|
from shelfmark.config.security import _migrate_security_settings
|
|
|
|
_migrate_security_settings()
|
|
|
|
mock_logger.debug.assert_any_call(
|
|
"No existing security config file found - nothing to migrate"
|
|
)
|
|
|
|
def test_migrate_no_changes_needed(self, temp_config_dir, mock_logger):
|
|
"""No-op migration should not rewrite config."""
|
|
config_file = temp_config_dir / "config.json"
|
|
modern_config = {
|
|
"AUTH_METHOD": "builtin",
|
|
"BUILTIN_USERNAME": "admin",
|
|
"BUILTIN_PASSWORD_HASH": "hashed_password",
|
|
}
|
|
config_file.write_text(json.dumps(modern_config, indent=2))
|
|
|
|
with patch("shelfmark.config.security.load_config_file", return_value=modern_config.copy()):
|
|
with patch(
|
|
"shelfmark.core.settings_registry._get_config_file_path",
|
|
return_value=str(config_file),
|
|
):
|
|
with patch("shelfmark.core.settings_registry._ensure_config_dir"):
|
|
with patch("shelfmark.config.security.logger", mock_logger):
|
|
from shelfmark.config.security import _migrate_security_settings
|
|
|
|
_migrate_security_settings()
|
|
|
|
final_config = json.loads(config_file.read_text())
|
|
assert final_config == modern_config
|
|
|
|
|
|
class TestSecuritySettings:
|
|
"""Tests for security settings registration."""
|
|
|
|
def test_security_settings_without_cwa_shows_warning_but_keeps_option(self):
|
|
"""CWA remains selectable but warns when the DB is unavailable."""
|
|
with patch("shelfmark.config.env.CWA_DB_PATH", None):
|
|
import importlib
|
|
|
|
import shelfmark.config.security
|
|
|
|
importlib.reload(shelfmark.config.security)
|
|
from shelfmark.config.security import security_settings
|
|
|
|
fields = security_settings()
|
|
auth_method_field = next((f for f in fields if f.key == "AUTH_METHOD"), None)
|
|
assert auth_method_field is not None
|
|
|
|
option_values = [opt["value"] for opt in auth_method_field.options]
|
|
assert "none" in option_values
|
|
assert "builtin" in option_values
|
|
assert "proxy" in option_values
|
|
assert "cwa" in option_values
|
|
|
|
cwa_warning_field = next((f for f in fields if f.key == "cwa_db_missing"), None)
|
|
assert cwa_warning_field is not None
|
|
|
|
def test_security_settings_with_cwa(self):
|
|
"""CWA option should be shown when DB is mounted."""
|
|
mock_path = MagicMock()
|
|
mock_path.exists.return_value = True
|
|
|
|
with patch("shelfmark.config.env.CWA_DB_PATH", mock_path):
|
|
import importlib
|
|
|
|
import shelfmark.config.security
|
|
|
|
importlib.reload(shelfmark.config.security)
|
|
from shelfmark.config.security import security_settings
|
|
|
|
fields = security_settings()
|
|
auth_method_field = next((f for f in fields if f.key == "AUTH_METHOD"), None)
|
|
assert auth_method_field is not None
|
|
|
|
option_values = [opt["value"] for opt in auth_method_field.options]
|
|
assert "cwa" in option_values
|
|
|
|
def test_builtin_credential_fields_hidden(self):
|
|
"""Builtin username/password fields should be removed from settings UI."""
|
|
from shelfmark.config.security import security_settings
|
|
|
|
fields = security_settings()
|
|
field_keys = [f.key for f in fields]
|
|
|
|
assert "BUILTIN_USERNAME" not in field_keys
|
|
assert "BUILTIN_PASSWORD" not in field_keys
|
|
assert "BUILTIN_PASSWORD_CONFIRM" not in field_keys
|
|
|
|
def test_builtin_notice_field_removed(self):
|
|
"""Builtin guidance should be handled by the action button only."""
|
|
from shelfmark.config.security import security_settings
|
|
|
|
fields = security_settings()
|
|
notice = next((f for f in fields if f.key == "builtin_auth_notice"), None)
|
|
assert notice is None
|
|
|
|
def test_builtin_admin_requirement_hint_present(self):
|
|
"""Builtin mode should show local-admin requirement warning."""
|
|
from shelfmark.config.security import security_settings
|
|
|
|
fields = security_settings()
|
|
hint = next((f for f in fields if f.key == "builtin_admin_requirement"), None)
|
|
assert hint is not None
|
|
assert hint.component == "oidc_admin_hint"
|
|
assert hint.show_when == {"field": "AUTH_METHOD", "value": "builtin"}
|
|
assert "required" in hint.label.lower()
|
|
assert "local admin" in hint.label.lower()
|
|
|
|
def test_oidc_admin_requirement_hint_absent_when_local_auth_is_disabled(self):
|
|
"""OIDC mode should not show the local-admin warning when local auth is disabled."""
|
|
from shelfmark.config.security import security_settings
|
|
|
|
with patch("shelfmark.config.env.DISABLE_LOCAL_AUTH", True):
|
|
fields = security_settings()
|
|
|
|
hint = next((f for f in fields if f.key == "oidc_admin_requirement"), None)
|
|
assert hint is None
|
|
|
|
def test_builtin_option_label_is_local(self):
|
|
"""Builtin auth option should be labeled Local."""
|
|
from shelfmark.config.security import security_settings
|
|
|
|
fields = security_settings()
|
|
auth_field = next((f for f in fields if f.key == "AUTH_METHOD"), None)
|
|
builtin_option = next(
|
|
(opt for opt in auth_field.options if opt["value"] == "builtin"), None
|
|
)
|
|
assert builtin_option is not None
|
|
assert builtin_option["label"] == "Local"
|
|
|
|
def test_builtin_users_navigation_action_present(self):
|
|
"""Builtin mode should include an action button to open Users tab."""
|
|
from shelfmark.config.security import security_settings
|
|
|
|
fields = security_settings()
|
|
action = next((f for f in fields if f.key == "open_users_tab"), None)
|
|
assert action is not None
|
|
assert action.label == "Go to Users"
|
|
assert action.show_when == {"field": "AUTH_METHOD", "value": ["builtin", "oidc"]}
|
|
|
|
|
|
class TestSecurityOnSave:
|
|
"""Tests for current security on-save guard behavior."""
|
|
|
|
def test_on_save_passthrough_for_proxy(self, tmp_path, monkeypatch):
|
|
from shelfmark.config.security import _on_save_security
|
|
|
|
_set_config_dir(monkeypatch, tmp_path)
|
|
values = {"AUTH_METHOD": "proxy", "PROXY_AUTH_USER_HEADER": "X-Auth-User"}
|
|
|
|
result = _on_save_security(values.copy())
|
|
|
|
assert result["error"] is False
|
|
assert result["values"] == values
|
|
|
|
def test_on_save_blocks_builtin_without_local_admin(self, tmp_path, monkeypatch):
|
|
from shelfmark.config.security import _on_save_security
|
|
|
|
_set_config_dir(monkeypatch, tmp_path)
|
|
UserDB(str(tmp_path / "users.db")).initialize()
|
|
|
|
result = _on_save_security({"AUTH_METHOD": "builtin"})
|
|
|
|
assert result["error"] is True
|
|
assert "local admin" in result["message"].lower()
|
|
|
|
def test_on_save_allows_builtin_with_local_admin(self, tmp_path, monkeypatch):
|
|
from shelfmark.config.security import _on_save_security
|
|
|
|
_set_config_dir(monkeypatch, tmp_path)
|
|
user_db = UserDB(str(tmp_path / "users.db"))
|
|
user_db.initialize()
|
|
user_db.create_user(username="admin", password_hash="hash", role="admin")
|
|
|
|
result = _on_save_security({"AUTH_METHOD": "builtin"})
|
|
|
|
assert result["error"] is False
|
|
|
|
def test_on_save_blocks_oidc_without_local_admin(self, tmp_path, monkeypatch):
|
|
from shelfmark.config.security import _on_save_security
|
|
|
|
_set_config_dir(monkeypatch, tmp_path)
|
|
UserDB(str(tmp_path / "users.db")).initialize()
|
|
|
|
result = _on_save_security({"AUTH_METHOD": "oidc"})
|
|
|
|
assert result["error"] is True
|
|
assert "local admin" in result["message"].lower()
|
|
|
|
def test_on_save_allows_oidc_without_local_admin_when_local_auth_is_disabled(
|
|
self, tmp_path, monkeypatch
|
|
):
|
|
from shelfmark.config.security import _on_save_security
|
|
|
|
_set_config_dir(monkeypatch, tmp_path)
|
|
UserDB(str(tmp_path / "users.db")).initialize()
|
|
|
|
with patch("shelfmark.config.security_handlers.DISABLE_LOCAL_AUTH", True):
|
|
result = _on_save_security(
|
|
{
|
|
"AUTH_METHOD": "oidc",
|
|
"OIDC_DISCOVERY_URL": (
|
|
"https://auth.example.com/.well-known/openid-configuration"
|
|
),
|
|
"OIDC_CLIENT_ID": "shelfmark",
|
|
"OIDC_CLIENT_SECRET": "secret123",
|
|
}
|
|
)
|
|
|
|
assert result["error"] is False
|
|
|
|
def test_on_save_blocks_oidc_when_client_id_is_missing(self, tmp_path, monkeypatch):
|
|
from shelfmark.config.security import _on_save_security
|
|
|
|
_set_config_dir(monkeypatch, tmp_path)
|
|
user_db = UserDB(str(tmp_path / "users.db"))
|
|
user_db.initialize()
|
|
user_db.create_user(username="admin", password_hash="hash", role="admin")
|
|
|
|
result = _on_save_security(
|
|
{
|
|
"AUTH_METHOD": "oidc",
|
|
"OIDC_DISCOVERY_URL": "https://auth.example.com/.well-known/openid-configuration",
|
|
"OIDC_CLIENT_SECRET": "secret123",
|
|
}
|
|
)
|
|
|
|
assert result["error"] is True
|
|
assert "client id" in result["message"].lower()
|
|
|
|
def test_on_save_blocks_oidc_when_discovery_url_is_missing(self, tmp_path, monkeypatch):
|
|
from shelfmark.config.security import _on_save_security
|
|
|
|
_set_config_dir(monkeypatch, tmp_path)
|
|
user_db = UserDB(str(tmp_path / "users.db"))
|
|
user_db.initialize()
|
|
user_db.create_user(username="admin", password_hash="hash", role="admin")
|
|
|
|
result = _on_save_security(
|
|
{
|
|
"AUTH_METHOD": "oidc",
|
|
"OIDC_CLIENT_ID": "shelfmark",
|
|
"OIDC_CLIENT_SECRET": "secret123",
|
|
}
|
|
)
|
|
|
|
assert result["error"] is True
|
|
assert "discovery url" in result["message"].lower()
|
|
|
|
def test_on_save_blocks_oidc_when_secret_is_missing(self, tmp_path, monkeypatch):
|
|
from shelfmark.config.security import _on_save_security
|
|
|
|
_set_config_dir(monkeypatch, tmp_path)
|
|
user_db = UserDB(str(tmp_path / "users.db"))
|
|
user_db.initialize()
|
|
user_db.create_user(username="admin", password_hash="hash", role="admin")
|
|
|
|
result = _on_save_security(
|
|
{
|
|
"AUTH_METHOD": "oidc",
|
|
"OIDC_DISCOVERY_URL": "https://auth.example.com/.well-known/openid-configuration",
|
|
"OIDC_CLIENT_ID": "shelfmark",
|
|
}
|
|
)
|
|
|
|
assert result["error"] is True
|
|
assert "client secret" in result["message"].lower()
|
|
|
|
def test_on_save_allows_oidc_with_existing_saved_secret(self, tmp_path, monkeypatch):
|
|
from shelfmark.config.security import _on_save_security
|
|
from shelfmark.core.settings_registry import save_config_file
|
|
|
|
_set_config_dir(monkeypatch, tmp_path)
|
|
user_db = UserDB(str(tmp_path / "users.db"))
|
|
user_db.initialize()
|
|
user_db.create_user(username="admin", password_hash="hash", role="admin")
|
|
save_config_file(
|
|
"security",
|
|
{
|
|
"AUTH_METHOD": "oidc",
|
|
"OIDC_DISCOVERY_URL": "https://auth.example.com/.well-known/openid-configuration",
|
|
"OIDC_CLIENT_ID": "existing-client",
|
|
"OIDC_CLIENT_SECRET": "saved-secret",
|
|
},
|
|
)
|
|
|
|
result = _on_save_security({"AUTH_METHOD": "oidc", "OIDC_CLIENT_ID": "updated-client"})
|
|
|
|
assert result["error"] is False
|
|
assert result["values"]["OIDC_CLIENT_ID"] == "updated-client"
|
|
|
|
def test_on_save_normalizes_oidc_discovery_url_without_stripping_trailing_slash(
|
|
self, tmp_path, monkeypatch
|
|
):
|
|
from shelfmark.config.security import _on_save_security
|
|
|
|
_set_config_dir(monkeypatch, tmp_path)
|
|
values = {"OIDC_DISCOVERY_URL": " 'auth.example.com/.well-known/openid-configuration/' "}
|
|
|
|
result = _on_save_security(values)
|
|
|
|
assert result["error"] is False
|
|
assert (
|
|
result["values"]["OIDC_DISCOVERY_URL"]
|
|
== "https://auth.example.com/.well-known/openid-configuration/"
|
|
)
|
|
|
|
def test_on_save_normalizes_proxy_logout_url(self, tmp_path, monkeypatch):
|
|
from shelfmark.config.security import _on_save_security
|
|
|
|
_set_config_dir(monkeypatch, tmp_path)
|
|
values = {"PROXY_AUTH_LOGOUT_URL": "auth.example.com/logout"}
|
|
|
|
result = _on_save_security(values)
|
|
|
|
assert result["error"] is False
|
|
assert result["values"]["PROXY_AUTH_LOGOUT_URL"] == "https://auth.example.com/logout"
|