Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ dist
.vscode-test

# End of https://www.toptal.com/developers/gitignore/api/node

integration-tests/*.db
# Created by https://www.toptal.com/developers/gitignore/api/macos
# Edit at https://www.toptal.com/developers/gitignore?templates=macos

Expand Down
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ edition = "2021"
crate-type = ["cdylib"]

[dependencies]
glob-match = "0.2"
libsql = { version = "0.9.30", features = ["encryption"] }
napi = { version = "2", default-features = false, features = ["napi6", "tokio_rt", "async"] }
napi-derive = "2"
Expand Down
53 changes: 52 additions & 1 deletion auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* @enum {number}
* @property {number} ALLOW - Allow access to a resource.
* @property {number} DENY - Deny access to a resource and throw an error.
* @property {number} IGNORE - For READ: return NULL instead of the column value. For other actions: equivalent to DENY.
*/
const Authorization = {
/**
Expand All @@ -18,5 +19,55 @@ const Authorization = {
* @type {number}
*/
DENY: 1,

/**
* For READ: return NULL instead of the actual column value.
* For other actions: equivalent to DENY.
* @type {number}
*/
IGNORE: 2,
};

/**
* SQLite authorizer action codes.
*
* @readonly
* @enum {number}
*/
const Action = {
CREATE_INDEX: 1,
CREATE_TABLE: 2,
CREATE_TEMP_INDEX: 3,
CREATE_TEMP_TABLE: 4,
CREATE_TEMP_TRIGGER: 5,
CREATE_TEMP_VIEW: 6,
CREATE_TRIGGER: 7,
CREATE_VIEW: 8,
DELETE: 9,
DROP_INDEX: 10,
DROP_TABLE: 11,
DROP_TEMP_INDEX: 12,
DROP_TEMP_TABLE: 13,
DROP_TEMP_TRIGGER: 14,
DROP_TEMP_VIEW: 15,
DROP_TRIGGER: 16,
DROP_VIEW: 17,
INSERT: 18,
PRAGMA: 19,
READ: 20,
SELECT: 21,
TRANSACTION: 22,
UPDATE: 23,
ATTACH: 24,
DETACH: 25,
ALTER_TABLE: 26,
REINDEX: 27,
ANALYZE: 28,
CREATE_VTABLE: 29,
DROP_VTABLE: 30,
FUNCTION: 31,
SAVEPOINT: 32,
RECURSIVE: 33,
};
module.exports = Authorization;

module.exports = { Authorization, Action };
19 changes: 8 additions & 11 deletions compat.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

const { Database: NativeDb, databasePrepareSync, databaseSyncSync, databaseExecSync, statementRunSync, statementGetSync, statementIterateSync, iteratorNextSync } = require("./index.js");
const SqliteError = require("./sqlite-error.js");
const Authorization = require("./auth");
const { Authorization, Action } = require("./auth");

function convertError(err) {
// Handle errors from Rust with JSON-encoded message
Expand Down Expand Up @@ -167,14 +167,6 @@ class Database {
throw new Error("not implemented");
}

authorizer(rules) {
try {
this.db.authorizer(rules);
} catch (err) {
throw convertError(err);
}
}

loadExtension(...args) {
try {
this.db.loadExtension(...args);
Expand Down Expand Up @@ -218,8 +210,12 @@ class Database {
this.db.close();
}

authorizer(hook) {
this.db.authorizer(hook);
authorizer(config) {
try {
this.db.authorizer(config);
} catch (err) {
throw convertError(err);
}
return this;
}

Expand Down Expand Up @@ -372,3 +368,4 @@ class Statement {
module.exports = Database;
module.exports.SqliteError = SqliteError;
module.exports.Authorization = Authorization;
module.exports.Action = Action;
103 changes: 92 additions & 11 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,31 +68,112 @@ This function is currently not supported.

This function is currently not supported.

### authorizer(rules) ⇒ this
### authorizer(config) ⇒ 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.
Configure authorization rules for the database. Accepts three formats:

Example:
- **Legacy format** — a map from table name to `Authorization.ALLOW` or `Authorization.DENY`
- **Rule-based format** — an `AuthorizerConfig` object with ordered rules and pattern matching
- **`null`** — removes the authorizer entirely

#### Legacy format

A simple object mapping table names to `Authorization.ALLOW` (0) or `Authorization.DENY` (1).
Tables without an entry are denied by default.

```javascript
const { Authorization } = require('libsql');

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

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

// Access to "secrets" throws SQLITE_AUTH.
const stmt = db.prepare("SELECT * FROM secrets"); // Error!
```

#### Rule-based format

An object with a `rules` array and an optional `defaultPolicy`. Rules are evaluated in order — **first match wins**. If no rule matches, `defaultPolicy` applies (defaults to `DENY`).

```javascript
const { Authorization, Action } = require('libsql');

db.authorizer({
"users": Authorization.DENY
rules: [
// Hide sensitive columns (returns NULL instead of the real value)
{ action: Action.READ, table: "users", column: "password_hash", policy: Authorization.IGNORE },
{ action: Action.READ, table: "users", column: "ssn", policy: Authorization.IGNORE },

// Allow all reads
{ action: Action.READ, policy: Authorization.ALLOW },

// Allow inserts on tables matching a glob pattern
{ action: Action.INSERT, table: { glob: "logs_*" }, policy: Authorization.ALLOW },

// Deny DDL operations
{ action: [Action.CREATE_TABLE, Action.DROP_TABLE, Action.ALTER_TABLE], policy: Authorization.DENY },

// Allow transactions and selects
{ action: Action.TRANSACTION, policy: Authorization.ALLOW },
{ action: Action.SELECT, policy: Authorization.ALLOW },
],
defaultPolicy: Authorization.DENY,
});
```

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

| Field | Type | Description |
| -------- | ----------------------------------------- | -------------------------------------------------------------------- |
| action | <code>number \| number[]</code> | Action code(s) to match (from `Action`). Omit to match all actions. |
| table | <code>string \| { glob: string }</code> | Table name pattern. Omit to match any table. |
| column | <code>string \| { glob: string }</code> | Column name pattern (relevant for READ/UPDATE). Omit to match any. |
| entity | <code>string \| { glob: string }</code> | Entity name (index, trigger, view, pragma, function). Omit to match any. |
| policy | <code>number</code> | `Authorization.ALLOW`, `Authorization.DENY`, or `Authorization.IGNORE`. |

#### Pattern matching

Pattern fields (`table`, `column`, `entity`) accept either:

- A **plain string** for exact matching: `"users"`
- An **object with a `glob` key** for glob matching: `{ glob: "logs_*" }`

Glob patterns support `*` (match any number of characters) and `?` (match exactly one character).

```javascript
// Exact match
{ action: Action.READ, table: "users", policy: Authorization.ALLOW }

// Glob: all tables starting with "logs_"
{ action: Action.READ, table: { glob: "logs_*" }, policy: Authorization.ALLOW }

// Glob: single-character wildcard
{ action: Action.READ, table: { glob: "t?_data" }, policy: Authorization.ALLOW }

// Glob: match all tables
{ action: Action.READ, table: { glob: "*" }, policy: Authorization.ALLOW }
```

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

| Value | Effect |
| -------------------------- | ---------------------------------------------------------------------- |
| `Authorization.ALLOW` (0) | Permit the operation. |
| `Authorization.DENY` (1) | Reject the entire SQL statement with a `SQLITE_AUTH` error. |
| `Authorization.IGNORE` (2) | For READ: return NULL instead of the column value. Otherwise: deny. |

#### Removing the authorizer

Pass `null` to remove the authorizer and allow all operations:

```javascript
db.authorizer(null);
```

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

Expand Down
23 changes: 5 additions & 18 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,25 +76,12 @@ export declare class Database {
/**
* Sets the authorizer for the database.
*
* # Arguments
*
* * `env` - The environment.
* * `rules_obj` - The rules object.
*
* The `rules_obj` is a JavaScript object with the following properties:
*
* * `Authorization.ALLOW` - Allow access to the table.
* * `Authorization.DENY` - Deny access to the table.
*
* Example:
*
* ```javascript
* db.authorizer({
* "users": Authorization.ALLOW
* });
* ```
* Accepts either:
* - Legacy format: `{ [tableName: string]: 0 | 1 }`
* - Full format: `{ rules: AuthRule[], defaultPolicy?: 0 | 1 | 2 }`
* - `null` to remove the authorizer
*/
authorizer(rulesObj: object): void
authorizer(config: unknown): void
/**
* Loads an extension into the database.
*
Expand Down
Loading
Loading