1use std::collections::{HashMap, HashSet};
8
9use crate::auth::User;
10use crate::error::AuthError;
11
12pub trait Authorizer: Send + Sync {
13 fn can(&self, user: &User, action: &str, resource: &str) -> Result<bool, AuthError>;
15}
16
17pub struct RbacAuthorizer {
19 role_permissions: HashMap<String, HashSet<String>>,
20}
21
22impl RbacAuthorizer {
23 pub fn new() -> Self {
25 let mut role_permissions = HashMap::new();
26 role_permissions.insert("admin".to_string(), HashSet::from(["*".to_string()]));
27 Self { role_permissions }
28 }
29
30 pub fn with_role_permission(mut self, role: &str, permission: &str) -> Self {
32 self.role_permissions
33 .entry(role.to_string())
34 .or_default()
35 .insert(permission.to_string());
36 self
37 }
38
39 pub fn grant(&mut self, role: &str, permission: &str) {
41 self.role_permissions
42 .entry(role.to_string())
43 .or_default()
44 .insert(permission.to_string());
45 }
46
47 pub fn revoke(&mut self, role: &str, permission: &str) {
49 if let Some(perms) = self.role_permissions.get_mut(role) {
50 perms.remove(permission);
51 }
52 }
53
54 pub fn role_has_permission(&self, role: &str, permission: &str) -> bool {
56 self.role_permissions
57 .get(role)
58 .map(|perms| perms.contains(permission) || perms.contains("*"))
59 .unwrap_or(false)
60 }
61
62 pub fn permissions_for_role(&self, role: &str) -> Vec<String> {
64 self.role_permissions
65 .get(role)
66 .map(|perms| {
67 let mut v: Vec<String> = perms.iter().cloned().collect();
68 v.sort();
69 v
70 })
71 .unwrap_or_default()
72 }
73
74 fn check_permission(&self, user: &User, permission: &str) -> bool {
75 if user.permissions.iter().any(|p| p == permission || p == "*") {
77 return true;
78 }
79 for role in &user.roles {
81 if self.role_has_permission(role, permission) {
82 return true;
83 }
84 }
85 false
86 }
87}
88
89impl Default for RbacAuthorizer {
90 fn default() -> Self {
91 Self::new()
92 }
93}
94
95impl Authorizer for RbacAuthorizer {
96 fn can(&self, user: &User, action: &str, resource: &str) -> Result<bool, AuthError> {
97 let specific = format!("{}:{}", action, resource);
98 if self.check_permission(user, &specific) {
99 return Ok(true);
100 }
101 if self.check_permission(user, action) {
103 return Ok(true);
104 }
105 Ok(false)
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 fn make_user(id: i64, name: &str) -> User {
114 User::new(id, name)
115 }
116
117 #[test]
118 fn test_admin_role_has_all_permissions() {
119 let authz = RbacAuthorizer::new();
120 let user = make_user(1, "root").with_roles(vec!["admin".to_string()]);
121
122 assert!(authz.can(&user, "read", "posts").unwrap());
123 assert!(authz.can(&user, "delete", "users").unwrap());
124 assert!(authz.can(&user, "anything", "anything").unwrap());
125 }
126
127 #[test]
128 fn test_direct_user_permission_grants_action_resource() {
129 let authz = RbacAuthorizer::new();
130 let user = make_user(1, "alice").with_permissions(vec!["read:posts".to_string()]);
131
132 assert!(authz.can(&user, "read", "posts").unwrap());
133 assert!(!authz.can(&user, "read", "users").unwrap());
135 assert!(!authz.can(&user, "delete", "posts").unwrap());
136 }
137
138 #[test]
139 fn test_action_only_permission_grants_all_resources() {
140 let authz = RbacAuthorizer::new();
141 let user = make_user(1, "bob").with_permissions(vec!["read".to_string()]);
142
143 assert!(authz.can(&user, "read", "posts").unwrap());
144 assert!(authz.can(&user, "read", "users").unwrap());
145 assert!(!authz.can(&user, "write", "posts").unwrap());
146 }
147
148 #[test]
149 fn test_role_grants_permission() {
150 let authz = RbacAuthorizer::new()
151 .with_role_permission("editor", "write:posts")
152 .with_role_permission("viewer", "read:posts");
153
154 let editor = make_user(1, "ed").with_roles(vec!["editor".to_string()]);
155 let viewer = make_user(2, "vi").with_roles(vec!["viewer".to_string()]);
156
157 assert!(authz.can(&editor, "write", "posts").unwrap());
158 assert!(!authz.can(&editor, "delete", "posts").unwrap());
159
160 assert!(authz.can(&viewer, "read", "posts").unwrap());
161 assert!(!authz.can(&viewer, "write", "posts").unwrap());
162 }
163
164 #[test]
165 fn test_grant_and_revoke() {
166 let mut authz = RbacAuthorizer::new();
167 authz.grant("editor", "write:posts");
168 let user = make_user(1, "ed").with_roles(vec!["editor".to_string()]);
169 assert!(authz.can(&user, "write", "posts").unwrap());
170
171 authz.revoke("editor", "write:posts");
172 assert!(!authz.can(&user, "write", "posts").unwrap());
173 }
174
175 #[test]
176 fn test_user_with_no_permissions_is_denied() {
177 let authz = RbacAuthorizer::new();
178 let user = make_user(1, "anon");
179 assert!(!authz.can(&user, "read", "anything").unwrap());
180 }
181
182 #[test]
183 fn test_permissions_for_role() {
184 let authz = RbacAuthorizer::new()
185 .with_role_permission("editor", "write:posts")
186 .with_role_permission("editor", "read:posts");
187
188 let perms = authz.permissions_for_role("editor");
189 assert_eq!(
190 perms,
191 vec!["read:posts".to_string(), "write:posts".to_string()]
192 );
193 assert!(authz.permissions_for_role("nonexistent").is_empty());
194 }
195
196 #[test]
197 fn test_user_permission_overrides_role() {
198 let authz = RbacAuthorizer::new();
200 let user = make_user(1, "lone")
201 .with_permissions(vec!["read:posts".to_string(), "write:posts".to_string()]);
202 assert!(authz.can(&user, "read", "posts").unwrap());
203 assert!(authz.can(&user, "write", "posts").unwrap());
204 }
205
206 #[test]
207 fn test_multiple_roles_combination() {
208 let authz = RbacAuthorizer::new()
209 .with_role_permission("reader", "read:posts")
210 .with_role_permission("writer", "write:posts");
211 let user =
212 make_user(1, "combo").with_roles(vec!["reader".to_string(), "writer".to_string()]);
213 assert!(authz.can(&user, "read", "posts").unwrap());
214 assert!(authz.can(&user, "write", "posts").unwrap());
215 assert!(!authz.can(&user, "delete", "posts").unwrap());
216 }
217
218 #[test]
219 fn test_default_admin_wildcard() {
220 let authz = RbacAuthorizer::new();
221 assert!(authz.role_has_permission("admin", "anything"));
222 assert!(authz.role_has_permission("admin", "*"));
223 assert!(!authz.role_has_permission("editor", "anything"));
224 }
225}