@bedrockio/model 0.1.5 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -29,19 +29,17 @@ function hasAccess(type, allowed = 'all', options = {}) {
29
29
  document,
30
30
  authUser
31
31
  } = options;
32
- if (!Array.isArray(allowed)) {
33
- allowed = [allowed];
34
- }
35
32
  const scopes = resolveScopes(options);
33
+ allowed = resolveAllowed(allowed);
36
34
  return allowed.some(token => {
37
35
  if (token === 'self') {
38
- assertOptions(type, token, options);
36
+ assertOptions(token, options);
39
37
  return document.id == authUser.id;
40
38
  } else if (token === 'user') {
41
- assertOptions(type, token, options);
39
+ assertOptions(token, options);
42
40
  return document.user?.id == authUser.id;
43
41
  } else if (token === 'owner') {
44
- assertOptions(type, token, options);
42
+ assertOptions(token, options);
45
43
  return document.owner?.id == authUser.id;
46
44
  } else {
47
45
  return scopes?.includes(token);
@@ -53,18 +51,34 @@ function resolveScopes(options) {
53
51
  if (!options.scope && !options.scopes) {
54
52
  (0, _warn.default)('Scopes were requested but not provided.');
55
53
  }
56
- const {
57
- scope,
58
- scopes = []
59
- } = options;
60
- return scope ? [scope] : scopes;
54
+ let scopes;
55
+ if (options.scopes) {
56
+ scopes = options.scopes;
57
+ } else if (options.scope) {
58
+ scopes = [options.scope];
59
+ } else {
60
+ scopes = [];
61
+ }
62
+ return scopes;
61
63
  }
62
- function assertOptions(type, token, options) {
63
- if (!options.authUser || !options.document) {
64
- if (type === 'read') {
65
- throw new _errors.ImplementationError(`Read access "${token}" requires .toObject({ authUser }).`);
64
+ function resolveAllowed(arg) {
65
+ const allowed = Array.isArray(arg) ? arg : [arg];
66
+
67
+ // Sort allowed scopes to put "self" last allowing
68
+ // role based scopes to be fulfilled first.
69
+ allowed.sort((a, b) => {
70
+ if (a === b) {
71
+ return 0;
72
+ } else if (a === 'self') {
73
+ return 1;
66
74
  } else {
67
- throw new _errors.ImplementationError(`Write access "${token}" requires passing { document, authUser } to the validator.`);
75
+ return -1;
68
76
  }
77
+ });
78
+ return allowed;
79
+ }
80
+ function assertOptions(token, options) {
81
+ if (!options.authUser || !options.document) {
82
+ throw new _errors.ImplementationError(token);
69
83
  }
70
84
  }
@@ -6,7 +6,12 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.ReferenceError = exports.PermissionsError = exports.ImplementationError = void 0;
7
7
  class PermissionsError extends Error {}
8
8
  exports.PermissionsError = PermissionsError;
9
- class ImplementationError extends Error {}
9
+ class ImplementationError extends Error {
10
+ constructor(name) {
11
+ super();
12
+ this.name = name;
13
+ }
14
+ }
10
15
  exports.ImplementationError = ImplementationError;
11
16
  class ReferenceError extends Error {
12
17
  constructor(message, references) {
@@ -333,14 +333,31 @@ function validateAccess(type, schema, allowed, options) {
333
333
  } = options.model;
334
334
  return schema.custom((val, options) => {
335
335
  const document = options[(0, _lodash.lowerFirst)(modelName)] || options['document'];
336
- const isAllowed = (0, _access.hasAccess)(type, allowed, {
337
- ...options,
338
- document
339
- });
336
+ let isAllowed;
337
+ try {
338
+ isAllowed = (0, _access.hasAccess)(type, allowed, {
339
+ ...options,
340
+ document
341
+ });
342
+ } catch (error) {
343
+ if (error instanceof _errors.ImplementationError) {
344
+ if (type === 'read') {
345
+ // Read access validation for search means that "self" access
346
+ // cannot be fulfilled as there is no single document to test
347
+ // against, so continue on to throw a normal permissions error
348
+ // here instead of raising a problem with the implementation.
349
+ isAllowed = false;
350
+ } else if (type === 'write') {
351
+ throw new Error(`Write access "${error.name}" requires passing { document, authUser } to the validator.`);
352
+ }
353
+ } else {
354
+ throw error;
355
+ }
356
+ }
340
357
  if (!isAllowed) {
341
358
  const currentValue = (0, _lodash.get)(document, options.path);
342
359
  if (val !== currentValue) {
343
- throw new _errors.PermissionsError('requires write permissions.');
360
+ throw new _errors.PermissionsError(`requires ${type} permissions.`);
344
361
  }
345
362
  }
346
363
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bedrockio/model",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "Bedrock utilities for model creation.",
5
5
  "type": "module",
6
6
  "scripts": {
@@ -29,7 +29,7 @@
29
29
  "lodash": "^4.17.21"
30
30
  },
31
31
  "peerDependencies": {
32
- "@bedrockio/yada": "^1.0.20",
32
+ "@bedrockio/yada": "^1.0.25",
33
33
  "mongoose": "^6.9.0"
34
34
  },
35
35
  "devDependencies": {
@@ -37,7 +37,7 @@
37
37
  "@babel/core": "^7.20.12",
38
38
  "@babel/preset-env": "^7.20.2",
39
39
  "@bedrockio/prettier-config": "^1.0.2",
40
- "@bedrockio/yada": "^1.0.20",
40
+ "@bedrockio/yada": "^1.0.25",
41
41
  "@shelf/jest-mongodb": "^4.1.6",
42
42
  "babel-plugin-import-replacement": "^1.0.1",
43
43
  "eslint": "^8.33.0",
package/src/access.js CHANGED
@@ -19,19 +19,18 @@ export function hasAccess(type, allowed = 'all', options = {}) {
19
19
  return false;
20
20
  } else {
21
21
  const { document, authUser } = options;
22
- if (!Array.isArray(allowed)) {
23
- allowed = [allowed];
24
- }
25
22
  const scopes = resolveScopes(options);
23
+
24
+ allowed = resolveAllowed(allowed);
26
25
  return allowed.some((token) => {
27
26
  if (token === 'self') {
28
- assertOptions(type, token, options);
27
+ assertOptions(token, options);
29
28
  return document.id == authUser.id;
30
29
  } else if (token === 'user') {
31
- assertOptions(type, token, options);
30
+ assertOptions(token, options);
32
31
  return document.user?.id == authUser.id;
33
32
  } else if (token === 'owner') {
34
- assertOptions(type, token, options);
33
+ assertOptions(token, options);
35
34
  return document.owner?.id == authUser.id;
36
35
  } else {
37
36
  return scopes?.includes(token);
@@ -44,20 +43,39 @@ function resolveScopes(options) {
44
43
  if (!options.scope && !options.scopes) {
45
44
  warn('Scopes were requested but not provided.');
46
45
  }
47
- const { scope, scopes = [] } = options;
48
- return scope ? [scope] : scopes;
46
+
47
+ let scopes;
48
+ if (options.scopes) {
49
+ scopes = options.scopes;
50
+ } else if (options.scope) {
51
+ scopes = [options.scope];
52
+ } else {
53
+ scopes = [];
54
+ }
55
+
56
+ return scopes;
49
57
  }
50
58
 
51
- function assertOptions(type, token, options) {
52
- if (!options.authUser || !options.document) {
53
- if (type === 'read') {
54
- throw new ImplementationError(
55
- `Read access "${token}" requires .toObject({ authUser }).`
56
- );
59
+ function resolveAllowed(arg) {
60
+ const allowed = Array.isArray(arg) ? arg : [arg];
61
+
62
+ // Sort allowed scopes to put "self" last allowing
63
+ // role based scopes to be fulfilled first.
64
+ allowed.sort((a, b) => {
65
+ if (a === b) {
66
+ return 0;
67
+ } else if (a === 'self') {
68
+ return 1;
57
69
  } else {
58
- throw new ImplementationError(
59
- `Write access "${token}" requires passing { document, authUser } to the validator.`
60
- );
70
+ return -1;
61
71
  }
72
+ });
73
+
74
+ return allowed;
75
+ }
76
+
77
+ function assertOptions(token, options) {
78
+ if (!options.authUser || !options.document) {
79
+ throw new ImplementationError(token);
62
80
  }
63
81
  }
package/src/errors.js CHANGED
@@ -1,5 +1,11 @@
1
1
  export class PermissionsError extends Error {}
2
- export class ImplementationError extends Error {}
2
+
3
+ export class ImplementationError extends Error {
4
+ constructor(name) {
5
+ super();
6
+ this.name = name;
7
+ }
8
+ }
3
9
 
4
10
  export class ReferenceError extends Error {
5
11
  constructor(message, references) {
package/src/validation.js CHANGED
@@ -5,7 +5,7 @@ import { get, omit, lowerFirst } from 'lodash';
5
5
 
6
6
  import { hasAccess } from './access';
7
7
  import { searchValidation } from './search';
8
- import { PermissionsError } from './errors';
8
+ import { PermissionsError, ImplementationError } from './errors';
9
9
  import { hasUniqueConstraints, assertUnique } from './soft-delete';
10
10
  import { isMongooseSchema, isSchemaTypedef } from './utils';
11
11
  import { RESERVED_FIELDS } from './schema';
@@ -368,14 +368,35 @@ function validateAccess(type, schema, allowed, options) {
368
368
  const { modelName } = options.model;
369
369
  return schema.custom((val, options) => {
370
370
  const document = options[lowerFirst(modelName)] || options['document'];
371
- const isAllowed = hasAccess(type, allowed, {
372
- ...options,
373
- document,
374
- });
371
+
372
+ let isAllowed;
373
+ try {
374
+ isAllowed = hasAccess(type, allowed, {
375
+ ...options,
376
+ document,
377
+ });
378
+ } catch (error) {
379
+ if (error instanceof ImplementationError) {
380
+ if (type === 'read') {
381
+ // Read access validation for search means that "self" access
382
+ // cannot be fulfilled as there is no single document to test
383
+ // against, so continue on to throw a normal permissions error
384
+ // here instead of raising a problem with the implementation.
385
+ isAllowed = false;
386
+ } else if (type === 'write') {
387
+ throw new Error(
388
+ `Write access "${error.name}" requires passing { document, authUser } to the validator.`
389
+ );
390
+ }
391
+ } else {
392
+ throw error;
393
+ }
394
+ }
395
+
375
396
  if (!isAllowed) {
376
397
  const currentValue = get(document, options.path);
377
398
  if (val !== currentValue) {
378
- throw new PermissionsError('requires write permissions.');
399
+ throw new PermissionsError(`requires ${type} permissions.`);
379
400
  }
380
401
  }
381
402
  });
@@ -1 +1 @@
1
- {"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../src/access.js"],"names":[],"mappings":"AAGA,mEAEC;AAED,oEAEC;AAED;;GAEG;AACH,+CAFW,MAAM,GAAC,MAAM,EAAE,yBA4BzB"}
1
+ {"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../src/access.js"],"names":[],"mappings":"AAGA,mEAEC;AAED,oEAEC;AAED;;GAEG;AACH,+CAFW,MAAM,GAAC,MAAM,EAAE,yBA2BzB"}
package/types/errors.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  export class PermissionsError extends Error {
2
2
  }
3
3
  export class ImplementationError extends Error {
4
+ constructor(name: any);
5
+ name: any;
4
6
  }
5
7
  export class ReferenceError extends Error {
6
8
  constructor(message: any, references: any);
@@ -1 +1 @@
1
- {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.js"],"names":[],"mappings":"AAAA;CAA8C;AAC9C;CAAiD;AAEjD;IACE,2CAGC;IADC,gBAA4B;CAE/B"}
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.js"],"names":[],"mappings":"AAAA;CAA8C;AAE9C;IACE,uBAGC;IADC,UAAgB;CAEnB;AAED;IACE,2CAGC;IADC,gBAA4B;CAE/B"}
package/types/search.d.ts CHANGED
@@ -3,6 +3,8 @@ export function searchValidation(definition: any, options?: {}): {
3
3
  setup: any;
4
4
  getFields: any;
5
5
  append(arg: import("@bedrockio/yada/types/Schema").default | import("@bedrockio/yada/types/object").SchemaMap): any;
6
+ pick(...names?: string[]): any;
7
+ omit(...names?: string[]): any;
6
8
  format(name: any, fn: any): import("@bedrockio/yada/types/TypeSchema").default;
7
9
  toString(): any;
8
10
  assertions: any[];
@@ -20,6 +22,7 @@ export function searchValidation(definition: any, options?: {}): {
20
22
  validate(value: any, options?: {}): Promise<any>;
21
23
  clone(meta: any): any;
22
24
  toOpenApi(extra: any): any;
25
+ expandExtra(extra?: {}): {};
23
26
  assertEnum(set: any, allow: any): any;
24
27
  assert(type: any, fn: any): any;
25
28
  pushAssertion(assertion: any): void;
@@ -1 +1 @@
1
- {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../src/search.js"],"names":[],"mappings":"AAmBA,gEAyDC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwBC"}
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../src/search.js"],"names":[],"mappings":"AAmBA,gEAyDC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAwBC"}
@@ -1 +1 @@
1
- {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.js"],"names":[],"mappings":"AAiEA,kDAEC;AAED,oEA2DC;AAaD,wEAeC;AAqOD;;;EAEC;AAED;;;EAOC;AArXD,8EAUK"}
1
+ {"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.js"],"names":[],"mappings":"AAiEA,kDAEC;AAED,oEA2DC;AAaD,wEAeC;AA0PD;;;EAEC;AAED;;;EAOC;AA1YD,8EAUK"}