1use crate::context::Context;
2use crate::error::Error;
3use crate::http::{Request, Response};
4use crate::middleware::Next;
5
6#[derive(Debug, Clone)]
7pub struct Identity {
8 pub user_id: String,
9 pub is_admin: bool,
10}
11
12pub async fn authenticate(mut req: Request, next: Next) -> Result<Response, Error> {
13 if let Some(token) = bearer_token(&req) {
14 if let Some(identity) = dev_identity(token) {
15 req.ctx_mut().insert(identity);
16 }
17 }
18 next.run(req).await
19}
20
21pub fn bearer_token(req: &Request) -> Option<&str> {
22 req.headers()
23 .get("authorization")
24 .and_then(|v| v.to_str().ok())
25 .and_then(|s| s.strip_prefix("Bearer "))
26}
27
28fn dev_identity(token: &str) -> Option<Identity> {
29 match token {
30 "dev-admin" => Some(Identity {
31 user_id: String::from("admin"),
32 is_admin: true,
33 }),
34 "dev-user" => Some(Identity {
35 user_id: String::from("user"),
36 is_admin: false,
37 }),
38 _ => None,
39 }
40}
41
42pub fn identity(ctx: &Context) -> Option<&Identity> {
43 ctx.get::<Identity>()
44}
45
46pub fn require_auth(ctx: &Context) -> Result<&Identity, Error> {
47 identity(ctx).ok_or(Error::Unauthorized)
48}
49
50pub fn require_admin(ctx: &Context) -> Result<&Identity, Error> {
51 let id = require_auth(ctx)?;
52 if !id.is_admin {
53 return Err(Error::Forbidden);
54 }
55 Ok(id)
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61
62 fn user(is_admin: bool) -> Identity {
63 Identity {
64 user_id: String::from(if is_admin { "admin" } else { "user" }),
65 is_admin,
66 }
67 }
68
69 #[test]
70 fn identity_returns_none_when_absent() {
71 let ctx = Context::new();
72 assert!(identity(&ctx).is_none());
73 }
74
75 #[test]
76 fn identity_returns_reference_when_attached() {
77 let mut ctx = Context::new();
78 ctx.insert(user(false));
79 assert_eq!(identity(&ctx).map(|i| i.user_id.as_str()), Some("user"));
80 }
81
82 #[test]
83 fn require_auth_missing_returns_unauthorized() {
84 let ctx = Context::new();
85 assert!(matches!(require_auth(&ctx), Err(Error::Unauthorized)));
86 }
87
88 #[test]
89 fn require_auth_present_returns_identity() {
90 let mut ctx = Context::new();
91 ctx.insert(user(false));
92 let id = require_auth(&ctx).unwrap();
93 assert_eq!(id.user_id, "user");
94 assert!(!id.is_admin);
95 }
96
97 #[test]
98 fn require_admin_without_identity_returns_unauthorized() {
99 let ctx = Context::new();
100 assert!(matches!(require_admin(&ctx), Err(Error::Unauthorized)));
101 }
102
103 #[test]
104 fn require_admin_with_non_admin_returns_forbidden() {
105 let mut ctx = Context::new();
106 ctx.insert(user(false));
107 assert!(matches!(require_admin(&ctx), Err(Error::Forbidden)));
108 }
109
110 #[test]
111 fn require_admin_with_admin_returns_identity() {
112 let mut ctx = Context::new();
113 ctx.insert(user(true));
114 let id = require_admin(&ctx).unwrap();
115 assert_eq!(id.user_id, "admin");
116 assert!(id.is_admin);
117 }
118
119 #[test]
120 fn dev_identity_rejects_unknown_tokens() {
121 assert!(dev_identity("garbage").is_none());
122 assert!(dev_identity("").is_none());
123 }
124
125 #[test]
126 fn dev_identity_maps_known_tokens() {
127 let admin = dev_identity("dev-admin").unwrap();
128 assert!(admin.is_admin);
129 let user = dev_identity("dev-user").unwrap();
130 assert!(!user.is_admin);
131 }
132}