mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-11 01:12:20 +02:00
feat(auth): define Default/Local owner contract (#5795)
* feat(auth): define default local owner contract * test(auth): harden default local owner matrix --------- Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com>
This commit is contained in:
co-authored by
Alexandre Teixeira
parent
9c71948376
commit
0dd70a7556
@@ -0,0 +1,102 @@
|
||||
from types import SimpleNamespace
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
def test_effective_storage_owner_matrix(monkeypatch):
|
||||
from src.owner_identity import DEFAULT_LOCAL_OWNER, effective_storage_owner
|
||||
|
||||
monkeypatch.delenv("AUTH_ENABLED", raising=False)
|
||||
assert effective_storage_owner(None) is None
|
||||
assert effective_storage_owner("") is None
|
||||
assert effective_storage_owner("alice") == "alice"
|
||||
for sentinel in ("api", "demo", "system", "internal-tool"):
|
||||
assert effective_storage_owner(sentinel) is None
|
||||
assert effective_storage_owner(f" {sentinel.upper()} ") is None
|
||||
|
||||
monkeypatch.setenv("AUTH_ENABLED", "false")
|
||||
assert effective_storage_owner(None) == DEFAULT_LOCAL_OWNER
|
||||
assert effective_storage_owner("") == DEFAULT_LOCAL_OWNER
|
||||
assert effective_storage_owner("admin") == "admin"
|
||||
for sentinel in ("api", "demo", "system", "internal-tool"):
|
||||
assert effective_storage_owner(sentinel) is None
|
||||
|
||||
|
||||
def test_storage_owner_for_request_uses_api_token_owner(monkeypatch):
|
||||
from src.auth_helpers import storage_owner_for_request
|
||||
|
||||
monkeypatch.delenv("AUTH_ENABLED", raising=False)
|
||||
request = SimpleNamespace(
|
||||
state=SimpleNamespace(
|
||||
current_user="api",
|
||||
api_token=True,
|
||||
api_token_owner="alice",
|
||||
)
|
||||
)
|
||||
|
||||
assert storage_owner_for_request(request) == "alice"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("sentinel", ["api", "demo", "system", "internal-tool"])
|
||||
def test_storage_owner_for_request_rejects_request_sentinel(monkeypatch, sentinel):
|
||||
from src.auth_helpers import storage_owner_for_request
|
||||
|
||||
monkeypatch.delenv("AUTH_ENABLED", raising=False)
|
||||
request = SimpleNamespace(
|
||||
state=SimpleNamespace(
|
||||
current_user=sentinel,
|
||||
api_token=sentinel == "api",
|
||||
api_token_owner=None,
|
||||
)
|
||||
)
|
||||
|
||||
assert storage_owner_for_request(request) is None
|
||||
|
||||
|
||||
def test_storage_owner_for_request_uses_default_local_when_auth_disabled(monkeypatch):
|
||||
from src.auth_helpers import storage_owner_for_request
|
||||
from src.owner_identity import DEFAULT_LOCAL_OWNER
|
||||
|
||||
monkeypatch.setenv("AUTH_ENABLED", "false")
|
||||
request = SimpleNamespace(state=SimpleNamespace(current_user=None))
|
||||
|
||||
assert storage_owner_for_request(request) == DEFAULT_LOCAL_OWNER
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"value,expected",
|
||||
[
|
||||
(None, False),
|
||||
("", False),
|
||||
("true", False),
|
||||
("0", False),
|
||||
("false", True),
|
||||
("FALSE", True),
|
||||
(" false ", True),
|
||||
],
|
||||
)
|
||||
def test_auth_disabled_parser_is_centralized(monkeypatch, value, expected):
|
||||
from src.owner_identity import auth_disabled
|
||||
|
||||
if value is None:
|
||||
monkeypatch.delenv("AUTH_ENABLED", raising=False)
|
||||
else:
|
||||
monkeypatch.setenv("AUTH_ENABLED", value)
|
||||
|
||||
assert auth_disabled() is expected
|
||||
|
||||
|
||||
def test_default_local_owner_is_reserved_auth_name_but_valid_storage_owner():
|
||||
from src.owner_identity import (
|
||||
DEFAULT_LOCAL_OWNER,
|
||||
REQUEST_SENTINEL_OWNERS,
|
||||
RESERVED_AUTH_USERNAMES,
|
||||
effective_storage_owner,
|
||||
is_default_local_owner,
|
||||
)
|
||||
|
||||
assert DEFAULT_LOCAL_OWNER in RESERVED_AUTH_USERNAMES
|
||||
assert DEFAULT_LOCAL_OWNER not in REQUEST_SENTINEL_OWNERS
|
||||
assert effective_storage_owner(DEFAULT_LOCAL_OWNER, auth_is_disabled=False) == DEFAULT_LOCAL_OWNER
|
||||
assert effective_storage_owner(DEFAULT_LOCAL_OWNER, auth_is_disabled=True) == DEFAULT_LOCAL_OWNER
|
||||
assert is_default_local_owner(f" {DEFAULT_LOCAL_OWNER.upper()} ")
|
||||
@@ -16,8 +16,11 @@ from types import SimpleNamespace
|
||||
import pytest
|
||||
from fastapi import HTTPException
|
||||
|
||||
from src.owner_identity import DEFAULT_LOCAL_OWNER
|
||||
from tests.helpers.import_state import clear_module
|
||||
|
||||
_RESERVED_NAMES = ["internal-tool", "api", "demo", "system", DEFAULT_LOCAL_OWNER]
|
||||
|
||||
|
||||
def _fresh_auth_manager(tmp_path):
|
||||
# Same import dance as test_security_regressions: drop any cached stub so
|
||||
@@ -30,7 +33,7 @@ def _fresh_auth_manager(tmp_path):
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"name",
|
||||
["internal-tool", "api", "demo", "system", "INTERNAL-TOOL", " Internal-Tool ", "Api", "SYSTEM"],
|
||||
_RESERVED_NAMES + ["INTERNAL-TOOL", " Internal-Tool ", "Api", "SYSTEM"],
|
||||
)
|
||||
def test_create_user_rejects_reserved_usernames(tmp_path, name):
|
||||
mgr = _fresh_auth_manager(tmp_path)
|
||||
@@ -45,34 +48,37 @@ def test_create_user_rejects_empty_username(tmp_path):
|
||||
assert "" not in mgr.users
|
||||
|
||||
|
||||
def test_setup_rejects_reserved_admin_username(tmp_path):
|
||||
@pytest.mark.parametrize("name", _RESERVED_NAMES)
|
||||
def test_setup_rejects_reserved_admin_username(tmp_path, name):
|
||||
mgr = _fresh_auth_manager(tmp_path)
|
||||
# First-run admin setup funnels through create_user, so it's covered too.
|
||||
assert mgr.setup("internal-tool", "pw-123456") is False
|
||||
assert mgr.setup(name, "pw-123456") is False
|
||||
assert mgr.is_configured is False
|
||||
|
||||
|
||||
def test_rename_into_reserved_username_is_blocked(tmp_path):
|
||||
@pytest.mark.parametrize("name", _RESERVED_NAMES)
|
||||
def test_rename_into_reserved_username_is_blocked(tmp_path, name):
|
||||
mgr = _fresh_auth_manager(tmp_path)
|
||||
assert mgr.create_user("admin", "pw-123456", is_admin=True) is True
|
||||
assert mgr.create_user("bob", "pw-123456") is True
|
||||
assert mgr.rename_user("bob", "internal-tool", "admin") is False
|
||||
assert "internal-tool" not in mgr.users
|
||||
assert mgr.rename_user("bob", name, "admin") is False
|
||||
assert name not in mgr.users
|
||||
assert "bob" in mgr.users
|
||||
|
||||
|
||||
def test_legacy_reserved_username_is_removed_on_load(tmp_path):
|
||||
@pytest.mark.parametrize("name", _RESERVED_NAMES)
|
||||
def test_legacy_reserved_username_is_removed_on_load(tmp_path, name):
|
||||
auth_path = tmp_path / "auth.json"
|
||||
auth_path.write_text(
|
||||
'{"users": {"internal-tool": {"password_hash": "unused", "is_admin": false}, '
|
||||
'"admin": {"password_hash": "unused", "is_admin": true}}}',
|
||||
'{"users": {"%s": {"password_hash": "unused", "is_admin": false}, '
|
||||
'"admin": {"password_hash": "unused", "is_admin": true}}}' % name,
|
||||
encoding="utf-8",
|
||||
)
|
||||
mgr = _fresh_auth_manager(tmp_path)
|
||||
|
||||
assert "internal-tool" not in mgr.users
|
||||
assert name not in mgr.users
|
||||
assert "admin" in mgr.users
|
||||
assert "internal-tool" not in auth_path.read_text(encoding="utf-8")
|
||||
assert name not in auth_path.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_legacy_reserved_username_session_cannot_authenticate(tmp_path):
|
||||
@@ -121,15 +127,16 @@ def test_legacy_reserved_username_session_cannot_pass_admin_gate(tmp_path, monke
|
||||
assert exc.value.status_code == 403
|
||||
|
||||
|
||||
def test_legacy_reserved_single_user_migrates_to_admin(tmp_path):
|
||||
@pytest.mark.parametrize("name", _RESERVED_NAMES)
|
||||
def test_legacy_reserved_single_user_migrates_to_admin(tmp_path, name):
|
||||
auth_path = tmp_path / "auth.json"
|
||||
auth_path.write_text(
|
||||
'{"username": "internal-tool", "password_hash": "unused"}',
|
||||
'{"username": "%s", "password_hash": "unused"}' % name,
|
||||
encoding="utf-8",
|
||||
)
|
||||
mgr = _fresh_auth_manager(tmp_path)
|
||||
|
||||
assert "internal-tool" not in mgr.users
|
||||
assert name not in mgr.users
|
||||
assert "admin" in mgr.users
|
||||
assert mgr.is_admin("admin") is True
|
||||
|
||||
@@ -141,8 +148,8 @@ def test_token_cache_owner_normalization_requires_current_user():
|
||||
users = {"alice": {}, "admin": {}}
|
||||
|
||||
assert normalize_known_username(users, " Alice ") == "alice"
|
||||
assert normalize_known_username(users, "internal-tool") is None
|
||||
assert normalize_known_username(users, "api") is None
|
||||
for name in _RESERVED_NAMES:
|
||||
assert normalize_known_username(users, name) is None
|
||||
assert normalize_known_username(users, "") is None
|
||||
|
||||
|
||||
|
||||
@@ -131,6 +131,12 @@ def test_readme_native_quickstart_uses_loopback():
|
||||
assert "0.0.0.0` only when you intentionally want" in docs
|
||||
|
||||
|
||||
def test_readme_warns_auth_enabled_for_network_access():
|
||||
readme = Path("README.md").read_text(encoding="utf-8")
|
||||
assert "Keep `AUTH_ENABLED=true` for any network-accessible deployment." in readme
|
||||
assert "Keep `LOCALHOST_BYPASS=false` outside local development." in readme
|
||||
|
||||
|
||||
def test_ollama_cookbook_runner_does_not_force_public_bind():
|
||||
route = Path("routes/cookbook_routes.py").read_text(encoding="utf-8")
|
||||
cookbook_js = Path("static/js/cookbook.js").read_text(encoding="utf-8")
|
||||
@@ -738,6 +744,27 @@ def test_require_admin_allows_when_auth_explicitly_disabled(monkeypatch):
|
||||
assert require_admin(_Req()) is None
|
||||
|
||||
|
||||
def test_require_admin_uses_central_auth_disabled_parser(monkeypatch):
|
||||
from core.middleware import require_admin
|
||||
|
||||
monkeypatch.setenv("AUTH_ENABLED", " false ")
|
||||
|
||||
class _State:
|
||||
current_user = None
|
||||
|
||||
class _AppState:
|
||||
auth_manager = None
|
||||
|
||||
class _App:
|
||||
state = _AppState()
|
||||
|
||||
class _Req:
|
||||
state = _State()
|
||||
app = _App()
|
||||
|
||||
assert require_admin(_Req()) is None
|
||||
|
||||
|
||||
def test_internal_tool_owner_header_logic_requires_known_user():
|
||||
"""Pin the owner-attribution branch used by app.AuthMiddleware without
|
||||
booting the full FastAPI app."""
|
||||
|
||||
Reference in New Issue
Block a user