odgn-rights 0.2.0 → 0.5.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/README.md +368 -0
- package/dist/adapters/base-adapter.d.ts +83 -0
- package/dist/adapters/base-adapter.js +142 -0
- package/dist/adapters/factories.d.ts +31 -0
- package/dist/adapters/factories.js +48 -0
- package/dist/adapters/index.d.ts +11 -0
- package/dist/adapters/index.js +12 -0
- package/dist/adapters/postgres-adapter.d.ts +51 -0
- package/dist/adapters/postgres-adapter.js +469 -0
- package/dist/adapters/redis-adapter.d.ts +84 -0
- package/dist/adapters/redis-adapter.js +673 -0
- package/dist/adapters/schema.d.ts +25 -0
- package/dist/adapters/schema.js +186 -0
- package/dist/adapters/sqlite-adapter.d.ts +78 -0
- package/dist/adapters/sqlite-adapter.js +655 -0
- package/dist/adapters/types.d.ts +174 -0
- package/dist/adapters/types.js +1 -0
- package/dist/cli/commands/explain.js +13 -5
- package/dist/helpers.d.ts +16 -0
- package/dist/{utils.js → helpers.js} +22 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/integrations/elysia.d.ts +235 -0
- package/dist/integrations/elysia.js +375 -0
- package/dist/right.d.ts +7 -0
- package/dist/right.js +63 -8
- package/dist/rights.d.ts +19 -0
- package/dist/rights.js +48 -2
- package/dist/role-registry.d.ts +9 -0
- package/dist/role-registry.js +12 -0
- package/dist/subject-registry.d.ts +77 -0
- package/dist/subject-registry.js +123 -0
- package/dist/subject.d.ts +4 -0
- package/dist/subject.js +3 -0
- package/package.json +41 -6
- package/dist/utils.d.ts +0 -2
package/README.md
CHANGED
|
@@ -98,6 +98,100 @@ rights.write('/posts/1', { userId: 'abc', ownerId: 'xyz' }); // false
|
|
|
98
98
|
- `**` matches across segments (`/*/device/**`)
|
|
99
99
|
- `?` matches a single character (no slash)
|
|
100
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
|
+
|
|
101
195
|
## JSON Round‑Trip
|
|
102
196
|
|
|
103
197
|
```ts
|
|
@@ -106,6 +200,74 @@ const json = rights.toJSON();
|
|
|
106
200
|
const loaded = Rights.fromJSON(json);
|
|
107
201
|
```
|
|
108
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
|
+
|
|
109
271
|
## CLI Tool
|
|
110
272
|
|
|
111
273
|
The CLI tool helps test and debug permission configurations from the command line.
|
|
@@ -226,3 +388,209 @@ The CLI supports two configuration formats:
|
|
|
226
388
|
| ALL | \* | All permissions |
|
|
227
389
|
|
|
228
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
|
+
```
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Flags } from '../constants';
|
|
2
|
+
import { Right } from '../right';
|
|
3
|
+
import { Rights } from '../rights';
|
|
4
|
+
import { Role } from '../role';
|
|
5
|
+
import { RoleRegistry } from '../role-registry';
|
|
6
|
+
import { Subject } from '../subject';
|
|
7
|
+
import type { BaseAdapterOptions, DatabaseAdapter, RightsRow, TableNames } from './types';
|
|
8
|
+
/**
|
|
9
|
+
* Abstract base class for database adapters.
|
|
10
|
+
* Provides common utility methods for serialization/deserialization
|
|
11
|
+
* and table name management.
|
|
12
|
+
*/
|
|
13
|
+
export declare abstract class BaseAdapter implements DatabaseAdapter {
|
|
14
|
+
protected readonly tablePrefix: string;
|
|
15
|
+
protected readonly _tables: TableNames;
|
|
16
|
+
constructor(options?: BaseAdapterOptions);
|
|
17
|
+
/**
|
|
18
|
+
* Get the table names with the configured prefix
|
|
19
|
+
*/
|
|
20
|
+
protected get tables(): TableNames;
|
|
21
|
+
/**
|
|
22
|
+
* Serialize tags array to JSON string
|
|
23
|
+
*/
|
|
24
|
+
protected serializeTags: (tags: string[]) => string | null;
|
|
25
|
+
/**
|
|
26
|
+
* Deserialize JSON string to tags array
|
|
27
|
+
*/
|
|
28
|
+
protected deserializeTags: (json: string | null) => string[];
|
|
29
|
+
/**
|
|
30
|
+
* Convert a Right instance to a database row (partial, without id and timestamps)
|
|
31
|
+
*/
|
|
32
|
+
protected rightToRow: (right: Right) => Omit<RightsRow, "id" | "created_at" | "updated_at">;
|
|
33
|
+
/**
|
|
34
|
+
* Convert a database row to a Right instance
|
|
35
|
+
*/
|
|
36
|
+
protected rowToRight: (row: RightsRow) => Right;
|
|
37
|
+
/**
|
|
38
|
+
* Convert a bitmask to an array of Flag values
|
|
39
|
+
*/
|
|
40
|
+
protected maskToFlags: (mask: number) => Flags[];
|
|
41
|
+
/**
|
|
42
|
+
* Convert an array of Flag values to a bitmask
|
|
43
|
+
*/
|
|
44
|
+
protected flagsToMask: (flags: Flags[]) => number;
|
|
45
|
+
/**
|
|
46
|
+
* Parse an ISO 8601 timestamp string to a Date, or undefined if null/invalid
|
|
47
|
+
*/
|
|
48
|
+
protected parseTimestamp: (value: string | null) => Date | undefined;
|
|
49
|
+
abstract connect(): Promise<void>;
|
|
50
|
+
abstract disconnect(): Promise<void>;
|
|
51
|
+
abstract migrate(): Promise<void>;
|
|
52
|
+
abstract saveRight(right: Right): Promise<number>;
|
|
53
|
+
abstract saveRights(rights: Rights): Promise<number[]>;
|
|
54
|
+
abstract loadRight(id: number): Promise<Right | null>;
|
|
55
|
+
abstract loadRights(): Promise<Rights>;
|
|
56
|
+
abstract loadRightsByPath(pathPattern: string): Promise<Rights>;
|
|
57
|
+
abstract deleteRight(id: number): Promise<boolean>;
|
|
58
|
+
abstract saveRole(role: Role): Promise<number>;
|
|
59
|
+
abstract loadRole(name: string): Promise<Role | null>;
|
|
60
|
+
abstract loadRoles(): Promise<Role[]>;
|
|
61
|
+
abstract deleteRole(name: string): Promise<boolean>;
|
|
62
|
+
abstract saveRegistry(registry: RoleRegistry): Promise<void>;
|
|
63
|
+
abstract loadRegistry(): Promise<RoleRegistry>;
|
|
64
|
+
abstract saveSubject(identifier: string, subject: Subject): Promise<number>;
|
|
65
|
+
abstract loadSubject(identifier: string): Promise<Subject | null>;
|
|
66
|
+
abstract deleteSubject(identifier: string): Promise<boolean>;
|
|
67
|
+
/**
|
|
68
|
+
* Get all subject identifiers from the database.
|
|
69
|
+
* Used by findSubjectsWithAccess and can be overridden for optimization.
|
|
70
|
+
*/
|
|
71
|
+
protected abstract getAllSubjectIdentifiers(): Promise<string[]>;
|
|
72
|
+
/**
|
|
73
|
+
* Find all subject identifiers that have access to a specific path with given flags.
|
|
74
|
+
* Default implementation uses getAllSubjectIdentifiers + loadSubject.
|
|
75
|
+
* Subclasses can override with optimized batch loading implementations.
|
|
76
|
+
* @param pathPattern The path pattern to check (supports wildcards)
|
|
77
|
+
* @param flags The flags to check for
|
|
78
|
+
* @returns Array of subject identifiers that have access
|
|
79
|
+
*/
|
|
80
|
+
findSubjectsWithAccess(pathPattern: string, flags: Flags): Promise<string[]>;
|
|
81
|
+
abstract clear(): Promise<void>;
|
|
82
|
+
abstract transaction<T>(fn: (adapter: DatabaseAdapter) => Promise<T>): Promise<T>;
|
|
83
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { Flags, hasBit } from '../constants';
|
|
2
|
+
import { Right } from '../right';
|
|
3
|
+
import { Rights } from '../rights';
|
|
4
|
+
import { Role } from '../role';
|
|
5
|
+
import { RoleRegistry } from '../role-registry';
|
|
6
|
+
import { Subject } from '../subject';
|
|
7
|
+
import { DEFAULT_TABLE_PREFIX, createTableNames } from './schema';
|
|
8
|
+
/**
|
|
9
|
+
* Abstract base class for database adapters.
|
|
10
|
+
* Provides common utility methods for serialization/deserialization
|
|
11
|
+
* and table name management.
|
|
12
|
+
*/
|
|
13
|
+
export class BaseAdapter {
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
// ===========================================================================
|
|
16
|
+
// Serialization Utilities
|
|
17
|
+
// ===========================================================================
|
|
18
|
+
/**
|
|
19
|
+
* Serialize tags array to JSON string
|
|
20
|
+
*/
|
|
21
|
+
this.serializeTags = (tags) => {
|
|
22
|
+
if (tags.length === 0) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return JSON.stringify(tags.sort());
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Deserialize JSON string to tags array
|
|
29
|
+
*/
|
|
30
|
+
this.deserializeTags = (json) => {
|
|
31
|
+
if (!json) {
|
|
32
|
+
return [];
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(json);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Convert a Right instance to a database row (partial, without id and timestamps)
|
|
43
|
+
*/
|
|
44
|
+
this.rightToRow = (right) => ({
|
|
45
|
+
allow_mask: right.allowMaskValue,
|
|
46
|
+
deny_mask: right.denyMaskValue,
|
|
47
|
+
description: right.description ?? null,
|
|
48
|
+
path: right.path,
|
|
49
|
+
priority: right.priority,
|
|
50
|
+
tags: this.serializeTags(right.tags),
|
|
51
|
+
valid_from: right.validFrom?.toISOString() ?? null,
|
|
52
|
+
valid_until: right.validUntil?.toISOString() ?? null
|
|
53
|
+
});
|
|
54
|
+
/**
|
|
55
|
+
* Convert a database row to a Right instance
|
|
56
|
+
*/
|
|
57
|
+
this.rowToRight = (row) => {
|
|
58
|
+
const init = {
|
|
59
|
+
allow: this.maskToFlags(row.allow_mask),
|
|
60
|
+
deny: this.maskToFlags(row.deny_mask),
|
|
61
|
+
description: row.description ?? undefined,
|
|
62
|
+
priority: row.priority,
|
|
63
|
+
tags: this.deserializeTags(row.tags),
|
|
64
|
+
validFrom: row.valid_from ? new Date(row.valid_from) : undefined,
|
|
65
|
+
validUntil: row.valid_until ? new Date(row.valid_until) : undefined
|
|
66
|
+
};
|
|
67
|
+
return new Right(row.path, init);
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* Convert a bitmask to an array of Flag values
|
|
71
|
+
*/
|
|
72
|
+
this.maskToFlags = (mask) => {
|
|
73
|
+
const flags = [];
|
|
74
|
+
if (hasBit(mask, Flags.READ)) {
|
|
75
|
+
flags.push(Flags.READ);
|
|
76
|
+
}
|
|
77
|
+
if (hasBit(mask, Flags.WRITE)) {
|
|
78
|
+
flags.push(Flags.WRITE);
|
|
79
|
+
}
|
|
80
|
+
if (hasBit(mask, Flags.CREATE)) {
|
|
81
|
+
flags.push(Flags.CREATE);
|
|
82
|
+
}
|
|
83
|
+
if (hasBit(mask, Flags.DELETE)) {
|
|
84
|
+
flags.push(Flags.DELETE);
|
|
85
|
+
}
|
|
86
|
+
if (hasBit(mask, Flags.EXECUTE)) {
|
|
87
|
+
flags.push(Flags.EXECUTE);
|
|
88
|
+
}
|
|
89
|
+
return flags;
|
|
90
|
+
};
|
|
91
|
+
/**
|
|
92
|
+
* Convert an array of Flag values to a bitmask
|
|
93
|
+
*/
|
|
94
|
+
this.flagsToMask = (flags) => {
|
|
95
|
+
let mask = 0;
|
|
96
|
+
for (const f of flags) {
|
|
97
|
+
mask |= f;
|
|
98
|
+
}
|
|
99
|
+
return mask;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Parse an ISO 8601 timestamp string to a Date, or undefined if null/invalid
|
|
103
|
+
*/
|
|
104
|
+
this.parseTimestamp = (value) => {
|
|
105
|
+
if (!value) {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
const d = new Date(value);
|
|
109
|
+
if (Number.isNaN(d.getTime())) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
return d;
|
|
113
|
+
};
|
|
114
|
+
this.tablePrefix = options.tablePrefix ?? DEFAULT_TABLE_PREFIX;
|
|
115
|
+
this._tables = createTableNames(this.tablePrefix);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Get the table names with the configured prefix
|
|
119
|
+
*/
|
|
120
|
+
get tables() {
|
|
121
|
+
return this._tables;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Find all subject identifiers that have access to a specific path with given flags.
|
|
125
|
+
* Default implementation uses getAllSubjectIdentifiers + loadSubject.
|
|
126
|
+
* Subclasses can override with optimized batch loading implementations.
|
|
127
|
+
* @param pathPattern The path pattern to check (supports wildcards)
|
|
128
|
+
* @param flags The flags to check for
|
|
129
|
+
* @returns Array of subject identifiers that have access
|
|
130
|
+
*/
|
|
131
|
+
async findSubjectsWithAccess(pathPattern, flags) {
|
|
132
|
+
const allIdentifiers = await this.getAllSubjectIdentifiers();
|
|
133
|
+
const matchingSubjects = [];
|
|
134
|
+
for (const identifier of allIdentifiers) {
|
|
135
|
+
const subject = await this.loadSubject(identifier);
|
|
136
|
+
if (subject?.has(pathPattern, flags)) {
|
|
137
|
+
matchingSubjects.push(identifier);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return matchingSubjects;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Rights, RoleRegistry } from '../index';
|
|
2
|
+
import { PostgresAdapter, type PostgresAdapterOptions } from './postgres-adapter';
|
|
3
|
+
import { RedisAdapter, type RedisAdapterOptions } from './redis-adapter';
|
|
4
|
+
import { SQLiteAdapter, type SQLiteAdapterOptions } from './sqlite-adapter';
|
|
5
|
+
export type CreateSQLiteRightsOptions = SQLiteAdapterOptions;
|
|
6
|
+
export type CreatePostgresRegistryOptions = PostgresAdapterOptions;
|
|
7
|
+
export declare const createSQLiteRights: (options?: CreateSQLiteRightsOptions) => Promise<{
|
|
8
|
+
adapter: SQLiteAdapter;
|
|
9
|
+
rights: Rights;
|
|
10
|
+
}>;
|
|
11
|
+
export declare const createPostgresRegistry: (options: CreatePostgresRegistryOptions) => Promise<{
|
|
12
|
+
adapter: PostgresAdapter;
|
|
13
|
+
registry: RoleRegistry;
|
|
14
|
+
}>;
|
|
15
|
+
export declare const createSQLiteRegistry: (options?: CreateSQLiteRightsOptions) => Promise<{
|
|
16
|
+
adapter: SQLiteAdapter;
|
|
17
|
+
registry: RoleRegistry;
|
|
18
|
+
}>;
|
|
19
|
+
export declare const createPostgresRights: (options: CreatePostgresRegistryOptions) => Promise<{
|
|
20
|
+
adapter: PostgresAdapter;
|
|
21
|
+
rights: Rights;
|
|
22
|
+
}>;
|
|
23
|
+
export type CreateRedisRightsOptions = RedisAdapterOptions;
|
|
24
|
+
export declare const createRedisRights: (options: CreateRedisRightsOptions) => Promise<{
|
|
25
|
+
adapter: RedisAdapter;
|
|
26
|
+
rights: Rights;
|
|
27
|
+
}>;
|
|
28
|
+
export declare const createRedisRegistry: (options: CreateRedisRightsOptions) => Promise<{
|
|
29
|
+
adapter: RedisAdapter;
|
|
30
|
+
registry: RoleRegistry;
|
|
31
|
+
}>;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Rights, RoleRegistry } from '../index';
|
|
2
|
+
import { PostgresAdapter } from './postgres-adapter';
|
|
3
|
+
import { RedisAdapter } from './redis-adapter';
|
|
4
|
+
import { SQLiteAdapter } from './sqlite-adapter';
|
|
5
|
+
export const createSQLiteRights = async (options = {}) => {
|
|
6
|
+
const adapter = new SQLiteAdapter(options);
|
|
7
|
+
await adapter.connect();
|
|
8
|
+
await adapter.migrate();
|
|
9
|
+
adapter.prepareStatementsAfterMigration();
|
|
10
|
+
const rights = await adapter.loadRights();
|
|
11
|
+
return { adapter, rights };
|
|
12
|
+
};
|
|
13
|
+
export const createPostgresRegistry = async (options) => {
|
|
14
|
+
const adapter = new PostgresAdapter(options);
|
|
15
|
+
await adapter.connect();
|
|
16
|
+
await adapter.migrate();
|
|
17
|
+
const registry = await adapter.loadRegistry();
|
|
18
|
+
return { adapter, registry };
|
|
19
|
+
};
|
|
20
|
+
export const createSQLiteRegistry = async (options = {}) => {
|
|
21
|
+
const adapter = new SQLiteAdapter(options);
|
|
22
|
+
await adapter.connect();
|
|
23
|
+
await adapter.migrate();
|
|
24
|
+
adapter.prepareStatementsAfterMigration();
|
|
25
|
+
const registry = await adapter.loadRegistry();
|
|
26
|
+
return { adapter, registry };
|
|
27
|
+
};
|
|
28
|
+
export const createPostgresRights = async (options) => {
|
|
29
|
+
const adapter = new PostgresAdapter(options);
|
|
30
|
+
await adapter.connect();
|
|
31
|
+
await adapter.migrate();
|
|
32
|
+
const rights = await adapter.loadRights();
|
|
33
|
+
return { adapter, rights };
|
|
34
|
+
};
|
|
35
|
+
export const createRedisRights = async (options) => {
|
|
36
|
+
const adapter = new RedisAdapter(options);
|
|
37
|
+
await adapter.connect();
|
|
38
|
+
await adapter.migrate();
|
|
39
|
+
const rights = await adapter.loadRights();
|
|
40
|
+
return { adapter, rights };
|
|
41
|
+
};
|
|
42
|
+
export const createRedisRegistry = async (options) => {
|
|
43
|
+
const adapter = new RedisAdapter(options);
|
|
44
|
+
await adapter.connect();
|
|
45
|
+
await adapter.migrate();
|
|
46
|
+
const registry = await adapter.loadRegistry();
|
|
47
|
+
return { adapter, registry };
|
|
48
|
+
};
|