Skip to content

Add Database.authorizer() API #180

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 23, 2025
Merged
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
22 changes: 22 additions & 0 deletions auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Authorization outcome.
*
* @readonly
* @enum {number}
* @property {number} ALLOW - Allow access to a resource.
* @property {number} DENY - Deny access to a resource and throw an error.
*/
const Authorization = {
/**
* Allow access to a resource.
* @type {number}
*/
ALLOW: 0,

/**
* Deny access to a resource and throw an error in `prepare()`.
* @type {number}
*/
DENY: 1,
};
module.exports = Authorization;
26 changes: 26 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,32 @@ This function is currently not supported.

This function is currently not supported.

### authorizer(rules) ⇒ this

Configure authorization rules. The `rules` object is a map from table name to
`Authorization` object, which defines if access to table is allowed or denied.
If a table has no authorization rule, access to it is _denied_ by default.

Example:

```javascript
db.authorizer({
"users": Authorization.ALLOW
});

// Access is allowed.
const stmt = db.prepare("SELECT * FROM users");

db.authorizer({
"users": Authorization.DENY
});

// Access is denied.
const stmt = db.prepare("SELECT * FROM users");
```

**Note: This is an experimental API and, therefore, subject to change.**

### loadExtension(path, [entryPoint]) ⇒ this

Loads a SQLite3 extension
Expand Down
7 changes: 7 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ const {
databaseExecSync,
databasePrepareSync,
databaseDefaultSafeIntegers,
databaseAuthorizer,
databaseLoadExtension,
databaseMaxWriteReplicationIndex,
statementRaw,
Expand All @@ -46,6 +47,7 @@ const {
rowsNext,
} = requireNative();

const Authorization = require("./auth");
const SqliteError = require("./sqlite-error");

function convertError(err) {
Expand Down Expand Up @@ -227,6 +229,10 @@ class Database {
throw new Error("not implemented");
}

authorizer(rules) {
databaseAuthorizer.call(this.db, rules);
}

loadExtension(...args) {
databaseLoadExtension.call(this.db, ...args);
}
Expand Down Expand Up @@ -425,4 +431,5 @@ class Statement {
}

module.exports = Database;
module.exports.Authorization = Authorization;
module.exports.SqliteError = SqliteError;
27 changes: 27 additions & 0 deletions integration-tests/tests/extensions.test.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import test from "ava";
import { Authorization } from "libsql";

test.serial("Statement.run() returning duration", async (t) => {
const db = t.context.db;
Expand All @@ -18,6 +19,32 @@ test.serial("Statement.get() returning duration", async (t) => {
t.log(info._metadata?.duration)
});

test.serial("Database.authorizer()/allow", async (t) => {
const db = t.context.db;

db.authorizer({
"users": Authorization.ALLOW
});

const stmt = db.prepare("SELECT * FROM users");
const users = stmt.all();
t.is(users.length, 2);
});

test.serial("Database.authorizer()/deny", async (t) => {
const db = t.context.db;

db.authorizer({
"users": Authorization.DENY
});
await t.throwsAsync(async () => {
return await db.prepare("SELECT * FROM users");
}, {
instanceOf: t.context.errorType,
code: "SQLITE_AUTH"
});
});

const connect = async (path_opt) => {
const path = path_opt ?? "hello.db";
const x = await import("libsql");
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"main": "index.js",
"types": "types/index.d.ts",
"files": [
"auth.js",
"index.js",
"sqlite-error.js",
"promise.js",
Expand Down
7 changes: 7 additions & 0 deletions promise.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ if (0) {
require("./.targets");
}

const Authorization = require("./auth");
const SqliteError = require("./sqlite-error");

function convertError(err) {
Expand Down Expand Up @@ -48,6 +49,7 @@ const {
databasePrepareAsync,
databaseMaxWriteReplicationIndex,
databaseDefaultSafeIntegers,
databaseAuthorizer,
databaseLoadExtension,
statementRaw,
statementIsReader,
Expand Down Expand Up @@ -231,6 +233,10 @@ class Database {
throw new Error("not implemented");
}

authorizer(rules) {
databaseAuthorizer.call(this.db, rules);
}

loadExtension(...args) {
databaseLoadExtension.call(this.db, ...args);
}
Expand Down Expand Up @@ -429,4 +435,5 @@ class Statement {
}

module.exports = Database;
module.exports.Authorization = Authorization;
module.exports.SqliteError = SqliteError;
100 changes: 100 additions & 0 deletions src/auth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
use tracing::trace;

use std::collections::HashSet;

pub struct AuthorizerBuilder {
allow_list: HashSet<String>,
deny_list: HashSet<String>,
}

impl AuthorizerBuilder {
pub fn new() -> Self {
Self {
allow_list: HashSet::new(),
deny_list: HashSet::new(),
}
}

pub fn allow(&mut self, table: &str) -> &mut Self {
self.allow_list.insert(table.to_string());
self
}

pub fn deny(&mut self, table: &str) -> &mut Self {
self.deny_list.insert(table.to_string());
self
}

pub fn build(self) -> Authorizer {
Authorizer::new(self.allow_list, self.deny_list)
}
}

pub struct Authorizer {
allow_list: HashSet<String>,
deny_list: HashSet<String>,
}

impl Authorizer {
pub fn new(
allow_list: HashSet<String>,
deny_list: HashSet<String>,
) -> Self {
Self {
allow_list,
deny_list,
}
}

pub fn authorize(&self, ctx: &libsql::AuthContext) -> libsql::Authorization {
use libsql::AuthAction;
let ret = match ctx.action {
AuthAction::Unknown { .. } => libsql::Authorization::Deny,
AuthAction::CreateIndex { table_name, .. } => self.authorize_table(table_name),
AuthAction::CreateTable { table_name, .. } => self.authorize_table(table_name),
AuthAction::CreateTempIndex { table_name, .. } => self.authorize_table(table_name),
AuthAction::CreateTempTable { table_name, .. } => self.authorize_table(table_name),
AuthAction::CreateTempTrigger { table_name, .. } => self.authorize_table(table_name),
AuthAction::CreateTempView { .. } => libsql::Authorization::Deny,
AuthAction::CreateTrigger { table_name, .. } => self.authorize_table(table_name),
AuthAction::CreateView { .. } => libsql::Authorization::Deny,
AuthAction::Delete { table_name, .. } => self.authorize_table(table_name),
AuthAction::DropIndex { table_name, .. } => self.authorize_table(table_name),
AuthAction::DropTable { table_name, .. } => self.authorize_table(table_name),
AuthAction::DropTempIndex { table_name, .. } => self.authorize_table(table_name),
AuthAction::DropTempTable { table_name, .. } => self.authorize_table(table_name),
AuthAction::DropTempTrigger { table_name, .. } => self.authorize_table(table_name),
AuthAction::DropTempView { .. } => libsql::Authorization::Deny,
AuthAction::DropTrigger { .. } => libsql::Authorization::Deny,
AuthAction::DropView { .. } => libsql::Authorization::Deny,
AuthAction::Insert { table_name, .. } => self.authorize_table(table_name),
AuthAction::Pragma { .. } => libsql::Authorization::Deny,
AuthAction::Read { table_name, .. } => self.authorize_table(table_name),
AuthAction::Select { .. } => libsql::Authorization::Allow,
AuthAction::Transaction { .. } => libsql::Authorization::Deny,
AuthAction::Update { table_name, .. } => self.authorize_table(table_name),
AuthAction::Attach { .. } => libsql::Authorization::Deny,
AuthAction::Detach { .. } => libsql::Authorization::Deny,
AuthAction::AlterTable { table_name, .. } => self.authorize_table(table_name),
AuthAction::Reindex { .. } => libsql::Authorization::Deny,
AuthAction::Analyze { .. } => libsql::Authorization::Deny,
AuthAction::CreateVtable { .. } => libsql::Authorization::Deny,
AuthAction::DropVtable { .. } => libsql::Authorization::Deny,
AuthAction::Function { .. } => libsql::Authorization::Deny,
AuthAction::Savepoint { .. } => libsql::Authorization::Deny,
AuthAction::Recursive { .. } => libsql::Authorization::Deny,
};
trace!("authorize(ctx = {:?}) -> {:?}", ctx, ret);
ret
}

fn authorize_table(&self, table: &str) -> libsql::Authorization {
if self.deny_list.contains(table) {
return libsql::Authorization::Deny;
}
if self.allow_list.contains(table) {
return libsql::Authorization::Allow;
}
libsql::Authorization::Deny
}
}
34 changes: 34 additions & 0 deletions src/database.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use std::time::Duration;
use tokio::sync::Mutex;
use tracing::trace;

use crate::auth::AuthorizerBuilder;
use crate::errors::{throw_database_closed_error, throw_libsql_error};
use crate::runtime;
use crate::Statement;
Expand Down Expand Up @@ -371,6 +372,39 @@ impl Database {
self.default_safe_integers.replace(toggle);
}

pub fn js_authorizer(mut cx: FunctionContext) -> JsResult<JsUndefined> {
let db: Handle<'_, JsBox<Database>> = cx.this()?;
let rules_obj = cx.argument::<JsObject>(0)?;
let conn = match db.get_conn(&mut cx) {
Some(conn) => conn,
None => throw_database_closed_error(&mut cx)?,
};
let mut auth = AuthorizerBuilder::new();
let prop_names: Handle<JsArray> = rules_obj.get_own_property_names(&mut cx)?;
let prop_len = prop_names.len(&mut cx);
for i in 0..prop_len {
let key_js = prop_names.get::<JsString, _, _>(&mut cx, i)?;
let key: String = key_js.to_string(&mut cx)?.value(&mut cx);
let value = rules_obj.get::<JsNumber, _, _>(&mut cx, key.as_str())?;
let value = value.value(&mut cx) as i32;
if value == 0 {
// Authorization.ALLOW
auth.allow(&key);
} else if value == 1 {
// Authorization.DENY
auth.deny(&key);
}
}
let auth = auth.build();
if let Err(err) = conn
.blocking_lock()
.authorizer(Some(Arc::new(move |ctx| auth.authorize(ctx))))
{
throw_libsql_error(&mut cx, err)?;
}
Ok(cx.undefined())
}

pub fn js_load_extension(mut cx: FunctionContext) -> JsResult<JsUndefined> {
let db: Handle<'_, JsBox<Database>> = cx.this()?;
let extension = cx.argument::<JsString>(0)?.value(&mut cx);
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod auth;
mod database;
mod errors;
mod statement;
Expand Down Expand Up @@ -44,6 +45,7 @@ fn main(mut cx: ModuleContext) -> NeonResult<()> {
"databaseDefaultSafeIntegers",
Database::js_default_safe_integers,
)?;
cx.export_function("databaseAuthorizer", Database::js_authorizer)?;
cx.export_function("databaseLoadExtension", Database::js_load_extension)?;
cx.export_function(
"databaseMaxWriteReplicationIndex",
Expand Down
4 changes: 3 additions & 1 deletion src/statement.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,9 @@ impl Statement {
pub fn js_interrupt(mut cx: FunctionContext) -> JsResult<JsNull> {
let stmt: Handle<'_, JsBox<Statement>> = cx.this()?;
let mut raw_stmt = stmt.stmt.blocking_lock();
raw_stmt.interrupt().or_else(|err| throw_libsql_error(&mut cx, err))?;
raw_stmt
.interrupt()
.or_else(|err| throw_libsql_error(&mut cx, err))?;
Ok(cx.null())
}

Expand Down
10 changes: 10 additions & 0 deletions types/auth.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export = Authorization;
/**
* *
*/
type Authorization = number;
declare namespace Authorization {
let ALLOW: number;
let DENY: number;
}
//# sourceMappingURL=auth.d.ts.map
1 change: 1 addition & 0 deletions types/auth.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion types/promise.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ declare class Database {
function(name: any, options: any, fn: any): void;
aggregate(name: any, options: any): void;
table(name: any, factory: any): void;
authorizer(rules: any): void;
loadExtension(...args: any[]): void;
maxWriteReplicationIndex(): any;
/**
Expand All @@ -58,7 +59,8 @@ declare class Database {
unsafeMode(...args: any[]): void;
}
declare namespace Database {
export { SqliteError };
export { Authorization, SqliteError };
}
import Authorization = require("./auth");
import SqliteError = require("./sqlite-error");
//# sourceMappingURL=promise.d.ts.map
2 changes: 1 addition & 1 deletion types/promise.d.ts.map

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading