1use std::sync::atomic::{AtomicBool, Ordering};
16
17use crate::context::Context;
18use crate::error::Error;
19use crate::http::{Request, Response};
20use crate::middleware::Next;
21
22#[derive(Debug, Clone)]
23pub struct Identity {
24 pub user_id: String,
25 pub is_admin: bool,
26}
27
28static PRODUCTION_WARNED: AtomicBool = AtomicBool::new(false);
31
32pub fn in_production() -> bool {
37 std::env::var("RUSTIO_ENV")
38 .map(|v| {
39 let v = v.to_ascii_lowercase();
40 v == "production" || v == "prod"
41 })
42 .unwrap_or(false)
43}
44
45pub async fn authenticate(mut req: Request, next: Next) -> Result<Response, Error> {
46 if in_production() {
47 if !PRODUCTION_WARNED.swap(true, Ordering::Relaxed) {
51 eprintln!(
52 "rustio_core::auth: RUSTIO_ENV={} — built-in dev tokens are disabled. \
53 Replace `authenticate` with your own middleware before accepting traffic.",
54 std::env::var("RUSTIO_ENV").unwrap_or_default()
55 );
56 }
57 return next.run(req).await;
60 }
61
62 let token = bearer_token(&req)
67 .map(str::to_owned)
68 .or_else(|| req.cookie("rustio_token"));
69
70 if let Some(t) = token {
71 if let Some(identity) = dev_identity(&t) {
72 req.ctx_mut().insert(identity);
73 }
74 }
75 next.run(req).await
76}
77
78pub fn bearer_token(req: &Request) -> Option<&str> {
79 req.headers()
80 .get("authorization")
81 .and_then(|v| v.to_str().ok())
82 .and_then(|s| s.strip_prefix("Bearer "))
83}
84
85pub(crate) fn dev_identity(token: &str) -> Option<Identity> {
86 match token {
87 "dev-admin" => Some(Identity {
88 user_id: String::from("admin"),
89 is_admin: true,
90 }),
91 "dev-user" => Some(Identity {
92 user_id: String::from("user"),
93 is_admin: false,
94 }),
95 _ => None,
96 }
97}
98
99pub fn identity(ctx: &Context) -> Option<&Identity> {
100 ctx.get::<Identity>()
101}
102
103pub fn require_auth(ctx: &Context) -> Result<&Identity, Error> {
104 identity(ctx).ok_or(Error::Unauthorized)
105}
106
107pub fn require_admin(ctx: &Context) -> Result<&Identity, Error> {
108 let id = require_auth(ctx)?;
109 if !id.is_admin {
110 return Err(Error::Forbidden);
111 }
112 Ok(id)
113}
114
115#[cfg(test)]
116mod tests {
117 use super::*;
118
119 fn user(is_admin: bool) -> Identity {
120 Identity {
121 user_id: String::from(if is_admin { "admin" } else { "user" }),
122 is_admin,
123 }
124 }
125
126 #[test]
127 fn identity_returns_none_when_absent() {
128 let ctx = Context::new();
129 assert!(identity(&ctx).is_none());
130 }
131
132 #[test]
133 fn identity_returns_reference_when_attached() {
134 let mut ctx = Context::new();
135 ctx.insert(user(false));
136 assert_eq!(identity(&ctx).map(|i| i.user_id.as_str()), Some("user"));
137 }
138
139 #[test]
140 fn require_auth_missing_returns_unauthorized() {
141 let ctx = Context::new();
142 assert!(matches!(require_auth(&ctx), Err(Error::Unauthorized)));
143 }
144
145 #[test]
146 fn require_auth_present_returns_identity() {
147 let mut ctx = Context::new();
148 ctx.insert(user(false));
149 let id = require_auth(&ctx).unwrap();
150 assert_eq!(id.user_id, "user");
151 assert!(!id.is_admin);
152 }
153
154 #[test]
155 fn require_admin_without_identity_returns_unauthorized() {
156 let ctx = Context::new();
157 assert!(matches!(require_admin(&ctx), Err(Error::Unauthorized)));
158 }
159
160 #[test]
161 fn require_admin_with_non_admin_returns_forbidden() {
162 let mut ctx = Context::new();
163 ctx.insert(user(false));
164 assert!(matches!(require_admin(&ctx), Err(Error::Forbidden)));
165 }
166
167 #[test]
168 fn require_admin_with_admin_returns_identity() {
169 let mut ctx = Context::new();
170 ctx.insert(user(true));
171 let id = require_admin(&ctx).unwrap();
172 assert_eq!(id.user_id, "admin");
173 assert!(id.is_admin);
174 }
175
176 #[test]
177 fn dev_identity_rejects_unknown_tokens() {
178 assert!(dev_identity("garbage").is_none());
179 assert!(dev_identity("").is_none());
180 }
181
182 #[test]
183 fn dev_identity_maps_known_tokens() {
184 let admin = dev_identity("dev-admin").unwrap();
185 assert!(admin.is_admin);
186 let user = dev_identity("dev-user").unwrap();
187 assert!(!user.is_admin);
188 }
189
190 #[test]
191 fn in_production_detects_known_values() {
192 fn detect(v: Option<&str>) -> bool {
195 v.map(|s| {
196 let s = s.to_ascii_lowercase();
197 s == "production" || s == "prod"
198 })
199 .unwrap_or(false)
200 }
201 assert!(detect(Some("production")));
202 assert!(detect(Some("PRODUCTION")));
203 assert!(detect(Some("prod")));
204 assert!(detect(Some("Prod")));
205 assert!(!detect(Some("dev")));
206 assert!(!detect(Some("staging")));
207 assert!(!detect(Some("")));
208 assert!(!detect(None));
209 }
210}