strapi/packages/strapi-admin/services/__tests__/permissions.engine.test.js
Jean-Sébastien Herbaux 1e5b1c99f6
I18n/ permissions rework (#9535)
* Add a domain layer for the permission, rework the engine handling of the permissions

Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu>

* Add permissions-fields-to-properties migration for the admin

Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu>

* Removes useless console.log

Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu>

* Remove debug logLevel from provider-login.test.e2e.js

Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu>

* Adds the new layout for the GET permissions, allow to subscribe to actionRegistered events, adds i18n handlers

Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu>

* Fix typo

Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu>

* Update permissions validators

Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu>

* Update unit tests

Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu>

* Update integrations test + fix some validation issues

Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu>

* Change plugins & settings section format for the permissions layout

* only return locales property to localized subjects for the permission's layout

* Do not send the locales property to the permission's layout when there is no locales created

* Add the 'locales' property to publish & delete routes

* Fix unwanted mutation of the sections builder states on multiple builds

* Fix units tests with (new engine)

* Fix admin-role e2e test - Add locales property to the update payload

* fix e2e testsé

* Update e2e snapshots

* Fix unit test for i18n bootstrap

* Add mocks for i18n/bootstrap test

* Fix has-locale condition & updatePermission validator

* Avoid mutation in migration, always authorize super admin for has-locales condition

* Rework rbac domain objects, add a hook module and a provider factory

* Remove old providers

* Update the admin services & tests for the new rbac domain & providers

* Fix tests, bootstrap functions & services following rbac domain rework

* Update migration runner

* PR comments

Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu>

* Remove useless console.log

* Fix sanitizeCondition bug

* Section builder rework

* Add test for the section-builder section & add jsdoc for the permission domain

* pr comments (without the migrations)

* fix fields-to-properties migration

* Add jsdoc for the sections-builder

* Moves createBoundAbstractDomain from permission domain to the engine service

* Remove debug logLevel for admin role test (e2e)

* Fix core-store

* Fix hooks & move business logic from i18n bootstrap to dedicated services

* add route get-non-localized-fields

* use write and read permission

* refacto

* add input validator

* add route doc

* handle ST

Co-authored-by: Pierre Noël <petersg83@gmail.com>
Co-authored-by: Alexandre BODIN <alexandrebodin@users.noreply.github.com>
2021-03-25 14:59:44 +01:00

404 lines
12 KiB
JavaScript

'use strict';
const _ = require('lodash');
const { subject } = require('@casl/ability');
const createConditionProvider = require('../../domain/condition/provider');
const createPermissionsEngine = require('../permission/engine');
describe('Permissions Engine', () => {
let conditionProvider;
let engine;
const localTestData = {
users: {
bob: {
firstname: 'Bob',
title: 'guest',
roles: [{ id: 1 }, { id: 2 }],
},
alice: {
firstname: 'Alice',
title: 'admin',
roles: [{ id: 1 }],
},
kai: {
firstname: 'Kai',
title: 'admin',
roles: [{ id: 3 }],
},
foo: {
firstname: 'Foo',
title: 'Bar',
roles: [{ id: 4 }],
},
},
roles: {
1: {
permissions: [
{
action: 'read',
subject: 'article',
properties: { fields: ['**'] },
conditions: ['plugins::test.isBob'],
},
{
action: 'read',
subject: 'user',
properties: { fields: ['title'] },
conditions: ['plugins::test.isAdmin'],
},
],
},
2: {
permissions: [
{
action: 'post',
subject: 'article',
properties: { fields: ['*'] },
conditions: ['plugins::test.isBob'],
},
],
},
3: {
permissions: [
{
action: 'read',
subject: 'user',
properties: { fields: ['title'] },
conditions: ['plugins::test.isContainedIn'],
},
],
},
4: {
permissions: [
{
action: 'read',
subject: 'user',
properties: { fields: [] },
},
],
},
},
conditions: [
{
plugin: 'test',
name: 'isBob',
category: 'default',
handler: async user => new Promise(resolve => resolve(user.firstname === 'Bob')),
},
{
plugin: 'test',
name: 'isAdmin',
category: 'default',
handler: user => user.title === 'admin',
},
{
plugin: 'test',
name: 'isCreatedBy',
category: 'default',
handler: user => ({ created_by: user.firstname }),
},
{
plugin: 'test',
name: 'isContainedIn',
category: 'default',
handler: { firstname: { $in: ['Alice', 'Foo'] } },
},
],
};
const getUser = name => localTestData.users[name];
beforeEach(async () => {
global.strapi = {
isLoaded: false,
admin: {
services: {
permission: {
actionProvider: {
get() {
return { applyToProperties: undefined };
},
},
findUserPermissions: jest.fn(({ roles }) =>
_.reduce(
localTestData.roles,
(acc, { permissions: value }, key) => {
return roles.map(_.property('id')).includes(_.toNumber(key))
? [...acc, ...value]
: acc;
},
[]
)
),
},
},
},
};
conditionProvider = createConditionProvider();
await conditionProvider.registerMany(localTestData.conditions);
engine = createPermissionsEngine(conditionProvider);
jest.spyOn(engine, 'evaluate');
jest.spyOn(engine, 'createRegisterFunction');
jest.spyOn(engine, 'generateAbilityCreatorFor');
});
afterEach(() => {
jest.clearAllMocks();
});
describe('GenerateUserAbility', () => {
test('Successfully creates an ability for Bob', async () => {
const user = getUser('bob');
const ability = await engine.generateUserAbility(user);
const expected = [
{
action: 'read',
fields: ['**'],
subject: 'article',
},
{
action: 'post',
fields: ['*'],
subject: 'article',
},
];
expect(engine.generateAbilityCreatorFor).toHaveBeenCalledWith(user);
expect(_.orderBy(ability.rules, ['subject'], ['asc'])).toMatchObject(expected);
expect(ability.can('post', 'article')).toBeTruthy();
expect(ability.can('post', 'article', 'user')).toBeTruthy();
expect(ability.can('post', 'article', 'user.nested')).toBeFalsy();
expect(ability.can('read', 'article')).toBeTruthy();
expect(ability.can('read', 'article', 'title')).toBeTruthy();
expect(ability.can('read', 'article', 'title.nested')).toBeTruthy();
expect(ability.can('read', 'user')).toBeFalsy();
expect(ability.can('read', 'user', 'firstname')).toBeFalsy();
expect(ability.can('read', 'user', 'title')).toBeFalsy();
expect(ability.can('read', 'user', 'title.nested')).toBeFalsy();
});
test('Successfully creates an ability for Alice', async () => {
const user = getUser('alice');
const ability = await engine.generateUserAbility(user);
const expected = [
{
action: 'read',
fields: ['title'],
subject: 'user',
},
];
expect(engine.generateAbilityCreatorFor).toHaveBeenCalledWith(user);
expect(_.orderBy(ability.rules, ['action'], ['asc'])).toMatchObject(expected);
expect(ability.can('post', 'article')).toBeFalsy();
expect(ability.can('post', 'article', 'user')).toBeFalsy();
expect(ability.can('post', 'article', 'user.nested')).toBeFalsy();
expect(ability.can('read', 'article')).toBeFalsy();
expect(ability.can('read', 'article', 'title')).toBeFalsy();
expect(ability.can('read', 'article', 'title.nested')).toBeFalsy();
expect(ability.can('read', 'user')).toBeTruthy();
expect(ability.can('read', 'user', 'firstname')).toBeFalsy();
expect(ability.can('read', 'user', 'title')).toBeTruthy();
expect(ability.can('read', 'user', 'title.nested')).toBeFalsy();
});
test('Ignore permission on empty fields array', async () => {
const user = getUser('foo');
const ability = await engine.generateUserAbility(user);
expect(engine.generateAbilityCreatorFor).toHaveBeenCalledWith(user);
expect(ability.rules).toHaveLength(0);
expect(ability.can('read', 'user')).toBeFalsy();
});
describe('Use objects as subject', () => {
let ability;
beforeAll(async () => {
const user = getUser('kai');
ability = await engine.generateUserAbility(user);
});
test('Fails to validate the object condition', () => {
const args = ['read', subject('user', { firstname: 'Bar' }), 'title'];
expect(ability.can(...args)).toBeFalsy();
});
test('Fails to read a restricted field', () => {
const args = ['read', subject('user', { firstname: 'Foo' }), 'bar'];
expect(ability.can(...args)).toBeFalsy();
});
test('Successfully validate the permission', () => {
const args = ['read', subject('user', { firstname: 'Foo' }), 'title'];
expect(ability.can(...args)).toBeTruthy();
});
});
});
describe('Generate Ability Creator For', () => {
test('Successfully generates an ability creator for Alice', async () => {
const user = getUser('alice');
const abilityCreator = engine.generateAbilityCreatorFor(user);
const ability = await abilityCreator([]);
expect(abilityCreator).not.toBeUndefined();
expect(typeof abilityCreator).toBe('function');
expect(ability.rules).toStrictEqual([]);
});
});
describe('Evaluate', () => {
test('It should register the permission (no conditions / true result)', async () => {
const permission = { action: 'read', subject: 'article', properties: { fields: ['title'] } };
const user = getUser('alice');
const registerFn = jest.fn();
await engine.evaluate({ permission, user, registerFn });
expect(registerFn).toHaveBeenCalledWith({
..._.pick(permission, ['action', 'subject']),
fields: permission.properties.fields,
condition: true,
});
});
test('It should register the permission (conditions / true result)', async () => {
const permission = {
action: 'read',
subject: 'article',
properties: { fields: ['title'] },
conditions: ['plugins::test.isAdmin'],
};
const user = getUser('alice');
const registerFn = jest.fn();
await engine.evaluate({ permission, user, registerFn });
const expected = {
..._.omit(permission, ['conditions', 'properties']),
fields: permission.properties.fields,
condition: true,
};
expect(registerFn).toHaveBeenCalledWith(expected);
});
test('It should not register the permission (conditions / false result)', async () => {
const permission = {
action: 'read',
subject: 'article',
properties: { fields: ['title'] },
conditions: ['plugins::test.isBob'],
};
const user = getUser('alice');
const registerFn = jest.fn();
await engine.evaluate({ permission, user, registerFn });
expect(registerFn).not.toHaveBeenCalled();
});
test('It should register the permission (conditions / object result)', async () => {
const permission = {
action: 'read',
subject: 'article',
properties: { fields: ['title'] },
conditions: ['plugins::test.isCreatedBy'],
};
global.strapi.admin.services.permission.actionProvider.get = () => ({
applyToProperties: ['fields'],
});
const user = getUser('alice');
const registerFn = jest.fn();
await engine.evaluate({ permission, user, registerFn });
const expected = {
..._.omit(permission, ['conditions', 'properties']),
fields: permission.properties.fields,
condition: { created_by: user.firstname },
};
expect(registerFn).toHaveBeenCalledWith(expected);
});
});
describe('Create Register Function', () => {
let can;
let registerFn;
beforeEach(() => {
can = jest.fn();
registerFn = engine.createRegisterFunction(can);
});
test('It should calls the can function without any condition', () => {
registerFn({ action: 'read', subject: 'article', fields: '*', condition: true });
expect(can).toHaveBeenCalledTimes(1);
expect(can).toHaveBeenCalledWith('read', 'article', '*', undefined);
});
test('It should calls the can function with a condition', () => {
registerFn({ action: 'read', subject: 'article', fields: '*', condition: { created_by: 1 } });
expect(can).toHaveBeenCalledTimes(1);
expect(can).toHaveBeenCalledWith('read', 'article', '*', { created_by: 1 });
});
});
describe('Check Many', () => {
let ability;
const permissions = [
{ action: 'read', subject: 'user', field: 'title' },
{ action: 'post', subject: 'article' },
];
beforeEach(() => {
ability = { can: jest.fn(() => true) };
});
afterEach(() => {
jest.clearAllMocks();
});
test('Using curried version of checkMany', () => {
const checkMany = engine.checkMany(ability);
const res = checkMany(permissions);
expect(res).toHaveLength(permissions.length);
expect(ability.can).toHaveBeenCalledTimes(2);
});
test('Using raw version of checkMany', () => {
const res = engine.checkMany(ability, permissions);
expect(res).toHaveLength(permissions.length);
expect(ability.can).toHaveBeenCalledTimes(2);
});
});
});