Skip to main content

web_net/
gate.rs

1//! The two gate checkpoints: authentication (who is asking) and
2//! authorization (may they use this route).
3//!
4//! A handler only ever receives a [`Cx`](crate::Cx), and web-net builds one
5//! only after the route's access check passed. There is no other way to
6//! make one, so a route that skips the gate does not compile.
7
8use std::fmt;
9
10use crate::Request;
11
12/// Who is asking. A request that reaches a handler always carries one,
13/// possibly a guest.
14pub trait Identity: Send + Sync + 'static {
15    /// Whether this is a visitor with no signed-in session.
16    fn is_guest(&self) -> bool;
17
18    /// The value CSRF tokens are bound to: the session id, or a pre-session
19    /// id for a guest. `None` means no binding exists, and an unsafe method
20    /// on a CSRF-protected route is refused.
21    fn csrf_binding(&self) -> Option<&[u8]> {
22        None
23    }
24
25    /// A `Set-Cookie` value the gate wants on the reply (a dead session's
26    /// cookie cleared, a pre-session id set). [`App::respond`](crate::App::respond)
27    /// writes it on every exit after authentication, once; with
28    /// [`App::respond_identified`](crate::App::respond_identified) the caller
29    /// authenticated and writes its own. A reply that carries it is never
30    /// reported as keepable by a cache.
31    fn set_cookie(&self) -> Option<&str> {
32        None
33    }
34}
35
36/// The identity of [`Open`]: everyone is a guest.
37#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
38pub struct Guest;
39
40impl Identity for Guest {
41    #[inline]
42    fn is_guest(&self) -> bool {
43        true
44    }
45}
46
47/// Authentication and authorization, the two checkpoints a request passes
48/// before a handler runs.
49///
50/// One trait, so an application has one type parameter. An implementation
51/// backed by remote state fails closed: when it cannot decide, it denies.
52///
53/// ```
54/// use web_net::{Denial, Gate, Identity, Request};
55///
56/// struct User { admin: bool }
57/// impl Identity for User {
58///     fn is_guest(&self) -> bool { false }
59/// }
60///
61/// enum Rule { Admin }
62///
63/// struct Demo;
64/// impl Gate for Demo {
65///     type Identity = User;
66///     type Rule = Rule;
67///     fn authenticate(&self, req: &Request<'_>) -> Result<User, Denial> {
68///         Ok(User { admin: req.header("x-demo-admin").is_some() })
69///     }
70///     fn authorize(&self, who: &User, rule: &Rule, _req: &Request<'_>) -> Result<(), Denial> {
71///         match rule {
72///             Rule::Admin if who.admin => Ok(()),
73///             Rule::Admin => Err(Denial::forbidden("demo.not_admin")),
74///         }
75///     }
76/// }
77/// ```
78pub trait Gate: Send + Sync + 'static {
79    /// Who a request comes from.
80    type Identity: Identity;
81
82    /// A route's access rule, declared with [`Route::allow`](crate::Route::allow).
83    type Rule: Send + Sync + 'static;
84
85    /// Runs once per request, before routing. A missing or invalid
86    /// credential is a guest (`Ok`); a banned visitor is a denial.
87    ///
88    /// Not called by [`App::respond_identified`](crate::App::respond_identified),
89    /// whose caller authenticated already.
90    ///
91    /// # Errors
92    ///
93    /// A [`Denial`] ends the request with its status.
94    fn authenticate(&self, req: &Request<'_>) -> Result<Self::Identity, Denial>;
95
96    /// Runs after the route and method matched and before anything else of
97    /// the route. Not called for routes declared public.
98    ///
99    /// # Errors
100    ///
101    /// A [`Denial`] ends the request with its status.
102    fn authorize(
103        &self,
104        who: &Self::Identity,
105        rule: &Self::Rule,
106        req: &Request<'_>,
107    ) -> Result<(), Denial>;
108}
109
110/// A rule that cannot be made: the rule type of [`Open`], whose routes can
111/// only be public.
112#[derive(Clone, Copy, Debug, PartialEq, Eq)]
113pub enum NoRule {}
114
115/// The default gate: every visitor is a [`Guest`], and only routes declared
116/// [`public`](crate::Route::public) can exist.
117#[derive(Clone, Copy, Debug, Default)]
118pub struct Open;
119
120impl Gate for Open {
121    type Identity = Guest;
122    type Rule = NoRule;
123
124    #[inline]
125    fn authenticate(&self, _req: &Request<'_>) -> Result<Guest, Denial> {
126        Ok(Guest)
127    }
128
129    fn authorize(&self, _who: &Guest, rule: &NoRule, _req: &Request<'_>) -> Result<(), Denial> {
130        match *rule {}
131    }
132}
133
134/// What the visitor is told when a gate denies.
135#[non_exhaustive]
136#[derive(Clone, Copy, Debug, PartialEq, Eq)]
137pub enum DenialKind {
138    /// Sign in first: a 303 to the site's sign-in page for a page request,
139    /// a 401 otherwise.
140    SignIn,
141    /// 403 Forbidden.
142    Forbidden,
143    /// 404 Not Found: the visitor is not told that the thing exists.
144    Hidden,
145    /// Another 4xx status (a 429 for a standing limit, for example).
146    Status(u16),
147}
148
149/// A refusal from a gate: what the visitor sees, and a stable code for logs
150/// and events (never shown to the visitor).
151#[derive(Clone, PartialEq, Eq)]
152pub struct Denial {
153    kind: DenialKind,
154    code: &'static str,
155    cookie: Option<Box<str>>,
156}
157
158impl Denial {
159    /// Sign in first.
160    #[must_use]
161    pub const fn sign_in(code: &'static str) -> Self {
162        Self {
163            kind: DenialKind::SignIn,
164            code,
165            cookie: None,
166        }
167    }
168
169    /// 403 Forbidden.
170    #[must_use]
171    pub const fn forbidden(code: &'static str) -> Self {
172        Self {
173            kind: DenialKind::Forbidden,
174            code,
175            cookie: None,
176        }
177    }
178
179    /// 404 Not Found, hiding that the resource exists.
180    #[must_use]
181    pub const fn hidden(code: &'static str) -> Self {
182        Self {
183            kind: DenialKind::Hidden,
184            code,
185            cookie: None,
186        }
187    }
188
189    /// Another client-error status. A status outside 400 to 499 is answered
190    /// as 403: a gate cannot turn a denial into a success or a server error.
191    #[must_use]
192    pub const fn status(status: u16, code: &'static str) -> Self {
193        let kind = if matches!(status, 400..=499) {
194            DenialKind::Status(status)
195        } else {
196            DenialKind::Forbidden
197        };
198        Self {
199            kind,
200            code,
201            cookie: None,
202        }
203    }
204
205    /// A `Set-Cookie` value the refusal carries (a dead session's cookie
206    /// cleared): written on the denial's reply.
207    #[must_use]
208    pub fn with_cookie(mut self, set_cookie: impl Into<Box<str>>) -> Self {
209        self.cookie = Some(set_cookie.into());
210        self
211    }
212
213    /// The `Set-Cookie` value, if any.
214    #[must_use]
215    pub fn cookie(&self) -> Option<&str> {
216        self.cookie.as_deref()
217    }
218
219    /// What the visitor is told.
220    #[must_use]
221    pub const fn kind(&self) -> DenialKind {
222        self.kind
223    }
224
225    /// The code for logs and events.
226    #[must_use]
227    pub const fn code(&self) -> &'static str {
228        self.code
229    }
230
231    /// The status answered when no sign-in page applies.
232    #[must_use]
233    pub const fn status_code(&self) -> u16 {
234        match self.kind {
235            DenialKind::SignIn => 401,
236            DenialKind::Forbidden => 403,
237            DenialKind::Hidden => 404,
238            DenialKind::Status(s) => s,
239        }
240    }
241}
242
243impl fmt::Debug for Denial {
244    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245        f.debug_struct("Denial")
246            .field("kind", &self.kind)
247            .field("code", &self.code)
248            .field("cookie", &self.cookie.as_ref().map(|_| "<set>"))
249            .finish()
250    }
251}
252
253impl fmt::Display for Denial {
254    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255        write!(f, "denied ({}): {}", self.status_code(), self.code)
256    }
257}
258
259impl std::error::Error for Denial {}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn test_denial_statuses() {
267        assert_eq!(Denial::sign_in("a").status_code(), 401);
268        assert_eq!(Denial::forbidden("a").status_code(), 403);
269        assert_eq!(Denial::hidden("a").status_code(), 404);
270        assert_eq!(Denial::status(429, "a").status_code(), 429);
271        assert_eq!(Denial::status(200, "a").status_code(), 403);
272        assert_eq!(Denial::status(500, "a").status_code(), 403);
273        assert_eq!(Denial::forbidden("x.y").code(), "x.y");
274    }
275}