@strapi/utils 4.0.0-next.6 → 4.0.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/lib/build-query.js +1 -1
- package/lib/config.js +10 -6
- package/lib/content-types.js +23 -116
- package/lib/convert-query-params.js +252 -0
- package/lib/errors.js +82 -0
- package/lib/format-yup-error.js +20 -0
- package/lib/hooks.js +5 -3
- package/lib/index.js +18 -14
- package/lib/pagination.js +88 -0
- package/lib/parse-multipart.js +7 -3
- package/lib/pipe-async.js +11 -0
- package/lib/policy.js +75 -119
- package/lib/print-value.js +51 -0
- package/lib/sanitize/index.js +51 -0
- package/lib/sanitize/sanitizers.js +26 -0
- package/lib/sanitize/visitors/allowed-fields.js +92 -0
- package/lib/sanitize/visitors/index.js +9 -0
- package/lib/sanitize/visitors/remove-password.js +7 -0
- package/lib/sanitize/visitors/remove-private.js +11 -0
- package/lib/sanitize/visitors/remove-restricted-relations.js +67 -0
- package/lib/sanitize/visitors/restricted-fields.js +31 -0
- package/lib/string-formatting.js +4 -0
- package/lib/traverse-entity.js +100 -0
- package/lib/validators.js +70 -16
- package/package.json +21 -24
- package/lib/convert-rest-query-params.js +0 -243
- package/lib/finder.js +0 -32
- package/lib/sanitize-entity.js +0 -172
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { merge, pipe, omit, isNil } = require('lodash/fp');
|
|
4
|
+
const { PaginationError } = require('./errors');
|
|
5
|
+
|
|
6
|
+
const STRAPI_DEFAULTS = {
|
|
7
|
+
offset: {
|
|
8
|
+
start: 0,
|
|
9
|
+
limit: 10,
|
|
10
|
+
},
|
|
11
|
+
page: {
|
|
12
|
+
page: 1,
|
|
13
|
+
pageSize: 10,
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const paginationAttributes = ['start', 'limit', 'page', 'pageSize'];
|
|
18
|
+
|
|
19
|
+
const withMaxLimit = (limit, maxLimit = -1) => {
|
|
20
|
+
if (maxLimit === -1 || limit < maxLimit) {
|
|
21
|
+
return limit;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return maxLimit;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Ensure minimum page & pageSize values (page >= 1, pageSize >= 0, start >= 0, limit >= 0)
|
|
28
|
+
const ensureMinValues = ({ start, limit }) => ({
|
|
29
|
+
start: Math.max(start, 0),
|
|
30
|
+
limit: Math.max(limit, 1),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const ensureMaxValues = (maxLimit = -1) => ({ start, limit }) => ({
|
|
34
|
+
start,
|
|
35
|
+
limit: withMaxLimit(limit, maxLimit),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const withDefaultPagination = (args, { defaults = {}, maxLimit = -1 } = {}) => {
|
|
39
|
+
const defaultValues = merge(STRAPI_DEFAULTS, defaults);
|
|
40
|
+
|
|
41
|
+
const usePagePagination = !isNil(args.page) || !isNil(args.pageSize);
|
|
42
|
+
const useOffsetPagination = !isNil(args.start) || !isNil(args.limit);
|
|
43
|
+
|
|
44
|
+
const ensureValidValues = pipe(
|
|
45
|
+
ensureMinValues,
|
|
46
|
+
ensureMaxValues(maxLimit)
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// If there is no pagination attribute, don't modify the payload
|
|
50
|
+
if (!usePagePagination && !useOffsetPagination) {
|
|
51
|
+
return merge(args, ensureValidValues(defaultValues.offset));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// If there is page & offset pagination attributes, throw an error
|
|
55
|
+
if (usePagePagination && useOffsetPagination) {
|
|
56
|
+
throw new PaginationError('Cannot use both page & offset pagination in the same query');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const pagination = {};
|
|
60
|
+
|
|
61
|
+
// Start / Limit
|
|
62
|
+
if (useOffsetPagination) {
|
|
63
|
+
const { start, limit } = merge(defaultValues.offset, args);
|
|
64
|
+
|
|
65
|
+
Object.assign(pagination, { start, limit });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Page / PageSize
|
|
69
|
+
if (usePagePagination) {
|
|
70
|
+
const { page, pageSize } = merge(defaultValues.page, args);
|
|
71
|
+
|
|
72
|
+
Object.assign(pagination, {
|
|
73
|
+
start: (page - 1) * pageSize,
|
|
74
|
+
limit: pageSize,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const replacePaginationAttributes = pipe(
|
|
79
|
+
// Remove pagination attributes
|
|
80
|
+
omit(paginationAttributes),
|
|
81
|
+
// Merge the object with the new pagination + ensure minimum & maximum values
|
|
82
|
+
merge(ensureValidValues(pagination))
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
return replacePaginationAttributes(args);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
module.exports = { withDefaultPagination };
|
package/lib/parse-multipart.js
CHANGED
|
@@ -3,10 +3,14 @@
|
|
|
3
3
|
const _ = require('lodash');
|
|
4
4
|
|
|
5
5
|
module.exports = ctx => {
|
|
6
|
+
if (!ctx.is('multipart')) {
|
|
7
|
+
return { data: ctx.request.body, files: {} };
|
|
8
|
+
}
|
|
9
|
+
|
|
6
10
|
const { body = {}, files = {} } = ctx.request;
|
|
7
11
|
|
|
8
12
|
if (!body.data) {
|
|
9
|
-
|
|
13
|
+
return ctx.badRequest(
|
|
10
14
|
`When using multipart/form-data you need to provide your data in a JSON 'data' field.`
|
|
11
15
|
);
|
|
12
16
|
}
|
|
@@ -15,14 +19,14 @@ module.exports = ctx => {
|
|
|
15
19
|
try {
|
|
16
20
|
data = JSON.parse(body.data);
|
|
17
21
|
} catch (error) {
|
|
18
|
-
|
|
22
|
+
return ctx.badRequest(`Invalid 'data' field. 'data' should be a valid JSON.`);
|
|
19
23
|
}
|
|
20
24
|
|
|
21
25
|
const filesToUpload = Object.keys(files).reduce((acc, key) => {
|
|
22
26
|
const fullPath = _.toPath(key);
|
|
23
27
|
|
|
24
28
|
if (fullPath.length <= 1 || fullPath[0] !== 'files') {
|
|
25
|
-
|
|
29
|
+
return ctx.badRequest(
|
|
26
30
|
`When using multipart/form-data you need to provide your files by prefixing them with the 'files'.
|
|
27
31
|
For example, when a media file is named "avatar", make sure the form key name is "files.avatar"`
|
|
28
32
|
);
|
package/lib/policy.js
CHANGED
|
@@ -4,59 +4,28 @@
|
|
|
4
4
|
'use strict';
|
|
5
5
|
|
|
6
6
|
const _ = require('lodash');
|
|
7
|
+
const { eq } = require('lodash/fp');
|
|
7
8
|
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const ADMIN_PREFIX = 'admin::';
|
|
11
|
-
const APPLICATION_PREFIX = 'application::';
|
|
9
|
+
const PLUGIN_PREFIX = 'plugin::';
|
|
10
|
+
const API_PREFIX = 'api::';
|
|
12
11
|
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return (
|
|
17
|
-
_.get(container, ['config', 'policies', policy]) ||
|
|
18
|
-
_.get(container, ['config', 'policies', _.toLower(policy)])
|
|
19
|
-
);
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const policyExistsIn = (container, policy) => !_.isUndefined(getPolicyIn(container, policy));
|
|
23
|
-
|
|
24
|
-
const stripPolicy = (policy, prefix) => policy.replace(prefix, '');
|
|
25
|
-
|
|
26
|
-
const createPolicy = (policyName, ...args) => ({ policyName, args });
|
|
27
|
-
|
|
28
|
-
const resolveHandler = policy => (_.isFunction(policy) ? policy : policy.handler);
|
|
29
|
-
|
|
30
|
-
const parsePolicy = policy =>
|
|
31
|
-
isPolicyFactory(policy) ? createPolicy(...policy) : createPolicy(policy);
|
|
32
|
-
|
|
33
|
-
const resolvePolicy = policyName => {
|
|
34
|
-
const resolver = policyResolvers.find(resolver => resolver.exists(policyName));
|
|
35
|
-
|
|
36
|
-
return resolver ? resolveHandler(resolver.get)(policyName) : undefined;
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
const searchLocalPolicy = (policy, plugin, apiName) => {
|
|
40
|
-
let [absoluteApiName, policyName] = policy.split('.');
|
|
41
|
-
let absoluteApi = _.get(strapi.api, absoluteApiName);
|
|
42
|
-
const resolver = policyResolvers.find(({ name }) => name === 'plugin');
|
|
43
|
-
|
|
44
|
-
if (policyExistsIn(absoluteApi, policyName)) {
|
|
45
|
-
return resolveHandler(getPolicyIn(absoluteApi, policyName));
|
|
12
|
+
const parsePolicy = policy => {
|
|
13
|
+
if (typeof policy === 'string') {
|
|
14
|
+
return { policyName: policy, config: {} };
|
|
46
15
|
}
|
|
47
16
|
|
|
48
|
-
const
|
|
17
|
+
const { name, config } = policy;
|
|
18
|
+
return { policyName: name, config };
|
|
19
|
+
};
|
|
49
20
|
|
|
50
|
-
|
|
51
|
-
|
|
21
|
+
const searchLocalPolicy = (policyName, { pluginName, apiName }) => {
|
|
22
|
+
if (pluginName) {
|
|
23
|
+
return strapi.policy(`${PLUGIN_PREFIX}${pluginName}.${policyName}`);
|
|
52
24
|
}
|
|
53
25
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
return resolveHandler(getPolicyIn(api, policy));
|
|
26
|
+
if (apiName) {
|
|
27
|
+
return strapi.policy(`${API_PREFIX}${apiName}.${policyName}`);
|
|
57
28
|
}
|
|
58
|
-
|
|
59
|
-
return undefined;
|
|
60
29
|
};
|
|
61
30
|
|
|
62
31
|
const globalPolicy = ({ method, endpoint, controller, action, plugin }) => {
|
|
@@ -73,100 +42,87 @@ const globalPolicy = ({ method, endpoint, controller, action, plugin }) => {
|
|
|
73
42
|
};
|
|
74
43
|
};
|
|
75
44
|
|
|
76
|
-
const
|
|
77
|
-
{
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
},
|
|
85
|
-
get: policy => {
|
|
86
|
-
const [, policyWithoutPrefix] = policy.split('::');
|
|
87
|
-
const [api = '', policyName = ''] = policyWithoutPrefix.split('.');
|
|
88
|
-
return getPolicyIn(_.get(strapi, ['api', api]), policyName);
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
name: 'admin',
|
|
93
|
-
is(policy) {
|
|
94
|
-
return _.startsWith(policy, ADMIN_PREFIX);
|
|
95
|
-
},
|
|
96
|
-
exists(policy) {
|
|
97
|
-
return this.is(policy) && !_.isUndefined(this.get(policy));
|
|
98
|
-
},
|
|
99
|
-
get: policy => {
|
|
100
|
-
return getPolicyIn(_.get(strapi, 'admin'), stripPolicy(policy, ADMIN_PREFIX));
|
|
101
|
-
},
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
name: 'plugin',
|
|
105
|
-
is(policy) {
|
|
106
|
-
return _.startsWith(policy, PLUGIN_PREFIX);
|
|
107
|
-
},
|
|
108
|
-
exists(policy) {
|
|
109
|
-
return this.is(policy) && !_.isUndefined(this.get(policy));
|
|
110
|
-
},
|
|
111
|
-
get(policy) {
|
|
112
|
-
const [plugin = '', policyName = ''] = stripPolicy(policy, PLUGIN_PREFIX).split('.');
|
|
113
|
-
return getPolicyIn(_.get(strapi, ['plugins', plugin]), policyName);
|
|
114
|
-
},
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
name: 'global',
|
|
118
|
-
is(policy) {
|
|
119
|
-
return _.startsWith(policy, GLOBAL_PREFIX);
|
|
120
|
-
},
|
|
121
|
-
exists(policy) {
|
|
122
|
-
return this.is(policy) && !_.isUndefined(this.get(policy));
|
|
123
|
-
},
|
|
124
|
-
get(policy) {
|
|
125
|
-
return getPolicyIn(strapi, stripPolicy(policy, GLOBAL_PREFIX));
|
|
126
|
-
},
|
|
127
|
-
},
|
|
128
|
-
];
|
|
129
|
-
|
|
130
|
-
const get = (policy, plugin, apiName) => {
|
|
131
|
-
const { policyName, args } = parsePolicy(policy);
|
|
45
|
+
const resolvePolicies = (config, { pluginName, apiName } = {}) => {
|
|
46
|
+
return config.map(policyConfig => {
|
|
47
|
+
return {
|
|
48
|
+
handler: getPolicy(policyConfig, { pluginName, apiName }),
|
|
49
|
+
config: policyConfig.config || {},
|
|
50
|
+
};
|
|
51
|
+
});
|
|
52
|
+
};
|
|
132
53
|
|
|
133
|
-
|
|
54
|
+
const findPolicy = (name, { pluginName, apiName } = {}) => {
|
|
55
|
+
const resolvedPolicy = strapi.policy(name);
|
|
134
56
|
|
|
135
57
|
if (resolvedPolicy !== undefined) {
|
|
136
|
-
return
|
|
58
|
+
return resolvedPolicy;
|
|
137
59
|
}
|
|
138
60
|
|
|
139
|
-
const localPolicy = searchLocalPolicy(
|
|
61
|
+
const localPolicy = searchLocalPolicy(name, { pluginName, apiName });
|
|
140
62
|
|
|
141
63
|
if (localPolicy !== undefined) {
|
|
142
64
|
return localPolicy;
|
|
143
65
|
}
|
|
144
66
|
|
|
145
|
-
throw new Error(`Could not find policy "${
|
|
67
|
+
throw new Error(`Could not find policy "${name}"`);
|
|
146
68
|
};
|
|
147
69
|
|
|
148
|
-
const
|
|
149
|
-
|
|
70
|
+
const getPolicy = (policyConfig, { pluginName, apiName } = {}) => {
|
|
71
|
+
if (typeof policyConfig === 'function') {
|
|
72
|
+
return policyConfig;
|
|
73
|
+
}
|
|
150
74
|
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
} catch (e) {
|
|
155
|
-
throw new Error(`Invalid objects submitted to "${name}" policy.`);
|
|
156
|
-
}
|
|
157
|
-
};
|
|
75
|
+
const { policyName, config } = parsePolicy(policyConfig);
|
|
76
|
+
|
|
77
|
+
const policy = findPolicy(policyName, { pluginName, apiName });
|
|
158
78
|
|
|
159
|
-
|
|
79
|
+
if (typeof policy === 'function') {
|
|
80
|
+
return policy;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (policy.validator) {
|
|
84
|
+
policy.validator(config);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return policy.handler;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const createPolicy = options => {
|
|
91
|
+
const { name = 'unnamed', validator, handler } = options;
|
|
92
|
+
|
|
93
|
+
const wrappedValidator = config => {
|
|
160
94
|
if (validator) {
|
|
161
|
-
|
|
95
|
+
try {
|
|
96
|
+
validator(config);
|
|
97
|
+
} catch (e) {
|
|
98
|
+
throw new Error(`Invalid config passed to "${name}" policy.`);
|
|
99
|
+
}
|
|
162
100
|
}
|
|
101
|
+
};
|
|
163
102
|
|
|
164
|
-
|
|
103
|
+
return {
|
|
104
|
+
name,
|
|
105
|
+
validator: wrappedValidator,
|
|
106
|
+
handler,
|
|
165
107
|
};
|
|
166
108
|
};
|
|
167
109
|
|
|
110
|
+
const createPolicyContext = (type, ctx) => {
|
|
111
|
+
return Object.assign(
|
|
112
|
+
{
|
|
113
|
+
is: eq(type),
|
|
114
|
+
get type() {
|
|
115
|
+
return type;
|
|
116
|
+
},
|
|
117
|
+
},
|
|
118
|
+
ctx
|
|
119
|
+
);
|
|
120
|
+
};
|
|
121
|
+
|
|
168
122
|
module.exports = {
|
|
169
|
-
get,
|
|
123
|
+
get: getPolicy,
|
|
124
|
+
resolve: resolvePolicies,
|
|
170
125
|
globalPolicy,
|
|
171
|
-
|
|
126
|
+
createPolicy,
|
|
127
|
+
createPolicyContext,
|
|
172
128
|
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Code copied from the yup library (https://github.com/jquense/yup)
|
|
4
|
+
// https://github.com/jquense/yup/blob/2778b88bdacd5260d593c6468793da2e77daf21f/src/util/printValue.ts
|
|
5
|
+
|
|
6
|
+
const toString = Object.prototype.toString;
|
|
7
|
+
const errorToString = Error.prototype.toString;
|
|
8
|
+
const regExpToString = RegExp.prototype.toString;
|
|
9
|
+
const symbolToString = typeof Symbol !== 'undefined' ? Symbol.prototype.toString : () => '';
|
|
10
|
+
|
|
11
|
+
const SYMBOL_REGEXP = /^Symbol\((.*)\)(.*)$/;
|
|
12
|
+
|
|
13
|
+
function printNumber(val) {
|
|
14
|
+
if (val != +val) return 'NaN';
|
|
15
|
+
const isNegativeZero = val === 0 && 1 / val < 0;
|
|
16
|
+
return isNegativeZero ? '-0' : '' + val;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function printSimpleValue(val, quoteStrings = false) {
|
|
20
|
+
if (val == null || val === true || val === false) return '' + val;
|
|
21
|
+
|
|
22
|
+
const typeOf = typeof val;
|
|
23
|
+
if (typeOf === 'number') return printNumber(val);
|
|
24
|
+
if (typeOf === 'string') return quoteStrings ? `"${val}"` : val;
|
|
25
|
+
if (typeOf === 'function') return '[Function ' + (val.name || 'anonymous') + ']';
|
|
26
|
+
if (typeOf === 'symbol') return symbolToString.call(val).replace(SYMBOL_REGEXP, 'Symbol($1)');
|
|
27
|
+
|
|
28
|
+
const tag = toString.call(val).slice(8, -1);
|
|
29
|
+
if (tag === 'Date') return isNaN(val.getTime()) ? '' + val : val.toISOString(val);
|
|
30
|
+
if (tag === 'Error' || val instanceof Error) return '[' + errorToString.call(val) + ']';
|
|
31
|
+
if (tag === 'RegExp') return regExpToString.call(val);
|
|
32
|
+
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function printValue(value, quoteStrings) {
|
|
37
|
+
let result = printSimpleValue(value, quoteStrings);
|
|
38
|
+
if (result !== null) return result;
|
|
39
|
+
|
|
40
|
+
return JSON.stringify(
|
|
41
|
+
value,
|
|
42
|
+
function(key, value) {
|
|
43
|
+
let result = printSimpleValue(this[key], quoteStrings);
|
|
44
|
+
if (result !== null) return result;
|
|
45
|
+
return value;
|
|
46
|
+
},
|
|
47
|
+
2
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = printValue;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { isArray } = require('lodash/fp');
|
|
4
|
+
|
|
5
|
+
const traverseEntity = require('../traverse-entity');
|
|
6
|
+
const { getNonWritableAttributes } = require('../content-types');
|
|
7
|
+
const pipeAsync = require('../pipe-async');
|
|
8
|
+
|
|
9
|
+
const visitors = require('./visitors');
|
|
10
|
+
const sanitizers = require('./sanitizers');
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
contentAPI: {
|
|
14
|
+
input(data, schema, { auth } = {}) {
|
|
15
|
+
if (isArray(data)) {
|
|
16
|
+
return Promise.all(data.map(entry => this.input(entry, schema, { auth })));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const nonWritableAttributes = getNonWritableAttributes(schema);
|
|
20
|
+
|
|
21
|
+
const transforms = [
|
|
22
|
+
// Remove non writable attributes
|
|
23
|
+
traverseEntity(visitors.restrictedFields(nonWritableAttributes), { schema }),
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
if (auth) {
|
|
27
|
+
// Remove restricted relations
|
|
28
|
+
transforms.push(traverseEntity(visitors.removeRestrictedRelations(auth), { schema }));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return pipeAsync(...transforms)(data);
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
output(data, schema, { auth } = {}) {
|
|
35
|
+
if (isArray(data)) {
|
|
36
|
+
return Promise.all(data.map(entry => this.output(entry, schema, { auth })));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const transforms = [sanitizers.defaultSanitizeOutput(schema)];
|
|
40
|
+
|
|
41
|
+
if (auth) {
|
|
42
|
+
transforms.push(traverseEntity(visitors.removeRestrictedRelations(auth), { schema }));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return pipeAsync(...transforms)(data);
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
|
|
49
|
+
sanitizers,
|
|
50
|
+
visitors,
|
|
51
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { curry } = require('lodash/fp');
|
|
4
|
+
|
|
5
|
+
const pipeAsync = require('../pipe-async');
|
|
6
|
+
const traverseEntity = require('../traverse-entity');
|
|
7
|
+
|
|
8
|
+
const { removePassword, removePrivate } = require('./visitors');
|
|
9
|
+
|
|
10
|
+
const sanitizePasswords = curry((schema, entity) => {
|
|
11
|
+
return traverseEntity(removePassword, { schema }, entity);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const sanitizePrivates = curry((schema, entity) => {
|
|
15
|
+
return traverseEntity(removePrivate, { schema }, entity);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const defaultSanitizeOutput = curry((schema, entity) => {
|
|
19
|
+
return pipeAsync(sanitizePrivates(schema), sanitizePasswords(schema))(entity);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
module.exports = {
|
|
23
|
+
sanitizePasswords,
|
|
24
|
+
sanitizePrivates,
|
|
25
|
+
defaultSanitizeOutput,
|
|
26
|
+
};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { isArray, toPath } = require('lodash/fp');
|
|
4
|
+
|
|
5
|
+
module.exports = (allowedFields = null) => ({ key, path }, { remove }) => {
|
|
6
|
+
// All fields are allowed
|
|
7
|
+
if (allowedFields === null) {
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Ignore invalid formats
|
|
12
|
+
if (!isArray(allowedFields)) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const containedPaths = getContainedPaths(path);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Tells if the current path should be kept or not based
|
|
20
|
+
* on the success of the check functions for any of the allowed paths.
|
|
21
|
+
*
|
|
22
|
+
* The check functions are defined as follow:
|
|
23
|
+
*
|
|
24
|
+
* `containedPaths.includes(p)`
|
|
25
|
+
* @example
|
|
26
|
+
* ```js
|
|
27
|
+
* const path = 'foo.bar.field';
|
|
28
|
+
* const p = 'foo.bar';
|
|
29
|
+
* // it should match
|
|
30
|
+
*
|
|
31
|
+
* const path = 'foo.bar.field';
|
|
32
|
+
* const p = 'bar.foo';
|
|
33
|
+
* // it shouldn't match
|
|
34
|
+
*
|
|
35
|
+
* const path = 'foo.bar';
|
|
36
|
+
* const p = 'foo.bar.field';
|
|
37
|
+
* // it should match but isn't handled by this check
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* `p.startsWith(`${path}.`)`
|
|
41
|
+
* @example
|
|
42
|
+
* ```js
|
|
43
|
+
* const path = 'foo.bar';
|
|
44
|
+
* const p = 'foo.bar.field';
|
|
45
|
+
* // it should match
|
|
46
|
+
*
|
|
47
|
+
* const path = 'foo.bar.field';
|
|
48
|
+
* const p = 'bar.foo';
|
|
49
|
+
* // it shouldn't match
|
|
50
|
+
*
|
|
51
|
+
* const path = 'foo.bar.field';
|
|
52
|
+
* const p = 'foo.bar';
|
|
53
|
+
* // it should match but isn't handled by this check
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
const isPathAllowed = allowedFields.some(
|
|
57
|
+
p => containedPaths.includes(p) || p.startsWith(`${path}.`)
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
if (isPathAllowed) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Remove otherwise
|
|
65
|
+
remove(key);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Retrieve the list of allowed paths based on the given path
|
|
70
|
+
*
|
|
71
|
+
* @param {string} path
|
|
72
|
+
* @return {string[]}
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```js
|
|
76
|
+
* const containedPaths = getContainedPaths('foo');
|
|
77
|
+
* // ['foo']
|
|
78
|
+
*
|
|
79
|
+
* * const containedPaths = getContainedPaths('foo.bar');
|
|
80
|
+
* // ['foo', 'foo.bar']
|
|
81
|
+
*
|
|
82
|
+
* * const containedPaths = getContainedPaths('foo.bar.field');
|
|
83
|
+
* // ['foo', 'foo.bar', 'foo.bar.field']
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
const getContainedPaths = path => {
|
|
87
|
+
const parts = toPath(path);
|
|
88
|
+
|
|
89
|
+
return parts.reduce((acc, value, index, list) => {
|
|
90
|
+
return [...acc, list.slice(0, index + 1).join('.')];
|
|
91
|
+
}, []);
|
|
92
|
+
};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
removePassword: require('./remove-password'),
|
|
5
|
+
removePrivate: require('./remove-private'),
|
|
6
|
+
removeRestrictedRelations: require('./remove-restricted-relations'),
|
|
7
|
+
allowedFields: require('./allowed-fields'),
|
|
8
|
+
restrictedFields: require('./restricted-fields'),
|
|
9
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ACTIONS_TO_VERIFY = ['find'];
|
|
4
|
+
|
|
5
|
+
module.exports = auth => async ({ data, key, attribute }, { remove, set }) => {
|
|
6
|
+
const isRelation = attribute.type === 'relation';
|
|
7
|
+
|
|
8
|
+
if (!isRelation) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const handleMorphRelation = async () => {
|
|
13
|
+
const newMorphValue = [];
|
|
14
|
+
|
|
15
|
+
for (const element of data[key]) {
|
|
16
|
+
const scopes = ACTIONS_TO_VERIFY.map(action => `${element.__type}.${action}`);
|
|
17
|
+
const isAllowed = await hasAccessToSomeScopes(scopes, auth);
|
|
18
|
+
|
|
19
|
+
if (isAllowed) {
|
|
20
|
+
newMorphValue.push(element);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// If the new value is empty, remove the relation completely
|
|
25
|
+
if (newMorphValue.length === 0) {
|
|
26
|
+
remove(key);
|
|
27
|
+
} else {
|
|
28
|
+
set(key, newMorphValue);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const handleRegularRelation = async () => {
|
|
33
|
+
const scopes = ACTIONS_TO_VERIFY.map(action => `${attribute.target}.${action}`);
|
|
34
|
+
|
|
35
|
+
const isAllowed = await hasAccessToSomeScopes(scopes, auth);
|
|
36
|
+
|
|
37
|
+
// If the authenticated user don't have access to any of the scopes, then remove the field
|
|
38
|
+
if (!isAllowed) {
|
|
39
|
+
remove(key);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const isMorphRelation = attribute.relation.toLowerCase().startsWith('morph');
|
|
44
|
+
|
|
45
|
+
// Polymorphic relations
|
|
46
|
+
if (isMorphRelation) {
|
|
47
|
+
await handleMorphRelation();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Regular relations
|
|
51
|
+
else {
|
|
52
|
+
await handleRegularRelation();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const hasAccessToSomeScopes = async (scopes, auth) => {
|
|
57
|
+
for (const scope of scopes) {
|
|
58
|
+
try {
|
|
59
|
+
await strapi.auth.verify(auth, { scope });
|
|
60
|
+
return true;
|
|
61
|
+
} catch {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return false;
|
|
67
|
+
};
|