3.7 KiB
3.7 KiB
Plan — roles and rights
From architecture/roles-auth.md and quality/roles-auth.md.
- Deps: workspace
argon2(withstd),http,tower;crabidy-servergainsargon2,base64,http,tower,clap;cbd-tuigainsbase64. 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) -> Roledefault-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/AuthServicecheckingauthorizationagainst the method's minimum role before the inner service; denials answer trailers-only viaStatus::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/passwordconfig 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.tomloptions, security note), newcrabidy-server.tomlsection; architecture cross-links. Verify: markdownlint. - Gates: run the full suite + clippy + fmt; check off
quality/roles-auth.md; writeplan/summary.mdsection.
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):Authenticatorcomputes and exposesunauthenticated_role() -> Option<Role>(highest role with no hash;Nonewhen 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]);loadaborts on a bad order, andcli::write_role_hashrefuses 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.