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(
237 "admin".to_string(),
238 Role {
239 name: "admin".to_string(),
240 description: "Administrator with full access".to_string(),
241 permissions: vec![PermissionRule {
242 resource: "*".to_string(),
243 actions: vec!["admin".to_string()],
244 }],
245 inherits: Vec::new(),
246 },
247 );
248
249 roles.insert(
251 "user".to_string(),
252 Role {
253 name: "user".to_string(),
254 description: "Regular user with read/write access".to_string(),
255 permissions: vec![PermissionRule {
256 resource: "collection:*".to_string(),
257 actions: vec!["read".to_string(), "write".to_string()],
258 }],
259 inherits: Vec::new(),
260 },
261 );
262
263 roles.insert(
265 "reader".to_string(),
266 Role {
267 name: "reader".to_string(),
268 description: "Read-only user".to_string(),
269 permissions: vec![PermissionRule {
270 resource: "collection:*".to_string(),
271 actions: vec!["read".to_string()],
272 }],
273 inherits: Vec::new(),
274 },
275 );
276
277 info!("Using default built-in roles");
278 roles
279 }
280
281 pub fn authorize(
283 &self,
284 principal: &Principal,
285 action: &Action,
286 resource: &Resource,
287 ) -> AuthzResult<()> {
288 if !self.config.enabled {
289 return Ok(());
291 }
292
293 let role_name = principal
295 .get_attribute("role")
296 .map(|s| s.as_str())
297 .unwrap_or(&self.default_role);
298
299 if self.has_permission(role_name, action, resource)? {
301 debug!(
302 "Authorized: user={} role={} action={:?} resource={:?}",
303 principal.name, role_name, action, resource
304 );
305 Ok(())
306 } else {
307 Err(AuthzError::PermissionDenied(format!(
308 "User '{}' with role '{}' not authorized to {:?} on {:?}",
309 principal.name, role_name, action, resource
310 )))
311 }
312 }
313
314 fn has_permission(
316 &self,
317 role_name: &str,
318 action: &Action,
319 resource: &Resource,
320 ) -> AuthzResult<bool> {
321 let role = self
322 .roles
323 .get(role_name)
324 .ok_or_else(|| AuthzError::RoleNotFound(role_name.to_string()))?;
325
326 let permissions = self.collect_permissions(role)?;
328
329 let required_permission = action.required_permission();
331
332 for rule in &permissions {
333 if self.matches_resource(&rule.resource, resource) {
334 for action_str in &rule.actions {
336 if let Some(granted_permission) = Permission::parse(action_str) {
337 if granted_permission.includes(required_permission) {
338 return Ok(true);
339 }
340 }
341 }
342 }
343 }
344
345 Ok(!self.deny_by_default)
347 }
348
349 fn collect_permissions(&self, role: &Role) -> AuthzResult<Vec<PermissionRule>> {
351 let mut permissions = role.permissions.clone();
352 let mut visited = HashSet::new();
353 visited.insert(role.name.clone());
354
355 for parent_name in &role.inherits {
357 if visited.contains(parent_name) {
358 continue;
360 }
361
362 let parent = self.roles.get(parent_name).ok_or_else(|| {
363 AuthzError::ConfigError(format!("Parent role '{}' not found", parent_name))
364 })?;
365
366 let parent_permissions = self.collect_permissions(parent)?;
367 permissions.extend(parent_permissions);
368 visited.insert(parent_name.clone());
369 }
370
371 Ok(permissions)
372 }
373
374 fn matches_resource(&self, pattern: &str, resource: &Resource) -> bool {
376 match (pattern, resource) {
377 ("*", _) => true,
379
380 ("collection:*", Resource::Collection(_)) => true,
382 ("collection:*", Resource::AllCollections) => true,
383
384 (p, Resource::Collection(name)) if p.starts_with("collection:") => {
386 let pattern_name = &p["collection:".len()..];
387 pattern_name == name || pattern_name == "*"
388 }
389
390 ("server", Resource::Server) => true,
392
393 _ => false,
394 }
395 }
396
397 pub fn get_role(&self, role_name: &str) -> Option<&Role> {
399 self.roles.get(role_name)
400 }
401
402 pub fn list_roles(&self) -> Vec<&Role> {
404 self.roles.values().collect()
405 }
406
407 pub fn is_enabled(&self) -> bool {
409 self.config.enabled
410 }
411}
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416 use crate::auth::AuthMethod;
417
418 #[test]
419 fn test_permission_includes() {
420 assert!(Permission::Admin.includes(Permission::Read));
421 assert!(Permission::Admin.includes(Permission::Write));
422 assert!(Permission::Admin.includes(Permission::Admin));
423 assert!(Permission::Write.includes(Permission::Read));
424 assert!(Permission::Write.includes(Permission::Write));
425 assert!(!Permission::Write.includes(Permission::Admin));
426 assert!(Permission::Read.includes(Permission::Read));
427 assert!(!Permission::Read.includes(Permission::Write));
428 assert!(!Permission::Read.includes(Permission::Admin));
429 }
430
431 #[test]
432 fn test_permission_from_str() {
433 assert_eq!(Permission::parse("read"), Some(Permission::Read));
434 assert_eq!(Permission::parse("write"), Some(Permission::Write));
435 assert_eq!(Permission::parse("admin"), Some(Permission::Admin));
436 assert_eq!(Permission::parse("invalid"), None);
437 }
438
439 #[test]
440 fn test_action_required_permission() {
441 assert_eq!(Action::Read.required_permission(), Permission::Read);
442 assert_eq!(Action::Write.required_permission(), Permission::Write);
443 assert_eq!(Action::Delete.required_permission(), Permission::Write);
444 assert_eq!(
445 Action::CreateCollection.required_permission(),
446 Permission::Admin
447 );
448 assert_eq!(Action::Admin.required_permission(), Permission::Admin);
449 }
450
451 #[test]
452 fn test_authorizer_creation() {
453 let config = AuthorizationSettings {
454 enabled: true,
455 default_role: "user".to_string(),
456 roles_file: None,
457 policies_file: None,
458 collection_permissions: true,
459 default_mode: "deny-by-default".to_string(),
460 audit_enabled: true,
461 audit_log_path: None,
462 };
463
464 let authz = Authorizer::new(config).expect("Failed to create authorizer");
465 assert!(authz.is_enabled());
466 assert_eq!(authz.list_roles().len(), 3); }
468
469 #[test]
470 fn test_admin_role_authorization() {
471 let config = AuthorizationSettings {
472 enabled: true,
473 default_role: "user".to_string(),
474 roles_file: None,
475 policies_file: None,
476 collection_permissions: true,
477 default_mode: "deny-by-default".to_string(),
478 audit_enabled: true,
479 audit_log_path: None,
480 };
481
482 let authz = Authorizer::new(config).expect("Failed to create authorizer");
483
484 let principal = Principal::new(
485 "admin1".to_string(),
486 "Admin User".to_string(),
487 AuthMethod::Jwt,
488 )
489 .with_attribute("role".to_string(), "admin".to_string());
490
491 assert!(
493 authz
494 .authorize(
495 &principal,
496 &Action::Read,
497 &Resource::Collection("test".to_string())
498 )
499 .is_ok()
500 );
501
502 assert!(
503 authz
504 .authorize(
505 &principal,
506 &Action::Write,
507 &Resource::Collection("test".to_string())
508 )
509 .is_ok()
510 );
511
512 assert!(
513 authz
514 .authorize(&principal, &Action::CreateCollection, &Resource::Server)
515 .is_ok()
516 );
517 }
518
519 #[test]
520 fn test_user_role_authorization() {
521 let config = AuthorizationSettings {
522 enabled: true,
523 default_role: "user".to_string(),
524 roles_file: None,
525 policies_file: None,
526 collection_permissions: true,
527 default_mode: "deny-by-default".to_string(),
528 audit_enabled: true,
529 audit_log_path: None,
530 };
531
532 let authz = Authorizer::new(config).expect("Failed to create authorizer");
533
534 let principal = Principal::new(
535 "user1".to_string(),
536 "Regular User".to_string(),
537 AuthMethod::Jwt,
538 )
539 .with_attribute("role".to_string(), "user".to_string());
540
541 assert!(
543 authz
544 .authorize(
545 &principal,
546 &Action::Read,
547 &Resource::Collection("test".to_string())
548 )
549 .is_ok()
550 );
551
552 assert!(
553 authz
554 .authorize(
555 &principal,
556 &Action::Write,
557 &Resource::Collection("test".to_string())
558 )
559 .is_ok()
560 );
561
562 assert!(
564 authz
565 .authorize(&principal, &Action::CreateCollection, &Resource::Server)
566 .is_err()
567 );
568 }
569
570 #[test]
571 fn test_reader_role_authorization() {
572 let config = AuthorizationSettings {
573 enabled: true,
574 default_role: "user".to_string(),
575 roles_file: None,
576 policies_file: None,
577 collection_permissions: true,
578 default_mode: "deny-by-default".to_string(),
579 audit_enabled: true,
580 audit_log_path: None,
581 };
582
583 let authz = Authorizer::new(config).expect("Failed to create authorizer");
584
585 let principal = Principal::new(
586 "reader1".to_string(),
587 "Read User".to_string(),
588 AuthMethod::Jwt,
589 )
590 .with_attribute("role".to_string(), "reader".to_string());
591
592 assert!(
594 authz
595 .authorize(
596 &principal,
597 &Action::Read,
598 &Resource::Collection("test".to_string())
599 )
600 .is_ok()
601 );
602
603 assert!(
605 authz
606 .authorize(
607 &principal,
608 &Action::Write,
609 &Resource::Collection("test".to_string())
610 )
611 .is_err()
612 );
613 }
614
615 #[test]
616 fn test_authorization_disabled() {
617 let config = AuthorizationSettings {
618 enabled: false,
619 default_role: "user".to_string(),
620 roles_file: None,
621 policies_file: None,
622 collection_permissions: true,
623 default_mode: "deny-by-default".to_string(),
624 audit_enabled: true,
625 audit_log_path: None,
626 };
627
628 let authz = Authorizer::new(config).expect("Failed to create authorizer");
629
630 let principal = Principal::new(
631 "user1".to_string(),
632 "Test User".to_string(),
633 AuthMethod::Jwt,
634 );
635
636 assert!(
638 authz
639 .authorize(&principal, &Action::Admin, &Resource::Server)
640 .is_ok()
641 );
642 }
643
644 #[test]
645 fn test_resource_matching() {
646 let config = AuthorizationSettings {
647 enabled: true,
648 default_role: "user".to_string(),
649 roles_file: None,
650 policies_file: None,
651 collection_permissions: true,
652 default_mode: "deny-by-default".to_string(),
653 audit_enabled: true,
654 audit_log_path: None,
655 };
656
657 let authz = Authorizer::new(config).expect("Failed to create authorizer");
658
659 assert!(authz.matches_resource("*", &Resource::Collection("test".to_string())));
661 assert!(authz.matches_resource("*", &Resource::Server));
662
663 assert!(authz.matches_resource("collection:*", &Resource::Collection("test".to_string())));
665 assert!(!authz.matches_resource("collection:*", &Resource::Server));
666
667 assert!(
669 authz.matches_resource("collection:test", &Resource::Collection("test".to_string()))
670 );
671 assert!(!authz.matches_resource(
672 "collection:test",
673 &Resource::Collection("other".to_string())
674 ));
675 }
676}