65 lines
3.7 KiB
Markdown
65 lines
3.7 KiB
Markdown
# 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<String> }`;
|
|
`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<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.
|
|
- [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.
|