mirror of
https://github.com/strapi/strapi.git
synced 2025-10-22 21:43:53 +00:00
Merge pull request #13896 from strapi/api-token-v2/up-engine-impl
Make users-permissions auth strategy use the content API permissions engine
This commit is contained in:
commit
a6c7c028ad
@ -1,443 +0,0 @@
|
|||||||
'use strict';
|
|
||||||
|
|
||||||
const _ = require('lodash');
|
|
||||||
const { subject } = require('@casl/ability');
|
|
||||||
const createConditionProvider = require('../../domain/condition/provider');
|
|
||||||
const createPermissionsEngine = require('../permission/engine');
|
|
||||||
|
|
||||||
describe.skip('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: ['plugin::test.isBob'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
action: 'read',
|
|
||||||
subject: 'user',
|
|
||||||
properties: { fields: ['title'] },
|
|
||||||
conditions: ['plugin::test.isAdmin'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
2: {
|
|
||||||
permissions: [
|
|
||||||
{
|
|
||||||
action: 'post',
|
|
||||||
subject: 'article',
|
|
||||||
properties: { fields: ['*'] },
|
|
||||||
conditions: ['plugin::test.isBob'],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
3: {
|
|
||||||
permissions: [
|
|
||||||
{
|
|
||||||
action: 'read',
|
|
||||||
subject: 'user',
|
|
||||||
properties: { fields: ['title'] },
|
|
||||||
conditions: ['plugin::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 => ({ createdBy: 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: ['plugin::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: ['plugin::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: ['plugin::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: [{ createdBy: 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: ['plugin::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: [{ createdBy: 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: { createdBy: 1 },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(can).toHaveBeenCalledTimes(1);
|
|
||||||
expect(can).toHaveBeenCalledWith('read', 'article', '*', { createdBy: 1 });
|
|
||||||
});
|
|
||||||
|
|
||||||
test(`It should use 'all' as a subject if it's Nil`, async () => {
|
|
||||||
await registerFn({
|
|
||||||
action: 'read',
|
|
||||||
subject: null,
|
|
||||||
fields: null,
|
|
||||||
condition: { createdBy: 1 },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(can).toHaveBeenCalledTimes(1);
|
|
||||||
expect(can).toHaveBeenCalledWith('read', 'all', null, { createdBy: 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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@ -16,7 +16,7 @@ describe('Admin Auth Strategy', () => {
|
|||||||
const ctx = createContext({}, { request, state: {} });
|
const ctx = createContext({}, { request, state: {} });
|
||||||
const user = { id: 1, isActive: true };
|
const user = { id: 1, isActive: true };
|
||||||
const findOne = jest.fn(() => user);
|
const findOne = jest.fn(() => user);
|
||||||
const generateUserAbility = jest.fn();
|
const generateUserAbility = jest.fn(() => 'ability');
|
||||||
|
|
||||||
global.strapi = {
|
global.strapi = {
|
||||||
admin: {
|
admin: {
|
||||||
@ -32,7 +32,11 @@ describe('Admin Auth Strategy', () => {
|
|||||||
|
|
||||||
expect(decodeJwtToken).toHaveBeenCalledWith('admin_tests-jwt-token');
|
expect(decodeJwtToken).toHaveBeenCalledWith('admin_tests-jwt-token');
|
||||||
expect(findOne).toHaveBeenCalledWith({ where: { id: 1 }, populate: ['roles'] });
|
expect(findOne).toHaveBeenCalledWith({ where: { id: 1 }, populate: ['roles'] });
|
||||||
expect(response).toStrictEqual({ authenticated: true, credentials: user });
|
expect(response).toStrictEqual({
|
||||||
|
authenticated: true,
|
||||||
|
credentials: user,
|
||||||
|
ability: 'ability',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Fails to authenticate if the authorization header is missing', async () => {
|
test('Fails to authenticate if the authorization header is missing', async () => {
|
||||||
|
@ -33,10 +33,16 @@ const authenticate = async ctx => {
|
|||||||
|
|
||||||
const userAbility = await getService('permission').engine.generateUserAbility(user);
|
const userAbility = await getService('permission').engine.generateUserAbility(user);
|
||||||
|
|
||||||
|
// TODO: use the ability from ctx.state.auth instead of
|
||||||
|
// ctx.state.userAbility, and remove the assign below
|
||||||
ctx.state.userAbility = userAbility;
|
ctx.state.userAbility = userAbility;
|
||||||
ctx.state.user = user;
|
ctx.state.user = user;
|
||||||
|
|
||||||
return { authenticated: true, credentials: user };
|
return {
|
||||||
|
authenticated: true,
|
||||||
|
credentials: user,
|
||||||
|
ability: userAbility,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @type {import('.').AuthStrategy} */
|
/** @type {import('.').AuthStrategy} */
|
||||||
|
@ -78,7 +78,7 @@ const engine = permissions.engine
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on('post-format::validate.permission', ({ permission }) => {
|
.on('after-format::validate.permission', ({ permission }) => {
|
||||||
if (permission.action === 'update') {
|
if (permission.action === 'update') {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
2
packages/core/permissions/index.d.ts
vendored
2
packages/core/permissions/index.d.ts
vendored
@ -24,7 +24,7 @@ interface ConditionProvider<T extends Condition = Condition> extends Provider {}
|
|||||||
interface PermissionEngineHooks {
|
interface PermissionEngineHooks {
|
||||||
'before-format::validate.permission': ReturnType<typeof hooks.createAsyncBailHook>;
|
'before-format::validate.permission': ReturnType<typeof hooks.createAsyncBailHook>;
|
||||||
'format.permission': ReturnType<typeof hooks.createAsyncSeriesWaterfallHook>;
|
'format.permission': ReturnType<typeof hooks.createAsyncSeriesWaterfallHook>;
|
||||||
'post-format::validate.permission': ReturnType<typeof hooks.createAsyncBailHook>;
|
'after-format::validate.permission': ReturnType<typeof hooks.createAsyncBailHook>;
|
||||||
'before-evaluate.permission': ReturnType<typeof hooks.createAsyncSeriesHook>;
|
'before-evaluate.permission': ReturnType<typeof hooks.createAsyncSeriesHook>;
|
||||||
'before-register.permission': ReturnType<typeof hooks.createAsyncSeriesHook>;
|
'before-register.permission': ReturnType<typeof hooks.createAsyncSeriesHook>;
|
||||||
}
|
}
|
||||||
|
@ -377,7 +377,7 @@ describe('Permissions Engine', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('post-format::validate.permission', () => {
|
describe('after-format::validate.permission', () => {
|
||||||
it('can prevent action register', async () => {
|
it('can prevent action register', async () => {
|
||||||
const permissions = [
|
const permissions = [
|
||||||
{ action: 'read', subject: 'article' },
|
{ action: 'read', subject: 'article' },
|
||||||
@ -390,7 +390,7 @@ describe('Permissions Engine', () => {
|
|||||||
permissions,
|
permissions,
|
||||||
engineHooks: [
|
engineHooks: [
|
||||||
{
|
{
|
||||||
name: 'post-format::validate.permission',
|
name: 'after-format::validate.permission',
|
||||||
fn: generateInvalidateActionHook('read'),
|
fn: generateInvalidateActionHook('read'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -450,7 +450,7 @@ describe('Permissions Engine', () => {
|
|||||||
fn: generateInvalidateActionHook('view'),
|
fn: generateInvalidateActionHook('view'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'post-format::validate.permission',
|
name: 'after-format::validate.permission',
|
||||||
fn: generateInvalidateActionHook('update'),
|
fn: generateInvalidateActionHook('update'),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
@ -32,6 +32,7 @@ const createAuthentication = () => {
|
|||||||
|
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
|
|
||||||
async authenticate(ctx, next) {
|
async authenticate(ctx, next) {
|
||||||
const { route } = ctx.state;
|
const { route } = ctx.state;
|
||||||
|
|
||||||
@ -47,7 +48,7 @@ const createAuthentication = () => {
|
|||||||
for (const strategy of strategiesToUse) {
|
for (const strategy of strategiesToUse) {
|
||||||
const result = await strategy.authenticate(ctx);
|
const result = await strategy.authenticate(ctx);
|
||||||
|
|
||||||
const { authenticated = false, error = null, credentials } = result || {};
|
const { authenticated = false, credentials, ability = null, error = null } = result || {};
|
||||||
|
|
||||||
if (error !== null) {
|
if (error !== null) {
|
||||||
return ctx.unauthorized(error);
|
return ctx.unauthorized(error);
|
||||||
@ -58,6 +59,7 @@ const createAuthentication = () => {
|
|||||||
ctx.state.auth = {
|
ctx.state.auth = {
|
||||||
strategy,
|
strategy,
|
||||||
credentials,
|
credentials,
|
||||||
|
ability,
|
||||||
};
|
};
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
@ -66,6 +68,7 @@ const createAuthentication = () => {
|
|||||||
|
|
||||||
return ctx.unauthorized('Missing or invalid credentials');
|
return ctx.unauthorized('Missing or invalid credentials');
|
||||||
},
|
},
|
||||||
|
|
||||||
async verify(auth, config = {}) {
|
async verify(auth, config = {}) {
|
||||||
if (config === false) {
|
if (config === false) {
|
||||||
return;
|
return;
|
||||||
|
@ -23,6 +23,11 @@ export interface Strapi {
|
|||||||
*/
|
*/
|
||||||
readonly auth: any;
|
readonly auth: any;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Getter for the Strapi content API container
|
||||||
|
*/
|
||||||
|
readonly contentAPI: any;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Getter for the Strapi sanitizers container
|
* Getter for the Strapi sanitizers container
|
||||||
*/
|
*/
|
||||||
|
@ -6,6 +6,7 @@ const user = require('./user');
|
|||||||
const role = require('./role');
|
const role = require('./role');
|
||||||
const usersPermissions = require('./users-permissions');
|
const usersPermissions = require('./users-permissions');
|
||||||
const providersRegistry = require('./providers-registry');
|
const providersRegistry = require('./providers-registry');
|
||||||
|
const permission = require('./permission');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
jwt,
|
jwt,
|
||||||
@ -14,4 +15,5 @@ module.exports = {
|
|||||||
role,
|
role,
|
||||||
user,
|
user,
|
||||||
'users-permissions': usersPermissions,
|
'users-permissions': usersPermissions,
|
||||||
|
permission,
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,45 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const PUBLIC_ROLE_FILTER = { role: { type: 'public' } };
|
||||||
|
|
||||||
|
module.exports = ({ strapi }) => ({
|
||||||
|
/**
|
||||||
|
* Find permissions associated to a specific role ID
|
||||||
|
*
|
||||||
|
* @param {number} roleID
|
||||||
|
*
|
||||||
|
* @return {object[]}
|
||||||
|
*/
|
||||||
|
async findRolePermissions(roleID) {
|
||||||
|
return strapi.entityService.load(
|
||||||
|
'plugin::users-permissions.role',
|
||||||
|
{ id: roleID },
|
||||||
|
'permissions'
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find permissions for the public role
|
||||||
|
*
|
||||||
|
* @return {object[]}
|
||||||
|
*/
|
||||||
|
async findPublicPermissions() {
|
||||||
|
return strapi.entityService.findMany('plugin::users-permissions.permission', {
|
||||||
|
where: PUBLIC_ROLE_FILTER,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform a Users-Permissions' action into a content API one
|
||||||
|
*
|
||||||
|
* @param {object} permission
|
||||||
|
* @param {string} permission.action
|
||||||
|
*
|
||||||
|
* @return {{ action: string }}
|
||||||
|
*/
|
||||||
|
toContentAPIPermission(permission) {
|
||||||
|
const { action } = permission;
|
||||||
|
|
||||||
|
return { action };
|
||||||
|
},
|
||||||
|
});
|
@ -170,6 +170,10 @@ module.exports = ({ strapi }) => ({
|
|||||||
|
|
||||||
const toDelete = _.difference(permissionsFoundInDB, allActions);
|
const toDelete = _.difference(permissionsFoundInDB, allActions);
|
||||||
|
|
||||||
|
// Register actions into the content API action provider
|
||||||
|
// TODO: do this in the content API bootstrap phase instead
|
||||||
|
allActions.forEach(action => strapi.contentAPI.permissions.providers.action.register(action));
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
toDelete.map(action => {
|
toDelete.map(action => {
|
||||||
return strapi.query('plugin::users-permissions.permission').delete({ where: { action } });
|
return strapi.query('plugin::users-permissions.permission').delete({ where: { action } });
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const { castArray, map } = require('lodash/fp');
|
const { castArray, map, every, pipe } = require('lodash/fp');
|
||||||
const { ForbiddenError, UnauthorizedError } = require('@strapi/utils').errors;
|
const { ForbiddenError, UnauthorizedError } = require('@strapi/utils').errors;
|
||||||
|
|
||||||
const { getService } = require('../utils');
|
const { getService } = require('../utils');
|
||||||
@ -16,48 +16,61 @@ const authenticate = async ctx => {
|
|||||||
if (token) {
|
if (token) {
|
||||||
const { id } = token;
|
const { id } = token;
|
||||||
|
|
||||||
|
// Invalid token
|
||||||
if (id === undefined) {
|
if (id === undefined) {
|
||||||
return { authenticated: false };
|
return { authenticated: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetch authenticated user
|
|
||||||
const user = await getService('user').fetchAuthenticatedUser(id);
|
const user = await getService('user').fetchAuthenticatedUser(id);
|
||||||
|
|
||||||
|
// No user associated to the token
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return { error: 'Invalid credentials' };
|
return { error: 'Invalid credentials' };
|
||||||
}
|
}
|
||||||
|
|
||||||
const advancedSettings = await getAdvancedSettings();
|
const advancedSettings = await getAdvancedSettings();
|
||||||
|
|
||||||
|
// User not confirmed
|
||||||
if (advancedSettings.email_confirmation && !user.confirmed) {
|
if (advancedSettings.email_confirmation && !user.confirmed) {
|
||||||
return { error: 'Invalid credentials' };
|
return { error: 'Invalid credentials' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// User blocked
|
||||||
if (user.blocked) {
|
if (user.blocked) {
|
||||||
return { error: 'Invalid credentials' };
|
return { error: 'Invalid credentials' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fetch user's permissions
|
||||||
|
const permissions = await Promise.resolve(user.role.id)
|
||||||
|
.then(getService('permission').findRolePermissions)
|
||||||
|
.then(map(getService('permission').toContentAPIPermission));
|
||||||
|
|
||||||
|
// Generate an ability (content API engine) based on the given permissions
|
||||||
|
const ability = await strapi.contentAPI.permissions.engine.generateAbility(permissions);
|
||||||
|
|
||||||
ctx.state.user = user;
|
ctx.state.user = user;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
credentials: user,
|
credentials: user,
|
||||||
|
ability,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const publicPermissions = await strapi.query('plugin::users-permissions.permission').findMany({
|
const publicPermissions = await getService('permission')
|
||||||
where: {
|
.findPublicPermissions()
|
||||||
role: { type: 'public' },
|
.then(map(getService('permission').toContentAPIPermission));
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (publicPermissions.length === 0) {
|
if (publicPermissions.length === 0) {
|
||||||
return { authenticated: false };
|
return { authenticated: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ability = await strapi.contentAPI.permissions.engine.generateAbility(publicPermissions);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
credentials: null,
|
credentials: null,
|
||||||
|
ability,
|
||||||
};
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return { authenticated: false };
|
return { authenticated: false };
|
||||||
@ -65,7 +78,7 @@ const authenticate = async ctx => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const verify = async (auth, config) => {
|
const verify = async (auth, config) => {
|
||||||
const { credentials: user } = auth;
|
const { credentials: user, ability } = auth;
|
||||||
|
|
||||||
if (!config.scope) {
|
if (!config.scope) {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
@ -77,18 +90,17 @@ const verify = async (auth, config) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let allowedActions = auth.allowedActions;
|
// If no ability have been generated, then consider auth is missing
|
||||||
|
if (!ability) {
|
||||||
if (!allowedActions) {
|
throw new UnauthorizedError();
|
||||||
const permissions = await strapi.query('plugin::users-permissions.permission').findMany({
|
|
||||||
where: { role: user ? user.role.id : { type: 'public' } },
|
|
||||||
});
|
|
||||||
|
|
||||||
allowedActions = map('action', permissions);
|
|
||||||
auth.allowedActions = allowedActions;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const isAllowed = castArray(config.scope).every(scope => allowedActions.includes(scope));
|
const isAllowed = pipe(
|
||||||
|
// Make sure we're dealing with an array
|
||||||
|
castArray,
|
||||||
|
// Transform the scope array into an action array
|
||||||
|
every(scope => ability.can(scope))
|
||||||
|
)(config.scope);
|
||||||
|
|
||||||
if (!isAllowed) {
|
if (!isAllowed) {
|
||||||
throw new ForbiddenError();
|
throw new ForbiddenError();
|
||||||
|
@ -3,6 +3,7 @@ import * as user from '../services/user';
|
|||||||
import * as role from '../services/role';
|
import * as role from '../services/role';
|
||||||
import * as jwt from '../services/jwt';
|
import * as jwt from '../services/jwt';
|
||||||
import * as providers from '../services/providers';
|
import * as providers from '../services/providers';
|
||||||
|
import * as permission from '../services/permission';
|
||||||
|
|
||||||
type S = {
|
type S = {
|
||||||
['users-permissions']: typeof usersPermissions;
|
['users-permissions']: typeof usersPermissions;
|
||||||
@ -11,6 +12,7 @@ type S = {
|
|||||||
jwt: typeof jwt;
|
jwt: typeof jwt;
|
||||||
providers: typeof providers;
|
providers: typeof providers;
|
||||||
['providers-registry']: typeof providers;
|
['providers-registry']: typeof providers;
|
||||||
|
permission: typeof permission;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getService<T extends keyof S>(name: T): ReturnType<S[T]>;
|
export function getService<T extends keyof S>(name: T): ReturnType<S[T]>;
|
||||||
|
Loading…
x
Reference in New Issue
Block a user