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}