Skip to main content

axum_gate/authz/
authorization_service.rs

1use super::{AccessHierarchy, AccessPolicy};
2use crate::accounts::Account;
3
4use tracing::debug;
5
6/// Domain service for authorization decisions.
7///
8/// This service contains pure business logic for determining whether an account
9/// is authorized based on an access policy. It has no external dependencies
10/// and can be used across different application contexts.
11#[derive(Debug, Clone)]
12pub struct AuthorizationService<R, G>
13where
14    R: AccessHierarchy + Eq + std::fmt::Display,
15    G: Eq + Clone,
16{
17    policy: AccessPolicy<R, G>,
18}
19
20impl<R, G> AuthorizationService<R, G>
21where
22    R: AccessHierarchy + Eq + std::fmt::Display,
23    G: Eq + Clone,
24{
25    /// Creates a new authorization service with the given access policy.
26    pub fn new(policy: AccessPolicy<R, G>) -> Self {
27        Self { policy }
28    }
29
30    /// Determines if the account is authorized based on any of the configured criteria.
31    ///
32    /// Returns `true` if the account matches ANY of:
33    /// - Required permissions
34    /// - Required groups
35    /// - Required exact roles
36    /// - Required roles via supervisor relationship (ordering: higher privilege > lower privilege)
37    pub fn is_authorized(&self, account: &Account<R, G>) -> bool {
38        self.meets_permission_requirement(account)
39            || self.meets_group_requirement(account)
40            || self.meets_role_requirement(account)
41            || self.meets_role_hierarchy_requirement(account)
42    }
43
44    /// Checks if the account meets any of the required roles.
45    pub fn meets_role_requirement(&self, account: &Account<R, G>) -> bool {
46        account.roles.iter().any(|r| {
47            self.policy
48                .role_requirements()
49                .iter()
50                .any(|scope| scope.grants_role(r))
51        })
52    }
53
54    /// Checks if the account has a role that satisfies a required role via hierarchy ordering.
55    ///
56    /// Ordering contract: Higher privilege > Lower privilege.
57    /// A supervisor or the same role satisfies: user_role >= required_role.
58    pub fn meets_role_hierarchy_requirement(&self, account: &Account<R, G>) -> bool {
59        debug!("Checking role hierarchy (same-or-supervisor) against required roles.");
60        account.roles.iter().any(|user_role| {
61            self.policy
62                .role_requirements()
63                .iter()
64                .any(|scope| scope.grants_supervisor(user_role))
65        })
66    }
67
68    /// Checks if the account meets any of the required groups.
69    pub fn meets_group_requirement(&self, account: &Account<R, G>) -> bool {
70        account.groups.iter().any(|r| {
71            self.policy
72                .group_requirements()
73                .iter()
74                .any(|g_scope| g_scope.eq(r))
75        })
76    }
77
78    /// Checks if the account meets any of the required permissions.
79    pub fn meets_permission_requirement(&self, account: &Account<R, G>) -> bool {
80        // Check if any of the account's permissions match any of the required permissions
81        let account_permissions: std::collections::HashSet<u64> =
82            account.permissions.iter().collect();
83        let required_permissions: std::collections::HashSet<u64> =
84            self.policy.permission_requirements().iter().collect();
85
86        !account_permissions.is_disjoint(&required_permissions)
87    }
88
89    /// Returns true if the policy denies all access (no requirements configured).
90    pub fn policy_denies_all_access(&self) -> bool {
91        self.policy.denies_all()
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    use crate::groups::Group;
100    use crate::roles::Role;
101
102    fn create_test_account() -> Account<Role, Group> {
103        use crate::permissions::Permissions;
104        use uuid::Uuid;
105
106        let mut permissions = Permissions::new();
107        // Insert raw 64-bit permission IDs for testing
108        permissions.bitmap_mut().insert(1u64);
109        permissions.bitmap_mut().insert(5u64);
110
111        Account {
112            account_id: Uuid::new_v4(),
113            user_id: "test_user".to_string(),
114            roles: vec![Role::Admin],
115            groups: vec![Group::new("engineering")],
116            permissions,
117        }
118    }
119
120    #[test]
121    fn authorization_service_empty_criteria() {
122        let service: AuthorizationService<Role, Group> =
123            AuthorizationService::new(AccessPolicy::deny_all());
124        assert!(service.policy_denies_all_access());
125    }
126
127    #[test]
128    fn authorization_service_non_empty_criteria() {
129        let policy = AccessPolicy::require_role(Role::Admin);
130        let service: AuthorizationService<Role, Group> = AuthorizationService::new(policy);
131        assert!(!service.policy_denies_all_access());
132    }
133
134    #[test]
135    fn authorized_by_role_matching() {
136        let account = create_test_account();
137        let policy = AccessPolicy::require_role(Role::Admin);
138        let service = AuthorizationService::new(policy);
139
140        assert!(service.meets_role_requirement(&account));
141    }
142
143    #[test]
144    fn authorized_by_role_not_matching() {
145        let account = create_test_account();
146        let policy = AccessPolicy::require_role(Role::User);
147        let service = AuthorizationService::new(policy);
148
149        assert!(!service.meets_role_requirement(&account));
150    }
151
152    #[test]
153    fn authorized_by_group_matching() {
154        let account = create_test_account();
155        let policy = AccessPolicy::require_group(Group::new("engineering"));
156        let service = AuthorizationService::new(policy);
157
158        assert!(service.meets_group_requirement(&account));
159    }
160
161    #[test]
162    fn authorized_by_group_not_matching() {
163        let account = create_test_account();
164        let policy = AccessPolicy::require_group(Group::new("sales"));
165        let service = AuthorizationService::new(policy);
166
167        assert!(!service.meets_group_requirement(&account));
168    }
169
170    #[test]
171    fn authorized_by_permission_matching() {
172        let account = create_test_account();
173        let policy =
174            AccessPolicy::require_permission(crate::permissions::PermissionId::from_u64(1)); // Account has permission id 1 (64-bit)
175        let service = AuthorizationService::new(policy);
176
177        assert!(service.meets_permission_requirement(&account));
178    }
179
180    #[test]
181    fn authorized_by_permission_not_matching() {
182        let account = create_test_account();
183        let policy =
184            AccessPolicy::require_permission(crate::permissions::PermissionId::from_u64(10)); // Account doesn't have permission id 10
185        let service = AuthorizationService::new(policy);
186
187        assert!(!service.meets_permission_requirement(&account));
188    }
189
190    #[test]
191    fn is_authorized_returns_true_when_any_criteria_match() {
192        let account = create_test_account();
193        let policy = AccessPolicy::require_role(Role::User) // Won't match
194            .or_require_group(Group::new("engineering")); // Will match
195        let service = AuthorizationService::new(policy);
196
197        assert!(service.is_authorized(&account));
198    }
199
200    #[test]
201    fn is_authorized_returns_false_when_no_criteria_match() {
202        let account = create_test_account();
203        let policy = AccessPolicy::require_role(Role::User) // Won't match
204            .or_require_group(Group::new("sales")); // Won't match
205        let service = AuthorizationService::new(policy);
206
207        assert!(!service.is_authorized(&account));
208    }
209}