Move RBAC into CE

This commit is contained in:
Alexandre Bodin 2023-03-06 21:46:45 +01:00
parent b9e92de1af
commit e0e2084422
20 changed files with 1510 additions and 2263 deletions

View File

@ -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'),
};

View File

@ -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,
},
};
},
};

View File

@ -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,
});
},
};

View File

@ -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,
});
},
};

View File

@ -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
{

View File

@ -1,8 +0,0 @@
'use strict';
const { validateYupSchema } = require('@strapi/utils');
const validators = require('../../../server/validation/common-validators');
module.exports = {
validatedUpdatePermissionsInput: validateYupSchema(validators.updatePermissions),
};

View File

@ -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),

View File

@ -85,7 +85,6 @@ module.exports = async () => {
await roleService.resetSuperAdminPermissions();
await roleService.displayWarningIfNoSuperAdmin();
await permissionService.ensureBoundPermissionsInDatabase();
await permissionService.cleanPermissionsInDatabase();
await userService.displayWarningIfUsersDontHaveRole();

View File

@ -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);

View File

@ -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,
});
},
};

View File

@ -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 };

View File

@ -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'],
},
},
],
},
},
];

View File

@ -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,
};

View File

@ -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

View File

@ -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

View File

@ -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();
});

View File

@ -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),
};

View File

@ -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),
};