amaters_server/
authz.rs

1//! Authorization module
2//!
3//! This module provides authorization services:
4//! - Role-based access control (RBAC)
5//! - Collection-level permissions
6//! - Operation-level permissions (read/write/admin)
7//! - Policy enforcement
8//!
9//! Security model:
10//! - Deny by default (secure)
11//! - Explicit permissions required
12//! - Supports hierarchical roles
13
14use 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/// Authorization errors
25#[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/// Permission levels
52#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Hash)]
53pub enum Permission {
54    /// Read access
55    Read,
56    /// Write access (includes read)
57    Write,
58    /// Admin access (includes read and write)
59    Admin,
60}
61
62impl Permission {
63    /// Check if this permission includes another permission
64    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    /// Parse permission from string
75    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/// Authorization action
104#[derive(Debug, Clone, PartialEq, Eq)]
105pub enum Action {
106    /// Read from collection
107    Read,
108    /// Write to collection
109    Write,
110    /// Delete from collection
111    Delete,
112    /// Create collection
113    CreateCollection,
114    /// Drop collection
115    DropCollection,
116    /// List collections
117    ListCollections,
118    /// Administrative operation
119    Admin,
120}
121
122impl Action {
123    /// Get the minimum permission level required for this action
124    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/// Resource being accessed
134#[derive(Debug, Clone, PartialEq, Eq)]
135pub enum Resource {
136    /// Specific collection
137    Collection(String),
138    /// All collections
139    AllCollections,
140    /// Server administration
141    Server,
142}
143
144/// Role definition
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct Role {
147    /// Role name
148    pub name: String,
149    /// Role description
150    pub description: String,
151    /// Permissions for this role
152    pub permissions: Vec<PermissionRule>,
153    /// Roles that this role inherits from
154    #[serde(default)]
155    pub inherits: Vec<String>,
156}
157
158/// Permission rule
159#[derive(Debug, Clone, Serialize, Deserialize)]
160pub struct PermissionRule {
161    /// Resource pattern (e.g., "collection:users", "collection:*", "server")
162    pub resource: String,
163    /// Actions allowed (read, write, admin)
164    pub actions: Vec<String>,
165}
166
167/// Policy file format
168#[derive(Debug, Serialize, Deserialize)]
169struct PolicyFile {
170    /// Role definitions
171    roles: Vec<Role>,
172}
173
174/// Authorization service
175pub 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    /// Create a new authorizer
184    pub fn new(config: AuthorizationSettings) -> AuthzResult<Self> {
185        let config = Arc::new(config);
186
187        // Load roles
188        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    /// Load roles from file
205    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        // Try JSON first, then TOML
216        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    /// Get default built-in roles
232    fn default_roles() -> HashMap<String, Role> {
233        let mut roles = HashMap::new();
234
235        // Admin role - full access
236        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        // User role - read/write to own collections
247        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        // Reader role - read-only access
258        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    /// Check if a principal is authorized to perform an action on a resource
273    pub fn authorize(
274        &self,
275        principal: &Principal,
276        action: &Action,
277        resource: &Resource,
278    ) -> AuthzResult<()> {
279        if !self.config.enabled {
280            // Authorization disabled - allow all
281            return Ok(());
282        }
283
284        // Get the user's role
285        let role_name = principal
286            .get_attribute("role")
287            .map(|s| s.as_str())
288            .unwrap_or(&self.default_role);
289
290        // Check if user has permission
291        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    /// Check if a role has permission to perform an action on a resource
306    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        // Get all permissions (including inherited)
318        let permissions = self.collect_permissions(role)?;
319
320        // Check if any permission matches
321        let required_permission = action.required_permission();
322
323        for rule in &permissions {
324            if self.matches_resource(&rule.resource, resource) {
325                // Check if rule grants required permission
326                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        // No matching permission found
337        Ok(!self.deny_by_default)
338    }
339
340    /// Collect all permissions for a role (including inherited)
341    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        // Recursively collect inherited permissions
347        for parent_name in &role.inherits {
348            if visited.contains(parent_name) {
349                // Circular inheritance detected
350                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    /// Check if a resource pattern matches a specific resource
366    fn matches_resource(&self, pattern: &str, resource: &Resource) -> bool {
367        match (pattern, resource) {
368            // Wildcard matches everything
369            ("*", _) => true,
370
371            // Collection patterns
372            ("collection:*", Resource::Collection(_)) => true,
373            ("collection:*", Resource::AllCollections) => true,
374
375            // Specific collection
376            (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 pattern
382            ("server", Resource::Server) => true,
383
384            _ => false,
385        }
386    }
387
388    /// Get role information
389    pub fn get_role(&self, role_name: &str) -> Option<&Role> {
390        self.roles.get(role_name)
391    }
392
393    /// List all available roles
394    pub fn list_roles(&self) -> Vec<&Role> {
395        self.roles.values().collect()
396    }
397
398    /// Check if authorization is enabled
399    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); // admin, user, reader
458    }
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        // Admin should have access to everything
483        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        // User should have read/write access to collections
533        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        // User should NOT have admin access
554        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        // Reader should have read access
584        assert!(
585            authz
586                .authorize(
587                    &principal,
588                    &Action::Read,
589                    &Resource::Collection("test".to_string())
590                )
591                .is_ok()
592        );
593
594        // Reader should NOT have write access
595        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        // When disabled, all operations should be allowed
628        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        // Test wildcard pattern
651        assert!(authz.matches_resource("*", &Resource::Collection("test".to_string())));
652        assert!(authz.matches_resource("*", &Resource::Server));
653
654        // Test collection pattern
655        assert!(authz.matches_resource("collection:*", &Resource::Collection("test".to_string())));
656        assert!(!authz.matches_resource("collection:*", &Resource::Server));
657
658        // Test specific collection
659        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}