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}