axum_gate/authz/
access_policy.rs

1//! Access policy configuration for route protection
2//!
3//! This module defines domain objects for configuring access requirements
4//! to protected resources. Access policies are pure business logic that
5//! specify what roles, groups, or permissions are required for access.
6
7use super::{AccessHierarchy, AccessScope};
8use crate::permissions::{PermissionId, Permissions};
9
10/// Domain object representing access requirements for a protected resource.
11///
12/// This captures the business rules about what roles, groups, or permissions
13/// are required to access a particular resource or route. Access is granted
14/// if the user meets ANY of the specified requirements (OR logic).
15#[derive(Debug, Clone)]
16pub struct AccessPolicy<R, G>
17where
18    R: AccessHierarchy + Eq + std::fmt::Display,
19    G: Eq,
20{
21    role_requirements: Vec<AccessScope<R>>,
22    group_requirements: Vec<G>,
23    permission_requirements: Permissions,
24}
25
26impl<R, G> AccessPolicy<R, G>
27where
28    R: AccessHierarchy + Eq + std::fmt::Display,
29    G: Eq,
30{
31    /// Creates a new access policy with no requirements (denies all access).
32    ///
33    /// This is the secure default - no access is granted unless explicitly
34    /// configured through the builder methods.
35    pub fn deny_all() -> Self {
36        Self {
37            role_requirements: vec![],
38            group_requirements: vec![],
39            permission_requirements: Permissions::new(),
40        }
41    }
42
43    /// Creates a policy that allows access for users with the specified role.
44    ///
45    /// Use this when you need exact role matching without hierarchy. For scenarios
46    /// where supervisor roles should also have access, use `require_role_or_supervisor()`.
47    ///
48    /// # Example
49    /// ```rust
50    /// use axum_gate::authz::AccessPolicy;
51    /// use axum_gate::prelude::{Role, Group};
52    ///
53    /// let policy: AccessPolicy<Role, Group> = AccessPolicy::require_role(Role::Admin);
54    /// ```
55    pub fn require_role(role: R) -> Self {
56        Self {
57            role_requirements: vec![AccessScope::new(role)],
58            group_requirements: vec![],
59            permission_requirements: Permissions::new(),
60        }
61    }
62
63    /// Creates a policy that allows access for users with the specified role or any supervisor role.
64    ///
65    /// Use this when you want hierarchical access control where higher-level roles
66    /// automatically inherit permissions from lower-level roles. This is ideal for
67    /// organizational structures where managers should have access to employee resources.
68    ///
69    /// This leverages the role hierarchy defined by the `AccessHierarchy` trait.
70    ///
71    /// # Example
72    /// ```rust
73    /// use axum_gate::authz::AccessPolicy;
74    /// use axum_gate::prelude::{Role, Group};
75    ///
76    /// // Allows Moderator role and Admin role (if Admin supervises Moderator)
77    /// let policy: AccessPolicy<Role, Group> = AccessPolicy::require_role_or_supervisor(Role::Moderator);
78    /// ```
79    pub fn require_role_or_supervisor(role: R) -> Self {
80        Self {
81            role_requirements: vec![AccessScope::new(role).allow_supervisor()],
82            group_requirements: vec![],
83            permission_requirements: Permissions::new(),
84        }
85    }
86
87    /// Creates a policy that allows access for users in the specified group.
88    ///
89    /// Use this for team-based or department-based access control. Groups are ideal
90    /// for cross-cutting concerns that don't fit hierarchical role structures,
91    /// such as project teams, geographical regions, or temporary access grants.
92    ///
93    /// # Example
94    /// ```rust
95    /// use axum_gate::authz::AccessPolicy;
96    /// use axum_gate::prelude::{Role, Group};
97    ///
98    /// let policy = AccessPolicy::<Role, Group>::require_group(Group::new("engineering"));
99    /// ```
100    pub fn require_group(group: G) -> Self {
101        Self {
102            role_requirements: vec![],
103            group_requirements: vec![group],
104            permission_requirements: Permissions::new(),
105        }
106    }
107
108    /// Creates a policy that allows access for users with the specified permission.
109    ///
110    /// # Example
111    /// ```rust
112    /// use axum_gate::authz::AccessPolicy;
113    /// use axum_gate::permissions::PermissionId;
114    /// use axum_gate::prelude::{Role, Group};
115    ///
116    /// // Using a permission name (hashed deterministically to 64-bit ID)
117    /// let policy: AccessPolicy<Role, Group> =
118    ///     AccessPolicy::require_permission(PermissionId::from("read:api"));
119    ///
120    /// // Or directly from &str via Into<PermissionId>
121    /// let policy2: AccessPolicy<Role, Group> =
122    ///     AccessPolicy::require_permission("write:api");
123    /// ```
124    pub fn require_permission<P: Into<PermissionId>>(permission: P) -> Self {
125        let mut permissions = Permissions::new();
126        let id: PermissionId = permission.into();
127        permissions.bitmap_mut().insert(id.as_u64());
128        Self {
129            role_requirements: vec![],
130            group_requirements: vec![],
131            permission_requirements: permissions,
132        }
133    }
134
135    /// Adds an additional role requirement to this policy.
136    ///
137    /// Access will be granted if the user has ANY of the configured roles.
138    pub fn or_require_role(mut self, role: R) -> Self {
139        self.role_requirements.push(AccessScope::new(role));
140        self
141    }
142
143    /// Adds an additional role or supervisor requirement to this policy.
144    ///
145    /// Access will be granted if the user has the specified role or supervises it.
146    pub fn or_require_role_or_supervisor(mut self, role: R) -> Self {
147        self.role_requirements
148            .push(AccessScope::new(role).allow_supervisor());
149        self
150    }
151
152    /// Adds an additional group requirement to this policy.
153    ///
154    /// Access will be granted if the user is in ANY of the configured groups.
155    pub fn or_require_group(mut self, group: G) -> Self {
156        self.group_requirements.push(group);
157        self
158    }
159
160    /// Adds an additional permission requirement to this policy.
161    ///
162    /// Access will be granted if the user has ANY of the configured permissions.
163    pub fn or_require_permission<P: Into<PermissionId>>(mut self, permission: P) -> Self {
164        let id: PermissionId = permission.into();
165        self.permission_requirements
166            .bitmap_mut()
167            .insert(id.as_u64());
168        self
169    }
170
171    /// Adds multiple additional permission requirements to this policy.
172    ///
173    /// Access will be granted if the user has ANY of the configured permissions.
174    pub fn or_require_permissions<P: Into<PermissionId>>(mut self, permissions: Vec<P>) -> Self {
175        permissions.into_iter().for_each(|p| {
176            let id: PermissionId = p.into();
177            self.permission_requirements
178                .bitmap_mut()
179                .insert(id.as_u64());
180        });
181        self
182    }
183
184    /// Returns the role requirements for this policy.
185    pub fn role_requirements(&self) -> &[AccessScope<R>] {
186        &self.role_requirements
187    }
188
189    /// Returns the group requirements for this policy.
190    pub fn group_requirements(&self) -> &[G] {
191        &self.group_requirements
192    }
193
194    /// Returns the permission requirements for this policy.
195    pub fn permission_requirements(&self) -> &Permissions {
196        &self.permission_requirements
197    }
198
199    /// Returns true if this policy has no requirements (denies all access).
200    ///
201    /// This is useful for validation - a policy that denies all access
202    /// might indicate a configuration error.
203    pub fn denies_all(&self) -> bool {
204        self.role_requirements.is_empty()
205            && self.group_requirements.is_empty()
206            && self.permission_requirements.is_empty()
207    }
208
209    /// Returns true if this policy has at least one requirement configured.
210    ///
211    /// This is useful for validating that a policy is properly configured
212    /// with some access requirements rather than being completely empty.
213    pub fn has_requirements(&self) -> bool {
214        !self.denies_all()
215    }
216
217    /// Converts this policy into the components needed by the authorization service.
218    ///
219    /// This is primarily used internally when bridging to the authorization service.
220    pub fn into_components(self) -> (Vec<AccessScope<R>>, Vec<G>, Permissions) {
221        (
222            self.role_requirements,
223            self.group_requirements,
224            self.permission_requirements,
225        )
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232    use crate::groups::Group;
233    use crate::permissions::PermissionId;
234    use crate::roles::Role;
235
236    #[test]
237    fn deny_all_creates_empty_policy() {
238        let policy: AccessPolicy<Role, Group> = AccessPolicy::deny_all();
239        assert!(policy.denies_all());
240        assert!(!policy.has_requirements());
241        assert!(policy.role_requirements().is_empty());
242        assert!(policy.group_requirements().is_empty());
243        assert!(policy.permission_requirements().is_empty());
244    }
245
246    #[test]
247    fn require_role_creates_role_policy() {
248        let policy: AccessPolicy<Role, Group> = AccessPolicy::require_role(Role::Admin);
249        assert!(!policy.denies_all());
250        assert!(policy.has_requirements());
251        assert_eq!(policy.role_requirements().len(), 1);
252        assert!(policy.group_requirements().is_empty());
253        assert!(policy.permission_requirements().is_empty());
254    }
255
256    #[test]
257    fn require_role_or_supervisor_creates_supervisor_policy() {
258        let policy: AccessPolicy<Role, Group> =
259            AccessPolicy::require_role_or_supervisor(Role::Moderator);
260        assert!(!policy.denies_all());
261        assert!(policy.has_requirements());
262        assert_eq!(policy.role_requirements().len(), 1);
263        assert!(policy.role_requirements()[0].allow_supervisor_access);
264    }
265
266    #[test]
267    fn require_group_creates_group_policy() {
268        let policy: AccessPolicy<Role, Group> =
269            AccessPolicy::require_group(Group::new("engineering"));
270        assert!(!policy.denies_all());
271        assert!(policy.has_requirements());
272        assert!(policy.role_requirements().is_empty());
273        assert_eq!(policy.group_requirements().len(), 1);
274        assert!(policy.permission_requirements().is_empty());
275    }
276
277    #[test]
278    fn require_permission_creates_permission_policy() {
279        let permission_name = "read:api";
280        let expected_id = PermissionId::from(permission_name).as_u64();
281        let policy: AccessPolicy<Role, Group> = AccessPolicy::require_permission(permission_name);
282        assert!(!policy.denies_all());
283        assert!(policy.has_requirements());
284        assert!(policy.role_requirements().is_empty());
285        assert!(policy.group_requirements().is_empty());
286        assert!(
287            policy
288                .permission_requirements()
289                .iter()
290                .any(|id| id == expected_id)
291        );
292    }
293
294    #[test]
295    fn builder_methods_add_requirements() {
296        let base_perms = vec!["read:api", "write:api", "admin:panel"];
297        let policy: AccessPolicy<Role, Group> = AccessPolicy::require_role(Role::Admin)
298            .or_require_role_or_supervisor(Role::Moderator)
299            .or_require_group(Group::new("engineering"))
300            .or_require_permission(base_perms[0])
301            .or_require_permissions(vec![base_perms[1], base_perms[2]]);
302
303        assert!(!policy.denies_all());
304        assert!(policy.has_requirements());
305        assert_eq!(policy.role_requirements().len(), 2);
306        assert_eq!(policy.group_requirements().len(), 1);
307
308        let collected: Vec<u64> = policy.permission_requirements().iter().collect();
309        for name in &base_perms {
310            let id = PermissionId::from(*name).as_u64();
311            assert!(collected.contains(&id), "missing permission {}", name);
312        }
313    }
314
315    #[test]
316    fn into_components_returns_all_requirements() {
317        let permission_name = "system:health";
318        let expected = PermissionId::from(permission_name).as_u64();
319        let policy: AccessPolicy<Role, Group> = AccessPolicy::require_role(Role::Admin)
320            .or_require_group(Group::new("test"))
321            .or_require_permission(permission_name);
322
323        let (roles, groups, permissions) = policy.into_components();
324        assert_eq!(roles.len(), 1);
325        assert_eq!(groups.len(), 1);
326        assert!(permissions.iter().any(|id| id == expected));
327    }
328}