Skip to main content

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(
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        // User role - read/write to own collections
250        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        // Reader role - read-only access
264        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    /// Check if a principal is authorized to perform an action on a resource
282    pub fn authorize(
283        &self,
284        principal: &Principal,
285        action: &Action,
286        resource: &Resource,
287    ) -> AuthzResult<()> {
288        if !self.config.enabled {
289            // Authorization disabled - allow all
290            return Ok(());
291        }
292
293        // Get the user's role
294        let role_name = principal
295            .get_attribute("role")
296            .map(|s| s.as_str())
297            .unwrap_or(&self.default_role);
298
299        // Check if user has permission
300        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    /// Check if a role has permission to perform an action on a resource
315    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        // Get all permissions (including inherited)
327        let permissions = self.collect_permissions(role)?;
328
329        // Check if any permission matches
330        let required_permission = action.required_permission();
331
332        for rule in &permissions {
333            if self.matches_resource(&rule.resource, resource) {
334                // Check if rule grants required permission
335                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        // No matching permission found
346        Ok(!self.deny_by_default)
347    }
348
349    /// Collect all permissions for a role (including inherited)
350    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        // Recursively collect inherited permissions
356        for parent_name in &role.inherits {
357            if visited.contains(parent_name) {
358                // Circular inheritance detected
359                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    /// Check if a resource pattern matches a specific resource
375    fn matches_resource(&self, pattern: &str, resource: &Resource) -> bool {
376        match (pattern, resource) {
377            // Wildcard matches everything
378            ("*", _) => true,
379
380            // Collection patterns
381            ("collection:*", Resource::Collection(_)) => true,
382            ("collection:*", Resource::AllCollections) => true,
383
384            // Specific collection
385            (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 pattern
391            ("server", Resource::Server) => true,
392
393            _ => false,
394        }
395    }
396
397    /// Get role information
398    pub fn get_role(&self, role_name: &str) -> Option<&Role> {
399        self.roles.get(role_name)
400    }
401
402    /// List all available roles
403    pub fn list_roles(&self) -> Vec<&Role> {
404        self.roles.values().collect()
405    }
406
407    /// Check if authorization is enabled
408    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); // admin, user, reader
467    }
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        // Admin should have access to everything
492        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        // User should have read/write access to collections
542        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        // User should NOT have admin access
563        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        // Reader should have read access
593        assert!(
594            authz
595                .authorize(
596                    &principal,
597                    &Action::Read,
598                    &Resource::Collection("test".to_string())
599                )
600                .is_ok()
601        );
602
603        // Reader should NOT have write access
604        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        // When disabled, all operations should be allowed
637        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        // Test wildcard pattern
660        assert!(authz.matches_resource("*", &Resource::Collection("test".to_string())));
661        assert!(authz.matches_resource("*", &Resource::Server));
662
663        // Test collection pattern
664        assert!(authz.matches_resource("collection:*", &Resource::Collection("test".to_string())));
665        assert!(!authz.matches_resource("collection:*", &Resource::Server));
666
667        // Test specific collection
668        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}