modkit_db/secure/error.rs
1use uuid::Uuid;
2
3/// Errors that can occur during scoped query execution.
4#[derive(thiserror::Error, Debug)]
5pub enum ScopeError {
6 /// Database error occurred during query execution.
7 #[error("database error: {0}")]
8 Db(#[from] sea_orm::DbErr),
9
10 /// Invalid scope configuration.
11 #[error("invalid scope: {0}")]
12 Invalid(&'static str),
13
14 /// Tenant isolation violation: `tenant_id` is not included in the current scope.
15 #[error("access denied: tenant_id not present in security scope ({tenant_id})")]
16 TenantNotInScope { tenant_id: Uuid },
17
18 /// Operation denied - entity not accessible in current security scope.
19 #[error("access denied: {0}")]
20 Denied(&'static str),
21}
22
23impl ScopeError {
24 /// Returns `true` if this error wraps a unique-constraint violation.
25 #[must_use]
26 pub fn is_unique_violation(&self) -> bool {
27 match self {
28 Self::Db(db_err) => is_unique_violation(db_err),
29 _ => false,
30 }
31 }
32}
33
34/// Check whether a `sea_orm::DbErr` represents a unique-constraint violation.
35///
36/// First tries `SeaORM`'s built-in `sql_err()` detection (SQLSTATE-based).
37/// Falls back to string matching on the error message for cases where
38/// `sql_err()` fails to classify the error (e.g. certain connection proxies
39/// or driver wrappers that strip the SQLSTATE code).
40///
41/// Recognized patterns across backends:
42/// - **Postgres** SQLSTATE `23505` — "`unique_violation`" / "duplicate key"
43/// - **`SQLite`** extended code `2067` — "UNIQUE constraint failed"
44/// - **`MySQL`** error `1062` — "Duplicate entry"
45#[must_use]
46pub fn is_unique_violation(err: &sea_orm::DbErr) -> bool {
47 // Fast path: SeaORM parsed the SQLSTATE / vendor code correctly.
48 if matches!(
49 err.sql_err(),
50 Some(sea_orm::SqlErr::UniqueConstraintViolation(_))
51 ) {
52 return true;
53 }
54
55 // Fallback: string-based detection for wrapped / proxied errors.
56 let msg = err.to_string().to_lowercase();
57 msg.contains("unique constraint")
58 || msg.contains("duplicate key")
59 || msg.contains("unique_violation")
60 || msg.contains("duplicate entry")
61 || msg.contains("unique constraint failed")
62}