mirror of
https://github.com/zebrajr/node.git
synced 2025-12-06 12:20:27 +01:00
sqlite: create authorization api
PR-URL: https://github.com/nodejs/node/pull/59928 Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Edy Silva <edigleyssonsilva@gmail.com>
This commit is contained in:
parent
4a9a022dd4
commit
b217051d36
|
|
@ -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
|
||||
|
|
@ -1048,6 +1123,182 @@ resolution handler passed to [`database.applyChangeset()`][]. See also
|
|||
</tr>
|
||||
</table>
|
||||
|
||||
#### Authorization constants
|
||||
|
||||
The following constants are used with the [`database.setAuthorizer()`][] method.
|
||||
|
||||
##### Authorization result codes
|
||||
|
||||
One of the following constants must be returned from the authorizer callback
|
||||
function passed to [`database.setAuthorizer()`][].
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Constant</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_OK</code></td>
|
||||
<td>Allow the operation to proceed normally.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_DENY</code></td>
|
||||
<td>Deny the operation and cause an error to be returned.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_IGNORE</code></td>
|
||||
<td>Ignore the operation and continue as if it had never been requested.</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
##### Authorization action codes
|
||||
|
||||
The following constants are passed as the first argument to the authorizer
|
||||
callback function to indicate what type of operation is being authorized.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<th>Constant</th>
|
||||
<th>Description</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_CREATE_INDEX</code></td>
|
||||
<td>Create an index</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_CREATE_TABLE</code></td>
|
||||
<td>Create a table</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_CREATE_TEMP_INDEX</code></td>
|
||||
<td>Create a temporary index</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_CREATE_TEMP_TABLE</code></td>
|
||||
<td>Create a temporary table</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_CREATE_TEMP_TRIGGER</code></td>
|
||||
<td>Create a temporary trigger</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_CREATE_TEMP_VIEW</code></td>
|
||||
<td>Create a temporary view</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_CREATE_TRIGGER</code></td>
|
||||
<td>Create a trigger</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_CREATE_VIEW</code></td>
|
||||
<td>Create a view</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_DELETE</code></td>
|
||||
<td>Delete from a table</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_DROP_INDEX</code></td>
|
||||
<td>Drop an index</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_DROP_TABLE</code></td>
|
||||
<td>Drop a table</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_DROP_TEMP_INDEX</code></td>
|
||||
<td>Drop a temporary index</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_DROP_TEMP_TABLE</code></td>
|
||||
<td>Drop a temporary table</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_DROP_TEMP_TRIGGER</code></td>
|
||||
<td>Drop a temporary trigger</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_DROP_TEMP_VIEW</code></td>
|
||||
<td>Drop a temporary view</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_DROP_TRIGGER</code></td>
|
||||
<td>Drop a trigger</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_DROP_VIEW</code></td>
|
||||
<td>Drop a view</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_INSERT</code></td>
|
||||
<td>Insert into a table</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_PRAGMA</code></td>
|
||||
<td>Execute a PRAGMA statement</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_READ</code></td>
|
||||
<td>Read from a table</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_SELECT</code></td>
|
||||
<td>Execute a SELECT statement</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_TRANSACTION</code></td>
|
||||
<td>Begin, commit, or rollback a transaction</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_UPDATE</code></td>
|
||||
<td>Update a table</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_ATTACH</code></td>
|
||||
<td>Attach a database</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_DETACH</code></td>
|
||||
<td>Detach a database</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_ALTER_TABLE</code></td>
|
||||
<td>Alter a table</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_REINDEX</code></td>
|
||||
<td>Reindex</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_ANALYZE</code></td>
|
||||
<td>Analyze the database</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_CREATE_VTABLE</code></td>
|
||||
<td>Create a virtual table</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_DROP_VTABLE</code></td>
|
||||
<td>Drop a virtual table</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_FUNCTION</code></td>
|
||||
<td>Use a function</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_SAVEPOINT</code></td>
|
||||
<td>Create, release, or rollback a savepoint</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_COPY</code></td>
|
||||
<td>Copy data (legacy)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><code>SQLITE_RECURSIVE</code></td>
|
||||
<td>Recursive query</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
[Changesets and Patchsets]: https://www.sqlite.org/sessionintro.html#changesets_and_patchsets
|
||||
[Constants Passed To The Conflict Handler]: https://www.sqlite.org/session/c_changeset_conflict.html
|
||||
[Constants Returned From The Conflict Handler]: https://www.sqlite.org/session/c_changeset_abort.html
|
||||
|
|
@ -1059,6 +1310,7 @@ resolution handler passed to [`database.applyChangeset()`][]. See also
|
|||
[`SQLITE_DIRECTONLY`]: https://www.sqlite.org/c3ref/c_deterministic.html
|
||||
[`SQLITE_MAX_FUNCTION_ARG`]: https://www.sqlite.org/limits.html#max_function_arg
|
||||
[`database.applyChangeset()`]: #databaseapplychangesetchangeset-options
|
||||
[`database.setAuthorizer()`]: #databasesetauthorizercallback
|
||||
[`sqlite3_backup_finish()`]: https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupfinish
|
||||
[`sqlite3_backup_init()`]: https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupinit
|
||||
[`sqlite3_backup_step()`]: https://www.sqlite.org/c3ref/backup_finish.html#sqlite3backupstep
|
||||
|
|
@ -1078,6 +1330,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
|
||||
|
|
|
|||
|
|
@ -1126,6 +1126,7 @@ 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);
|
||||
|
||||
CHECK_ERROR_OR_THROW(env->isolate(), db, r, SQLITE_OK, void());
|
||||
BaseObjectPtr<StatementSync> stmt =
|
||||
StatementSync::Create(env, BaseObjectPtr<DatabaseSync>(db), s);
|
||||
|
|
@ -1871,6 +1872,113 @@ 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());
|
||||
|
||||
MaybeLocal<Value> retval = callback->Call(
|
||||
context, Undefined(isolate), js_argv.size(), js_argv.data());
|
||||
|
||||
Local<Value> result;
|
||||
|
||||
if (!retval.ToLocal(&result)) {
|
||||
db->SetIgnoreNextSQLiteError(true);
|
||||
return SQLITE_DENY;
|
||||
}
|
||||
|
||||
Local<String> error_message;
|
||||
|
||||
if (!result->IsInt32()) {
|
||||
if (!String::NewFromUtf8(
|
||||
isolate,
|
||||
"Authorizer callback must return an integer authorization code")
|
||||
.ToLocal(&error_message)) {
|
||||
return SQLITE_DENY;
|
||||
}
|
||||
|
||||
Local<Value> err = Exception::TypeError(error_message);
|
||||
isolate->ThrowException(err);
|
||||
db->SetIgnoreNextSQLiteError(true);
|
||||
return SQLITE_DENY;
|
||||
}
|
||||
|
||||
int32_t int_result = result.As<Int32>()->Value();
|
||||
if (int_result != SQLITE_OK && int_result != SQLITE_DENY &&
|
||||
int_result != SQLITE_IGNORE) {
|
||||
if (!String::NewFromUtf8(
|
||||
isolate,
|
||||
"Authorizer callback returned a invalid authorization code")
|
||||
.ToLocal(&error_message)) {
|
||||
return SQLITE_DENY;
|
||||
}
|
||||
|
||||
Local<Value> err = Exception::RangeError(error_message);
|
||||
isolate->ThrowException(err);
|
||||
db->SetIgnoreNextSQLiteError(true);
|
||||
return SQLITE_DENY;
|
||||
}
|
||||
|
||||
return int_result;
|
||||
}
|
||||
|
||||
StatementSync::StatementSync(Environment* env,
|
||||
Local<Object> object,
|
||||
BaseObjectPtr<DatabaseSync> db,
|
||||
|
|
@ -3102,6 +3210,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,
|
||||
|
|
@ -3140,6 +3289,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"),
|
||||
|
|
|
|||
|
|
@ -111,6 +111,11 @@ class StatementExecutionHelper {
|
|||
|
||||
class DatabaseSync : public BaseObject {
|
||||
public:
|
||||
enum InternalFields {
|
||||
kAuthorizerCallback = BaseObject::kInternalFieldCount,
|
||||
kInternalFieldCount
|
||||
};
|
||||
|
||||
DatabaseSync(Environment* env,
|
||||
v8::Local<v8::Object> object,
|
||||
DatabaseOpenConfiguration&& open_config,
|
||||
|
|
@ -136,6 +141,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);
|
||||
|
|
|
|||
278
test/parallel/test-sqlite-authz.js
Normal file
278
test/parallel/test-sqlite-authz.js
Normal file
|
|
@ -0,0 +1,278 @@
|
|||
'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()', () => {
|
||||
const createTestDatabase = () => {
|
||||
const db = new DatabaseSync(':memory:');
|
||||
db.exec('CREATE TABLE users (id INTEGER, name TEXT)');
|
||||
return db;
|
||||
};
|
||||
|
||||
it('receives correct parameters for SELECT operations', (t) => {
|
||||
const authorizer = t.mock.fn(() => constants.SQLITE_OK);
|
||||
const db = createTestDatabase();
|
||||
|
||||
db.setAuthorizer(authorizer);
|
||||
db.prepare('SELECT id FROM users').get();
|
||||
|
||||
assert.strictEqual(authorizer.mock.callCount(), 2);
|
||||
const callArguments = authorizer.mock.calls.map((call) => call.arguments);
|
||||
|
||||
assert.deepStrictEqual(
|
||||
callArguments,
|
||||
[
|
||||
[constants.SQLITE_SELECT, null, null, null, null],
|
||||
[constants.SQLITE_READ, 'users', 'id', 'main', null],
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
it('receives correct parameters for INSERT operations', (t) => {
|
||||
const authorizer = t.mock.fn(() => constants.SQLITE_OK);
|
||||
const db = createTestDatabase();
|
||||
|
||||
db.setAuthorizer(authorizer);
|
||||
db.prepare('INSERT INTO users (id, name) VALUES (?, ?)').run(1, 'node');
|
||||
|
||||
assert.strictEqual(authorizer.mock.callCount(), 1);
|
||||
|
||||
const callArguments = authorizer.mock.calls.map((call) => call.arguments);
|
||||
assert.deepStrictEqual(
|
||||
callArguments,
|
||||
[[constants.SQLITE_INSERT, 'users', null, 'main', null]],
|
||||
);
|
||||
});
|
||||
|
||||
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('ignores SELECT operations when authorizer returns SQLITE_IGNORE', () => {
|
||||
const db = createTestDatabase();
|
||||
db.prepare('INSERT INTO users (id, name) VALUES (?, ?)').run(1, 'Alice');
|
||||
|
||||
db.setAuthorizer((actionCode) => {
|
||||
if (actionCode === constants.SQLITE_SELECT) {
|
||||
return constants.SQLITE_IGNORE;
|
||||
}
|
||||
return constants.SQLITE_OK;
|
||||
});
|
||||
|
||||
// SELECT should be ignored and return no results
|
||||
const result = db.prepare('SELECT * FROM users').all();
|
||||
assert.deepStrictEqual(result, []);
|
||||
});
|
||||
|
||||
it('ignores READ operations when authorizer returns SQLITE_IGNORE', () => {
|
||||
const db = createTestDatabase();
|
||||
db.prepare('INSERT INTO users (id, name) VALUES (?, ?)').run(1, 'Alice');
|
||||
|
||||
db.setAuthorizer((actionCode, arg1, arg2) => {
|
||||
if (actionCode === constants.SQLITE_READ && arg1 === 'users' && arg2 === 'name') {
|
||||
return constants.SQLITE_IGNORE;
|
||||
}
|
||||
return constants.SQLITE_OK;
|
||||
});
|
||||
|
||||
// Reading the 'name' column should be ignored, returning NULL
|
||||
const result = db.prepare('SELECT id, name FROM users WHERE id = 1').get();
|
||||
assert.strictEqual(result.id, 1);
|
||||
assert.strictEqual(result.name, null);
|
||||
});
|
||||
|
||||
it('ignores INSERT operations when authorizer returns SQLITE_IGNORE', () => {
|
||||
const db = createTestDatabase();
|
||||
|
||||
db.setAuthorizer((actionCode) => {
|
||||
if (actionCode === constants.SQLITE_INSERT) {
|
||||
return constants.SQLITE_IGNORE;
|
||||
}
|
||||
return constants.SQLITE_OK;
|
||||
});
|
||||
|
||||
db.prepare('INSERT INTO users (id, name) VALUES (?, ?)').run(1, 'Alice');
|
||||
|
||||
// Verify no data was inserted
|
||||
const count = db.prepare('SELECT COUNT(*) as count FROM users').get();
|
||||
assert.strictEqual(count.count, 0);
|
||||
});
|
||||
|
||||
it('ignores UPDATE operations when authorizer returns SQLITE_IGNORE', () => {
|
||||
const db = createTestDatabase();
|
||||
db.exec("INSERT INTO users (id, name) VALUES (1, 'Alice')");
|
||||
|
||||
db.setAuthorizer((actionCode) => {
|
||||
if (actionCode === constants.SQLITE_UPDATE) {
|
||||
return constants.SQLITE_IGNORE;
|
||||
}
|
||||
return constants.SQLITE_OK;
|
||||
});
|
||||
|
||||
db.prepare('UPDATE users SET name = ? WHERE id = ?').run('Bob', 1);
|
||||
|
||||
// Verify data was not updated
|
||||
const result = db.prepare('SELECT name FROM users WHERE id = 1').get();
|
||||
assert.strictEqual(result.name, 'Alice');
|
||||
});
|
||||
|
||||
it('ignores DELETE operations when authorizer returns SQLITE_IGNORE', () => {
|
||||
const db = createTestDatabase();
|
||||
db.exec("INSERT INTO users (id, name) VALUES (1, 'Alice')");
|
||||
|
||||
db.setAuthorizer(() => constants.SQLITE_IGNORE);
|
||||
|
||||
db.prepare('DELETE FROM users WHERE id = ?').run(1);
|
||||
|
||||
db.setAuthorizer(null);
|
||||
|
||||
// Verify data was not deleted
|
||||
const count = db.prepare('SELECT COUNT(*) as count FROM users').get();
|
||||
assert.strictEqual(count.count, 1);
|
||||
});
|
||||
|
||||
it('rethrows error when authorizer throws error', () => {
|
||||
const db = new DatabaseSync(':memory:');
|
||||
db.setAuthorizer(() => {
|
||||
throw new Error('Unknown error');
|
||||
});
|
||||
|
||||
assert.throws(() => {
|
||||
db.exec('SELECT 1');
|
||||
}, {
|
||||
message: 'Unknown error'
|
||||
});
|
||||
});
|
||||
|
||||
it('throws error when authorizer returns nothing', () => {
|
||||
const db = new DatabaseSync(':memory:');
|
||||
db.setAuthorizer(() => {
|
||||
});
|
||||
|
||||
assert.throws(() => {
|
||||
db.exec('SELECT 1');
|
||||
}, {
|
||||
message: 'Authorizer callback must return an integer authorization code'
|
||||
});
|
||||
});
|
||||
|
||||
it('throws error when authorizer returns NaN', () => {
|
||||
const db = new DatabaseSync(':memory:');
|
||||
db.setAuthorizer(() => {
|
||||
return '1';
|
||||
});
|
||||
|
||||
assert.throws(() => {
|
||||
db.exec('SELECT 1');
|
||||
}, {
|
||||
message: 'Authorizer callback must return an integer authorization code'
|
||||
});
|
||||
});
|
||||
|
||||
it('throws error when authorizer returns a invalid code', () => {
|
||||
const db = new DatabaseSync(':memory:');
|
||||
db.setAuthorizer(() => {
|
||||
return 3;
|
||||
});
|
||||
|
||||
assert.throws(() => {
|
||||
db.exec('SELECT 1');
|
||||
}, {
|
||||
message: 'Authorizer callback returned a invalid authorization code'
|
||||
});
|
||||
});
|
||||
|
||||
it('clears authorizer when set to null', (t) => {
|
||||
const authorizer = t.mock.fn(() => constants.SQLITE_OK);
|
||||
const db = new DatabaseSync(':memory:');
|
||||
const statement = db.prepare('SELECT 1');
|
||||
|
||||
// Set authorizer and verify it's called
|
||||
db.setAuthorizer(authorizer);
|
||||
statement.run();
|
||||
assert.strictEqual(authorizer.mock.callCount(), 1);
|
||||
|
||||
// Clear authorizer and verify it's no longer called
|
||||
db.setAuthorizer(null);
|
||||
statement.run();
|
||||
assert.strictEqual(authorizer.mock.callCount(), 1);
|
||||
});
|
||||
|
||||
it('throws when callback is a string', () => {
|
||||
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/
|
||||
});
|
||||
});
|
||||
|
||||
it('throws when callback is a number', () => {
|
||||
const db = new DatabaseSync(':memory:');
|
||||
|
||||
assert.throws(() => {
|
||||
db.setAuthorizer(1);
|
||||
}, {
|
||||
code: 'ERR_INVALID_ARG_TYPE',
|
||||
message: /The "callback" argument must be a function/
|
||||
});
|
||||
});
|
||||
|
||||
it('throws when callback is an object', () => {
|
||||
const db = new DatabaseSync(':memory:');
|
||||
|
||||
assert.throws(() => {
|
||||
db.setAuthorizer({});
|
||||
}, {
|
||||
code: 'ERR_INVALID_ARG_TYPE',
|
||||
message: /The "callback" argument must be a function/
|
||||
});
|
||||
});
|
||||
|
||||
it('throws when callback is an array', () => {
|
||||
const db = new DatabaseSync(':memory:');
|
||||
|
||||
assert.throws(() => {
|
||||
db.setAuthorizer([]);
|
||||
}, {
|
||||
code: 'ERR_INVALID_ARG_TYPE',
|
||||
message: /The "callback" argument must be a function/
|
||||
});
|
||||
});
|
||||
|
||||
it('throws when callback is undefined', () => {
|
||||
const db = new DatabaseSync(':memory:');
|
||||
|
||||
assert.throws(() => {
|
||||
db.setAuthorizer();
|
||||
}, {
|
||||
code: 'ERR_INVALID_ARG_TYPE',
|
||||
message: /The "callback" argument must be a function/
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user