P2 minor set: password off argv, configurable share gate, POST logout, log path check
- Navidrome password is now passed to curl via stdin (--data-urlencode "p@-") in navidrome-scan.sh and pipeline-status.sh, so it no longer appears in ps/proc. Verified the query sent is identical and a live scan still triggers. - MIN_ARTIST_DIRS (the share-health gate) is now a setting, threaded through to the pipeline env, so a user with a small library can lower it instead of the scan/sync being permanently blocked by the hardcoded 500. - /auth/logout is now POST-only (with a nav form + aria-label), so a drive-by GET can't log the user out; enforced allowed_email already landed separately. - view_log now confirms the run's log_path resolves under the logs dir before serving it (defense in depth). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -55,7 +55,7 @@ async def auth_callback(request: Request):
|
||||
return RedirectResponse(url="/")
|
||||
|
||||
|
||||
@router.get("/logout")
|
||||
@router.post("/logout")
|
||||
async def logout(request: Request):
|
||||
request.session.clear()
|
||||
end_session_endpoint = None
|
||||
|
||||
+8
-1
@@ -99,7 +99,14 @@ async def view_log(run_id: int, user: dict = Depends(require_auth), db=Depends(g
|
||||
raise HTTPException(404, "no such run/log")
|
||||
from pathlib import Path
|
||||
|
||||
path = Path(run.log_path)
|
||||
from app.settings import settings
|
||||
|
||||
# Containment check: only ever serve files from under the logs dir. Paths
|
||||
# are app-generated so this is defense in depth against a malformed row.
|
||||
logs_dir = settings.logs_dir.resolve()
|
||||
path = Path(run.log_path).resolve()
|
||||
if logs_dir != path and logs_dir not in path.parents:
|
||||
raise HTTPException(404, "no such run/log")
|
||||
if not path.exists():
|
||||
raise HTTPException(404, "log file no longer exists")
|
||||
return path.read_text(errors="replace")
|
||||
|
||||
@@ -42,6 +42,7 @@ def _subprocess_env() -> dict:
|
||||
env["MUSIC_DATA_DIR"] = str(settings.music_data_dir)
|
||||
env["PIPELINE_DIR"] = str(settings.pipeline_dir)
|
||||
env["BEETSDIR"] = str(settings.beets_dir)
|
||||
env["MIN_ARTIST_DIRS"] = str(settings.min_artist_dirs)
|
||||
return env
|
||||
|
||||
|
||||
|
||||
@@ -48,6 +48,12 @@ class Settings(BaseSettings):
|
||||
validation_alias=AliasChoices("TZ", "ALEMBIC_TIMEZONE", "TIMEZONE"),
|
||||
)
|
||||
|
||||
# Share-health gate: the Navidrome scan and Bandcamp sync abort if the
|
||||
# library has fewer than this many artist folders, to catch a half-mounted
|
||||
# or empty share before it does damage. The default suits a large library;
|
||||
# set it lower (via env MIN_ARTIST_DIRS) if yours is small.
|
||||
min_artist_dirs: int = 500
|
||||
|
||||
@property
|
||||
def beets_dir(self) -> Path:
|
||||
return self.alembic_config_dir / "beets"
|
||||
|
||||
+12
-2
@@ -219,11 +219,21 @@ a:hover { color: var(--iris-pink); text-decoration: underline; }
|
||||
background: var(--status-success);
|
||||
box-shadow: 0 0 8px var(--status-success);
|
||||
}
|
||||
.user-chip a.logout {
|
||||
.user-chip a.logout,
|
||||
.user-chip button.logout {
|
||||
color: var(--muted-2);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.user-chip a.logout:hover { color: var(--status-danger); text-decoration: none; }
|
||||
.user-chip button.logout {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
line-height: 1;
|
||||
}
|
||||
.user-chip a.logout:hover,
|
||||
.user-chip button.logout:hover { color: var(--status-danger); text-decoration: none; }
|
||||
|
||||
/* ---- layout ---- */
|
||||
|
||||
|
||||
@@ -51,7 +51,9 @@
|
||||
<div class="user-chip">
|
||||
<span class="user-dot"></span>
|
||||
<span>{{ user.email or user.name }}</span>
|
||||
<a class="logout" href="/auth/logout" title="Log out">⏻</a>
|
||||
<form method="post" action="/auth/logout" style="display:inline; margin:0;">
|
||||
<button type="submit" class="logout" title="Log out" aria-label="Log out">⏻</button>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -21,7 +21,7 @@ NAVIDROME_ENV="$ALEMBIC_CONFIG_DIR/pipeline/navidrome/admin.env"
|
||||
|
||||
LIB="$MUSIC_DATA_DIR/Library"
|
||||
CANARY="$LIB/.navidrome-canary"
|
||||
MIN_ARTIST_DIRS=500 # library has ~1500; 500 is "clearly not wiped"
|
||||
MIN_ARTIST_DIRS="${MIN_ARTIST_DIRS:-500}" # override via env for a small library
|
||||
ND="${ND_BASE:-http://navidrome:4533}"
|
||||
ND_USER="${ND_USER:-}"
|
||||
ND_PASS="${ND_PASS:-}"
|
||||
@@ -46,8 +46,10 @@ if [ "$n" -lt "$MIN_ARTIST_DIRS" ]; then
|
||||
fi
|
||||
|
||||
log "library healthy ($n artist dirs) — triggering full scan"
|
||||
resp=$(curl -s -G "$ND/rest/startScan.view" \
|
||||
--data-urlencode "u=$ND_USER" --data-urlencode "p=$ND_PASS" \
|
||||
# Pass the password via stdin (--data-urlencode "p@-"), not on the command
|
||||
# line, so it isn't visible in ps/proc. printf %s avoids a trailing newline.
|
||||
resp=$(printf '%s' "$ND_PASS" | curl -s -G "$ND/rest/startScan.view" \
|
||||
--data-urlencode "u=$ND_USER" --data-urlencode "p@-" \
|
||||
--data-urlencode 'v=1.16.0' --data-urlencode 'c=cron' \
|
||||
--data-urlencode 'f=json' --data-urlencode 'fullScan=true' || true)
|
||||
# Don't log "scan triggered" on faith: a rotated password or Navidrome error
|
||||
|
||||
@@ -351,9 +351,10 @@ dedup_today_status() {
|
||||
|
||||
# 6. Navidrome
|
||||
section "Navidrome"
|
||||
ND_RESP=$(curl -s --connect-timeout 5 -G "$ND_BASE/rest/getScanStatus.view" \
|
||||
# Password via stdin (p@-), not argv, so it isn't exposed in ps/proc.
|
||||
ND_RESP=$(printf '%s' "$ND_PASS" | curl -s --connect-timeout 5 -G "$ND_BASE/rest/getScanStatus.view" \
|
||||
--data-urlencode "u=$ND_USER" \
|
||||
--data-urlencode "p=$ND_PASS" \
|
||||
--data-urlencode "p@-" \
|
||||
--data-urlencode 'v=1.16.0' --data-urlencode 'c=status' --data-urlencode 'f=json' 2>/dev/null)
|
||||
ND_LINE=$(echo "$ND_RESP" | python3 -c "
|
||||
import sys, json
|
||||
|
||||
@@ -41,7 +41,7 @@ fi
|
||||
# equivalent (same pattern as navidrome-scan.sh's share-health gate).
|
||||
LIB="${MUSIC_DATA_DIR:-/data/music}/Library"
|
||||
CANARY="$LIB/.navidrome-canary"
|
||||
MIN_ARTIST_DIRS=500
|
||||
MIN_ARTIST_DIRS="${MIN_ARTIST_DIRS:-500}"
|
||||
if [[ ! -r "$CANARY" ]]; then
|
||||
log "ERROR: canary $CANARY missing/unreadable — share looks unhealthy — aborting sync"
|
||||
exit 1
|
||||
|
||||
Reference in New Issue
Block a user