Runtime server feature flag

This commit is contained in:
Alexandre Bodin 2022-11-24 19:08:45 +01:00 committed by fdel-car
parent 9211620591
commit 5a2b379fa0
7 changed files with 144 additions and 107 deletions

View File

@ -17,5 +17,7 @@ module.exports = async () => {
await actionProvider.registerMany(actions.auditLogs);
}
// TODO: check admin seats
await executeCEBootstrap();
};

View File

@ -1,80 +0,0 @@
'use strict';
module.exports = {
sso: [
{
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',
path: '/providers/options',
handler: 'authentication.getProviderLoginOptions',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', config: { actions: ['admin::provider-login.read'] } },
],
},
},
{
method: 'PUT',
path: '/providers/options',
handler: 'authentication.updateProviderLoginOptions',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', config: { actions: ['admin::provider-login.update'] } },
],
},
},
],
'audit-logs': [
{
method: 'GET',
path: '/audit-logs',
handler: 'auditLogs.findMany',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{
name: 'admin::hasPermissions',
config: {
actions: ['admin::audit-logs.read'],
},
},
],
},
},
{
method: 'GET',
path: '/audit-logs/:id',
handler: 'auditLogs.findOne',
config: {
policies: [
'admin::isAuthenticatedAdmin',
{
name: 'admin::hasPermissions',
config: {
actions: ['admin::audit-logs.read'],
},
},
],
},
},
],
};

View File

@ -1,17 +1,14 @@
'use strict';
// eslint-disable-next-line node/no-extraneous-require
const { features } = require('@strapi/strapi/lib/utils/ee');
const featuresRoutes = require('./features-routes');
const getFeaturesRoutes = () => {
return Object.entries(featuresRoutes).flatMap(([featureName, featureRoutes]) => {
if (features.isEnabled(featureName)) {
return featureRoutes;
}
const enableFeatureMiddleware = (featureName) => (ctx, next) => {
console.log(featureName, features.isEnabled(featureName));
if (features.isEnabled(featureName)) {
return next();
}
return [];
});
ctx.status = 404;
};
module.exports = [
@ -63,5 +60,93 @@ module.exports = [
],
},
},
...getFeaturesRoutes(),
// SSO
{
method: 'GET',
path: '/providers',
handler: 'authentication.getProviders',
config: {
middlewares: [enableFeatureMiddleware('sso')],
auth: false,
},
},
{
method: 'GET',
path: '/connect/:provider',
handler: 'authentication.providerLogin',
config: {
middlewares: [enableFeatureMiddleware('sso')],
auth: false,
},
},
{
method: 'POST',
path: '/connect/:provider',
handler: 'authentication.providerLogin',
config: {
middlewares: [enableFeatureMiddleware('sso')],
auth: false,
},
},
{
method: 'GET',
path: '/providers/options',
handler: 'authentication.getProviderLoginOptions',
config: {
middlewares: [enableFeatureMiddleware('sso')],
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', config: { actions: ['admin::provider-login.read'] } },
],
},
},
{
method: 'PUT',
path: '/providers/options',
handler: 'authentication.updateProviderLoginOptions',
config: {
middlewares: [enableFeatureMiddleware('sso')],
policies: [
'admin::isAuthenticatedAdmin',
{ name: 'admin::hasPermissions', config: { actions: ['admin::provider-login.update'] } },
],
},
},
// Audit logs
{
method: 'GET',
path: '/audit-logs',
handler: 'auditLogs.findMany',
config: {
middlewares: [enableFeatureMiddleware('audit-logs')],
policies: [
'admin::isAuthenticatedAdmin',
{
name: 'admin::hasPermissions',
config: {
actions: ['admin::audit-logs.read'],
},
},
],
},
},
{
method: 'GET',
path: '/audit-logs/:id',
handler: 'auditLogs.findOne',
config: {
middlewares: [enableFeatureMiddleware('audit-logs')],
policies: [
'admin::isAuthenticatedAdmin',
{
name: 'admin::hasPermissions',
config: {
actions: ['admin::audit-logs.read'],
},
},
],
},
},
];

View File

@ -6,6 +6,28 @@ const { features } = require('@strapi/strapi/lib/utils/ee');
const createLocalStrategy = require('../../../server/services/passport/local-strategy');
const sso = require('./passport/sso');
// wrap functions with feature flag to allow execute code lazyly
// Looking at the code wrapped we probably can just add a condition in the functions
const wrapWithFeatureFlag = (flag, obj) => {
const newObj = {};
Object.keys(obj).forEach((key) => {
if (typeof obj[key] === 'function') {
newObj[key] = (...args) => {
if (!features.isEnabled(flag)) {
throw new Error(`${key} cannot be executed`);
}
return obj[key].apply(newObj, ...args);
};
} else {
newObj[key] = obj[key];
}
});
return newObj;
};
const getPassportStrategies = () => {
const localStrategy = createLocalStrategy(strapi);
@ -25,8 +47,5 @@ const getPassportStrategies = () => {
module.exports = {
getPassportStrategies,
...wrapWithFeatureFlag('sso', sso),
};
if (features.isEnabled('sso')) {
Object.assign(module.exports, sso);
}

View File

@ -21,7 +21,7 @@ const defaultFeatures = {
gold: ['sso'],
};
module.exports = ({ dir, logger = noLog }) => {
const EEService = ({ dir, logger = noLog }) => {
if (_.has(internals, 'isEE')) return internals.isEE;
const warnAndReturn = (msg = 'Invalid license. Starting in CE.') => {
@ -49,6 +49,8 @@ module.exports = ({ dir, logger = noLog }) => {
return false;
}
// TODO: optimistically return true if license key is valid
try {
const plainLicense = Buffer.from(license, 'base64').toString();
const [signatureb64, contentb64] = plainLicense.split('\n');
@ -64,6 +66,8 @@ module.exports = ({ dir, logger = noLog }) => {
if (!isValid) return warnAndReturn();
internals.licenseInfo = JSON.parse(content);
internals.licenseInfo.features =
internals.licenseInfo.features || defaultFeatures[internals.licenseInfo.type];
const expirationTime = new Date(internals.licenseInfo.expireAt).getTime();
if (expirationTime < new Date().getTime()) {
@ -77,7 +81,14 @@ module.exports = ({ dir, logger = noLog }) => {
return true;
};
Object.defineProperty(module.exports, 'licenseInfo', {
EEService.checkLicense = async () => {
// TODO: online / DB check of the license info
// TODO: refresh info if the DB info is outdated
// TODO: register cron
// internals.licenseInfo = await db.getLicense();
};
Object.defineProperty(EEService, 'licenseInfo', {
get() {
mustHaveKey('licenseInfo');
return internals.licenseInfo;
@ -86,7 +97,7 @@ Object.defineProperty(module.exports, 'licenseInfo', {
enumerable: false,
});
Object.defineProperty(module.exports, 'isEE', {
Object.defineProperty(EEService, 'isEE', {
get() {
mustHaveKey('isEE');
return internals.isEE;
@ -95,20 +106,14 @@ Object.defineProperty(module.exports, 'isEE', {
enumerable: false,
});
Object.defineProperty(module.exports, 'features', {
Object.defineProperty(EEService, 'features', {
get() {
const licenseInfo = module.exports.licenseInfo;
const { type: licenseType } = module.exports.licenseInfo;
const features = licenseInfo.features || defaultFeatures[licenseType];
return {
isEnabled(feature) {
return features.includes(feature);
return internals.licenseInfo.features.includes(feature);
},
getEnabled() {
return features;
return internals.licenseInfo.features;
},
};
},
@ -123,3 +128,5 @@ const mustHaveKey = (key) => {
throw err;
}
};
module.exports = EEService;

View File

@ -365,6 +365,8 @@ class Strapi {
}
async register() {
await this.loadEE();
await Promise.all([
this.loadApp(),
this.loadSanitizers(),
@ -455,6 +457,8 @@ class Strapi {
value: strapi.contentTypes,
});
await ee.checkLicense();
await this.startWebhooks();
await this.server.initMiddlewares();
@ -480,7 +484,6 @@ class Strapi {
}
async load() {
await this.loadEE();
await this.register();
await this.bootstrap();

View File

@ -47,6 +47,7 @@ const createTelemetryInstance = (strapi) => {
}
},
bootstrap() {
// TODO: remove
if (strapi.EE === true && ee.isEE === true) {
const pingDisabled =
isTruthy(process.env.STRAPI_LICENSE_PING_DISABLED) && ee.licenseInfo.type === 'gold';