From 2f8169c792e54e6f87a6ad1aab65f68c97b395fe Mon Sep 17 00:00:00 2001 From: Aurelsicoko Date: Tue, 6 Feb 2018 18:09:04 +0100 Subject: [PATCH 01/11] Hide specific model fields from response --- .../strapi-plugin-users-permissions/models/User.settings.json | 3 ++- packages/strapi/lib/core/configurations.js | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/strapi-plugin-users-permissions/models/User.settings.json b/packages/strapi-plugin-users-permissions/models/User.settings.json index e64b741cbb..f1aa1b2b13 100644 --- a/packages/strapi-plugin-users-permissions/models/User.settings.json +++ b/packages/strapi-plugin-users-permissions/models/User.settings.json @@ -25,7 +25,8 @@ "password": { "type": "password", "minLength": 6, - "configurable": false + "configurable": false, + "private": true }, "resetPasswordToken": { "type": "string", diff --git a/packages/strapi/lib/core/configurations.js b/packages/strapi/lib/core/configurations.js index 97e38a3411..4d6005b159 100755 --- a/packages/strapi/lib/core/configurations.js +++ b/packages/strapi/lib/core/configurations.js @@ -244,6 +244,9 @@ module.exports.app = async function() { boom: { enabled: true }, + mask: { + enabled: true + }, // Necessary middlewares for the administration panel. cors: { enabled: true From c383397d6e044445269f7846c96875a01c406c6c Mon Sep 17 00:00:00 2001 From: Aurelsicoko Date: Tue, 6 Feb 2018 18:18:35 +0100 Subject: [PATCH 02/11] Add mask middleware --- .../strapi/lib/middlewares/mask/defaults.json | 5 + packages/strapi/lib/middlewares/mask/index.js | 126 ++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 packages/strapi/lib/middlewares/mask/defaults.json create mode 100644 packages/strapi/lib/middlewares/mask/index.js diff --git a/packages/strapi/lib/middlewares/mask/defaults.json b/packages/strapi/lib/middlewares/mask/defaults.json new file mode 100644 index 0000000000..f9e6a509bc --- /dev/null +++ b/packages/strapi/lib/middlewares/mask/defaults.json @@ -0,0 +1,5 @@ +{ + "ip": { + "enabled": false + } +} diff --git a/packages/strapi/lib/middlewares/mask/index.js b/packages/strapi/lib/middlewares/mask/index.js new file mode 100644 index 0000000000..b643f5e7e7 --- /dev/null +++ b/packages/strapi/lib/middlewares/mask/index.js @@ -0,0 +1,126 @@ +'use strict'; + +/** + * Module dependencies + */ + +/** + * Mask filter middleware + */ + +const _ = require('lodash'); + +module.exports = strapi => { + return { + /** + * Initialize the hook + */ + + initialize: function (cb) { + strapi.app.use(async (ctx, next) => { + // Execute next middleware. + await next(); + + const start = Date.now(); + + // Array or plain object + if (_.isArray(ctx.body) || _.isPlainObject(ctx.body) && ctx.status === 200) { + // Array. + if (_.isArray(ctx.body)) { + ctx.body = ctx.body.map(value => { + if (_.isPlainObject(value)) { + console.log(this.mask(ctx, value)); + return this.mask(ctx, value); + } + + // Raw + return obj; + }); + } + + // Plain object. + ctx.body = this.mask(ctx, ctx.body); + } + }); + + cb(); + }, + + mask: function (ctx, value) { + const models = this.filteredModels(this.whichModels(value, ctx.request.route.plugin)); + + if (models.length === 0) { + return value; + } + + const attributesToHide = models.reduce((acc, match) => { + const attributes = match.plugin ? + strapi.plugins[match.plugin].models[match.model].attributes: + strapi.models[match.model].attributes; + + acc = acc.concat(Object.keys(attributes).filter(attr => attributes[attr].private === true)); + + return acc; + }, []); + + // Hide attribute. + return _.omit(value, attributesToHide); + }, + + whichModels: function (value, plugin) { + const keys = Object.keys(value); + let maxMatch = 0; + let matchs = []; + + + const match = (model, plugin) => { + const attributes = plugin ? + Object.keys(strapi.plugins[plugin].models[model].attributes): + Object.keys(strapi.models[model].attributes); + + const intersection = _.intersection(keys, attributes.filter(attr => ['id', '_id', '_v'].indexOf(attr) === -1 )).length; + + // Most matched model. + if (intersection > maxMatch) { + maxMatch = intersection; + matchs = [{ + plugin, + model, + intersection + }]; + } else if (intersection === maxMatch && intersection > 0) { + matchs.push({ + plugin, + model, + intersection + }); + } + }; + + // Application models. + Object.keys(strapi.models).forEach(model => match(model)); + // Plugins models. + Object.keys(strapi.plugins).forEach(plugin => { + Object.keys(strapi.plugins[plugin].models).forEach(model => match(model, plugin)); + }); + + return matchs; + }, + + filteredModels: function (matchs) { + return matchs.reduce((acc, match, index) => { + const attributes = match.plugin ? + strapi.plugins[match.plugin].models[match.model].attributes: + strapi.models[match.model].attributes; + + // Filtered model which have more than half of the attributes in common + // with the original model. + if (match.intersection >= Object.keys(attributes).length / 2) { + acc[index] = match; + } + + return acc; + }, []); + } + }; +}; From ed4dc1c7f729d25e11eb0e0c73405daad295ec8d Mon Sep 17 00:00:00 2001 From: Aurelsicoko Date: Tue, 6 Feb 2018 18:19:50 +0100 Subject: [PATCH 03/11] Fix typo --- packages/strapi/lib/middlewares/mask/defaults.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/strapi/lib/middlewares/mask/defaults.json b/packages/strapi/lib/middlewares/mask/defaults.json index f9e6a509bc..8fc1f914ab 100644 --- a/packages/strapi/lib/middlewares/mask/defaults.json +++ b/packages/strapi/lib/middlewares/mask/defaults.json @@ -1,5 +1,5 @@ { - "ip": { + "mask": { "enabled": false } } From 6adca3dd1e965f2ddfc6325901548cead97d31d7 Mon Sep 17 00:00:00 2001 From: Aurelsicoko Date: Tue, 6 Feb 2018 18:23:42 +0100 Subject: [PATCH 04/11] Clean spaces and logs --- packages/strapi/lib/middlewares/mask/index.js | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/strapi/lib/middlewares/mask/index.js b/packages/strapi/lib/middlewares/mask/index.js index b643f5e7e7..d3240a630f 100644 --- a/packages/strapi/lib/middlewares/mask/index.js +++ b/packages/strapi/lib/middlewares/mask/index.js @@ -21,25 +21,21 @@ module.exports = strapi => { // Execute next middleware. await next(); - const start = Date.now(); - - // Array or plain object - if (_.isArray(ctx.body) || _.isPlainObject(ctx.body) && ctx.status === 200) { - // Array. + if (ctx.status === 200) { + // Array if (_.isArray(ctx.body)) { ctx.body = ctx.body.map(value => { if (_.isPlainObject(value)) { - console.log(this.mask(ctx, value)); return this.mask(ctx, value); } // Raw return obj; }); + } else if (_.isPlainObject(ctx.body)) { + // Plain object. + ctx.body = this.mask(ctx, ctx.body); } - - // Plain object. - ctx.body = this.mask(ctx, ctx.body); } }); @@ -72,7 +68,6 @@ module.exports = strapi => { let maxMatch = 0; let matchs = []; - const match = (model, plugin) => { const attributes = plugin ? Object.keys(strapi.plugins[plugin].models[model].attributes): From cbd47828d8d3c4795855cdde68ee148cacd0be16 Mon Sep 17 00:00:00 2001 From: Aurelsicoko Date: Thu, 8 Feb 2018 14:58:19 +0100 Subject: [PATCH 05/11] Handle mask for nested objects (associations) --- packages/strapi/lib/middlewares/mask/index.js | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/packages/strapi/lib/middlewares/mask/index.js b/packages/strapi/lib/middlewares/mask/index.js index d3240a630f..bdd645f6fa 100644 --- a/packages/strapi/lib/middlewares/mask/index.js +++ b/packages/strapi/lib/middlewares/mask/index.js @@ -21,21 +21,27 @@ module.exports = strapi => { // Execute next middleware. await next(); - if (ctx.status === 200) { - // Array - if (_.isArray(ctx.body)) { - ctx.body = ctx.body.map(value => { - if (_.isPlainObject(value)) { - return this.mask(ctx, value); - } + // Recursive to mask the private properties. + const mask = (payload) => { + if (_.isArray(payload)) { + return payload.map(value => mask(value)); + } else if (_.isPlainObject(payload)) { + return this.mask( + ctx, + Object.keys(payload).reduce((acc, current) => { + acc[current] = _.isObjectLike(payload[current]) ? mask(payload[current]) : payload[current]; - // Raw - return obj; - }); - } else if (_.isPlainObject(ctx.body)) { - // Plain object. - ctx.body = this.mask(ctx, ctx.body); + return acc; + }, {}) + ); } + + return payload; + }; + + // Only pick successful JSON requests. + if ([200, 201, 202].includes(ctx.status) && ctx.type === 'application/json') { + ctx.body = mask(ctx.body); } }); @@ -108,8 +114,8 @@ module.exports = strapi => { strapi.plugins[match.plugin].models[match.model].attributes: strapi.models[match.model].attributes; - // Filtered model which have more than half of the attributes in common - // with the original model. + // Filtered model which have more than 50% of the attributes + // in common with the original model. if (match.intersection >= Object.keys(attributes).length / 2) { acc[index] = match; } From 2cb0e2c3d5cfcf99036b440ec155ae10004116e4 Mon Sep 17 00:00:00 2001 From: Aurelsicoko Date: Thu, 8 Feb 2018 14:59:39 +0100 Subject: [PATCH 06/11] Hide resetPasswordToken from response --- .../strapi-plugin-users-permissions/models/User.settings.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/strapi-plugin-users-permissions/models/User.settings.json b/packages/strapi-plugin-users-permissions/models/User.settings.json index f1aa1b2b13..99479b1cba 100644 --- a/packages/strapi-plugin-users-permissions/models/User.settings.json +++ b/packages/strapi-plugin-users-permissions/models/User.settings.json @@ -30,7 +30,8 @@ }, "resetPasswordToken": { "type": "string", - "configurable": false + "configurable": false, + "private": true }, "role": { "model": "role", From 45218543408efe458e54315ad38e7728c240b33b Mon Sep 17 00:00:00 2001 From: Aurelsicoko Date: Thu, 8 Feb 2018 15:27:24 +0100 Subject: [PATCH 07/11] Enable mask middleware if there is a private key set to true --- packages/strapi/lib/middlewares/mask/index.js | 75 +++++++++++++------ 1 file changed, 51 insertions(+), 24 deletions(-) diff --git a/packages/strapi/lib/middlewares/mask/index.js b/packages/strapi/lib/middlewares/mask/index.js index bdd645f6fa..c312fbc151 100644 --- a/packages/strapi/lib/middlewares/mask/index.js +++ b/packages/strapi/lib/middlewares/mask/index.js @@ -17,33 +17,60 @@ module.exports = strapi => { */ initialize: function (cb) { - strapi.app.use(async (ctx, next) => { - // Execute next middleware. - await next(); - - // Recursive to mask the private properties. - const mask = (payload) => { - if (_.isArray(payload)) { - return payload.map(value => mask(value)); - } else if (_.isPlainObject(payload)) { - return this.mask( - ctx, - Object.keys(payload).reduce((acc, current) => { - acc[current] = _.isObjectLike(payload[current]) ? mask(payload[current]) : payload[current]; - - return acc; - }, {}) - ); + // Enable the middleware if we need it. + const enabled = (() => { + const main = Object.keys(strapi.models).reduce((acc, current) => { + if (Object.values(strapi.models[current].attributes).find(attr => attr.private === true)) { + acc = true; } - return payload; - }; + return acc; + }, false); - // Only pick successful JSON requests. - if ([200, 201, 202].includes(ctx.status) && ctx.type === 'application/json') { - ctx.body = mask(ctx.body); - } - }); + const plugins = Object.keys(strapi.plugins).reduce((acc, plugin) => { + acc = Object.keys(strapi.plugins[plugin].models).reduce((acc, model) => { + if (Object.values(strapi.plugins[plugin].models[model].attributes).find(attr => attr.private === true)) { + acc = true; + } + + return acc; + }, false); + + return acc; + }, false); + + return main || plugins; + })(); + + if (enabled) { + strapi.app.use(async (ctx, next) => { + // Execute next middleware. + await next(); + + // Recursive to mask the private properties. + const mask = (payload) => { + if (_.isArray(payload)) { + return payload.map(value => mask(value)); + } else if (_.isPlainObject(payload)) { + return this.mask( + ctx, + Object.keys(payload).reduce((acc, current) => { + acc[current] = _.isObjectLike(payload[current]) ? mask(payload[current]) : payload[current]; + + return acc; + }, {}) + ); + } + + return payload; + }; + + // Only pick successful JSON requests. + if ([200, 201, 202].includes(ctx.status) && ctx.type === 'application/json') { + ctx.body = mask(ctx.body); + } + }); + } cb(); }, From 5f19c8b3b343c6ff7421f13ad6f35b6329c35b82 Mon Sep 17 00:00:00 2001 From: Aurelsicoko Date: Fri, 9 Feb 2018 10:46:37 +0100 Subject: [PATCH 08/11] Apply PR feedback --- packages/strapi/lib/middlewares/mask/index.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/strapi/lib/middlewares/mask/index.js b/packages/strapi/lib/middlewares/mask/index.js index c312fbc151..8f960f6d69 100644 --- a/packages/strapi/lib/middlewares/mask/index.js +++ b/packages/strapi/lib/middlewares/mask/index.js @@ -28,7 +28,7 @@ module.exports = strapi => { }, false); const plugins = Object.keys(strapi.plugins).reduce((acc, plugin) => { - acc = Object.keys(strapi.plugins[plugin].models).reduce((acc, model) => { + const bool = Object.keys(strapi.plugins[plugin].models).reduce((acc, model) => { if (Object.values(strapi.plugins[plugin].models[model].attributes).find(attr => attr.private === true)) { acc = true; } @@ -36,6 +36,10 @@ module.exports = strapi => { return acc; }, false); + if (bool) { + acc = true; + } + return acc; }, false); @@ -50,7 +54,7 @@ module.exports = strapi => { // Recursive to mask the private properties. const mask = (payload) => { if (_.isArray(payload)) { - return payload.map(value => mask(value)); + return payload.map(mask); } else if (_.isPlainObject(payload)) { return this.mask( ctx, From 36501aeeb66fc7bd93db37615149d7d7ad9e0ead Mon Sep 17 00:00:00 2001 From: Aurelsicoko Date: Fri, 9 Feb 2018 11:02:00 +0100 Subject: [PATCH 09/11] Write validations documentation --- docs/3.x.x/en/guides/models.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/docs/3.x.x/en/guides/models.md b/docs/3.x.x/en/guides/models.md index 404a6c84aa..e90c38af38 100644 --- a/docs/3.x.x/en/guides/models.md +++ b/docs/3.x.x/en/guides/models.md @@ -33,6 +33,23 @@ The following types are currently available: - `json` - `email` +#### Validations + +You can apply basic validations to the attributes. The following supported validations are *only supported by MongoDB* connection. +If you're using SQL databases, you should use the native SQL constraints to apply them. + + - `required` (boolean) — if true adds a required validator for this property. + - `unique` (boolean) — whether to define a unique index on this property. + - `max` (integer) — checks if the value is greater than or equal to the given minimum. + - `min` (integer) — checks if the value is less than or equal to the given maximum. + + +**Security validations** +To improve the Developer eXperience when developing or using the administration panel, the framework enhances the attributes with these "security validations": + + - `private` (boolean) — if true, the attribute will be removed from the server response (it's useful to hide sensitive data). + - `configurable` (boolean) - if false, the attribute isn't configurable from the Content Type Builder plugin. + #### Example **Path —** `User.settings.json`. From 200a40d7580b7d100d60cbcac44b3308b8513aad Mon Sep 17 00:00:00 2001 From: Aurelsicoko Date: Fri, 9 Feb 2018 11:03:53 +0100 Subject: [PATCH 10/11] Improve attributes example --- docs/3.x.x/en/guides/models.md | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/docs/3.x.x/en/guides/models.md b/docs/3.x.x/en/guides/models.md index e90c38af38..285710e845 100644 --- a/docs/3.x.x/en/guides/models.md +++ b/docs/3.x.x/en/guides/models.md @@ -67,14 +67,23 @@ To improve the Developer eXperience when developing or using the administration "lastname": { "type": "string" }, + "email": { + "type": "email", + "required": true, + "unique": true + }, "password": { - "type": "password" + "type": "password", + "required": true, + "private": true }, "about": { "type": "description" }, "age": { - "type": "integer" + "type": "integer", + "min": 18, + "max": 99 }, "birthday": { "type": "date" From 46a64b449f7e71905c22ee92ea7494abec899916 Mon Sep 17 00:00:00 2001 From: Aurelsicoko Date: Fri, 9 Feb 2018 16:36:23 +0100 Subject: [PATCH 11/11] Update default hook timeout to 3000 milliseconds --- packages/strapi-generate-new/files/config/hook.json | 2 +- packages/strapi-plugin-users-permissions/controllers/Auth.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/strapi-generate-new/files/config/hook.json b/packages/strapi-generate-new/files/config/hook.json index 94f0c561c0..ff33d41032 100755 --- a/packages/strapi-generate-new/files/config/hook.json +++ b/packages/strapi-generate-new/files/config/hook.json @@ -1,5 +1,5 @@ { - "timeout": 1000, + "timeout": 3000, "load": { "order": [ "Define the hooks' load order by putting their names in this array in the right order" diff --git a/packages/strapi-plugin-users-permissions/controllers/Auth.js b/packages/strapi-plugin-users-permissions/controllers/Auth.js index 235493c9cc..23879a7889 100644 --- a/packages/strapi-plugin-users-permissions/controllers/Auth.js +++ b/packages/strapi-plugin-users-permissions/controllers/Auth.js @@ -8,7 +8,6 @@ const _ = require('lodash'); const crypto = require('crypto'); -const Grant = require('grant-koa'); const emailRegExp = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; module.exports = { @@ -137,6 +136,7 @@ module.exports = { return ctx.badRequest(null, 'This provider is disabled.'); } + const Grant = require('grant-koa'); const grant = new Grant(strapi.plugins['users-permissions'].config.grant); return strapi.koaMiddlewares.compose(grant.middleware)(ctx, next);