Skip to main content

oxigdal_cluster/security/
mod.rs

1//! Security and access control for cluster operations.
2//!
3//! Provides authentication, authorization (RBAC), encryption, audit logging, and secret management.
4
5use crate::error::{ClusterError, Result};
6use dashmap::DashMap;
7use parking_lot::RwLock;
8use serde::{Deserialize, Serialize};
9use std::collections::{HashMap, HashSet};
10use std::sync::Arc;
11use std::time::{Duration, SystemTime};
12
13/// User identifier.
14pub type UserId = String;
15
16/// Role identifier.
17pub type RoleId = String;
18
19/// Permission string.
20pub type Permission = String;
21
22/// Authentication token.
23pub type Token = String;
24
25/// Security manager for cluster authentication and authorization.
26pub struct SecurityManager {
27    /// User database
28    users: Arc<DashMap<UserId, User>>,
29    /// Role definitions
30    roles: Arc<DashMap<RoleId, Role>>,
31    /// Active sessions
32    sessions: Arc<DashMap<Token, Session>>,
33    /// Audit log
34    audit_log: Arc<RwLock<Vec<AuditEntry>>>,
35    /// Secret store
36    secrets: Arc<RwLock<HashMap<String, Secret>>>,
37    /// Statistics
38    stats: Arc<RwLock<SecurityStats>>,
39}
40
41/// User account.
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct User {
44    /// Unique user identifier
45    pub id: UserId,
46    /// Username for login
47    pub username: String,
48    /// Email address (optional)
49    pub email: Option<String>,
50    /// Assigned roles
51    pub roles: Vec<RoleId>,
52    /// When the account was created
53    pub created_at: SystemTime,
54    /// Last successful login time
55    pub last_login: Option<SystemTime>,
56    /// Whether the account is enabled
57    pub enabled: bool,
58}
59
60/// Role definition with permissions.
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct Role {
63    /// Unique role identifier
64    pub id: RoleId,
65    /// Human-readable role name
66    pub name: String,
67    /// Role description
68    pub description: Option<String>,
69    /// Set of permissions granted by this role
70    pub permissions: HashSet<Permission>,
71}
72
73/// Active session.
74#[derive(Debug, Clone)]
75pub struct Session {
76    /// Authentication token
77    pub token: Token,
78    /// User who owns this session
79    pub user_id: UserId,
80    /// When the session was created
81    pub created_at: SystemTime,
82    /// When the session expires
83    pub expires_at: SystemTime,
84    /// IP address of the client (if available)
85    pub ip_address: Option<String>,
86}
87
88/// Audit log entry.
89#[derive(Debug, Clone, Serialize, Deserialize)]
90pub struct AuditEntry {
91    /// When the action occurred
92    pub timestamp: SystemTime,
93    /// User who performed the action (if applicable)
94    pub user_id: Option<UserId>,
95    /// Action that was performed
96    pub action: String,
97    /// Resource that was affected
98    pub resource: String,
99    /// Result of the action
100    pub result: AuditResult,
101    /// Additional details about the action
102    pub details: Option<String>,
103}
104
105/// Audit result.
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub enum AuditResult {
108    /// Action succeeded
109    Success,
110    /// Action failed due to error
111    Failure,
112    /// Action denied due to permissions
113    Denied,
114}
115
116/// Secret stored securely.
117#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct Secret {
119    /// Secret key/name
120    pub key: String,
121    /// Secret value (in production, this would be encrypted)
122    pub value: String,
123    /// When the secret was created
124    pub created_at: SystemTime,
125    /// When the secret expires (if applicable)
126    pub expires_at: Option<SystemTime>,
127}
128
129/// Security statistics.
130#[derive(Debug, Clone, Default, Serialize, Deserialize)]
131pub struct SecurityStats {
132    /// Total number of registered users
133    pub total_users: usize,
134    /// Number of currently active sessions
135    pub active_sessions: usize,
136    /// Total successful logins
137    pub total_logins: u64,
138    /// Total failed login attempts
139    pub failed_logins: u64,
140    /// Total audit log entries
141    pub total_audit_entries: u64,
142    /// Total authorization denial events
143    pub authorization_denials: u64,
144}
145
146impl SecurityManager {
147    /// Create a new security manager.
148    pub fn new() -> Self {
149        let manager = Self {
150            users: Arc::new(DashMap::new()),
151            roles: Arc::new(DashMap::new()),
152            sessions: Arc::new(DashMap::new()),
153            audit_log: Arc::new(RwLock::new(Vec::new())),
154            secrets: Arc::new(RwLock::new(HashMap::new())),
155            stats: Arc::new(RwLock::new(SecurityStats::default())),
156        };
157
158        // Create default roles
159        manager.create_default_roles();
160
161        manager
162    }
163
164    fn create_default_roles(&self) {
165        // Admin role
166        let admin_permissions: HashSet<Permission> = vec![
167            "cluster:*".to_string(),
168            "task:*".to_string(),
169            "worker:*".to_string(),
170            "user:*".to_string(),
171        ]
172        .into_iter()
173        .collect();
174
175        self.roles.insert(
176            "admin".to_string(),
177            Role {
178                id: "admin".to_string(),
179                name: "Administrator".to_string(),
180                description: Some("Full cluster access".to_string()),
181                permissions: admin_permissions,
182            },
183        );
184
185        // Operator role
186        let operator_permissions: HashSet<Permission> = vec![
187            "cluster:read".to_string(),
188            "task:*".to_string(),
189            "worker:read".to_string(),
190        ]
191        .into_iter()
192        .collect();
193
194        self.roles.insert(
195            "operator".to_string(),
196            Role {
197                id: "operator".to_string(),
198                name: "Operator".to_string(),
199                description: Some("Task and cluster operations".to_string()),
200                permissions: operator_permissions,
201            },
202        );
203
204        // User role
205        let user_permissions: HashSet<Permission> = vec![
206            "cluster:read".to_string(),
207            "task:create".to_string(),
208            "task:read".to_string(),
209            "task:cancel".to_string(),
210        ]
211        .into_iter()
212        .collect();
213
214        self.roles.insert(
215            "user".to_string(),
216            Role {
217                id: "user".to_string(),
218                name: "User".to_string(),
219                description: Some("Basic user access".to_string()),
220                permissions: user_permissions,
221            },
222        );
223    }
224
225    /// Create a new user.
226    pub fn create_user(
227        &self,
228        username: String,
229        email: Option<String>,
230        roles: Vec<RoleId>,
231    ) -> Result<UserId> {
232        let user_id = uuid::Uuid::new_v4().to_string();
233
234        let user = User {
235            id: user_id.clone(),
236            username,
237            email,
238            roles,
239            created_at: SystemTime::now(),
240            last_login: None,
241            enabled: true,
242        };
243
244        self.users.insert(user_id.clone(), user);
245
246        {
247            let mut stats = self.stats.write();
248            stats.total_users = self.users.len();
249        } // Lock is dropped here
250
251        self.audit(
252            "system".to_string(),
253            "user:create".to_string(),
254            user_id.clone(),
255            AuditResult::Success,
256            None,
257        );
258
259        Ok(user_id)
260    }
261
262    /// Authenticate user and create session.
263    pub fn authenticate(&self, user_id: &UserId, _credentials: &str) -> Result<Token> {
264        // In production, verify credentials (password hash, etc.)
265
266        let mut user = self
267            .users
268            .get_mut(user_id)
269            .ok_or_else(|| ClusterError::AuthenticationFailed("User not found".to_string()))?;
270
271        if !user.enabled {
272            let mut stats = self.stats.write();
273            stats.failed_logins += 1;
274            return Err(ClusterError::AuthenticationFailed(
275                "User disabled".to_string(),
276            ));
277        }
278
279        user.last_login = Some(SystemTime::now());
280
281        let token = uuid::Uuid::new_v4().to_string();
282        let session = Session {
283            token: token.clone(),
284            user_id: user_id.clone(),
285            created_at: SystemTime::now(),
286            expires_at: SystemTime::now() + Duration::from_secs(3600), // 1 hour
287            ip_address: None,
288        };
289
290        self.sessions.insert(token.clone(), session);
291
292        {
293            let mut stats = self.stats.write();
294            stats.total_logins += 1;
295            stats.active_sessions = self.sessions.len();
296        } // Lock is dropped here
297
298        self.audit(
299            user_id.clone(),
300            "auth:login".to_string(),
301            user_id.clone(),
302            AuditResult::Success,
303            None,
304        );
305
306        Ok(token)
307    }
308
309    /// Logout and invalidate session.
310    pub fn logout(&self, token: &Token) -> Result<()> {
311        if let Some((_, session)) = self.sessions.remove(token) {
312            self.audit(
313                session.user_id.clone(),
314                "auth:logout".to_string(),
315                session.user_id,
316                AuditResult::Success,
317                None,
318            );
319
320            let mut stats = self.stats.write();
321            stats.active_sessions = self.sessions.len();
322        }
323
324        Ok(())
325    }
326
327    /// Validate a session token.
328    pub fn validate_session(&self, token: &Token) -> Result<UserId> {
329        let session = self
330            .sessions
331            .get(token)
332            .ok_or_else(|| ClusterError::AuthenticationFailed("Invalid session".to_string()))?;
333
334        if SystemTime::now() > session.expires_at {
335            self.sessions.remove(token);
336            return Err(ClusterError::AuthenticationFailed(
337                "Session expired".to_string(),
338            ));
339        }
340
341        Ok(session.user_id.clone())
342    }
343
344    /// Check if user has permission.
345    pub fn check_permission(&self, user_id: &UserId, permission: &Permission) -> Result<bool> {
346        let user = self
347            .users
348            .get(user_id)
349            .ok_or_else(|| ClusterError::AuthenticationFailed("User not found".to_string()))?;
350
351        // Check each role's permissions
352        for role_id in &user.roles {
353            if let Some(role) = self.roles.get(role_id) {
354                // Check wildcard permissions
355                // Extract the prefix before the colon (e.g., "task" from "task:create")
356                let prefix = permission.split(':').next().unwrap_or("");
357                if role.permissions.contains(&format!("{}:*", prefix)) {
358                    return Ok(true);
359                }
360
361                // Check exact permission
362                if role.permissions.contains(permission) {
363                    return Ok(true);
364                }
365
366                // Check global wildcard
367                if role.permissions.contains("*") {
368                    return Ok(true);
369                }
370            }
371        }
372
373        {
374            let mut stats = self.stats.write();
375            stats.authorization_denials += 1;
376        } // Lock is dropped here
377
378        self.audit(
379            user_id.clone(),
380            permission.clone(),
381            user_id.clone(),
382            AuditResult::Denied,
383            Some(format!("Missing permission: {}", permission)),
384        );
385
386        Ok(false)
387    }
388
389    /// Require permission (throws error if not authorized).
390    pub fn require_permission(&self, user_id: &UserId, permission: &Permission) -> Result<()> {
391        if self.check_permission(user_id, permission)? {
392            Ok(())
393        } else {
394            Err(ClusterError::PermissionDenied(permission.clone()))
395        }
396    }
397
398    /// Create or update a role.
399    pub fn create_role(
400        &self,
401        id: RoleId,
402        name: String,
403        permissions: HashSet<Permission>,
404    ) -> Result<()> {
405        let role = Role {
406            id: id.clone(),
407            name,
408            description: None,
409            permissions,
410        };
411
412        self.roles.insert(id, role);
413
414        Ok(())
415    }
416
417    /// Assign role to user.
418    pub fn assign_role(&self, user_id: &UserId, role_id: &RoleId) -> Result<()> {
419        let mut user = self
420            .users
421            .get_mut(user_id)
422            .ok_or_else(|| ClusterError::AuthenticationFailed("User not found".to_string()))?;
423
424        if !user.roles.contains(role_id) {
425            user.roles.push(role_id.clone());
426        }
427
428        self.audit(
429            user_id.clone(),
430            "user:assign_role".to_string(),
431            user_id.clone(),
432            AuditResult::Success,
433            Some(format!("Role: {}", role_id)),
434        );
435
436        Ok(())
437    }
438
439    /// Store a secret.
440    pub fn store_secret(
441        &self,
442        key: String,
443        value: String,
444        expires_at: Option<SystemTime>,
445    ) -> Result<()> {
446        let secret = Secret {
447            key: key.clone(),
448            value, // In production, encrypt this
449            created_at: SystemTime::now(),
450            expires_at,
451        };
452
453        self.secrets.write().insert(key, secret);
454
455        Ok(())
456    }
457
458    /// Retrieve a secret.
459    pub fn get_secret(&self, key: &str) -> Result<String> {
460        let secrets = self.secrets.read();
461        let secret = secrets
462            .get(key)
463            .ok_or_else(|| ClusterError::SecretNotFound(key.to_string()))?;
464
465        // Check expiration
466        if let Some(expires) = secret.expires_at {
467            if SystemTime::now() > expires {
468                return Err(ClusterError::SecretNotFound("Secret expired".to_string()));
469            }
470        }
471
472        Ok(secret.value.clone())
473    }
474
475    /// Log an audit entry.
476    pub fn audit(
477        &self,
478        user_id: UserId,
479        action: String,
480        resource: String,
481        result: AuditResult,
482        details: Option<String>,
483    ) {
484        let entry = AuditEntry {
485            timestamp: SystemTime::now(),
486            user_id: Some(user_id),
487            action,
488            resource,
489            result,
490            details,
491        };
492
493        self.audit_log.write().push(entry);
494
495        let mut stats = self.stats.write();
496        stats.total_audit_entries += 1;
497    }
498
499    /// Get audit log.
500    pub fn get_audit_log(&self, limit: usize) -> Vec<AuditEntry> {
501        let log = self.audit_log.read();
502        log.iter().rev().take(limit).cloned().collect()
503    }
504
505    /// Get security statistics.
506    pub fn get_stats(&self) -> SecurityStats {
507        self.stats.read().clone()
508    }
509}
510
511impl Default for SecurityManager {
512    fn default() -> Self {
513        Self::new()
514    }
515}
516
517#[cfg(test)]
518#[allow(clippy::expect_used, clippy::unwrap_used)]
519mod tests {
520    use super::*;
521
522    #[test]
523    fn test_user_creation() {
524        let manager = SecurityManager::new();
525        let result = manager.create_user(
526            "testuser".to_string(),
527            Some("test@example.com".to_string()),
528            vec!["user".to_string()],
529        );
530
531        assert!(result.is_ok());
532        let stats = manager.get_stats();
533        assert_eq!(stats.total_users, 1);
534    }
535
536    #[test]
537    fn test_authentication() {
538        let manager = SecurityManager::new();
539        let user_id = manager
540            .create_user("testuser".to_string(), None, vec!["user".to_string()])
541            .expect("user creation should succeed");
542
543        let token = manager
544            .authenticate(&user_id, "password")
545            .expect("authentication should succeed");
546        assert!(!token.is_empty());
547
548        let validated_user = manager.validate_session(&token);
549        assert!(validated_user.is_ok());
550        assert_eq!(
551            validated_user.expect("session validation should succeed"),
552            user_id
553        );
554    }
555
556    #[test]
557    fn test_authorization() {
558        let manager = SecurityManager::new();
559        let user_id = manager
560            .create_user("testuser".to_string(), None, vec!["user".to_string()])
561            .expect("user creation should succeed");
562
563        // User should have task:create permission
564        let has_perm = manager
565            .check_permission(&user_id, &"task:create".to_string())
566            .expect("permission check should succeed");
567        assert!(has_perm);
568
569        // User should not have worker:delete permission
570        let has_perm = manager
571            .check_permission(&user_id, &"worker:delete".to_string())
572            .expect("permission check should succeed");
573        assert!(!has_perm);
574    }
575
576    #[test]
577    fn test_secret_management() {
578        let manager = SecurityManager::new();
579
580        manager
581            .store_secret("api_key".to_string(), "secret123".to_string(), None)
582            .ok();
583
584        let secret = manager.get_secret("api_key");
585        assert!(secret.is_ok());
586        assert_eq!(
587            secret.expect("secret retrieval should succeed"),
588            "secret123"
589        );
590    }
591}