1
0
mirror of https://github.com/Unleash/unleash.git synced 2024-11-01 19:07:38 +01:00
unleash.unleash/src/lib/create-config.ts

500 lines
14 KiB
TypeScript
Raw Normal View History

import { parse } from 'pg-connection-string';
import merge from 'deepmerge';
import * as fs from 'fs';
import {
IUnleashOptions,
IUnleashConfig,
IDBOption,
ISessionOption,
IServerOption,
IVersionOption,
IAuthOption,
IAuthType,
IImportOption,
IEmailOption,
IListeningPipe,
IListeningHost,
IUIConfig,
ICspDomainConfig,
ICspDomainOptions,
IClientCachingOption,
} from './types/option';
2021-04-23 15:31:12 +02:00
import { getDefaultLogProvider, LogLevel, validateLogProvider } from './logger';
import { defaultCustomAuthDenyAll } from './default-custom-auth-deny-all';
import { formatBaseUri } from './util/format-base-uri';
import { minutesToMilliseconds, secondsToMilliseconds } from 'date-fns';
2021-12-09 21:02:58 +01:00
import EventEmitter from 'events';
import {
ApiTokenType,
mapLegacyToken,
validateApiToken,
} from './types/models/api-token';
import {
parseEnvVarBoolean,
parseEnvVarNumber,
parseEnvVarStrings,
} from './util/parseEnvVar';
import {
defaultExperimentalOptions,
IExperimentalOptions,
} from './types/experimental';
import {
DEFAULT_SEGMENT_VALUES_LIMIT,
DEFAULT_STRATEGY_SEGMENTS_LIMIT,
} from './util/segments';
import FlagResolver from './util/flag-resolver';
import { validateOrigins } from './util/validateOrigin';
const safeToUpper = (s: string) => (s ? s.toUpperCase() : s);
export function authTypeFromString(
s?: string,
defaultType: IAuthType = IAuthType.OPEN_SOURCE,
): IAuthType {
return IAuthType[safeToUpper(s)] || defaultType;
}
function mergeAll<T>(objects: Partial<T>[]): T {
return merge.all<T>(objects.filter((i) => i));
}
function loadExperimental(options: IUnleashOptions): IExperimentalOptions {
return {
...defaultExperimentalOptions,
...options.experimental,
flags: {
...defaultExperimentalOptions.flags,
...options.experimental?.flags,
},
};
}
const defaultClientCachingOptions: IClientCachingOption = {
enabled: true,
maxAge: 600,
};
function loadClientCachingOptions(
options: IUnleashOptions,
): IClientCachingOption {
let envs: Partial<IClientCachingOption> = {};
if (process.env.CLIENT_FEATURE_CACHING_MAXAGE) {
envs.maxAge = parseEnvVarNumber(
process.env.CLIENT_FEATURE_CACHING_MAXAGE,
600,
);
}
if (process.env.CLIENT_FEATURE_CACHING_ENABLED) {
envs.enabled = parseEnvVarBoolean(
process.env.CLIENT_FEATURE_CACHING_ENABLED,
true,
);
}
return mergeAll([
defaultClientCachingOptions,
options.clientFeatureCaching,
envs,
]);
}
function loadUI(options: IUnleashOptions): IUIConfig {
const uiO = options.ui || {};
const ui: IUIConfig = {
environment: 'Open Source',
};
ui.flags = {
E: true,
ENABLE_DARK_MODE_SUPPORT: false,
};
2022-11-04 14:43:12 +01:00
return mergeAll([ui, uiO]);
}
const dateHandlingCallback = (connection, callback) => {
connection.query("set datestyle to 'ISO, DMY';", (err: any) => {
callback(err, connection);
});
};
const defaultDbOptions: IDBOption = {
user: process.env.DATABASE_USERNAME,
password: process.env.DATABASE_PASSWORD,
host: process.env.DATABASE_HOST,
port: parseEnvVarNumber(process.env.DATABASE_PORT, 5432),
database: process.env.DATABASE_NAME || 'unleash',
ssl:
process.env.DATABASE_SSL != null
? JSON.parse(process.env.DATABASE_SSL)
: { rejectUnauthorized: false },
driver: 'postgres',
version: process.env.DATABASE_VERSION,
acquireConnectionTimeout: secondsToMilliseconds(30),
pool: {
min: parseEnvVarNumber(process.env.DATABASE_POOL_MIN, 0),
max: parseEnvVarNumber(process.env.DATABASE_POOL_MAX, 4),
idleTimeoutMillis: parseEnvVarNumber(
process.env.DATABASE_POOL_IDLE_TIMEOUT_MS,
secondsToMilliseconds(30),
),
...(parseEnvVarBoolean(process.env.ALLOW_NON_STANDARD_DB_DATES, false)
? { afterCreate: dateHandlingCallback }
: {}),
propagateCreateError: false,
},
schema: process.env.DATABASE_SCHEMA || 'public',
disableMigration: false,
applicationName: process.env.DATABASE_APPLICATION_NAME || 'unleash',
};
const defaultSessionOption: ISessionOption = {
ttlHours: parseEnvVarNumber(process.env.SESSION_TTL_HOURS, 48),
clearSiteDataOnLogout: parseEnvVarBoolean(
process.env.SESSION_CLEAR_SITE_DATA_ON_LOGOUT,
true,
),
cookieName: 'unleash-session',
db: true,
};
const defaultServerOption: IServerOption = {
pipe: undefined,
host: process.env.HTTP_HOST,
port: parseEnvVarNumber(process.env.HTTP_PORT || process.env.PORT, 4242),
baseUriPath: formatBaseUri(process.env.BASE_URI_PATH),
cdnPrefix: process.env.CDN_PREFIX,
unleashUrl: process.env.UNLEASH_URL || 'http://localhost:4242',
serverMetrics: true,
enableHeapSnapshotEnpoint: parseEnvVarBoolean(
process.env.ENABLE_HEAP_SNAPSHOT_ENPOINT,
false,
),
keepAliveTimeout: minutesToMilliseconds(1),
headersTimeout: secondsToMilliseconds(61),
enableRequestLogger: false,
gracefulShutdownEnable: parseEnvVarBoolean(
process.env.GRACEFUL_SHUTDOWN_ENABLE,
true,
),
gracefulShutdownTimeout: parseEnvVarNumber(
process.env.GRACEFUL_SHUTDOWN_TIMEOUT,
secondsToMilliseconds(1),
),
secret: process.env.UNLEASH_SECRET || 'super-secret',
};
const defaultVersionOption: IVersionOption = {
url: process.env.UNLEASH_VERSION_URL || 'https://version.unleash.run',
enable: parseEnvVarBoolean(process.env.CHECK_VERSION, true),
};
const defaultAuthentication: IAuthOption = {
enableApiToken: parseEnvVarBoolean(process.env.AUTH_ENABLE_API_TOKEN, true),
type: authTypeFromString(process.env.AUTH_TYPE),
customAuthHandler: defaultCustomAuthDenyAll,
2021-04-22 22:54:08 +02:00
createAdminUser: true,
initApiTokens: [],
};
const defaultImport: IImportOption = {
file: process.env.IMPORT_FILE,
dropBeforeImport: parseEnvVarBoolean(
process.env.IMPORT_DROP_BEFORE_IMPORT,
false,
),
keepExisting: parseEnvVarBoolean(process.env.IMPORT_KEEP_EXISTING, false),
};
const defaultEmail: IEmailOption = {
host: process.env.EMAIL_HOST,
secure: parseEnvVarBoolean(process.env.EMAIL_SECURE, false),
port: parseEnvVarNumber(process.env.EMAIL_PORT, 587),
sender: process.env.EMAIL_SENDER || 'noreply@unleash-hosted.com',
smtpuser: process.env.EMAIL_USER,
smtppass: process.env.EMAIL_PASSWORD,
};
const dbPort = (dbConfig: Partial<IDBOption>): Partial<IDBOption> => {
if (typeof dbConfig.port === 'string') {
// eslint-disable-next-line no-param-reassign
dbConfig.port = Number.parseInt(dbConfig.port, 10);
}
return dbConfig;
};
const removeUndefinedKeys = (o: object): object =>
Object.keys(o).reduce((a, key) => {
if (o[key] !== undefined) {
// eslint-disable-next-line no-param-reassign
a[key] = o[key];
return a;
}
return a;
}, {});
const formatServerOptions = (
serverOptions?: Partial<IServerOption>,
): Partial<IServerOption> | undefined => {
if (!serverOptions) {
return {
baseUriPath: formatBaseUri(process.env.BASE_URI_PATH),
};
}
/* eslint-disable-next-line */
return {
...serverOptions,
baseUriPath: formatBaseUri(
process.env.BASE_URI_PATH || serverOptions.baseUriPath,
),
};
};
const loadTokensFromString = (tokenString: String, tokenType: ApiTokenType) => {
if (!tokenString) {
return [];
}
const initApiTokens = tokenString.split(/,\s?/);
const tokens = initApiTokens.map((secret) => {
const [project = '*', rest] = secret.split(':');
const [environment = '*'] = rest.split('.');
const token = {
createdAt: undefined,
project,
environment,
secret,
type: tokenType,
username: 'admin',
};
validateApiToken(mapLegacyToken(token));
return token;
});
return tokens;
};
const loadInitApiTokens = () => {
return [
...loadTokensFromString(
process.env.INIT_ADMIN_API_TOKENS,
ApiTokenType.ADMIN,
),
...loadTokensFromString(
process.env.INIT_CLIENT_API_TOKENS,
ApiTokenType.CLIENT,
),
];
};
const loadEnvironmentEnableOverrides = () => {
const environmentsString = process.env.ENABLED_ENVIRONMENTS;
if (environmentsString) {
return environmentsString.split(',');
}
return [];
};
const parseCspConfig = (
cspConfig: ICspDomainOptions,
): ICspDomainConfig | undefined => {
if (!cspConfig) {
return undefined;
}
return {
defaultSrc: cspConfig.defaultSrc || [],
fontSrc: cspConfig.fontSrc || [],
scriptSrc: cspConfig.scriptSrc || [],
imgSrc: cspConfig.imgSrc || [],
styleSrc: cspConfig.styleSrc || [],
};
};
const parseCspEnvironmentVariables = (): ICspDomainConfig => {
const defaultSrc = process.env.CSP_ALLOWED_DEFAULT?.split(',') || [];
const fontSrc = process.env.CSP_ALLOWED_FONT?.split(',') || [];
const styleSrc = process.env.CSP_ALLOWED_STYLE?.split(',') || [];
const scriptSrc = process.env.CSP_ALLOWED_SCRIPT?.split(',') || [];
const imgSrc = process.env.CSP_ALLOWED_IMG?.split(',') || [];
return {
defaultSrc,
fontSrc,
styleSrc,
scriptSrc,
imgSrc,
};
};
const parseFrontendApiOrigins = (options: IUnleashOptions): string[] => {
const frontendApiOrigins = parseEnvVarStrings(
process.env.UNLEASH_FRONTEND_API_ORIGINS,
options.frontendApiOrigins || ['*'],
);
const error = validateOrigins(frontendApiOrigins);
if (error) {
throw new Error(error);
}
return frontendApiOrigins;
};
export function createConfig(options: IUnleashOptions): IUnleashConfig {
let extraDbOptions = {};
if (options.databaseUrl) {
extraDbOptions = parse(options.databaseUrl);
} else if (process.env.DATABASE_URL) {
extraDbOptions = parse(process.env.DATABASE_URL);
}
let fileDbOptions = {};
if (options.databaseUrlFile && fs.existsSync(options.databaseUrlFile)) {
fileDbOptions = parse(
fs.readFileSync(options.databaseUrlFile, 'utf-8'),
);
} else if (
process.env.DATABASE_URL_FILE &&
fs.existsSync(process.env.DATABASE_URL_FILE)
) {
fileDbOptions = parse(
fs.readFileSync(process.env.DATABASE_URL_FILE, 'utf-8'),
);
}
const db: IDBOption = mergeAll<IDBOption>([
defaultDbOptions,
dbPort(extraDbOptions),
dbPort(fileDbOptions),
options.db,
]);
const session: ISessionOption = mergeAll([
defaultSessionOption,
options.session,
]);
2021-04-23 15:31:12 +02:00
const logLevel =
options.logLevel || LogLevel[process.env.LOG_LEVEL] || LogLevel.error;
const getLogger = options.getLogger || getDefaultLogProvider(logLevel);
validateLogProvider(getLogger);
const server: IServerOption = mergeAll([
defaultServerOption,
formatServerOptions(options.server),
]);
const versionCheck: IVersionOption = mergeAll([
defaultVersionOption,
options.versionCheck,
]);
const initApiTokens = loadInitApiTokens();
const authentication: IAuthOption = mergeAll([
defaultAuthentication,
options.authentication
? removeUndefinedKeys(options.authentication)
: options.authentication,
{ initApiTokens: initApiTokens },
]);
const environmentEnableOverrides = loadEnvironmentEnableOverrides();
const importSetting: IImportOption = mergeAll([
defaultImport,
options.import,
]);
const experimental = loadExperimental(options);
const flagResolver = new FlagResolver(experimental);
const ui = loadUI(options);
const email: IEmailOption = mergeAll([defaultEmail, options.email]);
let listen: IListeningPipe | IListeningHost;
if (server.pipe) {
listen = { path: server.pipe };
} else {
listen = { host: server.host || undefined, port: server.port };
}
const frontendApi = options.frontendApi || {
refreshIntervalInMs: parseEnvVarNumber(
process.env.FRONTEND_API_REFRESH_INTERVAL_MS,
10000,
),
};
const secureHeaders =
options.secureHeaders ||
parseEnvVarBoolean(process.env.SECURE_HEADERS, false);
const enableOAS =
options.enableOAS || parseEnvVarBoolean(process.env.ENABLE_OAS, false);
feat: custom project roles (#1220) * wip: environment for permissions * fix: add migration for roles * fix: connect environment with access service * feat: add tests * chore: Implement scaffolding for new rbac * fix: add fake store * feat: Add api endpoints for roles and permissions list * feat: Add ability to provide permissions when creating a role and rename environmentName to name in the list permissions datastructure * fix: Make project roles resolve correctly against new environments permissions structure * fix: Patch migration to also populate permission names * fix: Make permissions actually work with new environments * fix: Add back to get permissions working for editor role * fix: Removed ability to set role type through api during creation - it's now always custom * feat: Return permissions on get role endpoint * feat: Add in support for updating roles * fix: Get a bunch of tests working and delete a few that make no sense anymore * chore: A few small cleanups - remove logging and restore default on dev server config * chore: Refactor role/access stores into more logical domains * feat: Add in validation for roles * feat: Patch db migration to handle old stucture * fix: migration for project roles * fix: patch a few broken tests * fix: add permissions to editor * fix: update test name * fix: update user permission mapping * fix: create new user * fix: update root role test * fix: update tests * feat: Validation now works when updating a role * fix: Add in very barebones down migration for rbac so that tests work * fix: Improve responses from role resolution - getting a non existant role will throw a NotFound error * fix: remove unused permissions * fix: add test for connecting roles and deleting project * fix: add test for adding a project member with a custom role * fix: add test for changing user role * fix: add guard for deleting role if the role is in use * fix: alter migration * chore: Minor code cleanups * chore: Small code cleanups * chore: More minor cleanups of code * chore: Trim some dead code to make the linter happy * feat: Schema validation for roles * fix: setup permission for variant * fix: remove unused import * feat: Add cascading delete for role_permissions when deleting a role * feat: add configuration option for disabling legacy api * chore: update frontend to beta version * 4.6.0-beta.0 * fix: export default project constant * fix: update snapshot * fix: module pattern ../../lib * fix: move DEFAULT_PROJECT to types * fix: remove debug logging * fix: remove debug log state * fix: Change permission descriptions * fix: roles should have unique name * fix: root roles should be connected to the default project * fix: typo in role-schema.ts * fix: Role permission empty string for non environment type * feat: new permission for moving project * fix: add event for changeProject * fix: Removing a user from a project will now check to see if that project has an owner, rather than checking if any project has an owner * fix: add tests for move project * fix: Add in missing create/delete tag permissions * fix: Removed duplicate impl caused by multiple good samaritans putting it back in! * fix: Trim out add tag permissions, for now at least * chore: Trim out new add and delete tag permissions - we're going with update feature instead * chore: update frontend * 4.6.0-beta.1 * feat: Prevent editing of built in roles * fix: Patch an issue where permissions for variants/environments didn't match the front end * fix: lint Co-authored-by: Ivar Conradi Østhus <ivarconr@gmail.com> Co-authored-by: Fredrik Oseberg <fredrik.no@gmail.com>
2022-01-13 11:14:17 +01:00
const disableLegacyFeaturesApi =
options.disableLegacyFeaturesApi ||
parseEnvVarBoolean(process.env.DISABLE_LEGACY_FEATURES_API, false);
feat: custom project roles (#1220) * wip: environment for permissions * fix: add migration for roles * fix: connect environment with access service * feat: add tests * chore: Implement scaffolding for new rbac * fix: add fake store * feat: Add api endpoints for roles and permissions list * feat: Add ability to provide permissions when creating a role and rename environmentName to name in the list permissions datastructure * fix: Make project roles resolve correctly against new environments permissions structure * fix: Patch migration to also populate permission names * fix: Make permissions actually work with new environments * fix: Add back to get permissions working for editor role * fix: Removed ability to set role type through api during creation - it's now always custom * feat: Return permissions on get role endpoint * feat: Add in support for updating roles * fix: Get a bunch of tests working and delete a few that make no sense anymore * chore: A few small cleanups - remove logging and restore default on dev server config * chore: Refactor role/access stores into more logical domains * feat: Add in validation for roles * feat: Patch db migration to handle old stucture * fix: migration for project roles * fix: patch a few broken tests * fix: add permissions to editor * fix: update test name * fix: update user permission mapping * fix: create new user * fix: update root role test * fix: update tests * feat: Validation now works when updating a role * fix: Add in very barebones down migration for rbac so that tests work * fix: Improve responses from role resolution - getting a non existant role will throw a NotFound error * fix: remove unused permissions * fix: add test for connecting roles and deleting project * fix: add test for adding a project member with a custom role * fix: add test for changing user role * fix: add guard for deleting role if the role is in use * fix: alter migration * chore: Minor code cleanups * chore: Small code cleanups * chore: More minor cleanups of code * chore: Trim some dead code to make the linter happy * feat: Schema validation for roles * fix: setup permission for variant * fix: remove unused import * feat: Add cascading delete for role_permissions when deleting a role * feat: add configuration option for disabling legacy api * chore: update frontend to beta version * 4.6.0-beta.0 * fix: export default project constant * fix: update snapshot * fix: module pattern ../../lib * fix: move DEFAULT_PROJECT to types * fix: remove debug logging * fix: remove debug log state * fix: Change permission descriptions * fix: roles should have unique name * fix: root roles should be connected to the default project * fix: typo in role-schema.ts * fix: Role permission empty string for non environment type * feat: new permission for moving project * fix: add event for changeProject * fix: Removing a user from a project will now check to see if that project has an owner, rather than checking if any project has an owner * fix: add tests for move project * fix: Add in missing create/delete tag permissions * fix: Removed duplicate impl caused by multiple good samaritans putting it back in! * fix: Trim out add tag permissions, for now at least * chore: Trim out new add and delete tag permissions - we're going with update feature instead * chore: update frontend * 4.6.0-beta.1 * feat: Prevent editing of built in roles * fix: Patch an issue where permissions for variants/environments didn't match the front end * fix: lint Co-authored-by: Ivar Conradi Østhus <ivarconr@gmail.com> Co-authored-by: Fredrik Oseberg <fredrik.no@gmail.com>
2022-01-13 11:14:17 +01:00
const additionalCspAllowedDomains: ICspDomainConfig =
parseCspConfig(options.additionalCspAllowedDomains) ||
parseCspEnvironmentVariables();
const inlineSegmentConstraints =
typeof options.inlineSegmentConstraints === 'boolean'
? options.inlineSegmentConstraints
: true;
const segmentValuesLimit = parseEnvVarNumber(
process.env.UNLEASH_SEGMENT_VALUES_LIMIT,
DEFAULT_SEGMENT_VALUES_LIMIT,
);
const strategySegmentsLimit = parseEnvVarNumber(
process.env.UNLEASH_STRATEGY_SEGMENTS_LIMIT,
DEFAULT_STRATEGY_SEGMENTS_LIMIT,
);
const accessControlMaxAge = options.accessControlMaxAge
? options.accessControlMaxAge
: parseEnvVarNumber(process.env.ACCESS_CONTROL_MAX_AGE, 86400);
const clientFeatureCaching = loadClientCachingOptions(options);
return {
db,
session,
getLogger,
server,
listen,
versionCheck,
authentication,
ui,
import: importSetting,
experimental,
flagResolver,
frontendApi,
email,
secureHeaders,
enableOAS,
feat: custom project roles (#1220) * wip: environment for permissions * fix: add migration for roles * fix: connect environment with access service * feat: add tests * chore: Implement scaffolding for new rbac * fix: add fake store * feat: Add api endpoints for roles and permissions list * feat: Add ability to provide permissions when creating a role and rename environmentName to name in the list permissions datastructure * fix: Make project roles resolve correctly against new environments permissions structure * fix: Patch migration to also populate permission names * fix: Make permissions actually work with new environments * fix: Add back to get permissions working for editor role * fix: Removed ability to set role type through api during creation - it's now always custom * feat: Return permissions on get role endpoint * feat: Add in support for updating roles * fix: Get a bunch of tests working and delete a few that make no sense anymore * chore: A few small cleanups - remove logging and restore default on dev server config * chore: Refactor role/access stores into more logical domains * feat: Add in validation for roles * feat: Patch db migration to handle old stucture * fix: migration for project roles * fix: patch a few broken tests * fix: add permissions to editor * fix: update test name * fix: update user permission mapping * fix: create new user * fix: update root role test * fix: update tests * feat: Validation now works when updating a role * fix: Add in very barebones down migration for rbac so that tests work * fix: Improve responses from role resolution - getting a non existant role will throw a NotFound error * fix: remove unused permissions * fix: add test for connecting roles and deleting project * fix: add test for adding a project member with a custom role * fix: add test for changing user role * fix: add guard for deleting role if the role is in use * fix: alter migration * chore: Minor code cleanups * chore: Small code cleanups * chore: More minor cleanups of code * chore: Trim some dead code to make the linter happy * feat: Schema validation for roles * fix: setup permission for variant * fix: remove unused import * feat: Add cascading delete for role_permissions when deleting a role * feat: add configuration option for disabling legacy api * chore: update frontend to beta version * 4.6.0-beta.0 * fix: export default project constant * fix: update snapshot * fix: module pattern ../../lib * fix: move DEFAULT_PROJECT to types * fix: remove debug logging * fix: remove debug log state * fix: Change permission descriptions * fix: roles should have unique name * fix: root roles should be connected to the default project * fix: typo in role-schema.ts * fix: Role permission empty string for non environment type * feat: new permission for moving project * fix: add event for changeProject * fix: Removing a user from a project will now check to see if that project has an owner, rather than checking if any project has an owner * fix: add tests for move project * fix: Add in missing create/delete tag permissions * fix: Removed duplicate impl caused by multiple good samaritans putting it back in! * fix: Trim out add tag permissions, for now at least * chore: Trim out new add and delete tag permissions - we're going with update feature instead * chore: update frontend * 4.6.0-beta.1 * feat: Prevent editing of built in roles * fix: Patch an issue where permissions for variants/environments didn't match the front end * fix: lint Co-authored-by: Ivar Conradi Østhus <ivarconr@gmail.com> Co-authored-by: Fredrik Oseberg <fredrik.no@gmail.com>
2022-01-13 11:14:17 +01:00
disableLegacyFeaturesApi,
preHook: options.preHook,
preRouterHook: options.preRouterHook,
eventHook: options.eventHook,
enterpriseVersion: options.enterpriseVersion,
2021-12-09 21:02:58 +01:00
eventBus: new EventEmitter(),
environmentEnableOverrides,
additionalCspAllowedDomains,
frontendApiOrigins: parseFrontendApiOrigins(options),
inlineSegmentConstraints,
segmentValuesLimit,
strategySegmentsLimit,
clientFeatureCaching,
accessControlMaxAge,
};
}
module.exports = {
createConfig,
authTypeFromString,
};