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}