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