Skip to main content

rustio_core/
auth.rs

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}