Skip to main content

rustio_core/
auth.rs

1//! Identity-in-context authentication.
2//!
3//! [`authenticate`] is an additive middleware: it attaches an [`Identity`]
4//! to the request context when a valid `Authorization: Bearer` token is
5//! provided, and does nothing otherwise. Handlers enforce their own
6//! requirement with [`require_auth`] / [`require_admin`].
7//!
8//! The built-in token mapping is for development only — replace it with
9//! your own middleware before deploying.
10
11use 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}