@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.
Files changed (145) hide show
  1. package/.dockerignore +4 -0
  2. package/.github/workflows/flowzone.yml +21 -0
  3. package/.husky/pre-commit +4 -0
  4. package/.pinejs-cache.json +1 -0
  5. package/.resinci.yml +1 -0
  6. package/.versionbot/CHANGELOG.yml +9678 -2002
  7. package/CHANGELOG.md +2976 -2
  8. package/Dockerfile +14 -0
  9. package/Gruntfile.ts +3 -6
  10. package/README.md +10 -1
  11. package/VERSION +1 -0
  12. package/build/browser.ts +1 -1
  13. package/build/config.ts +0 -1
  14. package/docker-compose.npm-test.yml +11 -0
  15. package/docs/AdvancedUsage.md +77 -63
  16. package/docs/GettingStarted.md +90 -41
  17. package/docs/Migrations.md +102 -1
  18. package/docs/ProjectConfig.md +12 -21
  19. package/docs/Testing.md +7 -0
  20. package/out/bin/abstract-sql-compiler.js +17 -17
  21. package/out/bin/abstract-sql-compiler.js.map +1 -1
  22. package/out/bin/odata-compiler.js +23 -20
  23. package/out/bin/odata-compiler.js.map +1 -1
  24. package/out/bin/sbvr-compiler.js +22 -22
  25. package/out/bin/sbvr-compiler.js.map +1 -1
  26. package/out/bin/utils.d.ts +2 -2
  27. package/out/bin/utils.js +3 -3
  28. package/out/bin/utils.js.map +1 -1
  29. package/out/config-loader/config-loader.d.ts +9 -8
  30. package/out/config-loader/config-loader.js +135 -78
  31. package/out/config-loader/config-loader.js.map +1 -1
  32. package/out/config-loader/env.d.ts +41 -16
  33. package/out/config-loader/env.js +46 -2
  34. package/out/config-loader/env.js.map +1 -1
  35. package/out/data-server/sbvr-server.d.ts +2 -19
  36. package/out/data-server/sbvr-server.js +44 -38
  37. package/out/data-server/sbvr-server.js.map +1 -1
  38. package/out/database-layer/db.d.ts +32 -14
  39. package/out/database-layer/db.js +120 -41
  40. package/out/database-layer/db.js.map +1 -1
  41. package/out/express-emulator/express.js +10 -11
  42. package/out/express-emulator/express.js.map +1 -1
  43. package/out/http-transactions/transactions.d.ts +2 -18
  44. package/out/http-transactions/transactions.js +29 -21
  45. package/out/http-transactions/transactions.js.map +1 -1
  46. package/out/migrator/async.d.ts +7 -0
  47. package/out/migrator/async.js +168 -0
  48. package/out/migrator/async.js.map +1 -0
  49. package/out/migrator/migrations.sbvr +43 -0
  50. package/out/migrator/sync.d.ts +9 -0
  51. package/out/migrator/sync.js +106 -0
  52. package/out/migrator/sync.js.map +1 -0
  53. package/out/migrator/utils.d.ts +78 -0
  54. package/out/migrator/utils.js +283 -0
  55. package/out/migrator/utils.js.map +1 -0
  56. package/out/odata-metadata/odata-metadata-generator.js +10 -13
  57. package/out/odata-metadata/odata-metadata-generator.js.map +1 -1
  58. package/out/passport-pinejs/passport-pinejs.d.ts +1 -1
  59. package/out/passport-pinejs/passport-pinejs.js +8 -7
  60. package/out/passport-pinejs/passport-pinejs.js.map +1 -1
  61. package/out/pinejs-session-store/pinejs-session-store.d.ts +1 -1
  62. package/out/pinejs-session-store/pinejs-session-store.js +20 -6
  63. package/out/pinejs-session-store/pinejs-session-store.js.map +1 -1
  64. package/out/sbvr-api/abstract-sql.d.ts +3 -2
  65. package/out/sbvr-api/abstract-sql.js +9 -9
  66. package/out/sbvr-api/abstract-sql.js.map +1 -1
  67. package/out/sbvr-api/cached-compile.js +1 -1
  68. package/out/sbvr-api/cached-compile.js.map +1 -1
  69. package/out/sbvr-api/common-types.d.ts +6 -5
  70. package/out/sbvr-api/control-flow.d.ts +8 -1
  71. package/out/sbvr-api/control-flow.js +36 -9
  72. package/out/sbvr-api/control-flow.js.map +1 -1
  73. package/out/sbvr-api/errors.d.ts +47 -40
  74. package/out/sbvr-api/errors.js +78 -77
  75. package/out/sbvr-api/errors.js.map +1 -1
  76. package/out/sbvr-api/express-extension.d.ts +4 -0
  77. package/out/sbvr-api/hooks.d.ts +16 -15
  78. package/out/sbvr-api/hooks.js +74 -48
  79. package/out/sbvr-api/hooks.js.map +1 -1
  80. package/out/sbvr-api/odata-response.d.ts +2 -2
  81. package/out/sbvr-api/odata-response.js +28 -30
  82. package/out/sbvr-api/odata-response.js.map +1 -1
  83. package/out/sbvr-api/permissions.d.ts +17 -16
  84. package/out/sbvr-api/permissions.js +369 -304
  85. package/out/sbvr-api/permissions.js.map +1 -1
  86. package/out/sbvr-api/sbvr-utils.d.ts +33 -15
  87. package/out/sbvr-api/sbvr-utils.js +397 -235
  88. package/out/sbvr-api/sbvr-utils.js.map +1 -1
  89. package/out/sbvr-api/translations.d.ts +6 -0
  90. package/out/sbvr-api/translations.js +150 -0
  91. package/out/sbvr-api/translations.js.map +1 -0
  92. package/out/sbvr-api/uri-parser.d.ts +23 -17
  93. package/out/sbvr-api/uri-parser.js +33 -27
  94. package/out/sbvr-api/uri-parser.js.map +1 -1
  95. package/out/sbvr-api/user.sbvr +2 -0
  96. package/out/server-glue/module.d.ts +6 -6
  97. package/out/server-glue/module.js +4 -2
  98. package/out/server-glue/module.js.map +1 -1
  99. package/out/server-glue/server.js +5 -5
  100. package/out/server-glue/server.js.map +1 -1
  101. package/package.json +89 -73
  102. package/pinejs.png +0 -0
  103. package/repo.yml +9 -9
  104. package/src/bin/abstract-sql-compiler.ts +5 -7
  105. package/src/bin/odata-compiler.ts +11 -13
  106. package/src/bin/sbvr-compiler.ts +11 -17
  107. package/src/bin/utils.ts +3 -5
  108. package/src/config-loader/config-loader.ts +167 -53
  109. package/src/config-loader/env.ts +106 -6
  110. package/src/data-server/sbvr-server.js +44 -38
  111. package/src/database-layer/db.ts +205 -64
  112. package/src/express-emulator/express.js +10 -11
  113. package/src/http-transactions/transactions.js +29 -21
  114. package/src/migrator/async.ts +323 -0
  115. package/src/migrator/migrations.sbvr +43 -0
  116. package/src/migrator/sync.ts +152 -0
  117. package/src/migrator/utils.ts +458 -0
  118. package/src/odata-metadata/odata-metadata-generator.ts +12 -15
  119. package/src/passport-pinejs/passport-pinejs.ts +9 -7
  120. package/src/pinejs-session-store/pinejs-session-store.ts +15 -1
  121. package/src/sbvr-api/abstract-sql.ts +17 -14
  122. package/src/sbvr-api/common-types.ts +2 -1
  123. package/src/sbvr-api/control-flow.ts +45 -11
  124. package/src/sbvr-api/errors.ts +82 -77
  125. package/src/sbvr-api/express-extension.ts +6 -1
  126. package/src/sbvr-api/hooks.ts +123 -50
  127. package/src/sbvr-api/odata-response.ts +23 -28
  128. package/src/sbvr-api/permissions.ts +548 -415
  129. package/src/sbvr-api/sbvr-utils.ts +581 -259
  130. package/src/sbvr-api/translations.ts +248 -0
  131. package/src/sbvr-api/uri-parser.ts +63 -49
  132. package/src/sbvr-api/user.sbvr +2 -0
  133. package/src/server-glue/module.ts +16 -10
  134. package/src/server-glue/server.ts +5 -5
  135. package/tsconfig.dev.json +1 -0
  136. package/tsconfig.json +1 -2
  137. package/typings/lf-to-abstract-sql.d.ts +6 -9
  138. package/typings/memoizee.d.ts +1 -1
  139. package/.github/CODEOWNERS +0 -1
  140. package/circle.yml +0 -37
  141. package/docs/todo.txt +0 -22
  142. package/out/migrator/migrator.d.ts +0 -20
  143. package/out/migrator/migrator.js +0 -188
  144. package/out/migrator/migrator.js.map +0 -1
  145. package/src/migrator/migrator.ts +0 -286
@@ -1,12 +1,17 @@
1
1
  import type {
2
2
  AbstractSqlModel,
3
- AbstractSqlType,
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
- Definition,
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 = memoize(
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 rewriteBinds = (
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 rewriteBinds(odata, odataBinds);
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
- _.isObject(x) && 'and' in x;
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 = _.Dictionary<true | string[]>;
315
+ type PermissionLookup = Dictionary<true | string[]>;
310
316
 
311
- const getPermissionsLookup = memoize(
312
- (permissions: string[]): PermissionLookup => {
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
- if (permissionsLookup[target] == null) {
321
- permissionsLookup[target] = [];
322
- }
323
- (permissionsLookup[target] as Exclude<
324
- PermissionLookup[typeof target],
325
- true
326
- >).push(condition);
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
- primitive: true,
333
- max: env.cache.permissionsLookup.max,
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(checkObject, (permissionCheck):
347
- | boolean
348
- | string
349
- | NestedCheckOr<string> => {
350
- const resourcePermission = permissionsLookup['resource.' + permissionCheck];
351
- let vocabularyPermission: string[] | undefined;
352
- let vocabularyResourcePermission: string[] | undefined;
353
- if (resourcePermission === true) {
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
- vocabularyPermission = maybeVocabularyPermission;
363
- if (resourceName != null) {
364
- const maybeVocabularyResourcePermission =
365
- permissionsLookup[
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
- vocabularyResourcePermission = maybeVocabularyResourcePermission;
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
- // Get the unique permission set, ignoring undefined sets.
376
- const conditionalPermissions = _.union(
377
- resourcePermission,
378
- vocabularyPermission,
379
- vocabularyResourcePermission,
380
- );
393
+ // Get the unique permission set, ignoring undefined sets.
394
+ const conditionalPermissions = _.union(
395
+ resourcePermission,
396
+ vocabularyPermission,
397
+ vocabularyResourcePermission,
398
+ );
381
399
 
382
- if (conditionalPermissions.length === 1) {
383
- return conditionalPermissions[0];
384
- }
385
- if (conditionalPermissions.length > 1) {
386
- return {
387
- or: conditionalPermissions,
388
- };
389
- }
390
- return false;
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
- object.forEach((element) => {
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) || _.isObject(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 false;
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 = collapsePermissionFilters(
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 canAccessFunction: ResourceFunction = function (property: AnyObject) {
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
- delete property.method;
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
- property.name,
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
- if (canAccessTrace.includes(targetResourceName)) {
532
- // we don't want to allow permission loops for now, therefore we are
533
- // throwing the exception here. If we ever want to allow permission
534
- // loops return a false AST statement here (like true eq false), to
535
- // not recursivley follow query branches in a deep first search.
536
- throw new PermissionError(
537
- `Permissions for ${resourceName} form a circle by the following path: ${canAccessTrace.join(
538
- ' -> ',
539
- )} -> ${targetResourceName}`,
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 === false) {
554
- // We reuse a constant permission error here as it will be cached, and
555
- // using a single error instance can drastically reduce the memory used
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
- property.lambda = {
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(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 selectIndex = abstractSqlQuery.findIndex((v) => v[0] === 'Select');
594
- const select = (abstractSqlQuery[selectIndex] = [
595
- ...abstractSqlQuery[selectIndex],
596
- ] as SelectNode);
597
- select[1] = select[1].map(
598
- (selectField): AbstractSqlType => {
599
- if (selectField[0] === 'Alias') {
600
- const maybeField = (selectField as AliasNode<any>)[1];
601
- const fieldType = maybeField[0];
602
- if (fieldType === 'ReferencedField' || fieldType === 'Field') {
603
- return maybeField;
604
- }
605
- return [
606
- 'Alias',
607
- maybeField,
608
- odataNameToSqlName((selectField as AliasNode<any>)[2]),
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
- return selectField;
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 { extraBinds: odataBinds, abstractSqlQuery };
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 = (obj: AnyObject, propName: string, fn: () => any) => {
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).forEach((prop) => {
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) || _.isObject(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.keys(constrainedAbstractSqlModel.synonyms);
964
+ const origSynonyms = Object.entries(
965
+ constrainedAbstractSqlModel.synonyms,
966
+ );
892
967
  constrainedAbstractSqlModel.synonyms = new Proxy(
893
968
  constrainedAbstractSqlModel.synonyms,
894
969
  {
895
- get: (synonyms, permissionSynonym: string) => {
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
- origSynonyms.forEach((canonicalForm, synonym) => {
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
- bypassResourceName
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: (tables, permissionResourceName: string) => {
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
- resourceName,
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(permissionsTable.name),
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: (relationships, permissionResourceName: string) => {
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 getUserPermissionsQuery = _.once(() =>
1070
- sbvrUtils.api.Auth.prepare<{ userId: number }>({
1071
- resource: 'permission',
1072
- passthrough: {
1073
- req: rootRead,
1074
- },
1075
- options: {
1076
- $select: 'name',
1077
- $filter: {
1078
- $or: {
1079
- is_of__user: {
1080
- $any: {
1081
- $alias: 'uhp',
1082
- $expr: {
1083
- uhp: { user: { '@': 'userId' } },
1084
- $or: [
1085
- {
1086
- uhp: { expiry_date: null },
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
- is_of__role: {
1098
- $any: {
1099
- $alias: 'rhp',
1100
- $expr: {
1101
- rhp: {
1102
- role: {
1103
- $any: {
1104
- $alias: 'r',
1105
- $expr: {
1106
- r: {
1107
- is_of__user: {
1108
- $any: {
1109
- $alias: 'uhr',
1110
- $expr: {
1111
- uhr: { user: { '@': 'userId' } },
1112
- $or: [
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
- // We orderby to increase the hit rate for the `_checkPermissions` memoisation
1136
- $orderby: {
1137
- name: 'asc',
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
- export const getUserPermissions = async (userId: number): Promise<string[]> => {
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
- const permissions = (await getUserPermissionsQuery()({
1151
- userId,
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 getApiKeyPermissionsQuery = _.once(() =>
1161
- sbvrUtils.api.Auth.prepare<{ apiKey: string }>({
1162
- resource: 'permission',
1163
- passthrough: {
1164
- req: rootRead,
1165
- },
1166
- options: {
1167
- $select: 'name',
1168
- $filter: {
1169
- $or: {
1170
- is_of__api_key: {
1171
- $any: {
1172
- $alias: 'khp',
1173
- $expr: {
1174
- khp: {
1175
- api_key: {
1176
- $any: {
1177
- $alias: 'k',
1178
- $expr: {
1179
- k: { key: { '@': 'apiKey' } },
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
- is_of__role: {
1188
- $any: {
1189
- $alias: 'rhp',
1190
- $expr: {
1191
- rhp: {
1192
- role: {
1193
- $any: {
1194
- $alias: 'r',
1195
- $expr: {
1196
- r: {
1197
- is_of__api_key: {
1198
- $any: {
1199
- $alias: 'khr',
1200
- $expr: {
1201
- khr: {
1202
- api_key: {
1203
- $any: {
1204
- $alias: 'k',
1205
- $expr: {
1206
- k: { key: { '@': 'apiKey' } },
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
- // We orderby to increase the hit rate for the `_checkPermissions` memoisation
1225
- $orderby: {
1226
- name: 'asc',
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
- const permissions = (await getApiKeyPermissionsQuery()({
1239
- apiKey,
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 getApiKeyActorIdQuery = _.once(() =>
1249
- sbvrUtils.api.Auth.prepare<{ apiKey: string }>({
1250
- resource: 'api_key',
1251
- passthrough: {
1252
- req: rootRead,
1253
- },
1254
- id: {
1255
- key: { '@': 'apiKey' },
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
- options: {
1258
- $select: 'is_of__actor',
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
- if (apiKey == null || req.apiKey != null) {
1284
- return;
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(req, apiKey);
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] != null
1357
- ? req.params[paramName]
1358
- : req.body[paramName] != null
1359
- ? req.body[paramName]
1360
- : req.query[paramName];
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
- ): Express.RequestHandler => async (req, res, next) => {
1405
- try {
1406
- const allowed = await checkPermissions(req, action);
1407
- switch (allowed) {
1408
- case false:
1409
- res.sendStatus(401);
1410
- return;
1411
- case true:
1412
- next();
1413
- return;
1414
- default:
1415
- throw new Error(
1416
- 'checkPermissionsMiddleware returned a conditional permission',
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
- } catch (err) {
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
- return _.uniq(await getUserPermissions(result.id));
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 [guestPermissions] = await Promise.all([
1457
- getGuestPermissions(),
1458
- (async () => {
1459
- // TODO: Remove this extra actor ID lookup making actor non-optional and updating open-balena-api.
1460
- if (
1461
- req.apiKey != null &&
1462
- req.apiKey.actor == null &&
1463
- req.apiKey.permissions != null &&
1464
- req.apiKey.permissions.length > 0
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
- odataBinds[actorBind] = ['Real', actorId];
1488
- actorIndex++;
1489
- permissions = permissions.concat(actorPermissions);
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 != null && req.user.permissions != null) {
1629
+ if (req.user?.permissions != null) {
1493
1630
  addActorPermissions(req.user.actor, req.user.permissions);
1494
- } else if (req.apiKey != null && req.apiKey.permissions != null) {
1631
+ } else if (req.apiKey?.permissions != null) {
1495
1632
  addActorPermissions(req.apiKey.actor!, req.apiKey.permissions);
1496
1633
  }
1497
1634
 
1498
- permissions = _.uniq(permissions);
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 { vocabulary, resourceName, odataQuery, odataBinds } = request;
1508
- let { method } = request;
1642
+ const { resourceName, odataQuery, odataBinds } = request;
1643
+ const vocabulary = _.last(request.translateVersions)!;
1509
1644
  let abstractSqlModel = sbvrUtils.getAbstractSqlModel(request);
1510
- method = method.toUpperCase() as SupportedMethod;
1511
- const isMetadataEndpoint =
1512
- metadataEndpoints.includes(resourceName) || method === 'OPTIONS';
1513
-
1514
- let permissionType: PermissionCheck;
1515
- if (request.permissionType != null) {
1516
- permissionType = request.permissionType;
1517
- } else if (isMetadataEndpoint) {
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
- console.warn('Unknown method for permissions type check: ', method);
1525
- permissionType = 'all';
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 != null &&
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, data }) => {
1660
- if (request.custom.isAction === 'canAccess' && _.isEmpty(data)) {
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