crabidy/plan/roles-auth.md

3.7 KiB

Plan — roles and rights

From architecture/roles-auth.md and quality/roles-auth.md.

  • Deps: workspace argon2 (with std), http, tower; crabidy-server gains argon2, base64, http, tower, clap; cbd-tui gains base64. Verify: workspace builds.
  • Server settings (crabidy-server/src/settings.rs): ServerSettings { auth: AuthSettings }, AuthSettings { owner, queue_owner, queue_appender: Option<String> }; load(config_dir) — absent file ⇒ defaults, malformed file ⇒ startup error. Verify: unit tests for all three cases.
  • 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.
  • 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).
  • Wire-up (lib.rs::serve): load settings, fail startup on malformed config, install the layer. Verify: existing tests still pass; layer test covers enforcement.
  • hash-password (main.rs + clap): subcommand reads stdin, prints PHC string; round-trip test hash → authenticator accepts.
  • 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.
  • Docs: root README (config table row, cbd-tui.toml options, security note), new crabidy-server.toml section; architecture cross-links. Verify: markdownlint.
  • 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.

  • Unauthenticated role (auth.rs): Authenticator computes and exposes unauthenticated_role() -> Option<Role> (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.
  • 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.
  • Docs: architecture/roles-auth.md, quality/roles-auth.md, docs/src/auth.md, root README updated to the top-down model.