Skip to content

feat(accounts): whole-app login, first-run setup gate, multi-user roles, scrypt hashing - #45

Merged
tajanthind merged 3 commits into
mainfrom
fix/accounts-first-run-login
Sep 10, 2026
Merged

tajanthind merged 3 commits into
mainfrom
fix/accounts-first-run-login

Conversation

@tajanthind

Copy link
Copy Markdown
Owner

Merge order: merge #44 (plugin-arch-v101…) into main FIRST — this branch is built on top of it (they share the README/app.py edits). This PR is intentionally stacked.

What

Implements accounts (services/accounts.py) — the app's first real authentication:

First-run setup gate

When no user accounts exist (fresh volume, or a wiped/reset config DB), only /setup, /login, /logout, /health, /static, /socket.io are reachable. Pages redirect to /setup; /api/* returns 403 {"error": "No account is configured…"}. The setup page creates the initial admin account; it reappears whenever no account info can be found.

Whole-app lockdown

Every page and /api route requires an identity:

  1. Session account (login form; signed cookie, HttpOnly + SameSite=Lax, 14-day session)
  2. Optional M2M API key (X-API-Key / Authorization: Bearer) — machine clients keep working unchanged
  3. auth_provider plugins (reverse-proxy SSO) as an additional identity source

Industry-standard credential storage

werkzeug.security scrypt hashes — salted, one-way, constant-time verify. Never plaintext (test-asserted). New User model (users table; db.create_all() creates it at boot on existing DBs too — no manual migration).

Multi-user with roles

admin (account management) vs user (everything else); only the first account is admin. Admin-only /api/accounts CRUD + promote/demote; /api/accounts/me/password for everyone. Self-delete and last-admin-demotion are refused.

CSRF defence

Session-authenticated state changes from a cross-origin request are refused via an Origin check (plus SameSite=Lax cookies).

UI

login.html, setup.html, account chip + logout in every page nav, and Settings cards for change-password (everyone) and user management (admin).

Docs

README/DEPLOY first-run steps + M2M key note + FNACK_COOKIE_SECURE; docs/architecture.md "Accounts & authentication"; AUTHORING M2M wording.

Files

services/accounts.py (new), models.py (User), app.py (guard/blueprint/session config/context processor), templates/{login,setup}.html (new) + nav chips in all pages, static/app.js, docs/architecture.md, README.md, DEPLOY.md, docs/plugins/AUTHORING.md, tests/architecture/test_accounts_first_run.py (new).

Verification

  • Architecture suite 27/27 pass (incl. new accounts tests: setup gate, scrypt storage, login/logout, M2M key, role enforcement, CSRF origin check); smoke test passes.

…nstall, and update

Multi-repo duplicate-id handling was incoherent: browse de-duplicated by id
("newest/whatever-wins"), install picked the FIRST repository that had the
id, and update flags scanned every repo. Ordering (repo insertion time) was
an implicit side effect and there was no provenance in install requests.

New, documented contract (docs/architecture.md, "Marketplace identity"):

- Browse returns ONE entry per (repository, plugin) — duplicate ids across
  enabled repositories are never merged; every entry lists the other
  publishers (`also_in_repos`) and is labelled `installed_elsewhere` when
  the id is installed from a different repository.
- install(plugin_id, version, source_repo_id) — provenance is explicit and
  recorded on InstalledPlugin.source_repo_id. Without a source, an id
  published by >1 enabled repository is REFUSED as ambiguous (candidates
  listed); a version/id the chosen repo does not publish is refused.
- update() re-installs from the repository the plugin was INSTALLED from —
  never from whichever repo is checked first. latest_versions() is
  source-aware so duplicate ids elsewhere never drive update badges.
- Duplicates are detected, not ignored: add/refresh-repository responses
  carry conflict warnings; marketplace cards show "Also in: <repo>" inline.
- Config-as-code export now records source_repo_url per plugin; import
  re-installs from the same repository URL.

UI: install/update/switch buttons pass source_repo_id; switching an id
installed from another repository is an explicit labelled action; conflict
warnings render as toasts + inline card warnings.

New arch test tests/architecture/test_marketplace_repo_identity.py pins the
contract (per-repo browse, ambiguity refusal, provenance recording, source-
aware update, conflict detection).
…s, remove subsonic plugin

Three related "plugin ecosystem hygiene" items:

1. PLUGIN_API_VERSION 1.0.0 -> 1.0.1 (plugins/__init__.py; fnack/plugin_api
   SDK_VERSION mirrors). The security batch (permissions enforced on every
   facade, context.http=None without "network", secret-at-rest) changed the
   plugin contract behaviorally; manifests declaring ^1.0 remain compatible
   (1.0.1 is contained), this is a minor, non-breaking bump that records the
   change.

2. Remaining security limitations processed (detect + document):
   - NEW load-time static scan (plugins/manager.py _network_capable_imports):
     plugins importing network-capable modules (requests/urllib/http.client/
     httpx/aiohttp/urllib3/socket/websockets) WITHOUT declaring "network"
     now get a prominent warning at load — the direct-import bypass of
     context.http is surfaced instead of silent. Warning-only (static scans
     can't be perfect); bundled official plugins are held to the rule by
     tests/architecture/test_plugin_network_honesty.py (network imports =>
     "network" declared).
   - AUTHORING.md documents both remaining limitations with concrete
     guidance: the network-honesty scan + why it warns, and secret-key loss
     (plugin_secret.key must back up with the DB; deleting it revokes all
     stored plugin secrets; restore without it => re-enter secrets).

3. fnack.subsonic removed from the fnack ecosystem (core side):
   - bundled_plugins/fnack.subsonic deleted (paired with fnack-plugins PR
     removing it from the catalog/index/README).
   - app.py: header docstring, default_disabled set, blueprint comment no
     longer reference it.
   - docs/architecture.md: official-plugins table row, server.extension
     capability example and the media-scan vs server-API distinction
     paragraph rewritten at concept level.
   - README: subsonic-server claims removed.
   - Tests updated: test_essential_plugins optional list, doc-gate subsonic
     test now asserts removal + concept distinction, plugin_boundary no
     longer requires a bundled server.extension provider.
… roles, scrypt hashing

Accounts (services/accounts.py) — replaces the optional-only auth model:

- FIRST-RUN GATE: when no user accounts exist (fresh volume / wiped DB),
  only /setup (+ /health, /static, /login, /logout, /socket.io) is
  reachable until the initial ADMIN account is created. Pages redirect to
  /setup; /api returns 403 with a clear error.
- LOGIN: session-cookie auth for every page and /api route. Identity
  precedence: session account > optional M2M API key (X-API-Key / Bearer,
  unchanged for machine clients) > enabled auth_provider plugins
  (reverse-proxy SSO).
- CREDENTIALS: werkzeug scrypt hashes (salted, one-way) — never plaintext;
  constant-time verify. New User model (users table; created by the boot
  create_all, no manual migration needed).
- ROLES: admin (manage accounts) vs user (everything else). Only the first
  account is admin. Account management UI on Settings (list/create/delete/
  promote) + change-own-password; admin-only API under /api/accounts.
- HARDENING: cookies HttpOnly + SameSite=Lax (+ FNACK_COOKIE_SECURE=1
  behind https); cross-origin state changes from session-authenticated
  requests are refused (Origin check, CSRF); login session 14 days.
- UI: login.html + setup.html; account chip + logout in every page nav;
  Accounts + change-password cards on Settings.
- Docs: README/DEPLOY quick-start first-run steps + M2M key note;
  docs/architecture.md "Accounts & authentication" section; AUTHORING M2M
  key wording.

NOTE: built on top of the plugin-arch-1.0.1/subsonic-removal branch
(PR #44) — merge #44 into main first, then this one.

Tests: tests/architecture/test_accounts_first_run.py (setup gate, scrypt
storage, login/logout, M2M key, role enforcement, CSRF origin check) —
27/27 architecture tests + smoke pass.
@tajanthind
tajanthind merged commit 1dc02f2 into main Sep 10, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant