diff --git a/packages/core/admin/ee/server/routes/features-routes.js b/packages/core/admin/ee/server/routes/features-routes.js index 1612012426..b44a1b9cff 100644 --- a/packages/core/admin/ee/server/routes/features-routes.js +++ b/packages/core/admin/ee/server/routes/features-routes.js @@ -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', diff --git a/packages/core/admin/server/policies/hasPermissions.js b/packages/core/admin/server/policies/hasPermissions.js index 6b77c47de3..4fda9bee29 100644 --- a/packages/core/admin/server/policies/hasPermissions.js +++ b/packages/core/admin/server/policies/hasPermissions.js @@ -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(); } diff --git a/packages/core/admin/server/policies/isAuthenticatedAdmin.js b/packages/core/admin/server/policies/isAuthenticatedAdmin.js index 90dc786865..3d3bc8b7b6 100644 --- a/packages/core/admin/server/policies/isAuthenticatedAdmin.js +++ b/packages/core/admin/server/policies/isAuthenticatedAdmin.js @@ -1,7 +1,7 @@ 'use strict'; module.exports = (ctx, next) => { - if (!ctx.state.isAuthenticatedAdmin) { + if (!ctx.state.isAuthenticated) { return ctx.unauthorized(); } diff --git a/packages/core/admin/server/register.js b/packages/core/admin/server/register.js index e2d7d9c86b..23ca2dc11c 100644 --- a/packages/core/admin/server/register.js +++ b/packages/core/admin/server/register.js @@ -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); diff --git a/packages/core/admin/server/routes/admin.js b/packages/core/admin/server/routes/admin.js new file mode 100644 index 0000000000..35ab8a9027 --- /dev/null +++ b/packages/core/admin/server/routes/admin.js @@ -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'] }, + }, + ], + }, + }, +]; diff --git a/packages/core/admin/server/routes/authentication.js b/packages/core/admin/server/routes/authentication.js new file mode 100644 index 0000000000..9b031e07ca --- /dev/null +++ b/packages/core/admin/server/routes/authentication.js @@ -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 }, + }, +]; diff --git a/packages/core/admin/server/routes/index.js b/packages/core/admin/server/routes/index.js index f559f0946b..10fdf88795 100644 --- a/packages/core/admin/server/routes/index.js +++ b/packages/core/admin/server/routes/index.js @@ -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]; diff --git a/packages/core/admin/server/routes/permissions.js b/packages/core/admin/server/routes/permissions.js new file mode 100644 index 0000000000..5e279338e2 --- /dev/null +++ b/packages/core/admin/server/routes/permissions.js @@ -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'], + }, + }, +]; diff --git a/packages/core/admin/server/routes/roles.js b/packages/core/admin/server/routes/roles.js new file mode 100644 index 0000000000..e181adac75 --- /dev/null +++ b/packages/core/admin/server/routes/roles.js @@ -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'] } }, + ], + }, + }, +]; diff --git a/packages/core/admin/server/routes/users.js b/packages/core/admin/server/routes/users.js new file mode 100644 index 0000000000..272fddc470 --- /dev/null +++ b/packages/core/admin/server/routes/users.js @@ -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'] } }], + }, + }, +]; diff --git a/packages/core/admin/server/routes/webhooks.js b/packages/core/admin/server/routes/webhooks.js new file mode 100644 index 0000000000..6e5eaa6574 --- /dev/null +++ b/packages/core/admin/server/routes/webhooks.js @@ -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: [], + }, + }, +]; diff --git a/packages/core/email/server/routes/admin.js b/packages/core/email/server/routes/admin.js index 341550bb48..70b94e589b 100644 --- a/packages/core/email/server/routes/admin.js +++ b/packages/core/email/server/routes/admin.js @@ -7,7 +7,9 @@ module.exports = { method: 'POST', path: '/', handler: 'email.send', - config: {}, + config: { + policies: ['admin::isAuthenticatedAdmin'], + }, }, { method: 'POST', diff --git a/packages/core/strapi/lib/Strapi.js b/packages/core/strapi/lib/Strapi.js index 40c0699f96..e2dbba44cc 100644 --- a/packages/core/strapi/lib/Strapi.js +++ b/packages/core/strapi/lib/Strapi.js @@ -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(); diff --git a/packages/core/strapi/lib/middlewares/index.js b/packages/core/strapi/lib/middlewares/index.js index 506ed3734e..6aab681925 100644 --- a/packages/core/strapi/lib/middlewares/index.js +++ b/packages/core/strapi/lib/middlewares/index.js @@ -3,6 +3,7 @@ const { uniq, difference, get, isUndefined, merge } = require('lodash'); const requiredMiddlewares = [ + 'auth', 'responses', 'router', 'logger', diff --git a/packages/core/strapi/lib/middlewares/public/index.js b/packages/core/strapi/lib/middlewares/public/index.js index e273850d5b..25479a8fcc 100644 --- a/packages/core/strapi/lib/middlewares/public/index.js +++ b/packages/core/strapi/lib/middlewares/public/index.js @@ -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 }, }, ]); }, diff --git a/packages/core/strapi/lib/services/auth/index.js b/packages/core/strapi/lib/services/auth/index.js new file mode 100644 index 0000000000..093ad98de0 --- /dev/null +++ b/packages/core/strapi/lib/services/auth/index.js @@ -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; diff --git a/packages/core/strapi/lib/services/content-api/index.js b/packages/core/strapi/lib/services/content-api/index.js deleted file mode 100644 index e500ea53d8..0000000000 --- a/packages/core/strapi/lib/services/content-api/index.js +++ /dev/null @@ -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, - }, - }; -}; diff --git a/packages/core/strapi/lib/services/server/admin-api.js b/packages/core/strapi/lib/services/server/admin-api.js index 23f5b6e7b8..86c915a023 100644 --- a/packages/core/strapi/lib/services/server/admin-api.js +++ b/packages/core/strapi/lib/services/server/admin-api.js @@ -5,6 +5,7 @@ const { createAPI } = require('./api'); const createAdminAPI = strapi => { const opts = { prefix: '', // '/admin'; + type: 'admin', }; return createAPI(strapi, opts); diff --git a/packages/core/strapi/lib/services/server/api.js b/packages/core/strapi/lib/services/server/api.js index 02fadb0f1d..52c5dcab83 100644 --- a/packages/core/strapi/lib/services/server/api.js +++ b/packages/core/strapi/lib/services/server/api.js @@ -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) { diff --git a/packages/core/strapi/lib/services/server/compose-endpoint.js b/packages/core/strapi/lib/services/server/compose-endpoint.js index 97e65ba5d8..73777b4419 100644 --- a/packages/core/strapi/lib/services/server/compose-endpoint.js +++ b/packages/core/strapi/lib/services/server/compose-endpoint.js @@ -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; } diff --git a/packages/core/strapi/lib/services/server/content-api.js b/packages/core/strapi/lib/services/server/content-api.js index e1ec4a6fda..201ebb6c6b 100644 --- a/packages/core/strapi/lib/services/server/content-api.js +++ b/packages/core/strapi/lib/services/server/content-api.js @@ -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 = { diff --git a/packages/core/strapi/lib/services/server/index.js b/packages/core/strapi/lib/services/server/index.js index d5ae9453cb..9d549f28d8 100644 --- a/packages/core/strapi/lib/services/server/index.js +++ b/packages/core/strapi/lib/services/server/index.js @@ -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, diff --git a/packages/core/strapi/lib/services/server/policy.js b/packages/core/strapi/lib/services/server/policy.js index 984f24bda1..f35668dda1 100644 --- a/packages/core/strapi/lib/services/server/policy.js +++ b/packages/core/strapi/lib/services/server/policy.js @@ -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]; }; diff --git a/packages/core/strapi/lib/services/server/routing.js b/packages/core/strapi/lib/services/server/routing.js index e696063845..7f7c442644 100644 --- a/packages/core/strapi/lib/services/server/routing.js +++ b/packages/core/strapi/lib/services/server/routing.js @@ -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) => { diff --git a/packages/core/upload/server/middlewares/upload.js b/packages/core/upload/server/middlewares/upload.js index 8756833ccd..a96bf57db3 100644 --- a/packages/core/upload/server/middlewares/upload.js +++ b/packages/core/upload/server/middlewares/upload.js @@ -32,6 +32,7 @@ module.exports = { method: 'GET', path: '/uploads/(.*)', handler: [range, koaStatic(staticDir, { defer: true, ...localServerConfig })], + config: { auth: false }, }, ]); }, diff --git a/packages/plugins/users-permissions/server/auth/strategy.js b/packages/plugins/users-permissions/server/auth/strategy.js index 9f261f313b..01304e66b3 100644 --- a/packages/plugins/users-permissions/server/auth/strategy.js +++ b/packages/plugins/users-permissions/server/auth/strategy.js @@ -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; diff --git a/packages/plugins/users-permissions/server/register.js b/packages/plugins/users-permissions/server/register.js index c6454c832c..dd1a060fa1 100644 --- a/packages/plugins/users-permissions/server/register.js +++ b/packages/plugins/users-permissions/server/register.js @@ -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); }; diff --git a/test/helpers/strapi.js b/test/helpers/strapi.js index 6dbdf5955f..528d10c662 100644 --- a/test/helpers/strapi.js +++ b/test/helpers/strapi.js @@ -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();