diff --git a/docs/3.x.x/en/guides/models.md b/docs/3.x.x/en/guides/models.md index 404a6c84aa..285710e845 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`. @@ -50,14 +67,23 @@ The following types are currently available: "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" 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 7af86df703..6fa92a8cda 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); diff --git a/packages/strapi-plugin-users-permissions/models/User.settings.json b/packages/strapi-plugin-users-permissions/models/User.settings.json index e64b741cbb..99479b1cba 100644 --- a/packages/strapi-plugin-users-permissions/models/User.settings.json +++ b/packages/strapi-plugin-users-permissions/models/User.settings.json @@ -25,11 +25,13 @@ "password": { "type": "password", "minLength": 6, - "configurable": false + "configurable": false, + "private": true }, "resetPasswordToken": { "type": "string", - "configurable": false + "configurable": false, + "private": true }, "role": { "model": "role", 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 diff --git a/packages/strapi/lib/middlewares/mask/defaults.json b/packages/strapi/lib/middlewares/mask/defaults.json new file mode 100644 index 0000000000..8fc1f914ab --- /dev/null +++ b/packages/strapi/lib/middlewares/mask/defaults.json @@ -0,0 +1,5 @@ +{ + "mask": { + "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..8f960f6d69 --- /dev/null +++ b/packages/strapi/lib/middlewares/mask/index.js @@ -0,0 +1,158 @@ +'use strict'; + +/** + * Module dependencies + */ + +/** + * Mask filter middleware + */ + +const _ = require('lodash'); + +module.exports = strapi => { + return { + /** + * Initialize the hook + */ + + initialize: function (cb) { + // 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 acc; + }, false); + + const plugins = Object.keys(strapi.plugins).reduce((acc, plugin) => { + 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; + } + + return acc; + }, false); + + if (bool) { + acc = true; + } + + 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(mask); + } 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(); + }, + + 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 50% of the attributes + // in common with the original model. + if (match.intersection >= Object.keys(attributes).length / 2) { + acc[index] = match; + } + + return acc; + }, []); + } + }; +};