1use std::collections::HashMap;
4use thiserror::Error;
5
6#[derive(Debug, Clone)]
8pub struct RecoverySuggestion {
9 pub message: String,
10 pub suggested_actions: Vec<String>,
11 pub documentation_link: Option<String>,
12}
13
14impl RecoverySuggestion {
15 pub fn new(message: impl Into<String>) -> Self {
16 Self {
17 message: message.into(),
18 suggested_actions: Vec::new(),
19 documentation_link: None,
20 }
21 }
22
23 pub fn with_action(mut self, action: impl Into<String>) -> Self {
24 self.suggested_actions.push(action.into());
25 self
26 }
27
28 pub fn with_documentation(mut self, link: impl Into<String>) -> Self {
29 self.documentation_link = Some(link.into());
30 self
31 }
32}
33
34#[derive(Debug, Clone)]
36pub struct PermissionDeniedDetails {
37 pub action: String,
38 pub resource: String,
39 pub subject: String,
40 pub required_permissions: Vec<String>,
41 pub suggested_roles: Vec<String>,
42 pub recovery: Option<RecoverySuggestion>,
43}
44
45impl std::fmt::Display for PermissionDeniedDetails {
46 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
47 write!(
48 f,
49 "Permission denied: {} on {} for {}",
50 self.action, self.resource, self.subject
51 )
52 }
53}
54
55#[derive(Error, Debug, Clone)]
57pub enum Error {
58 #[error("Role '{0}' already exists")]
60 RoleAlreadyExists(String),
61
62 #[error("Role '{0}' not found")]
64 RoleNotFound(String),
65
66 #[error("Subject '{0}' not found")]
68 SubjectNotFound(String),
69
70 #[error("{0}")]
72 PermissionDenied(Box<PermissionDeniedDetails>),
73
74 #[error("Circular dependency detected in role hierarchy involving '{0}'")]
76 CircularDependency(String),
77
78 #[error("Invalid permission format: {0}")]
80 InvalidPermission(String),
81
82 #[error("Invalid resource format: {0}")]
84 InvalidResource(String),
85
86 #[error("Role elevation for subject '{0}' has expired")]
88 ElevationExpired(String),
89
90 #[error("Maximum role hierarchy depth exceeded (max: {0})")]
92 MaxDepthExceeded(usize),
93
94 #[cfg(feature = "persistence")]
96 #[error("Serialization error: {0}")]
97 Serialization(String),
98
99 #[error("Storage operation failed: {0}")]
101 Storage(String),
102
103 #[error("Invalid configuration: {0}")]
105 InvalidConfiguration(String),
106
107 #[error("Role operation failed: {operation} on role '{role}' - {reason}")]
109 RoleOperationFailed {
110 operation: String,
111 role: String,
112 reason: String,
113 },
114
115 #[error(
117 "Permission operation failed: {operation} for subject '{subject}' on resource '{resource}' - {reason}"
118 )]
119 PermissionOperationFailed {
120 operation: String,
121 subject: String,
122 resource: String,
123 reason: String,
124 context: Box<HashMap<String, String>>,
125 },
126
127 #[error("Validation failed for field '{field}': {reason}")]
129 ValidationError {
130 field: String,
131 reason: String,
132 invalid_value: Option<String>,
133 },
134
135 #[error("Rate limit exceeded for subject '{subject}': {limit} operations per {window}")]
137 RateLimitExceeded {
138 subject: String,
139 limit: u64,
140 window: String,
141 },
142
143 #[error("Concurrency conflict: {operation} failed due to concurrent modification")]
145 ConcurrencyConflict {
146 operation: String,
147 resource_id: String,
148 },
149
150 #[error("Authentication failed: {reason}")]
152 AuthenticationFailed {
153 reason: String,
154 subject_id: Option<String>,
155 },
156
157 #[error(
159 "Authorization failed: subject '{subject}' lacks permission '{permission}' for resource '{resource}'"
160 )]
161 AuthorizationFailed {
162 subject: String,
163 permission: String,
164 resource: String,
165 required_roles: Vec<String>,
166 },
167}
168
169pub type Result<T> = std::result::Result<T, Error>;
171
172#[cfg(feature = "persistence")]
174impl From<serde_json::Error> for Error {
175 fn from(err: serde_json::Error) -> Self {
176 Self::Serialization(err.to_string())
177 }
178}
179
180impl Error {
181 pub fn validate_identifier(value: &str, field_name: &str) -> Result<()> {
183 if value.is_empty() {
184 return Err(Error::ValidationError {
185 field: field_name.to_string(),
186 reason: "cannot be empty".to_string(),
187 invalid_value: Some(value.to_string()),
188 });
189 }
190
191 if value.len() > 256 {
192 return Err(Error::ValidationError {
193 field: field_name.to_string(),
194 reason: "too long (maximum 256 characters)".to_string(),
195 invalid_value: Some(value.to_string()),
196 });
197 }
198
199 let dangerous_chars = [';', '\'', '"', '\\', '\0', '\n', '\r'];
201 let dangerous_sequences = ["--", "/*", "*/", "<", ">", "{", "}", "[", "]"];
202
203 for &ch in &dangerous_chars {
204 if value.contains(ch) {
205 return Err(Error::ValidationError {
206 field: field_name.to_string(),
207 reason: "contains invalid characters".to_string(),
208 invalid_value: Some(value.to_string()),
209 });
210 }
211 }
212
213 for &seq in &dangerous_sequences {
214 if value.contains(seq) {
215 return Err(Error::ValidationError {
216 field: field_name.to_string(),
217 reason: "contains invalid characters".to_string(),
218 invalid_value: Some(value.to_string()),
219 });
220 }
221 }
222
223 if value.contains("..") {
225 return Err(Error::ValidationError {
226 field: field_name.to_string(),
227 reason: "potential path traversal detected".to_string(),
228 invalid_value: Some(value.to_string()),
229 });
230 }
231
232 Ok(())
233 }
234
235 pub fn validate_resource_path(path: &str) -> Result<()> {
237 if path.is_empty() {
239 return Ok(());
240 }
241
242 if !path.starts_with('/') {
244 return Err(Error::ValidationError {
245 field: "resource_path".to_string(),
246 reason: "must start with '/' or be empty".to_string(),
247 invalid_value: Some(path.to_string()),
248 });
249 }
250
251 if path.contains("../") || path.contains("..\\") {
253 return Err(Error::ValidationError {
254 field: "resource_path".to_string(),
255 reason: "path traversal detected".to_string(),
256 invalid_value: Some(path.to_string()),
257 });
258 }
259
260 if path.contains('\0') {
262 return Err(Error::ValidationError {
263 field: "resource_path".to_string(),
264 reason: "null byte detected".to_string(),
265 invalid_value: Some(path.to_string()),
266 });
267 }
268
269 Ok(())
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 #[test]
278 fn test_recovery_suggestion_creation() {
279 let suggestion = RecoverySuggestion::new("Permission denied")
280 .with_action("Assign the 'admin' role to the user")
281 .with_action("Check if the resource exists")
282 .with_documentation("https://docs.example.com/permissions");
283
284 assert_eq!(suggestion.message, "Permission denied");
285 assert_eq!(suggestion.suggested_actions.len(), 2);
286 assert_eq!(
287 suggestion.suggested_actions[0],
288 "Assign the 'admin' role to the user"
289 );
290 assert_eq!(
291 suggestion.suggested_actions[1],
292 "Check if the resource exists"
293 );
294 assert_eq!(
295 suggestion.documentation_link,
296 Some("https://docs.example.com/permissions".to_string())
297 );
298 }
299
300 #[test]
301 fn test_permission_denied_details_display() {
302 let details = PermissionDeniedDetails {
303 action: "delete".to_string(),
304 resource: "document.txt".to_string(),
305 subject: "alice".to_string(),
306 required_permissions: vec!["delete:documents".to_string()],
307 suggested_roles: vec!["admin".to_string(), "editor".to_string()],
308 recovery: Some(RecoverySuggestion::new("Assign appropriate role")),
309 };
310
311 let display = format!("{}", details);
312 assert!(display.contains("Permission denied"));
313 assert!(display.contains("delete"));
314 assert!(display.contains("document.txt"));
315 assert!(display.contains("alice"));
316 }
317
318 #[test]
319 fn test_permission_denied_error_creation() {
320 let details = PermissionDeniedDetails {
321 action: "read".to_string(),
322 resource: "secret.txt".to_string(),
323 subject: "bob".to_string(),
324 required_permissions: vec!["read:secrets".to_string()],
325 suggested_roles: vec!["security_admin".to_string()],
326 recovery: Some(
327 RecoverySuggestion::new("User needs security clearance")
328 .with_action("Contact security administrator")
329 .with_documentation("https://docs.example.com/security"),
330 ),
331 };
332
333 let error = Error::PermissionDenied(Box::new(details));
334
335 match error {
336 Error::PermissionDenied(d) => {
337 assert_eq!(d.action, "read");
338 assert_eq!(d.resource, "secret.txt");
339 assert_eq!(d.subject, "bob");
340 assert!(d.recovery.is_some());
341 assert_eq!(
342 d.recovery.unwrap().suggested_actions[0],
343 "Contact security administrator"
344 );
345 }
346 _ => panic!("Expected PermissionDenied error"),
347 }
348 }
349
350 #[test]
351 fn test_validation_error_formatting() {
352 let error = Error::ValidationError {
353 field: "username".to_string(),
354 reason: "contains invalid characters".to_string(),
355 invalid_value: Some("user@name!".to_string()),
356 };
357
358 let error_string = format!("{}", error);
359 assert!(error_string.contains("Validation failed"));
360 assert!(error_string.contains("username"));
361 assert!(error_string.contains("invalid characters"));
362 }
363
364 #[test]
365 fn test_security_validation_basic() {
366 assert!(Error::validate_identifier("valid_user", "username").is_ok());
368 assert!(Error::validate_identifier("role123", "role").is_ok());
369 assert!(Error::validate_identifier("resource_name", "resource").is_ok());
370 }
371
372 #[test]
373 fn test_security_validation_empty_input() {
374 let result = Error::validate_identifier("", "field");
375 assert!(result.is_err());
376 match result.unwrap_err() {
377 Error::ValidationError { field, reason, .. } => {
378 assert_eq!(field, "field");
379 assert!(reason.contains("cannot be empty"));
380 }
381 _ => panic!("Expected ValidationError"),
382 }
383 }
384
385 #[test]
386 fn test_security_validation_invalid_characters() {
387 let test_cases = vec![
388 "user;name", "user'name", "user\"name", "user--name", "user/*name", "user<script>", "user{name}", "user[name]", "user\\name", ];
398
399 for test_case in test_cases {
400 let result = Error::validate_identifier(test_case, "field");
401 assert!(result.is_err(), "Should reject: {}", test_case);
402 match result.unwrap_err() {
403 Error::ValidationError { reason, .. } => {
404 assert!(reason.contains("invalid characters"));
405 }
406 _ => panic!("Expected ValidationError for: {}", test_case),
407 }
408 }
409 }
410
411 #[test]
412 fn test_resource_path_validation_valid() {
413 assert!(Error::validate_resource_path("").is_ok()); assert!(Error::validate_resource_path("/documents").is_ok());
415 assert!(Error::validate_resource_path("/documents/file.txt").is_ok());
416 assert!(Error::validate_resource_path("/api/v1/users").is_ok());
417 }
418
419 #[test]
420 fn test_comprehensive_error_scenarios() {
421 let errors = vec![
423 Error::RoleNotFound("admin".to_string()),
424 Error::RoleAlreadyExists("user".to_string()),
425 Error::SubjectNotFound("alice".to_string()),
426 Error::CircularDependency("role cycle detected".to_string()),
427 Error::ValidationError {
428 field: "username".to_string(),
429 reason: "invalid format".to_string(),
430 invalid_value: Some("test@user".to_string()),
431 },
432 Error::Storage("connection failed".to_string()),
433 Error::InvalidConfiguration("missing config".to_string()),
434 Error::RateLimitExceeded {
435 subject: "user123".to_string(),
436 limit: 100,
437 window: "1 minute".to_string(),
438 },
439 Error::ConcurrencyConflict {
440 operation: "role_assignment".to_string(),
441 resource_id: "role_123".to_string(),
442 },
443 ];
444
445 for error in errors {
446 let error_string = format!("{}", error);
448 assert!(!error_string.is_empty());
449
450 let debug_string = format!("{:?}", error);
452 assert!(!debug_string.is_empty());
453 }
454 }
455
456 #[test]
457 fn test_enhanced_error_context_integration() {
458 let recovery = RecoverySuggestion::new("User needs additional permissions")
460 .with_action("Assign the 'documents_admin' role")
461 .with_action("Verify the document exists")
462 .with_action("Check if the user's access has expired")
463 .with_documentation("https://docs.company.com/rbac/troubleshooting");
464
465 let details = PermissionDeniedDetails {
466 action: "delete".to_string(),
467 resource: "/documents/confidential/report.pdf".to_string(),
468 subject: "employee_123".to_string(),
469 required_permissions: vec![
470 "delete:documents".to_string(),
471 "access:confidential".to_string(),
472 ],
473 suggested_roles: vec![
474 "documents_admin".to_string(),
475 "confidential_access".to_string(),
476 ],
477 recovery: Some(recovery),
478 };
479
480 let error = Error::PermissionDenied(Box::new(details));
481
482 match &error {
484 Error::PermissionDenied(d) => {
485 assert_eq!(d.action, "delete");
486 assert_eq!(d.resource, "/documents/confidential/report.pdf");
487 assert_eq!(d.subject, "employee_123");
488 assert_eq!(d.required_permissions.len(), 2);
489 assert_eq!(d.suggested_roles.len(), 2);
490 assert!(d.recovery.is_some());
491
492 let recovery = d.recovery.as_ref().unwrap();
493 assert_eq!(recovery.suggested_actions.len(), 3);
494 assert!(recovery.documentation_link.is_some());
495 }
496 _ => panic!("Expected PermissionDenied"),
497 }
498
499 let error_message = format!("{}", error);
501 assert!(error_message.contains("Permission denied"));
502 assert!(error_message.contains("delete"));
503 assert!(error_message.contains("confidential"));
504 }
505
506 #[test]
507 fn test_role_operation_failed_error() {
508 let error = Error::RoleOperationFailed {
509 operation: "assign".to_string(),
510 role: "admin".to_string(),
511 reason: "circular dependency detected".to_string(),
512 };
513
514 let error_string = format!("{}", error);
515 assert!(error_string.contains("Role operation failed"));
516 assert!(error_string.contains("assign"));
517 assert!(error_string.contains("admin"));
518 assert!(error_string.contains("circular dependency"));
519 }
520
521 #[test]
522 fn test_permission_operation_failed_error() {
523 let mut context = HashMap::new();
524 context.insert("user_group".to_string(), "employees".to_string());
525 context.insert("resource_owner".to_string(), "security_team".to_string());
526
527 let error = Error::PermissionOperationFailed {
528 operation: "check".to_string(),
529 subject: "alice".to_string(),
530 resource: "classified_document".to_string(),
531 reason: "insufficient clearance level".to_string(),
532 context: Box::new(context),
533 };
534
535 let error_string = format!("{}", error);
536 assert!(error_string.contains("Permission operation failed"));
537 assert!(error_string.contains("check"));
538 assert!(error_string.contains("alice"));
539 assert!(error_string.contains("classified_document"));
540 assert!(error_string.contains("insufficient clearance"));
541 }
542
543 #[test]
544 fn test_rate_limit_exceeded_error() {
545 let error = Error::RateLimitExceeded {
546 subject: "user123".to_string(),
547 limit: 100,
548 window: "1 minute".to_string(),
549 };
550
551 let error_string = format!("{}", error);
552 assert!(error_string.contains("Rate limit exceeded"));
553 assert!(error_string.contains("user123"));
554 assert!(error_string.contains("100"));
555 assert!(error_string.contains("1 minute"));
556 }
557
558 #[test]
559 fn test_concurrency_conflict_error() {
560 let error = Error::ConcurrencyConflict {
561 operation: "role_assignment".to_string(),
562 resource_id: "role_123".to_string(),
563 };
564
565 let error_string = format!("{}", error);
566 assert!(error_string.contains("Concurrency conflict"));
567 assert!(error_string.contains("role_assignment"));
568 assert!(error_string.contains("concurrent modification"));
569 }
570
571 #[test]
572 fn test_authentication_failed_error() {
573 let error = Error::AuthenticationFailed {
574 reason: "invalid credentials".to_string(),
575 subject_id: Some("user123".to_string()),
576 };
577
578 let error_string = format!("{}", error);
579 assert!(error_string.contains("Authentication failed"));
580 assert!(error_string.contains("invalid credentials"));
581 }
582
583 #[test]
584 fn test_authorization_failed_error() {
585 let error = Error::AuthorizationFailed {
586 subject: "alice".to_string(),
587 permission: "delete:documents".to_string(),
588 resource: "confidential.txt".to_string(),
589 required_roles: vec!["admin".to_string(), "editor".to_string()],
590 };
591
592 let error_string = format!("{}", error);
593 assert!(error_string.contains("Authorization failed"));
594 assert!(error_string.contains("alice"));
595 assert!(error_string.contains("delete:documents"));
596 assert!(error_string.contains("confidential.txt"));
597 }
598
599 #[cfg(feature = "persistence")]
600 #[test]
601 fn test_serialization_error_conversion() {
602 let json_error = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
604 let our_error: Error = json_error.into();
605
606 match &our_error {
607 Error::Serialization(msg) => {
608 assert!(msg.contains("expected value"));
609 }
610 _ => panic!("Expected Serialization error variant"),
611 }
612
613 let _cloned = our_error.clone();
615
616 let error_string = format!("{}", our_error);
617 assert!(error_string.contains("Serialization error"));
618 }
619}