diff --git a/packages/strapi-admin/config/functions/bootstrap.js b/packages/strapi-admin/config/functions/bootstrap.js index 528c478494..dac4675ba7 100644 --- a/packages/strapi-admin/config/functions/bootstrap.js +++ b/packages/strapi-admin/config/functions/bootstrap.js @@ -23,10 +23,18 @@ const registerAdminConditions = () => { const syncAuthSettings = async () => { const adminStore = await strapi.store({ type: 'core', environment: '', name: 'admin' }); - const adminAuthSettings = await adminStore.get({ key: 'auth' }); const newAuthSettings = merge(defaultAdminAuthSettings, adminAuthSettings); + const roleExists = await strapi.admin.services.role.exists({ + id: newAuthSettings.providers.defaultRole, + }); + + // Reset the default SSO role if it has been deleted manually + if (!roleExists) { + newAuthSettings.providers.defaultRole = null; + } + await adminStore.set({ key: 'auth', value: newAuthSettings }); }; diff --git a/packages/strapi-admin/controllers/authentication.js b/packages/strapi-admin/controllers/authentication.js index ba7e94bb5a..faf620fc06 100644 --- a/packages/strapi-admin/controllers/authentication.js +++ b/packages/strapi-admin/controllers/authentication.js @@ -16,14 +16,22 @@ module.exports = { (ctx, next) => { return passport.authenticate('local', { session: false }, (err, user, info) => { if (err) { + strapi.eventHub.emit('admin.auth.error', { error: err, provider: 'local' }); return ctx.badImplementation(); } if (!user) { + strapi.eventHub.emit('admin.auth.error', { + error: new Error(info.message), + provider: 'local', + }); return ctx.badRequest(info.message); } ctx.state.user = user; + + strapi.eventHub.emit('admin.auth.success', { user, provider: 'local' }); + return next(); })(ctx, next); }, diff --git a/packages/strapi-admin/ee/config/admin-actions.js b/packages/strapi-admin/ee/config/admin-actions.js index 747a175420..8eb826c40f 100644 --- a/packages/strapi-admin/ee/config/admin-actions.js +++ b/packages/strapi-admin/ee/config/admin-actions.js @@ -1,22 +1,24 @@ 'use strict'; module.exports = { - actions: [ - { - uid: 'provider-login.read', - displayName: 'Read', - pluginName: 'admin', - section: 'settings', - category: 'single sign on', - subCategory: 'options', - }, - { - uid: 'provider-login.update', - displayName: 'Update', - pluginName: 'admin', - section: 'settings', - category: 'single sign on', - subCategory: 'options', - }, - ], + features: { + sso: [ + { + uid: 'provider-login.read', + displayName: 'Read', + pluginName: 'admin', + section: 'settings', + category: 'single sign on', + subCategory: 'options', + }, + { + uid: 'provider-login.update', + displayName: 'Update', + pluginName: 'admin', + section: 'settings', + category: 'single sign on', + subCategory: 'options', + }, + ], + }, }; diff --git a/packages/strapi-admin/ee/config/functions/bootstrap.js b/packages/strapi-admin/ee/config/functions/bootstrap.js index 7623d60f52..e0b24de676 100644 --- a/packages/strapi-admin/ee/config/functions/bootstrap.js +++ b/packages/strapi-admin/ee/config/functions/bootstrap.js @@ -1,10 +1,15 @@ 'use strict'; +const { features } = require('../../../../strapi/lib/utils/ee'); const executeCEBootstrap = require('../../../config/functions/bootstrap'); -const { actions: eeActions } = require('../admin-actions'); +const { + features: { sso: ssoActions }, +} = require('../admin-actions'); module.exports = async () => { - strapi.admin.services.permission.actionProvider.register(eeActions); + if (features.isEnabled('sso')) { + strapi.admin.services.permission.actionProvider.register(ssoActions); + } await executeCEBootstrap(); }; diff --git a/packages/strapi-admin/ee/config/routes.json b/packages/strapi-admin/ee/config/routes.json index afb491c6c3..a54167815d 100644 --- a/packages/strapi-admin/ee/config/routes.json +++ b/packages/strapi-admin/ee/config/routes.json @@ -23,38 +23,6 @@ "config": { "policies": [] } - }, - { - "method": "GET", - "path": "/providers", - "handler": "authentication.getProviders" - }, - { - "method": "GET", - "path": "/connect/:provider", - "handler": "authentication.providerLogin" - }, - { - "method": "GET", - "path": "/providers/options", - "handler": "authentication.getProviderLoginOptions", - "config": { - "policies": [ - "admin::isAuthenticatedAdmin", - ["admin::hasPermissions", ["admin::provider-login.read"]] - ] - } - }, - { - "method": "PUT", - "path": "/providers/options", - "handler": "authentication.updateProviderLoginOptions", - "config": { - "policies": [ - "admin::isAuthenticatedAdmin", - ["admin::hasPermissions", ["admin::provider-login.update"]] - ] - } } ] } diff --git a/packages/strapi-admin/ee/controllers/authentication/middlewares.js b/packages/strapi-admin/ee/controllers/authentication/middlewares.js index 8ea57e78d0..2ade4ff211 100644 --- a/packages/strapi-admin/ee/controllers/authentication/middlewares.js +++ b/packages/strapi-admin/ee/controllers/authentication/middlewares.js @@ -5,6 +5,7 @@ const passport = require('koa-passport'); const utils = require('./utils'); const isProduction = process.env.NODE_ENV === 'production'; +const defaultConnectionError = new Error('Invalid connection payload'); const authenticate = async (ctx, next) => { const { @@ -13,11 +14,16 @@ const authenticate = async (ctx, next) => { const redirectUrls = utils.getPrefixedRedirectUrls(); return passport.authenticate(provider, null, async (error, profile) => { - if (error || !profile) { + if (error || !profile || !profile.email) { if (error) { strapi.log.error(error); } + strapi.eventHub.emit('admin.auth.error', { + error: error || defaultConnectionError, + provider, + }); + return ctx.redirect(redirectUrls.error); } @@ -37,6 +43,7 @@ const authenticate = async (ctx, next) => { const isMissingRegisterFields = !username && (!firstname || !lastname); if (!providers.autoRegister || !providers.defaultRole || isMissingRegisterFields) { + strapi.eventHub.emit('admin.auth.error', { error: defaultConnectionError, provider }); return ctx.redirect(redirectUrls.error); } @@ -44,6 +51,7 @@ const authenticate = async (ctx, next) => { // If the default role has been misconfigured, redirect with an error if (!defaultRole) { + strapi.eventHub.emit('admin.auth.error', { error: defaultConnectionError, provider }); return ctx.redirect(redirectUrls.error); } @@ -58,15 +66,26 @@ const authenticate = async (ctx, next) => { registrationToken: null, }); + strapi.eventHub.emit('admin.auth.autoRegistration', { + user: ctx.state.user, + provider, + }); + return next(); })(ctx, next); }; const redirectWithAuth = ctx => { + const { + params: { provider }, + } = ctx; + const redirectUrls = utils.getPrefixedRedirectUrls(); const { user } = ctx.state; + const jwt = strapi.admin.services.token.createJwtToken(user); const cookiesOptions = { httpOnly: false, secure: isProduction, overwrite: true }; - const redirectUrls = utils.getPrefixedRedirectUrls(); + + strapi.eventHub.emit('admin.auth.success', { user, provider }); ctx.cookies.set('jwtToken', jwt, cookiesOptions); ctx.redirect(redirectUrls.success); diff --git a/packages/strapi-admin/ee/controllers/authentication/utils.js b/packages/strapi-admin/ee/controllers/authentication/utils.js index 1302535913..eb953bf429 100644 --- a/packages/strapi-admin/ee/controllers/authentication/utils.js +++ b/packages/strapi-admin/ee/controllers/authentication/utils.js @@ -11,14 +11,8 @@ const PROVIDER_URLS_MAP = { const getAdminStore = async () => strapi.store({ type: 'core', environment: '', name: 'admin' }); const getPrefixedRedirectUrls = () => { - const { host, port, path } = strapi.config.get('admin'); - - let baseUrl = host || ''; - if (baseUrl && port) { - baseUrl = `${baseUrl}:${port}`; - } - - const prefixUrl = url => `${baseUrl}${path}${url}`; + const { url: adminUrl } = strapi.config.get('admin'); + const prefixUrl = url => `${adminUrl || ''}${url}`; return mapValues(prefixUrl, PROVIDER_URLS_MAP); }; diff --git a/packages/strapi-admin/ee/middlewares/features-routes/defaults.json b/packages/strapi-admin/ee/middlewares/features-routes/defaults.json new file mode 100644 index 0000000000..8258a3a555 --- /dev/null +++ b/packages/strapi-admin/ee/middlewares/features-routes/defaults.json @@ -0,0 +1,5 @@ +{ + "features-routes": { + "enabled": true + } +} diff --git a/packages/strapi-admin/ee/middlewares/features-routes/index.js b/packages/strapi-admin/ee/middlewares/features-routes/index.js new file mode 100644 index 0000000000..27a142e4a4 --- /dev/null +++ b/packages/strapi-admin/ee/middlewares/features-routes/index.js @@ -0,0 +1,22 @@ +'use strict'; + +const { features } = require('../../../../strapi/lib/utils/ee'); +const routes = require('./routes'); + +module.exports = strapi => ({ + beforeInitialize() { + strapi.config.hook.load.before.unshift('admin'); + }, + + initialize() { + loadFeaturesRoutes(); + }, +}); + +const loadFeaturesRoutes = () => { + for (const [feature, getFeatureRoutes] of Object.entries(routes)) { + if (features.isEnabled(feature)) { + strapi.admin.config.routes.push(...getFeatureRoutes()); + } + } +}; diff --git a/packages/strapi-admin/ee/middlewares/features-routes/routes.js b/packages/strapi-admin/ee/middlewares/features-routes/routes.js new file mode 100644 index 0000000000..7e057e111b --- /dev/null +++ b/packages/strapi-admin/ee/middlewares/features-routes/routes.js @@ -0,0 +1,38 @@ +'use strict'; + +module.exports = { + sso: () => [ + { + method: 'GET', + path: '/providers', + handler: 'authentication.getProviders', + }, + { + method: 'GET', + path: '/connect/:provider', + handler: 'authentication.providerLogin', + }, + { + method: 'GET', + path: '/providers/options', + handler: 'authentication.getProviderLoginOptions', + config: { + policies: [ + 'admin::isAuthenticatedAdmin', + ['admin::hasPermissions', ['admin::provider-login.read']], + ], + }, + }, + { + method: 'PUT', + path: '/providers/options', + handler: 'authentication.updateProviderLoginOptions', + config: { + policies: [ + 'admin::isAuthenticatedAdmin', + ['admin::hasPermissions', ['admin::provider-login.update']], + ], + }, + }, + ], +}; diff --git a/packages/strapi-admin/ee/services/passport.js b/packages/strapi-admin/ee/services/passport.js new file mode 100644 index 0000000000..1e6ba80d4f --- /dev/null +++ b/packages/strapi-admin/ee/services/passport.js @@ -0,0 +1,54 @@ +'use strict'; + +const { isFunction } = require('lodash/fp'); + +const { features } = require('../../../strapi/lib/utils/ee'); + +const createLocalStrategy = require('../../services/passport/local-strategy'); +const createProviderRegistry = require('./passport/provider-registry'); + +const valueIsFunctionType = ([, value]) => isFunction(value); + +const providerRegistry = createProviderRegistry(); + +const getStrategyCallbackURL = providerName => `/admin/connect/${providerName}`; + +const syncProviderRegistryWithConfig = () => { + const { providers = [], events = {} } = strapi.config.get('server.admin.auth', {}); + const eventList = Object.entries(events); + + providerRegistry.registerMany(providers); + + for (const [eventName, handler] of eventList.filter(valueIsFunctionType)) { + strapi.eventHub.on(`admin.auth.${eventName}`, handler); + } +}; + +const getPassportStrategies = () => { + const localStrategy = createLocalStrategy(strapi); + + if (!features.isEnabled('sso')) { + return [localStrategy]; + } + + if (!strapi.isLoaded) { + syncProviderRegistryWithConfig(); + } + + const providers = providerRegistry.toArray(); + const strategies = providers.map(provider => provider.createStrategy(strapi)); + + return [localStrategy, ...strategies]; +}; + +module.exports = { + getPassportStrategies, +}; + +if (features.isEnabled('sso')) { + Object.assign(module.exports, { + syncProviderRegistryWithConfig, + getStrategyCallbackURL, + providerRegistry, + }); +} diff --git a/packages/strapi-admin/services/passport/provider-registry.js b/packages/strapi-admin/ee/services/passport/provider-registry.js similarity index 100% rename from packages/strapi-admin/services/passport/provider-registry.js rename to packages/strapi-admin/ee/services/passport/provider-registry.js diff --git a/packages/strapi-admin/ee/services/role.js b/packages/strapi-admin/ee/services/role.js new file mode 100644 index 0000000000..412c644393 --- /dev/null +++ b/packages/strapi-admin/ee/services/role.js @@ -0,0 +1,22 @@ +'use strict'; + +const { toString } = require('lodash/fp'); + +const ssoCheckRolesIdForDeletion = async ids => { + const adminStore = await strapi.store({ type: 'core', environment: '', name: 'admin' }); + const { + providers: { defaultRole }, + } = await adminStore.get({ key: 'auth' }); + + for (const roleId of ids) { + if (defaultRole && toString(defaultRole) === toString(roleId)) { + throw new Error( + 'This role is used as the default SSO role. Make sure to change this configuration before deleting the role' + ); + } + } +}; + +module.exports = { + ssoCheckRolesIdForDeletion, +}; diff --git a/packages/strapi-admin/ee/validation/role.js b/packages/strapi-admin/ee/validation/role.js index d4dc60e003..8d618d88d4 100644 --- a/packages/strapi-admin/ee/validation/role.js +++ b/packages/strapi-admin/ee/validation/role.js @@ -2,6 +2,8 @@ const { yup, formatYupErrors } = require('strapi-utils'); +const { features } = require('../../../strapi/lib/utils/ee'); + const handleReject = error => Promise.reject(formatYupErrors(error)); const roleCreateSchema = yup @@ -26,6 +28,10 @@ const rolesDeleteSchema = yup .test('roles-deletion-checks', 'Roles deletion checks have failed', async function(ids) { try { await strapi.admin.services.role.checkRolesIdForDeletion(ids); + + if (features.isEnabled('sso')) { + await strapi.admin.services.role.ssoCheckRolesIdForDeletion(ids); + } } catch (e) { return this.createError({ path: 'ids', message: e.message }); } @@ -41,6 +47,10 @@ const roleDeleteSchema = yup .test('no-admin-single-delete', 'Role deletion checks have failed', async function(id) { try { await strapi.admin.services.role.checkRolesIdForDeletion([id]); + + if (features.isEnabled('sso')) { + await strapi.admin.services.role.ssoCheckRolesIdForDeletion([id]); + } } catch (e) { return this.createError({ path: 'id', message: e.message }); } diff --git a/packages/strapi-admin/services/passport.js b/packages/strapi-admin/services/passport.js index d543201b49..e13097e134 100644 --- a/packages/strapi-admin/services/passport.js +++ b/packages/strapi-admin/services/passport.js @@ -2,39 +2,19 @@ const passport = require('koa-passport'); -const createProviderRegistry = require('./passport/provider-registry'); const createLocalStrategy = require('./passport/local-strategy'); -const providerRegistry = createProviderRegistry(); - -const getProviderCallbackUrl = providerName => `/admin/connect/${providerName}`; - -const syncProviderRegistryWithConfig = () => { - const { providers = [] } = strapi.config.get('server.admin.auth', {}); - - providerRegistry.registerMany(providers); -}; +const getPassportStrategies = () => [createLocalStrategy(strapi)]; const init = () => { - syncProviderRegistryWithConfig(); - - const localStrategy = createLocalStrategy(strapi); - - const providers = providerRegistry.toArray(); - const strategies = providers.map(provider => provider.createStrategy(strapi)); - - // Register the local strategy - passport.use(localStrategy); - - // And add the ones provided with the config - strategies.forEach(provider => passport.use(provider)); + strapi.admin.services.passport + .getPassportStrategies() + .forEach(strategy => passport.use(strategy)); return passport.initialize(); }; module.exports = { init, - syncProviderRegistryWithConfig, - providerRegistry, - getProviderCallbackUrl, + getPassportStrategies, }; diff --git a/packages/strapi-admin/services/role.js b/packages/strapi-admin/services/role.js index 5e6f20c2fe..1671a1725c 100644 --- a/packages/strapi-admin/services/role.js +++ b/packages/strapi-admin/services/role.js @@ -1,7 +1,7 @@ 'use strict'; const _ = require('lodash'); -const { set, toString } = require('lodash/fp'); +const { set } = require('lodash/fp'); const { generateTimestampCode, stringIncludes } = require('strapi-utils'); const { createPermission } = require('../domain/permission'); const { validatePermissionsExist } = require('../validation/permission'); @@ -156,10 +156,6 @@ const count = async (params = {}) => { * @returns {Promise} */ const checkRolesIdForDeletion = async (ids = []) => { - const adminStore = await strapi.store({ type: 'core', environment: '', name: 'admin' }); - const { - providers: { defaultRole }, - } = await adminStore.get({ key: 'auth' }); const superAdminRole = await getSuperAdmin(); if (superAdminRole && stringIncludes(ids, superAdminRole.id)) { @@ -171,12 +167,6 @@ const checkRolesIdForDeletion = async (ids = []) => { if (usersCount !== 0) { throw new Error('Some roles are still assigned to some users'); } - - if (defaultRole && toString(defaultRole) === toString(roleId)) { - throw new Error( - 'This role is used as the default SSO role. Make sure to change this configuration before deleting the role' - ); - } } }; diff --git a/packages/strapi/lib/core/load-hooks.js b/packages/strapi/lib/core/load-hooks.js index f5a46338ef..27f7e1869a 100644 --- a/packages/strapi/lib/core/load-hooks.js +++ b/packages/strapi/lib/core/load-hooks.js @@ -17,6 +17,8 @@ module.exports = async function({ installedHooks, installedPlugins, appPath }) { loadHookDependencies(installedHooks, hooks), // local middleware loadLocalHooks(appPath, hooks), + // admin hooks + loadAdminHooks(hooks), // plugins middlewares loadPluginsHooks(installedPlugins, hooks), // local plugin middlewares @@ -46,6 +48,17 @@ const loadPluginsHooks = async (plugins, hooks) => { } }; +const loadAdminHooks = async hooks => { + const hooksDir = 'hooks'; + const dir = path.resolve(findPackagePath('strapi-admin'), hooksDir); + await loadHooksInDir(dir, hooks); + + // load ee admin hooks if they exist + if (process.env.STRAPI_DISABLE_EE !== 'true' && strapi.EE) { + await loadHooksInDir(`${dir}/../ee/${hooksDir}`, hooks); + } +}; + const loadLocalPluginsHooks = async (appPath, hooks) => { const pluginsDir = path.resolve(appPath, 'plugins'); if (!fs.existsSync(pluginsDir)) return; diff --git a/packages/strapi/lib/core/load-middlewares.js b/packages/strapi/lib/core/load-middlewares.js index 7ca28790ef..5c7a4ced67 100644 --- a/packages/strapi/lib/core/load-middlewares.js +++ b/packages/strapi/lib/core/load-middlewares.js @@ -78,8 +78,14 @@ const createLoaders = strapi => { }; const loadAdminMiddlewares = async middlewares => { - const dir = path.resolve(findPackagePath(`strapi-admin`), 'middlewares'); + const middlewaresDir = 'middlewares'; + const dir = path.resolve(findPackagePath(`strapi-admin`), middlewaresDir); await loadMiddlewaresInDir(dir, middlewares); + + // load ee admin middlewares if they exist + if (process.env.STRAPI_DISABLE_EE !== 'true' && strapi.EE) { + await loadMiddlewaresInDir(`${dir}/../ee/${middlewaresDir}`, middlewares); + } }; const loadMiddlewareDependencies = async (packages, middlewares) => { diff --git a/packages/strapi/lib/utils/ee.js b/packages/strapi/lib/utils/ee.js index def1b5ec6a..96bc867277 100644 --- a/packages/strapi/lib/utils/ee.js +++ b/packages/strapi/lib/utils/ee.js @@ -15,6 +15,11 @@ const noLog = { }; const internals = {}; +const features = { + bronze: [], + silver: [], + gold: ['sso'], +}; module.exports = ({ dir, logger = noLog }) => { if (_.has(internals, 'isEE')) return internals.isEE; @@ -89,6 +94,22 @@ Object.defineProperty(module.exports, 'isEE', { enumerable: false, }); +Object.defineProperty(module.exports, 'features', { + get: () => { + mustHaveKey('licenseInfo'); + + const { type: licenseType } = module.exports.licenseInfo; + + return { + isEnabled(feature) { + return features[licenseType].includes(feature); + }, + }; + }, + configurable: false, + enumerable: false, +}); + const mustHaveKey = key => { if (!_.has(internals, key)) { const err = new Error('Tampering with license');