Merge pull request #385 from Nezreka/fix/settings-endpoint-no-auth

Gate /api/settings endpoints behind admin profile
pull/386/head
BoulderBadgeDad 4 weeks ago committed by GitHub
commit e0573729ed
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -17,6 +17,7 @@ import re
import sqlite3
import types
import collections
import functools
from pathlib import Path
from urllib.parse import quote, urljoin, urlparse
@ -312,6 +313,32 @@ def get_current_profile_id() -> int:
except AttributeError:
return 1
def admin_only(view_fn):
"""Restrict a Flask view to the admin profile (profile_id == 1).
Settings-class endpoints expose / mutate service tokens, OAuth
secrets, and API keys. Non-admin profiles must not see them.
NOTE on the underlying auth model: `get_current_profile_id()`
defaults to 1 (admin) when no session is present, which means
single-admin / no-multi-profile installs have no actual gate here
any request from the local network is treated as admin. This
decorator's job is to gate non-admin profiles in MULTI-profile
setups, not to authenticate the network. The "trust local network"
posture is the project's existing model; tightening it (real auth
on every request) is out of scope for this decorator.
"""
@functools.wraps(view_fn)
def wrapper(*args, **kwargs):
if get_current_profile_id() != 1:
return jsonify({
"success": False,
"error": "Admin access required",
}), 403
return view_fn(*args, **kwargs)
return wrapper
# ── Per-profile Spotify client cache ──
_profile_spotify_clients = {} # profile_id -> SpotifyClient
_profile_spotify_lock = threading.Lock()
@ -6286,6 +6313,7 @@ def revoke_api_key_internal(key_id):
@app.route('/api/settings', methods=['GET', 'POST'])
@admin_only
def handle_settings():
global tidal_client # Declare that we might modify the global instance
if not config_manager:
@ -6584,6 +6612,7 @@ def hydrabase_send():
return jsonify({"success": False, "error": str(e)}), 500
@app.route('/api/settings/log-level', methods=['GET', 'POST'])
@admin_only
def handle_log_level():
"""Get or set the application log level"""
from utils.logging_config import set_log_level, get_current_log_level
@ -7443,6 +7472,7 @@ def test_connection_endpoint():
@app.route('/api/settings/config-status', methods=['GET'])
@admin_only
def settings_config_status_endpoint():
"""Return per-service config state for the Settings → Connections page.
Drives the green/yellow header gradient. No API calls just config reads.
@ -7516,6 +7546,7 @@ def _run_single_verify(service: str):
@app.route('/api/settings/verify', methods=['POST'])
@admin_only
def settings_verify_endpoint():
"""Run connection verification for one or more services.

@ -3446,6 +3446,7 @@ const WHATS_NEW = {
{ date: 'Unreleased — 2.4.1 dev cycle' },
{ title: 'Lock Down Socket.IO CORS', desc: 'socket.io was accepting websocket connections from any origin (cors=*). now defaults to same-origin only. if your websocket fails after updating, the server logs a clear warning with the rejected origin — add it to settings → security → allowed websocket origins.', page: 'settings' },
{ title: 'Faster Docker Startup — yt-dlp Pinned', desc: 'docker startup used to run `pip install -U yt-dlp` on every container start. removed that — yt-dlp is now pinned in requirements.txt so startup is fast and reproducible. tradeoff: youtube fixes ship via soulsync releases now instead of next container restart.' },
{ title: 'Settings Endpoints: Admin-Only', desc: 'the /api/settings endpoints (read, write, log-level, config-status, verify) had no auth gate — any logged-in profile could read or change service tokens, oauth secrets, api keys. now admin-only. single-admin setups (no multi-profile config) work transparently as before.', page: 'settings' },
],
'2.4.0': [
// --- April 26, 2026 — Search & Artists unification + reorganize queue ---

Loading…
Cancel
Save