Harden auth flows and add backend quality gate
This commit is contained in:
@@ -41,6 +41,7 @@ from ..db import (
|
||||
delete_user_activity_by_username,
|
||||
set_user_auto_search_enabled,
|
||||
set_auto_search_enabled_for_non_admin_users,
|
||||
set_user_email,
|
||||
set_user_invite_management_enabled,
|
||||
set_invite_management_enabled_for_non_admin_users,
|
||||
set_user_profile_id,
|
||||
@@ -78,6 +79,8 @@ from ..clients.jellyseerr import JellyseerrClient
|
||||
from ..services.jellyfin_sync import sync_jellyfin_users
|
||||
from ..services.user_cache import (
|
||||
build_jellyseerr_candidate_map,
|
||||
extract_jellyseerr_user_email,
|
||||
find_matching_jellyseerr_user,
|
||||
get_cached_jellyfin_users,
|
||||
get_cached_jellyseerr_users,
|
||||
match_jellyseerr_user_id,
|
||||
@@ -85,9 +88,11 @@ from ..services.user_cache import (
|
||||
save_jellyseerr_users_cache,
|
||||
clear_user_import_caches,
|
||||
)
|
||||
from ..security import validate_password_policy
|
||||
from ..services.invite_email import (
|
||||
TEMPLATE_KEYS as INVITE_EMAIL_TEMPLATE_KEYS,
|
||||
get_invite_email_templates,
|
||||
normalize_delivery_email,
|
||||
reset_invite_email_template,
|
||||
save_invite_email_template,
|
||||
send_test_email,
|
||||
@@ -106,6 +111,16 @@ events_router = APIRouter(prefix="/admin/events", tags=["admin"])
|
||||
logger = logging.getLogger(__name__)
|
||||
SELF_SERVICE_INVITE_MASTER_ID_KEY = "self_service_invite_master_id"
|
||||
|
||||
|
||||
def _require_recipient_email(value: object) -> str:
|
||||
normalized = normalize_delivery_email(value)
|
||||
if normalized:
|
||||
return normalized
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail="recipient_email is required and must be a valid email address",
|
||||
)
|
||||
|
||||
SENSITIVE_KEYS = {
|
||||
"magent_ssl_certificate_pem",
|
||||
"magent_ssl_private_key_pem",
|
||||
@@ -820,8 +835,12 @@ async def jellyseerr_users_sync() -> Dict[str, Any]:
|
||||
continue
|
||||
username = user.get("username") or ""
|
||||
matched_id = match_jellyseerr_user_id(username, candidate_to_id)
|
||||
matched_seerr_user = find_matching_jellyseerr_user(username, jellyseerr_users)
|
||||
matched_email = extract_jellyseerr_user_email(matched_seerr_user)
|
||||
if matched_id is not None:
|
||||
set_user_jellyseerr_id(username, matched_id)
|
||||
if matched_email:
|
||||
set_user_email(username, matched_email)
|
||||
updated += 1
|
||||
else:
|
||||
skipped += 1
|
||||
@@ -858,10 +877,12 @@ async def jellyseerr_users_resync() -> Dict[str, Any]:
|
||||
username = _pick_jellyseerr_username(user)
|
||||
if not username:
|
||||
continue
|
||||
email = extract_jellyseerr_user_email(user)
|
||||
created = create_user_if_missing(
|
||||
username,
|
||||
"jellyseerr-user",
|
||||
role="user",
|
||||
email=email,
|
||||
auth_provider="jellyseerr",
|
||||
jellyseerr_user_id=user_id,
|
||||
)
|
||||
@@ -869,6 +890,8 @@ async def jellyseerr_users_resync() -> Dict[str, Any]:
|
||||
imported += 1
|
||||
else:
|
||||
set_user_jellyseerr_id(username, user_id)
|
||||
if email:
|
||||
set_user_email(username, email)
|
||||
return {"status": "ok", "imported": imported, "cleared": cleared}
|
||||
|
||||
@router.post("/requests/sync")
|
||||
@@ -1458,12 +1481,15 @@ async def update_users_expiry_bulk(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
@router.post("/users/{username}/password")
|
||||
async def update_user_password(username: str, payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
new_password = payload.get("password") if isinstance(payload, dict) else None
|
||||
if not isinstance(new_password, str) or len(new_password.strip()) < 8:
|
||||
raise HTTPException(status_code=400, detail="Password must be at least 8 characters.")
|
||||
if not isinstance(new_password, str):
|
||||
raise HTTPException(status_code=400, detail="Invalid payload")
|
||||
try:
|
||||
new_password_clean = validate_password_policy(new_password)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
user = get_user_by_username(username)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
new_password_clean = new_password.strip()
|
||||
user = normalize_user_auth_provider(user)
|
||||
auth_provider = resolve_user_auth_provider(user)
|
||||
if auth_provider == "local":
|
||||
@@ -1775,7 +1801,7 @@ async def send_invite_email(payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if invite is None:
|
||||
invite = _resolve_user_invite(user)
|
||||
|
||||
recipient_email = _normalize_optional_text(payload.get("recipient_email"))
|
||||
recipient_email = _require_recipient_email(payload.get("recipient_email"))
|
||||
message = _normalize_optional_text(payload.get("message"))
|
||||
reason = _normalize_optional_text(payload.get("reason"))
|
||||
|
||||
@@ -1825,7 +1851,7 @@ async def create_invite(payload: Dict[str, Any], current_user: Dict[str, Any] =
|
||||
role = _normalize_role_or_none(payload.get("role"))
|
||||
max_uses = _parse_optional_positive_int(payload.get("max_uses"), "max_uses")
|
||||
expires_at = _parse_optional_expires_at(payload.get("expires_at"))
|
||||
recipient_email = _normalize_optional_text(payload.get("recipient_email"))
|
||||
recipient_email = _require_recipient_email(payload.get("recipient_email"))
|
||||
send_email = bool(payload.get("send_email"))
|
||||
delivery_message = _normalize_optional_text(payload.get("message"))
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user