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
26/// The identity of [`Open`]: everyone is a guest.
27#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
28pub struct Guest;
29
30impl Identity for Guest {
31    #[inline]
32    fn is_guest(&self) -> bool {
33        true
34    }
35}
36
37/// Authentication and authorization, the two checkpoints a request passes
38/// before a handler runs.
39///
40/// One trait, so an application has one type parameter. An implementation
41/// backed by remote state fails closed: when it cannot decide, it denies.
42///
43/// ```
44/// use web_net::{Denial, Gate, Identity, Request};
45///
46/// struct User { admin: bool }
47/// impl Identity for User {
48///     fn is_guest(&self) -> bool { false }
49/// }
50///
51/// enum Rule { Admin }
52///
53/// struct Demo;
54/// impl Gate for Demo {
55///     type Identity = User;
56///     type Rule = Rule;
57///     fn authenticate(&self, req: &Request<'_>) -> Result<User, Denial> {
58///         Ok(User { admin: req.header("x-demo-admin").is_some() })
59///     }
60///     fn authorize(&self, who: &User, rule: &Rule, _req: &Request<'_>) -> Result<(), Denial> {
61///         match rule {
62///             Rule::Admin if who.admin => Ok(()),
63///             Rule::Admin => Err(Denial::forbidden("demo.not_admin")),
64///         }
65///     }
66/// }
67/// ```
68pub trait Gate: Send + Sync + 'static {
69    /// Who a request comes from.
70    type Identity: Identity;
71
72    /// A route's access rule, declared with [`Route::allow`](crate::Route::allow).
73    type Rule: Send + Sync + 'static;
74
75    /// Runs once per request, before routing. A missing or invalid
76    /// credential is a guest (`Ok`); a banned visitor is a denial.
77    ///
78    /// Not called by [`App::respond_identified`](crate::App::respond_identified),
79    /// whose caller authenticated already.
80    ///
81    /// # Errors
82    ///
83    /// A [`Denial`] ends the request with its status.
84    fn authenticate(&self, req: &Request<'_>) -> Result<Self::Identity, Denial>;
85
86    /// Runs after the route and method matched and before anything else of
87    /// the route. Not called for routes declared public.
88    ///
89    /// # Errors
90    ///
91    /// A [`Denial`] ends the request with its status.
92    fn authorize(
93        &self,
94        who: &Self::Identity,
95        rule: &Self::Rule,
96        req: &Request<'_>,
97    ) -> Result<(), Denial>;
98}
99
100/// A rule that cannot be made: the rule type of [`Open`], whose routes can
101/// only be public.
102#[derive(Clone, Copy, Debug, PartialEq, Eq)]
103pub enum NoRule {}
104
105/// The default gate: every visitor is a [`Guest`], and only routes declared
106/// [`public`](crate::Route::public) can exist.
107#[derive(Clone, Copy, Debug, Default)]
108pub struct Open;
109
110impl Gate for Open {
111    type Identity = Guest;
112    type Rule = NoRule;
113
114    #[inline]
115    fn authenticate(&self, _req: &Request<'_>) -> Result<Guest, Denial> {
116        Ok(Guest)
117    }
118
119    fn authorize(&self, _who: &Guest, rule: &NoRule, _req: &Request<'_>) -> Result<(), Denial> {
120        match *rule {}
121    }
122}
123
124/// What the visitor is told when a gate denies.
125#[non_exhaustive]
126#[derive(Clone, Copy, Debug, PartialEq, Eq)]
127pub enum DenialKind {
128    /// Sign in first: a 303 to the site's sign-in page for a page request,
129    /// a 401 otherwise.
130    SignIn,
131    /// 403 Forbidden.
132    Forbidden,
133    /// 404 Not Found: the visitor is not told that the thing exists.
134    Hidden,
135    /// Another 4xx status (a 429 for a standing limit, for example).
136    Status(u16),
137}
138
139/// A refusal from a gate: what the visitor sees, and a stable code for logs
140/// and events (never shown to the visitor).
141#[derive(Clone, Copy, PartialEq, Eq)]
142pub struct Denial {
143    kind: DenialKind,
144    code: &'static str,
145}
146
147impl Denial {
148    /// Sign in first.
149    #[must_use]
150    pub const fn sign_in(code: &'static str) -> Self {
151        Self {
152            kind: DenialKind::SignIn,
153            code,
154        }
155    }
156
157    /// 403 Forbidden.
158    #[must_use]
159    pub const fn forbidden(code: &'static str) -> Self {
160        Self {
161            kind: DenialKind::Forbidden,
162            code,
163        }
164    }
165
166    /// 404 Not Found, hiding that the resource exists.
167    #[must_use]
168    pub const fn hidden(code: &'static str) -> Self {
169        Self {
170            kind: DenialKind::Hidden,
171            code,
172        }
173    }
174
175    /// Another client-error status. A status outside 400 to 499 is answered
176    /// as 403: a gate cannot turn a denial into a success or a server error.
177    #[must_use]
178    pub const fn status(status: u16, code: &'static str) -> Self {
179        let kind = if matches!(status, 400..=499) {
180            DenialKind::Status(status)
181        } else {
182            DenialKind::Forbidden
183        };
184        Self { kind, code }
185    }
186
187    /// What the visitor is told.
188    #[must_use]
189    pub const fn kind(&self) -> DenialKind {
190        self.kind
191    }
192
193    /// The code for logs and events.
194    #[must_use]
195    pub const fn code(&self) -> &'static str {
196        self.code
197    }
198
199    /// The status answered when no sign-in page applies.
200    #[must_use]
201    pub const fn status_code(&self) -> u16 {
202        match self.kind {
203            DenialKind::SignIn => 401,
204            DenialKind::Forbidden => 403,
205            DenialKind::Hidden => 404,
206            DenialKind::Status(s) => s,
207        }
208    }
209}
210
211impl fmt::Debug for Denial {
212    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213        f.debug_struct("Denial")
214            .field("kind", &self.kind)
215            .field("code", &self.code)
216            .finish()
217    }
218}
219
220impl fmt::Display for Denial {
221    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222        write!(f, "denied ({}): {}", self.status_code(), self.code)
223    }
224}
225
226impl std::error::Error for Denial {}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    #[test]
233    fn test_denial_statuses() {
234        assert_eq!(Denial::sign_in("a").status_code(), 401);
235        assert_eq!(Denial::forbidden("a").status_code(), 403);
236        assert_eq!(Denial::hidden("a").status_code(), 404);
237        assert_eq!(Denial::status(429, "a").status_code(), 429);
238        assert_eq!(Denial::status(200, "a").status_code(), 403);
239        assert_eq!(Denial::status(500, "a").status_code(), 403);
240        assert_eq!(Denial::forbidden("x.y").code(), "x.y");
241    }
242}