@strapi/utils 4.0.0-next.8 → 4.0.2
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 +6 -3
- package/lib/content-types.js +21 -9
- package/lib/convert-query-params.js +254 -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 +14 -14
- package/lib/pagination.js +100 -0
- package/lib/parse-multipart.js +7 -3
- package/lib/pipe-async.js +11 -0
- package/lib/policy.js +70 -129
- 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/traverse-entity.js +100 -0
- package/lib/validators.js +45 -16
- package/package.json +24 -24
- package/lib/convert-rest-query-params.js +0 -245
- package/lib/finder.js +0 -29
- package/lib/sanitize-entity.js +0 -172
package/lib/policy.js
CHANGED
|
@@ -4,61 +4,28 @@
|
|
|
4
4
|
'use strict';
|
|
5
5
|
|
|
6
6
|
const _ = require('lodash');
|
|
7
|
+
const { eq } = require('lodash/fp');
|
|
7
8
|
|
|
8
|
-
const GLOBAL_PREFIX = 'global::';
|
|
9
9
|
const PLUGIN_PREFIX = 'plugin::';
|
|
10
|
-
const
|
|
11
|
-
const APPLICATION_PREFIX = 'api::';
|
|
12
|
-
|
|
13
|
-
const getPolicyIn = (container, policy) => {
|
|
14
|
-
return (
|
|
15
|
-
_.get(container, ['config', 'policies', policy]) ||
|
|
16
|
-
_.get(container, ['config', 'policies', _.toLower(policy)])
|
|
17
|
-
);
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
const policyExistsIn = (container, policy) => !_.isUndefined(getPolicyIn(container, policy));
|
|
21
|
-
|
|
22
|
-
const createPolicy = (policyName, args) => ({ policyName, args });
|
|
23
|
-
|
|
24
|
-
const resolveHandler = policy => (_.isFunction(policy) ? policy : policy.handler);
|
|
10
|
+
const API_PREFIX = 'api::';
|
|
25
11
|
|
|
26
12
|
const parsePolicy = policy => {
|
|
27
13
|
if (typeof policy === 'string') {
|
|
28
|
-
return
|
|
14
|
+
return { policyName: policy, config: {} };
|
|
29
15
|
}
|
|
30
16
|
|
|
31
|
-
const { name,
|
|
32
|
-
return
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const resolvePolicy = policyName => {
|
|
36
|
-
const resolver = policyResolvers.find(resolver => resolver.exists(policyName));
|
|
37
|
-
|
|
38
|
-
return resolver ? resolveHandler(resolver.get)(policyName) : undefined;
|
|
17
|
+
const { name, config } = policy;
|
|
18
|
+
return { policyName: name, config };
|
|
39
19
|
};
|
|
40
20
|
|
|
41
|
-
const searchLocalPolicy = (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
const resolver = policyResolvers.find(({ name }) => name === 'plugin');
|
|
45
|
-
|
|
46
|
-
if (policyExistsIn(absoluteApi, policyName)) {
|
|
47
|
-
return resolveHandler(getPolicyIn(absoluteApi, policyName));
|
|
21
|
+
const searchLocalPolicy = (policyName, { pluginName, apiName }) => {
|
|
22
|
+
if (pluginName) {
|
|
23
|
+
return strapi.policy(`${PLUGIN_PREFIX}${pluginName}.${policyName}`);
|
|
48
24
|
}
|
|
49
25
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
if (plugin && resolver.exists(pluginPolicy)) {
|
|
53
|
-
return resolveHandler(resolver.get(pluginPolicy));
|
|
26
|
+
if (apiName) {
|
|
27
|
+
return strapi.policy(`${API_PREFIX}${apiName}.${policyName}`);
|
|
54
28
|
}
|
|
55
|
-
|
|
56
|
-
const api = _.get(strapi.api, apiName);
|
|
57
|
-
if (api && policyExistsIn(api, policy)) {
|
|
58
|
-
return resolveHandler(getPolicyIn(api, policy));
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return undefined;
|
|
62
29
|
};
|
|
63
30
|
|
|
64
31
|
const globalPolicy = ({ method, endpoint, controller, action, plugin }) => {
|
|
@@ -75,113 +42,87 @@ const globalPolicy = ({ method, endpoint, controller, action, plugin }) => {
|
|
|
75
42
|
};
|
|
76
43
|
};
|
|
77
44
|
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
+
});
|
|
84
52
|
};
|
|
85
53
|
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
name: 'api',
|
|
89
|
-
is(policy) {
|
|
90
|
-
return _.startsWith(policy, APPLICATION_PREFIX);
|
|
91
|
-
},
|
|
92
|
-
exists(policy) {
|
|
93
|
-
return this.is(policy) && !_.isUndefined(this.get(policy));
|
|
94
|
-
},
|
|
95
|
-
get: policy => {
|
|
96
|
-
const [, policyWithoutPrefix] = policy.split('::');
|
|
97
|
-
const [api = '', policyName = ''] = policyWithoutPrefix.split('.');
|
|
98
|
-
// TODO: load policies into the registry & user strapi.policy(policy)
|
|
99
|
-
return getPolicyIn(_.get(strapi, ['api', api]), policyName);
|
|
100
|
-
},
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
name: 'admin',
|
|
104
|
-
is(policy) {
|
|
105
|
-
return _.startsWith(policy, ADMIN_PREFIX);
|
|
106
|
-
},
|
|
107
|
-
exists(policy) {
|
|
108
|
-
return this.is(policy) && !_.isUndefined(this.get(policy));
|
|
109
|
-
},
|
|
110
|
-
get(policy) {
|
|
111
|
-
return strapi.policy(policy);
|
|
112
|
-
},
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
name: 'plugin',
|
|
116
|
-
is(policy) {
|
|
117
|
-
return _.startsWith(policy, PLUGIN_PREFIX);
|
|
118
|
-
},
|
|
119
|
-
exists(policy) {
|
|
120
|
-
return this.is(policy) && !_.isUndefined(this.get(policy));
|
|
121
|
-
},
|
|
122
|
-
get(policy) {
|
|
123
|
-
return strapi.policy(policy);
|
|
124
|
-
},
|
|
125
|
-
},
|
|
126
|
-
{
|
|
127
|
-
name: 'global',
|
|
128
|
-
is(policy) {
|
|
129
|
-
return _.startsWith(policy, GLOBAL_PREFIX);
|
|
130
|
-
},
|
|
131
|
-
exists(policy) {
|
|
132
|
-
return this.is(policy) && !_.isUndefined(this.get(policy));
|
|
133
|
-
},
|
|
134
|
-
get(policy) {
|
|
135
|
-
return strapi.policy(policy);
|
|
136
|
-
},
|
|
137
|
-
},
|
|
138
|
-
];
|
|
139
|
-
|
|
140
|
-
const get = (policy, plugin, apiName) => {
|
|
141
|
-
if (typeof policy === 'function') {
|
|
142
|
-
return policy;
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
const { policyName, args } = parsePolicy(policy);
|
|
146
|
-
|
|
147
|
-
const resolvedPolicy = resolvePolicy(policyName);
|
|
54
|
+
const findPolicy = (name, { pluginName, apiName } = {}) => {
|
|
55
|
+
const resolvedPolicy = strapi.policy(name);
|
|
148
56
|
|
|
149
57
|
if (resolvedPolicy !== undefined) {
|
|
150
|
-
return
|
|
58
|
+
return resolvedPolicy;
|
|
151
59
|
}
|
|
152
60
|
|
|
153
|
-
const localPolicy = searchLocalPolicy(
|
|
61
|
+
const localPolicy = searchLocalPolicy(name, { pluginName, apiName });
|
|
154
62
|
|
|
155
63
|
if (localPolicy !== undefined) {
|
|
156
64
|
return localPolicy;
|
|
157
65
|
}
|
|
158
66
|
|
|
159
|
-
throw new Error(`Could not find policy "${
|
|
67
|
+
throw new Error(`Could not find policy "${name}"`);
|
|
160
68
|
};
|
|
161
69
|
|
|
162
|
-
const
|
|
163
|
-
|
|
70
|
+
const getPolicy = (policyConfig, { pluginName, apiName } = {}) => {
|
|
71
|
+
if (typeof policyConfig === 'function') {
|
|
72
|
+
return policyConfig;
|
|
73
|
+
}
|
|
164
74
|
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
}
|
|
75
|
+
const { policyName, config } = parsePolicy(policyConfig);
|
|
76
|
+
|
|
77
|
+
const policy = findPolicy(policyName, { pluginName, apiName });
|
|
78
|
+
|
|
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
|
+
};
|
|
172
89
|
|
|
173
|
-
|
|
90
|
+
const createPolicy = options => {
|
|
91
|
+
const { name = 'unnamed', validator, handler } = options;
|
|
92
|
+
|
|
93
|
+
const wrappedValidator = config => {
|
|
174
94
|
if (validator) {
|
|
175
|
-
|
|
95
|
+
try {
|
|
96
|
+
validator(config);
|
|
97
|
+
} catch (e) {
|
|
98
|
+
throw new Error(`Invalid config passed to "${name}" policy.`);
|
|
99
|
+
}
|
|
176
100
|
}
|
|
101
|
+
};
|
|
177
102
|
|
|
178
|
-
|
|
103
|
+
return {
|
|
104
|
+
name,
|
|
105
|
+
validator: wrappedValidator,
|
|
106
|
+
handler,
|
|
179
107
|
};
|
|
180
108
|
};
|
|
181
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
|
+
|
|
182
122
|
module.exports = {
|
|
183
|
-
get,
|
|
123
|
+
get: getPolicy,
|
|
124
|
+
resolve: resolvePolicies,
|
|
184
125
|
globalPolicy,
|
|
185
|
-
|
|
186
|
-
|
|
126
|
+
createPolicy,
|
|
127
|
+
createPolicyContext,
|
|
187
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
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { isArray } = require('lodash/fp');
|
|
4
|
+
|
|
5
|
+
module.exports = (restrictedFields = null) => ({ key, path }, { remove }) => {
|
|
6
|
+
// Remove all fields
|
|
7
|
+
if (restrictedFields === null) {
|
|
8
|
+
remove(key);
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
// Ignore invalid formats
|
|
13
|
+
if (!isArray(restrictedFields)) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Remove if an exact match was found
|
|
18
|
+
if (restrictedFields.includes(path)) {
|
|
19
|
+
remove(key);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Remove nested matches
|
|
24
|
+
const isRestrictedNested = restrictedFields.some(allowedPath =>
|
|
25
|
+
path.startsWith(`${allowedPath}.`)
|
|
26
|
+
);
|
|
27
|
+
if (isRestrictedNested) {
|
|
28
|
+
remove(key);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { cloneDeep, isObject, isArray, isNil, curry } = require('lodash/fp');
|
|
4
|
+
|
|
5
|
+
const traverseEntity = async (visitor, options, entity) => {
|
|
6
|
+
const { path = null, schema } = options;
|
|
7
|
+
|
|
8
|
+
// End recursion
|
|
9
|
+
if (!isObject(entity) || isNil(schema)) {
|
|
10
|
+
return entity;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Don't mutate the original entity object
|
|
14
|
+
const copy = cloneDeep(entity);
|
|
15
|
+
|
|
16
|
+
for (const key of Object.keys(copy)) {
|
|
17
|
+
// Retrieve the attribute definition associated to the key from the schema
|
|
18
|
+
const attribute = schema.attributes[key];
|
|
19
|
+
|
|
20
|
+
// If the attribute doesn't exist within the schema, ignore it
|
|
21
|
+
if (isNil(attribute)) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const newPath = path ? `${path}.${key}` : key;
|
|
26
|
+
|
|
27
|
+
// Visit the current attribute
|
|
28
|
+
const visitorOptions = { data: copy, schema, key, value: copy[key], attribute, path: newPath };
|
|
29
|
+
const visitorUtils = createVisitorUtils({ data: copy });
|
|
30
|
+
|
|
31
|
+
await visitor(visitorOptions, visitorUtils);
|
|
32
|
+
|
|
33
|
+
// Extract the value for the current key (after calling the visitor)
|
|
34
|
+
const value = copy[key];
|
|
35
|
+
|
|
36
|
+
// Ignore Nil values
|
|
37
|
+
if (isNil(value)) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const isRelation = attribute.type === 'relation';
|
|
42
|
+
const isComponent = attribute.type === 'component';
|
|
43
|
+
const isDynamicZone = attribute.type === 'dynamiczone';
|
|
44
|
+
|
|
45
|
+
if (isRelation) {
|
|
46
|
+
const isMorphRelation = attribute.relation.toLowerCase().startsWith('morph');
|
|
47
|
+
|
|
48
|
+
const traverseTarget = entry => {
|
|
49
|
+
// Handle polymorphic relationships
|
|
50
|
+
const targetSchemaUID = isMorphRelation ? entry.__type : attribute.target;
|
|
51
|
+
const targetSchema = strapi.getModel(targetSchemaUID);
|
|
52
|
+
|
|
53
|
+
const traverseOptions = { schema: targetSchema, path: newPath };
|
|
54
|
+
|
|
55
|
+
return traverseEntity(visitor, traverseOptions, entry);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// need to update copy
|
|
59
|
+
copy[key] = isArray(value)
|
|
60
|
+
? await Promise.all(value.map(traverseTarget))
|
|
61
|
+
: await traverseTarget(value);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (isComponent) {
|
|
65
|
+
const targetSchema = strapi.getModel(attribute.component);
|
|
66
|
+
const traverseOptions = { schema: targetSchema, path: newPath };
|
|
67
|
+
|
|
68
|
+
const traverseComponent = entry => traverseEntity(visitor, traverseOptions, entry);
|
|
69
|
+
|
|
70
|
+
copy[key] = isArray(value)
|
|
71
|
+
? await Promise.all(value.map(traverseComponent))
|
|
72
|
+
: await traverseComponent(value);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (isDynamicZone && isArray(value)) {
|
|
76
|
+
const visitDynamicZoneEntry = entry => {
|
|
77
|
+
const targetSchema = strapi.getModel(entry.__component);
|
|
78
|
+
const traverseOptions = { schema: targetSchema, path: newPath };
|
|
79
|
+
|
|
80
|
+
return traverseEntity(visitor, traverseOptions, entry);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
copy[key] = await Promise.all(value.map(visitDynamicZoneEntry));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return copy;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const createVisitorUtils = ({ data }) => ({
|
|
91
|
+
remove(key) {
|
|
92
|
+
delete data[key];
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
set(key, value) {
|
|
96
|
+
data[key] = value;
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
module.exports = curry(traverseEntity);
|