Skip to content
Closed
Show file tree
Hide file tree
Changes from 13 commits
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
76 changes: 76 additions & 0 deletions doc/api/sqlite.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,81 @@ added:
This method is used to create SQLite user-defined functions. This method is a
wrapper around [`sqlite3_create_function_v2()`][].

### `database.setAuthorizer(callback)`

<!-- YAML
added: REPLACEME
-->

* `callback` {Function|null} The authorizer function to set, or `null` to
clear the current authorizer.

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.
This method is a wrapper around [`sqlite3_set_authorizer()`][].

When invoked, the callback receives five arguments:

* `actionCode` {number} The type of operation being performed (e.g.,
`SQLITE_INSERT`, `SQLITE_UPDATE`, `SQLITE_SELECT`).
* `arg1` {string|null} The first argument (context-dependent, often a table name).
* `arg2` {string|null} The second argument (context-dependent, often a column name).
* `dbName` {string|null} The name of the database.
* `triggerOrView` {string|null} The name of the trigger or view causing the access.

The callback must return one of the following constants:

* `SQLITE_OK` - Allow the operation.
* `SQLITE_DENY` - Deny the operation (causes an error).
* `SQLITE_IGNORE` - Ignore the operation (silently skip).

```cjs
const { DatabaseSync, constants } = require('node:sqlite');
const db = new DatabaseSync(':memory:');

// Set up an authorizer that denies all table creation
db.setAuthorizer((actionCode) => {
if (actionCode === constants.SQLITE_CREATE_TABLE) {
return constants.SQLITE_DENY;
}
return constants.SQLITE_OK;
});

// This will work
db.prepare('SELECT 1').get();

// This will throw an error due to authorization denial
try {
db.exec('CREATE TABLE blocked (id INTEGER)');
} catch (err) {
console.log('Operation blocked:', err.message);
}
```

```mjs
import { DatabaseSync, constants } from 'node:sqlite';
const db = new DatabaseSync(':memory:');

// Set up an authorizer that denies all table creation
db.setAuthorizer((actionCode) => {
if (actionCode === constants.SQLITE_CREATE_TABLE) {
return constants.SQLITE_DENY;
}
return constants.SQLITE_OK;
});

// This will work
db.prepare('SELECT 1').get();

// This will throw an error due to authorization denial
try {
db.exec('CREATE TABLE blocked (id INTEGER)');
} catch (err) {
console.log('Operation blocked:', err.message);
}
```

### `database.isOpen`

<!-- YAML
Expand Down Expand Up @@ -1078,6 +1153,7 @@ resolution handler passed to [`database.applyChangeset()`][]. See also
[`sqlite3_last_insert_rowid()`]: https://www.sqlite.org/c3ref/last_insert_rowid.html
[`sqlite3_load_extension()`]: https://www.sqlite.org/c3ref/load_extension.html
[`sqlite3_prepare_v2()`]: https://www.sqlite.org/c3ref/prepare.html
[`sqlite3_set_authorizer()`]: https://sqlite.org/c3ref/set_authorizer.html
[`sqlite3_sql()`]: https://www.sqlite.org/c3ref/expanded_sql.html
[`sqlite3changeset_apply()`]: https://www.sqlite.org/session/sqlite3changeset_apply.html
[`sqlite3session_attach()`]: https://www.sqlite.org/session/sqlite3session_attach.html
Expand Down
175 changes: 175 additions & 0 deletions src/node_sqlite.cc
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,28 @@ inline void THROW_ERR_SQLITE_ERROR(Isolate* isolate, DatabaseSync* db) {
}
}

bool DatabaseSync::HasPendingAuthorizerError() const {
Local<Value> error =
object()->GetInternalField(kPendingAuthorizerError).As<Value>();
return !error.IsEmpty() && !error->IsUndefined();
}

void DatabaseSync::StoreAuthorizerError(Local<Value> error) {
if (!HasPendingAuthorizerError()) {
object()->SetInternalField(kPendingAuthorizerError, error);
}
}

void DatabaseSync::RethrowPendingAuthorizerError() {
if (HasPendingAuthorizerError()) {
Local<Value> error =
object()->GetInternalField(kPendingAuthorizerError).As<Value>();
object()->SetInternalField(kPendingAuthorizerError,
Undefined(env()->isolate()));
env()->isolate()->ThrowException(error);
}
}

inline void THROW_ERR_SQLITE_ERROR(Isolate* isolate, const char* message) {
Local<Object> e;
if (CreateSQLiteError(isolate, message).ToLocal(&e)) {
Expand Down Expand Up @@ -1126,6 +1148,15 @@ void DatabaseSync::Prepare(const FunctionCallbackInfo<Value>& args) {
Utf8Value sql(env->isolate(), args[0].As<String>());
sqlite3_stmt* s = nullptr;
int r = sqlite3_prepare_v2(db->connection_, *sql, -1, &s, 0);

if (db->HasPendingAuthorizerError()) {
db->RethrowPendingAuthorizerError();
if (s != nullptr) {
sqlite3_finalize(s);
}
return;
}

CHECK_ERROR_OR_THROW(env->isolate(), db, r, SQLITE_OK, void());
BaseObjectPtr<StatementSync> stmt =
StatementSync::Create(env, BaseObjectPtr<DatabaseSync>(db), s);
Expand All @@ -1147,6 +1178,10 @@ void DatabaseSync::Exec(const FunctionCallbackInfo<Value>& args) {

Utf8Value sql(env->isolate(), args[0].As<String>());
int r = sqlite3_exec(db->connection_, *sql, nullptr, nullptr, nullptr);
if (db->HasPendingAuthorizerError()) {
db->RethrowPendingAuthorizerError();
return;
}
CHECK_ERROR_OR_THROW(env->isolate(), db, r, SQLITE_OK, void());
}

Expand Down Expand Up @@ -1860,6 +1895,103 @@ 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[0]->IsNull()) {
// Clear the authorizer
sqlite3_set_authorizer(db->connection_, nullptr, nullptr);
db->object()->SetInternalField(kAuthorizerCallback, Null(isolate));
return;
}

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

Local<Function> fn = args[0].As<Function>();

db->object()->SetInternalField(kAuthorizerCallback, fn);

int r = sqlite3_set_authorizer(
db->connection_, DatabaseSync::AuthorizerCallback, db);

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

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

Local<Value> cb =
db->object()->GetInternalField(kAuthorizerCallback).template As<Value>();

CHECK(cb->IsFunction());

Local<Function> callback = cb.As<Function>();
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 = callback->Call(
context, Undefined(isolate), js_argv.size(), js_argv.data());

if (try_catch.HasCaught()) {
db->StoreAuthorizerError(try_catch.Exception());
return SQLITE_DENY;
}

Local<Value> result;
if (!retval.ToLocal(&result) || result->IsUndefined() || result->IsNull() ||
!result->IsInt32()) {
Local<Value> err = Exception::TypeError(
String::NewFromUtf8(
isolate,
"Authorizer callback must return an integer authorization code")
.ToLocalChecked());
db->StoreAuthorizerError(err);
return SQLITE_DENY;
}

int32_t int_result = result.As<Int32>()->Value();
if (int_result != SQLITE_OK && int_result != SQLITE_DENY &&
int_result != SQLITE_IGNORE) {
Local<Value> err = Exception::RangeError(
String::NewFromUtf8(
isolate, "Authorizer callback returned invalid authorization code")
.ToLocalChecked());
db->StoreAuthorizerError(err);
return SQLITE_DENY;
}

return int_result;
}

StatementSync::StatementSync(Environment* env,
Local<Object> object,
BaseObjectPtr<DatabaseSync> db,
Expand Down Expand Up @@ -3093,6 +3225,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);

// 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 Down Expand Up @@ -3131,6 +3304,8 @@ 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
17 changes: 17 additions & 0 deletions src/node_sqlite.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,12 @@ class StatementExecutionHelper {

class DatabaseSync : public BaseObject {
public:
enum InternalFields {
kAuthorizerCallback = BaseObject::kInternalFieldCount,
kPendingAuthorizerError,
kInternalFieldCount
};

DatabaseSync(Environment* env,
v8::Local<v8::Object> object,
DatabaseOpenConfiguration&& open_config,
Expand All @@ -136,6 +142,13 @@ 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);
static int AuthorizerCallback(void* user_data,
int action_code,
const char* param1,
const char* param2,
const char* param3,
const char* param4);
void FinalizeStatements();
void RemoveBackup(BackupJob* backup);
void AddBackup(BackupJob* backup);
Expand All @@ -159,6 +172,10 @@ class DatabaseSync : public BaseObject {
void SetIgnoreNextSQLiteError(bool ignore);
bool ShouldIgnoreSQLiteError();

bool HasPendingAuthorizerError() const;
void StoreAuthorizerError(v8::Local<v8::Value> error);
void RethrowPendingAuthorizerError();

SET_MEMORY_INFO_NAME(DatabaseSync)
SET_SELF_SIZE(DatabaseSync)

Expand Down
Loading
Loading