Authentication events, sso gold only

This commit is contained in:
Convly 2021-01-27 11:52:02 +01:00
parent 3afb864e3d
commit 531c926dfd
19 changed files with 265 additions and 100 deletions

View File

@ -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 });
};

View File

@ -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);
},

View File

@ -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',
},
],
},
};

View File

@ -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();
};

View File

@ -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"]]
]
}
}
]
}

View File

@ -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);

View File

@ -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);
};

View File

@ -0,0 +1,5 @@
{
"features-routes": {
"enabled": true
}
}

View File

@ -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());
}
}
};

View File

@ -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']],
],
},
},
],
};

View File

@ -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,
});
}

View File

@ -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,
};

View File

@ -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 });
}

View File

@ -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,
};

View File

@ -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<void>}
*/
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'
);
}
}
};

View File

@ -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;

View File

@ -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) => {

View File

@ -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');