This commit is contained in:
Guru Aravinthan S 2026-08-09 02:41:42 +02:00 committed by GitHub
commit fa638b561d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 27 additions and 11 deletions

View file

@ -48,7 +48,7 @@ ADMIN_PRIVILEGES["allowed_models_restricted"] = False
# backwards for this sentinel.
ADMIN_PRIVILEGES["block_all_models"] = False
from src.constants import AUTH_FILE, PASSWORD_MIN_LENGTH
from src.constants import AUTH_FILE, PASSWORD_MIN_LENGTH, PASSWORD_MAX_LENGTH
DEFAULT_AUTH_PATH = AUTH_FILE
TOKEN_TTL = 60 * 60 * 24 * 7 # 7 days
@ -250,6 +250,7 @@ class AuthManager:
"""Return public auth policy constants for the frontend."""
return {
"password_min_length": PASSWORD_MIN_LENGTH,
"password_max_length": PASSWORD_MAX_LENGTH,
"reserved_usernames": sorted(RESERVED_USERNAMES),
"signup_enabled": self.signup_enabled,
"session_days": TOKEN_TTL // 86400,

View file

@ -453,6 +453,7 @@ Odysseus is a self-hosted workspace with powerful local tools: shell access, fil
- Do not expose it directly to the public internet without HTTPS and a trusted reverse proxy or private access layer.
- Keep `.env`, `data/`, `logs/`, databases, uploads, generated media, backups, auth/session files, API keys, and model/provider tokens out of Git and private shares. They are ignored by default.
- Review `data/auth.json` after first boot: disable open signup unless you intentionally want it, make only your own account admin, and keep demo/test accounts non-admin.
- Passwords must be at least 8 characters long and no more than 72 bytes (bcrypt's hard limit). Multi-byte characters count toward that byte limit.
- Non-admin users do not get shell/Python/file read/write by default, and admin-only routes/tools such as MCP management, API tokens, webhooks, model/cookbook serving, backup/vault, and app settings are admin-gated. Other features are controlled by per-user privileges, so review each user's privileges before exposing a deployment.
- Rotate any API keys or tokens that were ever pasted into a shared chat, demo, screenshot, or log.
- If you enable API tokens or webhooks, create separate tokens per integration and delete unused ones.

View file

@ -13,7 +13,8 @@ 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 validate_password_length
from src.constants import DEEP_RESEARCH_DIR, MEMORY_FILE, SKILLS_DIR
from src.rate_limiter import RateLimiter
from src.settings_scrub import scrub_settings
from src.settings import (
@ -102,8 +103,7 @@ def setup_auth_routes(auth_manager: AuthManager) -> APIRouter:
raise HTTPException(429, "Too many requests — try again later")
if auth_manager.is_configured:
raise HTTPException(400, "Already configured")
if len(body.password) < PASSWORD_MIN_LENGTH:
raise HTTPException(400, f"Password must be at least {PASSWORD_MIN_LENGTH} characters")
validate_password_length(body.password)
if len(body.username.strip()) < 1:
raise HTTPException(400, "Username is required")
if body.username.lower() in RESERVED_USERNAMES:
@ -122,8 +122,7 @@ def setup_auth_routes(auth_manager: AuthManager) -> APIRouter:
raise HTTPException(400, "Run setup first")
if not auth_manager.signup_enabled:
raise HTTPException(403, "Registration is disabled. Ask an admin for an account.")
if len(body.password) < PASSWORD_MIN_LENGTH:
raise HTTPException(400, f"Password must be at least {PASSWORD_MIN_LENGTH} characters")
validate_password_length(body.password)
if len(body.username.strip()) < 1:
raise HTTPException(400, "Username is required")
if body.username.lower() in RESERVED_USERNAMES:
@ -200,8 +199,7 @@ def setup_auth_routes(auth_manager: AuthManager) -> APIRouter:
user = _get_current_user(request)
if not user:
raise HTTPException(401, "Not authenticated")
if len(body.new_password) < PASSWORD_MIN_LENGTH:
raise HTTPException(400, f"Password must be at least {PASSWORD_MIN_LENGTH} characters")
validate_password_length(body.new_password)
current_token = request.cookies.get(SESSION_COOKIE)
ok = await asyncio.to_thread(auth_manager.change_password, user, body.current_password, body.new_password)
if not ok:
@ -281,8 +279,7 @@ def setup_auth_routes(auth_manager: AuthManager) -> APIRouter:
user = _get_current_user(request)
if not user or not auth_manager.is_admin(user):
raise HTTPException(403, "Admin only")
if len(body.password) < PASSWORD_MIN_LENGTH:
raise HTTPException(400, f"Password must be at least {PASSWORD_MIN_LENGTH} characters")
validate_password_length(body.password)
if len(body.username.strip()) < 1:
raise HTTPException(400, "Username is required")
if body.username.lower() in RESERVED_USERNAMES:

View file

@ -16,7 +16,7 @@ sys.path.insert(0, BASE_DIR)
from src.constants import (
DATA_DIR, AUTH_FILE, UPLOAD_DIR, PERSONAL_DIR, PERSONAL_UPLOADS_DIR,
TTS_CACHE_DIR, GENERATED_IMAGES_DIR, DEEP_RESEARCH_DIR, CHROMA_DIR,
RAG_DIR, MEMORY_VECTORS_DIR, PASSWORD_MIN_LENGTH,
RAG_DIR, MEMORY_VECTORS_DIR, PASSWORD_MIN_LENGTH, PASSWORD_MAX_LENGTH,
)
from core.auth import RESERVED_USERNAMES
@ -77,6 +77,9 @@ def _prompt_admin_credentials():
if len(password) < PASSWORD_MIN_LENGTH:
print(f" Password must be at least {PASSWORD_MIN_LENGTH} characters.")
continue
if len(password.encode("utf-8")) > PASSWORD_MAX_LENGTH:
print(f" Password must be {PASSWORD_MAX_LENGTH} bytes or fewer.")
continue
confirm = getpass.getpass(" Confirm password: ")
if password != confirm:
print(" Passwords don't match. Try again.")
@ -109,6 +112,9 @@ def create_default_admin():
if len(password) < PASSWORD_MIN_LENGTH:
print(f" [error] ODYSSEUS_ADMIN_PASSWORD must be at least {PASSWORD_MIN_LENGTH} characters")
return "failed"
if len(password.encode("utf-8")) > PASSWORD_MAX_LENGTH:
print(f" [error] ODYSSEUS_ADMIN_PASSWORD must be {PASSWORD_MAX_LENGTH} bytes or fewer")
return "failed"
elif sys.stdin.isatty() and not os.getenv("ODYSSEUS_SKIP_ADMIN_PROMPT"):
# Interactive terminal — ask the user
username, password = _prompt_admin_credentials()

View file

@ -4,6 +4,16 @@ import os
from typing import Optional
from fastapi import Request, HTTPException
from src.constants import PASSWORD_MAX_LENGTH, PASSWORD_MIN_LENGTH
def validate_password_length(password: str) -> None:
"""Raise HTTPException(400, ...) if password violates min/max length policy."""
if len(password) < PASSWORD_MIN_LENGTH:
raise HTTPException(400, f"Password must be at least {PASSWORD_MIN_LENGTH} characters")
if len(password.encode("utf-8")) > PASSWORD_MAX_LENGTH:
raise HTTPException(400, f"Password must be {PASSWORD_MAX_LENGTH} bytes or fewer")
def get_current_user(request: Request) -> Optional[str]:
"""Get current username from request state (set by auth middleware)."""

View file

@ -103,6 +103,7 @@ CLEANUP_INTERVAL_HOURS = int(os.getenv("CLEANUP_INTERVAL_HOURS", "24"))
# Auth policy
PASSWORD_MIN_LENGTH = 8
PASSWORD_MAX_LENGTH = 72
# Default parameters
DEFAULT_TEMPERATURE = 1.0