Gate /api/settings endpoints behind admin profile
Closes #370 (reported by JohnBaumb). The /api/settings endpoint and three siblings (/log-level, /config-status, /verify) had no auth check — any logged-in profile could read or modify service tokens, OAuth secrets, and API keys. Cin's "minimum" suggestion from the issue: gate to admin profile. Added an `admin_only` decorator near `get_current_profile_id` that returns 403 when the current profile isn't admin (id=1). Applied to all four endpoints. Auth model note (documented in the decorator docstring): SoulSync's existing model is "trust local network" — single-admin / no-multi- profile installs default `get_current_profile_id()` to 1, so the gate is a no-op for solo users. The decorator is meaningful in multi-profile setups where non-admin sessions exist. Tightening to real per-request auth is out of scope. Did NOT consolidate with api/settings.py (Cin's "better" suggestion): that endpoint uses API-key auth (for external tools), the web_server.py copy uses session/profile auth (for the web UI). Different consumers, different auth models — merging would break one or the other. 603 tests pass.
This commit is contained in:
parent
9e8b5aee3b
commit
01b7d50311
2 changed files with 32 additions and 0 deletions
|
|
@ -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…
Reference in a new issue