Add check many permissions route/controller / Add userAbility to the context's state / Add isAuthenticatedAdmin.js

Signed-off-by: Convly <jean-sebastien.herbaux@epitech.eu>
This commit is contained in:
Convly 2020-06-10 18:04:47 +02:00 committed by Alexandre Bodin
parent bfb782069e
commit 765d7aaa52
7 changed files with 203 additions and 5 deletions

View File

@ -0,0 +1,9 @@
'use strict';
module.exports = (ctx, next) => {
if (!ctx.state.isAuthenticatedAdmin) {
throw strapi.errors.forbidden();
}
return next();
};

View File

@ -198,9 +198,14 @@
{
"method": "GET",
"path": "/permissions",
"handler": "permission.getAll",
"handler": "permission.getAll"
},
{
"method": "POST",
"path": "/permissions/check",
"handler": "permission.check",
"config": {
"policies": []
"policies": ["admin::isAuthenticatedAdmin"]
}
}
]

View File

@ -0,0 +1,125 @@
'use strict';
const permissionController = require('../permission');
const createContext = ({ params = {}, query = {}, body = {} }, overrides = {}) => ({
params,
query,
request: {
body,
},
...overrides,
});
describe('Permission Controller', () => {
const localTestData = {
permissions: {
valid: [
{ action: 'read', subject: 'article', field: 'title' },
{ action: 'read', subject: 'article' },
{ action: 'read' },
],
invalid: [
{ action: {}, subject: '', field: '' },
{ subject: 'article', field: 'title' },
{ action: 'read', subject: {}, field: 'title' },
{ action: 'read', subject: 'article', field: {} },
{ action: 'read', subject: 'article', field: 'title', foo: 'bar' },
],
},
ability: {
can: jest.fn(() => true),
},
badRequest: jest.fn(),
};
global.strapi = {
admin: {
services: {
permission: {
engine: {
checkMany: jest.fn(ability => permissions => {
return permissions.map(({ action, subject, field }) =>
ability.can(action, subject, field)
);
}),
},
},
},
},
};
afterEach(async () => {
jest.clearAllMocks();
});
describe('Check Many Permissions', () => {
test('Invalid Permission Shape (bad type for action)', async () => {
const ctx = createContext(
{ body: { permissions: [localTestData.permissions.invalid[0]] } },
{ state: { userAbility: localTestData.ability }, badRequest: localTestData.badRequest }
);
await permissionController.check(ctx);
expect(localTestData.badRequest).toHaveBeenCalled();
});
test('Invalid Permission Shape (missing required action)', async () => {
const ctx = createContext(
{ body: { permissions: [localTestData.permissions.invalid[1]] } },
{ state: { userAbility: localTestData.ability }, badRequest: localTestData.badRequest }
);
await permissionController.check(ctx);
expect(localTestData.badRequest).toHaveBeenCalled();
});
test('Invalid Permission Shape (bad type for subject)', async () => {
const ctx = createContext(
{ body: { permissions: [localTestData.permissions.invalid[2]] } },
{ state: { userAbility: localTestData.ability }, badRequest: localTestData.badRequest }
);
await permissionController.check(ctx);
expect(localTestData.badRequest).toHaveBeenCalled();
});
test('Invalid Permission Shape (bad type for field)', async () => {
const ctx = createContext(
{ body: { permissions: [localTestData.permissions.invalid[3]] } },
{ state: { userAbility: localTestData.ability }, badRequest: localTestData.badRequest }
);
await permissionController.check(ctx);
expect(localTestData.badRequest).toHaveBeenCalled();
});
test('Invalid Permission Shape (unrecognized foo param)', async () => {
const ctx = createContext(
{ body: { permissions: [localTestData.permissions.invalid[4]] } },
{ state: { userAbility: localTestData.ability }, badRequest: localTestData.badRequest }
);
await permissionController.check(ctx);
expect(localTestData.badRequest).toHaveBeenCalled();
});
test('Check Many Permissions', async () => {
const ctx = createContext(
{ body: { permissions: localTestData.permissions.valid } },
{ state: { userAbility: localTestData.ability } }
);
await permissionController.check(ctx);
expect(localTestData.ability.can).toHaveBeenCalled();
expect(strapi.admin.services.permission.engine.checkMany).toHaveBeenCalled();
expect(ctx.body.data).toHaveLength(localTestData.permissions.valid.length);
});
});
});

View File

@ -1,8 +1,31 @@
'use strict';
const { formatActionsBySections } = require('./formatters');
const { validateCheckPermissionsInput } = require('../validation/permission');
module.exports = {
/**
* Check each permissions from `request.body.permissions` and returns an array of booleans
* @param {KoaContext} ctx - koa context
*/
async check(ctx) {
const { body: input } = ctx.request;
try {
await validateCheckPermissionsInput(input);
} catch (err) {
return ctx.badRequest('ValidationError', err);
}
const checkPermissions = strapi.admin.services.permission.engine.checkMany(
ctx.state.userAbility
);
ctx.body = {
data: checkPermissions(input.permissions),
};
},
/**
* Returns every permissions, in nested format
* @param {KoaContext} ctx - koa context

View File

@ -44,6 +44,9 @@ module.exports = strapi => ({
ctx.state.admin = admin;
ctx.state.user = admin;
ctx.state.userAbility = await strapi.admin.services.permission.engine.generateUserAbility(
admin
);
ctx.state.isAuthenticatedAdmin = true;
return next();
}

View File

@ -69,6 +69,25 @@ const updatePermissionsSchema = yup
.required()
.noUnknown();
const checkPermissionsSchema = yup.object().shape({
permissions: yup.array().of(
yup
.object()
.shape({
action: yup.string().required(),
subject: yup.string(),
field: yup.string(),
})
.noUnknown()
),
});
const validateCheckPermissionsInput = data => {
return checkPermissionsSchema
.validate(data, { strict: true, abortEarly: false })
.catch(handleReject);
};
const validatedUpdatePermissionsInput = data => {
return updatePermissionsSchema
.validate(data, { strict: true, abortEarly: true })
@ -110,4 +129,5 @@ const validatePermissionsExist = data => {
module.exports = {
validatedUpdatePermissionsInput,
validatePermissionsExist,
validateCheckPermissionsInput,
};

View File

@ -14,6 +14,10 @@ const get = (policy, plugin, apiName) => {
return parsePolicy(getPluginPolicy(policy));
}
if (adminPolicyExists(policy)) {
return parsePolicy(getAdminPolicy(policy));
}
if (APIPolicyExists(policy)) {
return parsePolicy(getAPIPolicy(policy));
}
@ -63,6 +67,7 @@ const parsePolicy = policy => {
const GLOBAL_PREFIX = 'global::';
const PLUGIN_PREFIX = 'plugins::';
const ADMIN_PREFIX = 'admin::';
const APPLICATION_PREFIX = 'application::';
const getPolicyIn = (container, policy) => {
@ -90,12 +95,20 @@ const getPluginPolicy = policy => {
return getPolicyIn(_.get(strapi, ['plugins', plugin]), policyName);
};
const pluginPolicyExists = policy => {
return isPluginPolicy(policy) && !_.isUndefined(getPluginPolicy(policy));
};
const pluginPolicyExists = policy =>
isPluginPolicy(policy) && !_.isUndefined(getPluginPolicy(policy));
const isPluginPolicy = policy => _.startsWith(policy, PLUGIN_PREFIX);
const getAdminPolicy = policy => {
const strippedPolicy = policy.replace(ADMIN_PREFIX, '');
return getPolicyIn(_.get(strapi, 'admin'), strippedPolicy);
};
const isAdminPolicy = policy => _.startsWith(policy, ADMIN_PREFIX);
const adminPolicyExists = policy => isAdminPolicy(policy) && !_.isUndefined(getAdminPolicy(policy));
const getAPIPolicy = policy => {
const [, policyWithoutPrefix] = policy.split('::');
const [api = '', policyName = ''] = policyWithoutPrefix.split('.');