# Plan — roles and rights From `architecture/roles-auth.md` and `quality/roles-auth.md`. - [x] **Deps**: workspace `argon2` (with `std`), `http`, `tower`; `crabidy-server` gains `argon2`, `base64`, `http`, `tower`, `clap`; `cbd-tui` gains `base64`. Verify: workspace builds. - [x] **Server settings** (`crabidy-server/src/settings.rs`): `ServerSettings { auth: AuthSettings }`, `AuthSettings { owner, queue_owner, queue_appender: Option }`; `load(config_dir)` — absent file ⇒ defaults, malformed file ⇒ startup error. Verify: unit tests for all three cases. - [x] **Auth core** (`crabidy-server/src/auth.rs`): `Role` (ordered), `minimum_role(method) -> Role` default-deny table, `Authenticator` (parse Basic header, argon2 verify, success cache, disabled mode). Verify: unit tests — header parsing never panics, wrong user/password indistinguishable, matrix samples per role, unknown method ⇒ owner, cache fed only by successes, disabled mode allows all without header. - [x] **Tower layer** (`auth.rs`): `AuthLayer`/`AuthService` checking `authorization` against the method's minimum role before the inner service; denials answer trailers-only via `Status::into_http()`. Verify: service-level tests with a counting inner service (deny short-circuits, allow forwards, unauthenticated vs permission-denied codes). - [x] **Wire-up** (`lib.rs::serve`): load settings, fail startup on malformed config, install the layer. Verify: existing tests still pass; layer test covers enforcement. - [x] **hash-password** (`main.rs` + clap): subcommand reads stdin, prints PHC string; round-trip test hash → authenticator accepts. - [x] **Client** (`cbd-tui`): `user`/`password` config options (and flags), auth interceptor attaching a precomputed Basic header to every request, type alias for the intercepted client. Verify: unit tests — no creds ⇒ no header, creds ⇒ header present. - [x] **Docs**: root README (config table row, `cbd-tui.toml` options, security note), new `crabidy-server.toml` section; architecture cross-links. Verify: markdownlint. - [x] **Gates**: run the full suite + clippy + fmt; check off `quality/roles-auth.md`; write `plan/summary.md` section. ## Refinement — anonymous callers inherit the highest unguarded role Superseding the original "any hash ⇒ every RPC requires credentials" switch. Anonymous (no-header) callers now get the most privileged *unguarded* role; a password lowers that floor. Guarding order is enforced top-down. - [x] **Unauthenticated role** (`auth.rs`): `Authenticator` computes and exposes `unauthenticated_role() -> Option` (highest role with no hash; `None` when all guarded, counting a present-but-unusable hash as guarded, fail-closed). `authenticate(None)` returns it or denies; a present-but-wrong header still denies, never downgrades. Verify: `the_unauthenticated_role_is_the_highest_unguarded_one`, `a_header_carrying_bad_credentials_never_falls_back_to_anonymous`, updated layer + failures tests. - [x] **Guarding-order validation** (`settings.rs`): `AuthSettings::validate()` rejects a lower role guarded without the higher one (prefix of `[owner, queue_owner, queue_appender]`); `load` aborts on a bad order, and `cli::write_role_hash` refuses to write one. Verify: `validate_*` unit tests, `a_broken_guarding_ order_aborts_load`, `guard_refuses_to_write_a_role_below_an_ unguarded_one`. - [x] **Docs**: `architecture/roles-auth.md`, `quality/roles-auth.md`, `docs/src/auth.md`, root README updated to the top-down model.