Skip to main content

chio_data_guards/
error.rs

1//! Error types for the Chio data layer guards.
2//!
3//! Data layer guards return [`Verdict::Deny`](chio_kernel::Verdict::Deny) on
4//! failure and emit a structured denial reason via tracing.  The reason types
5//! are exposed here so downstream integrations (for example the kernel's
6//! receipt builder or a policy test harness) can match on them structurally
7//! rather than string-parsing log lines.
8
9use thiserror::Error;
10
11/// Structured reason for a [`SqlQueryGuard`](crate::sql_guard::SqlQueryGuard)
12/// denial.
13///
14/// Every denial path in the SQL guard produces one of these variants.  The
15/// guard logs the reason via `tracing::warn!` and returns
16/// `Ok(Verdict::Deny)`; callers that need the reason programmatically can use
17/// [`SqlQueryGuard::analyze`](crate::sql_guard::SqlQueryGuard::analyze) which
18/// returns the reason alongside the verdict.
19#[derive(Clone, Debug, Error, PartialEq, Eq)]
20pub enum SqlGuardDenyReason {
21    /// The parsed operation class is not present in the guard's
22    /// `operation_allowlist` (fail-closed default).
23    #[error("sql operation '{operation}' is not allowed")]
24    OperationNotAllowed {
25        /// The parsed operation class (for example `SELECT`, `DROP`).
26        operation: String,
27    },
28
29    /// A referenced table is not present in the guard's `table_allowlist`.
30    #[error("table '{table}' is not in the allowlist")]
31    TableNotAllowed {
32        /// The offending table name, as parsed (case preserved for logs).
33        table: String,
34    },
35
36    /// A projected column is not present in the guard's `column_allowlist`
37    /// for the given table.
38    #[error("column '{column}' on table '{table}' is not in the allowlist")]
39    ColumnNotAllowed {
40        /// The table owning the column.
41        table: String,
42        /// The offending column name.
43        column: String,
44    },
45
46    /// The canonicalized WHERE/predicate text matched a denylist regex.
47    #[error("predicate matched denylist pattern '{pattern}'")]
48    PredicateDenylisted {
49        /// The regex pattern source that matched.
50        pattern: String,
51    },
52
53    /// A mutation (UPDATE, DELETE) lacked a WHERE clause.
54    #[error("{operation} without WHERE clause is not allowed")]
55    MissingWhereClause {
56        /// The mutation operation kind.
57        operation: String,
58    },
59
60    /// `sqlparser` could not parse the query.  Fail-closed.
61    #[error("sql parse error: {error}")]
62    ParseError {
63        /// Human readable parser error message.
64        error: String,
65    },
66
67    /// The guard config has no allowlists at all and `allow_all` is false.
68    /// Fail-closed default: an unconfigured guard denies every query.
69    #[error("sql guard has no configured allowlists and allow_all is false")]
70    NoConfig,
71
72    /// `SELECT *` attempted while a column allowlist is active.
73    #[error("SELECT * on table '{table}' is denied when a column allowlist is active")]
74    SelectStarDenied {
75        /// The offending table name.
76        table: String,
77    },
78}
79
80impl SqlGuardDenyReason {
81    /// Short stable tag suitable for metrics labels.
82    pub fn code(&self) -> &'static str {
83        match self {
84            Self::OperationNotAllowed { .. } => "operation_not_allowed",
85            Self::TableNotAllowed { .. } => "table_not_allowed",
86            Self::ColumnNotAllowed { .. } => "column_not_allowed",
87            Self::PredicateDenylisted { .. } => "predicate_denylisted",
88            Self::MissingWhereClause { .. } => "missing_where_clause",
89            Self::ParseError { .. } => "parse_error",
90            Self::NoConfig => "no_config",
91            Self::SelectStarDenied { .. } => "select_star_denied",
92        }
93    }
94}