Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
sqlite: create authz method
  • Loading branch information
araujogui committed Sep 18, 2025
commit 6bf9173262825f09789ce73fd277d6800665fdae
129 changes: 129 additions & 0 deletions src/node_sqlite.cc
Original file line number Diff line number Diff line change
Expand Up @@ -664,6 +664,61 @@ void UserDefinedFunction::xDestroy(void* self) {
delete static_cast<UserDefinedFunction*>(self);
}

AuthorizerFunction::AuthorizerFunction(Environment* env,
Local<Function> fn,
DatabaseSync* db)
: env_(env), fn_(env->isolate(), fn), db_(db) {}

AuthorizerFunction::~AuthorizerFunction() {}

int AuthorizerFunction::xAuthorizer(void* user_data,
int action_code,
const char* param1,
const char* param2,
const char* param3,
const char* param4) {
AuthorizerFunction* self = static_cast<AuthorizerFunction*>(user_data);
Environment* env = self->env_;
Isolate* isolate = env->isolate();
HandleScope handle_scope(isolate);
Local<Context> context = env->context();

auto fn = self->fn_.Get(isolate);
LocalVector<Value> js_argv(isolate);

// Convert SQLite authorizer parameters to JavaScript values
js_argv.emplace_back(Integer::New(isolate, action_code));
js_argv.emplace_back(NullableSQLiteStringToValue(isolate, param1).ToLocalChecked());
js_argv.emplace_back(NullableSQLiteStringToValue(isolate, param2).ToLocalChecked());
js_argv.emplace_back(NullableSQLiteStringToValue(isolate, param3).ToLocalChecked());
js_argv.emplace_back(NullableSQLiteStringToValue(isolate, param4).ToLocalChecked());

TryCatch try_catch(isolate);
MaybeLocal<Value> retval = fn->Call(context, Undefined(isolate), js_argv.size(), js_argv.data());

if (try_catch.HasCaught()) {
// If there's an exception in the callback, deny the operation
return SQLITE_DENY;
}
Comment thread
araujogui marked this conversation as resolved.
Outdated

Local<Value> result;
if (!retval.ToLocal(&result)) {
return SQLITE_DENY;
}

if (result->IsNumber()) {
double num_result = result.As<Number>()->Value();
return static_cast<int>(num_result);
}

// Default to OK if the result isn't a number
return SQLITE_OK;
}

void AuthorizerFunction::xDestroy(void* self) {
delete static_cast<AuthorizerFunction*>(self);
}
Comment thread
araujogui marked this conversation as resolved.
Outdated

DatabaseSync::DatabaseSync(Environment* env,
Local<Object> object,
DatabaseOpenConfiguration&& open_config,
Expand Down Expand Up @@ -1860,6 +1915,37 @@ void DatabaseSync::LoadExtension(const FunctionCallbackInfo<Value>& args) {
}
}

void DatabaseSync::SetAuthorizer(const FunctionCallbackInfo<Value>& args) {
DatabaseSync* db;
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
Environment* env = Environment::GetCurrent(args);
Isolate* isolate = env->isolate();

if (args.Length() == 0 || args[0]->IsNull() || args[0]->IsUndefined()) {
// Clear the authorizer
sqlite3_set_authorizer(db->connection_, nullptr, nullptr);
return;
}

if (!args[0]->IsFunction()) {
THROW_ERR_INVALID_ARG_TYPE(isolate,
"The \"callback\" argument must be a function.");
return;
}

Local<Function> fn = args[0].As<Function>();
AuthorizerFunction* user_data = new AuthorizerFunction(env, fn, db);

int r = sqlite3_set_authorizer(db->connection_,
AuthorizerFunction::xAuthorizer,
user_data);

if (r != SQLITE_OK) {
delete user_data;
CHECK_ERROR_OR_THROW(isolate, db, r, SQLITE_OK, void());
}
}

StatementSync::StatementSync(Environment* env,
Local<Object> object,
BaseObjectPtr<DatabaseSync> db,
Expand Down Expand Up @@ -3093,6 +3179,47 @@ void DefineConstants(Local<Object> target) {
NODE_DEFINE_CONSTANT(target, SQLITE_CHANGESET_CONFLICT);
NODE_DEFINE_CONSTANT(target, SQLITE_CHANGESET_CONSTRAINT);
NODE_DEFINE_CONSTANT(target, SQLITE_CHANGESET_FOREIGN_KEY);

// Authorization result codes
NODE_DEFINE_CONSTANT(target, SQLITE_OK);
NODE_DEFINE_CONSTANT(target, SQLITE_DENY);
NODE_DEFINE_CONSTANT(target, SQLITE_IGNORE);
Comment thread
araujogui marked this conversation as resolved.

// Authorization action codes
NODE_DEFINE_CONSTANT(target, SQLITE_CREATE_INDEX);
NODE_DEFINE_CONSTANT(target, SQLITE_CREATE_TABLE);
NODE_DEFINE_CONSTANT(target, SQLITE_CREATE_TEMP_INDEX);
NODE_DEFINE_CONSTANT(target, SQLITE_CREATE_TEMP_TABLE);
NODE_DEFINE_CONSTANT(target, SQLITE_CREATE_TEMP_TRIGGER);
NODE_DEFINE_CONSTANT(target, SQLITE_CREATE_TEMP_VIEW);
NODE_DEFINE_CONSTANT(target, SQLITE_CREATE_TRIGGER);
NODE_DEFINE_CONSTANT(target, SQLITE_CREATE_VIEW);
NODE_DEFINE_CONSTANT(target, SQLITE_DELETE);
NODE_DEFINE_CONSTANT(target, SQLITE_DROP_INDEX);
NODE_DEFINE_CONSTANT(target, SQLITE_DROP_TABLE);
NODE_DEFINE_CONSTANT(target, SQLITE_DROP_TEMP_INDEX);
NODE_DEFINE_CONSTANT(target, SQLITE_DROP_TEMP_TABLE);
NODE_DEFINE_CONSTANT(target, SQLITE_DROP_TEMP_TRIGGER);
NODE_DEFINE_CONSTANT(target, SQLITE_DROP_TEMP_VIEW);
NODE_DEFINE_CONSTANT(target, SQLITE_DROP_TRIGGER);
NODE_DEFINE_CONSTANT(target, SQLITE_DROP_VIEW);
NODE_DEFINE_CONSTANT(target, SQLITE_INSERT);
NODE_DEFINE_CONSTANT(target, SQLITE_PRAGMA);
NODE_DEFINE_CONSTANT(target, SQLITE_READ);
NODE_DEFINE_CONSTANT(target, SQLITE_SELECT);
NODE_DEFINE_CONSTANT(target, SQLITE_TRANSACTION);
NODE_DEFINE_CONSTANT(target, SQLITE_UPDATE);
NODE_DEFINE_CONSTANT(target, SQLITE_ATTACH);
NODE_DEFINE_CONSTANT(target, SQLITE_DETACH);
NODE_DEFINE_CONSTANT(target, SQLITE_ALTER_TABLE);
NODE_DEFINE_CONSTANT(target, SQLITE_REINDEX);
NODE_DEFINE_CONSTANT(target, SQLITE_ANALYZE);
NODE_DEFINE_CONSTANT(target, SQLITE_CREATE_VTABLE);
NODE_DEFINE_CONSTANT(target, SQLITE_DROP_VTABLE);
NODE_DEFINE_CONSTANT(target, SQLITE_FUNCTION);
NODE_DEFINE_CONSTANT(target, SQLITE_SAVEPOINT);
NODE_DEFINE_CONSTANT(target, SQLITE_COPY);
NODE_DEFINE_CONSTANT(target, SQLITE_RECURSIVE);
}

static void Initialize(Local<Object> target,
Expand All @@ -3117,6 +3244,7 @@ static void Initialize(Local<Object> target,
SetProtoMethod(isolate, db_tmpl, "function", DatabaseSync::CustomFunction);
SetProtoMethod(
isolate, db_tmpl, "createTagStore", DatabaseSync::CreateTagStore);
SetProtoMethod(isolate, db_tmpl, "setAuthorizer", DatabaseSync::SetAuthorizer);
SetProtoMethodNoSideEffect(
isolate, db_tmpl, "location", DatabaseSync::Location);
SetProtoMethod(
Expand All @@ -3131,6 +3259,7 @@ static void Initialize(Local<Object> target,
DatabaseSync::EnableLoadExtension);
SetProtoMethod(
isolate, db_tmpl, "loadExtension", DatabaseSync::LoadExtension);
SetProtoMethod(isolate, db_tmpl, "setAuthorizer", DatabaseSync::SetAuthorizer);
SetSideEffectFreeGetter(isolate,
db_tmpl,
FIXED_ONE_BYTE_STRING(isolate, "isOpen"),
Expand Down
21 changes: 21 additions & 0 deletions src/node_sqlite.h
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ class DatabaseSync : public BaseObject {
static void EnableLoadExtension(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void LoadExtension(const v8::FunctionCallbackInfo<v8::Value>& args);
static void SetAuthorizer(const v8::FunctionCallbackInfo<v8::Value>& args);
void FinalizeStatements();
void RemoveBackup(BackupJob* backup);
void AddBackup(BackupJob* backup);
Expand Down Expand Up @@ -317,6 +318,26 @@ class SQLTagStore : public BaseObject {
friend class StatementExecutionHelper;
};

class AuthorizerFunction {
public:
AuthorizerFunction(Environment* env,
v8::Local<v8::Function> fn,
DatabaseSync* db);
~AuthorizerFunction();
static int xAuthorizer(void* user_data,
int action_code,
const char* param1,
const char* param2,
const char* param3,
const char* param4);
static void xDestroy(void* self);
Comment thread
araujogui marked this conversation as resolved.
Outdated

private:
Environment* env_;
v8::Global<v8::Function> fn_;
Comment thread
araujogui marked this conversation as resolved.
Outdated
DatabaseSync* db_;
};

class UserDefinedFunction {
public:
UserDefinedFunction(Environment* env,
Expand Down
90 changes: 90 additions & 0 deletions test/parallel/test-sqlite-authz.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
'use strict';
const { skipIfSQLiteMissing } = require('../common');
skipIfSQLiteMissing();
const assert = require('node:assert');
const { DatabaseSync, constants } = require('node:sqlite');
const { suite, it } = require('node:test');

suite('DatabaseSync.prototype.setAuthorizer()', () => {
it('calls the authorizer with the correct parameters', (t) => {
const authorizer = t.mock.fn(() => constants.SQLITE_OK);

const db = new DatabaseSync(':memory:');

db.setAuthorizer(authorizer);

const insert = db.prepare('SELECT 1');
insert.run();

assert.strictEqual(authorizer.mock.callCount(), 1);

const call = authorizer.mock.calls[0];
assert.deepStrictEqual(call.arguments, [constants.SQLITE_SELECT, null, null, null, null]);
assert.strictEqual(call.result, constants.SQLITE_OK);
assert.strictEqual(call.error, undefined);
});

it('allows operations when authorizer returns SQLITE_OK', () => {
const db = new DatabaseSync(':memory:');

db.setAuthorizer(() => constants.SQLITE_OK);

db.exec('CREATE TABLE users (id INTEGER, name TEXT)');

const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all();

assert.strictEqual(tables[0].name, 'users');
});

it('blocks operations when authorizer returns SQLITE_DENY', () => {
const db = new DatabaseSync(':memory:');

db.setAuthorizer(() => constants.SQLITE_DENY);

assert.throws(() => {
db.exec('SELECT 1');
}, {
code: 'ERR_SQLITE_ERROR',
message: /not authorized/,
});
});

it('clears authorizer with null', (t) => {
const authorizer = t.mock.fn(() => constants.SQLITE_OK);

const db = new DatabaseSync(':memory:');

db.setAuthorizer(authorizer);

const statement = db.prepare('SELECT 1');
statement.run();

assert.strictEqual(authorizer.mock.callCount(), 1);

// Clear authorizer
db.setAuthorizer(null);

statement.run();

assert.strictEqual(authorizer.mock.callCount(), 1);
});

it('throws with invalid callback type', () => {
const db = new DatabaseSync(':memory:');

assert.throws(() => {
db.setAuthorizer('not a function');
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "callback" argument must be a function/,
});

assert.throws(() => {
db.setAuthorizer(1);
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: /The "callback" argument must be a function/,
});
});

});