@strapi/utils 4.0.0-next.9 → 4.0.3

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/policy.js CHANGED
@@ -4,40 +4,27 @@
4
4
  'use strict';
5
5
 
6
6
  const _ = require('lodash');
7
+ const { eq } = require('lodash/fp');
7
8
 
8
9
  const PLUGIN_PREFIX = 'plugin::';
9
10
  const API_PREFIX = 'api::';
10
11
 
11
- const createPolicy = (policyName, args) => ({ policyName, args });
12
-
13
- const resolveHandler = policy => {
14
- return _.has('handler', policy) ? policy.handler : policy;
15
- };
16
-
17
12
  const parsePolicy = policy => {
18
13
  if (typeof policy === 'string') {
19
- return createPolicy(policy);
14
+ return { policyName: policy, config: {} };
20
15
  }
21
16
 
22
- const { name, options = {} } = policy;
23
- return createPolicy(name, options);
24
- };
25
-
26
- const resolvePolicy = policyName => {
27
- const policy = strapi.policy(policyName);
28
-
29
- return resolveHandler(policy);
17
+ const { name, config } = policy;
18
+ return { policyName: name, config };
30
19
  };
31
20
 
32
21
  const searchLocalPolicy = (policyName, { pluginName, apiName }) => {
33
22
  if (pluginName) {
34
- const policy = strapi.policy(`${PLUGIN_PREFIX}${pluginName}.${policyName}`);
35
- return resolveHandler(policy);
23
+ return strapi.policy(`${PLUGIN_PREFIX}${pluginName}.${policyName}`);
36
24
  }
37
25
 
38
26
  if (apiName) {
39
- const policy = strapi.policy(`${API_PREFIX}${apiName}.${policyName}`);
40
- return resolveHandler(policy);
27
+ return strapi.policy(`${API_PREFIX}${apiName}.${policyName}`);
41
28
  }
42
29
  };
43
30
 
@@ -55,59 +42,87 @@ const globalPolicy = ({ method, endpoint, controller, action, plugin }) => {
55
42
  };
56
43
  };
57
44
 
58
- const bodyPolicy = async (ctx, next) => {
59
- const values = await next();
60
-
61
- if (_.isNil(ctx.body) && !_.isNil(values)) {
62
- ctx.body = values;
63
- }
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
+ });
64
52
  };
65
53
 
66
- const get = (policy, { pluginName, apiName } = {}) => {
67
- if (typeof policy === 'function') {
68
- return policy;
69
- }
70
-
71
- const { policyName, args } = parsePolicy(policy);
72
-
73
- const resolvedPolicy = resolvePolicy(policyName);
54
+ const findPolicy = (name, { pluginName, apiName } = {}) => {
55
+ const resolvedPolicy = strapi.policy(name);
74
56
 
75
57
  if (resolvedPolicy !== undefined) {
76
- return _.isPlainObject(policy) ? resolvedPolicy(args) : resolvedPolicy;
58
+ return resolvedPolicy;
77
59
  }
78
60
 
79
- const localPolicy = searchLocalPolicy(policy, { pluginName, apiName });
61
+ const localPolicy = searchLocalPolicy(name, { pluginName, apiName });
80
62
 
81
63
  if (localPolicy !== undefined) {
82
64
  return localPolicy;
83
65
  }
84
66
 
85
- throw new Error(`Could not find policy "${policy}"`);
67
+ throw new Error(`Could not find policy "${name}"`);
86
68
  };
87
69
 
88
- const createPolicyFactory = (factoryCallback, options) => {
89
- const { validator, name = 'unnamed' } = options;
70
+ const getPolicy = (policyConfig, { pluginName, apiName } = {}) => {
71
+ if (typeof policyConfig === 'function') {
72
+ return policyConfig;
73
+ }
74
+
75
+ const { policyName, config } = parsePolicy(policyConfig);
90
76
 
91
- const validate = (...args) => {
92
- try {
93
- validator(...args);
94
- } catch (e) {
95
- throw new Error(`Invalid objects submitted to "${name}" policy.`);
96
- }
97
- };
77
+ const policy = findPolicy(policyName, { pluginName, apiName });
78
+
79
+ if (typeof policy === 'function') {
80
+ return policy;
81
+ }
98
82
 
99
- return options => {
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 => {
100
94
  if (validator) {
101
- validate(options);
95
+ try {
96
+ validator(config);
97
+ } catch (e) {
98
+ throw new Error(`Invalid config passed to "${name}" policy.`);
99
+ }
102
100
  }
101
+ };
103
102
 
104
- return factoryCallback(options);
103
+ return {
104
+ name,
105
+ validator: wrappedValidator,
106
+ handler,
105
107
  };
106
108
  };
107
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
+
108
122
  module.exports = {
109
- get,
123
+ get: getPolicy,
124
+ resolve: resolvePolicies,
110
125
  globalPolicy,
111
- bodyPolicy,
112
- createPolicyFactory,
126
+ createPolicy,
127
+ createPolicyContext,
113
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,7 @@
1
+ 'use strict';
2
+
3
+ module.exports = ({ key, attribute }, { remove }) => {
4
+ if (attribute.type === 'password') {
5
+ remove(key);
6
+ }
7
+ };
@@ -0,0 +1,11 @@
1
+ 'use strict';
2
+
3
+ const { isPrivateAttribute } = require('../../content-types');
4
+
5
+ module.exports = ({ schema, key }, { remove }) => {
6
+ const isPrivate = isPrivateAttribute(schema, key);
7
+
8
+ if (isPrivate) {
9
+ remove(key);
10
+ }
11
+ };
@@ -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);