Skip to content

feat: persisted logins, API keys, read-only role, TOML config file (WI-1741) - #115

Merged
thedancingdeveloper merged 4 commits into
mainfrom
wi-1741-accounts-config
Oct 17, 2026
Merged

thedancingdeveloper merged 4 commits into
mainfrom
wi-1741-accounts-config

Conversation

@thedancingdeveloper

Copy link
Copy Markdown
Collaborator

WI-1741 (RT-WP12): accounts and config.

What this adds

Auth model (commits b67bafb, f208255, b754ff9)

  • Logins survive a restart. Access and refresh tokens go in sessions.json (stored as SHA-256 hashes, 0600). Each session is bound to the credential it was issued under, so a password change, a rtbit auth reset-password, or removing the read-only account ends it straight away, including on a running daemon. A login lasts at most 30 days, however often it is refreshed.
  • API keys (Authorization: Bearer rtb_…): GET/POST /auth/api_keys and DELETE /auth/api_keys/{id}. Admin only. Keys are stored hashed and default to the read-only role. They do not depend on the admin password.
  • Read-only role. rtbit auth set-read-only-user -u NAME (password read from stdin) and rtbit auth remove-read-only-user. A read-only caller can use only GET, HEAD and OPTIONS, and is refused at the WebUI API v2 compat login. reset-password and admin credential changes keep the read-only account. Its username is compared in constant time.

Config file (commit bafc4f4)

  • --config FILE / RTBIT_CONFIG takes a TOML file of non-secret settings. Unknown keys are errors. The command line and environment win over the file.
  • rtbit config dump prints the effective settings. Secrets are never included.

Review fixes in b754ff9

  • /auth/logout needs no session, like login and refresh. It used to sit behind the role gate, so a read-only caller got 403 and its refresh token stayed usable.
  • The rtbit auth read-only commands open the credentials with CredentialStore::open, which never touches the setup token. Before, running them before first-run setup replaced the server's setup-token file, and setup with that token was refused.

Verification (at b754ff9)

  • cargo test -p swarmforge --features http-api: 200 passed, 5 ignored
  • cargo test -p rtbit: 19 passed
  • cargo clippy --workspace --exclude rtbit-desktop --no-default-features --features default-tls -- -D warnings: clean
  • cargo fmt --check: clean
  • Scripted e2e against the built rtbit binary on loopback, with isolated data dirs: all checks pass. It covers setup-token integrity, read-only login/read/write-refusal/logout, API keys, persistence across a restart, CLI removal and reset on a running daemon, change_credentials, and config precedence and dump.

Not covered: the Docker image, reverse proxy and desktop builds. The desktop build gets persisted sessions but leaves API keys off.

Not in this PR

  • No WebUI yet for API keys or the read-only account.
  • The CLI still prints "restart rtbit so a running server picks it up", but changes now apply at once.

🤖 Generated with Claude Code

thedancingdeveloper and others added 4 commits October 17, 2026 10:30
…741)

- Sessions (access and refresh tokens) are saved to sessions.json in the
  credentials directory, hashed with SHA-256, so logins survive a restart.
  Expired entries are dropped on load. An unreadable file signs everyone out
  rather than refusing to start.
- API keys for automation, sent as `Authorization: Bearer rtb_...`. Stored
  hashed in api-keys.json; the key is returned once, at creation. Managed by
  admins through GET/POST /auth/api_keys and DELETE /auth/api_keys/{id}.
  A corrupt keys file refuses to start, as credentials do.
- A read-only account, added with `rtbit auth set-read-only-user` (password on
  stdin) and removed with `rtbit auth remove-read-only-user`. It may use only
  GET, HEAD and OPTIONS; anything else gets 403. Works over Bearer and Basic.
  Admin changes (`set_credentials`) keep it.
- The middleware resolves a role for every credential type and records it in
  the request. Key management is admin-only.
- The qBittorrent compat login refuses the read-only account. Its sessions
  carry no role, so allowing it would grant write access.

Verified against the built daemon: setup, read-only and admin access across
restarts, API key create/use/delete, refresh-token rotation, and that no raw
token or key is on disk.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
Adds `--config PATH` (or RTBIT_CONFIG) for a TOML file of non-secret
settings: http_api, network, limits, indexarr and logging. Precedence is
command line, then environment, then file, then built-in default, so a flag
or variable always overrides the file.

- Unknown keys are errors, so a typo cannot be silently ignored.
- Secrets are not accepted: the indexarr API key and passwords stay in the
  environment or the credentials store.
- `rtbit config dump` prints the merged settings as TOML. Secrets are never
  included, so the output is safe to share.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
…te (WI-1741)

- A session is bound to the credential it was issued under (a fingerprint of
  the admin username and password hash, plus the read-only account for its
  sessions). Changing the admin password, through the UI or `rtbit auth
  reset-password`, ends every session. Removing or changing the read-only
  account ends its sessions at once. Checked on every request and on refresh.
- Refresh cannot extend a login past 30 days from its first sign-in. Access
  and refresh tokens are both capped at that point.
- Credentials are read from credentials.json on every check, and every change
  is applied to the file under a lock starting from what is on disk. A server
  no longer holds a stale copy: an account added by `rtbit auth` is seen
  without a restart, a removed account cannot come back when the admin
  password is saved, and an admin save keeps accounts added by another process.
- `reset-password` keeps the read-only account when the existing file can be
  read. A corrupt file cannot be preserved, and that is reported.
- The read-only login compares the username by SHA-256 digest and always
  verifies a password (against a dummy hash when there is no account), so the
  work done does not depend on whether the account exists or the username
  matched.

Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
…-1741)

/auth/logout was behind the role gate, which refuses every POST from a
read-only caller, so a read-only session could not be ended and its refresh
token stayed usable. Logout now needs no session, like login and refresh: it
only revokes the refresh token it is sent. This also lets a caller whose
access token has expired log out.

`rtbit auth set-read-only-user` and `remove-read-only-user` used
CredentialStore::load, which before first-run setup made a new setup token and
wrote it over the running server's setup-token file, so setup with that file's
token was refused. They now use CredentialStore::open, which never touches the
setup token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@thedancingdeveloper
thedancingdeveloper merged commit 0d792e1 into main Oct 17, 2026
10 checks passed
@thedancingdeveloper
thedancingdeveloper deleted the wi-1741-accounts-config branch October 17, 2026 19:46
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