Skip to main content

qail_core/access/
error.rs

1use std::collections::BTreeSet;
2
3use crate::ast::Action;
4
5use super::model::AccessOperation;
6
7/// Access policy file loading failure.
8#[derive(Debug)]
9pub enum AccessPolicyLoadError {
10    /// Filesystem read failure.
11    Read(std::io::Error),
12    /// TOML parse failure.
13    Toml(toml::de::Error),
14    /// JSON parse failure.
15    Json(serde_json::Error),
16    /// File extension is not supported.
17    UnsupportedExtension(String),
18}
19
20impl std::fmt::Display for AccessPolicyLoadError {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        match self {
23            Self::Read(err) => write!(f, "failed to read access policy: {err}"),
24            Self::Toml(err) => write!(f, "failed to parse TOML access policy: {err}"),
25            Self::Json(err) => write!(f, "failed to parse JSON access policy: {err}"),
26            Self::UnsupportedExtension(extension) if extension.is_empty() => {
27                write!(f, "access policy file must use .toml or .json extension")
28            }
29            Self::UnsupportedExtension(extension) => {
30                write!(
31                    f,
32                    "unsupported access policy extension '.{extension}' (expected .toml or .json)"
33                )
34            }
35        }
36    }
37}
38
39impl std::error::Error for AccessPolicyLoadError {
40    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
41        match self {
42            Self::Read(err) => Some(err),
43            Self::Toml(err) => Some(err),
44            Self::Json(err) => Some(err),
45            Self::UnsupportedExtension(_) => None,
46        }
47    }
48}
49
50/// Access check failure.
51#[derive(Debug, Clone, PartialEq, Eq)]
52pub struct AccessError {
53    /// Table being checked.
54    pub table: String,
55    /// Operation being checked, if known.
56    pub operation: Option<AccessOperation>,
57    /// Specific failure reason.
58    pub kind: AccessErrorKind,
59}
60
61impl AccessError {
62    pub(super) fn new(
63        table: String,
64        operation: Option<AccessOperation>,
65        kind: AccessErrorKind,
66    ) -> Self {
67        Self {
68            table,
69            operation,
70            kind,
71        }
72    }
73}
74
75impl std::fmt::Display for AccessError {
76    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77        match &self.kind {
78            AccessErrorKind::NoPolicy => {
79                write!(f, "no access policy allows table '{}'", self.table)
80            }
81            AccessErrorKind::UnsupportedAction(action) => {
82                write!(f, "action {action:?} is not supported by access policy")
83            }
84            AccessErrorKind::OperationDenied => write!(
85                f,
86                "operation {:?} is denied on table '{}'",
87                self.operation, self.table
88            ),
89            AccessErrorKind::MissingRole { required } => write!(
90                f,
91                "table '{}' requires one of roles {:?}",
92                self.table, required
93            ),
94            AccessErrorKind::MissingScope { required } => {
95                write!(f, "table '{}' requires scopes {:?}", self.table, required)
96            }
97            AccessErrorKind::ColumnDenied { column } => write!(
98                f,
99                "column '{}' is denied for operation {:?} on table '{}'",
100                column, self.operation, self.table
101            ),
102            AccessErrorKind::WildcardProjectionDenied => write!(
103                f,
104                "wildcard projection is denied by column policy on table '{}'",
105                self.table
106            ),
107            AccessErrorKind::UnsupportedColumnExpression { context } => write!(
108                f,
109                "{} contains an expression that cannot be checked by column policy on table '{}'",
110                context, self.table
111            ),
112            AccessErrorKind::ExplicitWriteColumnsRequired => write!(
113                f,
114                "operation {:?} on table '{}' requires explicit write columns",
115                self.operation, self.table
116            ),
117            AccessErrorKind::JoinedTableColumnPolicyUnsupported => write!(
118                f,
119                "joined table '{}' has column policy that cannot be enforced in a flat join",
120                self.table
121            ),
122            AccessErrorKind::SourceTableColumnPolicyUnsupported => write!(
123                f,
124                "source table '{}' has column policy that cannot be enforced without an explicit source query",
125                self.table
126            ),
127            AccessErrorKind::AuxiliaryTableColumnPolicyUnsupported => write!(
128                f,
129                "auxiliary table '{}' has column policy that cannot be enforced in UPDATE FROM or DELETE USING",
130                self.table
131            ),
132            AccessErrorKind::CteMutationUnsupported => {
133                write!(f, "CTE relation '{}' cannot be mutated", self.table)
134            }
135            AccessErrorKind::EmptyTable => write!(f, "command has no target table"),
136        }
137    }
138}
139
140impl std::error::Error for AccessError {}
141
142/// Specific access denial reason.
143#[derive(Debug, Clone, PartialEq, Eq)]
144pub enum AccessErrorKind {
145    /// No matching table policy exists and default decision is deny.
146    NoPolicy,
147    /// Command action is not a runtime data action covered by this policy.
148    UnsupportedAction(Action),
149    /// The table policy does not allow this operation.
150    OperationDenied,
151    /// Required role gate failed.
152    MissingRole {
153        /// Accepted roles.
154        required: BTreeSet<String>,
155    },
156    /// Required scope gate failed.
157    MissingScope {
158        /// Required scopes.
159        required: BTreeSet<String>,
160    },
161    /// Column is not allowed by the relevant column rule.
162    ColumnDenied {
163        /// Normalized column name.
164        column: String,
165    },
166    /// `*` or `table.*` cannot be checked against a restrictive column rule.
167    WildcardProjectionDenied,
168    /// A projection expression cannot be mapped to a concrete column.
169    UnsupportedColumnExpression {
170        /// Human-readable context.
171        context: &'static str,
172    },
173    /// A write used positional or implicit payloads under a restrictive write rule.
174    ExplicitWriteColumnsRequired,
175    /// Joined table column policies cannot be enforced by this checker.
176    JoinedTableColumnPolicyUnsupported,
177    /// Source table column policies cannot be enforced without an explicit source projection.
178    SourceTableColumnPolicyUnsupported,
179    /// UPDATE FROM / DELETE USING table column policies cannot be enforced by this checker.
180    AuxiliaryTableColumnPolicyUnsupported,
181    /// CTE aliases are read-only derived relations.
182    CteMutationUnsupported,
183    /// Command did not carry a target table.
184    EmptyTable,
185}