Skip to main content

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}