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
|
@@ -0,0 +1,655 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite';
|
|
2
|
+
import { Flags } from '../constants';
|
|
3
|
+
import { Right } from '../right';
|
|
4
|
+
import { Rights } from '../rights';
|
|
5
|
+
import { Role } from '../role';
|
|
6
|
+
import { RoleRegistry } from '../role-registry';
|
|
7
|
+
import { Subject } from '../subject';
|
|
8
|
+
import { BaseAdapter } from './base-adapter';
|
|
9
|
+
import { generateSQLiteSchema } from './schema';
|
|
10
|
+
export class SQLiteAdapter extends BaseAdapter {
|
|
11
|
+
constructor(options = {}) {
|
|
12
|
+
super(options);
|
|
13
|
+
this.db = null;
|
|
14
|
+
this.roleIdCache = new Map();
|
|
15
|
+
this.transactionDepth = 0;
|
|
16
|
+
this.stmtInsertRight = null;
|
|
17
|
+
this.stmtSelectRightById = null;
|
|
18
|
+
this.stmtSelectAllRights = null;
|
|
19
|
+
this.stmtDeleteRight = null;
|
|
20
|
+
this.stmtInsertRole = null;
|
|
21
|
+
this.stmtSelectRoleByName = null;
|
|
22
|
+
this.stmtSelectAllRoles = null;
|
|
23
|
+
this.stmtSelectRoleById = null;
|
|
24
|
+
this.stmtDeleteRole = null;
|
|
25
|
+
this.stmtInsertRoleRight = null;
|
|
26
|
+
this.stmtDeleteRoleRights = null;
|
|
27
|
+
this.stmtSelectRoleRights = null;
|
|
28
|
+
this.stmtInsertRoleInheritance = null;
|
|
29
|
+
this.stmtDeleteRoleInheritance = null;
|
|
30
|
+
this.stmtDeleteChildInheritance = null;
|
|
31
|
+
this.stmtSelectRoleInheritance = null;
|
|
32
|
+
this.stmtInsertSubject = null;
|
|
33
|
+
this.stmtSelectSubjectByIdentifier = null;
|
|
34
|
+
this.stmtUpdateSubject = null;
|
|
35
|
+
this.stmtDeleteSubject = null;
|
|
36
|
+
this.stmtInsertSubjectRole = null;
|
|
37
|
+
this.stmtDeleteSubjectRoles = null;
|
|
38
|
+
this.stmtSelectSubjectRoles = null;
|
|
39
|
+
this.stmtInsertSubjectRight = null;
|
|
40
|
+
this.stmtDeleteSubjectRights = null;
|
|
41
|
+
this.stmtSelectSubjectRights = null;
|
|
42
|
+
// ===========================================================================
|
|
43
|
+
// Prepared Statements
|
|
44
|
+
// ===========================================================================
|
|
45
|
+
this.prepareStatements = () => {
|
|
46
|
+
if (!this.db) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const { rights, roleInheritance, roleRights, roles, subjectRights, subjectRoles, subjects } = this.tables;
|
|
50
|
+
this.stmtInsertRight = this.db.prepare(`
|
|
51
|
+
INSERT INTO ${rights} (path, allow_mask, deny_mask, priority, description, tags, valid_from, valid_until)
|
|
52
|
+
VALUES ($path, $allow_mask, $deny_mask, $priority, $description, $tags, $valid_from, $valid_until)
|
|
53
|
+
`);
|
|
54
|
+
this.stmtSelectRightById = this.db.prepare(`
|
|
55
|
+
SELECT * FROM ${rights} WHERE id = $id
|
|
56
|
+
`);
|
|
57
|
+
this.stmtSelectAllRights = this.db.prepare(`
|
|
58
|
+
SELECT * FROM ${rights} ORDER BY id
|
|
59
|
+
`);
|
|
60
|
+
this.stmtDeleteRight = this.db.prepare(`
|
|
61
|
+
DELETE FROM ${rights} WHERE id = $id
|
|
62
|
+
`);
|
|
63
|
+
this.stmtInsertRole = this.db.prepare(`
|
|
64
|
+
INSERT INTO ${roles} (name) VALUES ($name)
|
|
65
|
+
`);
|
|
66
|
+
this.stmtSelectRoleByName = this.db.prepare(`
|
|
67
|
+
SELECT * FROM ${roles} WHERE name = $name
|
|
68
|
+
`);
|
|
69
|
+
this.stmtSelectAllRoles = this.db.prepare(`
|
|
70
|
+
SELECT * FROM ${roles} ORDER BY id
|
|
71
|
+
`);
|
|
72
|
+
this.stmtSelectRoleById = this.db.prepare(`
|
|
73
|
+
SELECT * FROM ${roles} WHERE id = $id
|
|
74
|
+
`);
|
|
75
|
+
this.stmtDeleteRole = this.db.prepare(`
|
|
76
|
+
DELETE FROM ${roles} WHERE name = $name
|
|
77
|
+
`);
|
|
78
|
+
this.stmtInsertRoleRight = this.db.prepare(`
|
|
79
|
+
INSERT INTO ${roleRights} (role_id, right_id) VALUES ($role_id, $right_id)
|
|
80
|
+
`);
|
|
81
|
+
this.stmtDeleteRoleRights = this.db.prepare(`
|
|
82
|
+
DELETE FROM ${roleRights} WHERE role_id = $role_id
|
|
83
|
+
`);
|
|
84
|
+
this.stmtSelectRoleRights = this.db.prepare(`
|
|
85
|
+
SELECT right_id FROM ${roleRights} WHERE role_id = $role_id
|
|
86
|
+
`);
|
|
87
|
+
this.stmtInsertRoleInheritance = this.db.prepare(`
|
|
88
|
+
INSERT INTO ${roleInheritance} (child_role_id, parent_role_id) VALUES ($child_role_id, $parent_role_id)
|
|
89
|
+
`);
|
|
90
|
+
this.stmtDeleteRoleInheritance = this.db.prepare(`
|
|
91
|
+
DELETE FROM ${roleInheritance} WHERE child_role_id = $child_role_id AND parent_role_id = $parent_role_id
|
|
92
|
+
`);
|
|
93
|
+
this.stmtDeleteChildInheritance = this.db.prepare(`
|
|
94
|
+
DELETE FROM ${roleInheritance} WHERE child_role_id = $child_role_id
|
|
95
|
+
`);
|
|
96
|
+
this.stmtSelectRoleInheritance = this.db.prepare(`
|
|
97
|
+
SELECT child_role_id, parent_role_id FROM ${roleInheritance}
|
|
98
|
+
`);
|
|
99
|
+
this.stmtInsertSubject = this.db.prepare(`
|
|
100
|
+
INSERT INTO ${subjects} (identifier) VALUES ($identifier)
|
|
101
|
+
`);
|
|
102
|
+
this.stmtSelectSubjectByIdentifier = this.db.prepare(`
|
|
103
|
+
SELECT * FROM ${subjects} WHERE identifier = $identifier
|
|
104
|
+
`);
|
|
105
|
+
this.stmtUpdateSubject = this.db.prepare(`
|
|
106
|
+
UPDATE ${subjects} SET updated_at = datetime('now') WHERE id = $id
|
|
107
|
+
`);
|
|
108
|
+
this.stmtDeleteSubject = this.db.prepare(`
|
|
109
|
+
DELETE FROM ${subjects} WHERE identifier = $identifier
|
|
110
|
+
`);
|
|
111
|
+
this.stmtInsertSubjectRole = this.db.prepare(`
|
|
112
|
+
INSERT INTO ${subjectRoles} (subject_id, role_id) VALUES ($subject_id, $role_id)
|
|
113
|
+
`);
|
|
114
|
+
this.stmtDeleteSubjectRoles = this.db.prepare(`
|
|
115
|
+
DELETE FROM ${subjectRoles} WHERE subject_id = $subject_id
|
|
116
|
+
`);
|
|
117
|
+
this.stmtSelectSubjectRoles = this.db.prepare(`
|
|
118
|
+
SELECT role_id FROM ${subjectRoles} WHERE subject_id = $subject_id
|
|
119
|
+
`);
|
|
120
|
+
this.stmtInsertSubjectRight = this.db.prepare(`
|
|
121
|
+
INSERT INTO ${subjectRights} (subject_id, right_id) VALUES ($subject_id, $right_id)
|
|
122
|
+
`);
|
|
123
|
+
this.stmtDeleteSubjectRights = this.db.prepare(`
|
|
124
|
+
DELETE FROM ${subjectRights} WHERE subject_id = $subject_id
|
|
125
|
+
`);
|
|
126
|
+
this.stmtSelectSubjectRights = this.db.prepare(`
|
|
127
|
+
SELECT right_id FROM ${subjectRights} WHERE subject_id = $subject_id
|
|
128
|
+
`);
|
|
129
|
+
};
|
|
130
|
+
this.finalizeStatements = () => {
|
|
131
|
+
this.stmtInsertRight?.finalize();
|
|
132
|
+
this.stmtSelectRightById?.finalize();
|
|
133
|
+
this.stmtSelectAllRights?.finalize();
|
|
134
|
+
this.stmtDeleteRight?.finalize();
|
|
135
|
+
this.stmtInsertRole?.finalize();
|
|
136
|
+
this.stmtSelectRoleByName?.finalize();
|
|
137
|
+
this.stmtSelectAllRoles?.finalize();
|
|
138
|
+
this.stmtSelectRoleById?.finalize();
|
|
139
|
+
this.stmtDeleteRole?.finalize();
|
|
140
|
+
this.stmtInsertRoleRight?.finalize();
|
|
141
|
+
this.stmtDeleteRoleRights?.finalize();
|
|
142
|
+
this.stmtSelectRoleRights?.finalize();
|
|
143
|
+
this.stmtInsertRoleInheritance?.finalize();
|
|
144
|
+
this.stmtDeleteRoleInheritance?.finalize();
|
|
145
|
+
this.stmtDeleteChildInheritance?.finalize();
|
|
146
|
+
this.stmtSelectRoleInheritance?.finalize();
|
|
147
|
+
this.stmtInsertSubject?.finalize();
|
|
148
|
+
this.stmtSelectSubjectByIdentifier?.finalize();
|
|
149
|
+
this.stmtUpdateSubject?.finalize();
|
|
150
|
+
this.stmtDeleteSubject?.finalize();
|
|
151
|
+
this.stmtInsertSubjectRole?.finalize();
|
|
152
|
+
this.stmtDeleteSubjectRoles?.finalize();
|
|
153
|
+
this.stmtSelectSubjectRoles?.finalize();
|
|
154
|
+
this.stmtInsertSubjectRight?.finalize();
|
|
155
|
+
this.stmtDeleteSubjectRights?.finalize();
|
|
156
|
+
this.stmtSelectSubjectRights?.finalize();
|
|
157
|
+
};
|
|
158
|
+
this.options = {
|
|
159
|
+
create: true,
|
|
160
|
+
enableWAL: false,
|
|
161
|
+
filename: ':memory:',
|
|
162
|
+
...options
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
async connect() {
|
|
166
|
+
this.db = new Database(this.options.filename, {
|
|
167
|
+
create: this.options.create,
|
|
168
|
+
readonly: this.options.readonly,
|
|
169
|
+
strict: this.options.strict
|
|
170
|
+
});
|
|
171
|
+
if (this.options.enableWAL) {
|
|
172
|
+
this.db.run('PRAGMA journal_mode = WAL');
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
prepareStatementsAfterMigration() {
|
|
176
|
+
this.prepareStatements();
|
|
177
|
+
}
|
|
178
|
+
async disconnect() {
|
|
179
|
+
this.finalizeStatements();
|
|
180
|
+
this.db?.close();
|
|
181
|
+
this.db = null;
|
|
182
|
+
this.roleIdCache.clear();
|
|
183
|
+
}
|
|
184
|
+
async migrate() {
|
|
185
|
+
if (!this.db) {
|
|
186
|
+
throw new Error('Not connected');
|
|
187
|
+
}
|
|
188
|
+
this.db.run(generateSQLiteSchema(this.tables));
|
|
189
|
+
}
|
|
190
|
+
async enableWAL() {
|
|
191
|
+
if (!this.db) {
|
|
192
|
+
throw new Error('Not connected');
|
|
193
|
+
}
|
|
194
|
+
this.db.run('PRAGMA journal_mode = WAL');
|
|
195
|
+
}
|
|
196
|
+
// ===========================================================================
|
|
197
|
+
// Rights Operations
|
|
198
|
+
// ===========================================================================
|
|
199
|
+
async saveRight(right) {
|
|
200
|
+
if (!this.db || !this.stmtInsertRight) {
|
|
201
|
+
throw new Error('Not connected');
|
|
202
|
+
}
|
|
203
|
+
const row = this.rightToRow(right);
|
|
204
|
+
this.stmtInsertRight.run({
|
|
205
|
+
$allow_mask: row.allow_mask,
|
|
206
|
+
$deny_mask: row.deny_mask,
|
|
207
|
+
$description: row.description,
|
|
208
|
+
$path: row.path,
|
|
209
|
+
$priority: row.priority,
|
|
210
|
+
$tags: row.tags,
|
|
211
|
+
$valid_from: row.valid_from,
|
|
212
|
+
$valid_until: row.valid_until
|
|
213
|
+
});
|
|
214
|
+
const result = this.db.query('SELECT last_insert_rowid() as id').get();
|
|
215
|
+
right._setDbId(result.id);
|
|
216
|
+
return result.id;
|
|
217
|
+
}
|
|
218
|
+
async saveRights(rights) {
|
|
219
|
+
const ids = [];
|
|
220
|
+
await this.transaction(async () => {
|
|
221
|
+
for (const right of rights.allRights()) {
|
|
222
|
+
const id = await this.saveRight(right);
|
|
223
|
+
ids.push(id);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
return ids;
|
|
227
|
+
}
|
|
228
|
+
async loadRight(id) {
|
|
229
|
+
if (!this.db || !this.stmtSelectRightById) {
|
|
230
|
+
throw new Error('Not connected');
|
|
231
|
+
}
|
|
232
|
+
const row = this.stmtSelectRightById.get({ $id: id });
|
|
233
|
+
if (!row) {
|
|
234
|
+
return null;
|
|
235
|
+
}
|
|
236
|
+
return this.rowToRight(row);
|
|
237
|
+
}
|
|
238
|
+
async loadRights() {
|
|
239
|
+
if (!this.db || !this.stmtSelectAllRights) {
|
|
240
|
+
throw new Error('Not connected');
|
|
241
|
+
}
|
|
242
|
+
const rows = this.stmtSelectAllRights.all();
|
|
243
|
+
const loadedRights = new Rights();
|
|
244
|
+
for (const row of rows) {
|
|
245
|
+
loadedRights.add(this.rowToRight(row));
|
|
246
|
+
}
|
|
247
|
+
return loadedRights;
|
|
248
|
+
}
|
|
249
|
+
async loadRightsByPath(pathPattern) {
|
|
250
|
+
if (!this.db) {
|
|
251
|
+
throw new Error('Not connected');
|
|
252
|
+
}
|
|
253
|
+
const { rights: rightsTable } = this.tables;
|
|
254
|
+
const pattern = pathPattern
|
|
255
|
+
.replaceAll('%', String.raw `\%`)
|
|
256
|
+
.replaceAll('_', String.raw `\_`);
|
|
257
|
+
const sqlPattern = pattern.replaceAll('*', '%');
|
|
258
|
+
const stmt = this.db.prepare(String.raw `SELECT * FROM ${rightsTable} WHERE path LIKE $pattern ESCAPE '\' ORDER BY id`);
|
|
259
|
+
const rows = stmt.all({ $pattern: sqlPattern });
|
|
260
|
+
const loadedRights = new Rights();
|
|
261
|
+
for (const row of rows) {
|
|
262
|
+
loadedRights.add(this.rowToRight(row));
|
|
263
|
+
}
|
|
264
|
+
return loadedRights;
|
|
265
|
+
}
|
|
266
|
+
async deleteRight(id) {
|
|
267
|
+
if (!this.db || !this.stmtDeleteRight) {
|
|
268
|
+
throw new Error('Not connected');
|
|
269
|
+
}
|
|
270
|
+
const result = this.stmtDeleteRight.run({ $id: id });
|
|
271
|
+
return result.changes > 0;
|
|
272
|
+
}
|
|
273
|
+
// ===========================================================================
|
|
274
|
+
// Role Operations
|
|
275
|
+
// ===========================================================================
|
|
276
|
+
async saveRole(role) {
|
|
277
|
+
if (!this.db) {
|
|
278
|
+
throw new Error('Not connected');
|
|
279
|
+
}
|
|
280
|
+
const { roles } = this.tables;
|
|
281
|
+
return this.transaction(async () => {
|
|
282
|
+
let roleId;
|
|
283
|
+
const existing = this.db.prepare(`SELECT id FROM ${roles} WHERE name = $name`).get({ $name: role.name });
|
|
284
|
+
if (existing) {
|
|
285
|
+
roleId = existing.id;
|
|
286
|
+
this.db.prepare(`UPDATE ${roles} SET updated_at = datetime('now') WHERE id = $id`).run({ $id: roleId });
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
this.db.prepare(`INSERT INTO ${roles} (name) VALUES ($name)`).run({
|
|
290
|
+
$name: role.name
|
|
291
|
+
});
|
|
292
|
+
const result = this.db.query('SELECT last_insert_rowid() as id').get();
|
|
293
|
+
roleId = result.id;
|
|
294
|
+
}
|
|
295
|
+
this.roleIdCache.set(role.name, roleId);
|
|
296
|
+
this.stmtDeleteRoleRights?.run({ $role_id: roleId });
|
|
297
|
+
this.stmtDeleteChildInheritance?.run({ $child_role_id: roleId });
|
|
298
|
+
for (const right of role.rights.allRights()) {
|
|
299
|
+
const rightId = await this.saveRight(right);
|
|
300
|
+
this.stmtInsertRoleRight?.run({
|
|
301
|
+
$right_id: rightId,
|
|
302
|
+
$role_id: roleId
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
for (const parent of role.parents) {
|
|
306
|
+
const parentId = await this.saveRole(parent);
|
|
307
|
+
this.stmtInsertRoleInheritance?.run({
|
|
308
|
+
$child_role_id: roleId,
|
|
309
|
+
$parent_role_id: parentId
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
return roleId;
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
async loadRole(name) {
|
|
316
|
+
if (!this.db || !this.stmtSelectRoleByName) {
|
|
317
|
+
throw new Error('Not connected');
|
|
318
|
+
}
|
|
319
|
+
const roleRow = this.stmtSelectRoleByName.get({
|
|
320
|
+
$name: name
|
|
321
|
+
});
|
|
322
|
+
if (!roleRow) {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
const rights = new Rights();
|
|
326
|
+
const rightRows = this.stmtSelectRoleRights?.all({
|
|
327
|
+
$role_id: roleRow.id
|
|
328
|
+
});
|
|
329
|
+
if (rightRows) {
|
|
330
|
+
for (const rr of rightRows) {
|
|
331
|
+
const right = await this.loadRight(rr.right_id);
|
|
332
|
+
if (right) {
|
|
333
|
+
rights.add(right);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return new Role(name, rights);
|
|
338
|
+
}
|
|
339
|
+
async loadRoles() {
|
|
340
|
+
if (!this.db || !this.stmtSelectAllRoles) {
|
|
341
|
+
throw new Error('Not connected');
|
|
342
|
+
}
|
|
343
|
+
const roleRows = this.stmtSelectAllRoles.all();
|
|
344
|
+
const roles = [];
|
|
345
|
+
for (const roleRow of roleRows) {
|
|
346
|
+
const role = await this.loadRole(roleRow.name);
|
|
347
|
+
if (role) {
|
|
348
|
+
roles.push(role);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return roles;
|
|
352
|
+
}
|
|
353
|
+
async deleteRole(name) {
|
|
354
|
+
if (!this.db || !this.stmtDeleteRole) {
|
|
355
|
+
throw new Error('Not connected');
|
|
356
|
+
}
|
|
357
|
+
const roleRow = this.stmtSelectRoleByName?.get({ $name: name });
|
|
358
|
+
if (roleRow) {
|
|
359
|
+
this.roleIdCache.delete(name);
|
|
360
|
+
}
|
|
361
|
+
const result = this.stmtDeleteRole.run({ $name: name });
|
|
362
|
+
return result.changes > 0;
|
|
363
|
+
}
|
|
364
|
+
// ===========================================================================
|
|
365
|
+
// RoleRegistry Operations
|
|
366
|
+
// ===========================================================================
|
|
367
|
+
async saveRegistry(registry) {
|
|
368
|
+
if (!this.db) {
|
|
369
|
+
throw new Error('Not connected');
|
|
370
|
+
}
|
|
371
|
+
await this.transaction(async () => {
|
|
372
|
+
const rolesToSave = new Map();
|
|
373
|
+
const collectRoles = (role) => {
|
|
374
|
+
if (!rolesToSave.has(role.name)) {
|
|
375
|
+
rolesToSave.set(role.name, role);
|
|
376
|
+
for (const parent of role.parents) {
|
|
377
|
+
collectRoles(parent);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
registry.toJSON().forEach(roleJson => {
|
|
382
|
+
const role = registry.get(roleJson.name);
|
|
383
|
+
if (role) {
|
|
384
|
+
collectRoles(role);
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
for (const role of rolesToSave.values()) {
|
|
388
|
+
await this.saveRole(role);
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
async loadRegistry() {
|
|
393
|
+
const registry = new RoleRegistry();
|
|
394
|
+
const roles = await this.loadRoles();
|
|
395
|
+
const roleMap = new Map();
|
|
396
|
+
for (const role of roles) {
|
|
397
|
+
const registryRole = registry.define(role.name, role.rights);
|
|
398
|
+
const roleRow = this.stmtSelectRoleByName?.get({
|
|
399
|
+
$name: role.name
|
|
400
|
+
});
|
|
401
|
+
if (roleRow) {
|
|
402
|
+
roleMap.set(role.name, { id: roleRow.id, role: registryRole });
|
|
403
|
+
this.roleIdCache.set(role.name, roleRow.id);
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
const { roleInheritance } = this.tables;
|
|
407
|
+
const inheritRows = this.db
|
|
408
|
+
?.query(`SELECT child_role_id, parent_role_id FROM ${roleInheritance}`)
|
|
409
|
+
.all();
|
|
410
|
+
if (inheritRows) {
|
|
411
|
+
for (const ir of inheritRows) {
|
|
412
|
+
let childRole;
|
|
413
|
+
let parentRole;
|
|
414
|
+
for (const [, data] of roleMap.entries()) {
|
|
415
|
+
if (data.id === ir.child_role_id) {
|
|
416
|
+
childRole = data.role;
|
|
417
|
+
}
|
|
418
|
+
if (data.id === ir.parent_role_id) {
|
|
419
|
+
parentRole = data.role;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
if (childRole && parentRole) {
|
|
423
|
+
childRole.inheritsFrom(parentRole);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return registry;
|
|
428
|
+
}
|
|
429
|
+
// ===========================================================================
|
|
430
|
+
// Subject Operations
|
|
431
|
+
// ===========================================================================
|
|
432
|
+
async saveSubject(identifier, subject) {
|
|
433
|
+
if (!this.db) {
|
|
434
|
+
throw new Error('Not connected');
|
|
435
|
+
}
|
|
436
|
+
const { subjects } = this.tables;
|
|
437
|
+
return this.transaction(async () => {
|
|
438
|
+
let subjectId;
|
|
439
|
+
const existing = this.db.prepare(`SELECT id FROM ${subjects} WHERE identifier = $identifier`).get({ $identifier: identifier });
|
|
440
|
+
if (existing) {
|
|
441
|
+
subjectId = existing.id;
|
|
442
|
+
this.db.prepare(`UPDATE ${subjects} SET updated_at = datetime('now') WHERE id = $id`).run({ $id: subjectId });
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
this.db.prepare(`INSERT INTO ${subjects} (identifier) VALUES ($identifier)`).run({ $identifier: identifier });
|
|
446
|
+
const result = this.db.query('SELECT last_insert_rowid() as id').get();
|
|
447
|
+
subjectId = result.id;
|
|
448
|
+
}
|
|
449
|
+
this.stmtDeleteSubjectRoles?.run({ $subject_id: subjectId });
|
|
450
|
+
this.stmtDeleteSubjectRights?.run({ $subject_id: subjectId });
|
|
451
|
+
for (const role of subject.roles) {
|
|
452
|
+
const roleId = await this.saveRole(role);
|
|
453
|
+
this.stmtInsertSubjectRole?.run({
|
|
454
|
+
$role_id: roleId,
|
|
455
|
+
$subject_id: subjectId
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
for (const right of subject.rights.allRights()) {
|
|
459
|
+
const rightId = await this.saveRight(right);
|
|
460
|
+
this.stmtInsertSubjectRight?.run({
|
|
461
|
+
$right_id: rightId,
|
|
462
|
+
$subject_id: subjectId
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
return subjectId;
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
async loadSubject(identifier) {
|
|
469
|
+
if (!this.db || !this.stmtSelectSubjectByIdentifier) {
|
|
470
|
+
throw new Error('Not connected');
|
|
471
|
+
}
|
|
472
|
+
const subjectRow = this.stmtSelectSubjectByIdentifier.get({
|
|
473
|
+
$identifier: identifier
|
|
474
|
+
});
|
|
475
|
+
if (!subjectRow) {
|
|
476
|
+
return null;
|
|
477
|
+
}
|
|
478
|
+
const subject = new Subject();
|
|
479
|
+
const roleRows = this.stmtSelectSubjectRoles?.all({
|
|
480
|
+
$subject_id: subjectRow.id
|
|
481
|
+
});
|
|
482
|
+
if (roleRows) {
|
|
483
|
+
for (const sr of roleRows) {
|
|
484
|
+
const roleName = this.db.prepare(`SELECT name FROM ${this.tables.roles} WHERE id = $id`).get({ $id: sr.role_id });
|
|
485
|
+
if (roleName) {
|
|
486
|
+
const role = await this.loadRole(roleName.name);
|
|
487
|
+
if (role) {
|
|
488
|
+
subject.memberOf(role);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
const rightRows = this.stmtSelectSubjectRights?.all({
|
|
494
|
+
$subject_id: subjectRow.id
|
|
495
|
+
});
|
|
496
|
+
if (rightRows) {
|
|
497
|
+
for (const sr of rightRows) {
|
|
498
|
+
const right = await this.loadRight(sr.right_id);
|
|
499
|
+
if (right) {
|
|
500
|
+
subject.rights.add(right);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
return subject;
|
|
505
|
+
}
|
|
506
|
+
async deleteSubject(identifier) {
|
|
507
|
+
if (!this.db || !this.stmtDeleteSubject) {
|
|
508
|
+
throw new Error('Not connected');
|
|
509
|
+
}
|
|
510
|
+
const result = this.stmtDeleteSubject.run({ $identifier: identifier });
|
|
511
|
+
return result.changes > 0;
|
|
512
|
+
}
|
|
513
|
+
async getAllSubjectIdentifiers() {
|
|
514
|
+
if (!this.db) {
|
|
515
|
+
throw new Error('Not connected');
|
|
516
|
+
}
|
|
517
|
+
const rows = this.db
|
|
518
|
+
.prepare(`SELECT identifier FROM ${this.tables.subjects}`)
|
|
519
|
+
.all();
|
|
520
|
+
return rows.map(row => row.identifier);
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Optimized findSubjectsWithAccess using batch loading with JOINs.
|
|
524
|
+
* Reduces N+1 queries to a constant number of queries regardless of subject count.
|
|
525
|
+
*/
|
|
526
|
+
async findSubjectsWithAccess(pathPattern, flags) {
|
|
527
|
+
if (!this.db) {
|
|
528
|
+
throw new Error('Not connected');
|
|
529
|
+
}
|
|
530
|
+
const { rights, roleRights, roles, subjectRights, subjectRoles, subjects } = this.tables;
|
|
531
|
+
// Load all subjects with their data in batch queries
|
|
532
|
+
const subjectRows = this.db
|
|
533
|
+
.prepare(`SELECT id, identifier FROM ${subjects}`)
|
|
534
|
+
.all();
|
|
535
|
+
if (subjectRows.length === 0) {
|
|
536
|
+
return [];
|
|
537
|
+
}
|
|
538
|
+
// Create a map of subject id -> identifier for quick lookup
|
|
539
|
+
const subjectIdToIdentifier = new Map();
|
|
540
|
+
const subjectIdentifierToId = new Map();
|
|
541
|
+
for (const row of subjectRows) {
|
|
542
|
+
subjectIdToIdentifier.set(row.id, row.identifier);
|
|
543
|
+
subjectIdentifierToId.set(row.identifier, row.id);
|
|
544
|
+
}
|
|
545
|
+
// Batch load all subject-role mappings with role names
|
|
546
|
+
const subjectRoleRows = this.db
|
|
547
|
+
.prepare(`SELECT sr.subject_id, r.name as role_name
|
|
548
|
+
FROM ${subjectRoles} sr
|
|
549
|
+
JOIN ${roles} r ON sr.role_id = r.id`)
|
|
550
|
+
.all();
|
|
551
|
+
// Batch load all subject direct rights
|
|
552
|
+
const subjectRightRows = this.db
|
|
553
|
+
.prepare(`SELECT sr.subject_id, rt.*
|
|
554
|
+
FROM ${subjectRights} sr
|
|
555
|
+
JOIN ${rights} rt ON sr.right_id = rt.id`)
|
|
556
|
+
.all();
|
|
557
|
+
// Batch load all role rights
|
|
558
|
+
const roleRightRows = this.db
|
|
559
|
+
.prepare(`SELECT r.name as role_name, rt.*
|
|
560
|
+
FROM ${roleRights} rr
|
|
561
|
+
JOIN ${roles} r ON rr.role_id = r.id
|
|
562
|
+
JOIN ${rights} rt ON rr.right_id = rt.id`)
|
|
563
|
+
.all();
|
|
564
|
+
// Build role -> rights mapping
|
|
565
|
+
const roleRightsMap = new Map();
|
|
566
|
+
for (const row of roleRightRows) {
|
|
567
|
+
if (!roleRightsMap.has(row.role_name)) {
|
|
568
|
+
roleRightsMap.set(row.role_name, new Rights());
|
|
569
|
+
}
|
|
570
|
+
roleRightsMap.get(row.role_name).add(this.rowToRight(row));
|
|
571
|
+
}
|
|
572
|
+
// Build subject -> roles mapping
|
|
573
|
+
const subjectRolesMap = new Map();
|
|
574
|
+
for (const row of subjectRoleRows) {
|
|
575
|
+
if (!subjectRolesMap.has(row.subject_id)) {
|
|
576
|
+
subjectRolesMap.set(row.subject_id, []);
|
|
577
|
+
}
|
|
578
|
+
subjectRolesMap.get(row.subject_id).push(row.role_name);
|
|
579
|
+
}
|
|
580
|
+
// Build subject -> direct rights mapping
|
|
581
|
+
const subjectDirectRightsMap = new Map();
|
|
582
|
+
for (const row of subjectRightRows) {
|
|
583
|
+
if (!subjectDirectRightsMap.has(row.subject_id)) {
|
|
584
|
+
subjectDirectRightsMap.set(row.subject_id, new Rights());
|
|
585
|
+
}
|
|
586
|
+
subjectDirectRightsMap.get(row.subject_id).add(this.rowToRight(row));
|
|
587
|
+
}
|
|
588
|
+
// Now construct Subject objects and check access
|
|
589
|
+
const matchingSubjects = [];
|
|
590
|
+
for (const [subjectId, identifier] of subjectIdToIdentifier) {
|
|
591
|
+
const subject = new Subject();
|
|
592
|
+
// Add roles with their rights
|
|
593
|
+
const roleNames = subjectRolesMap.get(subjectId) ?? [];
|
|
594
|
+
for (const roleName of roleNames) {
|
|
595
|
+
const roleRights = roleRightsMap.get(roleName) ?? new Rights();
|
|
596
|
+
const role = new Role(roleName, roleRights);
|
|
597
|
+
subject.memberOf(role);
|
|
598
|
+
}
|
|
599
|
+
// Add direct rights
|
|
600
|
+
const directRights = subjectDirectRightsMap.get(subjectId);
|
|
601
|
+
if (directRights) {
|
|
602
|
+
for (const right of directRights.allRights()) {
|
|
603
|
+
subject.rights.add(right);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
// Check if subject has the requested access
|
|
607
|
+
if (subject.has(pathPattern, flags)) {
|
|
608
|
+
matchingSubjects.push(identifier);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return matchingSubjects;
|
|
612
|
+
}
|
|
613
|
+
// ===========================================================================
|
|
614
|
+
// Utility
|
|
615
|
+
// ===========================================================================
|
|
616
|
+
async clear() {
|
|
617
|
+
if (!this.db) {
|
|
618
|
+
throw new Error('Not connected');
|
|
619
|
+
}
|
|
620
|
+
const { rights, roleInheritance, roleRights, roles, subjectRights, subjectRoles, subjects } = this.tables;
|
|
621
|
+
this.db.run(`DELETE FROM ${subjectRights}`);
|
|
622
|
+
this.db.run(`DELETE FROM ${subjectRoles}`);
|
|
623
|
+
this.db.run(`DELETE FROM ${subjects}`);
|
|
624
|
+
this.db.run(`DELETE FROM ${roleInheritance}`);
|
|
625
|
+
this.db.run(`DELETE FROM ${roleRights}`);
|
|
626
|
+
this.db.run(`DELETE FROM ${roles}`);
|
|
627
|
+
this.db.run(`DELETE FROM ${rights}`);
|
|
628
|
+
this.roleIdCache.clear();
|
|
629
|
+
}
|
|
630
|
+
async transaction(fn) {
|
|
631
|
+
if (!this.db) {
|
|
632
|
+
throw new Error('Not connected');
|
|
633
|
+
}
|
|
634
|
+
const isNested = this.transactionDepth > 0;
|
|
635
|
+
if (!isNested) {
|
|
636
|
+
this.db.run('BEGIN IMMEDIATE TRANSACTION');
|
|
637
|
+
}
|
|
638
|
+
this.transactionDepth++;
|
|
639
|
+
try {
|
|
640
|
+
const result = await fn(this);
|
|
641
|
+
this.transactionDepth--;
|
|
642
|
+
if (!isNested) {
|
|
643
|
+
this.db.run('COMMIT');
|
|
644
|
+
}
|
|
645
|
+
return result;
|
|
646
|
+
}
|
|
647
|
+
catch (error) {
|
|
648
|
+
this.transactionDepth--;
|
|
649
|
+
if (!isNested) {
|
|
650
|
+
this.db.run('ROLLBACK');
|
|
651
|
+
}
|
|
652
|
+
throw error;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|