6.1 KiB
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, noW. - 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
- Per-handler checks inside
RpcService(tonic interceptor authenticates, each of the ~25 handlers callsrequire(role)?). Idiomatic tonic, but fail-open: a future RPC that forgets the line is unprotected. - 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), andCreateLibraryNode— 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):
[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):
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
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.