odgn-rights 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +0 -1
- package/dist/index.js +2 -1
- package/package.json +9 -38
- package/README.md +0 -596
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -6,4 +6,5 @@ export * from './role-registry';
|
|
|
6
6
|
export * from './subject';
|
|
7
7
|
export * from './subject-registry';
|
|
8
8
|
export * from './helpers';
|
|
9
|
-
|
|
9
|
+
// Note: Adapters are not exported from main entry point to keep it browser-compatible.
|
|
10
|
+
// Use subpath imports: 'odgn-rights/adapters', 'odgn-rights/adapters/sqlite', etc.
|
package/package.json
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "odgn-rights",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "TypeScript library for expressing and evaluating hierarchical rights with simple glob patterns.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"rights",
|
|
7
7
|
"permissions",
|
|
8
|
-
"glob",
|
|
9
8
|
"typescript"
|
|
10
9
|
],
|
|
11
10
|
"author": "Alex Veenendaal",
|
|
@@ -55,35 +54,18 @@
|
|
|
55
54
|
],
|
|
56
55
|
"type": "module",
|
|
57
56
|
"scripts": {
|
|
58
|
-
"format": "prettier --write .",
|
|
59
|
-
"format:check": "prettier --check .",
|
|
60
|
-
"lint": "eslint --cache .",
|
|
61
|
-
"lint:fix": "eslint --fix .",
|
|
62
|
-
"outdated": "bunx npm-check-updates --interactive --format group",
|
|
63
|
-
"test": "bun test src",
|
|
64
|
-
"test:coverage": "bun test --coverage src",
|
|
65
|
-
"test:e2e": "playwright test",
|
|
66
|
-
"test:e2e:ui": "playwright test --ui",
|
|
67
|
-
"unused": "knip",
|
|
68
57
|
"clean": "rm -rf dist",
|
|
69
58
|
"build": "tsc -p tsconfig.build.json",
|
|
70
|
-
"
|
|
71
|
-
|
|
72
|
-
|
|
59
|
+
"prepublishOnly": "bun run clean && bun run build"
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"commander": "^14.0.2",
|
|
63
|
+
"ioredis": "^5.9.0"
|
|
73
64
|
},
|
|
74
65
|
"devDependencies": {
|
|
75
|
-
"@ianvs/prettier-plugin-sort-imports": "^4.7.0",
|
|
76
|
-
"@nkzw/eslint-config": "^3.3.0",
|
|
77
|
-
"@playwright/test": "^1.57.0",
|
|
78
66
|
"@testcontainers/postgresql": "^11.11.0",
|
|
79
67
|
"@testcontainers/valkey": "^11.11.0",
|
|
80
|
-
"@types/bun": "latest",
|
|
81
|
-
"@types/react": "^19.2.7",
|
|
82
|
-
"@types/react-dom": "^19.2.3",
|
|
83
68
|
"elysia": "^1.4.21",
|
|
84
|
-
"eslint": "^9",
|
|
85
|
-
"knip": "^5.80.0",
|
|
86
|
-
"prettier": "^3",
|
|
87
69
|
"testcontainers": "^11.11.0"
|
|
88
70
|
},
|
|
89
71
|
"peerDependencies": {
|
|
@@ -98,16 +80,5 @@
|
|
|
98
80
|
"publishConfig": {
|
|
99
81
|
"access": "public"
|
|
100
82
|
},
|
|
101
|
-
"sideEffects": false
|
|
102
|
-
|
|
103
|
-
"commander": "^14.0.2",
|
|
104
|
-
"ioredis": "^5.9.0",
|
|
105
|
-
"jotai": "^2.16.1",
|
|
106
|
-
"react": "^19.2.3",
|
|
107
|
-
"react-dom": "^19.2.3"
|
|
108
|
-
},
|
|
109
|
-
"trustedDependencies": [
|
|
110
|
-
"protobufjs",
|
|
111
|
-
"unrs-resolver"
|
|
112
|
-
]
|
|
113
|
-
}
|
|
83
|
+
"sideEffects": false
|
|
84
|
+
}
|
package/README.md
DELETED
|
@@ -1,596 +0,0 @@
|
|
|
1
|
-
# ODGN Rights
|
|
2
|
-
|
|
3
|
-
Tiny TypeScript library for expressing and evaluating hierarchical rights with simple glob patterns.
|
|
4
|
-
|
|
5
|
-
## Install & Dev
|
|
6
|
-
|
|
7
|
-
- Install: `bun install`
|
|
8
|
-
- Test: `bun test` (watch: `bun test --watch`)
|
|
9
|
-
|
|
10
|
-
Use in TS:
|
|
11
|
-
|
|
12
|
-
```ts
|
|
13
|
-
import { Flags, Right, Rights } from 'odgn-rights'; // when packaged
|
|
14
|
-
|
|
15
|
-
// or locally: import { Rights, Right, Flags } from './src/index.ts';
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
## Quick Start
|
|
19
|
-
|
|
20
|
-
```ts
|
|
21
|
-
const rights = new Rights();
|
|
22
|
-
rights.allow('/', Flags.READ);
|
|
23
|
-
|
|
24
|
-
// Deny read, allow create anywhere matching /*/device/**
|
|
25
|
-
rights.add(
|
|
26
|
-
new Right('/*/device/**', { allow: [Flags.CREATE], deny: [Flags.READ] })
|
|
27
|
-
);
|
|
28
|
-
|
|
29
|
-
// Grant everything under /system/user/*
|
|
30
|
-
rights.add(new Right('/system/user/*', { allow: [Flags.ALL] }));
|
|
31
|
-
|
|
32
|
-
rights.read('/system/user/1'); // true
|
|
33
|
-
rights.write('/system/user/1'); // true
|
|
34
|
-
rights.create('/system/device/a'); // true
|
|
35
|
-
rights.read('/system/device/a'); // false (denied by more specific rule)
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
## API Overview
|
|
39
|
-
|
|
40
|
-
- `Right(path, {allow?, deny?, description?})`
|
|
41
|
-
- Flags: `Flags.READ | WRITE | DELETE | CREATE | EXECUTE | ALL`
|
|
42
|
-
- Methods: `allow(flag)`, `deny(flag)`, `clear()`, `has(mask)`
|
|
43
|
-
- String form: `-denies+allows:/path` (e.g., `-d+rw:/system`)
|
|
44
|
-
- `Right.parse(str)` creates a Right from its string form
|
|
45
|
-
- `Rights`
|
|
46
|
-
- `add(right)`, `allow(path, ...flags)`, `deny(path, flag)`
|
|
47
|
-
- Checks: `read|write|delete|create|execute|all(path)`
|
|
48
|
-
- Serialization: `format(separator?)`, `toJSON()`, `Rights.parse(str)`, `Rights.fromJSON(json)`
|
|
49
|
-
- `Role(name, rights?)`
|
|
50
|
-
- `inheritsFrom(role)`: Inherit rights from another role.
|
|
51
|
-
- `Subject`
|
|
52
|
-
- `memberOf(role)`: Assign a role to the subject.
|
|
53
|
-
- `rights`: Direct `Rights` assigned to the subject.
|
|
54
|
-
- `has(path, flag)`: Evaluates permissions across all roles and direct rights.
|
|
55
|
-
- `RoleRegistry`
|
|
56
|
-
- `define(name, rights?)`: Define or update a role.
|
|
57
|
-
- `toJSON() / RoleRegistry.fromJSON(json)`: Serialization with inheritance.
|
|
58
|
-
|
|
59
|
-
Matching precedence: the most specific matching rule wins; within a rule, a denied flag blocks that flag even if allowed by a less specific rule.
|
|
60
|
-
|
|
61
|
-
## RBAC Example
|
|
62
|
-
|
|
63
|
-
```ts
|
|
64
|
-
const registry = new RoleRegistry();
|
|
65
|
-
const viewer = registry.define('viewer', new Rights().allow('/', Flags.READ));
|
|
66
|
-
const editor = registry.define(
|
|
67
|
-
'editor',
|
|
68
|
-
new Rights().allow('/posts', Flags.WRITE)
|
|
69
|
-
);
|
|
70
|
-
editor.inheritsFrom(viewer);
|
|
71
|
-
|
|
72
|
-
const user = new Subject().memberOf(editor);
|
|
73
|
-
user.read('/posts/1'); // true (inherited from viewer)
|
|
74
|
-
user.write('/posts/1'); // true (from editor)
|
|
75
|
-
user.write('/config'); // false
|
|
76
|
-
```
|
|
77
|
-
|
|
78
|
-
## Contextual Rights (ABAC)
|
|
79
|
-
|
|
80
|
-
Rights can include a condition predicate that is evaluated at runtime with a provided context.
|
|
81
|
-
|
|
82
|
-
```ts
|
|
83
|
-
rights.add(
|
|
84
|
-
new Right('/posts/*', {
|
|
85
|
-
allow: [Flags.WRITE],
|
|
86
|
-
condition: ctx => ctx.userId === ctx.ownerId
|
|
87
|
-
})
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
// Provide context to the check
|
|
91
|
-
rights.write('/posts/1', { userId: 'abc', ownerId: 'abc' }); // true
|
|
92
|
-
rights.write('/posts/1', { userId: 'abc', ownerId: 'xyz' }); // false
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
## Glob Patterns
|
|
96
|
-
|
|
97
|
-
- `*` matches within a single path segment (`/system/*/id`)
|
|
98
|
-
- `**` matches across segments (`/*/device/**`)
|
|
99
|
-
- `?` matches a single character (no slash)
|
|
100
|
-
|
|
101
|
-
## Rule Priority
|
|
102
|
-
|
|
103
|
-
By default, rules are matched by **specificity** — the most specific matching rule wins. However, you can override this with explicit **priority** values.
|
|
104
|
-
|
|
105
|
-
### How Priority Works
|
|
106
|
-
|
|
107
|
-
1. **Higher priority wins** regardless of specificity
|
|
108
|
-
2. **Equal priorities** fall back to specificity comparison
|
|
109
|
-
3. **Default priority is 0** when not specified
|
|
110
|
-
4. **Negative priorities** can deprioritize rules below the default
|
|
111
|
-
|
|
112
|
-
```ts
|
|
113
|
-
const rights = new Rights();
|
|
114
|
-
|
|
115
|
-
// Specific path, default priority (0)
|
|
116
|
-
rights.add(
|
|
117
|
-
new Right('/posts/123', {
|
|
118
|
-
allow: [Flags.READ],
|
|
119
|
-
deny: [Flags.WRITE]
|
|
120
|
-
})
|
|
121
|
-
);
|
|
122
|
-
|
|
123
|
-
// Wildcard path, but high priority (100) — this rule wins!
|
|
124
|
-
rights.add(
|
|
125
|
-
new Right('/posts/*', {
|
|
126
|
-
allow: [Flags.READ, Flags.WRITE],
|
|
127
|
-
priority: 100
|
|
128
|
-
})
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
rights.write('/posts/123'); // true — high-priority wildcard rule wins
|
|
132
|
-
```
|
|
133
|
-
|
|
134
|
-
### Priority in Serialization
|
|
135
|
-
|
|
136
|
-
Priority is included in both text and JSON serialization formats.
|
|
137
|
-
|
|
138
|
-
**Text format** uses `^` after the path:
|
|
139
|
-
|
|
140
|
-
```ts
|
|
141
|
-
const right = new Right('/posts/*', {
|
|
142
|
-
allow: [Flags.WRITE],
|
|
143
|
-
priority: 100
|
|
144
|
-
});
|
|
145
|
-
right.toString(); // "+w:/posts/*^100"
|
|
146
|
-
|
|
147
|
-
// With tags and time ranges
|
|
148
|
-
// Format: [flags]:[path]^[priority]#[tags]@[validFrom]/[validUntil]
|
|
149
|
-
Right.parse('+rw:/admin/*^50#secure');
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
**JSON format** includes an optional `priority` field:
|
|
153
|
-
|
|
154
|
-
```json
|
|
155
|
-
[
|
|
156
|
-
{ "path": "/posts/*", "allow": "rw", "priority": 100 },
|
|
157
|
-
{ "path": "/posts/123", "allow": "r", "deny": "w" }
|
|
158
|
-
]
|
|
159
|
-
```
|
|
160
|
-
|
|
161
|
-
Priority is omitted from serialization when it equals 0 (the default).
|
|
162
|
-
|
|
163
|
-
### Use Cases
|
|
164
|
-
|
|
165
|
-
- **Emergency overrides**: Grant temporary high-priority access that bypasses normal rules
|
|
166
|
-
- **Default deny rules**: Use negative priority for fallback deny rules
|
|
167
|
-
- **Policy layers**: Implement organizational policies at different priority levels
|
|
168
|
-
|
|
169
|
-
```ts
|
|
170
|
-
// Low-priority default: deny all writes
|
|
171
|
-
rights.add(
|
|
172
|
-
new Right('/**', {
|
|
173
|
-
deny: [Flags.WRITE],
|
|
174
|
-
priority: -100
|
|
175
|
-
})
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
// Normal priority: department-level permissions
|
|
179
|
-
rights.add(
|
|
180
|
-
new Right('/dept/engineering/**', {
|
|
181
|
-
allow: [Flags.READ, Flags.WRITE]
|
|
182
|
-
})
|
|
183
|
-
);
|
|
184
|
-
|
|
185
|
-
// High priority: emergency maintenance access
|
|
186
|
-
rights.add(
|
|
187
|
-
new Right('/system/**', {
|
|
188
|
-
allow: [Flags.ALL],
|
|
189
|
-
priority: 1000,
|
|
190
|
-
tags: ['emergency']
|
|
191
|
-
})
|
|
192
|
-
);
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
## JSON Round‑Trip
|
|
196
|
-
|
|
197
|
-
```ts
|
|
198
|
-
const json = rights.toJSON();
|
|
199
|
-
// [ { path: '/', allow: 'r' }, { path: '/*/device/**', allow: 'c' }, ... ]
|
|
200
|
-
const loaded = Rights.fromJSON(json);
|
|
201
|
-
```
|
|
202
|
-
|
|
203
|
-
## Batch Permission Checks
|
|
204
|
-
|
|
205
|
-
Efficiently check multiple permissions at once with `checkMany()`.
|
|
206
|
-
|
|
207
|
-
### Basic Usage
|
|
208
|
-
|
|
209
|
-
```ts
|
|
210
|
-
const rights = new Rights();
|
|
211
|
-
rights.allow('/users/*', Flags.READ);
|
|
212
|
-
rights.allow('/posts/*', Flags.WRITE);
|
|
213
|
-
rights.deny('/admin', Flags.ALL);
|
|
214
|
-
|
|
215
|
-
const results = rights.checkMany([
|
|
216
|
-
{ path: '/users/1', flags: Flags.READ },
|
|
217
|
-
{ path: '/posts/1', flags: Flags.WRITE },
|
|
218
|
-
{ path: '/admin', flags: Flags.ALL }
|
|
219
|
-
]);
|
|
220
|
-
// Returns: [true, true, false]
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
### With Context
|
|
224
|
-
|
|
225
|
-
The same context is shared across all checks:
|
|
226
|
-
|
|
227
|
-
```ts
|
|
228
|
-
rights.add(
|
|
229
|
-
new Right('/posts/*', {
|
|
230
|
-
allow: [Flags.WRITE],
|
|
231
|
-
condition: ctx => ctx.userId === ctx.ownerId
|
|
232
|
-
})
|
|
233
|
-
);
|
|
234
|
-
|
|
235
|
-
const results = rights.checkMany(
|
|
236
|
-
[
|
|
237
|
-
{ path: '/posts/1', flags: Flags.WRITE },
|
|
238
|
-
{ path: '/posts/2', flags: Flags.WRITE },
|
|
239
|
-
{ path: '/posts/3', flags: Flags.WRITE }
|
|
240
|
-
],
|
|
241
|
-
{ userId: 'user1', ownerId: 'user1' }
|
|
242
|
-
);
|
|
243
|
-
// Returns: [true, true, true]
|
|
244
|
-
```
|
|
245
|
-
|
|
246
|
-
### With Subjects
|
|
247
|
-
|
|
248
|
-
Works with subjects that have multiple roles:
|
|
249
|
-
|
|
250
|
-
```ts
|
|
251
|
-
const viewer = new Role('viewer', new Rights().allow('/docs', Flags.READ));
|
|
252
|
-
const writer = new Role('writer', new Rights().allow('/docs', Flags.WRITE));
|
|
253
|
-
|
|
254
|
-
const subject = new Subject().memberOf(viewer).memberOf(writer);
|
|
255
|
-
|
|
256
|
-
const results = subject.checkMany([
|
|
257
|
-
{ path: '/docs', flags: Flags.READ },
|
|
258
|
-
{ path: '/docs', flags: Flags.WRITE },
|
|
259
|
-
{ path: '/docs', flags: Flags.DELETE }
|
|
260
|
-
]);
|
|
261
|
-
// Returns: [true, true, false]
|
|
262
|
-
```
|
|
263
|
-
|
|
264
|
-
### Use Cases
|
|
265
|
-
|
|
266
|
-
- **Bulk authorization**: Check multiple resource permissions in a single call
|
|
267
|
-
- **Feature flags**: Enable/disable multiple features based on permissions
|
|
268
|
-
- **API responses**: Include permission information for multiple resources
|
|
269
|
-
- **UI rendering**: Determine visibility of multiple UI elements efficiently
|
|
270
|
-
|
|
271
|
-
## CLI Tool
|
|
272
|
-
|
|
273
|
-
The CLI tool helps test and debug permission configurations from the command line.
|
|
274
|
-
|
|
275
|
-
### Installation
|
|
276
|
-
|
|
277
|
-
```bash
|
|
278
|
-
# Install globally
|
|
279
|
-
npm install -g @odgn/rights
|
|
280
|
-
|
|
281
|
-
# Or use with npx
|
|
282
|
-
npx @odgn/rights --help
|
|
283
|
-
|
|
284
|
-
# Or run directly with bun
|
|
285
|
-
bun run src/cli/index.ts --help
|
|
286
|
-
```
|
|
287
|
-
|
|
288
|
-
### Commands
|
|
289
|
-
|
|
290
|
-
#### check
|
|
291
|
-
|
|
292
|
-
Test if a permission is allowed:
|
|
293
|
-
|
|
294
|
-
```bash
|
|
295
|
-
# Basic usage
|
|
296
|
-
odgn-rights check -c config.json -p /users/123 -f READ
|
|
297
|
-
|
|
298
|
-
# With combined flags
|
|
299
|
-
odgn-rights check -c config.json -p /users/123 -f RW
|
|
300
|
-
|
|
301
|
-
# With comma-separated flags
|
|
302
|
-
odgn-rights check -c config.json -p /users/123 -f READ,WRITE
|
|
303
|
-
|
|
304
|
-
# Quiet mode for scripting (outputs 'true' or 'false')
|
|
305
|
-
odgn-rights check -c config.json -p /users/123 -f READ --quiet
|
|
306
|
-
|
|
307
|
-
# With context for conditional rights
|
|
308
|
-
odgn-rights check -c config.json -p /posts/1 -f WRITE --context '{"userId":"abc","ownerId":"abc"}'
|
|
309
|
-
|
|
310
|
-
# Override time for time-based rights
|
|
311
|
-
odgn-rights check -c config.json -p /scheduled -f READ --time 2025-06-15T12:00:00Z
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
Exit codes: `0` = allowed, `1` = denied, `2` = error
|
|
315
|
-
|
|
316
|
-
#### explain
|
|
317
|
-
|
|
318
|
-
Understand why a permission is allowed or denied:
|
|
319
|
-
|
|
320
|
-
```bash
|
|
321
|
-
# Basic usage
|
|
322
|
-
odgn-rights explain -c config.json -p /users/123 -f WRITE
|
|
323
|
-
|
|
324
|
-
# JSON output
|
|
325
|
-
odgn-rights explain -c config.json -p /users/123 -f READ --json
|
|
326
|
-
```
|
|
327
|
-
|
|
328
|
-
The explain command shows:
|
|
329
|
-
|
|
330
|
-
- Decision breakdown per flag
|
|
331
|
-
- Matching rules sorted by specificity
|
|
332
|
-
- Suggestions for granting denied permissions
|
|
333
|
-
|
|
334
|
-
#### validate
|
|
335
|
-
|
|
336
|
-
Validate a configuration file:
|
|
337
|
-
|
|
338
|
-
```bash
|
|
339
|
-
# Validate JSON config
|
|
340
|
-
odgn-rights validate config.json
|
|
341
|
-
|
|
342
|
-
# Validate string format config
|
|
343
|
-
odgn-rights validate config.txt
|
|
344
|
-
|
|
345
|
-
# Strict mode (warns on broad patterns like /**)
|
|
346
|
-
odgn-rights validate --strict config.json
|
|
347
|
-
|
|
348
|
-
# JSON output
|
|
349
|
-
odgn-rights validate --json config.json
|
|
350
|
-
```
|
|
351
|
-
|
|
352
|
-
Exit codes: `0` = valid, `1` = validation errors, `2` = file error
|
|
353
|
-
|
|
354
|
-
### Configuration Formats
|
|
355
|
-
|
|
356
|
-
The CLI supports two configuration formats:
|
|
357
|
-
|
|
358
|
-
**JSON format** (`config.json`):
|
|
359
|
-
|
|
360
|
-
```json
|
|
361
|
-
[
|
|
362
|
-
{ "path": "/", "allow": "r" },
|
|
363
|
-
{ "path": "/users/*", "allow": "rw" },
|
|
364
|
-
{ "path": "/admin/**", "allow": "*", "tags": ["admin"] },
|
|
365
|
-
{ "path": "/scheduled", "allow": "r", "validFrom": "2025-01-01T00:00:00Z" }
|
|
366
|
-
]
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
**String format** (`config.txt`):
|
|
370
|
-
|
|
371
|
-
```
|
|
372
|
-
# Comments start with #
|
|
373
|
-
+r:/
|
|
374
|
-
+rw:/users/*
|
|
375
|
-
+*:/admin/**
|
|
376
|
-
-d+rw:/public
|
|
377
|
-
```
|
|
378
|
-
|
|
379
|
-
### Flag Reference
|
|
380
|
-
|
|
381
|
-
| Flag | Letter | Description |
|
|
382
|
-
| ------- | ------ | ------------------ |
|
|
383
|
-
| READ | R | Read permission |
|
|
384
|
-
| WRITE | W | Write permission |
|
|
385
|
-
| CREATE | C | Create permission |
|
|
386
|
-
| DELETE | D | Delete permission |
|
|
387
|
-
| EXECUTE | X | Execute permission |
|
|
388
|
-
| ALL | \* | All permissions |
|
|
389
|
-
|
|
390
|
-
Flags can be combined: `RW`, `READ,WRITE`, `RWCDX`
|
|
391
|
-
|
|
392
|
-
## Database Adapters
|
|
393
|
-
|
|
394
|
-
Database adapters enable persistent storage of rights configurations in SQLite or PostgreSQL databases. This is useful for applications that need to load permissions from a database, share configurations across services, or audit permission changes.
|
|
395
|
-
|
|
396
|
-
### Installation
|
|
397
|
-
|
|
398
|
-
The adapters use Bun's built-in database drivers (`bun:sqlite` and `bun` SQL), so no additional dependencies are required.
|
|
399
|
-
|
|
400
|
-
```ts
|
|
401
|
-
import { PostgresAdapter, SQLiteAdapter } from 'odgn-rights/adapters';
|
|
402
|
-
```
|
|
403
|
-
|
|
404
|
-
### Table Prefix
|
|
405
|
-
|
|
406
|
-
All adapters support a configurable table prefix. The default prefix is `tbl_`.
|
|
407
|
-
|
|
408
|
-
```ts
|
|
409
|
-
// Default prefix creates tables: tbl_rights, tbl_roles, etc.
|
|
410
|
-
const adapter = new SQLiteAdapter({ filename: './permissions.db' });
|
|
411
|
-
|
|
412
|
-
// Custom prefix creates tables: auth_rights, auth_roles, etc.
|
|
413
|
-
const adapter = new SQLiteAdapter({
|
|
414
|
-
filename: './permissions.db',
|
|
415
|
-
tablePrefix: 'auth_'
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
// No prefix creates tables: rights, roles, etc.
|
|
419
|
-
const adapter = new SQLiteAdapter({
|
|
420
|
-
filename: './permissions.db',
|
|
421
|
-
tablePrefix: ''
|
|
422
|
-
});
|
|
423
|
-
```
|
|
424
|
-
|
|
425
|
-
### SQLite Adapter
|
|
426
|
-
|
|
427
|
-
SQLite is ideal for single-process applications, embedded systems, or local development.
|
|
428
|
-
|
|
429
|
-
```ts
|
|
430
|
-
import { Flags, Right, Rights } from 'odgn-rights';
|
|
431
|
-
import { SQLiteAdapter } from 'odgn-rights/adapters';
|
|
432
|
-
|
|
433
|
-
// Create adapter and connect
|
|
434
|
-
const adapter = new SQLiteAdapter({
|
|
435
|
-
filename: './permissions.db', // Use ':memory:' for in-memory
|
|
436
|
-
enableWAL: true // Enable WAL mode for better concurrency
|
|
437
|
-
});
|
|
438
|
-
|
|
439
|
-
await adapter.connect();
|
|
440
|
-
await adapter.migrate();
|
|
441
|
-
|
|
442
|
-
// Save rights
|
|
443
|
-
const rights = new Rights();
|
|
444
|
-
rights.allow('/users/*', Flags.READ);
|
|
445
|
-
rights.allow('/admin/**', Flags.ALL);
|
|
446
|
-
await adapter.saveRights(rights);
|
|
447
|
-
|
|
448
|
-
// Load rights
|
|
449
|
-
const loaded = await adapter.loadRights();
|
|
450
|
-
loaded.has('/users/123', Flags.READ); // true
|
|
451
|
-
|
|
452
|
-
// Save and load roles
|
|
453
|
-
const { Role, RoleRegistry } = await import('odgn-rights');
|
|
454
|
-
const registry = new RoleRegistry();
|
|
455
|
-
const admin = registry.define('admin');
|
|
456
|
-
admin.rights.allow('/**', Flags.ALL);
|
|
457
|
-
await registry.saveTo(adapter);
|
|
458
|
-
|
|
459
|
-
// Load registry from database
|
|
460
|
-
const loadedRegistry = await RoleRegistry.loadFrom(adapter);
|
|
461
|
-
|
|
462
|
-
await adapter.disconnect();
|
|
463
|
-
```
|
|
464
|
-
|
|
465
|
-
### PostgreSQL Adapter
|
|
466
|
-
|
|
467
|
-
PostgreSQL is ideal for multi-process applications, microservices, or when you need shared access to permissions.
|
|
468
|
-
|
|
469
|
-
```ts
|
|
470
|
-
import { Flags, RoleRegistry, Subject } from 'odgn-rights';
|
|
471
|
-
import { PostgresAdapter } from 'odgn-rights/adapters';
|
|
472
|
-
|
|
473
|
-
const adapter = new PostgresAdapter({
|
|
474
|
-
url: 'postgres://user:pass@localhost:5432/mydb',
|
|
475
|
-
// Or use individual options:
|
|
476
|
-
// hostname: 'localhost',
|
|
477
|
-
// port: 5432,
|
|
478
|
-
// database: 'mydb',
|
|
479
|
-
// username: 'user',
|
|
480
|
-
// password: 'pass',
|
|
481
|
-
tablePrefix: 'perms_' // Optional custom prefix
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
await adapter.connect();
|
|
485
|
-
await adapter.migrate();
|
|
486
|
-
|
|
487
|
-
// Load registry and make changes
|
|
488
|
-
const registry = await adapter.loadRegistry();
|
|
489
|
-
const editor = registry.define('editor');
|
|
490
|
-
editor.rights.allow('/content/**', Flags.READ, Flags.WRITE);
|
|
491
|
-
|
|
492
|
-
// Save back
|
|
493
|
-
await adapter.saveRegistry(registry);
|
|
494
|
-
|
|
495
|
-
// Save subjects with roles
|
|
496
|
-
const user = new Subject();
|
|
497
|
-
user.memberOf(editor);
|
|
498
|
-
await adapter.saveSubject('user-123', user);
|
|
499
|
-
|
|
500
|
-
await adapter.disconnect();
|
|
501
|
-
```
|
|
502
|
-
|
|
503
|
-
### Factory Functions
|
|
504
|
-
|
|
505
|
-
Convenience functions for common patterns:
|
|
506
|
-
|
|
507
|
-
```ts
|
|
508
|
-
import {
|
|
509
|
-
createPostgresRegistry,
|
|
510
|
-
createPostgresRights,
|
|
511
|
-
createSQLiteRegistry,
|
|
512
|
-
createSQLiteRights
|
|
513
|
-
} from 'odgn-rights/adapters';
|
|
514
|
-
|
|
515
|
-
// Create SQLite adapter with rights
|
|
516
|
-
const { adapter, rights } = await createSQLiteRights({
|
|
517
|
-
filename: './permissions.db'
|
|
518
|
-
});
|
|
519
|
-
rights.allow('/public/**', Flags.READ);
|
|
520
|
-
await adapter.saveRights(rights);
|
|
521
|
-
await adapter.disconnect();
|
|
522
|
-
|
|
523
|
-
// Create SQLite adapter with registry
|
|
524
|
-
const { adapter: regAdapter, registry } = await createSQLiteRegistry({
|
|
525
|
-
filename: ':memory:'
|
|
526
|
-
});
|
|
527
|
-
const viewer = registry.define('viewer');
|
|
528
|
-
viewer.rights.allow('/read/*', Flags.READ);
|
|
529
|
-
await registry.saveTo(regAdapter);
|
|
530
|
-
await regAdapter.disconnect();
|
|
531
|
-
```
|
|
532
|
-
|
|
533
|
-
### Transactions
|
|
534
|
-
|
|
535
|
-
Both adapters support transactions for atomic operations:
|
|
536
|
-
|
|
537
|
-
```ts
|
|
538
|
-
await adapter.transaction(async () => {
|
|
539
|
-
await adapter.saveRight(new Right('/a', { allow: [Flags.READ] }));
|
|
540
|
-
await adapter.saveRight(new Right('/b', { allow: [Flags.WRITE] }));
|
|
541
|
-
// If an error is thrown, all changes are rolled back
|
|
542
|
-
});
|
|
543
|
-
```
|
|
544
|
-
|
|
545
|
-
### Adapter Interface
|
|
546
|
-
|
|
547
|
-
All adapters implement the `DatabaseAdapter` interface:
|
|
548
|
-
|
|
549
|
-
| Method | Description |
|
|
550
|
-
| ---------------------------------- | --------------------------------- |
|
|
551
|
-
| `connect()` | Connect to the database |
|
|
552
|
-
| `disconnect()` | Disconnect from the database |
|
|
553
|
-
| `migrate()` | Create or update schema |
|
|
554
|
-
| `saveRight(right)` | Save a single right |
|
|
555
|
-
| `saveRights(rights)` | Save multiple rights |
|
|
556
|
-
| `loadRight(id)` | Load a right by ID |
|
|
557
|
-
| `loadRights()` | Load all rights |
|
|
558
|
-
| `loadRightsByPath(pattern)` | Load rights matching a pattern |
|
|
559
|
-
| `deleteRight(id)` | Delete a right |
|
|
560
|
-
| `saveRole(role)` | Save a role with its rights |
|
|
561
|
-
| `loadRole(name)` | Load a role by name |
|
|
562
|
-
| `loadRoles()` | Load all roles |
|
|
563
|
-
| `deleteRole(name)` | Delete a role |
|
|
564
|
-
| `saveRegistry(registry)` | Save entire RoleRegistry |
|
|
565
|
-
| `loadRegistry()` | Load RoleRegistry with all roles |
|
|
566
|
-
| `saveSubject(identifier, subject)` | Save a subject |
|
|
567
|
-
| `loadSubject(identifier)` | Load a subject |
|
|
568
|
-
| `deleteSubject(identifier)` | Delete a subject |
|
|
569
|
-
| `clear()` | Clear all data (for testing) |
|
|
570
|
-
| `transaction(fn)` | Execute operations in transaction |
|
|
571
|
-
|
|
572
|
-
### Database Schema
|
|
573
|
-
|
|
574
|
-
The adapters create the following tables (with the configured prefix):
|
|
575
|
-
|
|
576
|
-
| Table | Purpose |
|
|
577
|
-
| -------------------------- | ------------------------------------ |
|
|
578
|
-
| `{prefix}rights` | Individual rights with paths & flags |
|
|
579
|
-
| `{prefix}roles` | Role definitions |
|
|
580
|
-
| `{prefix}role_rights` | Role-to-rights mapping |
|
|
581
|
-
| `{prefix}role_inheritance` | Role inheritance relationships |
|
|
582
|
-
| `{prefix}subjects` | Subject records |
|
|
583
|
-
| `{prefix}subject_roles` | Subject-to-roles mapping |
|
|
584
|
-
| `{prefix}subject_rights` | Direct subject rights |
|
|
585
|
-
|
|
586
|
-
### Persistence Metadata
|
|
587
|
-
|
|
588
|
-
Rights saved to the database receive a `dbId` property:
|
|
589
|
-
|
|
590
|
-
```ts
|
|
591
|
-
const right = new Right('/test', { allow: [Flags.READ] });
|
|
592
|
-
console.log(right.dbId); // undefined
|
|
593
|
-
|
|
594
|
-
await adapter.saveRight(right);
|
|
595
|
-
console.log(right.dbId); // 1 (database ID)
|
|
596
|
-
```
|