1use std::collections::HashMap;
8
9use serde::{Deserialize, Serialize};
10
11use crate::{AuthError, error::Result, middleware::AuthenticatedUser};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
19#[non_exhaustive]
20pub enum OperationPermission {
21 CreateRule,
23 UpdateRule,
25 DeleteRule,
27 ExecuteRule,
29
30 CreateAction,
32 UpdateAction,
34 DeleteAction,
36 ExecuteAction,
38
39 ManageWebhooks,
41 ManageSecrets,
43 ManageUsers,
45 ManageRoles,
47 ManageTenants,
49
50 ExportData,
52 ImportData,
54 DeleteData,
56
57 ViewAuditLogs,
59 ManageConfiguration,
61 ManageIntegrations,
63}
64
65impl OperationPermission {
66 pub const fn name(&self) -> &'static str {
68 match self {
69 Self::CreateRule => "Create Observer Rule",
70 Self::UpdateRule => "Update Observer Rule",
71 Self::DeleteRule => "Delete Observer Rule",
72 Self::ExecuteRule => "Execute Observer Rule",
73 Self::CreateAction => "Create Action",
74 Self::UpdateAction => "Update Action",
75 Self::DeleteAction => "Delete Action",
76 Self::ExecuteAction => "Execute Action",
77 Self::ManageWebhooks => "Manage Webhooks",
78 Self::ManageSecrets => "Manage Secrets",
79 Self::ManageUsers => "Manage Users",
80 Self::ManageRoles => "Manage Roles",
81 Self::ManageTenants => "Manage Tenants",
82 Self::ExportData => "Export Data",
83 Self::ImportData => "Import Data",
84 Self::DeleteData => "Delete Data",
85 Self::ViewAuditLogs => "View Audit Logs",
86 Self::ManageConfiguration => "Manage Configuration",
87 Self::ManageIntegrations => "Manage Integrations",
88 }
89 }
90
91 pub const fn as_str(&self) -> &'static str {
93 match self {
94 Self::CreateRule => "create_rule",
95 Self::UpdateRule => "update_rule",
96 Self::DeleteRule => "delete_rule",
97 Self::ExecuteRule => "execute_rule",
98 Self::CreateAction => "create_action",
99 Self::UpdateAction => "update_action",
100 Self::DeleteAction => "delete_action",
101 Self::ExecuteAction => "execute_action",
102 Self::ManageWebhooks => "manage_webhooks",
103 Self::ManageSecrets => "manage_secrets",
104 Self::ManageUsers => "manage_users",
105 Self::ManageRoles => "manage_roles",
106 Self::ManageTenants => "manage_tenants",
107 Self::ExportData => "export_data",
108 Self::ImportData => "import_data",
109 Self::DeleteData => "delete_data",
110 Self::ViewAuditLogs => "view_audit_logs",
111 Self::ManageConfiguration => "manage_configuration",
112 Self::ManageIntegrations => "manage_integrations",
113 }
114 }
115}
116
117#[derive(Debug, Clone)]
119pub struct Role {
120 pub name: String,
122 pub permissions: Vec<OperationPermission>,
124}
125
126impl Role {
127 pub const fn new(name: String, permissions: Vec<OperationPermission>) -> Self {
129 Self { name, permissions }
130 }
131
132 pub fn has_permission(&self, permission: OperationPermission) -> bool {
134 self.permissions.contains(&permission)
135 }
136
137 pub fn get_permissions(&self) -> &[OperationPermission] {
139 &self.permissions
140 }
141}
142
143#[derive(Debug, Clone)]
145pub struct RBACPolicy {
146 roles: HashMap<String, Role>,
147}
148
149impl Default for RBACPolicy {
150 fn default() -> Self {
151 Self::new()
152 }
153}
154
155impl RBACPolicy {
156 pub fn new() -> Self {
158 let mut roles = HashMap::new();
159
160 roles.insert(
162 "admin".to_string(),
163 Role::new(
164 "admin".to_string(),
165 vec![
166 OperationPermission::CreateRule,
167 OperationPermission::UpdateRule,
168 OperationPermission::DeleteRule,
169 OperationPermission::ExecuteRule,
170 OperationPermission::CreateAction,
171 OperationPermission::UpdateAction,
172 OperationPermission::DeleteAction,
173 OperationPermission::ExecuteAction,
174 OperationPermission::ManageWebhooks,
175 OperationPermission::ManageSecrets,
176 OperationPermission::ManageUsers,
177 OperationPermission::ManageRoles,
178 OperationPermission::ManageTenants,
179 OperationPermission::ExportData,
180 OperationPermission::ImportData,
181 OperationPermission::DeleteData,
182 OperationPermission::ViewAuditLogs,
183 OperationPermission::ManageConfiguration,
184 OperationPermission::ManageIntegrations,
185 ],
186 ),
187 );
188
189 roles.insert(
191 "operator".to_string(),
192 Role::new(
193 "operator".to_string(),
194 vec![
195 OperationPermission::CreateRule,
196 OperationPermission::UpdateRule,
197 OperationPermission::DeleteRule,
198 OperationPermission::ExecuteRule,
199 OperationPermission::CreateAction,
200 OperationPermission::UpdateAction,
201 OperationPermission::DeleteAction,
202 OperationPermission::ExecuteAction,
203 OperationPermission::ManageWebhooks,
204 OperationPermission::ExportData,
205 OperationPermission::ViewAuditLogs,
206 ],
207 ),
208 );
209
210 roles.insert(
212 "viewer".to_string(),
213 Role::new(
214 "viewer".to_string(),
215 vec![
216 OperationPermission::ExportData,
217 OperationPermission::ViewAuditLogs,
218 ],
219 ),
220 );
221
222 Self { roles }
223 }
224
225 pub fn register_role(&mut self, role: Role) {
227 self.roles.insert(role.name.clone(), role);
228 }
229
230 pub fn authorize(
236 &self,
237 user: &AuthenticatedUser,
238 permission: OperationPermission,
239 ) -> Result<()> {
240 let user_roles = self.extract_user_roles(user);
242
243 for role_name in user_roles {
245 if let Some(role) = self.roles.get(&role_name) {
246 if role.has_permission(permission) {
247 return Ok(());
248 }
249 }
250 }
251
252 Err(AuthError::Forbidden {
253 message: format!(
254 "User {} does not have permission to: {}",
255 user.user_id,
256 permission.name()
257 ),
258 })
259 }
260
261 pub fn authorize_any(
267 &self,
268 user: &AuthenticatedUser,
269 permissions: &[OperationPermission],
270 ) -> Result<()> {
271 for permission in permissions {
272 if self.authorize(user, *permission).is_ok() {
273 return Ok(());
274 }
275 }
276
277 Err(AuthError::Forbidden {
278 message: format!("User {} does not have any of the required permissions", user.user_id),
279 })
280 }
281
282 pub fn authorize_all(
288 &self,
289 user: &AuthenticatedUser,
290 permissions: &[OperationPermission],
291 ) -> Result<()> {
292 for permission in permissions {
293 self.authorize(user, *permission)?;
294 }
295 Ok(())
296 }
297
298 pub fn get_user_permissions(&self, user: &AuthenticatedUser) -> Vec<OperationPermission> {
300 let user_roles = self.extract_user_roles(user);
301 let mut permissions = Vec::new();
302
303 for role_name in user_roles {
304 if let Some(role) = self.roles.get(&role_name) {
305 permissions.extend(role.get_permissions());
306 }
307 }
308
309 permissions.sort_by_key(|p| *p as u32);
311 permissions.dedup();
312
313 permissions
314 }
315
316 fn extract_user_roles(&self, user: &AuthenticatedUser) -> Vec<String> {
318 let mut roles = Vec::new();
319
320 if let Some(serde_json::Value::String(role)) = user.get_custom_claim("role") {
322 roles.push(role.clone());
323 }
324
325 if let Some(serde_json::Value::Array(role_array)) = user.get_custom_claim("roles") {
327 for role_val in role_array {
328 if let serde_json::Value::String(role_name) = role_val {
329 roles.push(role_name.clone());
330 }
331 }
332 }
333
334 if let Some(serde_json::Value::Array(role_array)) = user.get_custom_claim("fraiseql_roles")
336 {
337 for role_val in role_array {
338 if let serde_json::Value::String(role_name) = role_val {
339 roles.push(role_name.clone());
340 }
341 }
342 }
343
344 roles.sort();
346 roles.dedup();
347
348 roles
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 #[allow(clippy::wildcard_imports)]
355 use super::*;
357 use crate::jwt::Claims;
358
359 fn create_test_user(role: &str) -> AuthenticatedUser {
360 let mut extra = std::collections::HashMap::new();
361 extra.insert("role".to_string(), serde_json::json!(role));
362
363 AuthenticatedUser {
364 user_id: "test-user".to_string(),
365 claims: Claims {
366 sub: "test-user".to_string(),
367 iat: 1_000_000,
368 exp: 2_000_000,
369 iss: "test-issuer".to_string(),
370 aud: vec!["fraiseql".to_string()],
371 extra,
372 },
373 }
374 }
375
376 fn create_test_user_with_roles(roles: Vec<&str>) -> AuthenticatedUser {
377 let mut extra = std::collections::HashMap::new();
378 extra.insert("roles".to_string(), serde_json::json!(roles));
379
380 AuthenticatedUser {
381 user_id: "test-user".to_string(),
382 claims: Claims {
383 sub: "test-user".to_string(),
384 iat: 1_000_000,
385 exp: 2_000_000,
386 iss: "test-issuer".to_string(),
387 aud: vec!["fraiseql".to_string()],
388 extra,
389 },
390 }
391 }
392
393 #[test]
394 fn test_admin_has_all_permissions() {
395 let policy = RBACPolicy::new();
396 let user = create_test_user("admin");
397
398 let r = policy.authorize(&user, OperationPermission::CreateRule);
399 assert!(r.is_ok(), "admin should have CreateRule: {r:?}");
400 let r = policy.authorize(&user, OperationPermission::DeleteRule);
401 assert!(r.is_ok(), "admin should have DeleteRule: {r:?}");
402 let r = policy.authorize(&user, OperationPermission::ManageUsers);
403 assert!(r.is_ok(), "admin should have ManageUsers: {r:?}");
404 let r = policy.authorize(&user, OperationPermission::ManageTenants);
405 assert!(r.is_ok(), "admin should have ManageTenants: {r:?}");
406 }
407
408 #[test]
409 fn test_operator_has_limited_permissions() {
410 let policy = RBACPolicy::new();
411 let user = create_test_user("operator");
412
413 let r = policy.authorize(&user, OperationPermission::CreateRule);
414 assert!(r.is_ok(), "operator should have CreateRule: {r:?}");
415 let r = policy.authorize(&user, OperationPermission::ManageWebhooks);
416 assert!(r.is_ok(), "operator should have ManageWebhooks: {r:?}");
417 let r = policy.authorize(&user, OperationPermission::ManageUsers);
418 assert!(
419 matches!(r, Err(AuthError::Forbidden { .. })),
420 "operator should not have ManageUsers: {r:?}"
421 );
422 let r = policy.authorize(&user, OperationPermission::ManageTenants);
423 assert!(
424 matches!(r, Err(AuthError::Forbidden { .. })),
425 "operator should not have ManageTenants: {r:?}"
426 );
427 }
428
429 #[test]
430 fn test_viewer_has_minimal_permissions() {
431 let policy = RBACPolicy::new();
432 let user = create_test_user("viewer");
433
434 let r = policy.authorize(&user, OperationPermission::ExportData);
435 assert!(r.is_ok(), "viewer should have ExportData: {r:?}");
436 let r = policy.authorize(&user, OperationPermission::ViewAuditLogs);
437 assert!(r.is_ok(), "viewer should have ViewAuditLogs: {r:?}");
438 let r = policy.authorize(&user, OperationPermission::CreateRule);
439 assert!(
440 matches!(r, Err(AuthError::Forbidden { .. })),
441 "viewer should not have CreateRule: {r:?}"
442 );
443 let r = policy.authorize(&user, OperationPermission::ManageWebhooks);
444 assert!(
445 matches!(r, Err(AuthError::Forbidden { .. })),
446 "viewer should not have ManageWebhooks: {r:?}"
447 );
448 }
449
450 #[test]
451 fn test_multiple_roles() {
452 let policy = RBACPolicy::new();
453 let user = create_test_user_with_roles(vec!["viewer", "operator"]);
454
455 let r = policy.authorize(&user, OperationPermission::CreateRule);
457 assert!(r.is_ok(), "viewer+operator should have CreateRule: {r:?}");
458 let r = policy.authorize(&user, OperationPermission::ExportData);
459 assert!(r.is_ok(), "viewer+operator should have ExportData: {r:?}");
460
461 let r = policy.authorize(&user, OperationPermission::ManageTenants);
463 assert!(
464 matches!(r, Err(AuthError::Forbidden { .. })),
465 "viewer+operator should not have ManageTenants: {r:?}"
466 );
467 }
468
469 #[test]
470 fn test_authorize_any() {
471 let policy = RBACPolicy::new();
472 let user = create_test_user("viewer");
473
474 let permissions = vec![
475 OperationPermission::ManageTenants,
476 OperationPermission::ExportData,
477 ];
478
479 let r = policy.authorize_any(&user, &permissions);
480 assert!(r.is_ok(), "viewer should have at least one of the permissions: {r:?}");
481 }
482
483 #[test]
484 fn test_authorize_all() {
485 let policy = RBACPolicy::new();
486 let user = create_test_user("operator");
487
488 let permissions = vec![
489 OperationPermission::CreateRule,
490 OperationPermission::UpdateRule,
491 ];
492
493 let r = policy.authorize_all(&user, &permissions);
494 assert!(r.is_ok(), "operator should have all rule permissions: {r:?}");
495 }
496
497 #[test]
498 fn test_authorize_all_fails_if_missing_one() {
499 let policy = RBACPolicy::new();
500 let user = create_test_user("operator");
501
502 let permissions = vec![
503 OperationPermission::CreateRule,
504 OperationPermission::ManageTenants, ];
506
507 let r = policy.authorize_all(&user, &permissions);
508 assert!(
509 matches!(r, Err(AuthError::Forbidden { .. })),
510 "operator missing ManageTenants should fail authorize_all: {r:?}"
511 );
512 }
513
514 #[test]
515 fn test_get_user_permissions() {
516 let policy = RBACPolicy::new();
517 let user = create_test_user("viewer");
518
519 let permissions = policy.get_user_permissions(&user);
520 assert_eq!(permissions.len(), 2);
521 assert!(permissions.contains(&OperationPermission::ExportData));
522 assert!(permissions.contains(&OperationPermission::ViewAuditLogs));
523 }
524
525 #[test]
526 fn test_custom_role() {
527 let mut policy = RBACPolicy::new();
528
529 let custom_role = Role::new(
530 "auditor".to_string(),
531 vec![
532 OperationPermission::ViewAuditLogs,
533 OperationPermission::ExportData,
534 ],
535 );
536
537 policy.register_role(custom_role);
538 let user = create_test_user("auditor");
539
540 let r = policy.authorize(&user, OperationPermission::ViewAuditLogs);
541 assert!(r.is_ok(), "auditor should have ViewAuditLogs: {r:?}");
542 let r = policy.authorize(&user, OperationPermission::CreateRule);
543 assert!(
544 matches!(r, Err(AuthError::Forbidden { .. })),
545 "auditor should not have CreateRule: {r:?}"
546 );
547 }
548
549 #[test]
550 fn test_permission_string_format() {
551 assert_eq!(OperationPermission::CreateRule.as_str(), "create_rule");
552 assert_eq!(OperationPermission::ManageSecrets.as_str(), "manage_secrets");
553 assert_eq!(OperationPermission::ViewAuditLogs.as_str(), "view_audit_logs");
554 }
555}