Skip to content

Commit 8dcaf9d

Browse files
authored
Authorizer api (#215)
2 parents 0ac7e3a + d18582d commit 8dcaf9d

10 files changed

Lines changed: 1223 additions & 155 deletions

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ dist
111111
.vscode-test
112112

113113
# End of https://www.toptal.com/developers/gitignore/api/node
114-
114+
integration-tests/*.db
115115
# Created by https://www.toptal.com/developers/gitignore/api/macos
116116
# Edit at https://www.toptal.com/developers/gitignore?templates=macos
117117

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ edition = "2021"
1010
crate-type = ["cdylib"]
1111

1212
[dependencies]
13+
glob-match = "0.2"
1314
libsql = { version = "0.9.30", features = ["encryption"] }
1415
napi = { version = "2", default-features = false, features = ["napi6", "tokio_rt", "async"] }
1516
napi-derive = "2"

auth.js

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* @enum {number}
66
* @property {number} ALLOW - Allow access to a resource.
77
* @property {number} DENY - Deny access to a resource and throw an error.
8+
* @property {number} IGNORE - For READ: return NULL instead of the column value. For other actions: equivalent to DENY.
89
*/
910
const Authorization = {
1011
/**
@@ -18,5 +19,55 @@ const Authorization = {
1819
* @type {number}
1920
*/
2021
DENY: 1,
22+
23+
/**
24+
* For READ: return NULL instead of the actual column value.
25+
* For other actions: equivalent to DENY.
26+
* @type {number}
27+
*/
28+
IGNORE: 2,
29+
};
30+
31+
/**
32+
* SQLite authorizer action codes.
33+
*
34+
* @readonly
35+
* @enum {number}
36+
*/
37+
const Action = {
38+
CREATE_INDEX: 1,
39+
CREATE_TABLE: 2,
40+
CREATE_TEMP_INDEX: 3,
41+
CREATE_TEMP_TABLE: 4,
42+
CREATE_TEMP_TRIGGER: 5,
43+
CREATE_TEMP_VIEW: 6,
44+
CREATE_TRIGGER: 7,
45+
CREATE_VIEW: 8,
46+
DELETE: 9,
47+
DROP_INDEX: 10,
48+
DROP_TABLE: 11,
49+
DROP_TEMP_INDEX: 12,
50+
DROP_TEMP_TABLE: 13,
51+
DROP_TEMP_TRIGGER: 14,
52+
DROP_TEMP_VIEW: 15,
53+
DROP_TRIGGER: 16,
54+
DROP_VIEW: 17,
55+
INSERT: 18,
56+
PRAGMA: 19,
57+
READ: 20,
58+
SELECT: 21,
59+
TRANSACTION: 22,
60+
UPDATE: 23,
61+
ATTACH: 24,
62+
DETACH: 25,
63+
ALTER_TABLE: 26,
64+
REINDEX: 27,
65+
ANALYZE: 28,
66+
CREATE_VTABLE: 29,
67+
DROP_VTABLE: 30,
68+
FUNCTION: 31,
69+
SAVEPOINT: 32,
70+
RECURSIVE: 33,
2171
};
22-
module.exports = Authorization;
72+
73+
module.exports = { Authorization, Action };

compat.js

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

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

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

170-
authorizer(rules) {
171-
try {
172-
this.db.authorizer(rules);
173-
} catch (err) {
174-
throw convertError(err);
175-
}
176-
}
177-
178170
loadExtension(...args) {
179171
try {
180172
this.db.loadExtension(...args);
@@ -218,8 +210,12 @@ class Database {
218210
this.db.close();
219211
}
220212

221-
authorizer(hook) {
222-
this.db.authorizer(hook);
213+
authorizer(config) {
214+
try {
215+
this.db.authorizer(config);
216+
} catch (err) {
217+
throw convertError(err);
218+
}
223219
return this;
224220
}
225221

@@ -372,3 +368,4 @@ class Statement {
372368
module.exports = Database;
373369
module.exports.SqliteError = SqliteError;
374370
module.exports.Authorization = Authorization;
371+
module.exports.Action = Action;

docs/api.md

Lines changed: 92 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -68,31 +68,112 @@ This function is currently not supported.
6868

6969
This function is currently not supported.
7070

71-
### authorizer(rules) ⇒ this
71+
### authorizer(config) ⇒ this
7272

73-
Configure authorization rules. The `rules` object is a map from table name to
74-
`Authorization` object, which defines if access to table is allowed or denied.
75-
If a table has no authorization rule, access to it is _denied_ by default.
73+
Configure authorization rules for the database. Accepts three formats:
7674

77-
Example:
75+
- **Legacy format** — a map from table name to `Authorization.ALLOW` or `Authorization.DENY`
76+
- **Rule-based format** — an `AuthorizerConfig` object with ordered rules and pattern matching
77+
- **`null`** — removes the authorizer entirely
78+
79+
#### Legacy format
80+
81+
A simple object mapping table names to `Authorization.ALLOW` (0) or `Authorization.DENY` (1).
82+
Tables without an entry are denied by default.
7883

7984
```javascript
85+
const { Authorization } = require('libsql');
86+
8087
db.authorizer({
81-
"users": Authorization.ALLOW
88+
"users": Authorization.ALLOW,
89+
"secrets": Authorization.DENY,
8290
});
8391

84-
// Access is allowed.
92+
// Access to "users" is allowed.
8593
const stmt = db.prepare("SELECT * FROM users");
8694

95+
// Access to "secrets" throws SQLITE_AUTH.
96+
const stmt = db.prepare("SELECT * FROM secrets"); // Error!
97+
```
98+
99+
#### Rule-based format
100+
101+
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`).
102+
103+
```javascript
104+
const { Authorization, Action } = require('libsql');
105+
87106
db.authorizer({
88-
"users": Authorization.DENY
107+
rules: [
108+
// Hide sensitive columns (returns NULL instead of the real value)
109+
{ action: Action.READ, table: "users", column: "password_hash", policy: Authorization.IGNORE },
110+
{ action: Action.READ, table: "users", column: "ssn", policy: Authorization.IGNORE },
111+
112+
// Allow all reads
113+
{ action: Action.READ, policy: Authorization.ALLOW },
114+
115+
// Allow inserts on tables matching a glob pattern
116+
{ action: Action.INSERT, table: { glob: "logs_*" }, policy: Authorization.ALLOW },
117+
118+
// Deny DDL operations
119+
{ action: [Action.CREATE_TABLE, Action.DROP_TABLE, Action.ALTER_TABLE], policy: Authorization.DENY },
120+
121+
// Allow transactions and selects
122+
{ action: Action.TRANSACTION, policy: Authorization.ALLOW },
123+
{ action: Action.SELECT, policy: Authorization.ALLOW },
124+
],
125+
defaultPolicy: Authorization.DENY,
89126
});
127+
```
90128

91-
// Access is denied.
92-
const stmt = db.prepare("SELECT * FROM users");
129+
#### AuthRule fields
130+
131+
| Field | Type | Description |
132+
| -------- | ----------------------------------------- | -------------------------------------------------------------------- |
133+
| action | <code>number \| number[]</code> | Action code(s) to match (from `Action`). Omit to match all actions. |
134+
| table | <code>string \| { glob: string }</code> | Table name pattern. Omit to match any table. |
135+
| column | <code>string \| { glob: string }</code> | Column name pattern (relevant for READ/UPDATE). Omit to match any. |
136+
| entity | <code>string \| { glob: string }</code> | Entity name (index, trigger, view, pragma, function). Omit to match any. |
137+
| policy | <code>number</code> | `Authorization.ALLOW`, `Authorization.DENY`, or `Authorization.IGNORE`. |
138+
139+
#### Pattern matching
140+
141+
Pattern fields (`table`, `column`, `entity`) accept either:
142+
143+
- A **plain string** for exact matching: `"users"`
144+
- An **object with a `glob` key** for glob matching: `{ glob: "logs_*" }`
145+
146+
Glob patterns support `*` (match any number of characters) and `?` (match exactly one character).
147+
148+
```javascript
149+
// Exact match
150+
{ action: Action.READ, table: "users", policy: Authorization.ALLOW }
151+
152+
// Glob: all tables starting with "logs_"
153+
{ action: Action.READ, table: { glob: "logs_*" }, policy: Authorization.ALLOW }
154+
155+
// Glob: single-character wildcard
156+
{ action: Action.READ, table: { glob: "t?_data" }, policy: Authorization.ALLOW }
157+
158+
// Glob: match all tables
159+
{ action: Action.READ, table: { glob: "*" }, policy: Authorization.ALLOW }
93160
```
94161

95-
**Note: This is an experimental API and, therefore, subject to change.**
162+
#### Authorization values
163+
164+
| Value | Effect |
165+
| -------------------------- | ---------------------------------------------------------------------- |
166+
| `Authorization.ALLOW` (0) | Permit the operation. |
167+
| `Authorization.DENY` (1) | Reject the entire SQL statement with a `SQLITE_AUTH` error. |
168+
| `Authorization.IGNORE` (2) | For READ: return NULL instead of the column value. Otherwise: deny. |
169+
170+
#### Removing the authorizer
171+
172+
Pass `null` to remove the authorizer and allow all operations:
173+
174+
```javascript
175+
db.authorizer(null);
176+
```
96177

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

index.d.ts

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -76,25 +76,12 @@ export declare class Database {
7676
/**
7777
* Sets the authorizer for the database.
7878
*
79-
* # Arguments
80-
*
81-
* * `env` - The environment.
82-
* * `rules_obj` - The rules object.
83-
*
84-
* The `rules_obj` is a JavaScript object with the following properties:
85-
*
86-
* * `Authorization.ALLOW` - Allow access to the table.
87-
* * `Authorization.DENY` - Deny access to the table.
88-
*
89-
* Example:
90-
*
91-
* ```javascript
92-
* db.authorizer({
93-
* "users": Authorization.ALLOW
94-
* });
95-
* ```
79+
* Accepts either:
80+
* - Legacy format: `{ [tableName: string]: 0 | 1 }`
81+
* - Full format: `{ rules: AuthRule[], defaultPolicy?: 0 | 1 | 2 }`
82+
* - `null` to remove the authorizer
9683
*/
97-
authorizer(rulesObj: object): void
84+
authorizer(config: unknown): void
9885
/**
9986
* Loads an extension into the database.
10087
*

0 commit comments

Comments
 (0)