The backend auth for opt-in username/password mode (security.require_login, default off → zero change; the launch PIN + picker behave exactly as today). - core/security/login_gate.py: pure gate (mirrors launch_lock) — when login mode is on, an unauthenticated session reaches only the page shell, /api/auth/login, /api/auth/logout, /api/profiles/current, /api/setup/status, and the key-authed /api/v1 API. Deliberately does NOT expose the profile list pre-auth (you type your name, not pick from a roster). - _enforce_login before_request enforces it; _enforce_launch_pin no-ops when login mode is on (login replaces the shared PIN, per design). - POST /api/auth/login (username = profile name, case-insensitive; brute-force limited per IP; generic error so names don't leak) + POST /api/auth/logout. - Anti-lockout: the settings save refuses to turn ON login mode until the admin account has a password. Tests: gate blocks→login→access→logout→blocked; case-insensitive username; wrong password / passwordless profile / unknown user all 401 generically; login list not exposed pre-auth; can't enable login without an admin password. 12 tests pass. Next: the login screen + set-password UI + the toggle (increment 3).
36 lines
1.6 KiB
Python
36 lines
1.6 KiB
Python
"""Pure login-gate decision (opt-in username/password mode)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from core.security.login_gate import login_request_is_blocked as blocked
|
|
|
|
|
|
def test_off_never_blocks():
|
|
assert blocked('/api/anything', 'GET', require_login=False, authenticated=False) is False
|
|
|
|
|
|
def test_authenticated_never_blocked():
|
|
assert blocked('/api/anything', 'GET', require_login=True, authenticated=True) is False
|
|
|
|
|
|
def test_unauthenticated_blocked_on_normal_api():
|
|
assert blocked('/api/library', 'GET', require_login=True, authenticated=False) is True
|
|
|
|
|
|
def test_login_flow_and_shell_allowed_unauthenticated():
|
|
for p in ('/', '/static/app.js', '/favicon.ico'):
|
|
assert blocked(p, 'GET', require_login=True, authenticated=False) is False
|
|
assert blocked('/api/auth/login', 'POST', require_login=True, authenticated=False) is False
|
|
assert blocked('/api/auth/logout', 'POST', require_login=True, authenticated=False) is False
|
|
assert blocked('/api/profiles/current', 'GET', require_login=True, authenticated=False) is False
|
|
assert blocked('/api/setup/status', 'GET', require_login=True, authenticated=False) is False
|
|
|
|
|
|
def test_profile_list_NOT_exposed_pre_auth():
|
|
# login mode = type your name, don't pick from an exposed roster
|
|
assert blocked('/api/profiles', 'GET', require_login=True, authenticated=False) is True
|
|
|
|
|
|
def test_key_authed_public_api_allowed():
|
|
assert blocked('/api/v1/search', 'GET', require_login=True, authenticated=False) is False
|
|
assert blocked('/api/v1/api-keys-internal', 'GET', require_login=True, authenticated=False) is True
|