mirror of
				https://github.com/isar/rusqlite.git
				synced 2025-10-31 05:48:56 +08:00 
			
		
		
		
	Add support for authorizer hook (#946)
Co-authored-by: Thom Chiovoloni <chiovolonit@gmail.com>
This commit is contained in:
		
							
								
								
									
										451
									
								
								src/hooks.rs
									
									
									
									
									
								
							
							
						
						
									
										451
									
								
								src/hooks.rs
									
									
									
									
									
								
							| @@ -37,6 +37,306 @@ impl From<i32> for Action { | ||||
|     } | ||||
| } | ||||
|  | ||||
| /// `feature = "hooks"` The context recieved by an authorizer hook. | ||||
| /// | ||||
| /// See <https://sqlite.org/c3ref/set_authorizer.html> for more info. | ||||
| #[derive(Clone, Copy, Debug, PartialEq)] | ||||
| pub struct AuthContext<'c> { | ||||
|     // The action to be authorized. | ||||
|     pub action: AuthAction<'c>, | ||||
|  | ||||
|     /// The database name, if applicable. | ||||
|     pub database_name: Option<&'c str>, | ||||
|  | ||||
|     // The inner-most trigger or view responsible for the access attempt. | ||||
|     // `None` if the access attempt was made by top-level SQL code. | ||||
|     pub accessor: Option<&'c str>, | ||||
| } | ||||
|  | ||||
| /// `feature = "hooks"` Actions and arguments found within a statement during | ||||
| /// preparation. | ||||
| /// | ||||
| /// See <https://sqlite.org/c3ref/c_alter_table.html> for more info. | ||||
| #[derive(Clone, Copy, Debug, PartialEq)] | ||||
| #[non_exhaustive] | ||||
| #[allow(missing_docs)] | ||||
| pub enum AuthAction<'c> { | ||||
|     /// This variant is not normally produced by SQLite. You may encounter it | ||||
|     // if you're using a different version than what's supported by this library. | ||||
|     Unknown { | ||||
|         /// The unknown authorization action code. | ||||
|         code: i32, | ||||
|         /// The third arg to the authorizer callback. | ||||
|         arg1: Option<&'c str>, | ||||
|         /// The fourth arg to the authorizer callback. | ||||
|         arg2: Option<&'c str>, | ||||
|     }, | ||||
|     CreateIndex { | ||||
|         index_name: &'c str, | ||||
|         table_name: &'c str, | ||||
|     }, | ||||
|     CreateTable { | ||||
|         table_name: &'c str, | ||||
|     }, | ||||
|     CreateTempIndex { | ||||
|         index_name: &'c str, | ||||
|         table_name: &'c str, | ||||
|     }, | ||||
|     CreateTempTable { | ||||
|         table_name: &'c str, | ||||
|     }, | ||||
|     CreateTempTrigger { | ||||
|         trigger_name: &'c str, | ||||
|         table_name: &'c str, | ||||
|     }, | ||||
|     CreateTempView { | ||||
|         view_name: &'c str, | ||||
|     }, | ||||
|     CreateTrigger { | ||||
|         trigger_name: &'c str, | ||||
|         table_name: &'c str, | ||||
|     }, | ||||
|     CreateView { | ||||
|         view_name: &'c str, | ||||
|     }, | ||||
|     Delete { | ||||
|         table_name: &'c str, | ||||
|     }, | ||||
|     DropIndex { | ||||
|         index_name: &'c str, | ||||
|         table_name: &'c str, | ||||
|     }, | ||||
|     DropTable { | ||||
|         table_name: &'c str, | ||||
|     }, | ||||
|     DropTempIndex { | ||||
|         index_name: &'c str, | ||||
|         table_name: &'c str, | ||||
|     }, | ||||
|     DropTempTable { | ||||
|         table_name: &'c str, | ||||
|     }, | ||||
|     DropTempTrigger { | ||||
|         trigger_name: &'c str, | ||||
|         table_name: &'c str, | ||||
|     }, | ||||
|     DropTempView { | ||||
|         view_name: &'c str, | ||||
|     }, | ||||
|     DropTrigger { | ||||
|         trigger_name: &'c str, | ||||
|         table_name: &'c str, | ||||
|     }, | ||||
|     DropView { | ||||
|         view_name: &'c str, | ||||
|     }, | ||||
|     Insert { | ||||
|         table_name: &'c str, | ||||
|     }, | ||||
|     Pragma { | ||||
|         pragma_name: &'c str, | ||||
|         /// The pragma value, if present (e.g., `PRAGMA name = value;`). | ||||
|         pragma_value: Option<&'c str>, | ||||
|     }, | ||||
|     Read { | ||||
|         table_name: &'c str, | ||||
|         column_name: &'c str, | ||||
|     }, | ||||
|     Select, | ||||
|     Transaction { | ||||
|         operation: TransactionOperation, | ||||
|     }, | ||||
|     Update { | ||||
|         table_name: &'c str, | ||||
|         column_name: &'c str, | ||||
|     }, | ||||
|     Attach { | ||||
|         filename: &'c str, | ||||
|     }, | ||||
|     Detach { | ||||
|         database_name: &'c str, | ||||
|     }, | ||||
|     AlterTable { | ||||
|         database_name: &'c str, | ||||
|         table_name: &'c str, | ||||
|     }, | ||||
|     Reindex { | ||||
|         index_name: &'c str, | ||||
|     }, | ||||
|     Analyze { | ||||
|         table_name: &'c str, | ||||
|     }, | ||||
|     CreateVtable { | ||||
|         table_name: &'c str, | ||||
|         module_name: &'c str, | ||||
|     }, | ||||
|     DropVtable { | ||||
|         table_name: &'c str, | ||||
|         module_name: &'c str, | ||||
|     }, | ||||
|     Function { | ||||
|         function_name: &'c str, | ||||
|     }, | ||||
|     Savepoint { | ||||
|         operation: TransactionOperation, | ||||
|         savepoint_name: &'c str, | ||||
|     }, | ||||
|     #[cfg(feature = "modern_sqlite")] | ||||
|     Recursive, | ||||
| } | ||||
|  | ||||
| impl<'c> AuthAction<'c> { | ||||
|     fn from_raw(code: i32, arg1: Option<&'c str>, arg2: Option<&'c str>) -> Self { | ||||
|         match (code, arg1, arg2) { | ||||
|             (ffi::SQLITE_CREATE_INDEX, Some(index_name), Some(table_name)) => Self::CreateIndex { | ||||
|                 index_name, | ||||
|                 table_name, | ||||
|             }, | ||||
|             (ffi::SQLITE_CREATE_TABLE, Some(table_name), _) => Self::CreateTable { table_name }, | ||||
|             (ffi::SQLITE_CREATE_TEMP_INDEX, Some(index_name), Some(table_name)) => { | ||||
|                 Self::CreateTempIndex { | ||||
|                     index_name, | ||||
|                     table_name, | ||||
|                 } | ||||
|             } | ||||
|             (ffi::SQLITE_CREATE_TEMP_TABLE, Some(table_name), _) => { | ||||
|                 Self::CreateTempTable { table_name } | ||||
|             } | ||||
|             (ffi::SQLITE_CREATE_TEMP_TRIGGER, Some(trigger_name), Some(table_name)) => { | ||||
|                 Self::CreateTempTrigger { | ||||
|                     trigger_name, | ||||
|                     table_name, | ||||
|                 } | ||||
|             } | ||||
|             (ffi::SQLITE_CREATE_TEMP_VIEW, Some(view_name), _) => { | ||||
|                 Self::CreateTempView { view_name } | ||||
|             } | ||||
|             (ffi::SQLITE_CREATE_TRIGGER, Some(trigger_name), Some(table_name)) => { | ||||
|                 Self::CreateTrigger { | ||||
|                     trigger_name, | ||||
|                     table_name, | ||||
|                 } | ||||
|             } | ||||
|             (ffi::SQLITE_CREATE_VIEW, Some(view_name), _) => Self::CreateView { view_name }, | ||||
|             (ffi::SQLITE_DELETE, Some(table_name), None) => Self::Delete { table_name }, | ||||
|             (ffi::SQLITE_DROP_INDEX, Some(index_name), Some(table_name)) => Self::DropIndex { | ||||
|                 index_name, | ||||
|                 table_name, | ||||
|             }, | ||||
|             (ffi::SQLITE_DROP_TABLE, Some(table_name), _) => Self::DropTable { table_name }, | ||||
|             (ffi::SQLITE_DROP_TEMP_INDEX, Some(index_name), Some(table_name)) => { | ||||
|                 Self::DropTempIndex { | ||||
|                     index_name, | ||||
|                     table_name, | ||||
|                 } | ||||
|             } | ||||
|             (ffi::SQLITE_DROP_TEMP_TABLE, Some(table_name), _) => { | ||||
|                 Self::DropTempTable { table_name } | ||||
|             } | ||||
|             (ffi::SQLITE_DROP_TEMP_TRIGGER, Some(trigger_name), Some(table_name)) => { | ||||
|                 Self::DropTempTrigger { | ||||
|                     trigger_name, | ||||
|                     table_name, | ||||
|                 } | ||||
|             } | ||||
|             (ffi::SQLITE_DROP_TEMP_VIEW, Some(view_name), _) => Self::DropTempView { view_name }, | ||||
|             (ffi::SQLITE_DROP_TRIGGER, Some(trigger_name), Some(table_name)) => Self::DropTrigger { | ||||
|                 trigger_name, | ||||
|                 table_name, | ||||
|             }, | ||||
|             (ffi::SQLITE_DROP_VIEW, Some(view_name), _) => Self::DropView { view_name }, | ||||
|             (ffi::SQLITE_INSERT, Some(table_name), _) => Self::Insert { table_name }, | ||||
|             (ffi::SQLITE_PRAGMA, Some(pragma_name), pragma_value) => Self::Pragma { | ||||
|                 pragma_name, | ||||
|                 pragma_value, | ||||
|             }, | ||||
|             (ffi::SQLITE_READ, Some(table_name), Some(column_name)) => Self::Read { | ||||
|                 table_name, | ||||
|                 column_name, | ||||
|             }, | ||||
|             (ffi::SQLITE_SELECT, _, _) => Self::Select, | ||||
|             (ffi::SQLITE_TRANSACTION, Some(operation_str), _) => Self::Transaction { | ||||
|                 operation: TransactionOperation::from_str(operation_str), | ||||
|             }, | ||||
|             (ffi::SQLITE_UPDATE, Some(table_name), Some(column_name)) => Self::Update { | ||||
|                 table_name, | ||||
|                 column_name, | ||||
|             }, | ||||
|             (ffi::SQLITE_ATTACH, Some(filename), _) => Self::Attach { filename }, | ||||
|             (ffi::SQLITE_DETACH, Some(database_name), _) => Self::Detach { database_name }, | ||||
|             (ffi::SQLITE_ALTER_TABLE, Some(database_name), Some(table_name)) => Self::AlterTable { | ||||
|                 database_name, | ||||
|                 table_name, | ||||
|             }, | ||||
|             (ffi::SQLITE_REINDEX, Some(index_name), _) => Self::Reindex { index_name }, | ||||
|             (ffi::SQLITE_ANALYZE, Some(table_name), _) => Self::Analyze { table_name }, | ||||
|             (ffi::SQLITE_CREATE_VTABLE, Some(table_name), Some(module_name)) => { | ||||
|                 Self::CreateVtable { | ||||
|                     table_name, | ||||
|                     module_name, | ||||
|                 } | ||||
|             } | ||||
|             (ffi::SQLITE_DROP_VTABLE, Some(table_name), Some(module_name)) => Self::DropVtable { | ||||
|                 table_name, | ||||
|                 module_name, | ||||
|             }, | ||||
|             (ffi::SQLITE_FUNCTION, _, Some(function_name)) => Self::Function { function_name }, | ||||
|             (ffi::SQLITE_SAVEPOINT, Some(operation_str), Some(savepoint_name)) => Self::Savepoint { | ||||
|                 operation: TransactionOperation::from_str(operation_str), | ||||
|                 savepoint_name, | ||||
|             }, | ||||
|             #[cfg(feature = "modern_sqlite")] | ||||
|             (ffi::SQLITE_RECURSIVE, _, _) => Self::Recursive, | ||||
|             (code, arg1, arg2) => Self::Unknown { code, arg1, arg2 }, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| pub(crate) type BoxedAuthorizer = | ||||
|     Box<dyn for<'c> FnMut(AuthContext<'c>) -> Authorization + Send + 'static>; | ||||
|  | ||||
| /// `feature = "hooks"` A transaction operation. | ||||
| #[derive(Clone, Copy, Debug, PartialEq)] | ||||
| #[non_exhaustive] | ||||
| pub enum TransactionOperation { | ||||
|     Unknown, | ||||
|     Begin, | ||||
|     Release, | ||||
|     Rollback, | ||||
| } | ||||
|  | ||||
| impl TransactionOperation { | ||||
|     fn from_str(op_str: &str) -> Self { | ||||
|         match op_str { | ||||
|             "BEGIN" => Self::Begin, | ||||
|             "RELEASE" => Self::Release, | ||||
|             "ROLLBACK" => Self::Rollback, | ||||
|             _ => Self::Unknown, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Clone, Copy, Debug, PartialEq)] | ||||
| #[non_exhaustive] | ||||
| pub enum Authorization { | ||||
|     /// Authorize the action. | ||||
|     Allow, | ||||
|     /// Don't allow access, but don't trigger an error either. | ||||
|     Ignore, | ||||
|     /// Trigger an error. | ||||
|     Deny, | ||||
| } | ||||
|  | ||||
| impl Authorization { | ||||
|     fn into_raw(self) -> c_int { | ||||
|         match self { | ||||
|             Self::Allow => ffi::SQLITE_OK, | ||||
|             Self::Ignore => ffi::SQLITE_IGNORE, | ||||
|             Self::Deny => ffi::SQLITE_DENY, | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Connection { | ||||
|     /// `feature = "hooks"` Register a callback function to be invoked whenever | ||||
|     /// a transaction is committed. | ||||
| @@ -94,6 +394,16 @@ impl Connection { | ||||
|     { | ||||
|         self.db.borrow_mut().progress_handler(num_ops, handler); | ||||
|     } | ||||
|  | ||||
|     /// `feature = "hooks"` Register an authorizer callback that's invoked | ||||
|     /// as a statement is being prepared. | ||||
|     #[inline] | ||||
|     pub fn authorizer<'c, F>(&self, hook: Option<F>) | ||||
|     where | ||||
|         F: for<'r> FnMut(AuthContext<'r>) -> Authorization + Send + RefUnwindSafe + 'static, | ||||
|     { | ||||
|         self.db.borrow_mut().authorizer(hook) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl InnerConnection { | ||||
| @@ -103,6 +413,7 @@ impl InnerConnection { | ||||
|         self.commit_hook(None::<fn() -> bool>); | ||||
|         self.rollback_hook(None::<fn()>); | ||||
|         self.progress_handler(0, None::<fn() -> bool>); | ||||
|         self.authorizer(None::<fn(AuthContext<'_>) -> Authorization>); | ||||
|     } | ||||
|  | ||||
|     fn commit_hook<'c, F>(&'c mut self, hook: Option<F>) | ||||
| @@ -202,31 +513,19 @@ impl InnerConnection { | ||||
|         unsafe extern "C" fn call_boxed_closure<F>( | ||||
|             p_arg: *mut c_void, | ||||
|             action_code: c_int, | ||||
|             db_str: *const c_char, | ||||
|             tbl_str: *const c_char, | ||||
|             p_db_name: *const c_char, | ||||
|             p_table_name: *const c_char, | ||||
|             row_id: i64, | ||||
|         ) where | ||||
|             F: FnMut(Action, &str, &str, i64), | ||||
|         { | ||||
|             use std::ffi::CStr; | ||||
|             use std::str; | ||||
|  | ||||
|             let action = Action::from(action_code); | ||||
|             let db_name = { | ||||
|                 let c_slice = CStr::from_ptr(db_str).to_bytes(); | ||||
|                 str::from_utf8(c_slice) | ||||
|             }; | ||||
|             let tbl_name = { | ||||
|                 let c_slice = CStr::from_ptr(tbl_str).to_bytes(); | ||||
|                 str::from_utf8(c_slice) | ||||
|             }; | ||||
|  | ||||
|             let _ = catch_unwind(|| { | ||||
|                 let boxed_hook: *mut F = p_arg as *mut F; | ||||
|                 (*boxed_hook)( | ||||
|                     action, | ||||
|                     db_name.expect("illegal db name"), | ||||
|                     tbl_name.expect("illegal table name"), | ||||
|                     expect_utf8(p_db_name, "database name"), | ||||
|                     expect_utf8(p_table_name, "table name"), | ||||
|                     row_id, | ||||
|                 ); | ||||
|             }); | ||||
| @@ -297,12 +596,96 @@ impl InnerConnection { | ||||
|             } | ||||
|         }; | ||||
|     } | ||||
|  | ||||
|     pub fn authorizer<'c, F>(&'c mut self, authorizer: Option<F>) | ||||
|     where | ||||
|         F: for<'r> FnMut(AuthContext<'r>) -> Authorization + Send + RefUnwindSafe + 'static, | ||||
|     { | ||||
|         unsafe extern "C" fn call_boxed_closure<'c, F>( | ||||
|             p_arg: *mut c_void, | ||||
|             action_code: c_int, | ||||
|             param1: *const c_char, | ||||
|             param2: *const c_char, | ||||
|             db_name: *const c_char, | ||||
|             trigger_or_view_name: *const c_char, | ||||
|         ) -> c_int | ||||
|         where | ||||
|             F: FnMut(AuthContext<'c>) -> Authorization + Send + 'static, | ||||
|         { | ||||
|             catch_unwind(|| { | ||||
|                 let action = AuthAction::from_raw( | ||||
|                     action_code, | ||||
|                     expect_optional_utf8(param1, "authorizer param 1"), | ||||
|                     expect_optional_utf8(param2, "authorizer param 2"), | ||||
|                 ); | ||||
|                 let auth_ctx = AuthContext { | ||||
|                     action, | ||||
|                     database_name: expect_optional_utf8(db_name, "database name"), | ||||
|                     accessor: expect_optional_utf8( | ||||
|                         trigger_or_view_name, | ||||
|                         "accessor (inner-most trigger or view)", | ||||
|                     ), | ||||
|                 }; | ||||
|                 let boxed_hook: *mut F = p_arg as *mut F; | ||||
|                 (*boxed_hook)(auth_ctx) | ||||
|             }) | ||||
|             .map(Authorization::into_raw) | ||||
|             .unwrap_or_else(|_| ffi::SQLITE_ERROR) | ||||
|         } | ||||
|  | ||||
|         let callback_fn = authorizer | ||||
|             .as_ref() | ||||
|             .map(|_| call_boxed_closure::<'c, F> as unsafe extern "C" fn(_, _, _, _, _, _) -> _); | ||||
|         let boxed_authorizer = authorizer.map(Box::new); | ||||
|  | ||||
|         match unsafe { | ||||
|             ffi::sqlite3_set_authorizer( | ||||
|                 self.db(), | ||||
|                 callback_fn, | ||||
|                 boxed_authorizer | ||||
|                     .as_ref() | ||||
|                     .map(|f| &**f as *const F as *mut _) | ||||
|                     .unwrap_or_else(ptr::null_mut), | ||||
|             ) | ||||
|         } { | ||||
|             ffi::SQLITE_OK => { | ||||
|                 self.authorizer = boxed_authorizer.map(|ba| ba as _); | ||||
|             } | ||||
|             err_code => { | ||||
|                 // The only error that `sqlite3_set_authorizer` returns is `SQLITE_MISUSE` | ||||
|                 // when compiled with `ENABLE_API_ARMOR` and the db pointer is invalid. | ||||
|                 // This library does not allow constructing a null db ptr, so if this branch | ||||
|                 // is hit, something very bad has happened. Panicking instead of returning | ||||
|                 // `Result` keeps this hook's API consistent with the others. | ||||
|                 panic!("unexpectedly failed to set_authorizer: {}", unsafe { | ||||
|                     crate::error::error_from_handle(self.db(), err_code) | ||||
|                 }); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| unsafe fn free_boxed_hook<F>(p: *mut c_void) { | ||||
|     drop(Box::from_raw(p as *mut F)); | ||||
| } | ||||
|  | ||||
| unsafe fn expect_utf8<'a>(p_str: *const c_char, description: &'static str) -> &'a str { | ||||
|     expect_optional_utf8(p_str, description) | ||||
|         .unwrap_or_else(|| panic!("received empty {}", description)) | ||||
| } | ||||
|  | ||||
| unsafe fn expect_optional_utf8<'a>( | ||||
|     p_str: *const c_char, | ||||
|     description: &'static str, | ||||
| ) -> Option<&'a str> { | ||||
|     if p_str.is_null() { | ||||
|         return None; | ||||
|     } | ||||
|     std::str::from_utf8(std::ffi::CStr::from_ptr(p_str).to_bytes()) | ||||
|         .unwrap_or_else(|_| panic!("received non-utf8 string as {}", description)) | ||||
|         .into() | ||||
| } | ||||
|  | ||||
| #[cfg(test)] | ||||
| mod test { | ||||
|     use super::Action; | ||||
| @@ -398,4 +781,40 @@ mod test { | ||||
|             .unwrap_err(); | ||||
|         Ok(()) | ||||
|     } | ||||
|  | ||||
|     #[test] | ||||
|     fn test_authorizer() -> Result<()> { | ||||
|         use super::{AuthAction, AuthContext, Authorization}; | ||||
|  | ||||
|         let db = Connection::open_in_memory()?; | ||||
|         db.execute_batch("CREATE TABLE foo (public TEXT, private TEXT)") | ||||
|             .unwrap(); | ||||
|  | ||||
|         let authorizer = move |ctx: AuthContext<'_>| match ctx.action { | ||||
|             AuthAction::Read { column_name, .. } if column_name == "private" => { | ||||
|                 Authorization::Ignore | ||||
|             } | ||||
|             AuthAction::DropTable { .. } => Authorization::Deny, | ||||
|             AuthAction::Pragma { .. } => panic!("shouldn't be called"), | ||||
|             _ => Authorization::Allow, | ||||
|         }; | ||||
|  | ||||
|         db.authorizer(Some(authorizer)); | ||||
|         db.execute_batch( | ||||
|             "BEGIN TRANSACTION; INSERT INTO foo VALUES ('pub txt', 'priv txt'); COMMIT;", | ||||
|         ) | ||||
|         .unwrap(); | ||||
|         db.query_row_and_then("SELECT * FROM foo", [], |row| -> Result<()> { | ||||
|             assert_eq!(row.get::<_, String>("public")?, "pub txt"); | ||||
|             assert!(row.get::<_, Option<String>>("private")?.is_none()); | ||||
|             Ok(()) | ||||
|         }) | ||||
|         .unwrap(); | ||||
|         db.execute_batch("DROP TABLE foo").unwrap_err(); | ||||
|  | ||||
|         db.authorizer(None::<fn(AuthContext<'_>) -> Authorization>); | ||||
|         db.execute_batch("PRAGMA user_version=1").unwrap(); // Disallowed by first authorizer, but it's now removed. | ||||
|  | ||||
|         Ok(()) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -33,6 +33,8 @@ pub struct InnerConnection { | ||||
|     pub free_update_hook: Option<unsafe fn(*mut ::std::os::raw::c_void)>, | ||||
|     #[cfg(feature = "hooks")] | ||||
|     pub progress_handler: Option<Box<dyn FnMut() -> bool + Send>>, | ||||
|     #[cfg(feature = "hooks")] | ||||
|     pub authorizer: Option<crate::hooks::BoxedAuthorizer>, | ||||
|     owned: bool, | ||||
| } | ||||
|  | ||||
| @@ -51,6 +53,8 @@ impl InnerConnection { | ||||
|             free_update_hook: None, | ||||
|             #[cfg(feature = "hooks")] | ||||
|             progress_handler: None, | ||||
|             #[cfg(feature = "hooks")] | ||||
|             authorizer: None, | ||||
|             owned, | ||||
|         } | ||||
|     } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user