Skip to content

Commit fc9da3e

Browse files
committed
Add Database.authorizer() API
1 parent 0db0b14 commit fc9da3e

14 files changed

+281
-3
lines changed

auth.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* Authorization outcome.
3+
*
4+
* @readonly
5+
* @enum {number}
6+
* @property {number} ALLOW - Allow access to a resource.
7+
* @property {number} DENY - Deny access to a resource and throw an error.
8+
* @property {number} IGNORE - Don't return the value but keep running the query.
9+
*/
10+
const Authorization = {
11+
/**
12+
* Allow access to a resource.
13+
* @type {number}
14+
*/
15+
ALLOW: 0,
16+
17+
/**
18+
* Deny access to a resource and throw an error in `prepare()`.
19+
* @type {number}
20+
*/
21+
DENY: 1,
22+
23+
/**
24+
* Don't return the value but keep running the query.
25+
* @type {number}
26+
*/
27+
IGNORE: 2
28+
};
29+
module.exports = Authorization;

docs/api.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,33 @@ This function is currently not supported.
6767

6868
This function is currently not supported.
6969

70+
### authorizer(rules) ⇒ this
71+
72+
Configure authorization rules. The `rules` object is a map from table name to
73+
`Authorization` object, which defines if access to table is allowed, ignored,
74+
or denied. If a table has no authorization rule, access to it is _denied_ by
75+
default.
76+
77+
Example:
78+
79+
```javascript
80+
db.authorizer({
81+
"users": Authorization.ALLOW
82+
});
83+
84+
// Access is allowed.
85+
const stmt = db.prepare("SELECT * FROM users");
86+
87+
db.authorizer({
88+
"users": Authorization.DENY
89+
});
90+
91+
// Access is denied.
92+
const stmt = db.prepare("SELECT * FROM users");
93+
```
94+
95+
**Note: This is an experimental API and, therefore, subject to change.**
96+
7097
### loadExtension(path, [entryPoint]) ⇒ this
7198

7299
Loads a SQLite3 extension

index.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ const {
3333
databaseExecSync,
3434
databasePrepareSync,
3535
databaseDefaultSafeIntegers,
36+
databaseAuthorizer,
3637
databaseLoadExtension,
3738
databaseMaxWriteReplicationIndex,
3839
statementRaw,
@@ -46,6 +47,7 @@ const {
4647
rowsNext,
4748
} = requireNative();
4849

50+
const Authorization = require("./auth");
4951
const SqliteError = require("./sqlite-error");
5052

5153
function convertError(err) {
@@ -227,6 +229,10 @@ class Database {
227229
throw new Error("not implemented");
228230
}
229231

232+
authorizer(rules) {
233+
databaseAuthorizer.call(this.db, rules);
234+
}
235+
230236
loadExtension(...args) {
231237
databaseLoadExtension.call(this.db, ...args);
232238
}
@@ -425,4 +431,5 @@ class Statement {
425431
}
426432

427433
module.exports = Database;
434+
module.exports.Authorization = Authorization;
428435
module.exports.SqliteError = SqliteError;

integration-tests/tests/extensions.test.js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import test from "ava";
2+
import { Authorization } from "libsql";
23

34
test.serial("Statement.run() returning duration", async (t) => {
45
const db = t.context.db;
@@ -18,6 +19,44 @@ test.serial("Statement.get() returning duration", async (t) => {
1819
t.log(info._metadata?.duration)
1920
});
2021

22+
test.serial("Database.authorizer()/allow", async (t) => {
23+
const db = t.context.db;
24+
25+
db.authorizer({
26+
"users": Authorization.ALLOW
27+
});
28+
29+
const stmt = db.prepare("SELECT * FROM users");
30+
const users = stmt.all();
31+
t.is(users.length, 2);
32+
});
33+
34+
test.serial("Database.authorizer()/ignore", async (t) => {
35+
const db = t.context.db;
36+
37+
db.authorizer({
38+
"users": Authorization.IGNORE
39+
});
40+
41+
const stmt = db.prepare("SELECT * FROM users");
42+
const users = stmt.all();
43+
t.is(users.length, 0);
44+
});
45+
46+
test.serial("Database.authorizer()/deny", async (t) => {
47+
const db = t.context.db;
48+
49+
db.authorizer({
50+
"users": Authorization.DENY
51+
});
52+
await t.throwsAsync(async () => {
53+
return await db.prepare("SELECT * FROM users");
54+
}, {
55+
instanceOf: t.context.errorType,
56+
code: "SQLITE_AUTH"
57+
});
58+
});
59+
2160
const connect = async (path_opt) => {
2261
const path = path_opt ?? "hello.db";
2362
const x = await import("libsql");

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"main": "index.js",
1616
"types": "types/index.d.ts",
1717
"files": [
18+
"auth.js",
1819
"index.js",
1920
"sqlite-error.js",
2021
"promise.js",

promise.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ if (0) {
88
require("./.targets");
99
}
1010

11+
const Authorization = require("./auth");
1112
const SqliteError = require("./sqlite-error");
1213

1314
function convertError(err) {
@@ -48,6 +49,7 @@ const {
4849
databasePrepareAsync,
4950
databaseMaxWriteReplicationIndex,
5051
databaseDefaultSafeIntegers,
52+
databaseAuthorizer,
5153
databaseLoadExtension,
5254
statementRaw,
5355
statementIsReader,
@@ -231,6 +233,10 @@ class Database {
231233
throw new Error("not implemented");
232234
}
233235

236+
authorizer(rules) {
237+
databaseAuthorizer.call(this.db, rules);
238+
}
239+
234240
loadExtension(...args) {
235241
databaseLoadExtension.call(this.db, ...args);
236242
}
@@ -429,4 +435,5 @@ class Statement {
429435
}
430436

431437
module.exports = Database;
438+
module.exports.Authorization = Authorization;
432439
module.exports.SqliteError = SqliteError;

src/auth.rs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
use tracing::trace;
2+
3+
use std::collections::HashSet;
4+
5+
pub struct AuthorizerBuilder {
6+
allow_list: HashSet<String>,
7+
ignore_list: HashSet<String>,
8+
deny_list: HashSet<String>,
9+
}
10+
11+
impl AuthorizerBuilder {
12+
pub fn new() -> Self {
13+
Self {
14+
allow_list: HashSet::new(),
15+
ignore_list: HashSet::new(),
16+
deny_list: HashSet::new(),
17+
}
18+
}
19+
20+
pub fn allow(&mut self, table: &str) -> &mut Self {
21+
self.allow_list.insert(table.to_string());
22+
self
23+
}
24+
25+
pub fn ignore(&mut self, table: &str) -> &mut Self {
26+
self.ignore_list.insert(table.to_string());
27+
self
28+
}
29+
30+
pub fn deny(&mut self, table: &str) -> &mut Self {
31+
self.deny_list.insert(table.to_string());
32+
self
33+
}
34+
35+
pub fn build(self) -> Authorizer {
36+
Authorizer::new(self.allow_list, self.ignore_list, self.deny_list)
37+
}
38+
}
39+
40+
pub struct Authorizer {
41+
allow_list: HashSet<String>,
42+
ignore_list: HashSet<String>,
43+
deny_list: HashSet<String>,
44+
}
45+
46+
impl Authorizer {
47+
pub fn new(
48+
allow_list: HashSet<String>,
49+
ignore_list: HashSet<String>,
50+
deny_list: HashSet<String>,
51+
) -> Self {
52+
Self {
53+
allow_list,
54+
ignore_list,
55+
deny_list,
56+
}
57+
}
58+
59+
pub fn authorize(&self, ctx: &libsql::AuthContext) -> libsql::Authorization {
60+
use libsql::AuthAction;
61+
let ret = match ctx.action {
62+
AuthAction::Unknown { .. } => libsql::Authorization::Deny,
63+
AuthAction::CreateIndex { table_name, .. } => self.authorize_table(table_name),
64+
AuthAction::CreateTable { table_name, .. } => self.authorize_table(table_name),
65+
AuthAction::CreateTempIndex { table_name, .. } => self.authorize_table(table_name),
66+
AuthAction::CreateTempTable { table_name, .. } => self.authorize_table(table_name),
67+
AuthAction::CreateTempTrigger { table_name, .. } => self.authorize_table(table_name),
68+
AuthAction::CreateTempView { .. } => libsql::Authorization::Deny,
69+
AuthAction::CreateTrigger { table_name, .. } => self.authorize_table(table_name),
70+
AuthAction::CreateView { .. } => libsql::Authorization::Deny,
71+
AuthAction::Delete { table_name, .. } => self.authorize_table(table_name),
72+
AuthAction::DropIndex { table_name, .. } => self.authorize_table(table_name),
73+
AuthAction::DropTable { table_name, .. } => self.authorize_table(table_name),
74+
AuthAction::DropTempIndex { table_name, .. } => self.authorize_table(table_name),
75+
AuthAction::DropTempTable { table_name, .. } => self.authorize_table(table_name),
76+
AuthAction::DropTempTrigger { table_name, .. } => self.authorize_table(table_name),
77+
AuthAction::DropTempView { .. } => libsql::Authorization::Deny,
78+
AuthAction::DropTrigger { .. } => libsql::Authorization::Deny,
79+
AuthAction::DropView { .. } => libsql::Authorization::Deny,
80+
AuthAction::Insert { table_name, .. } => self.authorize_table(table_name),
81+
AuthAction::Pragma { .. } => libsql::Authorization::Deny,
82+
AuthAction::Read { table_name, .. } => self.authorize_table(table_name),
83+
AuthAction::Select { .. } => libsql::Authorization::Allow,
84+
AuthAction::Transaction { .. } => libsql::Authorization::Deny,
85+
AuthAction::Update { table_name, .. } => self.authorize_table(table_name),
86+
AuthAction::Attach { .. } => libsql::Authorization::Deny,
87+
AuthAction::Detach { .. } => libsql::Authorization::Deny,
88+
AuthAction::AlterTable { table_name, .. } => self.authorize_table(table_name),
89+
AuthAction::Reindex { .. } => libsql::Authorization::Deny,
90+
AuthAction::Analyze { .. } => libsql::Authorization::Deny,
91+
AuthAction::CreateVtable { .. } => libsql::Authorization::Deny,
92+
AuthAction::DropVtable { .. } => libsql::Authorization::Deny,
93+
AuthAction::Function { .. } => libsql::Authorization::Deny,
94+
AuthAction::Savepoint { .. } => libsql::Authorization::Deny,
95+
AuthAction::Recursive { .. } => libsql::Authorization::Deny,
96+
};
97+
trace!("authorize(ctx = {:?}) -> {:?}", ctx, ret);
98+
ret
99+
}
100+
101+
fn authorize_table(&self, table: &str) -> libsql::Authorization {
102+
if self.deny_list.contains(table) {
103+
return libsql::Authorization::Deny;
104+
}
105+
if self.ignore_list.contains(table) {
106+
return libsql::Authorization::Ignore;
107+
}
108+
if self.allow_list.contains(table) {
109+
return libsql::Authorization::Allow;
110+
}
111+
libsql::Authorization::Deny
112+
}
113+
}

src/database.rs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::time::Duration;
77
use tokio::sync::Mutex;
88
use tracing::trace;
99

10+
use crate::auth::AuthorizerBuilder;
1011
use crate::errors::{throw_database_closed_error, throw_libsql_error};
1112
use crate::runtime;
1213
use crate::Statement;
@@ -371,6 +372,42 @@ impl Database {
371372
self.default_safe_integers.replace(toggle);
372373
}
373374

375+
pub fn js_authorizer(mut cx: FunctionContext) -> JsResult<JsUndefined> {
376+
let db: Handle<'_, JsBox<Database>> = cx.this()?;
377+
let rules_obj = cx.argument::<JsObject>(0)?;
378+
let conn = match db.get_conn(&mut cx) {
379+
Some(conn) => conn,
380+
None => throw_database_closed_error(&mut cx)?,
381+
};
382+
let mut auth = AuthorizerBuilder::new();
383+
let prop_names: Handle<JsArray> = rules_obj.get_own_property_names(&mut cx)?;
384+
let prop_len = prop_names.len(&mut cx);
385+
for i in 0..prop_len {
386+
let key_js = prop_names.get::<JsString, _, _>(&mut cx, i)?;
387+
let key: String = key_js.to_string(&mut cx)?.value(&mut cx);
388+
let value = rules_obj.get::<JsNumber, _, _>(&mut cx, key.as_str())?;
389+
let value = value.value(&mut cx) as i32;
390+
if value == 0 {
391+
// Authorization.ALLOW
392+
auth.allow(&key);
393+
} else if value == 1 {
394+
// Authorization.DENY
395+
auth.deny(&key);
396+
} else if value == 2 {
397+
// Authorization.IGNORE
398+
auth.ignore(&key);
399+
}
400+
}
401+
let auth = auth.build();
402+
if let Err(err) = conn
403+
.blocking_lock()
404+
.authorizer(Some(Arc::new(move |ctx| auth.authorize(ctx))))
405+
{
406+
throw_libsql_error(&mut cx, err)?;
407+
}
408+
Ok(cx.undefined())
409+
}
410+
374411
pub fn js_load_extension(mut cx: FunctionContext) -> JsResult<JsUndefined> {
375412
let db: Handle<'_, JsBox<Database>> = cx.this()?;
376413
let extension = cx.argument::<JsString>(0)?.value(&mut cx);

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
mod auth;
12
mod database;
23
mod errors;
34
mod statement;
@@ -44,6 +45,7 @@ fn main(mut cx: ModuleContext) -> NeonResult<()> {
4445
"databaseDefaultSafeIntegers",
4546
Database::js_default_safe_integers,
4647
)?;
48+
cx.export_function("databaseAuthorizer", Database::js_authorizer)?;
4749
cx.export_function("databaseLoadExtension", Database::js_load_extension)?;
4850
cx.export_function(
4951
"databaseMaxWriteReplicationIndex",

src/statement.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,9 @@ impl Statement {
122122
pub fn js_interrupt(mut cx: FunctionContext) -> JsResult<JsNull> {
123123
let stmt: Handle<'_, JsBox<Statement>> = cx.this()?;
124124
let mut raw_stmt = stmt.stmt.blocking_lock();
125-
raw_stmt.interrupt().or_else(|err| throw_libsql_error(&mut cx, err))?;
125+
raw_stmt
126+
.interrupt()
127+
.or_else(|err| throw_libsql_error(&mut cx, err))?;
126128
Ok(cx.null())
127129
}
128130

types/auth.d.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
export = Authorization;
2+
/**
3+
* *
4+
*/
5+
type Authorization = number;
6+
declare namespace Authorization {
7+
let ALLOW: number;
8+
let DENY: number;
9+
let IGNORE: number;
10+
}
11+
//# sourceMappingURL=auth.d.ts.map

types/auth.d.ts.map

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)