Skip to main content

webgates_core/authz/
authorization_service.rs

1use crate::accounts::Account;
2use crate::authz::access_hierarchy::AccessHierarchy;
3use crate::authz::access_policy::AccessPolicy;
4
5use std::collections::HashSet;
6use tracing::debug;
7
8/// Service that evaluates an [`AccessPolicy`] against an [`Account`].
9///
10/// This is the runtime piece of the authorization model in `webgates-core`.
11/// You define the rule with [`AccessPolicy`], then ask `AuthorizationService`
12/// whether a specific account satisfies it.
13#[derive(Debug, Clone)]
14pub struct AuthorizationService<R, G>
15where
16    R: AccessHierarchy + Eq + std::fmt::Display,
17    G: Eq + Clone,
18{
19    policy: AccessPolicy<R, G>,
20}
21
22impl<R, G> AuthorizationService<R, G>
23where
24    R: AccessHierarchy + Eq + std::fmt::Display,
25    G: Eq + Clone,
26{
27    /// Creates a new authorization service for the provided policy.
28    pub fn new(policy: AccessPolicy<R, G>) -> Self {
29        Self { policy }
30    }
31
32    /// Returns `true` when the account satisfies the policy.
33    ///
34    /// Policies use OR semantics across requirement categories:
35    /// - exact role matches
36    /// - role hierarchy matches
37    /// - group membership matches
38    /// - permission matches
39    pub fn is_authorized(&self, account: &Account<R, G>) -> bool {
40        self.meets_role_requirement(account)
41            || self.meets_role_hierarchy_requirement(account)
42            || self.meets_group_requirement(account)
43            || self.meets_permission_requirement(account)
44    }
45
46    /// Returns `true` when the account has any exactly required role.
47    pub fn meets_role_requirement(&self, account: &Account<R, G>) -> bool {
48        account.roles.iter().any(|role| {
49            self.policy
50                .role_requirements()
51                .iter()
52                .any(|scope| scope.grants_role(role))
53        })
54    }
55
56    /// Returns `true` when the account has any role that satisfies a
57    /// same-or-supervisor requirement.
58    ///
59    /// Ordering contract: higher privilege is greater than lower privilege, so
60    /// a supervising role satisfies `user_role >= required_role`.
61    pub fn meets_role_hierarchy_requirement(&self, account: &Account<R, G>) -> bool {
62        debug!("Checking role hierarchy requirements.");
63        account.roles.iter().any(|user_role| {
64            self.policy
65                .role_requirements()
66                .iter()
67                .any(|scope| scope.grants_supervisor(user_role))
68        })
69    }
70
71    /// Returns `true` when the account belongs to any required group.
72    pub fn meets_group_requirement(&self, account: &Account<R, G>) -> bool {
73        account.groups.iter().any(|group| {
74            self.policy
75                .group_requirements()
76                .iter()
77                .any(|required_group| required_group == group)
78        })
79    }
80
81    /// Returns `true` when the account has any required permission.
82    pub fn meets_permission_requirement(&self, account: &Account<R, G>) -> bool {
83        let account_permissions: HashSet<u64> = account.permissions.iter().collect();
84        let required_permissions: HashSet<u64> =
85            self.policy.permission_requirements().iter().collect();
86
87        !account_permissions.is_disjoint(&required_permissions)
88    }
89
90    /// Returns `true` when the configured policy has no requirements.
91    ///
92    /// Such a policy denies all access.
93    pub fn policy_denies_all_access(&self) -> bool {
94        self.policy.denies_all()
95    }
96
97    /// Returns a clone of the configured policy.
98    pub fn clone_policy(&self) -> AccessPolicy<R, G> {
99        self.policy.clone()
100    }
101}
102
103#[cfg(test)]
104mod tests {
105    use super::AuthorizationService;
106    use crate::accounts::Account;
107    use crate::authz::access_policy::AccessPolicy;
108    use crate::groups::Group;
109    use crate::roles::Role;
110
111    fn create_test_account() -> Account<Role, Group> {
112        let mut account = Account::new("test_user");
113        account.roles = vec![Role::Admin];
114        account.groups = vec![Group::new("engineering")];
115        account.with_permissions(["read:api", "write:docs"].into_iter().collect())
116    }
117
118    #[test]
119    fn authorization_service_reports_empty_policy() {
120        let service: AuthorizationService<Role, Group> =
121            AuthorizationService::new(AccessPolicy::deny_all());
122
123        assert!(service.policy_denies_all_access());
124    }
125
126    #[test]
127    fn authorization_service_reports_non_empty_policy() {
128        let service: AuthorizationService<Role, Group> =
129            AuthorizationService::new(AccessPolicy::require_role(Role::Admin));
130
131        assert!(!service.policy_denies_all_access());
132    }
133
134    #[test]
135    fn meets_exact_role_requirement() {
136        let account = create_test_account();
137        let service = AuthorizationService::new(AccessPolicy::require_role(Role::Admin));
138
139        assert!(service.meets_role_requirement(&account));
140    }
141
142    #[test]
143    fn rejects_missing_exact_role_requirement() {
144        let account = create_test_account();
145        let service = AuthorizationService::new(AccessPolicy::require_role(Role::User));
146
147        assert!(!service.meets_role_requirement(&account));
148    }
149
150    #[test]
151    fn meets_group_requirement() {
152        let account = create_test_account();
153        let service =
154            AuthorizationService::new(AccessPolicy::require_group(Group::new("engineering")));
155
156        assert!(service.meets_group_requirement(&account));
157    }
158
159    #[test]
160    fn rejects_missing_group_requirement() {
161        let account = create_test_account();
162        let service = AuthorizationService::new(AccessPolicy::require_group(Group::new("sales")));
163
164        assert!(!service.meets_group_requirement(&account));
165    }
166
167    #[test]
168    fn meets_permission_requirement() {
169        let account = create_test_account();
170        let service = AuthorizationService::new(AccessPolicy::require_permission("read:api"));
171
172        assert!(service.meets_permission_requirement(&account));
173    }
174
175    #[test]
176    fn rejects_missing_permission_requirement() {
177        let account = create_test_account();
178        let service = AuthorizationService::new(AccessPolicy::require_permission("admin:system"));
179
180        assert!(!service.meets_permission_requirement(&account));
181    }
182
183    #[test]
184    fn meets_role_hierarchy_requirement_for_supervisor() {
185        let account = create_test_account();
186        let service =
187            AuthorizationService::new(AccessPolicy::require_role_or_supervisor(Role::Moderator));
188
189        assert!(service.meets_role_hierarchy_requirement(&account));
190    }
191
192    #[test]
193    fn rejects_role_hierarchy_requirement_when_supervisors_are_not_allowed() {
194        let account = create_test_account();
195        let service = AuthorizationService::new(AccessPolicy::require_role(Role::Moderator));
196
197        assert!(!service.meets_role_hierarchy_requirement(&account));
198    }
199
200    #[test]
201    fn is_authorized_when_any_requirement_matches() {
202        let account = create_test_account();
203        let service = AuthorizationService::new(
204            AccessPolicy::require_role(Role::User).or_require_group(Group::new("engineering")),
205        );
206
207        assert!(service.is_authorized(&account));
208    }
209
210    #[test]
211    fn is_not_authorized_when_nothing_matches() {
212        let account = create_test_account();
213        let service = AuthorizationService::new(
214            AccessPolicy::require_role(Role::User).or_require_group(Group::new("sales")),
215        );
216
217        assert!(!service.is_authorized(&account));
218    }
219}