mirror of
https://github.com/strapi/strapi.git
synced 2025-12-28 07:33:17 +00:00
Move RBAC into CE
This commit is contained in:
parent
b9e92de1af
commit
e0e2084422
@ -2,9 +2,9 @@
|
||||
|
||||
module.exports = {
|
||||
authentication: require('./authentication'),
|
||||
permission: require('./permission'),
|
||||
role: require('./role'),
|
||||
user: require('./user'),
|
||||
// permission: require('./permission'),
|
||||
// role: require('./role'),
|
||||
// user: require('./user'),
|
||||
auditLogs: require('./audit-logs'),
|
||||
admin: require('./admin'),
|
||||
};
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const { getService } = require('../../../server/utils');
|
||||
const { formatConditions } = require('../../../server/controllers/formatters');
|
||||
|
||||
module.exports = {
|
||||
async getAll(ctx) {
|
||||
const { sectionsBuilder, actionProvider, conditionProvider } = getService('permission');
|
||||
|
||||
const actions = actionProvider.values();
|
||||
const conditions = conditionProvider.values();
|
||||
const sections = await sectionsBuilder.build(actions);
|
||||
|
||||
ctx.body = {
|
||||
data: {
|
||||
conditions: formatConditions(conditions),
|
||||
sections,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@ -1,14 +1,11 @@
|
||||
'use strict';
|
||||
|
||||
const { ApplicationError } = require('@strapi/utils').errors;
|
||||
const {
|
||||
validateRoleCreateInput,
|
||||
validateRoleDeleteInput,
|
||||
validateRolesDeleteInput,
|
||||
} = require('../validation/role');
|
||||
const { getService } = require('../../../server/utils');
|
||||
const { validatedUpdatePermissionsInput } = require('../validation/permission');
|
||||
const { SUPER_ADMIN_CODE } = require('../../../server/services/constants');
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
@ -64,40 +61,4 @@ module.exports = {
|
||||
data: sanitizedRoles,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Updates the permissions assigned to a role
|
||||
* @param {KoaContext} ctx - koa context
|
||||
*/
|
||||
async updatePermissions(ctx) {
|
||||
const { id } = ctx.params;
|
||||
const { body: input } = ctx.request;
|
||||
|
||||
const roleService = getService('role');
|
||||
const permissionService = getService('permission');
|
||||
|
||||
const role = await roleService.findOne({ id });
|
||||
|
||||
if (!role) {
|
||||
return ctx.notFound('role.notFound');
|
||||
}
|
||||
|
||||
if (role.code === SUPER_ADMIN_CODE) {
|
||||
throw new ApplicationError("Super admin permissions can't be edited.");
|
||||
}
|
||||
|
||||
await validatedUpdatePermissionsInput(input);
|
||||
|
||||
if (!role) {
|
||||
return ctx.notFound('role.notFound');
|
||||
}
|
||||
|
||||
const permissions = await roleService.assignPermissions(role.id, input.permissions);
|
||||
|
||||
const sanitizedPermissions = permissions.map(permissionService.sanitizePermission);
|
||||
|
||||
return ctx.send({
|
||||
data: sanitizedPermissions,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@ -6,10 +6,7 @@ const _ = require('lodash');
|
||||
const { pick, isNil } = require('lodash/fp');
|
||||
const { ApplicationError, ForbiddenError } = require('@strapi/utils').errors;
|
||||
const { validateUserCreationInput } = require('../validation/user');
|
||||
const {
|
||||
validateUserUpdateInput,
|
||||
validateUsersDeleteInput,
|
||||
} = require('../../../server/validation/user');
|
||||
const { validateUserUpdateInput } = require('../../../server/validation/user');
|
||||
const { getService } = require('../../../server/utils');
|
||||
|
||||
const pickUserCreationAttributes = pick(['firstname', 'lastname', 'email', 'roles']);
|
||||
@ -98,35 +95,4 @@ module.exports = {
|
||||
data: getService('user').sanitizeUser(updatedUser),
|
||||
};
|
||||
},
|
||||
|
||||
async deleteOne(ctx) {
|
||||
const { id } = ctx.params;
|
||||
|
||||
const deletedUser = await getService('user').deleteById(id);
|
||||
|
||||
if (!deletedUser) {
|
||||
return ctx.notFound('User not found');
|
||||
}
|
||||
|
||||
return ctx.deleted({
|
||||
data: getService('user').sanitizeUser(deletedUser),
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete several users
|
||||
* @param {KoaContext} ctx - koa context
|
||||
*/
|
||||
async deleteMany(ctx) {
|
||||
const { body } = ctx.request;
|
||||
await validateUsersDeleteInput(body);
|
||||
|
||||
const users = await getService('user').deleteByIds(body.ids);
|
||||
|
||||
const sanitizedUsers = users.map(getService('user').sanitizeUser);
|
||||
|
||||
return ctx.deleted({
|
||||
data: sanitizedUsers,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@ -11,54 +11,54 @@ const enableFeatureMiddleware = (featureName) => (ctx, next) => {
|
||||
};
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/roles',
|
||||
handler: 'role.create',
|
||||
config: {
|
||||
policies: [
|
||||
'admin::isAuthenticatedAdmin',
|
||||
{
|
||||
name: 'admin::hasPermissions',
|
||||
config: {
|
||||
actions: ['admin::roles.create'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'DELETE',
|
||||
path: '/roles/:id',
|
||||
handler: 'role.deleteOne',
|
||||
config: {
|
||||
policies: [
|
||||
'admin::isAuthenticatedAdmin',
|
||||
{
|
||||
name: 'admin::hasPermissions',
|
||||
config: {
|
||||
actions: ['admin::roles.delete'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/roles/batch-delete',
|
||||
handler: 'role.deleteMany',
|
||||
config: {
|
||||
policies: [
|
||||
'admin::isAuthenticatedAdmin',
|
||||
{
|
||||
name: 'admin::hasPermissions',
|
||||
config: {
|
||||
actions: ['admin::roles.delete'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
// {
|
||||
// method: 'POST',
|
||||
// path: '/roles',
|
||||
// handler: 'role.create',
|
||||
// config: {
|
||||
// policies: [
|
||||
// 'admin::isAuthenticatedAdmin',
|
||||
// {
|
||||
// name: 'admin::hasPermissions',
|
||||
// config: {
|
||||
// actions: ['admin::roles.create'],
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// method: 'DELETE',
|
||||
// path: '/roles/:id',
|
||||
// handler: 'role.deleteOne',
|
||||
// config: {
|
||||
// policies: [
|
||||
// 'admin::isAuthenticatedAdmin',
|
||||
// {
|
||||
// name: 'admin::hasPermissions',
|
||||
// config: {
|
||||
// actions: ['admin::roles.delete'],
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// },
|
||||
// {
|
||||
// method: 'POST',
|
||||
// path: '/roles/batch-delete',
|
||||
// handler: 'role.deleteMany',
|
||||
// config: {
|
||||
// policies: [
|
||||
// 'admin::isAuthenticatedAdmin',
|
||||
// {
|
||||
// name: 'admin::hasPermissions',
|
||||
// config: {
|
||||
// actions: ['admin::roles.delete'],
|
||||
// },
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// },
|
||||
|
||||
// SSO
|
||||
{
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const { validateYupSchema } = require('@strapi/utils');
|
||||
const validators = require('../../../server/validation/common-validators');
|
||||
|
||||
module.exports = {
|
||||
validatedUpdatePermissionsInput: validateYupSchema(validators.updatePermissions),
|
||||
};
|
||||
@ -20,38 +20,46 @@ const rolesDeleteSchema = yup
|
||||
.of(yup.strapiID())
|
||||
.min(1)
|
||||
.required()
|
||||
.test('roles-deletion-checks', 'Roles deletion checks have failed', async function (ids) {
|
||||
try {
|
||||
await strapi.admin.services.role.checkRolesIdForDeletion(ids);
|
||||
.test(
|
||||
'roles-deletion-checks',
|
||||
'Roles deletion checks have failed',
|
||||
async function rolesDeletionChecks(ids) {
|
||||
try {
|
||||
await strapi.admin.services.role.checkRolesIdForDeletion(ids);
|
||||
|
||||
if (features.isEnabled('sso')) {
|
||||
await strapi.admin.services.role.ssoCheckRolesIdForDeletion(ids);
|
||||
if (features.isEnabled('sso')) {
|
||||
await strapi.admin.services.role.ssoCheckRolesIdForDeletion(ids);
|
||||
}
|
||||
} catch (e) {
|
||||
return this.createError({ path: 'ids', message: e.message });
|
||||
}
|
||||
} catch (e) {
|
||||
return this.createError({ path: 'ids', message: e.message });
|
||||
}
|
||||
|
||||
return true;
|
||||
}),
|
||||
return true;
|
||||
}
|
||||
),
|
||||
})
|
||||
.noUnknown();
|
||||
|
||||
const roleDeleteSchema = yup
|
||||
.strapiID()
|
||||
.required()
|
||||
.test('no-admin-single-delete', 'Role deletion checks have failed', async function (id) {
|
||||
try {
|
||||
await strapi.admin.services.role.checkRolesIdForDeletion([id]);
|
||||
.test(
|
||||
'no-admin-single-delete',
|
||||
'Role deletion checks have failed',
|
||||
async function noAdminSingleDelete(id) {
|
||||
try {
|
||||
await strapi.admin.services.role.checkRolesIdForDeletion([id]);
|
||||
|
||||
if (features.isEnabled('sso')) {
|
||||
await strapi.admin.services.role.ssoCheckRolesIdForDeletion([id]);
|
||||
if (features.isEnabled('sso')) {
|
||||
await strapi.admin.services.role.ssoCheckRolesIdForDeletion([id]);
|
||||
}
|
||||
} catch (e) {
|
||||
return this.createError({ path: 'id', message: e.message });
|
||||
}
|
||||
} catch (e) {
|
||||
return this.createError({ path: 'id', message: e.message });
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
return true;
|
||||
}
|
||||
);
|
||||
|
||||
module.exports = {
|
||||
validateRoleCreateInput: validateYupSchema(roleCreateSchema),
|
||||
|
||||
1
packages/core/admin/server/bootstrap.js
vendored
1
packages/core/admin/server/bootstrap.js
vendored
@ -85,7 +85,6 @@ module.exports = async () => {
|
||||
await roleService.resetSuperAdminPermissions();
|
||||
await roleService.displayWarningIfNoSuperAdmin();
|
||||
|
||||
await permissionService.ensureBoundPermissionsInDatabase();
|
||||
await permissionService.cleanPermissionsInDatabase();
|
||||
|
||||
await userService.displayWarningIfUsersDontHaveRole();
|
||||
|
||||
@ -29,11 +29,9 @@ module.exports = {
|
||||
* @param {KoaContext} ctx - koa context
|
||||
*/
|
||||
async getAll(ctx) {
|
||||
const { role: roleId } = ctx.query;
|
||||
const { sectionsBuilder, actionProvider, conditionProvider } = getService('permission');
|
||||
|
||||
const { sectionsBuilder, conditionProvider } = getService('permission');
|
||||
|
||||
const actions = await getService('action').getAllowedActionsForRole(roleId);
|
||||
const actions = actionProvider.values();
|
||||
const conditions = conditionProvider.values();
|
||||
const sections = await sectionsBuilder.build(actions);
|
||||
|
||||
|
||||
@ -1,12 +1,32 @@
|
||||
'use strict';
|
||||
|
||||
const { ApplicationError } = require('@strapi/utils').errors;
|
||||
const { validateRoleUpdateInput } = require('../validation/role');
|
||||
const {
|
||||
validateRoleUpdateInput,
|
||||
validateRoleCreateInput,
|
||||
validateRoleDeleteInput,
|
||||
validateRolesDeleteInput,
|
||||
} = require('../validation/role');
|
||||
const { validatedUpdatePermissionsInput } = require('../validation/permission');
|
||||
const { EDITOR_CODE, AUTHOR_CODE, SUPER_ADMIN_CODE } = require('../services/constants');
|
||||
const { SUPER_ADMIN_CODE } = require('../services/constants');
|
||||
const { getService } = require('../utils');
|
||||
|
||||
module.exports = {
|
||||
/**
|
||||
* Create a new role
|
||||
* @param {KoaContext} ctx - koa context
|
||||
*/
|
||||
async create(ctx) {
|
||||
await validateRoleCreateInput(ctx.request.body);
|
||||
|
||||
const roleService = getService('role');
|
||||
|
||||
const role = await roleService.create(ctx.request.body);
|
||||
const sanitizedRole = roleService.sanitizeRole(role);
|
||||
|
||||
ctx.created({ data: sanitizedRole });
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns on role by id
|
||||
* @param {KoaContext} ctx - koa context
|
||||
@ -99,10 +119,10 @@ module.exports = {
|
||||
const { id } = ctx.params;
|
||||
const { body: input } = ctx.request;
|
||||
|
||||
const { findOne, assignPermissions } = getService('role');
|
||||
const { sanitizePermission, actionProvider } = getService('permission');
|
||||
const roleService = getService('role');
|
||||
const permissionService = getService('permission');
|
||||
|
||||
const role = await findOne({ id });
|
||||
const role = await roleService.findOne({ id });
|
||||
|
||||
if (!role) {
|
||||
return ctx.notFound('role.notFound');
|
||||
@ -112,30 +132,57 @@ module.exports = {
|
||||
throw new ApplicationError("Super admin permissions can't be edited.");
|
||||
}
|
||||
|
||||
await validatedUpdatePermissionsInput(input, role);
|
||||
await validatedUpdatePermissionsInput(input);
|
||||
|
||||
let permissionsToAssign;
|
||||
|
||||
if ([EDITOR_CODE, AUTHOR_CODE].includes(role.code)) {
|
||||
permissionsToAssign = input.permissions.map((permission) => {
|
||||
const action = actionProvider.get(permission.action);
|
||||
|
||||
if (action.section !== 'contentTypes') {
|
||||
return permission;
|
||||
}
|
||||
|
||||
const conditions = role.code === AUTHOR_CODE ? ['admin::is-creator'] : [];
|
||||
|
||||
return { ...permission, conditions };
|
||||
});
|
||||
} else {
|
||||
permissionsToAssign = input.permissions;
|
||||
if (!role) {
|
||||
return ctx.notFound('role.notFound');
|
||||
}
|
||||
|
||||
const permissions = await assignPermissions(role.id, permissionsToAssign);
|
||||
const permissions = await roleService.assignPermissions(role.id, input.permissions);
|
||||
|
||||
ctx.body = {
|
||||
data: permissions.map(sanitizePermission),
|
||||
};
|
||||
const sanitizedPermissions = permissions.map(permissionService.sanitizePermission);
|
||||
|
||||
return ctx.send({
|
||||
data: sanitizedPermissions,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete a role
|
||||
* @param {KoaContext} ctx - koa context
|
||||
*/
|
||||
async deleteOne(ctx) {
|
||||
const { id } = ctx.params;
|
||||
|
||||
await validateRoleDeleteInput(id);
|
||||
|
||||
const roleService = getService('role');
|
||||
|
||||
const roles = await roleService.deleteByIds([id]);
|
||||
|
||||
const sanitizedRole = roles.map((role) => roleService.sanitizeRole(role))[0] || null;
|
||||
|
||||
return ctx.deleted({
|
||||
data: sanitizedRole,
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* delete several roles
|
||||
* @param {KoaContext} ctx - koa context
|
||||
*/
|
||||
async deleteMany(ctx) {
|
||||
const { body } = ctx.request;
|
||||
|
||||
await validateRolesDeleteInput(body);
|
||||
|
||||
const roleService = getService('role');
|
||||
|
||||
const roles = await roleService.deleteByIds(body.ids);
|
||||
const sanitizedRoles = roles.map(roleService.sanitizeRole);
|
||||
|
||||
return ctx.deleted({
|
||||
data: sanitizedRoles,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@ -1,29 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const {
|
||||
contentTypes: { hasDraftAndPublish },
|
||||
} = require('@strapi/utils');
|
||||
const {
|
||||
AUTHOR_CODE,
|
||||
PUBLISH_ACTION,
|
||||
DELETE_ACTION,
|
||||
UPDATE_ACTION,
|
||||
CREATE_ACTION,
|
||||
READ_ACTION,
|
||||
} = require('../services/constants');
|
||||
|
||||
const BOUND_ACTIONS = [READ_ACTION, CREATE_ACTION, UPDATE_ACTION, DELETE_ACTION, PUBLISH_ACTION];
|
||||
|
||||
const BOUND_ACTIONS_FOR_FIELDS = [READ_ACTION, CREATE_ACTION, UPDATE_ACTION];
|
||||
|
||||
const getBoundActionsBySubject = (role, subject) => {
|
||||
const model = strapi.contentTypes[subject];
|
||||
|
||||
if (role.code === AUTHOR_CODE || !hasDraftAndPublish(model)) {
|
||||
return [READ_ACTION, UPDATE_ACTION, CREATE_ACTION, DELETE_ACTION];
|
||||
}
|
||||
|
||||
return BOUND_ACTIONS;
|
||||
};
|
||||
|
||||
module.exports = { getBoundActionsBySubject, BOUND_ACTIONS, BOUND_ACTIONS_FOR_FIELDS };
|
||||
@ -45,6 +45,22 @@ module.exports = [
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/roles',
|
||||
handler: 'role.create',
|
||||
config: {
|
||||
policies: [
|
||||
'admin::isAuthenticatedAdmin',
|
||||
{
|
||||
name: 'admin::hasPermissions',
|
||||
config: {
|
||||
actions: ['admin::roles.create'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'PUT',
|
||||
path: '/roles/:id',
|
||||
@ -56,4 +72,36 @@ module.exports = [
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'DELETE',
|
||||
path: '/roles/:id',
|
||||
handler: 'role.deleteOne',
|
||||
config: {
|
||||
policies: [
|
||||
'admin::isAuthenticatedAdmin',
|
||||
{
|
||||
name: 'admin::hasPermissions',
|
||||
config: {
|
||||
actions: ['admin::roles.delete'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
method: 'POST',
|
||||
path: '/roles/batch-delete',
|
||||
handler: 'role.deleteMany',
|
||||
config: {
|
||||
policies: [
|
||||
'admin::isAuthenticatedAdmin',
|
||||
{
|
||||
name: 'admin::hasPermissions',
|
||||
config: {
|
||||
actions: ['admin::roles.delete'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@ -1,22 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const {
|
||||
flatMap,
|
||||
reject,
|
||||
isNil,
|
||||
isArray,
|
||||
prop,
|
||||
xor,
|
||||
eq,
|
||||
uniq,
|
||||
map,
|
||||
difference,
|
||||
differenceWith,
|
||||
pipe,
|
||||
} = require('lodash/fp');
|
||||
const { isNil, isArray, prop, xor, eq, map, differenceWith } = require('lodash/fp');
|
||||
const pmap = require('p-map');
|
||||
const { EDITOR_CODE } = require('../constants');
|
||||
const { getBoundActionsBySubject, BOUND_ACTIONS_FOR_FIELDS } = require('../../domain/role');
|
||||
const { getService } = require('../../utils');
|
||||
const permissionDomain = require('../../domain/permission/index');
|
||||
|
||||
@ -195,63 +180,6 @@ const cleanPermissionsInDatabase = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const ensureBoundPermissionsInDatabase = async () => {
|
||||
if (strapi.EE) {
|
||||
return;
|
||||
}
|
||||
|
||||
const contentTypes = Object.values(strapi.contentTypes);
|
||||
const editorRole = await strapi.query('admin::role').findOne({
|
||||
where: { code: EDITOR_CODE },
|
||||
});
|
||||
|
||||
if (isNil(editorRole)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const contentType of contentTypes) {
|
||||
const boundActions = getBoundActionsBySubject(editorRole, contentType.uid);
|
||||
|
||||
const permissions = await findMany({
|
||||
where: {
|
||||
subject: contentType.uid,
|
||||
action: boundActions,
|
||||
role: { id: editorRole.id },
|
||||
},
|
||||
});
|
||||
|
||||
if (permissions.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fields = pipe(
|
||||
flatMap(permissionDomain.getProperty('fields')),
|
||||
reject(isNil),
|
||||
uniq
|
||||
)(permissions);
|
||||
|
||||
// Handle the scenario where permissions are missing
|
||||
const missingActions = difference(map('action', permissions), boundActions);
|
||||
|
||||
if (missingActions.length > 0) {
|
||||
const permissions = pipe(
|
||||
// Create a permission skeleton from the action id
|
||||
map((action) => ({ action, subject: contentType.uid, role: editorRole.id })),
|
||||
// Use the permission domain to create a clean permission from the given object
|
||||
map(permissionDomain.create),
|
||||
// Adds the fields property if the permission action is eligible
|
||||
map((permission) =>
|
||||
BOUND_ACTIONS_FOR_FIELDS.includes(permission.action)
|
||||
? permissionDomain.setProperty('fields', fields, permission)
|
||||
: permission
|
||||
)
|
||||
)(missingActions);
|
||||
|
||||
await createMany(permissions);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createMany,
|
||||
findMany,
|
||||
@ -259,5 +187,4 @@ module.exports = {
|
||||
deleteByIds,
|
||||
findUserPermissions,
|
||||
cleanPermissionsInDatabase,
|
||||
ensureBoundPermissionsInDatabase,
|
||||
};
|
||||
|
||||
@ -5,8 +5,6 @@ const { createAuthRequest } = require('../../../../../test/helpers/request');
|
||||
const { createStrapiInstance, superAdmin } = require('../../../../../test/helpers/strapi');
|
||||
const { createUtils } = require('../../../../../test/helpers/utils');
|
||||
|
||||
const edition = process.env.STRAPI_DISABLE_EE === 'true' ? 'CE' : 'EE';
|
||||
|
||||
const internals = {
|
||||
role: null,
|
||||
};
|
||||
@ -21,20 +19,14 @@ describe('Admin Auth End to End', () => {
|
||||
rq = await createAuthRequest({ strapi });
|
||||
utils = createUtils(strapi);
|
||||
|
||||
if (edition === 'EE') {
|
||||
internals.role = await utils.createRole({
|
||||
name: 'auth_test_role',
|
||||
description: 'Only used for auth crud test (api)',
|
||||
});
|
||||
} else {
|
||||
internals.role = await utils.getSuperAdminRole();
|
||||
}
|
||||
internals.role = await utils.createRole({
|
||||
name: 'auth_test_role',
|
||||
description: 'Only used for auth crud test (api)',
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (edition === 'EE') {
|
||||
await utils.deleteRolesById([internals.role.id]);
|
||||
}
|
||||
await utils.deleteRolesById([internals.role.id]);
|
||||
|
||||
await strapi.destroy();
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -6,197 +6,189 @@ const { createStrapiInstance } = require('../../../../../test/helpers/strapi');
|
||||
const { createRequest, createAuthRequest } = require('../../../../../test/helpers/request');
|
||||
const { createUtils } = require('../../../../../test/helpers/utils');
|
||||
|
||||
const edition = process.env.STRAPI_DISABLE_EE === 'true' ? 'CE' : 'EE';
|
||||
describe('Admin Permissions - Conditions', () => {
|
||||
let strapi;
|
||||
let utils;
|
||||
const builder = createTestBuilder();
|
||||
const requests = {
|
||||
public: null,
|
||||
admin: null,
|
||||
};
|
||||
|
||||
if (edition === 'EE') {
|
||||
describe('Admin Permissions - Conditions', () => {
|
||||
let strapi;
|
||||
let utils;
|
||||
const builder = createTestBuilder();
|
||||
const requests = {
|
||||
public: null,
|
||||
admin: null,
|
||||
};
|
||||
|
||||
const localTestData = {
|
||||
models: {
|
||||
article: {
|
||||
singularName: 'article',
|
||||
pluralName: 'articles',
|
||||
displayName: 'Article',
|
||||
attributes: {
|
||||
title: {
|
||||
type: 'string',
|
||||
},
|
||||
price: {
|
||||
type: 'integer',
|
||||
},
|
||||
const localTestData = {
|
||||
models: {
|
||||
article: {
|
||||
singularName: 'article',
|
||||
pluralName: 'articles',
|
||||
displayName: 'Article',
|
||||
attributes: {
|
||||
title: {
|
||||
type: 'string',
|
||||
},
|
||||
price: {
|
||||
type: 'integer',
|
||||
},
|
||||
},
|
||||
},
|
||||
entry: {
|
||||
name: 'Test Article',
|
||||
price: 999,
|
||||
},
|
||||
entry: {
|
||||
name: 'Test Article',
|
||||
price: 999,
|
||||
},
|
||||
role: {
|
||||
name: 'foobar',
|
||||
description: 'A dummy test role',
|
||||
},
|
||||
permissions: [
|
||||
{
|
||||
action: 'plugin::content-manager.explorer.create',
|
||||
subject: 'api::article.article',
|
||||
fields: null,
|
||||
conditions: [],
|
||||
},
|
||||
role: {
|
||||
name: 'foobar',
|
||||
description: 'A dummy test role',
|
||||
{
|
||||
action: 'plugin::content-manager.explorer.read',
|
||||
subject: 'api::article.article',
|
||||
fields: null,
|
||||
conditions: ['admin::has-same-role-as-creator'],
|
||||
},
|
||||
permissions: [
|
||||
{
|
||||
action: 'plugin::content-manager.explorer.create',
|
||||
subject: 'api::article.article',
|
||||
fields: null,
|
||||
conditions: [],
|
||||
},
|
||||
{
|
||||
action: 'plugin::content-manager.explorer.read',
|
||||
subject: 'api::article.article',
|
||||
fields: null,
|
||||
conditions: ['admin::has-same-role-as-creator'],
|
||||
},
|
||||
{
|
||||
action: 'plugin::content-manager.explorer.delete',
|
||||
subject: 'api::article.article',
|
||||
fields: null,
|
||||
conditions: ['admin::is-creator'],
|
||||
},
|
||||
],
|
||||
userPassword: 'fooBar42',
|
||||
users: [
|
||||
{ firstname: 'Alice', lastname: 'Foo', email: 'alice.foo@test.com' },
|
||||
{ firstname: 'Bob', lastname: 'Bar', email: 'bob.bar@test.com' },
|
||||
],
|
||||
};
|
||||
{
|
||||
action: 'plugin::content-manager.explorer.delete',
|
||||
subject: 'api::article.article',
|
||||
fields: null,
|
||||
conditions: ['admin::is-creator'],
|
||||
},
|
||||
],
|
||||
userPassword: 'fooBar42',
|
||||
users: [
|
||||
{ firstname: 'Alice', lastname: 'Foo', email: 'alice.foo@test.com' },
|
||||
{ firstname: 'Bob', lastname: 'Bar', email: 'bob.bar@test.com' },
|
||||
],
|
||||
};
|
||||
|
||||
const createFixtures = async () => {
|
||||
// Login with admin and init admin tools
|
||||
requests.admin = await createAuthRequest({ strapi });
|
||||
requests.public = createRequest({ strapi });
|
||||
const createFixtures = async () => {
|
||||
// Login with admin and init admin tools
|
||||
requests.admin = await createAuthRequest({ strapi });
|
||||
requests.public = createRequest({ strapi });
|
||||
|
||||
// Create the foobar role
|
||||
const role = await utils.createRole(localTestData.role);
|
||||
// Create the foobar role
|
||||
const role = await utils.createRole(localTestData.role);
|
||||
|
||||
// Assign permissions to the foobar role
|
||||
const permissions = await utils.assignPermissionsToRole(role.id, localTestData.permissions);
|
||||
Object.assign(role, { permissions });
|
||||
// Assign permissions to the foobar role
|
||||
const permissions = await utils.assignPermissionsToRole(role.id, localTestData.permissions);
|
||||
Object.assign(role, { permissions });
|
||||
|
||||
// Create users with the new role & create associated auth requests
|
||||
const users = [];
|
||||
// Create users with the new role & create associated auth requests
|
||||
const users = [];
|
||||
|
||||
for (let i = 0; i < localTestData.users.length; i += 1) {
|
||||
const userFixture = localTestData.users[i];
|
||||
const userAttributes = {
|
||||
...userFixture,
|
||||
password: localTestData.userPassword,
|
||||
roles: [role.id],
|
||||
};
|
||||
for (let i = 0; i < localTestData.users.length; i += 1) {
|
||||
const userFixture = localTestData.users[i];
|
||||
const userAttributes = {
|
||||
...userFixture,
|
||||
password: localTestData.userPassword,
|
||||
roles: [role.id],
|
||||
};
|
||||
|
||||
const createdUser = await utils.createUser(userAttributes);
|
||||
const createdUser = await utils.createUser(userAttributes);
|
||||
|
||||
requests[createdUser.id] = await createAuthRequest({ strapi, userInfo: createdUser });
|
||||
requests[createdUser.id] = await createAuthRequest({ strapi, userInfo: createdUser });
|
||||
|
||||
users.push(createdUser);
|
||||
}
|
||||
users.push(createdUser);
|
||||
}
|
||||
|
||||
// Update the local data store
|
||||
Object.assign(localTestData, { role, permissions, users });
|
||||
};
|
||||
// Update the local data store
|
||||
Object.assign(localTestData, { role, permissions, users });
|
||||
};
|
||||
|
||||
const getUserRequest = (idx) => requests[localTestData.users[idx].id];
|
||||
const getModelName = () => localTestData.models.article.singularName;
|
||||
const getUserRequest = (idx) => requests[localTestData.users[idx].id];
|
||||
const getModelName = () => localTestData.models.article.singularName;
|
||||
|
||||
const deleteFixtures = async () => {
|
||||
// Delete users
|
||||
const usersId = localTestData.users.map(prop('id'));
|
||||
await utils.deleteUsersById(usersId);
|
||||
const deleteFixtures = async () => {
|
||||
// Delete users
|
||||
const usersId = localTestData.users.map(prop('id'));
|
||||
await utils.deleteUsersById(usersId);
|
||||
|
||||
// Delete the foobar role
|
||||
await utils.deleteRolesById([localTestData.role.id]);
|
||||
};
|
||||
// Delete the foobar role
|
||||
await utils.deleteRolesById([localTestData.role.id]);
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
await builder.addContentType(localTestData.models.article).build();
|
||||
beforeAll(async () => {
|
||||
await builder.addContentType(localTestData.models.article).build();
|
||||
|
||||
strapi = await createStrapiInstance();
|
||||
utils = createUtils(strapi);
|
||||
strapi = await createStrapiInstance();
|
||||
utils = createUtils(strapi);
|
||||
|
||||
await createFixtures();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await deleteFixtures();
|
||||
|
||||
await strapi.destroy();
|
||||
await builder.cleanup();
|
||||
});
|
||||
|
||||
test('User A can create an entry', async () => {
|
||||
const rq = getUserRequest(0);
|
||||
const modelName = getModelName();
|
||||
const res = await rq({
|
||||
method: 'POST',
|
||||
url: `/content-manager/collection-types/api::${modelName}.${modelName}`,
|
||||
body: localTestData.entry,
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
localTestData.entry = res.body;
|
||||
});
|
||||
|
||||
test('User A can read its entry', async () => {
|
||||
const { id } = localTestData.entry;
|
||||
const modelName = getModelName();
|
||||
const rq = getUserRequest(0);
|
||||
const res = await rq({
|
||||
method: 'GET',
|
||||
url: `/content-manager/collection-types/api::${modelName}.${modelName}/${id}`,
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toMatchObject(localTestData.entry);
|
||||
});
|
||||
|
||||
test('User B can read the entry created by user A', async () => {
|
||||
const { id } = localTestData.entry;
|
||||
const modelName = getModelName();
|
||||
const rq = getUserRequest(1);
|
||||
const res = await rq({
|
||||
method: 'GET',
|
||||
url: `/content-manager/collection-types/api::${modelName}.${modelName}/${id}`,
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toMatchObject(localTestData.entry);
|
||||
});
|
||||
|
||||
test('User B cannot delete the entry created by user A', async () => {
|
||||
const { id } = localTestData.entry;
|
||||
const modelName = getModelName();
|
||||
const rq = getUserRequest(1);
|
||||
const res = await rq({
|
||||
method: 'DELETE',
|
||||
url: `/content-manager/collection-types/api::${modelName}.${modelName}/${id}`,
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(403);
|
||||
});
|
||||
|
||||
test('User A can delete its entry', async () => {
|
||||
const { id } = localTestData.entry;
|
||||
const modelName = getModelName();
|
||||
const rq = getUserRequest(0);
|
||||
const res = await rq({
|
||||
method: 'DELETE',
|
||||
url: `/content-manager/collection-types/api::${modelName}.${modelName}/${id}`,
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toMatchObject(localTestData.entry);
|
||||
});
|
||||
await createFixtures();
|
||||
});
|
||||
} else {
|
||||
describe('Admin Permissions - Conditions ', () => {
|
||||
test.skip('Only in EE', () => {});
|
||||
|
||||
afterAll(async () => {
|
||||
await deleteFixtures();
|
||||
|
||||
await strapi.destroy();
|
||||
await builder.cleanup();
|
||||
});
|
||||
}
|
||||
|
||||
test('User A can create an entry', async () => {
|
||||
const rq = getUserRequest(0);
|
||||
const modelName = getModelName();
|
||||
const res = await rq({
|
||||
method: 'POST',
|
||||
url: `/content-manager/collection-types/api::${modelName}.${modelName}`,
|
||||
body: localTestData.entry,
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
localTestData.entry = res.body;
|
||||
});
|
||||
|
||||
test('User A can read its entry', async () => {
|
||||
const { id } = localTestData.entry;
|
||||
const modelName = getModelName();
|
||||
const rq = getUserRequest(0);
|
||||
const res = await rq({
|
||||
method: 'GET',
|
||||
url: `/content-manager/collection-types/api::${modelName}.${modelName}/${id}`,
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toMatchObject(localTestData.entry);
|
||||
});
|
||||
|
||||
test('User B can read the entry created by user A', async () => {
|
||||
const { id } = localTestData.entry;
|
||||
const modelName = getModelName();
|
||||
const rq = getUserRequest(1);
|
||||
const res = await rq({
|
||||
method: 'GET',
|
||||
url: `/content-manager/collection-types/api::${modelName}.${modelName}/${id}`,
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toMatchObject(localTestData.entry);
|
||||
});
|
||||
|
||||
test('User B cannot delete the entry created by user A', async () => {
|
||||
const { id } = localTestData.entry;
|
||||
const modelName = getModelName();
|
||||
const rq = getUserRequest(1);
|
||||
const res = await rq({
|
||||
method: 'DELETE',
|
||||
url: `/content-manager/collection-types/api::${modelName}.${modelName}/${id}`,
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(403);
|
||||
});
|
||||
|
||||
test('User A can delete its entry', async () => {
|
||||
const { id } = localTestData.entry;
|
||||
const modelName = getModelName();
|
||||
const rq = getUserRequest(0);
|
||||
const res = await rq({
|
||||
method: 'DELETE',
|
||||
url: `/content-manager/collection-types/api::${modelName}.${modelName}/${id}`,
|
||||
});
|
||||
|
||||
expect(res.statusCode).toBe(200);
|
||||
expect(res.body).toMatchObject(localTestData.entry);
|
||||
});
|
||||
});
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -5,8 +5,6 @@ const { createStrapiInstance } = require('../../../../../test/helpers/strapi');
|
||||
const { createAuthRequest } = require('../../../../../test/helpers/request');
|
||||
const { createUtils } = require('../../../../../test/helpers/utils');
|
||||
|
||||
const edition = process.env.STRAPI_DISABLE_EE === 'true' ? 'CE' : 'EE';
|
||||
|
||||
const omitTimestamps = omit(['updatedAt', 'createdAt']);
|
||||
const omitRegistrationToken = omit(['registrationToken']);
|
||||
|
||||
@ -59,11 +57,7 @@ describe('Admin User CRUD (api)', () => {
|
||||
rq = await createAuthRequest({ strapi });
|
||||
utils = createUtils(strapi);
|
||||
|
||||
if (edition === 'EE') {
|
||||
testData.role = await createUserRole();
|
||||
} else {
|
||||
testData.role = await utils.getSuperAdminRole();
|
||||
}
|
||||
testData.role = await createUserRole();
|
||||
|
||||
testData.firstSuperAdminUser = rq.getLoggedUser();
|
||||
testData.superAdminRole = await utils.getSuperAdminRole();
|
||||
@ -71,9 +65,8 @@ describe('Admin User CRUD (api)', () => {
|
||||
|
||||
// Cleanup actions
|
||||
afterAll(async () => {
|
||||
if (edition === 'EE') {
|
||||
await utils.deleteRolesById([testData.role.id]);
|
||||
}
|
||||
await utils.deleteRolesById([testData.role.id]);
|
||||
|
||||
await strapi.destroy();
|
||||
});
|
||||
|
||||
|
||||
@ -1,83 +1,9 @@
|
||||
'use strict';
|
||||
|
||||
const _ = require('lodash');
|
||||
const { yup, validateYupSchema } = require('@strapi/utils');
|
||||
const { getService } = require('../utils');
|
||||
const { AUTHOR_CODE, PUBLISH_ACTION } = require('../services/constants');
|
||||
const {
|
||||
BOUND_ACTIONS_FOR_FIELDS,
|
||||
BOUND_ACTIONS,
|
||||
getBoundActionsBySubject,
|
||||
} = require('../domain/role');
|
||||
const validators = require('./common-validators');
|
||||
|
||||
// validatedUpdatePermissionsInput
|
||||
|
||||
const actionFieldsAreEqual = (a, b) => {
|
||||
const aFields = a.properties.fields || [];
|
||||
const bFields = b.properties.fields || [];
|
||||
|
||||
return _.isEqual(aFields.sort(), bFields.sort());
|
||||
};
|
||||
|
||||
const haveSameFieldsAsOtherActions = (a, i, allActions) =>
|
||||
allActions.slice(i + 1).every((b) => actionFieldsAreEqual(a, b));
|
||||
|
||||
const checkPermissionsAreBound = (role) =>
|
||||
function (permissions) {
|
||||
const permsBySubject = _.groupBy(
|
||||
permissions.filter((perm) => BOUND_ACTIONS.includes(perm.action)),
|
||||
'subject'
|
||||
);
|
||||
|
||||
for (const [subject, perms] of Object.entries(permsBySubject)) {
|
||||
const boundActions = getBoundActionsBySubject(role, subject);
|
||||
const missingActions =
|
||||
_.xor(
|
||||
perms.map((p) => p.action),
|
||||
boundActions
|
||||
).length !== 0;
|
||||
if (missingActions) return false;
|
||||
|
||||
const permsBoundByFields = perms.filter((p) => BOUND_ACTIONS_FOR_FIELDS.includes(p.action));
|
||||
const everyActionsHaveSameFields = _.every(permsBoundByFields, haveSameFieldsAsOtherActions);
|
||||
if (!everyActionsHaveSameFields) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const noPublishPermissionForAuthorRole = (role) =>
|
||||
function (permissions) {
|
||||
const isAuthor = role.code === AUTHOR_CODE;
|
||||
const hasPublishPermission = permissions.some((perm) => perm.action === PUBLISH_ACTION);
|
||||
|
||||
return !(isAuthor && hasPublishPermission);
|
||||
};
|
||||
|
||||
const getUpdatePermissionsSchemas = (role) => [
|
||||
validators.updatePermissions,
|
||||
yup.object().shape({ permissions: actionsExistSchema.clone() }),
|
||||
yup.object().shape({
|
||||
permissions: yup
|
||||
.array()
|
||||
.test(
|
||||
'author-no-publish',
|
||||
'The author role cannot have the publish permission.',
|
||||
noPublishPermissionForAuthorRole(role)
|
||||
),
|
||||
}),
|
||||
yup.object().shape({
|
||||
permissions: yup
|
||||
.array()
|
||||
.test(
|
||||
'are-bond',
|
||||
'Permissions have to be defined all together for a subject field or not at all',
|
||||
checkPermissionsAreBound(role)
|
||||
),
|
||||
}),
|
||||
];
|
||||
|
||||
const checkPermissionsSchema = yup.object().shape({
|
||||
permissions: yup.array().of(
|
||||
yup
|
||||
@ -91,13 +17,6 @@ const checkPermissionsSchema = yup.object().shape({
|
||||
),
|
||||
});
|
||||
|
||||
const validatedUpdatePermissionsInput = async (permissions, role) => {
|
||||
const schemas = getUpdatePermissionsSchemas(role);
|
||||
for (const schema of schemas) {
|
||||
await validateYupSchema(schema)(permissions);
|
||||
}
|
||||
};
|
||||
|
||||
// validatePermissionsExist
|
||||
|
||||
const checkPermissionsExist = function (permissions) {
|
||||
@ -131,7 +50,8 @@ const actionsExistSchema = yup
|
||||
// exports
|
||||
|
||||
module.exports = {
|
||||
validatedUpdatePermissionsInput,
|
||||
// validatedUpdatePermissionsInput,
|
||||
validatedUpdatePermissionsInput: validateYupSchema(validators.updatePermissions),
|
||||
validatePermissionsExist: validateYupSchema(actionsExistSchema),
|
||||
validateCheckPermissionsInput: validateYupSchema(checkPermissionsSchema),
|
||||
};
|
||||
|
||||
@ -2,6 +2,47 @@
|
||||
|
||||
const { yup, validateYupSchema } = require('@strapi/utils');
|
||||
|
||||
const roleCreateSchema = yup
|
||||
.object()
|
||||
.shape({
|
||||
name: yup.string().min(1).required(),
|
||||
description: yup.string().nullable(),
|
||||
})
|
||||
.noUnknown();
|
||||
|
||||
const rolesDeleteSchema = yup
|
||||
.object()
|
||||
.shape({
|
||||
ids: yup
|
||||
.array()
|
||||
.of(yup.strapiID())
|
||||
.min(1)
|
||||
.required()
|
||||
.test('roles-deletion-checks', 'Roles deletion checks have failed', async function (ids) {
|
||||
try {
|
||||
await strapi.admin.services.role.checkRolesIdForDeletion(ids);
|
||||
} catch (e) {
|
||||
return this.createError({ path: 'ids', message: e.message });
|
||||
}
|
||||
|
||||
return true;
|
||||
}),
|
||||
})
|
||||
.noUnknown();
|
||||
|
||||
const roleDeleteSchema = yup
|
||||
.strapiID()
|
||||
.required()
|
||||
.test('no-admin-single-delete', 'Role deletion checks have failed', async function (id) {
|
||||
try {
|
||||
await strapi.admin.services.role.checkRolesIdForDeletion([id]);
|
||||
} catch (e) {
|
||||
return this.createError({ path: 'id', message: e.message });
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
const roleUpdateSchema = yup
|
||||
.object()
|
||||
.shape({
|
||||
@ -12,4 +53,7 @@ const roleUpdateSchema = yup
|
||||
|
||||
module.exports = {
|
||||
validateRoleUpdateInput: validateYupSchema(roleUpdateSchema),
|
||||
validateRoleCreateInput: validateYupSchema(roleCreateSchema),
|
||||
validateRolesDeleteInput: validateYupSchema(rolesDeleteSchema),
|
||||
validateRoleDeleteInput: validateYupSchema(roleDeleteSchema),
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user