diff --git a/packages/strapi-admin/config/functions/bootstrap.js b/packages/strapi-admin/config/functions/bootstrap.js index 3ceefcebd6..076b88b861 100644 --- a/packages/strapi-admin/config/functions/bootstrap.js +++ b/packages/strapi-admin/config/functions/bootstrap.js @@ -1,5 +1,6 @@ 'use strict'; +const _ = require('lodash'); const adminActions = require('../admin-actions'); const registerPermissionActions = () => { @@ -7,6 +8,17 @@ const registerPermissionActions = () => { actionProvider.register(adminActions.actions); }; +const registerAdminConditions = () => { + const { conditionProvider } = strapi.admin.services.permission; + + conditionProvider.register({ + displayName: 'Is Creator', + name: 'is-creator', + plugin: 'admin', + handler: user => ({ 'created_by.id': user.id }), + }); +}; + const cleanPermissionInDatabase = async () => { const { actionProvider } = strapi.admin.services.permission; const dbPermissions = await strapi.admin.services.permission.find(); @@ -26,19 +38,101 @@ const cleanPermissionInDatabase = async () => { await strapi.admin.services.permission.deleteByIds(permissionsToRemoveIds); }; -const registerAdminConditions = () => { - const { conditionProvider } = strapi.admin.services.permission; +const getNestedFields = (attributes, fieldPath = '', nestingLevel = 3) => { + if (nestingLevel === 0) { + return fieldPath ? [fieldPath] : []; + } - conditionProvider.register({ - displayName: 'Is Creator', - name: 'is-creator', - plugin: 'admin', - handler: user => ({ 'created_by.id': user.id }), + const fields = []; + _.forIn(attributes, (attribute, attributeName) => { + const newFieldPath = fieldPath ? `${fieldPath}.${attributeName}` : attributeName; + + if (attribute.type === 'component') { + const component = strapi.components[attribute.component]; + const componentFields = getNestedFields(component.attributes, newFieldPath, nestingLevel - 1); + fields.push(...componentFields); + } else { + fields.push(newFieldPath); + } }); + + return fields; +}; + +const createRolesIfNeeded = async () => { + const someRolesExist = await strapi.admin.services.role.exists(); + if (someRolesExist) { + return; + } + + const defaultActionsIds = [ + 'plugins::content-manager.read', + 'plugins::content-manager.create', + 'plugins::content-manager.update', + 'plugins::content-manager.delete', + ]; + const allActions = strapi.admin.services.permission.provider.getAll(); + const contentTypesActions = allActions.filter(a => defaultActionsIds.includes(a.actionId)); + + await strapi.admin.services.role.create({ + name: 'Super Admin', + code: 'strapi-super-admin', + description: 'Super Admins can access and manage all features and settings.', + }); + + const editorRole = await strapi.admin.services.role.create({ + name: 'Editor', + code: 'strapi-editor', + description: 'Editors can manage and publish contents including those of other users.', + }); + + const authorRole = await strapi.admin.services.role.create({ + name: 'Author', + code: 'strapi-author', + description: 'Authors can manage and publish their own content.', + }); + + const editorPermissions = []; + contentTypesActions.forEach(action => { + _.forIn(strapi.contentTypes, contentType => { + if (action.subjects.includes(contentType.uid)) { + const fields = getNestedFields(contentType.attributes); + editorPermissions.push({ + action: action.actionId, + subject: contentType.uid, + fields, + }); + } + }); + }); + + await strapi.admin.services.permission.assign(editorRole.id, editorPermissions); + await strapi.admin.services.permission.assign(authorRole.id, editorPermissions); +}; + +const displayWarningIfNoSuperAdmin = async () => { + const adminRole = await strapi.admin.services.role.getAdminWithUsersCount(); + const someUsersExists = await strapi.admin.services.user.exists(); + if (!adminRole) { + return strapi.log.warn("Your application doesn't have a super admin role."); + } else if (someUsersExists && adminRole.usersCount === 0) { + return strapi.log.warn("Your application doesn't have a super admin user."); + } +}; + +const displayWarningIfUsersDontHaveRole = async () => { + const count = await strapi.admin.services.user.countUsersWithoutRole(); + + if (count > 0) { + strapi.log.warn(`You have ${count} user${count === 1 ? '' : 's'} without any role.`); + } }; module.exports = async () => { registerAdminConditions(); registerPermissionActions(); await cleanPermissionInDatabase(); + await createRolesIfNeeded(); + await displayWarningIfNoSuperAdmin(); + await displayWarningIfUsersDontHaveRole(); }; diff --git a/packages/strapi-admin/config/settings.json b/packages/strapi-admin/config/settings.json new file mode 100644 index 0000000000..693b938cbe --- /dev/null +++ b/packages/strapi-admin/config/settings.json @@ -0,0 +1,3 @@ +{ + "superAdminCode": "strapi-super-admin" +} diff --git a/packages/strapi-admin/controllers/authentication.js b/packages/strapi-admin/controllers/authentication.js index 4a68cc2a1a..63893559e7 100644 --- a/packages/strapi-admin/controllers/authentication.js +++ b/packages/strapi-admin/controllers/authentication.js @@ -113,11 +113,13 @@ module.exports = { return ctx.badRequest('You cannot register a new super admin'); } - // TODO: assign super admin role + const adminRole = await strapi.admin.services.role.getAdmin(); + const user = await strapi.admin.services.user.create({ ...input, registrationToken: null, isActive: true, + roles: adminRole ? [adminRole.id] : [], }); ctx.body = { diff --git a/packages/strapi-admin/ee/controllers/role.js b/packages/strapi-admin/ee/controllers/role.js index 4d8084c719..ee95d3452a 100644 --- a/packages/strapi-admin/ee/controllers/role.js +++ b/packages/strapi-admin/ee/controllers/role.js @@ -3,6 +3,7 @@ const { validateRoleCreateInput, validateRoleUpdateInput, + validateRolesDeleteInput, validateRoleDeleteInput, } = require('../validation/role'); const { validatedUpdatePermissionsInput } = require('../validation/permission'); @@ -57,6 +58,12 @@ module.exports = { async deleteOne(ctx) { const { id } = ctx.params; + try { + await validateRoleDeleteInput(id); + } catch (err) { + return ctx.badRequest('ValidationError', err); + } + const roles = await strapi.admin.services.role.deleteByIds([id]); const sanitizedRole = roles.map(strapi.admin.services.role.sanitizeRole)[0] || null; @@ -73,7 +80,7 @@ module.exports = { async deleteMany(ctx) { const { body } = ctx.request; try { - await validateRoleDeleteInput(body); + await validateRolesDeleteInput(body); } catch (err) { return ctx.badRequest('ValidationError', err); } diff --git a/packages/strapi-admin/ee/validation/role.js b/packages/strapi-admin/ee/validation/role.js index 3f5bf753cc..323d7d6c6c 100644 --- a/packages/strapi-admin/ee/validation/role.js +++ b/packages/strapi-admin/ee/validation/role.js @@ -23,17 +23,31 @@ const roleUpdateSchema = yup }) .noUnknown(); -const roleDeleteSchema = yup +const rolesDeleteSchema = yup .object() .shape({ ids: yup .array() .of(yup.strapiID()) .min(1) - .required(), + .required() + .test('no-admin-many-delete', 'you cannot delete the super admin role', async ids => { + const adminRole = await strapi.admin.services.role.getAdmin(); + return !ids.map(String).includes(String(adminRole.id)); + }), }) .noUnknown(); +const roleDeleteSchema = yup + .strapiID() + .required() + .test('no-admin-single-delete', 'you cannot delete the super admin role', async function(id) { + const adminRole = await strapi.admin.services.role.getAdmin(); + return String(id) !== String(adminRole.id) + ? true + : this.createError({ path: 'id', message: `you cannot delete the super admin role` }); + }); + const validateRoleCreateInput = async data => { return roleCreateSchema.validate(data, { strict: true, abortEarly: false }).catch(handleReject); }; @@ -42,6 +56,10 @@ const validateRoleUpdateInput = async data => { return roleUpdateSchema.validate(data, { strict: true, abortEarly: false }).catch(handleReject); }; +const validateRolesDeleteInput = async data => { + return rolesDeleteSchema.validate(data, { strict: true, abortEarly: false }).catch(handleReject); +}; + const validateRoleDeleteInput = async data => { return roleDeleteSchema.validate(data, { strict: true, abortEarly: false }).catch(handleReject); }; @@ -49,5 +67,6 @@ const validateRoleDeleteInput = async data => { module.exports = { validateRoleCreateInput, validateRoleUpdateInput, + validateRolesDeleteInput, validateRoleDeleteInput, }; diff --git a/packages/strapi-admin/models/Role.settings.json b/packages/strapi-admin/models/Role.settings.json index c0f8de97eb..db74093a83 100644 --- a/packages/strapi-admin/models/Role.settings.json +++ b/packages/strapi-admin/models/Role.settings.json @@ -15,6 +15,12 @@ "configurable": false, "required": true }, + "code": { + "type": "string", + "minLength": 1, + "unique": true, + "configurable": false + }, "description": { "type": "string", "configurable": false diff --git a/packages/strapi-admin/services/role.js b/packages/strapi-admin/services/role.js index c6a164828d..9af1bcbcae 100644 --- a/packages/strapi-admin/services/role.js +++ b/packages/strapi-admin/services/role.js @@ -133,9 +133,20 @@ const deleteByIds = async (ids = []) => { * @returns {Promise} */ const getUsersCount = async roleId => { - return strapi.query('user', 'admin').count({ 'roles.id': roleId }); + return strapi.query('user', 'admin').count({ roles: [roleId] }); }; +/** Returns admin role + * @returns {Promise} + */ +const getAdmin = () => findOne({ code: strapi.admin.config.superAdminCode }); + +/** Returns admin role with userCount + * @returns {Promise} + */ +const getAdminWithUsersCount = () => + findOneWithUsersCount({ code: strapi.admin.config.superAdminCode }); + module.exports = { sanitizeRole, create, @@ -147,4 +158,6 @@ module.exports = { exists, deleteByIds, getUsersCount, + getAdmin, + getAdminWithUsersCount, }; diff --git a/packages/strapi-admin/services/user.js b/packages/strapi-admin/services/user.js index 4397af4c77..55219b23f7 100644 --- a/packages/strapi-admin/services/user.js +++ b/packages/strapi-admin/services/user.js @@ -140,6 +140,35 @@ const deleteOne = async query => { return strapi.query('user', 'admin').delete(query); }; +/** Count the users that don't have any associated roles + * @returns {Promise} + */ +const countUsersWithoutRole = async () => { + const userModel = strapi.query('user', 'admin').model; + const assocTable = userModel.associations.find(a => a.alias === 'roles').tableCollectionName; + let count; + + if (userModel.orm === 'bookshelf') { + const result = await userModel + .query(qb => { + qb.count() + .leftJoin(assocTable, `${userModel.collectionName}.id`, `${assocTable}.user_id`) + .where(`${assocTable}.role_id`, null); + }) + .fetch(); + count = result.toJSON()['count(*)']; + } else if (userModel.orm === 'mongoose') { + count = await strapi.query('user', 'admin').model.countDocuments({ roles: { $size: 0 } }); + } else { + const allRoles = await strapi.query('role', 'admin').find(); + count = await strapi.query('user', 'admin').count({ + roles_nin: allRoles.map(r => r.id), + }); + } + + return count; +}; + module.exports = { create, update, @@ -151,4 +180,5 @@ module.exports = { findPage, searchPage, deleteOne, + countUsersWithoutRole, };