Skip to main content

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}