fsqlite_func/authorizer.rs
1//! Authorizer callback trait (§9.4).
2//!
3//! The authorizer is consulted during **statement compilation** (not execution)
4//! to allow or deny access to database objects and operations. This enables
5//! sandboxing untrusted SQL.
6//!
7//! The [`Authorizer`] trait mirrors SQLite's `sqlite3_set_authorizer` API:
8//! each callback receives an [`AuthAction`] code plus up to four optional
9//! string parameters providing context (table name, column name, database
10//! name, trigger name).
11
12/// SQL operation being authorized.
13///
14/// Covers all DDL, DML, DROP, and miscellaneous operations that SQLite
15/// authorizes during statement compilation.
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub enum AuthAction {
18 // -- DDL (8) --
19 /// CREATE INDEX (arg1=index name, arg2=table name)
20 CreateIndex,
21 /// CREATE TABLE (arg1=table name, arg2=None)
22 CreateTable,
23 /// CREATE TEMP INDEX (arg1=index name, arg2=table name)
24 CreateTempIndex,
25 /// CREATE TEMP TABLE (arg1=table name, arg2=None)
26 CreateTempTable,
27 /// CREATE TEMP TRIGGER (arg1=trigger name, arg2=table name)
28 CreateTempTrigger,
29 /// CREATE TEMP VIEW (arg1=view name, arg2=None)
30 CreateTempView,
31 /// CREATE TRIGGER (arg1=trigger name, arg2=table name)
32 CreateTrigger,
33 /// CREATE VIEW (arg1=view name, arg2=None)
34 CreateView,
35
36 // -- DML (5) --
37 /// DELETE (arg1=table name, arg2=None)
38 Delete,
39 /// INSERT (arg1=table name, arg2=None)
40 Insert,
41 /// SELECT (arg1=None, arg2=None)
42 Select,
43 /// UPDATE (arg1=table name, arg2=column name)
44 Update,
45 /// READ (arg1=table name, arg2=column name)
46 Read,
47
48 // -- DROP (8) --
49 /// DROP INDEX (arg1=index name, arg2=table name)
50 DropIndex,
51 /// DROP TABLE (arg1=table name, arg2=None)
52 DropTable,
53 /// DROP TEMP INDEX (arg1=index name, arg2=table name)
54 DropTempIndex,
55 /// DROP TEMP TABLE (arg1=table name, arg2=None)
56 DropTempTable,
57 /// DROP TEMP TRIGGER (arg1=trigger name, arg2=table name)
58 DropTempTrigger,
59 /// DROP TEMP VIEW (arg1=view name, arg2=None)
60 DropTempView,
61 /// DROP TRIGGER (arg1=trigger name, arg2=table name)
62 DropTrigger,
63 /// DROP VIEW (arg1=view name, arg2=None)
64 DropView,
65
66 // -- Miscellaneous (12) --
67 /// PRAGMA (arg1=pragma name, arg2=pragma arg or None)
68 Pragma,
69 /// Transaction control (arg1=operation e.g. "BEGIN", arg2=None)
70 Transaction,
71 /// ATTACH (arg1=filename, arg2=None)
72 Attach,
73 /// DETACH (arg1=database name, arg2=None)
74 Detach,
75 /// ALTER TABLE (arg1=database name, arg2=table name)
76 AlterTable,
77 /// REINDEX (arg1=index name, arg2=None)
78 Reindex,
79 /// ANALYZE (arg1=table name, arg2=None)
80 Analyze,
81 /// CREATE VIRTUAL TABLE (arg1=table name, arg2=module name)
82 CreateVtable,
83 /// DROP VIRTUAL TABLE (arg1=table name, arg2=module name)
84 DropVtable,
85 /// Function invocation (arg1=None, arg2=function name)
86 Function,
87 /// SAVEPOINT (arg1=operation e.g. "BEGIN"/"RELEASE"/"ROLLBACK", arg2=name)
88 Savepoint,
89 /// Recursive query (arg1=None, arg2=None)
90 Recursive,
91}
92
93/// Result of an authorization check.
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum AuthResult {
96 /// Allow the operation to proceed.
97 Ok,
98 /// Deny the operation with an authorization error.
99 Deny,
100 /// Silently replace the result with NULL (for Read) or skip the
101 /// operation where semantics permit.
102 Ignore,
103}
104
105/// Statement authorizer callback (§9.4).
106///
107/// Called during SQL **compilation** (not execution) to approve or deny each
108/// operation. Used for sandboxing untrusted SQL.
109///
110/// # Parameters
111///
112/// - `action`: The type of SQL operation being authorized.
113/// - `arg1`/`arg2`: Context-dependent string parameters (table name, column
114/// name, index name, etc.). The meaning depends on the [`AuthAction`].
115/// - `db_name`: The database name (e.g. `"main"`, `"temp"`, attached name).
116/// - `trigger`: If the operation originates inside a trigger, this is the
117/// trigger name. `None` for top-level SQL.
118pub trait Authorizer: Send + Sync {
119 /// Return allow/deny/ignore for the given action.
120 fn authorize(
121 &self,
122 action: AuthAction,
123 arg1: Option<&str>,
124 arg2: Option<&str>,
125 db_name: Option<&str>,
126 trigger: Option<&str>,
127 ) -> AuthResult;
128}
129
130// Keep the old names as type aliases for a smooth transition in
131// downstream code that may reference them. Since we don't care about
132// backwards compatibility (AGENTS.md), these exist purely for the
133// re-export in lib.rs to compile.
134
135/// Alias for [`AuthAction`] (legacy name).
136pub type AuthorizerAction = AuthAction;
137
138/// Alias for [`AuthResult`] (legacy name).
139pub type AuthorizerDecision = AuthResult;
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144
145 // -- Read-only sandboxing authorizer --
146
147 struct ReadOnlyAuthorizer;
148
149 impl Authorizer for ReadOnlyAuthorizer {
150 fn authorize(
151 &self,
152 action: AuthAction,
153 _arg1: Option<&str>,
154 _arg2: Option<&str>,
155 _db_name: Option<&str>,
156 _trigger: Option<&str>,
157 ) -> AuthResult {
158 match action {
159 AuthAction::Read | AuthAction::Select => AuthResult::Ok,
160 _ => AuthResult::Deny,
161 }
162 }
163 }
164
165 #[test]
166 fn test_authorizer_allow_select() {
167 let auth = ReadOnlyAuthorizer;
168 assert_eq!(
169 auth.authorize(AuthAction::Select, None, None, Some("main"), None),
170 AuthResult::Ok
171 );
172 }
173
174 #[test]
175 fn test_authorizer_deny_insert() {
176 let auth = ReadOnlyAuthorizer;
177 assert_eq!(
178 auth.authorize(AuthAction::Insert, Some("users"), None, Some("main"), None),
179 AuthResult::Deny
180 );
181 }
182
183 #[test]
184 fn test_authorizer_ignore_read() {
185 // An authorizer that hides a specific column by returning Ignore.
186 struct ColumnHider;
187
188 impl Authorizer for ColumnHider {
189 fn authorize(
190 &self,
191 action: AuthAction,
192 _arg1: Option<&str>,
193 arg2: Option<&str>,
194 _db_name: Option<&str>,
195 _trigger: Option<&str>,
196 ) -> AuthResult {
197 if action == AuthAction::Read && arg2 == Some("secret") {
198 return AuthResult::Ignore;
199 }
200 AuthResult::Ok
201 }
202 }
203
204 let auth = ColumnHider;
205 // Reading the secret column -> Ignore (NULL replacement)
206 assert_eq!(
207 auth.authorize(
208 AuthAction::Read,
209 Some("users"),
210 Some("secret"),
211 Some("main"),
212 None
213 ),
214 AuthResult::Ignore
215 );
216 // Reading a normal column -> Ok
217 assert_eq!(
218 auth.authorize(
219 AuthAction::Read,
220 Some("users"),
221 Some("name"),
222 Some("main"),
223 None
224 ),
225 AuthResult::Ok
226 );
227 }
228
229 #[test]
230 fn test_authorizer_trigger_context() {
231 struct TriggerAwareAuthorizer;
232
233 impl Authorizer for TriggerAwareAuthorizer {
234 fn authorize(
235 &self,
236 _action: AuthAction,
237 _arg1: Option<&str>,
238 _arg2: Option<&str>,
239 _db_name: Option<&str>,
240 trigger: Option<&str>,
241 ) -> AuthResult {
242 if trigger.is_some() {
243 return AuthResult::Deny;
244 }
245 AuthResult::Ok
246 }
247 }
248
249 let auth = TriggerAwareAuthorizer;
250 // Top-level SQL -> allowed
251 assert_eq!(
252 auth.authorize(AuthAction::Insert, Some("t"), None, Some("main"), None),
253 AuthResult::Ok
254 );
255 // Inside trigger -> denied
256 assert_eq!(
257 auth.authorize(
258 AuthAction::Insert,
259 Some("t"),
260 None,
261 Some("main"),
262 Some("trg_audit")
263 ),
264 AuthResult::Deny
265 );
266 }
267
268 #[test]
269 #[allow(clippy::too_many_lines)]
270 fn test_auth_action_all_variants() {
271 // Verify every AuthAction variant can be constructed and matched.
272 let actions = [
273 AuthAction::CreateIndex,
274 AuthAction::CreateTable,
275 AuthAction::CreateTempIndex,
276 AuthAction::CreateTempTable,
277 AuthAction::CreateTempTrigger,
278 AuthAction::CreateTempView,
279 AuthAction::CreateTrigger,
280 AuthAction::CreateView,
281 AuthAction::Delete,
282 AuthAction::Insert,
283 AuthAction::Select,
284 AuthAction::Update,
285 AuthAction::Read,
286 AuthAction::DropIndex,
287 AuthAction::DropTable,
288 AuthAction::DropTempIndex,
289 AuthAction::DropTempTable,
290 AuthAction::DropTempTrigger,
291 AuthAction::DropTempView,
292 AuthAction::DropTrigger,
293 AuthAction::DropView,
294 AuthAction::Pragma,
295 AuthAction::Transaction,
296 AuthAction::Attach,
297 AuthAction::Detach,
298 AuthAction::AlterTable,
299 AuthAction::Reindex,
300 AuthAction::Analyze,
301 AuthAction::CreateVtable,
302 AuthAction::DropVtable,
303 AuthAction::Function,
304 AuthAction::Savepoint,
305 AuthAction::Recursive,
306 ];
307
308 // All 33 variants are constructible.
309 assert_eq!(actions.len(), 33);
310
311 // Each is pattern-matchable (exhaustive).
312 for a in &actions {
313 let _ = match a {
314 AuthAction::CreateIndex
315 | AuthAction::CreateTable
316 | AuthAction::CreateTempIndex
317 | AuthAction::CreateTempTable
318 | AuthAction::CreateTempTrigger
319 | AuthAction::CreateTempView
320 | AuthAction::CreateTrigger
321 | AuthAction::CreateView
322 | AuthAction::Delete
323 | AuthAction::Insert
324 | AuthAction::Select
325 | AuthAction::Update
326 | AuthAction::Read
327 | AuthAction::DropIndex
328 | AuthAction::DropTable
329 | AuthAction::DropTempIndex
330 | AuthAction::DropTempTable
331 | AuthAction::DropTempTrigger
332 | AuthAction::DropTempView
333 | AuthAction::DropTrigger
334 | AuthAction::DropView
335 | AuthAction::Pragma
336 | AuthAction::Transaction
337 | AuthAction::Attach
338 | AuthAction::Detach
339 | AuthAction::AlterTable
340 | AuthAction::Reindex
341 | AuthAction::Analyze
342 | AuthAction::CreateVtable
343 | AuthAction::DropVtable
344 | AuthAction::Function
345 | AuthAction::Savepoint
346 | AuthAction::Recursive => true,
347 };
348 }
349 }
350
351 #[test]
352 fn test_authorizer_send_sync() {
353 fn assert_send_sync<T: Send + Sync>() {}
354 assert_send_sync::<ReadOnlyAuthorizer>();
355 }
356}