Merge pull request #10910 from strapi/v4/content-api

Move admin auth to global auth system
This commit is contained in:
Alexandre BODIN 2021-09-09 00:26:13 +02:00 committed by GitHub
commit 9c81dbea7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 601 additions and 522 deletions

View File

@ -6,16 +6,19 @@ module.exports = {
method: 'GET',
path: '/providers',
handler: 'authentication.getProviders',
config: { auth: false },
},
{
method: 'GET',
path: '/connect/:provider',
handler: 'authentication.providerLogin',
config: { auth: false },
},
{
method: 'POST',
path: '/connect/:provider',
handler: 'authentication.providerLogin',
config: { auth: false },
},
{
method: 'GET',

View File

@ -31,9 +31,9 @@ module.exports = createPolicyFactory(
);
return (ctx, next) => {
const { userAbility: ability, isAuthenticatedAdmin } = ctx.state;
const { userAbility: ability, isAuthenticated } = ctx.state;
if (!isAuthenticatedAdmin || !ability) {
if (!isAuthenticated || !ability) {
return next();
}

View File

@ -1,7 +1,7 @@
'use strict';
module.exports = (ctx, next) => {
if (!ctx.state.isAuthenticatedAdmin) {
if (!ctx.state.isAuthenticated) {
return ctx.unauthorized();
}

View File

@ -3,56 +3,51 @@
// const permissionsFieldsToPropertiesMigration = require('../migrations/permissions-fields-to-properties');
/**
* Tries to authenticated admin user and calls next.
* @param {KoaContext} ctx
* @param {Middleware} next
* @returns {undefined}
*/
const authMiddleware = async (ctx, next) => {
if (!ctx.request.header.authorization) {
return next();
}
const adminAuthStrategy = {
name: 'admin',
async authenticate(ctx) {
const { authorization } = ctx.request.header;
if (
ctx.request.header.authorization &&
ctx.request.header.authorization.split(' ')[0] === 'Bearer'
) {
const token = ctx.request.header.authorization.split(' ')[1];
if (!authorization) {
return { authenticated: false };
}
const parts = authorization.split(/\s+/);
if (parts[0] !== 'Bearer' || parts.length !== 2) {
return { authenticated: false };
}
const token = parts[1];
const { payload, isValid } = strapi.admin.services.token.decodeJwtToken(token);
if (isValid) {
const admin = await strapi
const user = await strapi
.query('admin::user')
.findOne({ where: { id: payload.id }, populate: ['roles'] });
if (!admin || !(admin.isActive === true)) {
return ctx.unauthorized('Invalid credentials');
if (!user || !(user.isActive === true)) {
return { error: 'Invalid credentials' };
}
// TODO: use simple user & isAuthenticated
const userAbility = await strapi.admin.services.permission.engine.generateUserAbility(user);
ctx.state.admin = admin;
ctx.state.user = admin;
ctx.state.userAbility = await strapi.admin.services.permission.engine.generateUserAbility(
admin
);
ctx.state.userAbility = userAbility;
ctx.state.user = user;
ctx.state.isAuthenticatedAdmin = true;
return next();
return { authenticated: true, credentials: user };
}
}
ctx.unauthorized('Invalid credentials');
return { error: 'Invalid credentials' };
},
// async verify() {},
};
module.exports = () => {
const passportMiddleware = strapi.admin.services.passport.init();
strapi.server.api('admin').use(passportMiddleware);
strapi.server.api('admin').use(authMiddleware);
strapi.container.get('auth').register('admin', adminAuthStrategy);
// FIXME: to implement
// strapi.db.migrations.register(permissionsFieldsToPropertiesMigration);

View File

@ -0,0 +1,63 @@
'use strict';
module.exports = [
{
method: 'GET',
path: '/init',
handler: 'admin.init',
config: { auth: false },
},
{
method: 'GET',
path: '/project-type',
handler: 'admin.getProjectType',
config: { auth: false },
},
{
method: 'GET',
path: '/information',
handler: 'admin.information',
config: {
policies: ['admin::isAuthenticatedAdmin'],
},
},
{
method: 'GET',
path: '/plugins',
handler: 'admin.plugins',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::marketplace.read'] } },
],
},
},
{
method: 'POST',
path: '/plugins/install',
handler: 'admin.installPlugin',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{
name: 'admin::hasPermissions',
options: { actions: ['admin::marketplace.plugins.install'] },
},
],
},
},
{
method: 'DELETE',
path: '/plugins/uninstall/:plugin',
handler: 'admin.uninstallPlugin',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{
name: 'admin::hasPermissions',
options: { actions: ['admin::marketplace.plugins.uninstall'] },
},
],
},
},
];

View File

@ -0,0 +1,46 @@
'use strict';
module.exports = [
{
method: 'POST',
path: '/login',
handler: 'authentication.login',
config: { auth: false },
},
{
method: 'POST',
path: '/renew-token',
handler: 'authentication.renewToken',
config: { auth: false },
},
{
method: 'POST',
path: '/register-admin',
handler: 'authentication.registerAdmin',
config: { auth: false },
},
{
method: 'GET',
path: '/registration-info',
handler: 'authentication.registrationInfo',
config: { auth: false },
},
{
method: 'POST',
path: '/register',
handler: 'authentication.register',
config: { auth: false },
},
{
method: 'POST',
path: '/forgot-password',
handler: 'authentication.forgotPassword',
config: { auth: false },
},
{
method: 'POST',
path: '/reset-password',
handler: 'authentication.resetPassword',
config: { auth: false },
},
];

View File

@ -1,325 +1,10 @@
'use strict';
module.exports = [
{
method: 'GET',
path: '/init',
handler: 'admin.init',
},
{
method: 'GET',
path: '/project-type',
handler: 'admin.getProjectType',
},
{
method: 'GET',
path: '/information',
handler: 'admin.information',
config: {
policies: ['admin::isAuthenticatedAdmin'],
},
},
{
method: 'GET',
path: '/plugins',
handler: 'admin.plugins',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::marketplace.read'] } },
],
},
},
{
method: 'POST',
path: '/plugins/install',
handler: 'admin.installPlugin',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{
name: 'admin::hasPermissions',
options: { actions: ['admin::marketplace.plugins.install'] },
},
],
},
},
{
method: 'DELETE',
path: '/plugins/uninstall/:plugin',
handler: 'admin.uninstallPlugin',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{
name: 'admin::hasPermissions',
options: { actions: ['admin::marketplace.plugins.uninstall'] },
},
],
},
},
{
method: 'POST',
path: '/login',
handler: 'authentication.login',
},
{
method: 'POST',
path: '/renew-token',
handler: 'authentication.renewToken',
},
{
method: 'POST',
path: '/register-admin',
handler: 'authentication.registerAdmin',
},
{
method: 'GET',
path: '/registration-info',
handler: 'authentication.registrationInfo',
},
{
method: 'POST',
path: '/register',
handler: 'authentication.register',
},
{
method: 'POST',
path: '/forgot-password',
handler: 'authentication.forgotPassword',
},
{
method: 'POST',
path: '/reset-password',
handler: 'authentication.resetPassword',
},
{
method: 'GET',
path: '/webhooks',
handler: 'Webhooks.listWebhooks',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::webhooks.read'] } },
],
},
},
{
method: 'POST',
path: '/webhooks',
handler: 'Webhooks.createWebhook',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::webhooks.create'] } },
],
},
},
{
method: 'GET',
path: '/webhooks/:id',
handler: 'Webhooks.getWebhook',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::webhooks.read'] } },
],
},
},
{
method: 'PUT',
path: '/webhooks/:id',
handler: 'Webhooks.updateWebhook',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::webhooks.update'] } },
],
},
},
{
method: 'DELETE',
path: '/webhooks/:id',
handler: 'Webhooks.deleteWebhook',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::webhooks.delete'] } },
],
},
},
{
method: 'POST',
path: '/webhooks/batch-delete',
handler: 'Webhooks.deleteWebhooks',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::webhooks.delete'] } },
],
},
},
{
method: 'POST',
path: '/webhooks/:id/trigger',
handler: 'Webhooks.triggerWebhook',
config: {
policies: [],
},
},
{
method: 'GET',
path: '/users/me',
handler: 'authenticated-user.getMe',
config: {
policies: ['admin::isAuthenticatedAdmin'],
},
},
{
method: 'PUT',
path: '/users/me',
handler: 'authenticated-user.updateMe',
config: {
policies: ['admin::isAuthenticatedAdmin'],
},
},
{
method: 'GET',
path: '/users/me/permissions',
handler: 'authenticated-user.getOwnPermissions',
config: {
policies: ['admin::isAuthenticatedAdmin'],
},
},
{
method: 'POST',
path: '/users',
handler: 'user.create',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::users.create'] } },
],
},
},
{
method: 'GET',
path: '/users',
handler: 'user.find',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::users.read'] } },
],
},
},
{
method: 'GET',
path: '/users/:id',
handler: 'user.findOne',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::users.read'] } },
],
},
},
{
method: 'PUT',
path: '/users/:id',
handler: 'user.update',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::users.update'] } },
],
},
},
{
method: 'DELETE',
path: '/users/:id',
handler: 'user.deleteOne',
config: {
policies: [{ name: 'admin::hasPermissions', options: { actions: ['admin::users.delete'] } }],
},
},
{
method: 'POST',
path: '/users/batch-delete',
handler: 'user.deleteMany',
config: {
policies: [{ name: 'admin::hasPermissions', options: { actions: ['admin::users.delete'] } }],
},
},
{
method: 'GET',
path: '/roles/:id/permissions',
handler: 'role.getPermissions',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::roles.read'] } },
],
},
},
{
method: 'PUT',
path: '/roles/:id/permissions',
handler: 'role.updatePermissions',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::roles.update'] } },
],
},
},
{
method: 'GET',
path: '/roles/:id',
handler: 'role.findOne',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::roles.read'] } },
],
},
},
{
method: 'GET',
path: '/roles',
handler: 'role.findAll',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::roles.read'] } },
],
},
},
{
method: 'PUT',
path: '/roles/:id',
handler: 'role.update',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::roles.update'] } },
],
},
},
{
method: 'GET',
path: '/permissions',
handler: 'permission.getAll',
config: {
policies: ['admin::isAuthenticatedAdmin'],
},
},
{
method: 'POST',
path: '/permissions/check',
handler: 'permission.check',
config: {
policies: ['admin::isAuthenticatedAdmin'],
},
},
];
const admin = require('./admin');
const authentication = require('./authentication');
const permissions = require('./permissions');
const users = require('./users');
const roles = require('./roles');
const webhooks = require('./webhooks');
module.exports = [...admin, ...authentication, ...permissions, ...users, ...roles, ...webhooks];

View File

@ -0,0 +1,20 @@
'use strict';
module.exports = [
{
method: 'GET',
path: '/permissions',
handler: 'permission.getAll',
config: {
policies: ['admin::isAuthenticatedAdmin'],
},
},
{
method: 'POST',
path: '/permissions/check',
handler: 'permission.check',
config: {
policies: ['admin::isAuthenticatedAdmin'],
},
},
];

View File

@ -0,0 +1,67 @@
'use strict';
module.exports = [
{
method: 'POST',
path: '/users/batch-delete',
handler: 'user.deleteMany',
config: {
policies: [{ name: 'admin::hasPermissions', options: { actions: ['admin::users.delete'] } }],
},
},
{
method: 'GET',
path: '/roles/:id/permissions',
handler: 'role.getPermissions',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::roles.read'] } },
],
},
},
{
method: 'PUT',
path: '/roles/:id/permissions',
handler: 'role.updatePermissions',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::roles.update'] } },
],
},
},
{
method: 'GET',
path: '/roles/:id',
handler: 'role.findOne',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::roles.read'] } },
],
},
},
{
method: 'GET',
path: '/roles',
handler: 'role.findAll',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::roles.read'] } },
],
},
},
{
method: 'PUT',
path: '/roles/:id',
handler: 'role.update',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::roles.update'] } },
],
},
},
];

View File

@ -0,0 +1,88 @@
'use strict';
module.exports = [
{
method: 'GET',
path: '/users/me',
handler: 'authenticated-user.getMe',
config: {
policies: ['admin::isAuthenticatedAdmin'],
},
},
{
method: 'PUT',
path: '/users/me',
handler: 'authenticated-user.updateMe',
config: {
policies: ['admin::isAuthenticatedAdmin'],
},
},
{
method: 'GET',
path: '/users/me/permissions',
handler: 'authenticated-user.getOwnPermissions',
config: {
policies: ['admin::isAuthenticatedAdmin'],
},
},
{
method: 'POST',
path: '/users',
handler: 'user.create',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::users.create'] } },
],
},
},
{
method: 'GET',
path: '/users',
handler: 'user.find',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::users.read'] } },
],
},
},
{
method: 'GET',
path: '/users/:id',
handler: 'user.findOne',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::users.read'] } },
],
},
},
{
method: 'PUT',
path: '/users/:id',
handler: 'user.update',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::users.update'] } },
],
},
},
{
method: 'DELETE',
path: '/users/:id',
handler: 'user.deleteOne',
config: {
policies: [{ name: 'admin::hasPermissions', options: { actions: ['admin::users.delete'] } }],
},
},
{
method: 'POST',
path: '/users/batch-delete',
handler: 'user.deleteMany',
config: {
policies: [{ name: 'admin::hasPermissions', options: { actions: ['admin::users.delete'] } }],
},
},
];

View File

@ -0,0 +1,78 @@
'use strict';
module.exports = [
{
method: 'GET',
path: '/webhooks',
handler: 'Webhooks.listWebhooks',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::webhooks.read'] } },
],
},
},
{
method: 'POST',
path: '/webhooks',
handler: 'Webhooks.createWebhook',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::webhooks.create'] } },
],
},
},
{
method: 'GET',
path: '/webhooks/:id',
handler: 'Webhooks.getWebhook',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::webhooks.read'] } },
],
},
},
{
method: 'PUT',
path: '/webhooks/:id',
handler: 'Webhooks.updateWebhook',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::webhooks.update'] } },
],
},
},
{
method: 'DELETE',
path: '/webhooks/:id',
handler: 'Webhooks.deleteWebhook',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::webhooks.delete'] } },
],
},
},
{
method: 'POST',
path: '/webhooks/batch-delete',
handler: 'Webhooks.deleteWebhooks',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', options: { actions: ['admin::webhooks.delete'] } },
],
},
},
{
method: 'POST',
path: '/webhooks/:id/trigger',
handler: 'Webhooks.triggerWebhook',
config: {
policies: [],
},
},
];

View File

@ -7,7 +7,9 @@ module.exports = {
method: 'POST',
path: '/',
handler: 'email.send',
config: {},
config: {
policies: ['admin::isAuthenticatedAdmin'],
},
},
{
method: 'POST',

View File

@ -18,7 +18,7 @@ const { createCoreStore, coreStoreModel } = require('./services/core-store');
const createEntityService = require('./services/entity-service');
const entityValidator = require('./services/entity-validator');
const createTelemetry = require('./services/metrics');
const createContentAPI = require('./services/content-api');
const createAuth = require('./services/auth');
const createUpdateNotifier = require('./utils/update-notifier');
const createStartupLogger = require('./utils/startup-logger');
const ee = require('./utils/ee');
@ -53,7 +53,7 @@ class Strapi {
this.container.register('modules', modulesRegistry(this));
this.container.register('plugins', pluginsRegistry(this));
this.container.register('apis', apisRegistry(this));
this.container.register('content-api', createContentAPI(this));
this.container.register('auth', createAuth(this));
this.isLoaded = false;
this.reload = this.reload();

View File

@ -3,6 +3,7 @@
const { uniq, difference, get, isUndefined, merge } = require('lodash');
const requiredMiddlewares = [
'auth',
'responses',
'router',
'logger',

View File

@ -67,11 +67,13 @@ module.exports = strapi => {
method: 'GET',
path: '/',
handler: serveIndexPage,
config: { auth: false },
},
{
method: 'GET',
path: '/index.html',
handler: serveIndexPage,
config: { auth: false },
},
{
method: 'GET',
@ -80,6 +82,7 @@ module.exports = strapi => {
maxage: maxAge,
defer: true,
}),
config: { auth: false },
},
{
method: 'GET',
@ -88,6 +91,7 @@ module.exports = strapi => {
maxage: maxAge,
defer: true,
}),
config: { auth: false },
},
]);
}
@ -118,6 +122,7 @@ module.exports = strapi => {
serveAdmin,
serveStatic(buildDir, { maxage: maxAge, defer: false, index: 'index.html' }),
],
config: { auth: false },
},
]);
},

View File

@ -0,0 +1,92 @@
'use strict';
const { strict: assert } = require('assert');
const { has, prop } = require('lodash/fp');
class UnauthorizedError extends Error {}
class ForbiddenError extends Error {}
const INVALID_STRATEGY_MSG =
'Invalid auth strategy. Expecting an object with properties {name: string, authenticate: function, verify: function}';
const validStrategy = strategy => {
assert(has('authenticate', strategy), INVALID_STRATEGY_MSG);
assert(typeof strategy.authenticate === 'function', INVALID_STRATEGY_MSG);
if (has('verify', strategy)) {
assert(typeof strategy.verify === 'function', INVALID_STRATEGY_MSG);
}
};
const createAuthentication = () => {
const strategies = {};
return {
errors: {
UnauthorizedError,
ForbiddenError,
},
register(type, strategy) {
validStrategy(strategy);
if (!strategies[type]) {
strategies[type] = [];
}
strategies[type].push(strategy);
return this;
},
async authenticate(ctx, next) {
const { route } = ctx.state;
// use route strategy
const config = prop('config.auth', route);
if (config === false) {
return next();
}
const strategiesToUse = strategies[route.info.type];
for (const strategy of strategiesToUse) {
const result = await strategy.authenticate(ctx);
const { authenticated = false, error = null, credentials } = result || {};
if (error !== null) {
return ctx.unauthorized(error);
}
if (authenticated) {
ctx.state.isAuthenticated = true;
ctx.state.auth = {
strategy,
credentials,
};
return next();
}
}
return ctx.unauthorized('Missing credentials');
},
async verify(auth, config = {}) {
if (config === false) {
return;
}
if (!auth) {
throw new UnauthorizedError();
}
if (typeof auth.strategy.verify === 'function') {
return await auth.strategy.verify(auth, config);
}
return;
},
};
};
module.exports = createAuthentication;

View File

@ -1,71 +0,0 @@
'use strict';
const { strict: assert } = require('assert');
const { has } = require('lodash/fp');
class UnauthorizedError extends Error {}
class ForbiddenError extends Error {}
const INVALID_STRATEGY_MSG =
'Invalid auth strategy. Expecting an object with properties {name: string, authenticate: function, verify: function}';
const validStrategy = strategy => {
assert(has('name', strategy), INVALID_STRATEGY_MSG);
assert(typeof strategy.name === 'string', INVALID_STRATEGY_MSG);
assert(has('authenticate', strategy), INVALID_STRATEGY_MSG);
assert(typeof strategy.authenticate === 'function', INVALID_STRATEGY_MSG);
assert(has('verify', strategy), INVALID_STRATEGY_MSG);
assert(typeof strategy.verify === 'function', INVALID_STRATEGY_MSG);
};
const createAuthentication = () => {
const strategies = [];
return {
register(strategy) {
validStrategy(strategy);
strategies.push(strategy);
return () => {
strategies.splice(strategies.indexOf(strategy), 1);
};
},
async authenticate(ctx, next) {
for (const strategy of strategies) {
const result = await strategy.authenticate(ctx);
const { authenticated = false, credentials } = result || {};
if (authenticated) {
ctx.state.auth = { strategy, credentials };
return next();
}
}
return next();
},
async verify(auth, config = {}) {
if (config.public) {
return undefined;
}
if (!auth) {
throw new UnauthorizedError();
}
return await auth.strategy.verify(auth, config);
},
};
};
module.exports = () => {
return {
auth: createAuthentication(),
errors: {
UnauthorizedError,
ForbiddenError,
},
};
};

View File

@ -5,6 +5,7 @@ const { createAPI } = require('./api');
const createAdminAPI = strapi => {
const opts = {
prefix: '', // '/admin';
type: 'admin',
};
return createAPI(strapi, opts);

View File

@ -5,11 +5,11 @@ const Router = require('@koa/router');
const { createRouteManager } = require('./routing');
const createAPI = (strapi, opts = {}) => {
const { prefix, defaultPolicies } = opts;
const { prefix, type } = opts;
const api = new Router({ prefix });
const routeManager = createRouteManager(strapi, { defaultPolicies });
const routeManager = createRouteManager(strapi, { type });
return {
use(fn) {

View File

@ -1,6 +1,6 @@
'use strict';
const { toLower, castArray, trim } = require('lodash/fp');
const { toLower, castArray, trim, prop } = require('lodash/fp');
const compose = require('koa-compose');
const { resolveMiddlewares } = require('./middleware');
@ -9,24 +9,64 @@ const { resolvePolicies } = require('./policy');
const getMethod = route => trim(toLower(route.method));
const getPath = route => trim(route.path);
const routeInfoMiddleware = route => (ctx, next) => {
const createRouteInfoMiddleware = routeInfo => (ctx, next) => {
const route = {
...routeInfo,
config: routeInfo.config || {},
};
ctx.state.route = route;
return next();
};
const getAuthConfig = prop('config.auth');
const createAuthorizeMiddleware = strapi => async (ctx, next) => {
const { auth, route } = ctx.state;
const authService = strapi.container.get('auth');
try {
await authService.verify(auth, getAuthConfig(route));
return next();
} catch (error) {
const { UnauthorizedError, ForbiddenError } = authService.errors;
if (error instanceof UnauthorizedError) {
return ctx.unauthorized();
}
if (error instanceof ForbiddenError) {
return ctx.forbidden();
}
throw error;
}
};
const createAuthenticateMiddleware = strapi => async (ctx, next) => {
return strapi.container.get('auth').authenticate(ctx, next);
};
module.exports = strapi => {
return (route, { pluginName, router, apiName }) => {
const authenticate = createAuthenticateMiddleware(strapi);
const authorize = createAuthorizeMiddleware(strapi);
return (route, { router }) => {
try {
const method = getMethod(route);
const path = getPath(route);
const middlewares = resolveMiddlewares(route);
const policies = resolvePolicies(route, { pluginName, apiName });
const policies = resolvePolicies(route);
const action = getAction(route, { pluginName, apiName }, strapi);
const action = getAction(route, strapi);
const routeHandler = compose([
routeInfoMiddleware(route),
createRouteInfoMiddleware(route),
authenticate,
authorize,
...policies,
...middlewares,
...castArray(action),
@ -52,7 +92,10 @@ const getController = (name, { pluginName, apiName }, strapi) => {
return strapi.controller(name);
};
const getAction = ({ handler }, { pluginName, apiName }, strapi) => {
const getAction = (route, strapi) => {
const { handler, info = {} } = route;
const { pluginName, apiName } = info;
if (Array.isArray(handler) || typeof handler === 'function') {
return handler;
}

View File

@ -1,50 +1,14 @@
'use strict';
const { prop } = require('lodash/fp');
const { createAPI } = require('./api');
const getAuthConfig = prop('config.auth');
const createAuthPolicy = strapi => async (ctx, next) => {
const { auth, route } = ctx.state;
if (!route) {
return ctx.unauthorized();
}
try {
await strapi.container.get('content-api').auth.verify(auth, getAuthConfig(route));
return next();
} catch (error) {
const { errors } = strapi.container.get('content-api');
if (error instanceof errors.UnauthorizedError) {
return ctx.unauthorized();
}
if (error instanceof errors.ForbiddenError) {
return ctx.forbidden();
}
throw error;
}
};
const createContentAPI = strapi => {
const opts = {
prefix: strapi.config.get('api.prefix', '/api'),
defaultPolicies: [createAuthPolicy(strapi)],
type: 'content-api',
};
const api = createAPI(strapi, opts);
// implement auth providers
api.use((ctx, next) => {
return strapi.container.get('content-api').auth.authenticate(ctx, next);
});
return api;
return createAPI(strapi, opts);
};
module.exports = {

View File

@ -8,13 +8,9 @@ const { createRouteManager } = require('./routing');
const { createAdminAPI } = require('./admin-api');
const { createContentAPI } = require('./content-api');
const healthCheck = async (ctx, next) => {
if (ctx.request.url === '/_health' && ['HEAD', 'GET'].includes(ctx.request.method)) {
ctx.set('strapi', 'You are so French!');
ctx.status = 204;
} else {
await next();
}
const healthCheck = async ctx => {
ctx.set('strapi', 'You are so French!');
ctx.status = 204;
};
/**
@ -49,7 +45,7 @@ const createServer = strapi => {
};
// init health check
app.use(healthCheck);
router.all('/_health', healthCheck);
const state = {
mounted: false,

View File

@ -7,10 +7,14 @@ const { bodyPolicy } = policy;
const getPoliciesConfig = propOr([], 'config.policies');
const resolvePolicies = (route, opts = {}) => {
const resolvePolicies = route => {
const { pluginName, apiName } = route.info || {};
const policiesConfig = getPoliciesConfig(route);
const policies = policiesConfig.map(policyName => policy.get(policyName, opts));
const policies = policiesConfig.map(policyName => {
return policy.get(policyName, { pluginName, apiName });
});
return [...policies, bodyPolicy];
};

View File

@ -69,20 +69,16 @@ const validateRouteConfig = routeConfig => {
};
const createRouteManager = (strapi, opts = {}) => {
const { type } = opts;
const composeEndpoint = createEndpointComposer(strapi);
const createRoute = (route, router) => {
validateRouteConfig(route);
if (opts.defaultPolicies) {
if (has('config.policies', route)) {
route.config.policies.unshift(...opts.defaultPolicies);
} else {
_.set(route, 'config.policies', [...opts.defaultPolicies]);
}
}
_.set(route, 'info.type', type || 'admin');
composeEndpoint(route, { ...route.info, router });
composeEndpoint(route, { router });
};
const addRoutes = (routes, router) => {

View File

@ -32,6 +32,7 @@ module.exports = {
method: 'GET',
path: '/uploads/(.*)',
handler: [range, koaStatic(staticDir, { defer: true, ...localServerConfig })],
config: { auth: false },
},
]);
},

View File

@ -20,34 +20,34 @@ const authenticate = async ctx => {
const { id } = await getService('jwt').getToken(ctx);
if (id === undefined) {
throw new Error('Invalid token: Token did not contain required fields');
return { error: 'Invalid token: Token did not contain required fields' };
}
// fetch authenticated user
const user = await getService('user').fetchAuthenticatedUser(id);
if (user) {
return {
authenticated: true,
credentials: user,
};
if (!user) {
return { error: 'Invalid credentials' };
}
const advancedSettings = await getAdvancedSettings();
if (advancedSettings.email_confirmation && !user.confirmed) {
return { error: 'Invalid credentials' };
}
if (user.blocked) {
return { error: 'Invalid credentials' };
}
ctx.state.user = user;
return {
authenticated: true,
credentials: user,
};
} catch (err) {
return { authenticated: false };
}
if (!ctx.state.user) {
return { authenticated: false };
}
const advancedSettings = await getAdvancedSettings();
if (advancedSettings.email_confirmation && !ctx.state.user.confirmed) {
return { authenticated: false };
}
if (ctx.state.user.blocked) {
return { authenticated: false };
return { error: 'Invalid credentials' };
}
}
@ -68,7 +68,7 @@ const authenticate = async ctx => {
};
const verify = async (auth, config) => {
const { errors } = strapi.container.get('content-api');
const { errors } = strapi.container.get('auth');
const { credentials: user } = auth;

View File

@ -3,5 +3,5 @@
const authStrategy = require('./auth/strategy');
module.exports = strapi => {
strapi.container.get('content-api').auth.register(authStrategy);
strapi.container.get('auth').register('content-api', authStrategy);
};

View File

@ -20,10 +20,8 @@ const createStrapiInstance = async ({ ensureSuperAdmin = true, logLevel = 'fatal
const options = { dir: TEST_APP_URL };
const instance = strapi(options);
await instance.load();
instance.container.get('content-api').auth.register({
name: 'test-strategy',
instance.container.get('auth').register('content-api', {
name: 'test-auth',
authenticate() {
return { authenticated: true };
},
@ -32,6 +30,8 @@ const createStrapiInstance = async ({ ensureSuperAdmin = true, logLevel = 'fatal
},
});
await instance.load();
instance.log.level = logLevel;
instance.server.mount();