Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
284 changes: 220 additions & 64 deletions app_auth.py

Large diffs are not rendered by default.

33 changes: 33 additions & 0 deletions app_backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,39 @@ def _run_restore_runner(dump_file, log_file):
log.write(f"Restore command finished with return code {ret}\n")
log.flush()

if ret == 0:
try:
from database import USERS_PASSWORD_CHANGED_AT_DDL

ensure_cmd = _pg_cmd(
'psql', '-d', POSTGRES_DB, '-v', 'ON_ERROR_STOP=1',
'-c', USERS_PASSWORD_CHANGED_AT_DDL,
)
ensure_ret = -1
for attempt in (1, 2):
ensure_ret = subprocess.run(
ensure_cmd, env=env, stdout=log, stderr=subprocess.STDOUT, timeout=120
).returncode
if ensure_ret == 0:
log.write("Ensured users session schema after restore.\n")
break
log.write(f"Users session schema ensure attempt {attempt} failed (rc={ensure_ret}).\n")
log.flush()
if attempt == 1:
time.sleep(5)
if ensure_ret != 0:
log.write(
"WARNING: users session schema was not ensured; if logins fail "
"after this restore, restart the container to re-run schema init.\n"
)
log.flush()
except Exception as exc:
log.write(
f"Could not ensure users session schema after restore: {exc}; "
f"restart the container if logins fail.\n"
)
log.flush()

try:
try:
restart_manager.publish_start_request()
Expand Down
6 changes: 6 additions & 0 deletions database.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@
TASK_HISTORY_MAX_ROWS = 10
MAX_LOG_ENTRIES_STORED = 10

USERS_PASSWORD_CHANGED_AT_DDL = (
"ALTER TABLE IF EXISTS audiomuse_users "
"ADD COLUMN IF NOT EXISTS password_changed_at TIMESTAMP"
)

MAP_PROJECTION_CACHE = None

_embedded_server = None
Expand Down Expand Up @@ -982,6 +987,7 @@ def init_db():
cur.execute(
"ALTER TABLE audiomuse_users ADD COLUMN IF NOT EXISTS role TEXT NOT NULL DEFAULT 'user'"
)
cur.execute(USERS_PASSWORD_CHANGED_AT_DDL)
cur.execute(
"CREATE TABLE IF NOT EXISTS dashboard_stats ("
"id INTEGER PRIMARY KEY, "
Expand Down
14 changes: 14 additions & 0 deletions docs/AUTH.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@ curl -v \
-d '{}'
```

## Session validation and revocation

Every request authenticated with the JWT cookie is also validated against the `audiomuse_users` table:

- The user in the token must still exist; deleting a user immediately terminates their active sessions.
- Changing a user's password immediately invalidates every session token issued before the change (the token's issue time is compared with the `password_changed_at` column). When you change your own password, the response sets a fresh cookie so your current session keeps working; other devices are logged out.
- The role stored in the database wins over the role claim inside the token, so a stale token can never keep more privileges than the account currently has.

## Confirming sensitive operations

Creating a user, changing any password (your own, or - as an admin - another user's), and deleting a user require the acting user to re-enter their own password. The Users page asks for it in a dedicated confirmation field; API callers send it as `current_password` in the JSON body of `POST /api/users`, `PUT /api/users/<id>/password` and `DELETE /api/users/<id>`.

Bearer-token (`API_TOKEN`) callers are exempt from `current_password`: the token itself is the credential and it is not tied to an account password.

# Password reset

If you have lost access to all admin accounts, reset admin access by deleting both the legacy admin config entries and the admin rows in `audiomuse_users`.
Expand Down
Loading
Loading