Skip to main content

modkit_auth/
authorizer.rs

1use crate::{claims::Claims, errors::AuthError, traits::PrimaryAuthorizer, types::SecRequirement};
2use async_trait::async_trait;
3
4/// Role-based authorizer that checks resource:action patterns
5#[derive(Debug, Clone, Default)]
6pub struct RoleAuthorizer;
7
8impl RoleAuthorizer {
9    /// Check if any permission matches the requirement pattern
10    fn check_permission(claims: &Claims, requirement: &SecRequirement) -> bool {
11        claims.permissions.iter().any(|perm| {
12            let resource = perm.resource_pattern();
13            let action = perm.action();
14
15            // Exact match
16            if resource == requirement.resource && action == requirement.action {
17                return true;
18            }
19
20            // Resource wildcard: "users:*"
21            if resource == requirement.resource && action == "*" {
22                return true;
23            }
24
25            // Action wildcard: "*:read"
26            if resource == "*" && action == requirement.action {
27                return true;
28            }
29
30            // Full wildcard: "*:*"
31            if resource == "*" && action == "*" {
32                return true;
33            }
34
35            false
36        })
37    }
38}
39
40#[async_trait]
41impl PrimaryAuthorizer for RoleAuthorizer {
42    async fn check(&self, claims: &Claims, requirement: &SecRequirement) -> Result<(), AuthError> {
43        if Self::check_permission(claims, requirement) {
44            Ok(())
45        } else {
46            Err(AuthError::Forbidden)
47        }
48    }
49}
50
51#[cfg(test)]
52#[cfg_attr(coverage_nightly, coverage(off))]
53mod tests {
54    use super::*;
55    use crate::claims::Permission;
56    use uuid::Uuid;
57
58    fn mock_claims(permissions: Vec<Permission>) -> Claims {
59        Claims {
60            issuer: "test".to_owned(),
61            subject: Uuid::new_v4(),
62            audiences: vec![],
63            expires_at: None,
64            not_before: None,
65            issued_at: None,
66            jwt_id: None,
67            tenant_id: Uuid::new_v4(),
68            permissions,
69            extras: serde_json::Map::new(),
70        }
71    }
72
73    #[tokio::test]
74    async fn test_exact_role_match() {
75        let auth = RoleAuthorizer;
76        let perm = Permission::builder()
77            .resource_pattern("users")
78            .action("read")
79            .build()
80            .unwrap();
81        let claims = mock_claims(vec![perm]);
82        let req = SecRequirement::new("users", "read");
83
84        assert!(auth.check(&claims, &req).await.is_ok());
85    }
86
87    #[tokio::test]
88    async fn test_resource_wildcard() {
89        let auth = RoleAuthorizer;
90        let perm = Permission::builder()
91            .resource_pattern("users")
92            .action("*")
93            .build()
94            .unwrap();
95        let claims = mock_claims(vec![perm]);
96        let req = SecRequirement::new("users", "write");
97
98        assert!(auth.check(&claims, &req).await.is_ok());
99    }
100
101    #[tokio::test]
102    async fn test_action_wildcard() {
103        let auth = RoleAuthorizer;
104        let perm = Permission::builder()
105            .resource_pattern("*")
106            .action("read")
107            .build()
108            .unwrap();
109        let claims = mock_claims(vec![perm]);
110        let req = SecRequirement::new("posts", "read");
111
112        assert!(auth.check(&claims, &req).await.is_ok());
113    }
114
115    #[tokio::test]
116    async fn test_full_wildcard() {
117        let auth = RoleAuthorizer;
118        let perm = Permission::builder()
119            .resource_pattern("*")
120            .action("*")
121            .build()
122            .unwrap();
123        let claims = mock_claims(vec![perm]);
124        let req = SecRequirement::new("anything", "everything");
125
126        assert!(auth.check(&claims, &req).await.is_ok());
127    }
128
129    #[tokio::test]
130    async fn test_no_matching_role() {
131        let auth = RoleAuthorizer;
132        let perm = Permission::builder()
133            .resource_pattern("posts")
134            .action("read")
135            .build()
136            .unwrap();
137        let claims = mock_claims(vec![perm]);
138        let req = SecRequirement::new("users", "read");
139
140        assert!(matches!(
141            auth.check(&claims, &req).await,
142            Err(AuthError::Forbidden)
143        ));
144    }
145}