@bedrockio/model 0.2.21 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +81 -8
- package/dist/cjs/access.js +1 -10
- package/dist/cjs/include.js +29 -10
- package/dist/cjs/serialization.js +2 -2
- package/dist/cjs/validation.js +36 -15
- package/package.json +1 -1
- package/src/access.js +1 -9
- package/src/include.js +29 -9
- package/src/serialization.js +2 -2
- package/src/validation.js +35 -17
- package/types/access.d.ts +1 -3
- package/types/access.d.ts.map +1 -1
- package/types/include.d.ts +2 -1
- package/types/include.d.ts.map +1 -1
- package/types/validation.d.ts.map +1 -1
package/README.md
CHANGED
|
@@ -472,7 +472,7 @@ In the above example `getCreateValidation` returns a
|
|
|
472
472
|
`validateBody` middleware. The `password` field is an additional field that is
|
|
473
473
|
appended to the create schema.
|
|
474
474
|
|
|
475
|
-
There are
|
|
475
|
+
There are 4 main methods to generate schemas:
|
|
476
476
|
|
|
477
477
|
- `getCreateValidation`: Validates all fields while disallowing reserved fields
|
|
478
478
|
like `id`, `createdAt`, and `updatedAt`.
|
|
@@ -488,6 +488,7 @@ There are 3 main methods to generate schemas:
|
|
|
488
488
|
allowed.
|
|
489
489
|
- Array fields are "unwound". This means that for example given an array field
|
|
490
490
|
`categories`, input may be either a string or an array of strings.
|
|
491
|
+
- `getDeleteValidation`: Only used for access validation (more below).
|
|
491
492
|
|
|
492
493
|
#### Named Validations
|
|
493
494
|
|
|
@@ -752,8 +753,7 @@ queries and more unneeded data transfer over the wire.
|
|
|
752
753
|
|
|
753
754
|
For this reason calling `populate` manually is highly preferable, however in
|
|
754
755
|
complex situations this can easily be a lot of overhead. The include module
|
|
755
|
-
attempts to
|
|
756
|
-
queries:
|
|
756
|
+
attempts to streamline this process by adding an `include` method to queries:
|
|
757
757
|
|
|
758
758
|
```js
|
|
759
759
|
const product = await Product.findById(id).include([
|
|
@@ -905,18 +905,48 @@ The `getSearchValidation` will allow the `include` property to be passed,
|
|
|
905
905
|
letting the client populate documents as they require. Note that the fields a
|
|
906
906
|
client is able to include is subject to [access control](#access-control).
|
|
907
907
|
|
|
908
|
+
#### Other Differences with Populate
|
|
909
|
+
|
|
910
|
+
Calling `populate` on a Mongoose document will always load the current data. In
|
|
911
|
+
contrast, `include` will only load when not yet populated, providing better
|
|
912
|
+
performance for most situations such as pre save hooks:
|
|
913
|
+
|
|
914
|
+
```js
|
|
915
|
+
schema.pre('save', async () => {
|
|
916
|
+
// Will not result in a populate call if the
|
|
917
|
+
// owner document has already been populated.
|
|
918
|
+
await this.include('owner');
|
|
919
|
+
|
|
920
|
+
this.ownerName = this.owner.name;
|
|
921
|
+
});
|
|
922
|
+
```
|
|
923
|
+
|
|
924
|
+
If always fetching the current document is preferred, the `force` option can be
|
|
925
|
+
passed:
|
|
926
|
+
|
|
927
|
+
```js
|
|
928
|
+
await shop.include('owner', {
|
|
929
|
+
force: true,
|
|
930
|
+
});
|
|
931
|
+
```
|
|
932
|
+
|
|
908
933
|
### Access Control
|
|
909
934
|
|
|
910
935
|
This package applies two forms of access control:
|
|
911
936
|
|
|
912
|
-
|
|
937
|
+
- [Field Access](#field-access)
|
|
938
|
+
- [Document Access](#document-access)
|
|
939
|
+
|
|
940
|
+
#### Field Access:
|
|
941
|
+
|
|
942
|
+
##### Read Access
|
|
913
943
|
|
|
914
944
|
Read access influences how documents are serialized. Fields that have been
|
|
915
945
|
denied access will be stripped out. Additionally it will influence the
|
|
916
946
|
validation schema for `getSearchValidation`. Fields that have been denied access
|
|
917
947
|
are not allowed to be searched on and will throw an error.
|
|
918
948
|
|
|
919
|
-
|
|
949
|
+
##### Write Access
|
|
920
950
|
|
|
921
951
|
Write access influences validation in `getCreateValidation` and
|
|
922
952
|
`getUpdateValidation`. Fields that have been denied access will throw an error
|
|
@@ -924,7 +954,7 @@ unless they are identical to what is already set on the document. Note that in
|
|
|
924
954
|
the case of `getCreateValidation` no document has been created yet so a denied
|
|
925
955
|
field will always result in an error if passed.
|
|
926
956
|
|
|
927
|
-
|
|
957
|
+
##### Defining Field Access
|
|
928
958
|
|
|
929
959
|
Access is defined in schemas with the `readAccess` and `writeAccess` options:
|
|
930
960
|
|
|
@@ -1098,7 +1128,7 @@ owner, as this example illustrates:
|
|
|
1098
1128
|
}
|
|
1099
1129
|
```
|
|
1100
1130
|
|
|
1101
|
-
|
|
1131
|
+
##### Notes on Read Access
|
|
1102
1132
|
|
|
1103
1133
|
Note that all forms of read access require that `.toObject` is called on the
|
|
1104
1134
|
document with special parameters, however this method is called on internal
|
|
@@ -1107,11 +1137,54 @@ this reason it will never fail even if it cannot perform the correct access
|
|
|
1107
1137
|
checks. Instead any fields with `readAccess` defined on them will be stripped
|
|
1108
1138
|
out.
|
|
1109
1139
|
|
|
1110
|
-
|
|
1140
|
+
##### Notes on Write Access
|
|
1111
1141
|
|
|
1112
1142
|
Note that `self` is generally only meaningful on a User model as it will always
|
|
1113
1143
|
check the document is the same as `authUser`.
|
|
1114
1144
|
|
|
1145
|
+
#### Document Access
|
|
1146
|
+
|
|
1147
|
+
In addition to the fine grained control of accessing fields, documents
|
|
1148
|
+
themselves may also have access control. This can be defined in the `access` key
|
|
1149
|
+
of the model definition:
|
|
1150
|
+
|
|
1151
|
+
```jsonc
|
|
1152
|
+
// user.json
|
|
1153
|
+
{
|
|
1154
|
+
"attributes": {
|
|
1155
|
+
// ...
|
|
1156
|
+
},
|
|
1157
|
+
"access": {
|
|
1158
|
+
// A user may update themselves or an admin.
|
|
1159
|
+
"update": ["self", "admin"],
|
|
1160
|
+
// Only an admin may delete a user.
|
|
1161
|
+
"delete": ["admin"]
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
```
|
|
1165
|
+
|
|
1166
|
+
The same options can be used as
|
|
1167
|
+
[document based access on fields](#document-based-access), so this could be
|
|
1168
|
+
`owner`, etc:
|
|
1169
|
+
|
|
1170
|
+
```jsonc
|
|
1171
|
+
// shop.json
|
|
1172
|
+
{
|
|
1173
|
+
"attributes": {
|
|
1174
|
+
"owner": {
|
|
1175
|
+
"type": "ObjectId",
|
|
1176
|
+
"ref": "User"
|
|
1177
|
+
}
|
|
1178
|
+
},
|
|
1179
|
+
"access": {
|
|
1180
|
+
// An owner may update their own shop.
|
|
1181
|
+
"update": ["owner", "admin"],
|
|
1182
|
+
// Only an admin may delete a shop.
|
|
1183
|
+
"delete": ["admin"]
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
```
|
|
1187
|
+
|
|
1115
1188
|
### Delete Hooks
|
|
1116
1189
|
|
|
1117
1190
|
Delete hooks are a powerful way to define what actions are taken on document
|
package/dist/cjs/access.js
CHANGED
|
@@ -4,22 +4,13 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
4
4
|
value: true
|
|
5
5
|
});
|
|
6
6
|
exports.hasAccess = hasAccess;
|
|
7
|
-
exports.hasReadAccess = hasReadAccess;
|
|
8
|
-
exports.hasWriteAccess = hasWriteAccess;
|
|
9
7
|
var _errors = require("./errors");
|
|
10
8
|
var _warn = _interopRequireDefault(require("./warn"));
|
|
11
9
|
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
|
|
12
|
-
function hasReadAccess(allowed, options) {
|
|
13
|
-
return hasAccess('read', allowed, options);
|
|
14
|
-
}
|
|
15
|
-
function hasWriteAccess(allowed, options) {
|
|
16
|
-
return hasAccess('write', allowed, options);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
10
|
/**
|
|
20
11
|
* @param {string|string[]} allowed
|
|
21
12
|
*/
|
|
22
|
-
function hasAccess(
|
|
13
|
+
function hasAccess(allowed = 'all', options = {}) {
|
|
23
14
|
if (allowed === 'all') {
|
|
24
15
|
return true;
|
|
25
16
|
} else if (allowed === 'none') {
|
package/dist/cjs/include.js
CHANGED
|
@@ -6,7 +6,8 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
exports.INCLUDE_FIELD_SCHEMA = void 0;
|
|
7
7
|
exports.applyInclude = applyInclude;
|
|
8
8
|
exports.checkSelects = checkSelects;
|
|
9
|
-
exports.
|
|
9
|
+
exports.getDocumentParams = getDocumentParams;
|
|
10
|
+
exports.getParams = getParams;
|
|
10
11
|
var _mongoose = _interopRequireDefault(require("mongoose"));
|
|
11
12
|
var _lodash = require("lodash");
|
|
12
13
|
var _yada = _interopRequireDefault(require("@bedrockio/yada"));
|
|
@@ -36,7 +37,7 @@ function applyInclude(schema) {
|
|
|
36
37
|
const {
|
|
37
38
|
select,
|
|
38
39
|
populate
|
|
39
|
-
} =
|
|
40
|
+
} = getQueryParams(this, filter.include);
|
|
40
41
|
this.select(select);
|
|
41
42
|
this.populate(populate);
|
|
42
43
|
delete filter.include;
|
|
@@ -78,7 +79,7 @@ function applyInclude(schema) {
|
|
|
78
79
|
const {
|
|
79
80
|
select,
|
|
80
81
|
populate
|
|
81
|
-
} =
|
|
82
|
+
} = getDocumentParams(this, include);
|
|
82
83
|
this.$locals.select = select;
|
|
83
84
|
this.$locals.populate = populate;
|
|
84
85
|
}
|
|
@@ -98,12 +99,12 @@ function applyInclude(schema) {
|
|
|
98
99
|
// Perform population immediately when instance method is called.
|
|
99
100
|
// Store selects as a local variable which will be checked
|
|
100
101
|
// during serialization.
|
|
101
|
-
schema.method('include', async function include(include) {
|
|
102
|
+
schema.method('include', async function include(include, options) {
|
|
102
103
|
if (include) {
|
|
103
104
|
const {
|
|
104
105
|
select,
|
|
105
106
|
populate
|
|
106
|
-
} =
|
|
107
|
+
} = getDocumentParams(this, include, options);
|
|
107
108
|
this.$locals.select = select;
|
|
108
109
|
await this.populate(populate);
|
|
109
110
|
}
|
|
@@ -147,16 +148,34 @@ function checkSelects(doc, ret) {
|
|
|
147
148
|
}
|
|
148
149
|
|
|
149
150
|
// Exported for testing.
|
|
150
|
-
function
|
|
151
|
+
function getParams(modelName, arg) {
|
|
151
152
|
const paths = Array.isArray(arg) ? arg : [arg];
|
|
152
153
|
const node = pathsToNode(paths, modelName);
|
|
153
154
|
return nodeToPopulates(node);
|
|
154
155
|
}
|
|
155
|
-
|
|
156
|
-
|
|
156
|
+
|
|
157
|
+
// Exported for testing.
|
|
158
|
+
function getDocumentParams(doc, arg, options = {}) {
|
|
159
|
+
const params = getParams(doc.constructor.modelName, arg);
|
|
160
|
+
if (!options.force) {
|
|
161
|
+
params.populate = params.populate.filter(p => {
|
|
162
|
+
return !isDocumentPopulated(doc, p);
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
return params;
|
|
166
|
+
}
|
|
167
|
+
function isDocumentPopulated(doc, params) {
|
|
168
|
+
if (doc.populated(params.path)) {
|
|
169
|
+
const sub = doc.get(params.path);
|
|
170
|
+
return params.populate.every(p => {
|
|
171
|
+
return isDocumentPopulated(sub, p);
|
|
172
|
+
});
|
|
173
|
+
} else {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
157
176
|
}
|
|
158
|
-
function
|
|
159
|
-
return
|
|
177
|
+
function getQueryParams(query, arg) {
|
|
178
|
+
return getParams(query.model.modelName, arg);
|
|
160
179
|
}
|
|
161
180
|
|
|
162
181
|
// Note that:
|
|
@@ -6,8 +6,8 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
6
6
|
exports.serializeOptions = void 0;
|
|
7
7
|
var _lodash = require("lodash");
|
|
8
8
|
var _include = require("./include");
|
|
9
|
-
var _access = require("./access");
|
|
10
9
|
var _utils = require("./utils");
|
|
10
|
+
var _access = require("./access");
|
|
11
11
|
const DISALLOWED_FIELDS = ['deleted', 'deletedRefs'];
|
|
12
12
|
const serializeOptions = exports.serializeOptions = {
|
|
13
13
|
getters: true,
|
|
@@ -63,7 +63,7 @@ function isAllowedField(key, field, options) {
|
|
|
63
63
|
readAccess
|
|
64
64
|
} = (0, _utils.getField)(field, key);
|
|
65
65
|
try {
|
|
66
|
-
return (0, _access.
|
|
66
|
+
return (0, _access.hasAccess)(readAccess, options);
|
|
67
67
|
} catch {
|
|
68
68
|
return false;
|
|
69
69
|
}
|
package/dist/cjs/validation.js
CHANGED
|
@@ -112,6 +112,7 @@ function applyValidation(schema, definition) {
|
|
|
112
112
|
stripTimestamps: true,
|
|
113
113
|
allowExpandedRefs: true,
|
|
114
114
|
requireWriteAccess: true,
|
|
115
|
+
updateAccess: definition.access?.update,
|
|
115
116
|
...(hasUnique && {
|
|
116
117
|
assertUniqueOptions: {
|
|
117
118
|
schema,
|
|
@@ -120,6 +121,13 @@ function applyValidation(schema, definition) {
|
|
|
120
121
|
})
|
|
121
122
|
});
|
|
122
123
|
});
|
|
124
|
+
schema.static('getDeleteValidation', function getDeleteValidation() {
|
|
125
|
+
const allowed = definition.access?.delete || 'all';
|
|
126
|
+
return validateAccess('delete', _yada.default, allowed, {
|
|
127
|
+
model: this,
|
|
128
|
+
message: 'You do not have permissions to delete this document.'
|
|
129
|
+
});
|
|
130
|
+
});
|
|
123
131
|
schema.static('getSearchValidation', function getSearchValidation(options = {}) {
|
|
124
132
|
const {
|
|
125
133
|
defaults,
|
|
@@ -180,7 +188,8 @@ function getValidationSchema(attributes, options = {}) {
|
|
|
180
188
|
const {
|
|
181
189
|
appendSchema,
|
|
182
190
|
assertUniqueOptions,
|
|
183
|
-
allowInclude
|
|
191
|
+
allowInclude,
|
|
192
|
+
updateAccess
|
|
184
193
|
} = options;
|
|
185
194
|
let schema = getObjectSchema(attributes, options);
|
|
186
195
|
if (assertUniqueOptions) {
|
|
@@ -199,6 +208,12 @@ function getValidationSchema(attributes, options = {}) {
|
|
|
199
208
|
if (allowInclude) {
|
|
200
209
|
schema = schema.append(_include.INCLUDE_FIELD_SCHEMA);
|
|
201
210
|
}
|
|
211
|
+
if (updateAccess) {
|
|
212
|
+
return validateAccess('update', schema, updateAccess, {
|
|
213
|
+
...options,
|
|
214
|
+
message: 'You do not have permissions to update this document.'
|
|
215
|
+
});
|
|
216
|
+
}
|
|
202
217
|
return schema;
|
|
203
218
|
}
|
|
204
219
|
function getObjectSchema(arg, options) {
|
|
@@ -304,10 +319,10 @@ function getSchemaForTypedef(typedef, options = {}) {
|
|
|
304
319
|
schema = getSearchSchema(schema, type);
|
|
305
320
|
}
|
|
306
321
|
if (typedef.readAccess && options.requireReadAccess) {
|
|
307
|
-
schema =
|
|
322
|
+
schema = validateAccess('read', schema, typedef.readAccess, options);
|
|
308
323
|
}
|
|
309
324
|
if (typedef.writeAccess && options.requireWriteAccess) {
|
|
310
|
-
schema =
|
|
325
|
+
schema = validateAccess('write', schema, typedef.writeAccess, options);
|
|
311
326
|
}
|
|
312
327
|
return schema;
|
|
313
328
|
}
|
|
@@ -392,13 +407,10 @@ function isExcludedField(field, options) {
|
|
|
392
407
|
}
|
|
393
408
|
return false;
|
|
394
409
|
}
|
|
395
|
-
function validateReadAccess(schema, allowed, options) {
|
|
396
|
-
return validateAccess('read', schema, allowed, options);
|
|
397
|
-
}
|
|
398
|
-
function validateWriteAccess(schema, allowed, options) {
|
|
399
|
-
return validateAccess('write', schema, allowed, options);
|
|
400
|
-
}
|
|
401
410
|
function validateAccess(type, schema, allowed, options) {
|
|
411
|
+
let {
|
|
412
|
+
message
|
|
413
|
+
} = options;
|
|
402
414
|
const {
|
|
403
415
|
modelName
|
|
404
416
|
} = options.model;
|
|
@@ -406,7 +418,7 @@ function validateAccess(type, schema, allowed, options) {
|
|
|
406
418
|
const document = options[(0, _lodash.lowerFirst)(modelName)] || options['document'];
|
|
407
419
|
let isAllowed;
|
|
408
420
|
try {
|
|
409
|
-
isAllowed = (0, _access.hasAccess)(
|
|
421
|
+
isAllowed = (0, _access.hasAccess)(allowed, {
|
|
410
422
|
...options,
|
|
411
423
|
document
|
|
412
424
|
});
|
|
@@ -418,18 +430,27 @@ function validateAccess(type, schema, allowed, options) {
|
|
|
418
430
|
// against, so continue on to throw a normal permissions error
|
|
419
431
|
// here instead of raising a problem with the implementation.
|
|
420
432
|
isAllowed = false;
|
|
421
|
-
} else
|
|
422
|
-
throw new Error(`
|
|
433
|
+
} else {
|
|
434
|
+
throw new Error(`Access validation "${error.name}" requires passing { document, authUser } to the validator.`);
|
|
423
435
|
}
|
|
424
436
|
} else {
|
|
425
437
|
throw error;
|
|
426
438
|
}
|
|
427
439
|
}
|
|
428
440
|
if (!isAllowed) {
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
441
|
+
const {
|
|
442
|
+
path
|
|
443
|
+
} = options;
|
|
444
|
+
if (path) {
|
|
445
|
+
if ((0, _lodash.get)(document, path) === val) {
|
|
446
|
+
// If there is a path being accessed and the current
|
|
447
|
+
// value is the same as what is being set then do not
|
|
448
|
+
// throw the error.
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
message ||= `Field "${path.join('.')}" requires ${type} permissions.`;
|
|
432
452
|
}
|
|
453
|
+
throw new _errors.PermissionsError(message);
|
|
433
454
|
}
|
|
434
455
|
});
|
|
435
456
|
}
|
package/package.json
CHANGED
package/src/access.js
CHANGED
|
@@ -1,18 +1,10 @@
|
|
|
1
1
|
import { ImplementationError } from './errors';
|
|
2
2
|
import warn from './warn';
|
|
3
3
|
|
|
4
|
-
export function hasReadAccess(allowed, options) {
|
|
5
|
-
return hasAccess('read', allowed, options);
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export function hasWriteAccess(allowed, options) {
|
|
9
|
-
return hasAccess('write', allowed, options);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
4
|
/**
|
|
13
5
|
* @param {string|string[]} allowed
|
|
14
6
|
*/
|
|
15
|
-
export function hasAccess(
|
|
7
|
+
export function hasAccess(allowed = 'all', options = {}) {
|
|
16
8
|
if (allowed === 'all') {
|
|
17
9
|
return true;
|
|
18
10
|
} else if (allowed === 'none') {
|
package/src/include.js
CHANGED
|
@@ -31,7 +31,7 @@ export function applyInclude(schema) {
|
|
|
31
31
|
schema.pre(/^find/, function (next) {
|
|
32
32
|
const filter = this.getFilter();
|
|
33
33
|
if (filter.include) {
|
|
34
|
-
const { select, populate } =
|
|
34
|
+
const { select, populate } = getQueryParams(this, filter.include);
|
|
35
35
|
this.select(select);
|
|
36
36
|
this.populate(populate);
|
|
37
37
|
delete filter.include;
|
|
@@ -70,7 +70,7 @@ export function applyInclude(schema) {
|
|
|
70
70
|
this.assign(rest);
|
|
71
71
|
|
|
72
72
|
if (include) {
|
|
73
|
-
const { select, populate } =
|
|
73
|
+
const { select, populate } = getDocumentParams(this, include);
|
|
74
74
|
this.$locals.select = select;
|
|
75
75
|
this.$locals.populate = populate;
|
|
76
76
|
}
|
|
@@ -89,9 +89,9 @@ export function applyInclude(schema) {
|
|
|
89
89
|
// Perform population immediately when instance method is called.
|
|
90
90
|
// Store selects as a local variable which will be checked
|
|
91
91
|
// during serialization.
|
|
92
|
-
schema.method('include', async function include(include) {
|
|
92
|
+
schema.method('include', async function include(include, options) {
|
|
93
93
|
if (include) {
|
|
94
|
-
const { select, populate } =
|
|
94
|
+
const { select, populate } = getDocumentParams(this, include, options);
|
|
95
95
|
this.$locals.select = select;
|
|
96
96
|
await this.populate(populate);
|
|
97
97
|
}
|
|
@@ -133,18 +133,38 @@ export function checkSelects(doc, ret) {
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
// Exported for testing.
|
|
136
|
-
export function
|
|
136
|
+
export function getParams(modelName, arg) {
|
|
137
137
|
const paths = Array.isArray(arg) ? arg : [arg];
|
|
138
138
|
const node = pathsToNode(paths, modelName);
|
|
139
139
|
return nodeToPopulates(node);
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
// Exported for testing.
|
|
143
|
+
export function getDocumentParams(doc, arg, options = {}) {
|
|
144
|
+
const params = getParams(doc.constructor.modelName, arg);
|
|
145
|
+
|
|
146
|
+
if (!options.force) {
|
|
147
|
+
params.populate = params.populate.filter((p) => {
|
|
148
|
+
return !isDocumentPopulated(doc, p);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return params;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function isDocumentPopulated(doc, params) {
|
|
156
|
+
if (doc.populated(params.path)) {
|
|
157
|
+
const sub = doc.get(params.path);
|
|
158
|
+
return params.populate.every((p) => {
|
|
159
|
+
return isDocumentPopulated(sub, p);
|
|
160
|
+
});
|
|
161
|
+
} else {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
144
164
|
}
|
|
145
165
|
|
|
146
|
-
function
|
|
147
|
-
return
|
|
166
|
+
function getQueryParams(query, arg) {
|
|
167
|
+
return getParams(query.model.modelName, arg);
|
|
148
168
|
}
|
|
149
169
|
|
|
150
170
|
// Note that:
|
package/src/serialization.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { isPlainObject } from 'lodash';
|
|
2
2
|
|
|
3
3
|
import { checkSelects } from './include';
|
|
4
|
-
import { hasReadAccess } from './access';
|
|
5
4
|
import { getField, getInnerField } from './utils';
|
|
5
|
+
import { hasAccess } from './access';
|
|
6
6
|
|
|
7
7
|
const DISALLOWED_FIELDS = ['deleted', 'deletedRefs'];
|
|
8
8
|
|
|
@@ -60,7 +60,7 @@ function isAllowedField(key, field, options) {
|
|
|
60
60
|
} else {
|
|
61
61
|
const { readAccess } = getField(field, key);
|
|
62
62
|
try {
|
|
63
|
-
return
|
|
63
|
+
return hasAccess(readAccess, options);
|
|
64
64
|
} catch {
|
|
65
65
|
return false;
|
|
66
66
|
}
|
package/src/validation.js
CHANGED
|
@@ -125,6 +125,7 @@ export function applyValidation(schema, definition) {
|
|
|
125
125
|
stripTimestamps: true,
|
|
126
126
|
allowExpandedRefs: true,
|
|
127
127
|
requireWriteAccess: true,
|
|
128
|
+
updateAccess: definition.access?.update,
|
|
128
129
|
...(hasUnique && {
|
|
129
130
|
assertUniqueOptions: {
|
|
130
131
|
schema,
|
|
@@ -135,6 +136,14 @@ export function applyValidation(schema, definition) {
|
|
|
135
136
|
}
|
|
136
137
|
);
|
|
137
138
|
|
|
139
|
+
schema.static('getDeleteValidation', function getDeleteValidation() {
|
|
140
|
+
const allowed = definition.access?.delete || 'all';
|
|
141
|
+
return validateAccess('delete', yd, allowed, {
|
|
142
|
+
model: this,
|
|
143
|
+
message: 'You do not have permissions to delete this document.',
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
138
147
|
schema.static(
|
|
139
148
|
'getSearchValidation',
|
|
140
149
|
function getSearchValidation(options = {}) {
|
|
@@ -192,7 +201,8 @@ function getMongooseFields(schema, options) {
|
|
|
192
201
|
|
|
193
202
|
// Exported for testing
|
|
194
203
|
export function getValidationSchema(attributes, options = {}) {
|
|
195
|
-
const { appendSchema, assertUniqueOptions, allowInclude } =
|
|
204
|
+
const { appendSchema, assertUniqueOptions, allowInclude, updateAccess } =
|
|
205
|
+
options;
|
|
196
206
|
let schema = getObjectSchema(attributes, options);
|
|
197
207
|
if (assertUniqueOptions) {
|
|
198
208
|
schema = schema.custom(async (obj, { root }) => {
|
|
@@ -208,6 +218,14 @@ export function getValidationSchema(attributes, options = {}) {
|
|
|
208
218
|
if (allowInclude) {
|
|
209
219
|
schema = schema.append(INCLUDE_FIELD_SCHEMA);
|
|
210
220
|
}
|
|
221
|
+
|
|
222
|
+
if (updateAccess) {
|
|
223
|
+
return validateAccess('update', schema, updateAccess, {
|
|
224
|
+
...options,
|
|
225
|
+
message: 'You do not have permissions to update this document.',
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
|
|
211
229
|
return schema;
|
|
212
230
|
}
|
|
213
231
|
|
|
@@ -321,10 +339,10 @@ function getSchemaForTypedef(typedef, options = {}) {
|
|
|
321
339
|
schema = getSearchSchema(schema, type);
|
|
322
340
|
}
|
|
323
341
|
if (typedef.readAccess && options.requireReadAccess) {
|
|
324
|
-
schema =
|
|
342
|
+
schema = validateAccess('read', schema, typedef.readAccess, options);
|
|
325
343
|
}
|
|
326
344
|
if (typedef.writeAccess && options.requireWriteAccess) {
|
|
327
|
-
schema =
|
|
345
|
+
schema = validateAccess('write', schema, typedef.writeAccess, options);
|
|
328
346
|
}
|
|
329
347
|
return schema;
|
|
330
348
|
}
|
|
@@ -449,22 +467,15 @@ function isExcludedField(field, options) {
|
|
|
449
467
|
return false;
|
|
450
468
|
}
|
|
451
469
|
|
|
452
|
-
function validateReadAccess(schema, allowed, options) {
|
|
453
|
-
return validateAccess('read', schema, allowed, options);
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function validateWriteAccess(schema, allowed, options) {
|
|
457
|
-
return validateAccess('write', schema, allowed, options);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
470
|
function validateAccess(type, schema, allowed, options) {
|
|
471
|
+
let { message } = options;
|
|
461
472
|
const { modelName } = options.model;
|
|
462
473
|
return schema.custom((val, options) => {
|
|
463
474
|
const document = options[lowerFirst(modelName)] || options['document'];
|
|
464
475
|
|
|
465
476
|
let isAllowed;
|
|
466
477
|
try {
|
|
467
|
-
isAllowed = hasAccess(
|
|
478
|
+
isAllowed = hasAccess(allowed, {
|
|
468
479
|
...options,
|
|
469
480
|
document,
|
|
470
481
|
});
|
|
@@ -476,9 +487,9 @@ function validateAccess(type, schema, allowed, options) {
|
|
|
476
487
|
// against, so continue on to throw a normal permissions error
|
|
477
488
|
// here instead of raising a problem with the implementation.
|
|
478
489
|
isAllowed = false;
|
|
479
|
-
} else
|
|
490
|
+
} else {
|
|
480
491
|
throw new Error(
|
|
481
|
-
`
|
|
492
|
+
`Access validation "${error.name}" requires passing { document, authUser } to the validator.`
|
|
482
493
|
);
|
|
483
494
|
}
|
|
484
495
|
} else {
|
|
@@ -487,10 +498,17 @@ function validateAccess(type, schema, allowed, options) {
|
|
|
487
498
|
}
|
|
488
499
|
|
|
489
500
|
if (!isAllowed) {
|
|
490
|
-
const
|
|
491
|
-
if (
|
|
492
|
-
|
|
501
|
+
const { path } = options;
|
|
502
|
+
if (path) {
|
|
503
|
+
if (get(document, path) === val) {
|
|
504
|
+
// If there is a path being accessed and the current
|
|
505
|
+
// value is the same as what is being set then do not
|
|
506
|
+
// throw the error.
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
message ||= `Field "${path.join('.')}" requires ${type} permissions.`;
|
|
493
510
|
}
|
|
511
|
+
throw new PermissionsError(message);
|
|
494
512
|
}
|
|
495
513
|
});
|
|
496
514
|
}
|
package/types/access.d.ts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
export function hasReadAccess(allowed: any, options: any): boolean;
|
|
2
|
-
export function hasWriteAccess(allowed: any, options: any): boolean;
|
|
3
1
|
/**
|
|
4
2
|
* @param {string|string[]} allowed
|
|
5
3
|
*/
|
|
6
|
-
export function hasAccess(
|
|
4
|
+
export function hasAccess(allowed?: string | string[], options?: {}): boolean;
|
|
7
5
|
//# sourceMappingURL=access.d.ts.map
|
package/types/access.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../src/access.js"],"names":[],"mappings":"AAGA
|
|
1
|
+
{"version":3,"file":"access.d.ts","sourceRoot":"","sources":["../src/access.js"],"names":[],"mappings":"AAGA;;GAEG;AACH,oCAFW,MAAM,GAAC,MAAM,EAAE,yBA2BzB"}
|
package/types/include.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export function applyInclude(schema: any): void;
|
|
2
2
|
export function checkSelects(doc: any, ret: any): void;
|
|
3
|
-
export function
|
|
3
|
+
export function getParams(modelName: any, arg: any): any;
|
|
4
|
+
export function getDocumentParams(doc: any, arg: any, options?: {}): any;
|
|
4
5
|
export const INCLUDE_FIELD_SCHEMA: {
|
|
5
6
|
setup: any;
|
|
6
7
|
getFields: any;
|
package/types/include.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"include.d.ts","sourceRoot":"","sources":["../src/include.js"],"names":[],"mappings":"AA2BA,gDAuEC;AAMD,uDA4BC;AAGD,
|
|
1
|
+
{"version":3,"file":"include.d.ts","sourceRoot":"","sources":["../src/include.js"],"names":[],"mappings":"AA2BA,gDAuEC;AAMD,uDA4BC;AAGD,yDAIC;AAGD,yEAUC;AApID;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAKG"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.js"],"names":[],"mappings":"AAkFA,kDAEC;AAED,
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.js"],"names":[],"mappings":"AAkFA,kDAEC;AAED,oEA8FC;AAsBD,wEA2BC;AAgSD;;;EAEC;AAED;;;EAOC;AA9fD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAQK"}
|