mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-15 11:22:21 +02:00
fix(auth): honor AUTH_ENABLED=false in admin-gated app-config routes (#6029)
* fix(auth): honor AUTH_ENABLED=false in admin-gated app-config routes With auth explicitly disabled (reverse-proxy deployments) there is no session user, so the inline admin checks in auth_routes.py returned 403 unconditionally and settings changes were silently lost. The central require_admin in core/middleware.py already allows this case; the inline checks did not. - add _require_admin_user helper mirroring require_admin semantics and use it in the app-config routes (settings, features, integrations) - return unscrubbed settings on GET /settings when auth is disabled (the caller is the operator) - settings.js: check res.ok in saveDefault so a failed save no longer reports 'Saved' - add regression tests User-management routes keep requiring a real admin identity; they act on the user store, which is not meaningful with auth disabled. Fixes #6028 * fix(settings): verify response.ok on save and expand auth regression tests - Ensure all settings save handlers in settings.js check response.ok before displaying success notifications, preventing false 'Saved' messages on server rejection. - Add DOM and JS unit tests verifying response.ok checks and error handling across settings handlers. - Expand tests/test_admin_routes_auth_disabled.py with comprehensive authorization test coverage for AUTH_ENABLED=false and AUTH_ENABLED=true across settings, features, integrations, secret scrubbing, and user management. --------- Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com> Co-authored-by: PR #6029 Reviewer <reviewer@odysseus-review.local>
This commit is contained in:
co-authored by
Alexandre Teixeira
PR #6029 Reviewer
parent
0c56527d12
commit
353ea00736
+25
-25
@@ -15,6 +15,7 @@ from pathlib import Path
|
||||
from core.atomic_io import atomic_write_json, atomic_write_text
|
||||
from core.auth import AuthManager, RESERVED_USERNAMES, SetAdminResult, TOKEN_TTL
|
||||
from src.constants import DEEP_RESEARCH_DIR, MEMORY_FILE, PASSWORD_MIN_LENGTH, SKILLS_DIR
|
||||
from src.auth_helpers import _auth_disabled
|
||||
from src.rate_limiter import RateLimiter
|
||||
from src.settings_scrub import scrub_settings
|
||||
from src.settings import (
|
||||
@@ -129,6 +130,21 @@ def setup_auth_routes(auth_manager: AuthManager) -> APIRouter:
|
||||
token = request.cookies.get(SESSION_COOKIE)
|
||||
return auth_manager.get_username_for_token(token)
|
||||
|
||||
def _require_admin_user(request: Request) -> Optional[str]:
|
||||
"""Resolve the current user, enforcing admin access.
|
||||
|
||||
Honors AUTH_ENABLED=false the same way core.middleware.require_admin
|
||||
does: when the operator has explicitly disabled auth (e.g. behind a
|
||||
reverse proxy that handles login), there is no session user, and
|
||||
admin-gated app configuration must stay reachable.
|
||||
"""
|
||||
user = _get_current_user(request)
|
||||
if _auth_disabled():
|
||||
return user
|
||||
if not user or not auth_manager.is_admin(user):
|
||||
raise HTTPException(403, "Admin only")
|
||||
return user
|
||||
|
||||
@router.post("/setup")
|
||||
async def first_run_setup(body: SetupRequest, request: Request):
|
||||
"""Create initial admin account. Only works if no accounts exist."""
|
||||
@@ -718,9 +734,7 @@ def setup_auth_routes(auth_manager: AuthManager) -> APIRouter:
|
||||
@router.post("/features")
|
||||
async def set_features(request: Request):
|
||||
"""Admin only: update feature toggles."""
|
||||
user = _get_current_user(request)
|
||||
if not user or not auth_manager.is_admin(user):
|
||||
raise HTTPException(403, "Admin only")
|
||||
_require_admin_user(request)
|
||||
body = await request.json()
|
||||
current = _load_features()
|
||||
for key in current:
|
||||
@@ -738,16 +752,14 @@ def setup_auth_routes(auth_manager: AuthManager) -> APIRouter:
|
||||
for keybinds + TTS prefs, so it stays callable without admin."""
|
||||
user = _get_current_user(request)
|
||||
settings = without_retired_settings(_load_settings())
|
||||
if user and auth_manager.is_admin(user):
|
||||
if _auth_disabled() or (user and auth_manager.is_admin(user)):
|
||||
return settings
|
||||
return scrub_settings(settings)
|
||||
|
||||
@router.post("/settings")
|
||||
async def set_settings(request: Request):
|
||||
"""Admin only: update app settings."""
|
||||
user = _get_current_user(request)
|
||||
if not user or not auth_manager.is_admin(user):
|
||||
raise HTTPException(403, "Admin only")
|
||||
_require_admin_user(request)
|
||||
body = await request.json()
|
||||
current = _load_settings()
|
||||
# Per-key validation for numeric settings: coerce to int and clamp to a
|
||||
@@ -780,9 +792,7 @@ def setup_auth_routes(auth_manager: AuthManager) -> APIRouter:
|
||||
file: UploadFile = File(...),
|
||||
):
|
||||
"""Infer the general prose style from one user-supplied document."""
|
||||
user = _get_current_user(request)
|
||||
if not user or not auth_manager.is_admin(user):
|
||||
raise HTTPException(403, "Admin only")
|
||||
user = _require_admin_user(request)
|
||||
filename = Path(file.filename or "sample.txt").name
|
||||
suffix = Path(filename).suffix.lower()
|
||||
allowed = {
|
||||
@@ -861,9 +871,7 @@ def setup_auth_routes(auth_manager: AuthManager) -> APIRouter:
|
||||
@router.get("/integrations")
|
||||
async def list_integrations_route(request: Request):
|
||||
"""List all integrations (admin only, keys masked)."""
|
||||
user = _get_current_user(request)
|
||||
if not user or not auth_manager.is_admin(user):
|
||||
raise HTTPException(403, "Admin only")
|
||||
_require_admin_user(request)
|
||||
items = load_integrations()
|
||||
# Mask API keys for frontend display
|
||||
safe = [mask_integration_secret(item) for item in items]
|
||||
@@ -877,9 +885,7 @@ def setup_auth_routes(auth_manager: AuthManager) -> APIRouter:
|
||||
@router.post("/integrations")
|
||||
async def create_integration(request: Request):
|
||||
"""Create a new integration (admin only)."""
|
||||
user = _get_current_user(request)
|
||||
if not user or not auth_manager.is_admin(user):
|
||||
raise HTTPException(403, "Admin only")
|
||||
_require_admin_user(request)
|
||||
body = await request.json()
|
||||
item = add_integration(body)
|
||||
return {"ok": True, "integration": mask_integration_secret(item)}
|
||||
@@ -887,9 +893,7 @@ def setup_auth_routes(auth_manager: AuthManager) -> APIRouter:
|
||||
@router.put("/integrations/{integration_id}")
|
||||
async def update_integration_route(integration_id: str, request: Request):
|
||||
"""Update an existing integration (admin only)."""
|
||||
user = _get_current_user(request)
|
||||
if not user or not auth_manager.is_admin(user):
|
||||
raise HTTPException(403, "Admin only")
|
||||
_require_admin_user(request)
|
||||
body = await request.json()
|
||||
item = update_integration(integration_id, body)
|
||||
if not item:
|
||||
@@ -899,9 +903,7 @@ def setup_auth_routes(auth_manager: AuthManager) -> APIRouter:
|
||||
@router.delete("/integrations/{integration_id}")
|
||||
async def delete_integration_route(integration_id: str, request: Request):
|
||||
"""Delete an integration (admin only)."""
|
||||
user = _get_current_user(request)
|
||||
if not user or not auth_manager.is_admin(user):
|
||||
raise HTTPException(403, "Admin only")
|
||||
_require_admin_user(request)
|
||||
ok = delete_integration(integration_id)
|
||||
if not ok:
|
||||
raise HTTPException(404, "Integration not found")
|
||||
@@ -910,9 +912,7 @@ def setup_auth_routes(auth_manager: AuthManager) -> APIRouter:
|
||||
@router.post("/integrations/{integration_id}/test")
|
||||
async def test_integration_route(integration_id: str, request: Request):
|
||||
"""Test connectivity to an integration (admin only)."""
|
||||
user = _get_current_user(request)
|
||||
if not user or not auth_manager.is_admin(user):
|
||||
raise HTTPException(403, "Admin only")
|
||||
_require_admin_user(request)
|
||||
integ = get_integration(integration_id)
|
||||
if not integ:
|
||||
raise HTTPException(404, "Integration not found")
|
||||
|
||||
Reference in New Issue
Block a user