thunder/server/dispatch.rs
1//! The product integration surface (SRV-020..022): one trait, three hooks.
2//!
3//! Command routing, argument extraction and business logic are product-side
4//! (SRV-020); credential validation is product code — Thunder owns the
5//! handshake state machine, never the credential store (SRV-012). Command
6//! name matching is byte-exact pass-through: case policy lives inside the
7//! product's `dispatch` (SRV-022).
8
9use std::future::Future;
10
11use crate::wire::Value;
12
13use crate::server::session::Session;
14
15/// Credentials parsed by Thunder from `HELLO`/`AUTH` payloads (SRV-012).
16///
17/// - `AUTH <api_key>` → [`Credentials::ApiKey`] (single-arg form)
18/// - `AUTH <user> <pass>` → [`Credentials::UserPass`]
19/// - `HELLO {token: …}` → [`Credentials::Token`] (map payload)
20/// - `HELLO {api_key: …}` → [`Credentials::ApiKey`]
21/// - `HELLO {}` / missing map → [`Credentials::None`] — a deployment with
22/// `auth_required = false` accepts it; everyone else rejects it.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum Credentials {
25 /// A bare API key.
26 ApiKey(String),
27 /// Username + password.
28 UserPass(String, String),
29 /// A bearer token from a `MapPayload` HELLO.
30 Token(String),
31 /// No credentials supplied.
32 None,
33}
34
35/// The identity a successful [`Dispatch::authenticate`] resolves to. Stored
36/// on the [`Session`] and fed to [`Dispatch::capabilities`] for the HELLO
37/// reply (SRV-014).
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct Principal<I = ()> {
40 /// Product-defined principal name (user, key id, …).
41 pub name: String,
42 /// The product's own resolved identity — roles, permissions, quotas,
43 /// tenant, whatever authorization actually needs.
44 ///
45 /// Before this existed a product could only carry the *name*, so every
46 /// privileged command had to re-resolve the user from its credential
47 /// store. That was not merely a cost: the second lookup reads live state,
48 /// so a user edited or deleted mid-session was evaluated against the new
49 /// record. Carrying the identity here restores the other semantics —
50 /// **captured at `AUTH`, stable for the session** — and makes the choice
51 /// the product's rather than an accident of the transport.
52 ///
53 /// Defaults to `()` for products that only need the name.
54 pub identity: I,
55}
56
57impl Principal {
58 /// A principal carrying only a name — the `Identity = ()` case.
59 pub fn new(name: impl Into<String>) -> Self {
60 Self {
61 name: name.into(),
62 identity: (),
63 }
64 }
65}
66
67impl<I> Principal<I> {
68 /// A principal carrying the product's resolved identity.
69 pub fn with_identity(name: impl Into<String>, identity: I) -> Self {
70 Self {
71 name: name.into(),
72 identity,
73 }
74 }
75}
76
77/// Authentication failure from the product hook (SRV-012). Thunder maps it
78/// to the profile's error convention before it reaches the wire (SRV-021).
79#[derive(Debug, thiserror::Error)]
80pub enum AuthError {
81 /// Credentials failed validation. Rendered as the family's
82 /// `WRONGPASS …` string under `Resp3Prefixes`, `[unauthorized] …`
83 /// under `BracketCode`/`Both`.
84 #[error("invalid credentials")]
85 InvalidCredentials,
86 /// Product-specific failure; the message travels verbatim (WIRE-040).
87 #[error("{0}")]
88 Message(String),
89}
90
91/// Product integration is exactly this trait (SRV-020).
92///
93/// Declared with return-position `impl Future + Send` so implementers can
94/// write plain `async fn` (no `async-trait` dependency) while the listener
95/// can still spawn dispatch futures onto the runtime. The listener is
96/// generic over `D: Dispatch`, so object safety is not required.
97pub trait Dispatch: Send + Sync + 'static {
98 /// The product's own identity payload, resolved once at `AUTH` and
99 /// carried on the session (SRV-012).
100 ///
101 /// Write `type Identity = ();` when the principal's name is all the
102 /// product needs. Rust has no stable associated-type defaults, so the
103 /// line is required even in that case — the ergonomics live on
104 /// [`Principal`] and [`Session`], which both default their parameter
105 /// to `()`.
106 type Identity: Send + Sync + 'static;
107
108 /// Run one command. The error `String` travels verbatim on the wire
109 /// (SRV-021, WIRE-040); a returned `Err` never closes the connection
110 /// (SRV-005).
111 fn dispatch(
112 &self,
113 session: &Session<Self::Identity>,
114 command: &str,
115 args: Vec<Value>,
116 ) -> impl Future<Output = Result<Value, String>> + Send;
117
118 /// Validate credentials parsed from `HELLO`/`AUTH` (SRV-012). Thunder
119 /// flips the session's auth flag on `Ok` — product code never touches
120 /// the state machine.
121 fn authenticate(
122 &self,
123 creds: Credentials,
124 ) -> impl Future<Output = Result<Principal<Self::Identity>, AuthError>> + Send;
125
126 /// Capability names advertised in `MapPayload` HELLO replies
127 /// (SRV-014). Defaults to none.
128 fn capabilities(&self, principal: &Principal<Self::Identity>) -> Vec<String> {
129 let _ = principal;
130 vec![]
131 }
132}