mirror of
https://github.com/strapi/strapi.git
synced 2025-12-27 07:03:38 +00:00
Authentication events, sso gold only
This commit is contained in:
parent
3afb864e3d
commit
531c926dfd
@ -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 });
|
||||
};
|
||||
|
||||
|
||||
@ -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);
|
||||
},
|
||||
|
||||
@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
@ -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();
|
||||
};
|
||||
|
||||
@ -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"]]
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
{
|
||||
"features-routes": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -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']],
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
54
packages/strapi-admin/ee/services/passport.js
Normal file
54
packages/strapi-admin/ee/services/passport.js
Normal 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,
|
||||
});
|
||||
}
|
||||
22
packages/strapi-admin/ee/services/role.js
Normal file
22
packages/strapi-admin/ee/services/role.js
Normal 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,
|
||||
};
|
||||
@ -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 });
|
||||
}
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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'
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) => {
|
||||
|
||||
@ -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');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user