155 lines
6.1 KiB
Markdown
155 lines
6.1 KiB
Markdown
# Roles and rights (basic-auth authorization)
|
|
|
|
crabidy listens on `0.0.0.0:50051`: anyone on the network can control
|
|
playback and — worse — rename or delete library stores. The owner wants
|
|
to hand out *limited* remotes: a co-host who may run the queue but not
|
|
touch the library, and guests who may only add tracks.
|
|
|
|
## Problem statement
|
|
|
|
Three roles, credentialed by password hashes in the server config:
|
|
|
|
- **owner** — the normal user; everything.
|
|
- **queue-owner** — anything on the queue (and playback), but no
|
|
library writes: no `w`, no `W`.
|
|
- **queue-appender** — may only add tracks to the end of the queue;
|
|
no removal, no reordering, no queue settings.
|
|
|
|
## Assumptions (confirmed by the request, or decided here)
|
|
|
|
- Transport stays plain HTTP/2 gRPC. Basic auth over cleartext is
|
|
acceptable on a trusted home network; anything else (Internet
|
|
exposure) needs TLS termination in front (reverse proxy, VPN) and is
|
|
out of scope. The README says so.
|
|
- No auth configured (no `[auth]` section, or no hashes in it) means
|
|
the server behaves exactly as before: open, everyone is owner. Auth
|
|
switches on as soon as **any** role hash is configured; from then on
|
|
every RPC requires credentials.
|
|
- One password per role, not per person. The Basic-auth *username*
|
|
selects the role (`owner`, `queue-owner`, `queue-appender`), the
|
|
password is verified against that role's hash.
|
|
|
|
## Options considered
|
|
|
|
### Where to enforce
|
|
|
|
1. **Per-handler checks** inside `RpcService` (tonic interceptor
|
|
authenticates, each of the ~25 handlers calls
|
|
`require(role)?`). Idiomatic tonic, but *fail-open*: a future RPC
|
|
that forgets the line is unprotected.
|
|
2. **One tower layer** in front of the tonic service, mapping the
|
|
gRPC method path to a minimum role, *default-deny* (unknown method
|
|
⇒ owner only). Fail-closed, single enforcement point, zero handler
|
|
churn; costs a small amount of manual HTTP plumbing for the
|
|
deny response (gRPC trailers-only response).
|
|
|
|
**Decision: option 2.** Authorization is a security boundary; new
|
|
RPCs must start locked. A unit test pins the method table against the
|
|
proto service definition so an unmapped addition fails loudly.
|
|
|
|
### Hash scheme
|
|
|
|
PHC-format strings verified with the pure-Rust `argon2` crate
|
|
(RustCrypto). Argon2id is the default the crate generates; any PHC
|
|
variant the crate parses is accepted. bcrypt/scrypt support is not
|
|
worth a second dependency. To keep users out of hash-tooling misery,
|
|
`crabidy-server hash-password` reads a password on stdin and prints
|
|
the PHC string to paste into the config.
|
|
|
|
### Verification cost
|
|
|
|
Argon2 verification is deliberately slow (tens of ms); per-keypress
|
|
RPCs cannot re-verify. Successful credentials are cached in memory
|
|
(`authorization` header value → role). Only *successful* verifications
|
|
are cached, so the cache is bounded by the number of valid credentials
|
|
(≤ 3); failures pay the full argon2 cost every time, which doubles as
|
|
throttling.
|
|
|
|
## The rights matrix
|
|
|
|
Minimum role per RPC; higher roles include lower ones
|
|
(owner ⊃ queue-owner ⊃ queue-appender):
|
|
|
|
- **queue-appender** (and up): `Init`, `GetLibraryNode`,
|
|
`GetUpdateStream` (reads), `Append` (the one queue write), and
|
|
`CreateLibraryNode` — creatable nodes are exactly the search terms,
|
|
and guests must be able to search for what they append. (Search
|
|
terms do persist in the owner's provider config; accepted — they are
|
|
the mechanism of finding tracks, not library data.)
|
|
- **queue-owner** (and up): every other queue and playback verb —
|
|
`Queue`, `Replace`, `Remove`, `Insert`, `ClearQueue`, `SetCurrent`,
|
|
`ToggleShuffle`, `ToggleRepeat`, `TogglePlay`, `Stop`, `Next`,
|
|
`Prev`, `RestartTrack`, `ChangeVolume`, `ToggleMute`.
|
|
- **owner** only: the library writes — `CaptureLibraryNode` (`w`/`W`),
|
|
`SaveQueue` (writes `/queues`), `RenameLibraryNode`,
|
|
`DeleteLibraryNode` (these two also cover search terms; a
|
|
queue-owner can create terms but not rename/delete them — the
|
|
server cannot cheaply tell a term from a bookmark store at this
|
|
layer, so renames/deletes stay owner-only, fail-closed).
|
|
- **unknown / future methods**: owner only.
|
|
|
|
Denied requests get `PERMISSION_DENIED`; missing or wrong credentials
|
|
get `UNAUTHENTICATED`. Credentials are never logged (hard rule:
|
|
secrets redacted).
|
|
|
|
## Configuration
|
|
|
|
`~/.config/crabidy/crabidy-server.toml` (new, read by the server —
|
|
both standalone and inside `cbd`; absent file = auth off):
|
|
|
|
```toml
|
|
[auth]
|
|
# One PHC hash per role; omit a role to disable it.
|
|
# Generate with: crabidy-server hash-password
|
|
owner = "$argon2id$v=19$m=19456,t=2,p=1$..."
|
|
queue_owner = "$argon2id$v=19$..."
|
|
queue_appender = "$argon2id$v=19$..."
|
|
```
|
|
|
|
`~/.config/crabidy/cbd-tui.toml` (client side, also as CLI flags):
|
|
|
|
```toml
|
|
address = "http://127.0.0.1:50051"
|
|
# Sent as HTTP basic auth when set. `user` is the role name.
|
|
user = "queue-owner"
|
|
password = "plaintext"
|
|
```
|
|
|
|
The client config holds a *plaintext* password (client credentials
|
|
always are); the README tells users to keep the file private. The TUI
|
|
attaches `authorization: Basic …` to every request through a tonic
|
|
interceptor; without configured credentials it sends no header, which
|
|
keeps today's zero-config local setup working against an open server.
|
|
|
|
## Structure
|
|
|
|
```d2
|
|
direction: right
|
|
tui: cbd-tui {
|
|
cfg: "cbd-tui.toml user/password"
|
|
interceptor: "auth interceptor\n(adds Basic header)"
|
|
cfg -> interceptor
|
|
}
|
|
server: crabidy-server {
|
|
layer: "AuthLayer (tower)\nheader → role → method check"
|
|
rpc: RpcService
|
|
cache: "verified-creds cache"
|
|
cfg: "crabidy-server.toml [auth] hashes"
|
|
cfg -> layer
|
|
layer -> cache: hit = skip argon2
|
|
layer -> rpc: authorized
|
|
}
|
|
tui.interceptor -> server.layer: every RPC
|
|
server.layer -> tui: "UNAUTHENTICATED /\nPERMISSION_DENIED"
|
|
```
|
|
|
|
## Risks and open questions
|
|
|
|
- **Denied-action UX**: the TUI currently logs RPC errors; an
|
|
appender pressing a forbidden key sees a silent no-op (the log has
|
|
the denial). A status-line hint is deferred until the flow has been
|
|
felt in practice.
|
|
- **Cleartext transport**: documented; TLS stays out of scope.
|
|
- The update stream is authorized once at subscription; the stream
|
|
itself only carries state every role may read.
|