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}