Repository: denoland/deno
Pull request: fix(ext/node): implement `DatabaseSync.setAuthorizer()`

Allows https://github.com/nodejs/node/blob/v24.12.0/test/parallel/test-sqlite-authz.js to pass.

Changed files:
filename: ext/node/ops/sqlite/database.rs
patch: @@ -516,6 +516,7 @@ pub struct DatabaseSync {
   options: DatabaseSyncOptions,
   location: String,
   ignore_next_sqlite_error: Rc<Cell<bool>>,
+  authorizer_data: Rc<RefCell<Option<*mut AuthorizerData>>>,
 }
 
 // SAFETY: we're sure this can be GCed
@@ -723,6 +724,7 @@ impl DatabaseSync {
       location,
       options,
       ignore_next_sqlite_error: Rc::new(Cell::new(false)),
+      authorizer_data: Rc::new(RefCell::new(None)),
     })
   }
 
@@ -784,6 +786,16 @@ impl DatabaseSync {
       };
     }
 
+    {
+      let mut authorizer_data = self.authorizer_data.borrow_mut();
+      if let Some(data_ptr) = authorizer_data.take() {
+        // SAFETY: data_ptr was allocated in authorizer setup.
+        unsafe {
+          free_authorizer_data(data_ptr);
+        }
+      }
+    }
+
     let _ = self.conn.borrow_mut().take();
 
     Ok(())
@@ -1404,6 +1416,91 @@ impl DatabaseSync {
     Ok(())
   }
 
+  // Sets an authorizer callback that SQLite will invoke whenever it attempts
+  // to access data or modify the database schema through prepared statements.
+  // This can be used to implement security policies, audit access, or restrict
+  // certain operations.
+  #[fast]
+  #[validate(is_open)]
+  #[undefined]
+  fn set_authorizer<'a>(
+    &self,
+    scope: &mut v8::PinScope<'a, '_>,
+    callback: v8::Local<'a, v8::Value>,
+  ) -> Result<(), SqliteError> {
+    let db = self.conn.borrow();
+    let conn = db.as_ref().ok_or(SqliteError::AlreadyClosed)?;
+
+    // SAFETY: lifetime of the connection is guaranteed by reference counting.
+    let raw_handle = unsafe { conn.handle() };
+
+    {
+      let mut authorizer_data = self.authorizer_data.borrow_mut();
+      if let Some(old_data_ptr) = authorizer_data.take() {
+        // SAFETY: data_ptr was allocated in authorizer setup.
+        unsafe {
+          free_authorizer_data(old_data_ptr);
+        }
+      }
+    }
+
+    if callback.is_null() {
+      // SAFETY: `raw_handle` is a valid database handle.
+      let result = unsafe {
+        libsqlite3_sys::sqlite3_set_authorizer(
+          raw_handle,
+          None,
+          std::ptr::null_mut(),
+        )
+      };
+      check_error_code(result, raw_handle)?;
+      return Ok(());
+    }
+
+    let Ok(function) = v8::Local::<v8::Function>::try_from(callback) else {
+      return Err(
+        validators::Error::InvalidArgType(
+          "The \"callback\" argument must be a function or null.",
+        )
+        .into(),
+      );
+    };
+
+    let callback_global = v8::Global::new(scope, function).into_raw();
+    let context_global =
+      v8::Global::new(scope, scope.get_current_context()).into_raw();
+
+    let data = Box::new(AuthorizerData {
+      callback: callback_global,
+      context: context_global,
+      ignore_next_sqlite_error: Rc::clone(&self.ignore_next_sqlite_error),
+    });
+    let data_ptr = Box::into_raw(data);
+
+    // SAFETY: `raw_handle` is a valid database handle.
+    // `data_ptr` points to a valid memory location.
+    let result = unsafe {
+      libsqlite3_sys::sqlite3_set_authorizer(
+        raw_handle,
+        Some(authorizer_callback),
+        data_ptr as *mut c_void,
+      )
+    };
+
+    if result != libsqlite3_sys::SQLITE_OK {
+      // Clean up the data we allocated if setting the authorizer failed
+      // SAFETY: we just allocated this data and it failed to be registered
+      unsafe {
+        free_authorizer_data(data_ptr);
+      }
+      check_error_code(result, raw_handle)?;
+    }
+
+    *self.authorizer_data.borrow_mut() = Some(data_ptr);
+
+    Ok(())
+  }
+
   #[getter]
   fn is_open(&self) -> bool {
     self.conn.borrow().is_some()
@@ -1912,6 +2009,12 @@ struct CustomFunctionData {
   ignore_next_sqlite_error: Rc<Cell<bool>>,
 }
 
+struct AuthorizerData {
+  callback: NonNull<v8::Function>,
+  context: NonNull<v8::Context>,
+  ignore_next_sqlite_error: Rc<Cell<bool>>,
+}
+
 unsafe extern "C" fn custom_function_handler(
   ctx: *mut libsqlite3_sys::sqlite3_context,
   argc: i32,
@@ -2152,6 +2255,134 @@ fn sqlite_result_error(
   }
 }
 
+macro_rules! nullable_cstring_to_v8_value {
+  ($scope:expr, $ptr:expr) => {{
+    if ($ptr).is_null() {
+      v8::null($scope).into()
+    } else {
+      let cstr = CStr::from_ptr($ptr);
+      v8::String::new_from_utf8(
+        $scope,
+        cstr.to_bytes(),
+        v8::NewStringType::Normal,
+      )
+      .map(|s| s.into())
+      .unwrap_or_else(|| v8::null($scope).into())
+    }
+  }};
+}
+
+unsafe extern "C" fn authorizer_callback(
+  user_data: *mut c_void,
+  action_code: i32,
+  param1: *const c_char,
+  param2: *const c_char,
+  param3: *const c_char,
+  param4: *const c_char,
+) -> i32 {
+  // SAFETY: `user_data` is a valid pointer to AuthorizerData.
+  unsafe {
+    let data_ptr = user_data as *mut AuthorizerData;
+    if data_ptr.is_null() {
+      return libsqlite3_sys::SQLITE_DENY;
+    }
+
+    let data = &*data_ptr;
+    let context_local: v8::Local<v8::Context> =
+      std::mem::transmute(data.context.as_ptr());
+
+    v8::callback_scope!(unsafe cb_scope, context_local);
+    v8::scope!(scope, cb_scope);
+
+    let result: Option<v8::Local<'_, v8::Value>>;
+    {
+      v8::tc_scope!(tc_scope, scope);
+
+      let function_local: v8::Local<v8::Function> =
+        std::mem::transmute(data.callback.as_ptr());
+
+      let action_code_js = v8::Integer::new(tc_scope, action_code).into();
+
+      let param1_js: v8::Local<v8::Value> =
+        nullable_cstring_to_v8_value!(tc_scope, param1);
+      let param2_js: v8::Local<v8::Value> =
+        nullable_cstring_to_v8_value!(tc_scope, param2);
+      let param3_js: v8::Local<v8::Value> =
+        nullable_cstring_to_v8_value!(tc_scope, param3);
+      let param4_js: v8::Local<v8::Value> =
+        nullable_cstring_to_v8_value!(tc_scope, param4);
+
+      let js_args =
+        [action_code_js, param1_js, param2_js, param3_js, param4_js];
+      let recv = v8::undefined(tc_scope).into();
+      result = function_local.call(tc_scope, recv, &js_args);
+
+      if tc_scope.has_caught() {
+        data.ignore_next_sqlite_error.set(true);
+        tc_scope.rethrow();
+        return libsqlite3_sys::SQLITE_DENY;
+      }
+    }
+
+    let Some(value) = result else {
+      data.ignore_next_sqlite_error.set(true);
+      return libsqlite3_sys::SQLITE_DENY;
+    };
+
+    if !value.is_int32() {
+      let message = v8::String::new(
+        scope,
+        "Authorizer callback must return an integer authorization code",
+      )
+      .unwrap();
+      let err = v8::Exception::type_error(scope, message);
+      data.ignore_next_sqlite_error.set(true);
+      scope.throw_exception(err);
+      return libsqlite3_sys::SQLITE_DENY;
+    }
+
+    let int_result = value
+      .int32_value(scope)
+      .unwrap_or(libsqlite3_sys::SQLITE_DENY);
+
+    if int_result != libsqlite3_sys::SQLITE_OK
+      && int_result != libsqlite3_sys::SQLITE_DENY
+      && int_result != libsqlite3_sys::SQLITE_IGNORE
+    {
+      let message = v8::String::new(
+        scope,
+        "Authorizer callback returned a invalid authorization code",
+      )
+      .unwrap();
+      let err = v8::Exception::range_error(scope, message);
+      data.ignore_next_sqlite_error.set(true);
+      scope.throw_exception(err);
+      return libsqlite3_sys::SQLITE_DENY;
+    }
+
+    int_result
+  }
+}
+
+unsafe fn free_authorizer_data(data_ptr: *mut AuthorizerData) {
+  if data_ptr.is_null() {
+    return;
+  }
+  // SAFETY: `data_ptr` is a valid pointer to AuthorizerData.
+  // The v8 handles are properly dropped here.
+  unsafe {
+    let data = Box::from_raw(data_ptr);
+    let context_local: v8::Local<v8::Context> =
+      std::mem::transmute(data.context.as_ptr());
+
+    v8::callback_scope!(unsafe cb_scope, context_local);
+    v8::scope!(scope, cb_scope);
+
+    let _ = v8::Global::from_raw(scope, data.callback);
+    let _ = v8::Global::from_raw(scope, data.context);
+  }
+}
+
 fn throw_range_error(scope: &mut v8::PinScope<'_, '_>, message: &str) {
   let msg = v8::String::new(scope, message).unwrap();
   let error = v8::Exception::range_error(scope, msg);

filename: ext/node/polyfills/sqlite.ts
patch: @@ -190,6 +190,44 @@ export const constants = {
   SQLITE_CHANGESET_CONFLICT: 3,
   SQLITE_CHANGESET_CONSTRAINT: 4,
   SQLITE_CHANGESET_FOREIGN_KEY: 5,
+
+  SQLITE_OK: 0,
+  SQLITE_DENY: 1,
+  SQLITE_IGNORE: 2,
+  SQLITE_CREATE_INDEX: 1,
+  SQLITE_CREATE_TABLE: 2,
+  SQLITE_CREATE_TEMP_INDEX: 3,
+  SQLITE_CREATE_TEMP_TABLE: 4,
+  SQLITE_CREATE_TEMP_TRIGGER: 5,
+  SQLITE_CREATE_TEMP_VIEW: 6,
+  SQLITE_CREATE_TRIGGER: 7,
+  SQLITE_CREATE_VIEW: 8,
+  SQLITE_DELETE: 9,
+  SQLITE_DROP_INDEX: 10,
+  SQLITE_DROP_TABLE: 11,
+  SQLITE_DROP_TEMP_INDEX: 12,
+  SQLITE_DROP_TEMP_TABLE: 13,
+  SQLITE_DROP_TEMP_TRIGGER: 14,
+  SQLITE_DROP_TEMP_VIEW: 15,
+  SQLITE_DROP_TRIGGER: 16,
+  SQLITE_DROP_VIEW: 17,
+  SQLITE_INSERT: 18,
+  SQLITE_PRAGMA: 19,
+  SQLITE_READ: 20,
+  SQLITE_SELECT: 21,
+  SQLITE_TRANSACTION: 22,
+  SQLITE_UPDATE: 23,
+  SQLITE_ATTACH: 24,
+  SQLITE_DETACH: 25,
+  SQLITE_ALTER_TABLE: 26,
+  SQLITE_REINDEX: 27,
+  SQLITE_ANALYZE: 28,
+  SQLITE_CREATE_VTABLE: 29,
+  SQLITE_DROP_VTABLE: 30,
+  SQLITE_FUNCTION: 31,
+  SQLITE_SAVEPOINT: 32,
+  SQLITE_COPY: 0,
+  SQLITE_RECURSIVE: 33,
 };
 
 const sqliteTypeSymbol = SymbolFor("sqlite-type");

filename: ext/node/polyfills/testing.ts
patch: @@ -98,8 +98,7 @@ class NodeTestContext {
   }
 
   get mock() {
-    notImplemented("test.TestContext.mock");
-    return null;
+    return mock;
   }
 
   runOnly() {

filename: tests/node_compat/config.jsonc
patch: @@ -1132,6 +1132,7 @@
     "parallel/test-socket-write-after-fin-error.js": {},
     // TODO(bartlomieju): disabled during work on `node:inspector`, this test didn't actualy run before
     // "parallel/test-source-map-enable.js": {},
+    "parallel/test-sqlite-authz.js": {},
     "parallel/test-sqlite-config.js": {},
     "parallel/test-sqlite-custom-functions.js": {},
     "parallel/test-sqlite-data-types.js": {},