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 [`Context`](crate::Context), 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.
14///
15/// ```
16/// use web_net::Identity;
17///
18/// struct User { session: Option<Vec<u8>> }
19/// impl Identity for User {
20///     fn is_guest(&self) -> bool { self.session.is_none() }
21///     fn csrf_binding(&self) -> Option<&[u8]> { self.session.as_deref() }
22/// }
23///
24/// let ann = User { session: Some(b"s-41".to_vec()) };
25/// assert!(!ann.is_guest());
26/// assert_eq!(ann.csrf_binding(), Some(&b"s-41"[..]));
27/// assert_eq!(ann.set_cookie(), None); // the default
28/// ```
29pub trait Identity: Send + Sync + 'static {
30    /// Whether this is a visitor with no signed-in session.
31    fn is_guest(&self) -> bool;
32
33    /// The value CSRF tokens are bound to: the session id, or a pre-session
34    /// id for a guest. `None` means no binding exists, and an unsafe method
35    /// on a CSRF-protected route is refused.
36    fn csrf_binding(&self) -> Option<&[u8]> {
37        None
38    }
39
40    /// A `Set-Cookie` value the gate wants on the reply (a dead session's
41    /// cookie cleared, a pre-session id set). [`App::respond`](crate::App::respond)
42    /// writes it on every exit after authentication, once; when the caller
43    /// passes an identity to `respond`, it authenticated and writes its own. A reply that carries it is never
44    /// reported as keepable by a cache.
45    fn set_cookie(&self) -> Option<&str> {
46        None
47    }
48}
49
50/// The identity of [`Open`]: everyone is a guest.
51///
52/// ```
53/// use web_net::{Guest, Identity};
54///
55/// assert!(Guest.is_guest());
56/// assert_eq!(Guest.csrf_binding(), None);
57/// ```
58#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
59pub struct Guest;
60
61impl Identity for Guest {
62    #[inline]
63    fn is_guest(&self) -> bool {
64        true
65    }
66}
67
68/// Authentication and authorization, the two checkpoints a request passes
69/// before a handler runs.
70///
71/// One trait, so an application has one type parameter. An implementation
72/// backed by remote state fails closed: when it cannot decide, it denies.
73///
74/// ```
75/// use web_net::{Denial, Gate, Identity, Request};
76///
77/// struct User { admin: bool }
78/// impl Identity for User {
79///     fn is_guest(&self) -> bool { false }
80/// }
81///
82/// enum Rule { Admin }
83///
84/// struct Demo;
85/// impl Gate for Demo {
86///     type Identity = User;
87///     type Rule = Rule;
88///     fn authenticate(&self, req: &Request<'_>) -> Result<User, Denial> {
89///         Ok(User { admin: req.header("x-demo-admin").is_some() })
90///     }
91///     fn authorize(&self, who: &User, rule: &Rule, _req: &Request<'_>) -> Result<(), Denial> {
92///         match rule {
93///             Rule::Admin if who.admin => Ok(()),
94///             Rule::Admin => Err(Denial::forbidden("demo.not_admin")),
95///         }
96///     }
97/// }
98/// ```
99pub trait Gate: Send + Sync + 'static {
100    /// Who a request comes from.
101    type Identity: Identity;
102
103    /// A route's access rule, declared with [`Route::allow`](crate::Route::allow).
104    type Rule: Send + Sync + 'static;
105
106    /// Runs once per request, before routing. A missing or invalid
107    /// credential is a guest (`Ok`); a banned visitor is a denial.
108    ///
109    /// Not called when the caller passes an identity to
110    /// [`App::respond`](crate::App::respond): it authenticated already.
111    ///
112    /// # Errors
113    ///
114    /// A [`Denial`] ends the request with its status.
115    fn authenticate(&self, req: &Request<'_>) -> Result<Self::Identity, Denial>;
116
117    /// Runs after the route and method matched and before anything else of
118    /// the route. Not called for routes declared public.
119    ///
120    /// # Errors
121    ///
122    /// A [`Denial`] ends the request with its status.
123    fn authorize(
124        &self,
125        who: &Self::Identity,
126        rule: &Self::Rule,
127        req: &Request<'_>,
128    ) -> Result<(), Denial>;
129}
130
131/// A rule that cannot be made: the rule type of [`Open`], whose routes can
132/// only be public.
133///
134/// ```
135/// use web_net::{Gate, NoRule, Open};
136///
137/// // No value of `NoRule` exists, so no route of an `Open` app can carry a rule.
138/// fn rule_type<G: Gate<Rule = NoRule>>(_: &G) {}
139/// rule_type(&Open);
140/// ```
141#[derive(Clone, Copy, Debug, PartialEq, Eq)]
142pub enum NoRule {}
143
144/// The default gate: every visitor is a [`Guest`], and only routes declared
145/// [`public`](crate::Route::public) can exist.
146///
147/// ```
148/// use web_net::{App, Open, get};
149///
150/// let app = App::with_gate(Open).route(get("/", |cx| cx.text(200, "hi"))).build()?;
151/// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
152/// assert_eq!(r.text(), "hi");
153/// # Ok::<(), web_net::BuildError>(())
154/// ```
155#[derive(Clone, Copy, Debug, Default)]
156pub struct Open;
157
158impl Gate for Open {
159    type Identity = Guest;
160    type Rule = NoRule;
161
162    #[inline]
163    fn authenticate(&self, _req: &Request<'_>) -> Result<Guest, Denial> {
164        Ok(Guest)
165    }
166
167    fn authorize(&self, _who: &Guest, rule: &NoRule, _req: &Request<'_>) -> Result<(), Denial> {
168        match *rule {}
169    }
170}
171
172/// What the visitor is told when a gate denies.
173///
174/// ```
175/// use web_net::{Denial, DenialKind};
176///
177/// assert_eq!(Denial::hidden("post.draft").kind(), DenialKind::Hidden);
178/// assert_eq!(Denial::with_status(429, "rate.limited").kind(), DenialKind::Status(429));
179/// ```
180#[non_exhaustive]
181#[derive(Clone, Copy, Debug, PartialEq, Eq)]
182pub enum DenialKind {
183    /// Sign in first: a 303 to the site's sign-in page for a page request,
184    /// a 401 otherwise.
185    SignIn,
186    /// 403 Forbidden.
187    Forbidden,
188    /// 404 Not Found: the visitor is not told that the thing exists.
189    Hidden,
190    /// Another 4xx status (a 429 for a standing limit, for example).
191    Status(u16),
192    /// 503 Service Unavailable: the gate cannot decide now (its stores are
193    /// down) and fails closed; the client may retry.
194    Unavailable,
195}
196
197/// A refusal from a gate: what the visitor sees, and a stable code for logs
198/// and events (never shown to the visitor).
199///
200/// ```
201/// use web_net::Denial;
202///
203/// let d = Denial::forbidden("admin.only");
204/// assert_eq!((d.status(), d.code()), (403, "admin.only"));
205/// assert_eq!(d.to_string(), "denied (403): admin.only");
206/// ```
207#[derive(Clone, PartialEq, Eq)]
208pub struct Denial {
209    kind: DenialKind,
210    code: &'static str,
211    cookie: Option<Box<str>>,
212    retry_after: Option<u32>,
213}
214
215impl Denial {
216    /// Sign in first.
217    ///
218    /// ```
219    /// use web_net::{Denial, DenialKind};
220    ///
221    /// let d = Denial::sign_in("session.none");
222    /// assert_eq!((d.kind(), d.status()), (DenialKind::SignIn, 401));
223    /// ```
224    #[must_use]
225    pub const fn sign_in(code: &'static str) -> Self {
226        Self {
227            kind: DenialKind::SignIn,
228            code,
229            cookie: None,
230            retry_after: None,
231        }
232    }
233
234    /// 403 Forbidden.
235    ///
236    /// ```
237    /// use web_net::Denial;
238    ///
239    /// assert_eq!(Denial::forbidden("account.locked").status(), 403);
240    /// ```
241    #[must_use]
242    pub const fn forbidden(code: &'static str) -> Self {
243        Self {
244            kind: DenialKind::Forbidden,
245            code,
246            cookie: None,
247            retry_after: None,
248        }
249    }
250
251    /// 404 Not Found, hiding that the resource exists.
252    ///
253    /// ```
254    /// use web_net::Denial;
255    ///
256    /// assert_eq!(Denial::hidden("post.draft").status(), 404);
257    /// ```
258    #[must_use]
259    pub const fn hidden(code: &'static str) -> Self {
260        Self {
261            kind: DenialKind::Hidden,
262            code,
263            cookie: None,
264            retry_after: None,
265        }
266    }
267
268    /// Another client-error status (429, 451, ...). A status outside 400
269    /// to 499 is answered as 403: a gate cannot turn a denial into a success
270    /// or a server error.
271    ///
272    /// ```
273    /// use web_net::Denial;
274    ///
275    /// assert_eq!(Denial::with_status(429, "rate.limited").status(), 429);
276    /// assert_eq!(Denial::with_status(500, "oops").status(), 403);
277    /// ```
278    #[must_use]
279    pub const fn with_status(status: u16, code: &'static str) -> Self {
280        let kind = if matches!(status, 400..=499) {
281            DenialKind::Status(status)
282        } else {
283            DenialKind::Forbidden
284        };
285        Self {
286            kind,
287            code,
288            cookie: None,
289            retry_after: None,
290        }
291    }
292
293    /// 503 Service Unavailable: the gate cannot decide now (a store it
294    /// needs is down) and fails closed. Clients and load balancers read it
295    /// as "try again", not "you may not".
296    ///
297    /// ```
298    /// use web_net::Denial;
299    ///
300    /// let d = Denial::unavailable("gate.unavailable").retry_after(5);
301    /// assert_eq!((d.status(), d.retry_seconds()), (503, Some(5)));
302    /// ```
303    #[must_use]
304    pub const fn unavailable(code: &'static str) -> Self {
305        Self {
306            kind: DenialKind::Unavailable,
307            code,
308            cookie: None,
309            retry_after: None,
310        }
311    }
312
313    /// A `Retry-After: <seconds>` field on the refusal. Sent only when the
314    /// status is 429 or 503, the statuses a client retries.
315    ///
316    /// ```
317    /// use web_net::Denial;
318    ///
319    /// let d = Denial::with_status(429, "rate.limited").retry_after(30);
320    /// assert_eq!(d.retry_seconds(), Some(30));
321    /// ```
322    #[must_use]
323    pub const fn retry_after(mut self, seconds: u32) -> Self {
324        self.retry_after = Some(seconds);
325        self
326    }
327
328    /// The `Retry-After` seconds, if any.
329    ///
330    /// ```
331    /// use web_net::Denial;
332    ///
333    /// assert_eq!(Denial::forbidden("account.locked").retry_seconds(), None);
334    /// assert_eq!(Denial::unavailable("store.down").retry_after(5).retry_seconds(), Some(5));
335    /// ```
336    #[must_use]
337    pub const fn retry_seconds(&self) -> Option<u32> {
338        self.retry_after
339    }
340
341    /// A `Set-Cookie` value the refusal carries (a dead session's cookie
342    /// cleared): written on the denial's reply.
343    ///
344    /// ```
345    /// use web_net::Denial;
346    ///
347    /// let d = Denial::sign_in("session.expired").with_cookie("sid=; Max-Age=0; Path=/");
348    /// assert_eq!(d.cookie(), Some("sid=; Max-Age=0; Path=/"));
349    /// ```
350    #[must_use]
351    pub fn with_cookie(mut self, set_cookie: impl Into<Box<str>>) -> Self {
352        self.cookie = Some(set_cookie.into());
353        self
354    }
355
356    /// The `Set-Cookie` value, if any.
357    ///
358    /// ```
359    /// use web_net::Denial;
360    ///
361    /// assert_eq!(Denial::forbidden("account.locked").cookie(), None);
362    /// assert!(Denial::sign_in("session.expired").with_cookie("sid=; Max-Age=0").cookie().is_some());
363    /// ```
364    #[must_use]
365    pub fn cookie(&self) -> Option<&str> {
366        self.cookie.as_deref()
367    }
368
369    /// What the visitor is told.
370    ///
371    /// ```
372    /// use web_net::{Denial, DenialKind};
373    ///
374    /// assert_eq!(Denial::unavailable("store.down").kind(), DenialKind::Unavailable);
375    /// ```
376    #[must_use]
377    pub const fn kind(&self) -> DenialKind {
378        self.kind
379    }
380
381    /// The code for logs and events.
382    ///
383    /// ```
384    /// use web_net::Denial;
385    ///
386    /// assert_eq!(Denial::hidden("post.draft").code(), "post.draft");
387    /// ```
388    #[must_use]
389    pub const fn code(&self) -> &'static str {
390        self.code
391    }
392
393    /// The status answered when no sign-in page applies.
394    ///
395    /// ```
396    /// use web_net::Denial;
397    ///
398    /// assert_eq!(Denial::sign_in("session.none").status(), 401);
399    /// assert_eq!(Denial::unavailable("store.down").status(), 503);
400    /// ```
401    #[must_use]
402    pub const fn status(&self) -> u16 {
403        match self.kind {
404            DenialKind::SignIn => 401,
405            DenialKind::Forbidden => 403,
406            DenialKind::Hidden => 404,
407            DenialKind::Status(s) => s,
408            DenialKind::Unavailable => 503,
409        }
410    }
411}
412
413impl fmt::Debug for Denial {
414    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
415        f.debug_struct("Denial")
416            .field("kind", &self.kind)
417            .field("code", &self.code)
418            .field("cookie", &self.cookie.as_ref().map(|_| "<set>"))
419            .field("retry_after", &self.retry_after)
420            .finish()
421    }
422}
423
424impl fmt::Display for Denial {
425    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
426        write!(f, "denied ({}): {}", self.status(), self.code)
427    }
428}
429
430impl std::error::Error for Denial {}
431
432#[cfg(test)]
433mod tests {
434    use super::*;
435
436    #[test]
437    fn test_denial_statuses() {
438        assert_eq!(Denial::sign_in("a").status(), 401);
439        assert_eq!(Denial::forbidden("a").status(), 403);
440        assert_eq!(Denial::hidden("a").status(), 404);
441        assert_eq!(Denial::with_status(429, "a").status(), 429);
442        assert_eq!(Denial::with_status(200, "a").status(), 403);
443        assert_eq!(Denial::with_status(500, "a").status(), 403);
444        assert_eq!(Denial::forbidden("x.y").code(), "x.y");
445        assert_eq!(Denial::unavailable("a").status(), 503);
446        assert_eq!(Denial::unavailable("a").kind(), DenialKind::Unavailable);
447        assert_eq!(Denial::unavailable("a").retry_seconds(), None);
448        assert_eq!(
449            Denial::forbidden("a").retry_after(3).retry_seconds(),
450            Some(3)
451        );
452    }
453}