1use std::fmt;
9
10#[derive(Debug, Clone)]
15#[non_exhaustive]
16pub enum SecurityError {
17 RateLimitExceeded {
24 retry_after: u64,
26 limit: usize,
28 window_secs: u64,
30 },
31
32 QueryTooDeep {
37 depth: usize,
39 max_depth: usize,
41 },
42
43 QueryTooComplex {
48 complexity: usize,
50 max_complexity: usize,
52 },
53
54 QueryTooLarge {
58 size: usize,
60 max_size: usize,
62 },
63
64 OriginNotAllowed(String),
66
67 MethodNotAllowed(String),
69
70 HeaderNotAllowed(String),
72
73 InvalidCSRFToken(String),
75
76 CSRFSessionMismatch,
78
79 AuditLogFailure(String),
84
85 SecurityConfigError(String),
89
90 TlsRequired {
95 detail: String,
97 },
98
99 TlsVersionTooOld {
104 current: crate::security::TlsVersion,
106 required: crate::security::TlsVersion,
108 },
109
110 MtlsRequired {
115 detail: String,
117 },
118
119 InvalidClientCert {
125 detail: String,
127 },
128
129 AuthRequired,
135
136 InvalidToken,
141
142 JwtSignatureInvalid,
147
148 JwtIssuerMismatch {
153 expected: String,
155 },
156
157 JwtAudienceMismatch {
162 expected: String,
164 },
165
166 TokenExpired {
171 expired_at: chrono::DateTime<chrono::Utc>,
173 },
174
175 TokenMissingClaim {
179 claim: String,
181 },
182
183 InvalidTokenAlgorithm {
188 algorithm: String,
190 },
191
192 TokenReplayed,
198
199 IntrospectionDisabled {
204 detail: String,
206 },
207
208 TooManyAliases {
213 alias_count: usize,
215 max_aliases: usize,
217 },
218
219 MalformedQuery(String),
223}
224
225pub(crate) type Result<T> = std::result::Result<T, SecurityError>;
229
230impl fmt::Display for SecurityError {
231 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232 match self {
233 Self::RateLimitExceeded {
234 retry_after,
235 limit,
236 window_secs,
237 } => {
238 write!(
239 f,
240 "Rate limit exceeded. Limit: {limit} per {window_secs} seconds. Retry after: {retry_after} seconds"
241 )
242 },
243 Self::QueryTooDeep { depth, max_depth } => {
244 write!(f, "Query too deep: {depth} levels (max: {max_depth})")
245 },
246 Self::QueryTooComplex {
247 complexity,
248 max_complexity,
249 } => {
250 write!(f, "Query too complex: {complexity} (max: {max_complexity})")
251 },
252 Self::QueryTooLarge { size, max_size } => {
253 write!(f, "Query too large: {size} bytes (max: {max_size})")
254 },
255 Self::OriginNotAllowed(origin) => {
256 write!(f, "CORS origin not allowed: {origin}")
257 },
258 Self::MethodNotAllowed(method) => {
259 write!(f, "CORS method not allowed: {method}")
260 },
261 Self::HeaderNotAllowed(header) => {
262 write!(f, "CORS header not allowed: {header}")
263 },
264 Self::InvalidCSRFToken(reason) => {
265 write!(f, "Invalid CSRF token: {reason}")
266 },
267 Self::CSRFSessionMismatch => {
268 write!(f, "CSRF token session mismatch")
269 },
270 Self::AuditLogFailure(reason) => {
271 write!(f, "Audit logging failed: {reason}")
272 },
273 Self::SecurityConfigError(reason) => {
274 write!(f, "Security configuration error: {reason}")
275 },
276 Self::TlsRequired { detail } => {
277 write!(f, "TLS/HTTPS required: {detail}")
278 },
279 Self::TlsVersionTooOld { current, required } => {
280 write!(f, "TLS version too old: {current} (required: {required})")
281 },
282 Self::MtlsRequired { detail } => {
283 write!(f, "Mutual TLS required: {detail}")
284 },
285 Self::InvalidClientCert { detail } => {
286 write!(f, "Invalid client certificate: {detail}")
287 },
288 Self::AuthRequired => {
289 write!(f, "Authentication required")
290 },
291 Self::InvalidToken => {
292 write!(f, "Invalid authentication token")
293 },
294 Self::JwtSignatureInvalid => {
295 write!(
296 f,
297 "JWT signature invalid — verify FRAISEQL_JWT_SECRET matches the secret \
298 used to sign the token"
299 )
300 },
301 Self::JwtIssuerMismatch { expected } => {
302 write!(
303 f,
304 "JWT issuer does not match expected '{expected}' — \
305 check [security.jwt] issuer in fraiseql.toml"
306 )
307 },
308 Self::JwtAudienceMismatch { expected } => {
309 write!(
310 f,
311 "JWT audience does not match expected '{expected}' — \
312 check [security.jwt] audience in fraiseql.toml"
313 )
314 },
315 Self::TokenExpired { expired_at } => {
316 write!(f, "Token expired at {expired_at}")
317 },
318 Self::TokenMissingClaim { claim } => {
319 write!(f, "Token missing required claim: {claim}")
320 },
321 Self::InvalidTokenAlgorithm { algorithm } => {
322 write!(f, "Invalid token algorithm: {algorithm}")
323 },
324 Self::TokenReplayed => {
325 write!(f, "Token has already been used (replay detected)")
326 },
327 Self::IntrospectionDisabled { detail } => {
328 write!(f, "Introspection disabled: {detail}")
329 },
330 Self::TooManyAliases {
331 alias_count,
332 max_aliases,
333 } => {
334 write!(f, "Query contains too many aliases: {alias_count} > {max_aliases}")
335 },
336 Self::MalformedQuery(msg) => {
337 write!(f, "Malformed GraphQL query: {msg}")
338 },
339 }
340 }
341}
342
343impl std::error::Error for SecurityError {
344 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
345 None
349 }
350}
351
352impl PartialEq for SecurityError {
353 #[allow(clippy::match_same_arms)] fn eq(&self, other: &Self) -> bool {
355 match (self, other) {
356 (
357 Self::RateLimitExceeded {
358 retry_after: r1,
359 limit: l1,
360 window_secs: w1,
361 },
362 Self::RateLimitExceeded {
363 retry_after: r2,
364 limit: l2,
365 window_secs: w2,
366 },
367 ) => r1 == r2 && l1 == l2 && w1 == w2,
368 (
369 Self::QueryTooDeep {
370 depth: d1,
371 max_depth: m1,
372 },
373 Self::QueryTooDeep {
374 depth: d2,
375 max_depth: m2,
376 },
377 ) => d1 == d2 && m1 == m2,
378 (
379 Self::QueryTooComplex {
380 complexity: c1,
381 max_complexity: m1,
382 },
383 Self::QueryTooComplex {
384 complexity: c2,
385 max_complexity: m2,
386 },
387 ) => c1 == c2 && m1 == m2,
388 (
389 Self::QueryTooLarge {
390 size: s1,
391 max_size: m1,
392 },
393 Self::QueryTooLarge {
394 size: s2,
395 max_size: m2,
396 },
397 ) => s1 == s2 && m1 == m2,
398 (Self::OriginNotAllowed(o1), Self::OriginNotAllowed(o2)) => o1 == o2,
399 (Self::MethodNotAllowed(m1), Self::MethodNotAllowed(m2)) => m1 == m2,
400 (Self::HeaderNotAllowed(h1), Self::HeaderNotAllowed(h2)) => h1 == h2,
401 (Self::InvalidCSRFToken(r1), Self::InvalidCSRFToken(r2)) => r1 == r2,
402 (Self::CSRFSessionMismatch, Self::CSRFSessionMismatch) => true,
403 (Self::AuditLogFailure(r1), Self::AuditLogFailure(r2)) => r1 == r2,
404 (Self::SecurityConfigError(r1), Self::SecurityConfigError(r2)) => r1 == r2,
405 (Self::TlsRequired { detail: d1 }, Self::TlsRequired { detail: d2 }) => d1 == d2,
406 (
407 Self::TlsVersionTooOld {
408 current: c1,
409 required: r1,
410 },
411 Self::TlsVersionTooOld {
412 current: c2,
413 required: r2,
414 },
415 ) => c1 == c2 && r1 == r2,
416 (Self::MtlsRequired { detail: d1 }, Self::MtlsRequired { detail: d2 }) => d1 == d2,
417 (Self::InvalidClientCert { detail: d1 }, Self::InvalidClientCert { detail: d2 }) => {
418 d1 == d2
419 },
420 (Self::AuthRequired, Self::AuthRequired) => true,
421 (Self::InvalidToken, Self::InvalidToken) => true,
422 (Self::JwtSignatureInvalid, Self::JwtSignatureInvalid) => true,
423 (
424 Self::JwtIssuerMismatch { expected: e1 },
425 Self::JwtIssuerMismatch { expected: e2 },
426 ) => e1 == e2,
427 (
428 Self::JwtAudienceMismatch { expected: e1 },
429 Self::JwtAudienceMismatch { expected: e2 },
430 ) => e1 == e2,
431 (Self::TokenExpired { expired_at: e1 }, Self::TokenExpired { expired_at: e2 }) => {
432 e1 == e2
433 },
434 (Self::TokenMissingClaim { claim: c1 }, Self::TokenMissingClaim { claim: c2 }) => {
435 c1 == c2
436 },
437 (
438 Self::InvalidTokenAlgorithm { algorithm: a1 },
439 Self::InvalidTokenAlgorithm { algorithm: a2 },
440 ) => a1 == a2,
441 (Self::TokenReplayed, Self::TokenReplayed) => true,
442 (
443 Self::IntrospectionDisabled { detail: d1 },
444 Self::IntrospectionDisabled { detail: d2 },
445 ) => d1 == d2,
446 (
447 Self::TooManyAliases {
448 alias_count: a1,
449 max_aliases: m1,
450 },
451 Self::TooManyAliases {
452 alias_count: a2,
453 max_aliases: m2,
454 },
455 ) => a1 == a2 && m1 == m2,
456 (Self::MalformedQuery(m1), Self::MalformedQuery(m2)) => m1 == m2,
457 _ => false,
458 }
459 }
460}
461
462impl Eq for SecurityError {}
463
464#[cfg(test)]
465mod tests {
466 use super::*;
467
468 #[test]
469 fn test_rate_limit_error_display() {
470 let err = SecurityError::RateLimitExceeded {
471 retry_after: 60,
472 limit: 100,
473 window_secs: 60,
474 };
475
476 assert!(err.to_string().contains("Rate limit exceeded"));
477 assert!(err.to_string().contains("100"));
478 assert!(err.to_string().contains("60"));
479 }
480
481 #[test]
482 fn test_query_too_deep_display() {
483 let err = SecurityError::QueryTooDeep {
484 depth: 20,
485 max_depth: 10,
486 };
487
488 assert!(err.to_string().contains("Query too deep"));
489 assert!(err.to_string().contains("20"));
490 assert!(err.to_string().contains("10"));
491 }
492
493 #[test]
494 fn test_query_too_complex_display() {
495 let err = SecurityError::QueryTooComplex {
496 complexity: 500,
497 max_complexity: 100,
498 };
499
500 assert!(err.to_string().contains("Query too complex"));
501 assert!(err.to_string().contains("500"));
502 assert!(err.to_string().contains("100"));
503 }
504
505 #[test]
506 fn test_query_too_large_display() {
507 let err = SecurityError::QueryTooLarge {
508 size: 100_000,
509 max_size: 10_000,
510 };
511
512 assert!(err.to_string().contains("Query too large"));
513 assert!(err.to_string().contains("100000"));
514 assert!(err.to_string().contains("10000"));
515 }
516
517 #[test]
518 fn test_cors_errors() {
519 let origin_err = SecurityError::OriginNotAllowed("https://evil.com".to_string());
520 assert!(origin_err.to_string().contains("CORS origin"));
521
522 let method_err = SecurityError::MethodNotAllowed("DELETE".to_string());
523 assert!(method_err.to_string().contains("CORS method"));
524
525 let header_err = SecurityError::HeaderNotAllowed("X-Custom".to_string());
526 assert!(header_err.to_string().contains("CORS header"));
527 }
528
529 #[test]
530 fn test_csrf_errors() {
531 let invalid = SecurityError::InvalidCSRFToken("expired".to_string());
532 assert!(invalid.to_string().contains("Invalid CSRF token"));
533
534 let mismatch = SecurityError::CSRFSessionMismatch;
535 assert!(mismatch.to_string().contains("session mismatch"));
536 }
537
538 #[test]
539 fn test_audit_error() {
540 let err = SecurityError::AuditLogFailure("connection timeout".to_string());
541 assert!(err.to_string().contains("Audit logging failed"));
542 }
543
544 #[test]
545 fn test_config_error() {
546 let err = SecurityError::SecurityConfigError("missing config key".to_string());
547 assert!(err.to_string().contains("Security configuration error"));
548 }
549
550 #[test]
551 fn test_error_equality() {
552 let err1 = SecurityError::QueryTooDeep {
553 depth: 20,
554 max_depth: 10,
555 };
556 let err2 = SecurityError::QueryTooDeep {
557 depth: 20,
558 max_depth: 10,
559 };
560 assert_eq!(err1, err2);
561
562 let err3 = SecurityError::QueryTooDeep {
563 depth: 30,
564 max_depth: 10,
565 };
566 assert_ne!(err1, err3);
567 }
568
569 #[test]
570 fn test_rate_limit_equality() {
571 let err1 = SecurityError::RateLimitExceeded {
572 retry_after: 60,
573 limit: 100,
574 window_secs: 60,
575 };
576 let err2 = SecurityError::RateLimitExceeded {
577 retry_after: 60,
578 limit: 100,
579 window_secs: 60,
580 };
581 assert_eq!(err1, err2);
582 }
583
584 #[test]
589 fn test_tls_required_error_display() {
590 let err = SecurityError::TlsRequired {
591 detail: "HTTPS required".to_string(),
592 };
593
594 assert!(err.to_string().contains("TLS/HTTPS required"));
595 assert!(err.to_string().contains("HTTPS required"));
596 }
597
598 #[test]
599 fn test_tls_version_too_old_error_display() {
600 use crate::security::tls_enforcer::TlsVersion;
601
602 let err = SecurityError::TlsVersionTooOld {
603 current: TlsVersion::V1_2,
604 required: TlsVersion::V1_3,
605 };
606
607 assert!(err.to_string().contains("TLS version too old"));
608 assert!(err.to_string().contains("1.2"));
609 assert!(err.to_string().contains("1.3"));
610 }
611
612 #[test]
613 fn test_mtls_required_error_display() {
614 let err = SecurityError::MtlsRequired {
615 detail: "Client certificate required".to_string(),
616 };
617
618 assert!(err.to_string().contains("Mutual TLS required"));
619 assert!(err.to_string().contains("Client certificate"));
620 }
621
622 #[test]
623 fn test_invalid_client_cert_error_display() {
624 let err = SecurityError::InvalidClientCert {
625 detail: "Certificate validation failed".to_string(),
626 };
627
628 assert!(err.to_string().contains("Invalid client certificate"));
629 assert!(err.to_string().contains("validation failed"));
630 }
631
632 #[test]
633 fn test_auth_required_error_display() {
634 let err = SecurityError::AuthRequired;
635 assert!(err.to_string().contains("Authentication required"));
636 }
637
638 #[test]
639 fn test_invalid_token_error_display() {
640 let err = SecurityError::InvalidToken;
641 assert!(err.to_string().contains("Invalid authentication token"));
642 }
643
644 #[test]
645 fn test_token_expired_error_display() {
646 use chrono::{Duration, Utc};
647
648 let expired_at = Utc::now() - Duration::hours(1);
649 let err = SecurityError::TokenExpired { expired_at };
650
651 assert!(err.to_string().contains("Token expired"));
652 }
653
654 #[test]
655 fn test_token_missing_claim_error_display() {
656 let err = SecurityError::TokenMissingClaim {
657 claim: "sub".to_string(),
658 };
659
660 assert!(err.to_string().contains("Token missing required claim"));
661 assert!(err.to_string().contains("sub"));
662 }
663
664 #[test]
665 fn test_invalid_token_algorithm_error_display() {
666 let err = SecurityError::InvalidTokenAlgorithm {
667 algorithm: "HS256".to_string(),
668 };
669
670 assert!(err.to_string().contains("Invalid token algorithm"));
671 assert!(err.to_string().contains("HS256"));
672 }
673
674 #[test]
675 fn test_introspection_disabled_error_display() {
676 let err = SecurityError::IntrospectionDisabled {
677 detail: "Introspection not allowed in production".to_string(),
678 };
679
680 assert!(err.to_string().contains("Introspection disabled"));
681 assert!(err.to_string().contains("production"));
682 }
683
684 #[test]
689 fn test_tls_required_equality() {
690 let err1 = SecurityError::TlsRequired {
691 detail: "test".to_string(),
692 };
693 let err2 = SecurityError::TlsRequired {
694 detail: "test".to_string(),
695 };
696 assert_eq!(err1, err2);
697
698 let err3 = SecurityError::TlsRequired {
699 detail: "different".to_string(),
700 };
701 assert_ne!(err1, err3);
702 }
703
704 #[test]
705 fn test_tls_version_too_old_equality() {
706 use crate::security::tls_enforcer::TlsVersion;
707
708 let err1 = SecurityError::TlsVersionTooOld {
709 current: TlsVersion::V1_2,
710 required: TlsVersion::V1_3,
711 };
712 let err2 = SecurityError::TlsVersionTooOld {
713 current: TlsVersion::V1_2,
714 required: TlsVersion::V1_3,
715 };
716 assert_eq!(err1, err2);
717
718 let err3 = SecurityError::TlsVersionTooOld {
719 current: TlsVersion::V1_1,
720 required: TlsVersion::V1_3,
721 };
722 assert_ne!(err1, err3);
723 }
724
725 #[test]
726 fn test_mtls_required_equality() {
727 let err1 = SecurityError::MtlsRequired {
728 detail: "test".to_string(),
729 };
730 let err2 = SecurityError::MtlsRequired {
731 detail: "test".to_string(),
732 };
733 assert_eq!(err1, err2);
734 }
735
736 #[test]
737 fn test_invalid_token_equality() {
738 assert_eq!(SecurityError::InvalidToken, SecurityError::InvalidToken);
739 }
740
741 #[test]
742 fn test_auth_required_equality() {
743 assert_eq!(SecurityError::AuthRequired, SecurityError::AuthRequired);
744 }
745}