mirror of
https://github.com/strapi/strapi.git
synced 2025-08-31 12:23: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 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} */
|
||||||
|
@ -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,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);
|
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(
|
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,21 @@ 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
|
||||||
|
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) {
|
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