mirror of
https://github.com/strapi/strapi.git
synced 2025-11-03 11:25:17 +00:00
* Fix RBAC permissions without subject ignoring conditions * Add unit test for nil subject in the permission engine
444 lines
12 KiB
JavaScript
444 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)', 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,
|
|
});
|
|
});
|
|
|
|
test('It should register the permission without a condition (non required 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,
|
|
};
|
|
|
|
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 (non required 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: {
|
|
$and: [
|
|
{
|
|
$or: [{ created_by: user.firstname }],
|
|
},
|
|
],
|
|
},
|
|
};
|
|
|
|
expect(registerFn).toHaveBeenCalledWith(expected);
|
|
});
|
|
});
|
|
|
|
test('It should register the condition even if the subject is Nil', async () => {
|
|
const permission = {
|
|
action: 'read',
|
|
subject: null,
|
|
properties: {},
|
|
conditions: ['plugins::test.isCreatedBy'],
|
|
};
|
|
|
|
const user = getUser('alice');
|
|
const can = jest.fn();
|
|
const registerFn = engine.createRegisterFunction(can, {}, user);
|
|
|
|
await engine.evaluate({ permission, user, registerFn });
|
|
|
|
expect(can).toHaveBeenCalledWith('read', 'all', undefined, {
|
|
$and: [{ $or: [{ created_by: user.firstname }] }],
|
|
});
|
|
});
|
|
|
|
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', async () => {
|
|
await 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', async () => {
|
|
await registerFn({
|
|
action: 'read',
|
|
subject: 'article',
|
|
fields: '*',
|
|
condition: { created_by: 1 },
|
|
});
|
|
|
|
expect(can).toHaveBeenCalledTimes(1);
|
|
expect(can).toHaveBeenCalledWith('read', 'article', '*', { created_by: 1 });
|
|
});
|
|
|
|
test(`It should use 'all' as a subject if it's Nil`, async () => {
|
|
await registerFn({
|
|
action: 'read',
|
|
subject: null,
|
|
fields: null,
|
|
condition: { created_by: 1 },
|
|
});
|
|
|
|
expect(can).toHaveBeenCalledTimes(1);
|
|
expect(can).toHaveBeenCalledWith('read', 'all', null, { 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);
|
|
});
|
|
});
|
|
});
|