mirror of
https://github.com/strapi/strapi.git
synced 2025-08-30 19:56:05 +00:00
Make users-permissions auth strategy use the content API permission engine
This commit is contained in:
parent
43e360a641
commit
4ebc128ffb
@ -16,7 +16,7 @@ describe('Admin Auth Strategy', () => {
|
||||
const ctx = createContext({}, { request, state: {} });
|
||||
const user = { id: 1, isActive: true };
|
||||
const findOne = jest.fn(() => user);
|
||||
const generateUserAbility = jest.fn();
|
||||
const generateUserAbility = jest.fn(() => 'ability');
|
||||
|
||||
global.strapi = {
|
||||
admin: {
|
||||
@ -32,7 +32,11 @@ describe('Admin Auth Strategy', () => {
|
||||
|
||||
expect(decodeJwtToken).toHaveBeenCalledWith('admin_tests-jwt-token');
|
||||
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 () => {
|
||||
|
@ -33,10 +33,16 @@ const authenticate = async ctx => {
|
||||
|
||||
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.user = user;
|
||||
|
||||
return { authenticated: true, credentials: user };
|
||||
return {
|
||||
authenticated: true,
|
||||
credentials: user,
|
||||
ability: userAbility,
|
||||
};
|
||||
};
|
||||
|
||||
/** @type {import('.').AuthStrategy} */
|
||||
|
@ -32,6 +32,7 @@ const createAuthentication = () => {
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
async authenticate(ctx, next) {
|
||||
const { route } = ctx.state;
|
||||
|
||||
@ -47,7 +48,7 @@ const createAuthentication = () => {
|
||||
for (const strategy of strategiesToUse) {
|
||||
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) {
|
||||
return ctx.unauthorized(error);
|
||||
@ -58,6 +59,7 @@ const createAuthentication = () => {
|
||||
ctx.state.auth = {
|
||||
strategy,
|
||||
credentials,
|
||||
ability,
|
||||
};
|
||||
|
||||
return next();
|
||||
@ -66,6 +68,7 @@ const createAuthentication = () => {
|
||||
|
||||
return ctx.unauthorized('Missing or invalid credentials');
|
||||
},
|
||||
|
||||
async verify(auth, config = {}) {
|
||||
if (config === false) {
|
||||
return;
|
||||
|
@ -23,6 +23,11 @@ export interface Strapi {
|
||||
*/
|
||||
readonly auth: any;
|
||||
|
||||
/**
|
||||
* Getter for the Strapi content API container
|
||||
*/
|
||||
readonly contentAPI: any;
|
||||
|
||||
/**
|
||||
* Getter for the Strapi sanitizers container
|
||||
*/
|
||||
|
@ -6,6 +6,7 @@ const user = require('./user');
|
||||
const role = require('./role');
|
||||
const usersPermissions = require('./users-permissions');
|
||||
const providersRegistry = require('./providers-registry');
|
||||
const permission = require('./permission');
|
||||
|
||||
module.exports = {
|
||||
jwt,
|
||||
@ -14,4 +15,5 @@ module.exports = {
|
||||
role,
|
||||
user,
|
||||
'users-permissions': usersPermissions,
|
||||
permission,
|
||||
};
|
||||
|
@ -0,0 +1,55 @@
|
||||
'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' permission into a content API one
|
||||
*
|
||||
* @example
|
||||
* const upPermission = { action: 'api::foo.foo.find' };
|
||||
*
|
||||
* const permission = toContentAPIPermission(upPermission);
|
||||
* // ^? { action: 'find', subject: 'api::foo.foo' }
|
||||
*
|
||||
* @param {object} permission
|
||||
* @param {string} permission.action
|
||||
*
|
||||
* @return {{ action: string, subject: string }}
|
||||
*/
|
||||
toContentAPIPermission(permission) {
|
||||
const { action } = permission;
|
||||
const actionIndex = action.lastIndexOf('.');
|
||||
|
||||
return {
|
||||
action: action.slice(actionIndex + 1),
|
||||
subject: action.slice(0, actionIndex),
|
||||
};
|
||||
},
|
||||
});
|
@ -170,6 +170,13 @@ module.exports = ({ strapi }) => ({
|
||||
|
||||
const toDelete = _.difference(permissionsFoundInDB, allActions);
|
||||
|
||||
// Register actions into the content API action provider
|
||||
// TODO: do this in the content API bootstrap phase instead
|
||||
allActions
|
||||
// 'api::foo.foo.find' => { action: 'find', subject: 'api.foo.foo' } => 'find';
|
||||
.map(action => getService('permission').toContentAPIPermission({ action }).action)
|
||||
.forEach(action => strapi.contentAPI.permissions.providers.action.register(action));
|
||||
|
||||
await Promise.all(
|
||||
toDelete.map(action => {
|
||||
return strapi.query('plugin::users-permissions.permission').delete({ where: { action } });
|
||||
|
@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const { castArray, map } = require('lodash/fp');
|
||||
const { castArray, map, every, pipe } = require('lodash/fp');
|
||||
const { ForbiddenError, UnauthorizedError } = require('@strapi/utils').errors;
|
||||
|
||||
const { getService } = require('../utils');
|
||||
@ -16,48 +16,61 @@ const authenticate = async ctx => {
|
||||
if (token) {
|
||||
const { id } = token;
|
||||
|
||||
// Invalid token
|
||||
if (id === undefined) {
|
||||
return { authenticated: false };
|
||||
}
|
||||
|
||||
// fetch authenticated user
|
||||
const user = await getService('user').fetchAuthenticatedUser(id);
|
||||
|
||||
// No user associated to the token
|
||||
if (!user) {
|
||||
return { error: 'Invalid credentials' };
|
||||
}
|
||||
|
||||
const advancedSettings = await getAdvancedSettings();
|
||||
|
||||
// User not confirmed
|
||||
if (advancedSettings.email_confirmation && !user.confirmed) {
|
||||
return { error: 'Invalid credentials' };
|
||||
}
|
||||
|
||||
// User blocked
|
||||
if (user.blocked) {
|
||||
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;
|
||||
|
||||
return {
|
||||
authenticated: true,
|
||||
credentials: user,
|
||||
ability,
|
||||
};
|
||||
}
|
||||
|
||||
const publicPermissions = await strapi.query('plugin::users-permissions.permission').findMany({
|
||||
where: {
|
||||
role: { type: 'public' },
|
||||
},
|
||||
});
|
||||
const publicPermissions = await getService('permission')
|
||||
.findPublicPermissions()
|
||||
.then(map(getService('permission').toContentAPIPermission));
|
||||
|
||||
if (publicPermissions.length === 0) {
|
||||
return { authenticated: false };
|
||||
}
|
||||
|
||||
const ability = await strapi.contentAPI.permissions.engine.generateAbility(publicPermissions);
|
||||
|
||||
return {
|
||||
authenticated: true,
|
||||
credentials: null,
|
||||
ability,
|
||||
};
|
||||
} catch (err) {
|
||||
return { authenticated: false };
|
||||
@ -65,7 +78,7 @@ const authenticate = async ctx => {
|
||||
};
|
||||
|
||||
const verify = async (auth, config) => {
|
||||
const { credentials: user } = auth;
|
||||
const { credentials: user, ability } = auth;
|
||||
|
||||
if (!config.scope) {
|
||||
if (!user) {
|
||||
@ -77,18 +90,21 @@ const verify = async (auth, config) => {
|
||||
}
|
||||
}
|
||||
|
||||
let allowedActions = auth.allowedActions;
|
||||
|
||||
if (!allowedActions) {
|
||||
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;
|
||||
// If no ability have been generated, then consider auth is missing
|
||||
if (!ability) {
|
||||
throw new UnauthorizedError();
|
||||
}
|
||||
|
||||
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
|
||||
map(scope => ({ action: scope })),
|
||||
// Map the users-permissions permissions into content API permissions
|
||||
map(getService('permission').toContentAPIPermission),
|
||||
// Check that every required scope is allowed by the ability
|
||||
every(({ action, subject }) => ability.can(action, subject))
|
||||
)(config.scope);
|
||||
|
||||
if (!isAllowed) {
|
||||
throw new ForbiddenError();
|
||||
|
@ -3,6 +3,7 @@ import * as user from '../services/user';
|
||||
import * as role from '../services/role';
|
||||
import * as jwt from '../services/jwt';
|
||||
import * as providers from '../services/providers';
|
||||
import * as permission from '../services/permission';
|
||||
|
||||
type S = {
|
||||
['users-permissions']: typeof usersPermissions;
|
||||
@ -11,6 +12,7 @@ type S = {
|
||||
jwt: typeof jwt;
|
||||
providers: typeof providers;
|
||||
['providers-registry']: typeof providers;
|
||||
permission: typeof permission;
|
||||
};
|
||||
|
||||
export function getService<T extends keyof S>(name: T): ReturnType<S[T]>;
|
||||
|
Loading…
x
Reference in New Issue
Block a user