@balena/pinejs 15.0.0-true-boolean-7896b116c446d891d7a0d5e4085c02a13bc9c725 → 15.0.1-build-migrations-clarify-marking-sbvr-optional-d6d0ded8eccc6eadb2492f4697918cf0afd00215-1
Sign up to get free protection for your applications and to get access to all the features.
- package/.dockerignore +4 -0
- package/.github/workflows/flowzone.yml +21 -0
- package/.husky/pre-commit +4 -0
- package/.pinejs-cache.json +1 -0
- package/.resinci.yml +1 -0
- package/.versionbot/CHANGELOG.yml +9678 -2002
- package/CHANGELOG.md +2976 -2
- package/Dockerfile +14 -0
- package/Gruntfile.ts +3 -6
- package/README.md +10 -1
- package/VERSION +1 -0
- package/build/browser.ts +1 -1
- package/build/config.ts +0 -1
- package/docker-compose.npm-test.yml +11 -0
- package/docs/AdvancedUsage.md +77 -63
- package/docs/GettingStarted.md +90 -41
- package/docs/Migrations.md +102 -1
- package/docs/ProjectConfig.md +12 -21
- package/docs/Testing.md +7 -0
- package/out/bin/abstract-sql-compiler.js +17 -17
- package/out/bin/abstract-sql-compiler.js.map +1 -1
- package/out/bin/odata-compiler.js +23 -20
- package/out/bin/odata-compiler.js.map +1 -1
- package/out/bin/sbvr-compiler.js +22 -22
- package/out/bin/sbvr-compiler.js.map +1 -1
- package/out/bin/utils.d.ts +2 -2
- package/out/bin/utils.js +3 -3
- package/out/bin/utils.js.map +1 -1
- package/out/config-loader/config-loader.d.ts +9 -8
- package/out/config-loader/config-loader.js +135 -78
- package/out/config-loader/config-loader.js.map +1 -1
- package/out/config-loader/env.d.ts +41 -16
- package/out/config-loader/env.js +46 -2
- package/out/config-loader/env.js.map +1 -1
- package/out/data-server/sbvr-server.d.ts +2 -19
- package/out/data-server/sbvr-server.js +44 -38
- package/out/data-server/sbvr-server.js.map +1 -1
- package/out/database-layer/db.d.ts +32 -14
- package/out/database-layer/db.js +120 -41
- package/out/database-layer/db.js.map +1 -1
- package/out/express-emulator/express.js +10 -11
- package/out/express-emulator/express.js.map +1 -1
- package/out/http-transactions/transactions.d.ts +2 -18
- package/out/http-transactions/transactions.js +29 -21
- package/out/http-transactions/transactions.js.map +1 -1
- package/out/migrator/async.d.ts +7 -0
- package/out/migrator/async.js +168 -0
- package/out/migrator/async.js.map +1 -0
- package/out/migrator/migrations.sbvr +43 -0
- package/out/migrator/sync.d.ts +9 -0
- package/out/migrator/sync.js +106 -0
- package/out/migrator/sync.js.map +1 -0
- package/out/migrator/utils.d.ts +78 -0
- package/out/migrator/utils.js +283 -0
- package/out/migrator/utils.js.map +1 -0
- package/out/odata-metadata/odata-metadata-generator.js +10 -13
- package/out/odata-metadata/odata-metadata-generator.js.map +1 -1
- package/out/passport-pinejs/passport-pinejs.d.ts +1 -1
- package/out/passport-pinejs/passport-pinejs.js +8 -7
- package/out/passport-pinejs/passport-pinejs.js.map +1 -1
- package/out/pinejs-session-store/pinejs-session-store.d.ts +1 -1
- package/out/pinejs-session-store/pinejs-session-store.js +20 -6
- package/out/pinejs-session-store/pinejs-session-store.js.map +1 -1
- package/out/sbvr-api/abstract-sql.d.ts +3 -2
- package/out/sbvr-api/abstract-sql.js +9 -9
- package/out/sbvr-api/abstract-sql.js.map +1 -1
- package/out/sbvr-api/cached-compile.js +1 -1
- package/out/sbvr-api/cached-compile.js.map +1 -1
- package/out/sbvr-api/common-types.d.ts +6 -5
- package/out/sbvr-api/control-flow.d.ts +8 -1
- package/out/sbvr-api/control-flow.js +36 -9
- package/out/sbvr-api/control-flow.js.map +1 -1
- package/out/sbvr-api/errors.d.ts +47 -40
- package/out/sbvr-api/errors.js +78 -77
- package/out/sbvr-api/errors.js.map +1 -1
- package/out/sbvr-api/express-extension.d.ts +4 -0
- package/out/sbvr-api/hooks.d.ts +16 -15
- package/out/sbvr-api/hooks.js +74 -48
- package/out/sbvr-api/hooks.js.map +1 -1
- package/out/sbvr-api/odata-response.d.ts +2 -2
- package/out/sbvr-api/odata-response.js +28 -30
- package/out/sbvr-api/odata-response.js.map +1 -1
- package/out/sbvr-api/permissions.d.ts +17 -16
- package/out/sbvr-api/permissions.js +369 -304
- package/out/sbvr-api/permissions.js.map +1 -1
- package/out/sbvr-api/sbvr-utils.d.ts +33 -15
- package/out/sbvr-api/sbvr-utils.js +397 -235
- package/out/sbvr-api/sbvr-utils.js.map +1 -1
- package/out/sbvr-api/translations.d.ts +6 -0
- package/out/sbvr-api/translations.js +150 -0
- package/out/sbvr-api/translations.js.map +1 -0
- package/out/sbvr-api/uri-parser.d.ts +23 -17
- package/out/sbvr-api/uri-parser.js +33 -27
- package/out/sbvr-api/uri-parser.js.map +1 -1
- package/out/sbvr-api/user.sbvr +2 -0
- package/out/server-glue/module.d.ts +6 -6
- package/out/server-glue/module.js +4 -2
- package/out/server-glue/module.js.map +1 -1
- package/out/server-glue/server.js +5 -5
- package/out/server-glue/server.js.map +1 -1
- package/package.json +89 -73
- package/pinejs.png +0 -0
- package/repo.yml +9 -9
- package/src/bin/abstract-sql-compiler.ts +5 -7
- package/src/bin/odata-compiler.ts +11 -13
- package/src/bin/sbvr-compiler.ts +11 -17
- package/src/bin/utils.ts +3 -5
- package/src/config-loader/config-loader.ts +167 -53
- package/src/config-loader/env.ts +106 -6
- package/src/data-server/sbvr-server.js +44 -38
- package/src/database-layer/db.ts +205 -64
- package/src/express-emulator/express.js +10 -11
- package/src/http-transactions/transactions.js +29 -21
- package/src/migrator/async.ts +323 -0
- package/src/migrator/migrations.sbvr +43 -0
- package/src/migrator/sync.ts +152 -0
- package/src/migrator/utils.ts +458 -0
- package/src/odata-metadata/odata-metadata-generator.ts +12 -15
- package/src/passport-pinejs/passport-pinejs.ts +9 -7
- package/src/pinejs-session-store/pinejs-session-store.ts +15 -1
- package/src/sbvr-api/abstract-sql.ts +17 -14
- package/src/sbvr-api/common-types.ts +2 -1
- package/src/sbvr-api/control-flow.ts +45 -11
- package/src/sbvr-api/errors.ts +82 -77
- package/src/sbvr-api/express-extension.ts +6 -1
- package/src/sbvr-api/hooks.ts +123 -50
- package/src/sbvr-api/odata-response.ts +23 -28
- package/src/sbvr-api/permissions.ts +548 -415
- package/src/sbvr-api/sbvr-utils.ts +581 -259
- package/src/sbvr-api/translations.ts +248 -0
- package/src/sbvr-api/uri-parser.ts +63 -49
- package/src/sbvr-api/user.sbvr +2 -0
- package/src/server-glue/module.ts +16 -10
- package/src/server-glue/server.ts +5 -5
- package/tsconfig.dev.json +1 -0
- package/tsconfig.json +1 -2
- package/typings/lf-to-abstract-sql.d.ts +6 -9
- package/typings/memoizee.d.ts +1 -1
- package/.github/CODEOWNERS +0 -1
- package/circle.yml +0 -37
- package/docs/todo.txt +0 -22
- package/out/migrator/migrator.d.ts +0 -20
- package/out/migrator/migrator.js +0 -188
- package/out/migrator/migrator.js.map +0 -1
- package/src/migrator/migrator.ts +0 -286
@@ -1,12 +1,17 @@
|
|
1
1
|
import type {
|
2
2
|
AbstractSqlModel,
|
3
|
-
|
3
|
+
AbstractSqlQuery,
|
4
4
|
AliasNode,
|
5
|
+
AnyTypeNodes,
|
6
|
+
Definition,
|
7
|
+
FieldNode,
|
8
|
+
ReferencedFieldNode,
|
5
9
|
Relationship,
|
6
10
|
RelationshipInternalNode,
|
7
11
|
RelationshipLeafNode,
|
8
12
|
RelationshipMapping,
|
9
13
|
SelectNode,
|
14
|
+
SelectQueryNode,
|
10
15
|
} from '@balena/abstract-sql-compiler';
|
11
16
|
import './express-extension';
|
12
17
|
import type * as Express from 'express';
|
@@ -15,12 +20,13 @@ import type {
|
|
15
20
|
ODataQuery,
|
16
21
|
SupportedMethod,
|
17
22
|
} from '@balena/odata-parser';
|
23
|
+
import type { Tx } from '../database-layer/db';
|
18
24
|
import type { ApiKey, User } from '../sbvr-api/sbvr-utils';
|
19
|
-
import type { AnyObject } from './common-types';
|
25
|
+
import type { AnyObject, Dictionary } from './common-types';
|
20
26
|
|
21
27
|
import {
|
22
|
-
|
23
|
-
OData2AbstractSQL,
|
28
|
+
isBindReference,
|
29
|
+
type OData2AbstractSQL,
|
24
30
|
odataNameToSqlName,
|
25
31
|
ResourceFunction,
|
26
32
|
sqlNameToODataName,
|
@@ -95,7 +101,7 @@ type MappedType<I, O> = O extends NestedCheck<infer T>
|
|
95
101
|
type MappedNestedCheck<
|
96
102
|
T extends NestedCheck<I>,
|
97
103
|
I,
|
98
|
-
O
|
104
|
+
O,
|
99
105
|
> = T extends NestedCheckOr<I>
|
100
106
|
? NestedCheckOr<MappedType<I, O>>
|
101
107
|
: T extends NestedCheckAnd<I>
|
@@ -117,7 +123,8 @@ const methodPermissions: {
|
|
117
123
|
DELETE: 'delete',
|
118
124
|
};
|
119
125
|
|
120
|
-
const $parsePermissions =
|
126
|
+
const $parsePermissions = env.createCache(
|
127
|
+
'parsePermissions',
|
121
128
|
(filter: string) => {
|
122
129
|
const { tree, binds } = ODataParser.parse(filter, {
|
123
130
|
startRule: 'ProcessRule',
|
@@ -130,11 +137,10 @@ const $parsePermissions = memoize(
|
|
130
137
|
},
|
131
138
|
{
|
132
139
|
primitive: true,
|
133
|
-
max: env.cache.parsePermissions.max,
|
134
140
|
},
|
135
141
|
);
|
136
142
|
|
137
|
-
const
|
143
|
+
const rewriteODataBinds = (
|
138
144
|
{ tree, extraBinds }: { tree: ODataQuery; extraBinds: ODataBinds },
|
139
145
|
odataBinds: ODataBinds,
|
140
146
|
): ODataQuery => {
|
@@ -157,7 +163,7 @@ const parsePermissions = (
|
|
157
163
|
odataBinds: ODataBinds,
|
158
164
|
): ODataQuery => {
|
159
165
|
const odata = $parsePermissions(filter);
|
160
|
-
return
|
166
|
+
return rewriteODataBinds(odata, odataBinds);
|
161
167
|
};
|
162
168
|
|
163
169
|
// Traverses all values in `check`, actions for the following data types:
|
@@ -166,22 +172,22 @@ const parsePermissions = (
|
|
166
172
|
// array: Treated as an AND of all elements
|
167
173
|
// object: Must have only one key of either `AND` or `OR`, with an array value that will be treated according to the key.
|
168
174
|
const isAnd = <T>(x: any): x is NestedCheckAnd<T> =>
|
169
|
-
|
175
|
+
typeof x === 'object' && 'and' in x;
|
170
176
|
const isOr = <T>(x: any): x is NestedCheckOr<T> =>
|
171
177
|
typeof x === 'object' && 'or' in x;
|
172
|
-
export function nestedCheck<I, O>(
|
178
|
+
export function nestedCheck<I extends {}, O>(
|
173
179
|
check: string,
|
174
180
|
stringCallback: (s: string) => O,
|
175
181
|
): O;
|
176
|
-
export function nestedCheck<I, O>(
|
182
|
+
export function nestedCheck<I extends {}, O>(
|
177
183
|
check: boolean,
|
178
184
|
stringCallback: (s: string) => O,
|
179
185
|
): boolean;
|
180
|
-
export function nestedCheck<I, O>(
|
186
|
+
export function nestedCheck<I extends {}, O>(
|
181
187
|
check: NestedCheck<I>,
|
182
188
|
stringCallback: (s: string) => O,
|
183
189
|
): Exclude<I, string> | O | MappedNestedCheck<typeof check, I, O>;
|
184
|
-
export function nestedCheck<I, O>(
|
190
|
+
export function nestedCheck<I extends {}, O>(
|
185
191
|
check: NestedCheck<I>,
|
186
192
|
stringCallback: (s: string) => O,
|
187
193
|
): boolean | Exclude<I, string> | O | MappedNestedCheck<typeof check, I, O> {
|
@@ -306,10 +312,14 @@ const namespaceRelationships = (
|
|
306
312
|
});
|
307
313
|
};
|
308
314
|
|
309
|
-
type PermissionLookup =
|
315
|
+
type PermissionLookup = Dictionary<true | string[]>;
|
310
316
|
|
311
|
-
const getPermissionsLookup =
|
312
|
-
|
317
|
+
const getPermissionsLookup = env.createCache(
|
318
|
+
'permissionsLookup',
|
319
|
+
(permissions: string[], guestPermissions?: string[]): PermissionLookup => {
|
320
|
+
if (guestPermissions != null) {
|
321
|
+
permissions = [...guestPermissions, ...permissions];
|
322
|
+
}
|
313
323
|
const permissionsLookup: PermissionLookup = {};
|
314
324
|
for (const permission of permissions) {
|
315
325
|
const [target, condition] = permission.split('?');
|
@@ -317,20 +327,28 @@ const getPermissionsLookup = memoize(
|
|
317
327
|
// We have unconditional permission
|
318
328
|
permissionsLookup[target] = true;
|
319
329
|
} else if (permissionsLookup[target] !== true) {
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
330
|
+
permissionsLookup[target] ??= [];
|
331
|
+
(
|
332
|
+
permissionsLookup[target] as Exclude<
|
333
|
+
PermissionLookup[typeof target],
|
334
|
+
true
|
335
|
+
>
|
336
|
+
).push(condition);
|
337
|
+
}
|
338
|
+
}
|
339
|
+
// Ensure there are no duplicate conditions as applying both would be wasteful
|
340
|
+
for (const target of Object.keys(permissionsLookup)) {
|
341
|
+
const conditions = permissionsLookup[target];
|
342
|
+
if (conditions !== true) {
|
343
|
+
permissionsLookup[target] = _.uniq(conditions);
|
327
344
|
}
|
328
345
|
}
|
329
346
|
return permissionsLookup;
|
330
347
|
},
|
331
348
|
{
|
332
|
-
|
333
|
-
|
349
|
+
normalizer: ([permissions, guestPermissions]) =>
|
350
|
+
// When guestPermissions is present it should always be the same, so we can key by presence not content
|
351
|
+
`${permissions}${guestPermissions == null}`,
|
334
352
|
},
|
335
353
|
);
|
336
354
|
|
@@ -343,52 +361,53 @@ const $checkPermissions = (
|
|
343
361
|
const checkObject: PermissionCheck = {
|
344
362
|
or: ['all', actionList],
|
345
363
|
};
|
346
|
-
return nestedCheck(
|
347
|
-
|
348
|
-
| string
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
return true;
|
355
|
-
}
|
356
|
-
if (vocabulary != null) {
|
357
|
-
const maybeVocabularyPermission =
|
358
|
-
permissionsLookup[vocabulary + '.' + permissionCheck];
|
359
|
-
if (maybeVocabularyPermission === true) {
|
364
|
+
return nestedCheck(
|
365
|
+
checkObject,
|
366
|
+
(permissionCheck): boolean | string | NestedCheckOr<string> => {
|
367
|
+
const resourcePermission =
|
368
|
+
permissionsLookup['resource.' + permissionCheck];
|
369
|
+
let vocabularyPermission: string[] | undefined;
|
370
|
+
let vocabularyResourcePermission: string[] | undefined;
|
371
|
+
if (resourcePermission === true) {
|
360
372
|
return true;
|
361
373
|
}
|
362
|
-
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
vocabulary + '.' + resourceName + '.' + permissionCheck
|
367
|
-
];
|
368
|
-
if (maybeVocabularyResourcePermission === true) {
|
374
|
+
if (vocabulary != null) {
|
375
|
+
const maybeVocabularyPermission =
|
376
|
+
permissionsLookup[vocabulary + '.' + permissionCheck];
|
377
|
+
if (maybeVocabularyPermission === true) {
|
369
378
|
return true;
|
370
379
|
}
|
371
|
-
|
380
|
+
vocabularyPermission = maybeVocabularyPermission;
|
381
|
+
if (resourceName != null) {
|
382
|
+
const maybeVocabularyResourcePermission =
|
383
|
+
permissionsLookup[
|
384
|
+
vocabulary + '.' + resourceName + '.' + permissionCheck
|
385
|
+
];
|
386
|
+
if (maybeVocabularyResourcePermission === true) {
|
387
|
+
return true;
|
388
|
+
}
|
389
|
+
vocabularyResourcePermission = maybeVocabularyResourcePermission;
|
390
|
+
}
|
372
391
|
}
|
373
|
-
}
|
374
392
|
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
393
|
+
// Get the unique permission set, ignoring undefined sets.
|
394
|
+
const conditionalPermissions = _.union(
|
395
|
+
resourcePermission,
|
396
|
+
vocabularyPermission,
|
397
|
+
vocabularyResourcePermission,
|
398
|
+
);
|
381
399
|
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
400
|
+
if (conditionalPermissions.length === 1) {
|
401
|
+
return conditionalPermissions[0];
|
402
|
+
}
|
403
|
+
if (conditionalPermissions.length > 1) {
|
404
|
+
return {
|
405
|
+
or: conditionalPermissions,
|
406
|
+
};
|
407
|
+
}
|
408
|
+
return false;
|
409
|
+
},
|
410
|
+
);
|
392
411
|
};
|
393
412
|
|
394
413
|
const convertToLambda = (filter: AnyObject, identifier: string) => {
|
@@ -401,9 +420,9 @@ const convertToLambda = (filter: AnyObject, identifier: string) => {
|
|
401
420
|
return;
|
402
421
|
}
|
403
422
|
if (Array.isArray(object)) {
|
404
|
-
|
423
|
+
for (const element of object) {
|
405
424
|
replaceObject(element);
|
406
|
-
}
|
425
|
+
}
|
407
426
|
}
|
408
427
|
|
409
428
|
if (object.hasOwnProperty('name')) {
|
@@ -426,7 +445,7 @@ const rewriteSubPermissionBindings = (filter: AnyObject, counter: number) => {
|
|
426
445
|
object.bind = counter + object.bind;
|
427
446
|
}
|
428
447
|
|
429
|
-
if (Array.isArray(object) ||
|
448
|
+
if (Array.isArray(object) || typeof object === 'object') {
|
430
449
|
_.forEach(object, (v) => {
|
431
450
|
rewrite(v);
|
432
451
|
});
|
@@ -459,7 +478,7 @@ const buildODataPermission = (
|
|
459
478
|
}
|
460
479
|
if (conditionalPerms === true) {
|
461
480
|
// If we have full access then no need to provide a constrained definition
|
462
|
-
return
|
481
|
+
return;
|
463
482
|
}
|
464
483
|
|
465
484
|
const permissionFilters = nestedCheck(conditionalPerms, (permissionCheck) => {
|
@@ -468,7 +487,7 @@ const buildODataPermission = (
|
|
468
487
|
return {
|
469
488
|
filter: parsePermissions(permissionCheck, odata.binds),
|
470
489
|
};
|
471
|
-
} catch (e) {
|
490
|
+
} catch (e: any) {
|
472
491
|
console.warn(
|
473
492
|
'Failed to parse conditional permissions: ',
|
474
493
|
permissionCheck,
|
@@ -477,9 +496,8 @@ const buildODataPermission = (
|
|
477
496
|
}
|
478
497
|
});
|
479
498
|
|
480
|
-
const collapsedPermissionFilters =
|
481
|
-
permissionFilters
|
482
|
-
);
|
499
|
+
const collapsedPermissionFilters =
|
500
|
+
collapsePermissionFilters(permissionFilters);
|
483
501
|
|
484
502
|
return collapsedPermissionFilters;
|
485
503
|
};
|
@@ -490,7 +508,7 @@ const generateConstrainedAbstractSql = (
|
|
490
508
|
actionList: PermissionCheck,
|
491
509
|
vocabulary: string,
|
492
510
|
resourceName: string,
|
493
|
-
) => {
|
511
|
+
): Definition | undefined => {
|
494
512
|
const abstractSQLModel = sbvrUtils.getAbstractSqlModel({
|
495
513
|
vocabulary,
|
496
514
|
});
|
@@ -504,6 +522,12 @@ const generateConstrainedAbstractSql = (
|
|
504
522
|
odata,
|
505
523
|
);
|
506
524
|
|
525
|
+
if (collapsePermissionFilters == null) {
|
526
|
+
// If we have full access then there's no need to provide a constrained
|
527
|
+
// definition, just use the table directly.
|
528
|
+
return;
|
529
|
+
}
|
530
|
+
|
507
531
|
_.set(odata, ['tree', 'options', '$filter'], collapsedPermissionFilters);
|
508
532
|
|
509
533
|
const lambdaAlias = randomstring.generate(20);
|
@@ -513,9 +537,19 @@ const generateConstrainedAbstractSql = (
|
|
513
537
|
// permissions circles.
|
514
538
|
const canAccessTrace: string[] = [resourceName];
|
515
539
|
|
516
|
-
const
|
540
|
+
const resolveBind = (maybeBind: any, extraBinds: ODataBinds) => {
|
541
|
+
if (isBindReference(maybeBind)) {
|
542
|
+
const { bind } = maybeBind;
|
543
|
+
if (typeof bind === 'string' || bind < odata.binds.length) {
|
544
|
+
return odata.binds[bind];
|
545
|
+
}
|
546
|
+
return extraBinds[bind - odata.binds.length];
|
547
|
+
}
|
548
|
+
return maybeBind;
|
549
|
+
};
|
550
|
+
const canAccessFunction: ResourceFunction = function (property) {
|
517
551
|
// remove method property so that we won't loop back here again at this point
|
518
|
-
|
552
|
+
const { method, ...resolvedProperty } = property;
|
519
553
|
|
520
554
|
if (!this.defaultResource) {
|
521
555
|
throw new Error(`No resource selected in AST.`);
|
@@ -523,21 +557,54 @@ const generateConstrainedAbstractSql = (
|
|
523
557
|
|
524
558
|
const targetResource = this.NavigateResources(
|
525
559
|
this.defaultResource,
|
526
|
-
|
560
|
+
resolvedProperty.name,
|
527
561
|
);
|
528
562
|
|
563
|
+
const lambdaId = `${lambdaAlias}+${inc}`;
|
564
|
+
inc = inc + 1;
|
565
|
+
|
529
566
|
const targetResourceName = sqlNameToODataName(targetResource.resource.name);
|
530
567
|
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
568
|
+
const traceIndex = canAccessTrace.findIndex(
|
569
|
+
(rName) => rName === targetResourceName,
|
570
|
+
);
|
571
|
+
if (traceIndex !== -1) {
|
572
|
+
if (canAccessTrace[canAccessTrace.length - 1] !== targetResourceName) {
|
573
|
+
throw new Error(
|
574
|
+
`Indirectly circular 'canAccess()' permissions are not supported, currently permissions for ${resourceName} form an indirect circle by the following path: ${canAccessTrace.join(
|
575
|
+
' -> ',
|
576
|
+
)} -> ${targetResourceName}`,
|
577
|
+
);
|
578
|
+
}
|
579
|
+
|
580
|
+
const { args } = method[1];
|
581
|
+
const depthArg = resolveBind(args[0], this.extraBindVars);
|
582
|
+
if (depthArg == null) {
|
583
|
+
// To enable directly circular dependencies a depth must be specified and it was not
|
584
|
+
throw new Error(
|
585
|
+
`You must specify a depth if you want to enable directly circular 'canAccess()' permissions, currently permissions for ${resourceName} form a direct circle by the following path: ${canAccessTrace.join(
|
586
|
+
' -> ',
|
587
|
+
)} -> ${targetResourceName}`,
|
588
|
+
);
|
589
|
+
}
|
590
|
+
const [type, depth] = depthArg;
|
591
|
+
if (type !== 'Real' || !Number.isInteger(depth) || depth < 1) {
|
592
|
+
throw new Error('The depth for `canAccess` must be an integer >= 1');
|
593
|
+
}
|
594
|
+
if (
|
595
|
+
// Make sure we have enough traces yet to have exceeded the depth before checking if they match
|
596
|
+
canAccessTrace.length > depth &&
|
597
|
+
// We know there cannot be any gaps due to the indirect circle check above so we only need to check
|
598
|
+
// that the final depth entry is the target resource to know they all must be
|
599
|
+
canAccessTrace[canAccessTrace.length - depth] === targetResourceName
|
600
|
+
) {
|
601
|
+
resolvedProperty.lambda = {
|
602
|
+
method: 'any',
|
603
|
+
identifier: lambdaId,
|
604
|
+
expression: ['eq', true, false],
|
605
|
+
};
|
606
|
+
return this.Property(resolvedProperty);
|
607
|
+
}
|
541
608
|
}
|
542
609
|
|
543
610
|
const parentOdata = memoizedParseOdata(`/${targetResourceName}`);
|
@@ -550,20 +617,17 @@ const generateConstrainedAbstractSql = (
|
|
550
617
|
parentOdata,
|
551
618
|
);
|
552
619
|
|
553
|
-
if (collapsedParentPermissionFilters
|
554
|
-
//
|
555
|
-
|
556
|
-
throw constrainedPermissionError;
|
620
|
+
if (collapsedParentPermissionFilters == null) {
|
621
|
+
// full access
|
622
|
+
return ['Equals', ['Boolean', true], ['Boolean', true]];
|
557
623
|
}
|
558
624
|
|
559
|
-
const lambdaId = `${lambdaAlias}+${inc}`;
|
560
|
-
inc = inc + 1;
|
561
625
|
rewriteSubPermissionBindings(
|
562
626
|
collapsedParentPermissionFilters,
|
563
627
|
this.bindVarsLength + this.extraBindVars.length,
|
564
628
|
);
|
565
629
|
convertToLambda(collapsedParentPermissionFilters, lambdaId);
|
566
|
-
|
630
|
+
resolvedProperty.lambda = {
|
567
631
|
method: 'any',
|
568
632
|
identifier: lambdaId,
|
569
633
|
expression: collapsedParentPermissionFilters,
|
@@ -573,7 +637,7 @@ const generateConstrainedAbstractSql = (
|
|
573
637
|
|
574
638
|
canAccessTrace.push(targetResourceName);
|
575
639
|
try {
|
576
|
-
return this.Property(
|
640
|
+
return this.Property(resolvedProperty);
|
577
641
|
} finally {
|
578
642
|
canAccessTrace.pop();
|
579
643
|
}
|
@@ -588,38 +652,41 @@ const generateConstrainedAbstractSql = (
|
|
588
652
|
odata.binds.push(...extraBindVars);
|
589
653
|
const odataBinds = odata.binds;
|
590
654
|
|
591
|
-
const abstractSqlQuery = [...tree];
|
655
|
+
const abstractSqlQuery = [...tree] as SelectQueryNode;
|
592
656
|
// Remove aliases from the top level select
|
593
|
-
const
|
594
|
-
|
595
|
-
|
596
|
-
]
|
597
|
-
|
598
|
-
|
599
|
-
|
600
|
-
|
601
|
-
|
602
|
-
|
603
|
-
|
604
|
-
|
605
|
-
|
606
|
-
|
607
|
-
|
608
|
-
|
609
|
-
|
610
|
-
}
|
611
|
-
if (selectField.length === 2 && Array.isArray(selectField[0])) {
|
612
|
-
return selectField[0];
|
657
|
+
const select = abstractSqlQuery.find(
|
658
|
+
(v): v is SelectNode => v[0] === 'Select',
|
659
|
+
)!;
|
660
|
+
select[1] = select[1].map((selectField): AnyTypeNodes => {
|
661
|
+
if (selectField[0] === 'Alias') {
|
662
|
+
const sqlName = odataNameToSqlName((selectField as AliasNode<any>)[2]);
|
663
|
+
const maybeField = (
|
664
|
+
selectField as AliasNode<
|
665
|
+
ReferencedFieldNode | FieldNode | AbstractSqlQuery
|
666
|
+
>
|
667
|
+
)[1];
|
668
|
+
if (
|
669
|
+
(maybeField[0] === 'ReferencedField' && maybeField[2] === sqlName) ||
|
670
|
+
(maybeField[0] === 'Field' && maybeField[1] === sqlName)
|
671
|
+
) {
|
672
|
+
// If the field name matches the sql name version of the alias then use it directly
|
673
|
+
return maybeField;
|
613
674
|
}
|
614
|
-
|
615
|
-
|
616
|
-
|
675
|
+
// Otherwise update the alias to use the sql name
|
676
|
+
return ['Alias', maybeField, sqlName];
|
677
|
+
}
|
678
|
+
return selectField;
|
679
|
+
});
|
617
680
|
|
618
|
-
return {
|
681
|
+
return { binds: odataBinds, abstractSql: abstractSqlQuery };
|
619
682
|
};
|
620
683
|
|
621
684
|
// Call the function once and either return the same result or throw the same error on subsequent calls
|
622
|
-
const onceGetter =
|
685
|
+
const onceGetter = <T, U extends keyof T>(
|
686
|
+
obj: T,
|
687
|
+
propName: U,
|
688
|
+
fn: () => T[U],
|
689
|
+
) => {
|
623
690
|
// We have `nullableFn` to keep fn required but still allow us to clear the fn reference
|
624
691
|
// after we have called fn
|
625
692
|
let nullableFn: undefined | typeof fn = fn;
|
@@ -637,7 +704,7 @@ const onceGetter = (obj: AnyObject, propName: string, fn: () => any) => {
|
|
637
704
|
// and the delete removes that restriction
|
638
705
|
delete this[propName];
|
639
706
|
return (this[propName] = result);
|
640
|
-
} catch (e) {
|
707
|
+
} catch (e: any) {
|
641
708
|
thrownErr = e;
|
642
709
|
throw thrownErr;
|
643
710
|
} finally {
|
@@ -650,7 +717,7 @@ const onceGetter = (obj: AnyObject, propName: string, fn: () => any) => {
|
|
650
717
|
const deepFreezeExceptDefinition = (obj: AnyObject) => {
|
651
718
|
Object.freeze(obj);
|
652
719
|
|
653
|
-
Object.getOwnPropertyNames(obj)
|
720
|
+
for (const prop of Object.getOwnPropertyNames(obj)) {
|
654
721
|
// We skip the definition because we know it's a property we've defined that will throw an error in some cases
|
655
722
|
if (
|
656
723
|
prop !== 'definition' &&
|
@@ -660,7 +727,7 @@ const deepFreezeExceptDefinition = (obj: AnyObject) => {
|
|
660
727
|
) {
|
661
728
|
deepFreezeExceptDefinition(obj);
|
662
729
|
}
|
663
|
-
}
|
730
|
+
}
|
664
731
|
};
|
665
732
|
|
666
733
|
const createBypassDefinition = (definition: Definition) =>
|
@@ -754,6 +821,12 @@ const rewriteRelationship = memoizeWeak(
|
|
754
821
|
odata,
|
755
822
|
);
|
756
823
|
|
824
|
+
if (collapsedPermissionFilters == null) {
|
825
|
+
// If we have full access already then there's no need to
|
826
|
+
// check for/rewrite based on `canAccess`
|
827
|
+
return;
|
828
|
+
}
|
829
|
+
|
757
830
|
_.set(
|
758
831
|
odata,
|
759
832
|
['tree', 'options', '$filter'],
|
@@ -805,7 +878,7 @@ const rewriteRelationship = memoizeWeak(
|
|
805
878
|
canAccess: canAccessFunction,
|
806
879
|
},
|
807
880
|
);
|
808
|
-
} catch (e) {
|
881
|
+
} catch (e: any) {
|
809
882
|
throw new ODataParser.SyntaxError(e);
|
810
883
|
}
|
811
884
|
if (foundCanAccessLink) {
|
@@ -833,7 +906,7 @@ const rewriteRelationship = memoizeWeak(
|
|
833
906
|
}
|
834
907
|
}
|
835
908
|
|
836
|
-
if (Array.isArray(object) ||
|
909
|
+
if (Array.isArray(object) || typeof object === 'object') {
|
837
910
|
_.forEach(object, (v) => {
|
838
911
|
// we want to recurse into the relationship path, but
|
839
912
|
// in case we hit a plain string, we don't need to bother
|
@@ -888,11 +961,16 @@ const getBoundConstrainedMemoizer = memoizeWeak(
|
|
888
961
|
(permissionsLookup: PermissionLookup, vocabulary: string) => {
|
889
962
|
const constrainedAbstractSqlModel = _.cloneDeep(abstractSqlModel);
|
890
963
|
|
891
|
-
const origSynonyms = Object.
|
964
|
+
const origSynonyms = Object.entries(
|
965
|
+
constrainedAbstractSqlModel.synonyms,
|
966
|
+
);
|
892
967
|
constrainedAbstractSqlModel.synonyms = new Proxy(
|
893
968
|
constrainedAbstractSqlModel.synonyms,
|
894
969
|
{
|
895
|
-
get
|
970
|
+
get(synonyms, permissionSynonym, receiver) {
|
971
|
+
if (typeof permissionSynonym === 'symbol') {
|
972
|
+
return Reflect.get(synonyms, permissionSynonym, receiver);
|
973
|
+
}
|
896
974
|
if (synonyms[permissionSynonym]) {
|
897
975
|
return synonyms[permissionSynonym];
|
898
976
|
}
|
@@ -900,9 +978,9 @@ const getBoundConstrainedMemoizer = memoizeWeak(
|
|
900
978
|
if (!alias) {
|
901
979
|
return;
|
902
980
|
}
|
903
|
-
|
981
|
+
for (const [synonym, canonicalForm] of origSynonyms) {
|
904
982
|
synonyms[`${synonym}$${alias}`] = `${canonicalForm}$${alias}`;
|
905
|
-
}
|
983
|
+
}
|
906
984
|
return synonyms[permissionSynonym];
|
907
985
|
},
|
908
986
|
},
|
@@ -917,14 +995,12 @@ const getBoundConstrainedMemoizer = memoizeWeak(
|
|
917
995
|
constrainedAbstractSqlModel.tables[bypassResourceName] = {
|
918
996
|
...table,
|
919
997
|
};
|
920
|
-
constrainedAbstractSqlModel.tables[
|
921
|
-
bypassResourceName
|
922
|
-
].resourceName = bypassResourceName;
|
998
|
+
constrainedAbstractSqlModel.tables[bypassResourceName].resourceName =
|
999
|
+
bypassResourceName;
|
923
1000
|
if (table.definition) {
|
924
1001
|
// If the table is definition based then just make the bypass version match but pointing to the equivalent bypassed resources
|
925
|
-
constrainedAbstractSqlModel.tables[
|
926
|
-
|
927
|
-
].definition = createBypassDefinition(table.definition);
|
1002
|
+
constrainedAbstractSqlModel.tables[bypassResourceName].definition =
|
1003
|
+
createBypassDefinition(table.definition);
|
928
1004
|
} else {
|
929
1005
|
// Otherwise constrain the non-bypass table
|
930
1006
|
onceGetter(
|
@@ -942,14 +1018,15 @@ const getBoundConstrainedMemoizer = memoizeWeak(
|
|
942
1018
|
constrainedAbstractSqlModel.tables = new Proxy(
|
943
1019
|
constrainedAbstractSqlModel.tables,
|
944
1020
|
{
|
945
|
-
get
|
1021
|
+
get(tables, permissionResourceName, receiver) {
|
1022
|
+
if (typeof permissionResourceName === 'symbol') {
|
1023
|
+
return Reflect.get(tables, permissionResourceName, receiver);
|
1024
|
+
}
|
946
1025
|
if (tables[permissionResourceName]) {
|
947
1026
|
return tables[permissionResourceName];
|
948
1027
|
}
|
949
|
-
const [
|
950
|
-
|
951
|
-
permissionsJSON,
|
952
|
-
] = permissionResourceName.split('$permissions');
|
1028
|
+
const [resourceName, permissionsJSON] =
|
1029
|
+
permissionResourceName.split('$permissions');
|
953
1030
|
if (!permissionsJSON) {
|
954
1031
|
return;
|
955
1032
|
}
|
@@ -968,9 +1045,12 @@ const getBoundConstrainedMemoizer = memoizeWeak(
|
|
968
1045
|
permissionsLookup,
|
969
1046
|
permissions,
|
970
1047
|
vocabulary,
|
971
|
-
sqlNameToODataName(
|
1048
|
+
sqlNameToODataName(
|
1049
|
+
permissionsTable.modifyName ?? permissionsTable.name,
|
1050
|
+
),
|
972
1051
|
),
|
973
1052
|
);
|
1053
|
+
|
974
1054
|
return permissionsTable;
|
975
1055
|
},
|
976
1056
|
},
|
@@ -989,7 +1069,14 @@ const getBoundConstrainedMemoizer = memoizeWeak(
|
|
989
1069
|
constrainedAbstractSqlModel.relationships = new Proxy(
|
990
1070
|
constrainedAbstractSqlModel.relationships,
|
991
1071
|
{
|
992
|
-
get
|
1072
|
+
get(relationships, permissionResourceName, receiver) {
|
1073
|
+
if (typeof permissionResourceName === 'symbol') {
|
1074
|
+
return Reflect.get(
|
1075
|
+
relationships,
|
1076
|
+
permissionResourceName,
|
1077
|
+
receiver,
|
1078
|
+
);
|
1079
|
+
}
|
993
1080
|
if (relationships[permissionResourceName]) {
|
994
1081
|
return relationships[permissionResourceName];
|
995
1082
|
}
|
@@ -1066,59 +1153,61 @@ export const checkPassword = async (
|
|
1066
1153
|
};
|
1067
1154
|
};
|
1068
1155
|
|
1069
|
-
const
|
1070
|
-
|
1071
|
-
|
1072
|
-
|
1073
|
-
|
1074
|
-
|
1075
|
-
|
1076
|
-
|
1077
|
-
|
1078
|
-
$
|
1079
|
-
|
1080
|
-
|
1081
|
-
$
|
1082
|
-
|
1083
|
-
|
1084
|
-
|
1085
|
-
|
1086
|
-
|
1087
|
-
|
1088
|
-
{
|
1089
|
-
uhp: {
|
1090
|
-
expiry_date: { $gt: { $now: null } },
|
1156
|
+
const $getUserPermissions = (() => {
|
1157
|
+
const getUserPermissionsQuery = _.once(() =>
|
1158
|
+
sbvrUtils.api.Auth.prepare<{ userId: number }>({
|
1159
|
+
resource: 'permission',
|
1160
|
+
passthrough: {
|
1161
|
+
req: rootRead,
|
1162
|
+
},
|
1163
|
+
options: {
|
1164
|
+
$select: 'name',
|
1165
|
+
$filter: {
|
1166
|
+
$or: {
|
1167
|
+
is_of__user: {
|
1168
|
+
$any: {
|
1169
|
+
$alias: 'uhp',
|
1170
|
+
$expr: {
|
1171
|
+
uhp: { user: { '@': 'userId' } },
|
1172
|
+
$or: [
|
1173
|
+
{
|
1174
|
+
uhp: { expiry_date: null },
|
1091
1175
|
},
|
1092
|
-
|
1093
|
-
|
1176
|
+
{
|
1177
|
+
uhp: {
|
1178
|
+
expiry_date: { $gt: { $now: null } },
|
1179
|
+
},
|
1180
|
+
},
|
1181
|
+
],
|
1182
|
+
},
|
1094
1183
|
},
|
1095
1184
|
},
|
1096
|
-
|
1097
|
-
|
1098
|
-
|
1099
|
-
|
1100
|
-
|
1101
|
-
|
1102
|
-
|
1103
|
-
|
1104
|
-
|
1105
|
-
|
1106
|
-
|
1107
|
-
|
1108
|
-
|
1109
|
-
|
1110
|
-
|
1111
|
-
|
1112
|
-
|
1113
|
-
|
1114
|
-
uhr: { expiry_date: null },
|
1115
|
-
},
|
1116
|
-
{
|
1117
|
-
uhr: {
|
1118
|
-
expiry_date: { $gt: { $now: null } },
|
1185
|
+
is_of__role: {
|
1186
|
+
$any: {
|
1187
|
+
$alias: 'rhp',
|
1188
|
+
$expr: {
|
1189
|
+
rhp: {
|
1190
|
+
role: {
|
1191
|
+
$any: {
|
1192
|
+
$alias: 'r',
|
1193
|
+
$expr: {
|
1194
|
+
r: {
|
1195
|
+
is_of__user: {
|
1196
|
+
$any: {
|
1197
|
+
$alias: 'uhr',
|
1198
|
+
$expr: {
|
1199
|
+
uhr: { user: { '@': 'userId' } },
|
1200
|
+
$or: [
|
1201
|
+
{
|
1202
|
+
uhr: { expiry_date: null },
|
1119
1203
|
},
|
1120
|
-
|
1121
|
-
|
1204
|
+
{
|
1205
|
+
uhr: {
|
1206
|
+
expiry_date: { $gt: { $now: null } },
|
1207
|
+
},
|
1208
|
+
},
|
1209
|
+
],
|
1210
|
+
},
|
1122
1211
|
},
|
1123
1212
|
},
|
1124
1213
|
},
|
@@ -1131,15 +1220,36 @@ const getUserPermissionsQuery = _.once(() =>
|
|
1131
1220
|
},
|
1132
1221
|
},
|
1133
1222
|
},
|
1223
|
+
// We orderby to increase the hit rate for the `_checkPermissions` memoisation
|
1224
|
+
$orderby: {
|
1225
|
+
name: 'asc',
|
1226
|
+
},
|
1134
1227
|
},
|
1135
|
-
|
1136
|
-
|
1137
|
-
|
1138
|
-
|
1228
|
+
}),
|
1229
|
+
);
|
1230
|
+
return env.createCache(
|
1231
|
+
'userPermissions',
|
1232
|
+
async (userId: number, tx?: Tx) => {
|
1233
|
+
const permissions = (await getUserPermissionsQuery()(
|
1234
|
+
{
|
1235
|
+
userId,
|
1236
|
+
},
|
1237
|
+
undefined,
|
1238
|
+
{ tx },
|
1239
|
+
)) as Array<{ name: string }>;
|
1240
|
+
return permissions.map((permission) => permission.name);
|
1139
1241
|
},
|
1140
|
-
|
1141
|
-
|
1142
|
-
|
1242
|
+
{
|
1243
|
+
primitive: true,
|
1244
|
+
promise: true,
|
1245
|
+
normalizer: ([userId]) => `${userId}`,
|
1246
|
+
},
|
1247
|
+
);
|
1248
|
+
})();
|
1249
|
+
export const getUserPermissions = async (
|
1250
|
+
userId: number,
|
1251
|
+
tx?: Tx,
|
1252
|
+
): Promise<string[]> => {
|
1143
1253
|
if (typeof userId === 'string') {
|
1144
1254
|
userId = parseInt(userId, 10);
|
1145
1255
|
}
|
@@ -1147,63 +1257,84 @@ export const getUserPermissions = async (userId: number): Promise<string[]> => {
|
|
1147
1257
|
throw new Error(`User ID has to be numeric, got: ${typeof userId}`);
|
1148
1258
|
}
|
1149
1259
|
try {
|
1150
|
-
|
1151
|
-
|
1152
|
-
})) as Array<{ name: string }>;
|
1153
|
-
return permissions.map((permission) => permission.name);
|
1154
|
-
} catch (err) {
|
1260
|
+
return await $getUserPermissions(userId, tx);
|
1261
|
+
} catch (err: any) {
|
1155
1262
|
sbvrUtils.api.Auth.logger.error('Error loading user permissions', err);
|
1156
1263
|
throw err;
|
1157
1264
|
}
|
1158
1265
|
};
|
1159
1266
|
|
1160
|
-
const
|
1161
|
-
|
1162
|
-
|
1163
|
-
|
1164
|
-
|
1165
|
-
|
1166
|
-
|
1167
|
-
|
1168
|
-
|
1169
|
-
$
|
1170
|
-
|
1171
|
-
|
1172
|
-
$
|
1173
|
-
|
1174
|
-
|
1175
|
-
|
1176
|
-
|
1177
|
-
$
|
1178
|
-
|
1179
|
-
|
1267
|
+
const $getApiKeyPermissions = (() => {
|
1268
|
+
const getApiKeyPermissionsQuery = _.once(() =>
|
1269
|
+
sbvrUtils.api.Auth.prepare<{ apiKey: string }>({
|
1270
|
+
resource: 'permission',
|
1271
|
+
passthrough: {
|
1272
|
+
req: rootRead,
|
1273
|
+
},
|
1274
|
+
options: {
|
1275
|
+
$select: 'name',
|
1276
|
+
$filter: {
|
1277
|
+
$or: {
|
1278
|
+
is_of__api_key: {
|
1279
|
+
$any: {
|
1280
|
+
$alias: 'khp',
|
1281
|
+
$expr: {
|
1282
|
+
khp: {
|
1283
|
+
api_key: {
|
1284
|
+
$any: {
|
1285
|
+
$alias: 'k',
|
1286
|
+
$expr: {
|
1287
|
+
k: { key: { '@': 'apiKey' } },
|
1288
|
+
$or: [
|
1289
|
+
{
|
1290
|
+
k: { expiry_date: null },
|
1291
|
+
},
|
1292
|
+
{
|
1293
|
+
k: {
|
1294
|
+
expiry_date: { $gt: { $now: null } },
|
1295
|
+
},
|
1296
|
+
},
|
1297
|
+
],
|
1298
|
+
},
|
1180
1299
|
},
|
1181
1300
|
},
|
1182
1301
|
},
|
1183
1302
|
},
|
1184
1303
|
},
|
1185
1304
|
},
|
1186
|
-
|
1187
|
-
|
1188
|
-
|
1189
|
-
|
1190
|
-
|
1191
|
-
|
1192
|
-
|
1193
|
-
|
1194
|
-
|
1195
|
-
|
1196
|
-
|
1197
|
-
|
1198
|
-
|
1199
|
-
|
1200
|
-
|
1201
|
-
|
1202
|
-
|
1203
|
-
|
1204
|
-
|
1205
|
-
|
1206
|
-
|
1305
|
+
is_of__role: {
|
1306
|
+
$any: {
|
1307
|
+
$alias: 'rhp',
|
1308
|
+
$expr: {
|
1309
|
+
rhp: {
|
1310
|
+
role: {
|
1311
|
+
$any: {
|
1312
|
+
$alias: 'r',
|
1313
|
+
$expr: {
|
1314
|
+
r: {
|
1315
|
+
is_of__api_key: {
|
1316
|
+
$any: {
|
1317
|
+
$alias: 'khr',
|
1318
|
+
$expr: {
|
1319
|
+
khr: {
|
1320
|
+
api_key: {
|
1321
|
+
$any: {
|
1322
|
+
$alias: 'k',
|
1323
|
+
$expr: {
|
1324
|
+
k: { key: { '@': 'apiKey' } },
|
1325
|
+
$or: [
|
1326
|
+
{
|
1327
|
+
k: { expiry_date: null },
|
1328
|
+
},
|
1329
|
+
{
|
1330
|
+
k: {
|
1331
|
+
expiry_date: {
|
1332
|
+
$gt: { $now: null },
|
1333
|
+
},
|
1334
|
+
},
|
1335
|
+
},
|
1336
|
+
],
|
1337
|
+
},
|
1207
1338
|
},
|
1208
1339
|
},
|
1209
1340
|
},
|
@@ -1220,94 +1351,116 @@ const getApiKeyPermissionsQuery = _.once(() =>
|
|
1220
1351
|
},
|
1221
1352
|
},
|
1222
1353
|
},
|
1354
|
+
// We orderby to increase the hit rate for the `_checkPermissions` memoisation
|
1355
|
+
$orderby: {
|
1356
|
+
name: 'asc',
|
1357
|
+
},
|
1223
1358
|
},
|
1224
|
-
|
1225
|
-
|
1226
|
-
|
1227
|
-
|
1359
|
+
}),
|
1360
|
+
);
|
1361
|
+
return env.createCache(
|
1362
|
+
'apiKeyPermissions',
|
1363
|
+
async (apiKey: string, tx?: Tx) => {
|
1364
|
+
const permissions = (await getApiKeyPermissionsQuery()(
|
1365
|
+
{
|
1366
|
+
apiKey,
|
1367
|
+
},
|
1368
|
+
undefined,
|
1369
|
+
{ tx },
|
1370
|
+
)) as Array<{ name: string }>;
|
1371
|
+
return permissions.map((permission) => permission.name);
|
1228
1372
|
},
|
1229
|
-
|
1230
|
-
|
1373
|
+
{
|
1374
|
+
primitive: true,
|
1375
|
+
promise: true,
|
1376
|
+
normalizer: ([apiKey]) => apiKey,
|
1377
|
+
},
|
1378
|
+
);
|
1379
|
+
})();
|
1231
1380
|
export const getApiKeyPermissions = async (
|
1232
1381
|
apiKey: string,
|
1382
|
+
tx?: Tx,
|
1233
1383
|
): Promise<string[]> => {
|
1234
1384
|
if (typeof apiKey !== 'string') {
|
1235
1385
|
throw new Error('API key has to be a string, got: ' + typeof apiKey);
|
1236
1386
|
}
|
1237
1387
|
try {
|
1238
|
-
|
1239
|
-
|
1240
|
-
})) as Array<{ name: string }>;
|
1241
|
-
return permissions.map((permission) => permission.name);
|
1242
|
-
} catch (err) {
|
1388
|
+
return await $getApiKeyPermissions(apiKey, tx);
|
1389
|
+
} catch (err: any) {
|
1243
1390
|
sbvrUtils.api.Auth.logger.error('Error loading api key permissions', err);
|
1244
1391
|
throw err;
|
1245
1392
|
}
|
1246
1393
|
};
|
1247
1394
|
|
1248
|
-
const
|
1249
|
-
|
1250
|
-
|
1251
|
-
|
1252
|
-
|
1253
|
-
|
1254
|
-
|
1255
|
-
|
1395
|
+
const getApiKeyActorId = (() => {
|
1396
|
+
const getApiKeyActorIdQuery = _.once(() =>
|
1397
|
+
sbvrUtils.api.Auth.prepare<{ apiKey: string }>({
|
1398
|
+
resource: 'api_key',
|
1399
|
+
passthrough: {
|
1400
|
+
req: rootRead,
|
1401
|
+
},
|
1402
|
+
id: {
|
1403
|
+
key: { '@': 'apiKey' },
|
1404
|
+
},
|
1405
|
+
options: {
|
1406
|
+
$select: 'is_of__actor',
|
1407
|
+
$filter: {
|
1408
|
+
$or: [
|
1409
|
+
{ expiry_date: null },
|
1410
|
+
{ expiry_date: { $gt: { $now: null } } },
|
1411
|
+
],
|
1412
|
+
},
|
1413
|
+
},
|
1414
|
+
}),
|
1415
|
+
);
|
1416
|
+
const apiActorPermissionError = new PermissionError();
|
1417
|
+
return env.createCache(
|
1418
|
+
'apiKeyActorId',
|
1419
|
+
async (apiKey: string, tx?: Tx) => {
|
1420
|
+
const apiKeyResult = await getApiKeyActorIdQuery()(
|
1421
|
+
{
|
1422
|
+
apiKey,
|
1423
|
+
},
|
1424
|
+
undefined,
|
1425
|
+
{ tx },
|
1426
|
+
);
|
1427
|
+
if (apiKeyResult == null) {
|
1428
|
+
// We reuse a constant permission error here as it will be cached, and
|
1429
|
+
// using a single error instance can drastically reduce the memory used
|
1430
|
+
throw apiActorPermissionError;
|
1431
|
+
}
|
1432
|
+
const apiKeyActorID = apiKeyResult.is_of__actor.__id;
|
1433
|
+
if (apiKeyActorID == null) {
|
1434
|
+
throw new Error('API key is not linked to a actor?!');
|
1435
|
+
}
|
1436
|
+
return apiKeyActorID as number;
|
1256
1437
|
},
|
1257
|
-
|
1258
|
-
|
1438
|
+
{
|
1439
|
+
promise: true,
|
1440
|
+
primitive: true,
|
1441
|
+
normalizer: ([apiKey]) => apiKey,
|
1259
1442
|
},
|
1260
|
-
|
1261
|
-
);
|
1262
|
-
const apiActorPermissionError = new PermissionError();
|
1263
|
-
const getApiKeyActorId = async (apiKey: string) => {
|
1264
|
-
const apiKeyResult = await getApiKeyActorIdQuery()({
|
1265
|
-
apiKey,
|
1266
|
-
});
|
1267
|
-
if (apiKeyResult == null) {
|
1268
|
-
// We reuse a constant permission error here as it will be cached, and
|
1269
|
-
// using a single error instance can drastically reduce the memory used
|
1270
|
-
throw apiActorPermissionError;
|
1271
|
-
}
|
1272
|
-
const apiKeyActorID = apiKeyResult.is_of__actor.__id;
|
1273
|
-
if (apiKeyActorID == null) {
|
1274
|
-
throw new Error('API key is not linked to a actor?!');
|
1275
|
-
}
|
1276
|
-
return apiKeyActorID as number;
|
1277
|
-
};
|
1443
|
+
);
|
1444
|
+
})();
|
1278
1445
|
|
1279
1446
|
const checkApiKey = async (
|
1280
|
-
req: PermissionReq,
|
1281
1447
|
apiKey: string,
|
1448
|
+
tx?: Tx,
|
1282
1449
|
): Promise<PermissionReq['apiKey']> => {
|
1283
|
-
|
1284
|
-
|
1285
|
-
|
1286
|
-
let permissions: string[];
|
1287
|
-
try {
|
1288
|
-
permissions = await getApiKeyPermissions(apiKey);
|
1289
|
-
} catch (err) {
|
1290
|
-
console.warn('Error with API key:', err);
|
1291
|
-
// Ignore errors getting the api key and just use an empty permissions object.
|
1292
|
-
permissions = [];
|
1293
|
-
}
|
1294
|
-
let actor;
|
1295
|
-
if (permissions.length > 0) {
|
1296
|
-
actor = await getApiKeyActorId(apiKey);
|
1297
|
-
}
|
1298
|
-
const resolvedApiKey: PermissionReq['apiKey'] = {
|
1450
|
+
const permissions = await getApiKeyPermissions(apiKey, tx);
|
1451
|
+
const actor = await getApiKeyActorId(apiKey, tx);
|
1452
|
+
return {
|
1299
1453
|
key: apiKey,
|
1300
1454
|
permissions,
|
1455
|
+
actor,
|
1301
1456
|
};
|
1302
|
-
if (actor != null) {
|
1303
|
-
resolvedApiKey.actor = actor;
|
1304
|
-
}
|
1305
|
-
return resolvedApiKey;
|
1306
1457
|
};
|
1307
1458
|
|
1308
1459
|
export const resolveAuthHeader = async (
|
1309
1460
|
req: Express.Request,
|
1310
1461
|
expectedScheme = 'Bearer',
|
1462
|
+
// TODO: Consider making tx the second argument in the next major
|
1463
|
+
tx?: Tx,
|
1311
1464
|
): Promise<PermissionReq['apiKey']> => {
|
1312
1465
|
const auth = req.header('Authorization');
|
1313
1466
|
if (!auth) {
|
@@ -1324,7 +1477,7 @@ export const resolveAuthHeader = async (
|
|
1324
1477
|
return;
|
1325
1478
|
}
|
1326
1479
|
|
1327
|
-
return await checkApiKey(
|
1480
|
+
return await checkApiKey(apiKey, tx);
|
1328
1481
|
};
|
1329
1482
|
|
1330
1483
|
export const customAuthorizationMiddleware = (expectedScheme = 'Bearer') => {
|
@@ -1351,20 +1504,18 @@ export const authorizationMiddleware = customAuthorizationMiddleware();
|
|
1351
1504
|
export const resolveApiKey = async (
|
1352
1505
|
req: HookReq | Express.Request,
|
1353
1506
|
paramName = 'apikey',
|
1507
|
+
// TODO: Consider making tx the second argument in the next major
|
1508
|
+
tx?: Tx,
|
1354
1509
|
): Promise<PermissionReq['apiKey']> => {
|
1355
1510
|
const apiKey =
|
1356
|
-
req.params[paramName]
|
1357
|
-
|
1358
|
-
|
1359
|
-
|
1360
|
-
|
1361
|
-
return await checkApiKey(req, apiKey);
|
1511
|
+
req.params[paramName] ?? req.body[paramName] ?? req.query[paramName];
|
1512
|
+
if (apiKey == null) {
|
1513
|
+
return;
|
1514
|
+
}
|
1515
|
+
return await checkApiKey(apiKey, tx);
|
1362
1516
|
};
|
1363
1517
|
|
1364
1518
|
export const customApiKeyMiddleware = (paramName = 'apikey') => {
|
1365
|
-
if (paramName == null) {
|
1366
|
-
paramName = 'apikey';
|
1367
|
-
}
|
1368
1519
|
return async (
|
1369
1520
|
req: HookReq | Express.Request,
|
1370
1521
|
_res?: Express.Response,
|
@@ -1399,33 +1550,30 @@ export const checkPermissions = async (
|
|
1399
1550
|
);
|
1400
1551
|
};
|
1401
1552
|
|
1402
|
-
export const checkPermissionsMiddleware =
|
1403
|
-
action: PermissionCheck
|
1404
|
-
|
1405
|
-
|
1406
|
-
|
1407
|
-
|
1408
|
-
|
1409
|
-
|
1410
|
-
|
1411
|
-
|
1412
|
-
|
1413
|
-
|
1414
|
-
|
1415
|
-
|
1416
|
-
|
1417
|
-
|
1553
|
+
export const checkPermissionsMiddleware =
|
1554
|
+
(action: PermissionCheck): Express.RequestHandler =>
|
1555
|
+
async (req, res, next) => {
|
1556
|
+
try {
|
1557
|
+
const allowed = await checkPermissions(req, action);
|
1558
|
+
switch (allowed) {
|
1559
|
+
case false:
|
1560
|
+
res.status(401).end();
|
1561
|
+
return;
|
1562
|
+
case true:
|
1563
|
+
next();
|
1564
|
+
return;
|
1565
|
+
default:
|
1566
|
+
throw new Error(
|
1567
|
+
'checkPermissionsMiddleware returned a conditional permission',
|
1568
|
+
);
|
1569
|
+
}
|
1570
|
+
} catch (err: any) {
|
1571
|
+
sbvrUtils.api.Auth.logger.error('Error checking permissions', err);
|
1572
|
+
res.status(503).end();
|
1418
1573
|
}
|
1419
|
-
}
|
1420
|
-
sbvrUtils.api.Auth.logger.error(
|
1421
|
-
'Error checking permissions',
|
1422
|
-
err,
|
1423
|
-
err.stack,
|
1424
|
-
);
|
1425
|
-
res.sendStatus(503);
|
1426
|
-
}
|
1427
|
-
};
|
1574
|
+
};
|
1428
1575
|
|
1576
|
+
let guestPermissionsInitialized = false;
|
1429
1577
|
const getGuestPermissions = memoize(
|
1430
1578
|
async () => {
|
1431
1579
|
// Get guest user
|
@@ -1444,102 +1592,75 @@ const getGuestPermissions = memoize(
|
|
1444
1592
|
if (result == null) {
|
1445
1593
|
throw new Error('No guest user');
|
1446
1594
|
}
|
1447
|
-
|
1595
|
+
const guestPermissions = _.uniq(await getUserPermissions(result.id));
|
1596
|
+
|
1597
|
+
if (guestPermissions.some((p) => DEFAULT_ACTOR_BIND_REGEX.test(p))) {
|
1598
|
+
throw new Error('Guest permissions cannot reference actors');
|
1599
|
+
}
|
1600
|
+
guestPermissionsInitialized = true;
|
1601
|
+
return guestPermissions;
|
1448
1602
|
},
|
1449
1603
|
{ promise: true },
|
1450
1604
|
);
|
1451
1605
|
|
1452
1606
|
const getReqPermissions = async (
|
1453
1607
|
req: PermissionReq,
|
1454
|
-
odataBinds: ODataBinds = [],
|
1608
|
+
odataBinds: ODataBinds = [] as any as ODataBinds,
|
1455
1609
|
) => {
|
1456
|
-
const
|
1457
|
-
|
1458
|
-
|
1459
|
-
|
1460
|
-
|
1461
|
-
|
1462
|
-
|
1463
|
-
|
1464
|
-
|
1465
|
-
) {
|
1466
|
-
const actorId = await getApiKeyActorId(req.apiKey.key);
|
1467
|
-
req.apiKey!.actor = actorId;
|
1468
|
-
}
|
1469
|
-
})(),
|
1470
|
-
]);
|
1471
|
-
|
1472
|
-
if (guestPermissions.some((p) => DEFAULT_ACTOR_BIND_REGEX.test(p))) {
|
1473
|
-
throw new Error('Guest permissions cannot reference actors');
|
1474
|
-
}
|
1475
|
-
|
1476
|
-
let permissions = guestPermissions;
|
1477
|
-
|
1478
|
-
let actorIndex = 0;
|
1479
|
-
const addActorPermissions = (actorId: number, actorPermissions: string[]) => {
|
1480
|
-
let actorBind = DEFAULT_ACTOR_BIND;
|
1481
|
-
if (actorIndex > 0) {
|
1482
|
-
actorBind += actorIndex;
|
1483
|
-
actorPermissions = actorPermissions.map((actorPermission) =>
|
1484
|
-
actorPermission.replace(DEFAULT_ACTOR_BIND_REGEX, actorBind),
|
1485
|
-
);
|
1610
|
+
const guestPermissions = await (async () => {
|
1611
|
+
if (
|
1612
|
+
guestPermissionsInitialized === false &&
|
1613
|
+
(req.user === root.user || req.user === rootRead.user)
|
1614
|
+
) {
|
1615
|
+
// In the case that guest permissions are not initialized yet and the query is being made with root permissions
|
1616
|
+
// then we need to bypass `getGuestPermissions` as it will cause an infinite loop back to here.
|
1617
|
+
// Therefore to break that loop we just ignore guest permissions.
|
1618
|
+
return [];
|
1486
1619
|
}
|
1487
|
-
|
1488
|
-
|
1489
|
-
|
1620
|
+
return await getGuestPermissions();
|
1621
|
+
})();
|
1622
|
+
|
1623
|
+
let actorPermissions: string[] = [];
|
1624
|
+
const addActorPermissions = (actorId: number, perms: string[]) => {
|
1625
|
+
odataBinds[DEFAULT_ACTOR_BIND] = ['Real', actorId];
|
1626
|
+
actorPermissions = perms;
|
1490
1627
|
};
|
1491
1628
|
|
1492
|
-
if (req.user
|
1629
|
+
if (req.user?.permissions != null) {
|
1493
1630
|
addActorPermissions(req.user.actor, req.user.permissions);
|
1494
|
-
} else if (req.apiKey
|
1631
|
+
} else if (req.apiKey?.permissions != null) {
|
1495
1632
|
addActorPermissions(req.apiKey.actor!, req.apiKey.permissions);
|
1496
1633
|
}
|
1497
1634
|
|
1498
|
-
|
1499
|
-
|
1500
|
-
return getPermissionsLookup(permissions);
|
1635
|
+
return getPermissionsLookup(actorPermissions, guestPermissions);
|
1501
1636
|
};
|
1502
1637
|
|
1503
1638
|
export const addPermissions = async (
|
1504
1639
|
req: PermissionReq,
|
1505
1640
|
request: ODataRequest & { permissionType?: PermissionCheck },
|
1506
1641
|
): Promise<void> => {
|
1507
|
-
const {
|
1508
|
-
|
1642
|
+
const { resourceName, odataQuery, odataBinds } = request;
|
1643
|
+
const vocabulary = _.last(request.translateVersions)!;
|
1509
1644
|
let abstractSqlModel = sbvrUtils.getAbstractSqlModel(request);
|
1510
|
-
|
1511
|
-
|
1512
|
-
|
1513
|
-
|
1514
|
-
|
1515
|
-
|
1516
|
-
|
1517
|
-
|
1518
|
-
permissionType = 'model';
|
1519
|
-
} else {
|
1520
|
-
const methodPermission = methodPermissions[method];
|
1521
|
-
if (methodPermission != null) {
|
1522
|
-
permissionType = methodPermission;
|
1645
|
+
|
1646
|
+
let { permissionType } = request;
|
1647
|
+
if (permissionType == null) {
|
1648
|
+
const method = request.method.toUpperCase() as SupportedMethod;
|
1649
|
+
const isMetadataEndpoint =
|
1650
|
+
method === 'OPTIONS' || metadataEndpoints.includes(resourceName);
|
1651
|
+
if (isMetadataEndpoint) {
|
1652
|
+
permissionType = 'model';
|
1523
1653
|
} else {
|
1524
|
-
|
1525
|
-
|
1654
|
+
const methodPermission = methodPermissions[method];
|
1655
|
+
if (methodPermission != null) {
|
1656
|
+
permissionType = methodPermission;
|
1657
|
+
} else {
|
1658
|
+
console.warn('Unknown method for permissions type check: ', method);
|
1659
|
+
permissionType = 'all';
|
1660
|
+
}
|
1526
1661
|
}
|
1527
1662
|
}
|
1528
1663
|
|
1529
|
-
// This bypasses in the root cases, needed for fetching guest permissions to work, it can almost certainly be done better though
|
1530
|
-
let permissions = req.user?.permissions ?? [];
|
1531
|
-
permissions = permissions.concat(req.apiKey?.permissions ?? []);
|
1532
|
-
if (
|
1533
|
-
permissions.length > 0 &&
|
1534
|
-
$checkPermissions(
|
1535
|
-
getPermissionsLookup(permissions),
|
1536
|
-
permissionType,
|
1537
|
-
vocabulary,
|
1538
|
-
) === true
|
1539
|
-
) {
|
1540
|
-
// We have unconditional permission to access the vocab so there's no need to intercept anything
|
1541
|
-
return;
|
1542
|
-
}
|
1543
1664
|
const permissionsLookup = await getReqPermissions(req, odataBinds);
|
1544
1665
|
// Update the request's abstract sql model to use the constrained version
|
1545
1666
|
request.abstractSqlModel = abstractSqlModel = memoizedGetConstrainedModel(
|
@@ -1591,6 +1712,10 @@ export const config = {
|
|
1591
1712
|
ALTER TABLE "role-has-permission"
|
1592
1713
|
ADD COLUMN IF NOT EXISTS "modified at" TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL;
|
1593
1714
|
`,
|
1715
|
+
'14.42.0-api-key-expiry-date': `
|
1716
|
+
ALTER TABLE "api key"
|
1717
|
+
ADD COLUMN IF NOT EXISTS "expiry date" TIMESTAMP NULL;
|
1718
|
+
`,
|
1594
1719
|
},
|
1595
1720
|
},
|
1596
1721
|
] as sbvrUtils.ExecutableModel[],
|
@@ -1613,8 +1738,7 @@ export const setup = () => {
|
|
1613
1738
|
}
|
1614
1739
|
if (
|
1615
1740
|
request.method === 'POST' &&
|
1616
|
-
request.odataQuery.property
|
1617
|
-
request.odataQuery.property.resource === 'canAccess'
|
1741
|
+
request.odataQuery.property?.resource === 'canAccess'
|
1618
1742
|
) {
|
1619
1743
|
if (request.odataQuery.key == null) {
|
1620
1744
|
throw new BadRequestError();
|
@@ -1639,6 +1763,10 @@ export const setup = () => {
|
|
1639
1763
|
0,
|
1640
1764
|
-'#canAccess'.length,
|
1641
1765
|
);
|
1766
|
+
request.originalResourceName = request.originalResourceName.slice(
|
1767
|
+
0,
|
1768
|
+
-'#canAccess'.length,
|
1769
|
+
);
|
1642
1770
|
const resourceName = sbvrUtils.resolveSynonym(request);
|
1643
1771
|
const resourceTable = abstractSqlModel.tables[resourceName];
|
1644
1772
|
if (resourceTable == null) {
|
@@ -1656,8 +1784,13 @@ export const setup = () => {
|
|
1656
1784
|
}
|
1657
1785
|
await addPermissions(req, request);
|
1658
1786
|
},
|
1659
|
-
PRERESPOND: ({ request,
|
1660
|
-
if (
|
1787
|
+
PRERESPOND: ({ request, response }) => {
|
1788
|
+
if (
|
1789
|
+
request.custom.isAction === 'canAccess' &&
|
1790
|
+
(response.body == null ||
|
1791
|
+
typeof response.body === 'string' ||
|
1792
|
+
_.isEmpty(response.body?.d))
|
1793
|
+
) {
|
1661
1794
|
// If the caller does not have any permissions to access the
|
1662
1795
|
// resource pine will throw a PermissionError. To have the
|
1663
1796
|
// same behavior for the case that the user has permissions
|