1use crate::auth::Principal;
15use crate::config::AuthorizationSettings;
16use serde::{Deserialize, Serialize};
17use std::collections::{HashMap, HashSet};
18use std::fs;
19use std::path::Path;
20use std::sync::Arc;
21use thiserror::Error;
22use tracing::{debug, info};
23
24#[derive(Error, Debug)]
26pub enum AuthzError {
27 #[error("Permission denied: {0}")]
28 PermissionDenied(String),
29
30 #[error("Role not found: {0}")]
31 RoleNotFound(String),
32
33 #[error("Invalid permission: {0}")]
34 InvalidPermission(String),
35
36 #[error("Configuration error: {0}")]
37 ConfigError(String),
38
39 #[error("IO error: {0}")]
40 Io(#[from] std::io::Error),
41
42 #[error("JSON error: {0}")]
43 Json(#[from] serde_json::Error),
44
45 #[error("TOML error: {0}")]
46 Toml(#[from] toml::de::Error),
47}
48
49pub type AuthzResult<T> = Result<T, AuthzError>;
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash)]
53pub enum Permission {
54 Read,
56 Write,
58 Admin,
60}
61
62impl Permission {
63 pub fn includes(&self, other: Permission) -> bool {
65 matches!(
66 (self, other),
67 (Permission::Admin, _)
68 | (Permission::Write, Permission::Read)
69 | (Permission::Write, Permission::Write)
70 | (Permission::Read, Permission::Read)
71 )
72 }
73
74 pub fn parse(s: &str) -> Option<Self> {
76 match s.to_lowercase().as_str() {
77 "read" => Some(Permission::Read),
78 "write" => Some(Permission::Write),
79 "admin" => Some(Permission::Admin),
80 _ => None,
81 }
82 }
83}
84
85impl std::str::FromStr for Permission {
86 type Err = ();
87
88 fn from_str(s: &str) -> Result<Self, Self::Err> {
89 Permission::parse(s).ok_or(())
90 }
91}
92
93impl std::fmt::Display for Permission {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 match self {
96 Permission::Read => write!(f, "read"),
97 Permission::Write => write!(f, "write"),
98 Permission::Admin => write!(f, "admin"),
99 }
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
105pub enum Action {
106 Read,
108 Write,
110 Delete,
112 CreateCollection,
114 DropCollection,
116 ListCollections,
118 Admin,
120}
121
122impl Action {
123 pub fn required_permission(&self) -> Permission {
125 match self {
126 Action::Read | Action::ListCollections => Permission::Read,
127 Action::Write | Action::Delete => Permission::Write,
128 Action::CreateCollection | Action::DropCollection | Action::Admin => Permission::Admin,
129 }
130 }
131}
132
133#[derive(Debug, Clone, PartialEq, Eq)]
135pub enum Resource {
136 Collection(String),
138 AllCollections,
140 Server,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct Role {
147 pub name: String,
149 pub description: String,
151 pub permissions: Vec<PermissionRule>,
153 #[serde(default)]
155 pub inherits: Vec<String>,
156}
157
158#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct PermissionRule {
161 pub resource: String,
163 pub actions: Vec<String>,
165}
166
167#[derive(Debug, Serialize, Deserialize)]
169struct PolicyFile {
170 roles: Vec<Role>,
172}
173
174pub struct Authorizer {
176 config: Arc<AuthorizationSettings>,
177 roles: HashMap<String, Role>,
178 default_role: String,
179 deny_by_default: bool,
180}
181
182impl Authorizer {
183 pub fn new(config: AuthorizationSettings) -> AuthzResult<Self> {
185 let config = Arc::new(config);
186
187 let roles = if let Some(ref roles_file) = config.roles_file {
189 Self::load_roles(roles_file)?
190 } else {
191 Self::default_roles()
192 };
193
194 let deny_by_default = config.default_mode == "deny-by-default";
195
196 Ok(Self {
197 default_role: config.default_role.clone(),
198 config,
199 roles,
200 deny_by_default,
201 })
202 }
203
204 fn load_roles(path: &Path) -> AuthzResult<HashMap<String, Role>> {
206 if !path.exists() {
207 return Err(AuthzError::ConfigError(format!(
208 "Roles file does not exist: {}",
209 path.display()
210 )));
211 }
212
213 let contents = fs::read_to_string(path)?;
214
215 let policy: PolicyFile = if path.extension().and_then(|s| s.to_str()) == Some("json") {
217 serde_json::from_str(&contents)?
218 } else {
219 toml::from_str(&contents)?
220 };
221
222 let mut roles = HashMap::new();
223 for role in policy.roles {
224 roles.insert(role.name.clone(), role);
225 }
226
227 info!("Loaded {} roles from {}", roles.len(), path.display());
228 Ok(roles)
229 }
230
231 fn default_roles() -> HashMap<String, Role> {
233 let mut roles = HashMap::new();
234
235 roles.insert("admin".to_string(), Role {
237 name: "admin".to_string(),
238 description: "Administrator with full access".to_string(),
239 permissions: vec![PermissionRule {
240 resource: "*".to_string(),
241 actions: vec!["admin".to_string()],
242 }],
243 inherits: Vec::new(),
244 });
245
246 roles.insert("user".to_string(), Role {
248 name: "user".to_string(),
249 description: "Regular user with read/write access".to_string(),
250 permissions: vec![PermissionRule {
251 resource: "collection:*".to_string(),
252 actions: vec!["read".to_string(), "write".to_string()],
253 }],
254 inherits: Vec::new(),
255 });
256
257 roles.insert("reader".to_string(), Role {
259 name: "reader".to_string(),
260 description: "Read-only user".to_string(),
261 permissions: vec![PermissionRule {
262 resource: "collection:*".to_string(),
263 actions: vec!["read".to_string()],
264 }],
265 inherits: Vec::new(),
266 });
267
268 info!("Using default built-in roles");
269 roles
270 }
271
272 pub fn authorize(
274 &self,
275 principal: &Principal,
276 action: &Action,
277 resource: &Resource,
278 ) -> AuthzResult<()> {
279 if !self.config.enabled {
280 return Ok(());
282 }
283
284 let role_name = principal
286 .get_attribute("role")
287 .map(|s| s.as_str())
288 .unwrap_or(&self.default_role);
289
290 if self.has_permission(role_name, action, resource)? {
292 debug!(
293 "Authorized: user={} role={} action={:?} resource={:?}",
294 principal.name, role_name, action, resource
295 );
296 Ok(())
297 } else {
298 Err(AuthzError::PermissionDenied(format!(
299 "User '{}' with role '{}' not authorized to {:?} on {:?}",
300 principal.name, role_name, action, resource
301 )))
302 }
303 }
304
305 fn has_permission(
307 &self,
308 role_name: &str,
309 action: &Action,
310 resource: &Resource,
311 ) -> AuthzResult<bool> {
312 let role = self
313 .roles
314 .get(role_name)
315 .ok_or_else(|| AuthzError::RoleNotFound(role_name.to_string()))?;
316
317 let permissions = self.collect_permissions(role)?;
319
320 let required_permission = action.required_permission();
322
323 for rule in &permissions {
324 if self.matches_resource(&rule.resource, resource) {
325 for action_str in &rule.actions {
327 if let Some(granted_permission) = Permission::parse(action_str) {
328 if granted_permission.includes(required_permission) {
329 return Ok(true);
330 }
331 }
332 }
333 }
334 }
335
336 Ok(!self.deny_by_default)
338 }
339
340 fn collect_permissions(&self, role: &Role) -> AuthzResult<Vec<PermissionRule>> {
342 let mut permissions = role.permissions.clone();
343 let mut visited = HashSet::new();
344 visited.insert(role.name.clone());
345
346 for parent_name in &role.inherits {
348 if visited.contains(parent_name) {
349 continue;
351 }
352
353 let parent = self.roles.get(parent_name).ok_or_else(|| {
354 AuthzError::ConfigError(format!("Parent role '{}' not found", parent_name))
355 })?;
356
357 let parent_permissions = self.collect_permissions(parent)?;
358 permissions.extend(parent_permissions);
359 visited.insert(parent_name.clone());
360 }
361
362 Ok(permissions)
363 }
364
365 fn matches_resource(&self, pattern: &str, resource: &Resource) -> bool {
367 match (pattern, resource) {
368 ("*", _) => true,
370
371 ("collection:*", Resource::Collection(_)) => true,
373 ("collection:*", Resource::AllCollections) => true,
374
375 (p, Resource::Collection(name)) if p.starts_with("collection:") => {
377 let pattern_name = &p["collection:".len()..];
378 pattern_name == name || pattern_name == "*"
379 }
380
381 ("server", Resource::Server) => true,
383
384 _ => false,
385 }
386 }
387
388 pub fn get_role(&self, role_name: &str) -> Option<&Role> {
390 self.roles.get(role_name)
391 }
392
393 pub fn list_roles(&self) -> Vec<&Role> {
395 self.roles.values().collect()
396 }
397
398 pub fn is_enabled(&self) -> bool {
400 self.config.enabled
401 }
402}
403
404#[cfg(test)]
405mod tests {
406 use super::*;
407 use crate::auth::AuthMethod;
408
409 #[test]
410 fn test_permission_includes() {
411 assert!(Permission::Admin.includes(Permission::Read));
412 assert!(Permission::Admin.includes(Permission::Write));
413 assert!(Permission::Admin.includes(Permission::Admin));
414 assert!(Permission::Write.includes(Permission::Read));
415 assert!(Permission::Write.includes(Permission::Write));
416 assert!(!Permission::Write.includes(Permission::Admin));
417 assert!(Permission::Read.includes(Permission::Read));
418 assert!(!Permission::Read.includes(Permission::Write));
419 assert!(!Permission::Read.includes(Permission::Admin));
420 }
421
422 #[test]
423 fn test_permission_from_str() {
424 assert_eq!(Permission::parse("read"), Some(Permission::Read));
425 assert_eq!(Permission::parse("write"), Some(Permission::Write));
426 assert_eq!(Permission::parse("admin"), Some(Permission::Admin));
427 assert_eq!(Permission::parse("invalid"), None);
428 }
429
430 #[test]
431 fn test_action_required_permission() {
432 assert_eq!(Action::Read.required_permission(), Permission::Read);
433 assert_eq!(Action::Write.required_permission(), Permission::Write);
434 assert_eq!(Action::Delete.required_permission(), Permission::Write);
435 assert_eq!(
436 Action::CreateCollection.required_permission(),
437 Permission::Admin
438 );
439 assert_eq!(Action::Admin.required_permission(), Permission::Admin);
440 }
441
442 #[test]
443 fn test_authorizer_creation() {
444 let config = AuthorizationSettings {
445 enabled: true,
446 default_role: "user".to_string(),
447 roles_file: None,
448 policies_file: None,
449 collection_permissions: true,
450 default_mode: "deny-by-default".to_string(),
451 audit_enabled: true,
452 audit_log_path: None,
453 };
454
455 let authz = Authorizer::new(config).expect("Failed to create authorizer");
456 assert!(authz.is_enabled());
457 assert_eq!(authz.list_roles().len(), 3); }
459
460 #[test]
461 fn test_admin_role_authorization() {
462 let config = AuthorizationSettings {
463 enabled: true,
464 default_role: "user".to_string(),
465 roles_file: None,
466 policies_file: None,
467 collection_permissions: true,
468 default_mode: "deny-by-default".to_string(),
469 audit_enabled: true,
470 audit_log_path: None,
471 };
472
473 let authz = Authorizer::new(config).expect("Failed to create authorizer");
474
475 let principal = Principal::new(
476 "admin1".to_string(),
477 "Admin User".to_string(),
478 AuthMethod::Jwt,
479 )
480 .with_attribute("role".to_string(), "admin".to_string());
481
482 assert!(
484 authz
485 .authorize(
486 &principal,
487 &Action::Read,
488 &Resource::Collection("test".to_string())
489 )
490 .is_ok()
491 );
492
493 assert!(
494 authz
495 .authorize(
496 &principal,
497 &Action::Write,
498 &Resource::Collection("test".to_string())
499 )
500 .is_ok()
501 );
502
503 assert!(
504 authz
505 .authorize(&principal, &Action::CreateCollection, &Resource::Server)
506 .is_ok()
507 );
508 }
509
510 #[test]
511 fn test_user_role_authorization() {
512 let config = AuthorizationSettings {
513 enabled: true,
514 default_role: "user".to_string(),
515 roles_file: None,
516 policies_file: None,
517 collection_permissions: true,
518 default_mode: "deny-by-default".to_string(),
519 audit_enabled: true,
520 audit_log_path: None,
521 };
522
523 let authz = Authorizer::new(config).expect("Failed to create authorizer");
524
525 let principal = Principal::new(
526 "user1".to_string(),
527 "Regular User".to_string(),
528 AuthMethod::Jwt,
529 )
530 .with_attribute("role".to_string(), "user".to_string());
531
532 assert!(
534 authz
535 .authorize(
536 &principal,
537 &Action::Read,
538 &Resource::Collection("test".to_string())
539 )
540 .is_ok()
541 );
542
543 assert!(
544 authz
545 .authorize(
546 &principal,
547 &Action::Write,
548 &Resource::Collection("test".to_string())
549 )
550 .is_ok()
551 );
552
553 assert!(
555 authz
556 .authorize(&principal, &Action::CreateCollection, &Resource::Server)
557 .is_err()
558 );
559 }
560
561 #[test]
562 fn test_reader_role_authorization() {
563 let config = AuthorizationSettings {
564 enabled: true,
565 default_role: "user".to_string(),
566 roles_file: None,
567 policies_file: None,
568 collection_permissions: true,
569 default_mode: "deny-by-default".to_string(),
570 audit_enabled: true,
571 audit_log_path: None,
572 };
573
574 let authz = Authorizer::new(config).expect("Failed to create authorizer");
575
576 let principal = Principal::new(
577 "reader1".to_string(),
578 "Read User".to_string(),
579 AuthMethod::Jwt,
580 )
581 .with_attribute("role".to_string(), "reader".to_string());
582
583 assert!(
585 authz
586 .authorize(
587 &principal,
588 &Action::Read,
589 &Resource::Collection("test".to_string())
590 )
591 .is_ok()
592 );
593
594 assert!(
596 authz
597 .authorize(
598 &principal,
599 &Action::Write,
600 &Resource::Collection("test".to_string())
601 )
602 .is_err()
603 );
604 }
605
606 #[test]
607 fn test_authorization_disabled() {
608 let config = AuthorizationSettings {
609 enabled: false,
610 default_role: "user".to_string(),
611 roles_file: None,
612 policies_file: None,
613 collection_permissions: true,
614 default_mode: "deny-by-default".to_string(),
615 audit_enabled: true,
616 audit_log_path: None,
617 };
618
619 let authz = Authorizer::new(config).expect("Failed to create authorizer");
620
621 let principal = Principal::new(
622 "user1".to_string(),
623 "Test User".to_string(),
624 AuthMethod::Jwt,
625 );
626
627 assert!(
629 authz
630 .authorize(&principal, &Action::Admin, &Resource::Server)
631 .is_ok()
632 );
633 }
634
635 #[test]
636 fn test_resource_matching() {
637 let config = AuthorizationSettings {
638 enabled: true,
639 default_role: "user".to_string(),
640 roles_file: None,
641 policies_file: None,
642 collection_permissions: true,
643 default_mode: "deny-by-default".to_string(),
644 audit_enabled: true,
645 audit_log_path: None,
646 };
647
648 let authz = Authorizer::new(config).expect("Failed to create authorizer");
649
650 assert!(authz.matches_resource("*", &Resource::Collection("test".to_string())));
652 assert!(authz.matches_resource("*", &Resource::Server));
653
654 assert!(authz.matches_resource("collection:*", &Resource::Collection("test".to_string())));
656 assert!(!authz.matches_resource("collection:*", &Resource::Server));
657
658 assert!(
660 authz.matches_resource("collection:test", &Resource::Collection("test".to_string()))
661 );
662 assert!(!authz.matches_resource(
663 "collection:test",
664 &Resource::Collection("other".to_string())
665 ));
666 }
667}