diff --git a/src/config/app.config.ts b/src/config/app.config.ts index 980eabfe3..69573ecc2 100644 --- a/src/config/app.config.ts +++ b/src/config/app.config.ts @@ -7,6 +7,7 @@ import { registerAs } from '@nestjs/config'; import * as Joi from 'joi'; import { Loglevel } from './loglevel.enum'; +import { buildErrorMessage } from './utils'; export interface AppConfig { domain: string; @@ -15,12 +16,13 @@ export interface AppConfig { } const schema = Joi.object({ - domain: Joi.string(), - port: Joi.number().default(3000).optional(), + domain: Joi.string().label('HD_DOMAIN'), + port: Joi.number().default(3000).optional().label('PORT'), loglevel: Joi.string() .valid(...Object.values(Loglevel)) .default(Loglevel.WARN) - .optional(), + .optional() + .label('HD_LOGLEVEL'), }); export default registerAs('appConfig', async () => { @@ -36,7 +38,10 @@ export default registerAs('appConfig', async () => { }, ); if (appConfig.error) { - throw new Error(appConfig.error.toString()); + const errorMessages = await appConfig.error.details.map( + (detail) => detail.message, + ); + throw new Error(buildErrorMessage(errorMessages)); } return appConfig.value; }); diff --git a/src/config/auth.config.ts b/src/config/auth.config.ts index b722b5b50..ec36789cc 100644 --- a/src/config/auth.config.ts +++ b/src/config/auth.config.ts @@ -6,7 +6,11 @@ import * as Joi from 'joi'; import { GitlabScope, GitlabVersion } from './gitlab.enum'; -import { toArrayConfig } from './utils'; +import { + buildErrorMessage, + replaceAuthErrorsWithEnvironmentVariables, + toArrayConfig, +} from './utils'; import { registerAs } from '@nestjs/config'; export interface AuthConfig { @@ -102,38 +106,50 @@ export interface AuthConfig { const authSchema = Joi.object({ email: { - enableLogin: Joi.boolean().default(false).optional(), - enableRegister: Joi.boolean().default(false).optional(), + enableLogin: Joi.boolean() + .default(false) + .optional() + .label('HD_AUTH_EMAIL_ENABLE_LOGIN'), + enableRegister: Joi.boolean() + .default(false) + .optional() + .label('HD_AUTH_EMAIL_ENABLE_REGISTER'), }, facebook: { - clientID: Joi.string().optional(), - clientSecret: Joi.string().optional(), + clientID: Joi.string().optional().label('HD_AUTH_FACEBOOK_CLIENT_ID'), + clientSecret: Joi.string() + .optional() + .label('HD_AUTH_FACEBOOK_CLIENT_SECRET'), }, twitter: { - consumerKey: Joi.string().optional(), - consumerSecret: Joi.string().optional(), + consumerKey: Joi.string().optional().label('HD_AUTH_TWITTER_CONSUMER_KEY'), + consumerSecret: Joi.string() + .optional() + .label('HD_AUTH_TWITTER_CONSUMER_SECRET'), }, github: { - clientID: Joi.string().optional(), - clientSecret: Joi.string().optional(), + clientID: Joi.string().optional().label('HD_AUTH_GITHUB_CLIENT_ID'), + clientSecret: Joi.string().optional().label('HD_AUTH_GITHUB_CLIENT_SECRET'), }, dropbox: { - clientID: Joi.string().optional(), - clientSecret: Joi.string().optional(), - appKey: Joi.string().optional(), + clientID: Joi.string().optional().label('HD_AUTH_DROPBOX_CLIENT_ID'), + clientSecret: Joi.string() + .optional() + .label('HD_AUTH_DROPBOX_CLIENT_SECRET'), + appKey: Joi.string().optional().label('HD_AUTH_DROPBOX_APP_KEY'), }, google: { - clientID: Joi.string().optional(), - clientSecret: Joi.string().optional(), - apiKey: Joi.string().optional(), + clientID: Joi.string().optional().label('HD_AUTH_GOOGLE_CLIENT_ID'), + clientSecret: Joi.string().optional().label('HD_AUTH_GOOGLE_CLIENT_SECRET'), + apiKey: Joi.string().optional().label('HD_AUTH_GOOGLE_APP_KEY'), }, gitlab: Joi.array() .items( Joi.object({ providerName: Joi.string().default('Gitlab').optional(), - baseURL: Joi.string().optional(), - clientID: Joi.string().optional(), - clientSecret: Joi.string().optional(), + baseURL: Joi.string(), + clientID: Joi.string(), + clientSecret: Joi.string(), scope: Joi.string() .valid(...Object.values(GitlabScope)) .default(GitlabScope.READ_USER) @@ -142,7 +158,7 @@ const authSchema = Joi.object({ .valid(...Object.values(GitlabVersion)) .default(GitlabVersion.V4) .optional(), - }), + }).optional(), ) .optional(), // ToDo: should searchfilter have a default? @@ -150,155 +166,168 @@ const authSchema = Joi.object({ .items( Joi.object({ providerName: Joi.string().default('LDAP').optional(), - url: Joi.string().optional(), + url: Joi.string(), bindDn: Joi.string().optional(), bindCredentials: Joi.string().optional(), - searchBase: Joi.string().optional(), + searchBase: Joi.string(), searchFilter: Joi.string().default('(uid={{username}})').optional(), - searchAttributes: Joi.array().items(Joi.string()), - usernameField: Joi.string().default('userid').optional(), - useridField: Joi.string().optional(), - tlsCa: Joi.array().items(Joi.string()), - }), + searchAttributes: Joi.array() + .items(Joi.string()) + .default(['displayName', 'mail']) + .optional(), + usernameField: Joi.string().optional(), + useridField: Joi.string(), + tlsCa: Joi.array().items(Joi.string()).optional(), + }).optional(), ) .optional(), saml: Joi.array() .items( Joi.object({ providerName: Joi.string().default('SAML').optional(), - idpSsoUrl: Joi.string().optional(), - idpCert: Joi.string().optional(), + idpSsoUrl: Joi.string(), + idpCert: Joi.string(), clientCert: Joi.string().optional(), // ToDo: (default: config.serverURL) will be build on-the-fly in the config/index.js from domain, urlAddPort and urlPath. - issuer: Joi.string().optional(), //.default().optional(), + issuer: Joi.string().optional(), identifierFormat: Joi.string() .default('urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress') .optional(), disableRequestedAuthnContext: Joi.boolean().default(false).optional(), groupAttribute: Joi.string().optional(), - requiredGroups: Joi.array().items(Joi.string()), - externalGroups: Joi.array().items(Joi.string()), + requiredGroups: Joi.array().items(Joi.string()).optional(), + externalGroups: Joi.array().items(Joi.string()).optional(), attribute: { id: Joi.string().default('NameId').optional(), username: Joi.string().default('NameId').optional(), email: Joi.string().default('NameId').optional(), }, - }), + }).optional(), ) .optional(), oauth2: Joi.array() .items( Joi.object({ providerName: Joi.string().default('OAuth2').optional(), - baseURL: Joi.string().optional(), - userProfileURL: Joi.string().optional(), + baseURL: Joi.string(), + userProfileURL: Joi.string(), userProfileIdAttr: Joi.string().optional(), - userProfileUsernameAttr: Joi.string().optional(), - userProfileDisplayNameAttr: Joi.string().optional(), - userProfileEmailAttr: Joi.string().optional(), - tokenURL: Joi.string().optional(), - authorizationURL: Joi.string().optional(), - clientID: Joi.string().optional(), - clientSecret: Joi.string().optional(), + userProfileUsernameAttr: Joi.string(), + userProfileDisplayNameAttr: Joi.string(), + userProfileEmailAttr: Joi.string(), + tokenURL: Joi.string(), + authorizationURL: Joi.string(), + clientID: Joi.string(), + clientSecret: Joi.string(), scope: Joi.string().optional(), rolesClaim: Joi.string().optional(), accessRole: Joi.string().optional(), - }), + }).optional(), ) .optional(), }); -// ToDo: Validate these with Joi to prevent duplicate entries? - -const gitlabNames = toArrayConfig(process.env.HD_AUTH_GITLABS, ','); -const ldapNames = toArrayConfig(process.env.HD_AUTH_LDAPS, ','); -const samlNames = toArrayConfig(process.env.HD_AUTH_SAMLS, ','); -const oauth2Names = toArrayConfig(process.env.HD_AUTH_OAUTH2S, ','); - -const gitlabs = gitlabNames.map((gitlabName) => { - return { - providerName: process.env[`HD_AUTH_GITLAB_${gitlabName}_PROVIDER_NAME`], - baseURL: process.env[`HD_AUTH_GITLAB_${gitlabName}_BASE_URL`], - clientID: process.env[`HD_AUTH_GITLAB_${gitlabName}_CLIENT_ID`], - clientSecret: process.env[`HD_AUTH_GITLAB_${gitlabName}_CLIENT_SECRET`], - scope: process.env[`HD_AUTH_GITLAB_${gitlabName}_GITLAB_SCOPE`], - version: process.env[`HD_AUTH_GITLAB_${gitlabName}_GITLAB_VERSION`], - }; -}); - -const ldaps = ldapNames.map((ldapName) => { - return { - providerName: process.env[`HD_AUTH_LDAP_${ldapName}_PROVIDER_NAME`], - url: process.env[`HD_AUTH_LDAP_${ldapName}_URL`], - bindDn: process.env[`HD_AUTH_LDAP_${ldapName}_BIND_DN`], - bindCredentials: process.env[`HD_AUTH_LDAP_${ldapName}_BIND_CREDENTIALS`], - searchBase: process.env[`HD_AUTH_LDAP_${ldapName}_SEARCH_BASE`], - searchFilter: process.env[`HD_AUTH_LDAP_${ldapName}_SEARCH_FILTER`], - searchAttributes: toArrayConfig( - process.env[`HD_AUTH_LDAP_${ldapName}_SEARCH_ATTRIBUTES`], - ',', - ), - usernameField: process.env[`HD_AUTH_LDAP_${ldapName}_USERNAME_FIELD`], - useridField: process.env[`HD_AUTH_LDAP_${ldapName}_USERID_FIELD`], - tlsCa: toArrayConfig(process.env[`HD_AUTH_LDAP_${ldapName}_TLS_CA`], ','), - }; -}); - -const samls = samlNames.map((samlName) => { - return { - providerName: process.env[`HD_AUTH_SAML_${samlName}_PROVIDER_NAME`], - idpSsoUrl: process.env[`HD_AUTH_SAML_${samlName}_IDPSSOURL`], - idpCert: process.env[`HD_AUTH_SAML_${samlName}_IDPCERT`], - clientCert: process.env[`HD_AUTH_SAML_${samlName}_CLIENTCERT`], - issuer: process.env[`HD_AUTH_SAML_${samlName}_ISSUER`], - identifierFormat: process.env[`HD_AUTH_SAML_${samlName}_IDENTIFIERFORMAT`], - disableRequestedAuthnContext: - process.env[`HD_AUTH_SAML_${samlName}_DISABLEREQUESTEDAUTHNCONTEXT`], - groupAttribute: process.env[`HD_AUTH_SAML_${samlName}_GROUPATTRIBUTE`], - requiredGroups: toArrayConfig( - process.env[`HD_AUTH_SAML_${samlName}_REQUIREDGROUPS`], - '|', - ), - externalGroups: toArrayConfig( - process.env[`HD_AUTH_SAML_${samlName}_EXTERNALGROUPS`], - '|', - ), - attribute: { - id: process.env[`HD_AUTH_SAML_${samlName}_ATTRIBUTE_ID`], - username: process.env[`HD_AUTH_SAML_${samlName}_ATTRIBUTE_USERNAME`], - email: process.env[`HD_AUTH_SAML_${samlName}_ATTRIBUTE_USERNAME`], - }, - }; -}); - -const oauth2s = oauth2Names.map((oauth2Name) => { - return { - providerName: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_PROVIDER_NAME`], - baseURL: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_BASEURL`], - userProfileURL: - process.env[`HD_AUTH_OAUTH2_${oauth2Name}_USER_PROFILE_URL`], - userProfileIdAttr: - process.env[`HD_AUTH_OAUTH2_${oauth2Name}_USER_PROFILE_ID_ATTR`], - userProfileUsernameAttr: - process.env[`HD_AUTH_OAUTH2_${oauth2Name}_USER_PROFILE_USERNAME_ATTR`], - userProfileDisplayNameAttr: - process.env[ - `HD_AUTH_OAUTH2_${oauth2Name}_USER_PROFILE_DISPLAY_NAME_ATTR` - ], - userProfileEmailAttr: - process.env[`HD_AUTH_OAUTH2_${oauth2Name}_USER_PROFILE_EMAIL_ATTR`], - tokenURL: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_TOKEN_URL`], - authorizationURL: - process.env[`HD_AUTH_OAUTH2_${oauth2Name}_AUTHORIZATION_URL`], - clientID: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_CLIENT_ID`], - clientSecret: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_CLIENT_SECRET`], - scope: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_SCOPE`], - rolesClaim: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_ROLES_CLAIM`], - accessRole: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_ACCESS_ROLE`], - }; -}); - export default registerAs('authConfig', async () => { + // ToDo: Validate these with Joi to prevent duplicate entries? + const gitlabNames = toArrayConfig( + process.env.HD_AUTH_GITLABS, + ',', + ).map((name) => name.toUpperCase()); + const ldapNames = toArrayConfig(process.env.HD_AUTH_LDAPS, ',').map((name) => + name.toUpperCase(), + ); + const samlNames = toArrayConfig(process.env.HD_AUTH_SAMLS, ',').map((name) => + name.toUpperCase(), + ); + const oauth2Names = toArrayConfig( + process.env.HD_AUTH_OAUTH2S, + ',', + ).map((name) => name.toUpperCase()); + + const gitlabs = gitlabNames.map((gitlabName) => { + return { + providerName: process.env[`HD_AUTH_GITLAB_${gitlabName}_PROVIDER_NAME`], + baseURL: process.env[`HD_AUTH_GITLAB_${gitlabName}_BASE_URL`], + clientID: process.env[`HD_AUTH_GITLAB_${gitlabName}_CLIENT_ID`], + clientSecret: process.env[`HD_AUTH_GITLAB_${gitlabName}_CLIENT_SECRET`], + scope: process.env[`HD_AUTH_GITLAB_${gitlabName}_SCOPE`], + version: process.env[`HD_AUTH_GITLAB_${gitlabName}_GITLAB_VERSION`], + }; + }); + + const ldaps = ldapNames.map((ldapName) => { + return { + providerName: process.env[`HD_AUTH_LDAP_${ldapName}_PROVIDER_NAME`], + url: process.env[`HD_AUTH_LDAP_${ldapName}_URL`], + bindDn: process.env[`HD_AUTH_LDAP_${ldapName}_BIND_DN`], + bindCredentials: process.env[`HD_AUTH_LDAP_${ldapName}_BIND_CREDENTIALS`], + searchBase: process.env[`HD_AUTH_LDAP_${ldapName}_SEARCH_BASE`], + searchFilter: process.env[`HD_AUTH_LDAP_${ldapName}_SEARCH_FILTER`], + searchAttributes: toArrayConfig( + process.env[`HD_AUTH_LDAP_${ldapName}_SEARCH_ATTRIBUTES`], + ',', + ), + usernameField: process.env[`HD_AUTH_LDAP_${ldapName}_USERNAME_FIELD`], + useridField: process.env[`HD_AUTH_LDAP_${ldapName}_USERID_FIELD`], + tlsCa: toArrayConfig(process.env[`HD_AUTH_LDAP_${ldapName}_TLS_CA`], ','), + }; + }); + + const samls = samlNames.map((samlName) => { + return { + providerName: process.env[`HD_AUTH_SAML_${samlName}_PROVIDER_NAME`], + idpSsoUrl: process.env[`HD_AUTH_SAML_${samlName}_IDPSSOURL`], + idpCert: process.env[`HD_AUTH_SAML_${samlName}_IDPCERT`], + clientCert: process.env[`HD_AUTH_SAML_${samlName}_CLIENTCERT`], + issuer: process.env[`HD_AUTH_SAML_${samlName}_ISSUER`], + identifierFormat: + process.env[`HD_AUTH_SAML_${samlName}_IDENTIFIERFORMAT`], + disableRequestedAuthnContext: + process.env[`HD_AUTH_SAML_${samlName}_DISABLEREQUESTEDAUTHNCONTEXT`], + groupAttribute: process.env[`HD_AUTH_SAML_${samlName}_GROUPATTRIBUTE`], + requiredGroups: toArrayConfig( + process.env[`HD_AUTH_SAML_${samlName}_REQUIREDGROUPS`], + '|', + ), + externalGroups: toArrayConfig( + process.env[`HD_AUTH_SAML_${samlName}_EXTERNALGROUPS`], + '|', + ), + attribute: { + id: process.env[`HD_AUTH_SAML_${samlName}_ATTRIBUTE_ID`], + username: process.env[`HD_AUTH_SAML_${samlName}_ATTRIBUTE_USERNAME`], + email: process.env[`HD_AUTH_SAML_${samlName}_ATTRIBUTE_USERNAME`], + }, + }; + }); + + const oauth2s = oauth2Names.map((oauth2Name) => { + return { + providerName: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_PROVIDER_NAME`], + baseURL: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_BASE_URL`], + userProfileURL: + process.env[`HD_AUTH_OAUTH2_${oauth2Name}_USER_PROFILE_URL`], + userProfileIdAttr: + process.env[`HD_AUTH_OAUTH2_${oauth2Name}_USER_PROFILE_ID_ATTR`], + userProfileUsernameAttr: + process.env[`HD_AUTH_OAUTH2_${oauth2Name}_USER_PROFILE_USERNAME_ATTR`], + userProfileDisplayNameAttr: + process.env[ + `HD_AUTH_OAUTH2_${oauth2Name}_USER_PROFILE_DISPLAY_NAME_ATTR` + ], + userProfileEmailAttr: + process.env[`HD_AUTH_OAUTH2_${oauth2Name}_USER_PROFILE_EMAIL_ATTR`], + tokenURL: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_TOKEN_URL`], + authorizationURL: + process.env[`HD_AUTH_OAUTH2_${oauth2Name}_AUTHORIZATION_URL`], + clientID: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_CLIENT_ID`], + clientSecret: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_CLIENT_SECRET`], + scope: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_SCOPE`], + rolesClaim: process.env[`HD_AUTH_OAUTH2_${oauth2Name}`], + accessRole: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_ACCESS_ROLE`], + }; + }); + const authConfig = authSchema.validate( { email: { @@ -338,7 +367,36 @@ export default registerAs('authConfig', async () => { }, ); if (authConfig.error) { - throw new Error(authConfig.error.toString()); + const errorMessages = await authConfig.error.details + .map((detail) => detail.message) + .map((error) => { + error = replaceAuthErrorsWithEnvironmentVariables( + error, + 'gitlab', + 'HD_AUTH_GITLAB_', + gitlabNames, + ); + error = replaceAuthErrorsWithEnvironmentVariables( + error, + 'ldap', + 'HD_AUTH_LDAP_', + ldapNames, + ); + error = replaceAuthErrorsWithEnvironmentVariables( + error, + 'saml', + 'HD_AUTH_SAML_', + samlNames, + ); + error = replaceAuthErrorsWithEnvironmentVariables( + error, + 'oauth2', + 'HD_AUTH_OAUTH2_', + oauth2Names, + ); + return error; + }); + throw new Error(buildErrorMessage(errorMessages)); } return authConfig.value; }); diff --git a/src/config/csp.config.ts b/src/config/csp.config.ts index a6de092d6..000118a31 100644 --- a/src/config/csp.config.ts +++ b/src/config/csp.config.ts @@ -6,17 +6,16 @@ import * as Joi from 'joi'; import { registerAs } from '@nestjs/config'; +import { buildErrorMessage } from './utils'; export interface CspConfig { enable: boolean; - maxAgeSeconds: number; - includeSubdomains: boolean; - preload: boolean; + reportURI: string; } const cspSchema = Joi.object({ - enable: Joi.boolean().default(true).optional(), - reportURI: Joi.string().optional(), + enable: Joi.boolean().default(true).optional().label('HD_CSP_ENABLE'), + reportURI: Joi.string().optional().label('HD_CSP_REPORTURI'), }); export default registerAs('cspConfig', async () => { @@ -31,7 +30,10 @@ export default registerAs('cspConfig', async () => { }, ); if (cspConfig.error) { - throw new Error(cspConfig.error.toString()); + const errorMessages = await cspConfig.error.details.map( + (detail) => detail.message, + ); + throw new Error(buildErrorMessage(errorMessages)); } return cspConfig.value; }); diff --git a/src/config/database.config.ts b/src/config/database.config.ts index 24575128d..652d74249 100644 --- a/src/config/database.config.ts +++ b/src/config/database.config.ts @@ -7,6 +7,7 @@ import * as Joi from 'joi'; import { DatabaseDialect } from './database-dialect.enum'; import { registerAs } from '@nestjs/config'; +import { buildErrorMessage } from './utils'; export interface DatabaseConfig { username: string; @@ -23,33 +24,35 @@ const databaseSchema = Joi.object({ is: Joi.invalid(DatabaseDialect.SQLITE), then: Joi.string(), otherwise: Joi.optional(), - }), + }).label('HD_DATABASE_USER'), password: Joi.when('dialect', { is: Joi.invalid(DatabaseDialect.SQLITE), then: Joi.string(), otherwise: Joi.optional(), - }), + }).label('HD_DATABASE_PASS'), database: Joi.when('dialect', { is: Joi.invalid(DatabaseDialect.SQLITE), then: Joi.string(), otherwise: Joi.optional(), - }), + }).label('HD_DATABASE_NAME'), host: Joi.when('dialect', { is: Joi.invalid(DatabaseDialect.SQLITE), then: Joi.string(), otherwise: Joi.optional(), - }), + }).label('HD_DATABASE_HOST'), port: Joi.when('dialect', { is: Joi.invalid(DatabaseDialect.SQLITE), then: Joi.number(), otherwise: Joi.optional(), - }), + }).label('HD_DATABASE_PORT'), storage: Joi.when('dialect', { is: Joi.valid(DatabaseDialect.SQLITE), then: Joi.string(), otherwise: Joi.optional(), - }), - dialect: Joi.string().valid(...Object.values(DatabaseDialect)), + }).label('HD_DATABASE_STORAGE'), + dialect: Joi.string() + .valid(...Object.values(DatabaseDialect)) + .label('HD_DATABASE_DIALECT'), }); export default registerAs('databaseConfig', async () => { @@ -69,7 +72,10 @@ export default registerAs('databaseConfig', async () => { }, ); if (databaseConfig.error) { - throw new Error(databaseConfig.error.toString()); + const errorMessages = await databaseConfig.error.details.map( + (detail) => detail.message, + ); + throw new Error(buildErrorMessage(errorMessages)); } return databaseConfig.value; }); diff --git a/src/config/hsts.config.ts b/src/config/hsts.config.ts index 08d5bdc66..c64f3f416 100644 --- a/src/config/hsts.config.ts +++ b/src/config/hsts.config.ts @@ -6,6 +6,7 @@ import * as Joi from 'joi'; import { registerAs } from '@nestjs/config'; +import { buildErrorMessage } from './utils'; export interface HstsConfig { enable: boolean; @@ -15,12 +16,16 @@ export interface HstsConfig { } const hstsSchema = Joi.object({ - enable: Joi.boolean().default(true).optional(), + enable: Joi.boolean().default(true).optional().label('HD_HSTS_ENABLE'), maxAgeSeconds: Joi.number() .default(60 * 60 * 24 * 365) - .optional(), - includeSubdomains: Joi.boolean().default(true).optional(), - preload: Joi.boolean().default(true).optional(), + .optional() + .label('HD_HSTS_MAX_AGE'), + includeSubdomains: Joi.boolean() + .default(true) + .optional() + .label('HD_HSTS_INCLUDE_SUBDOMAINS'), + preload: Joi.boolean().default(true).optional().label('HD_HSTS_PRELOAD'), }); export default registerAs('hstsConfig', async () => { @@ -37,7 +42,10 @@ export default registerAs('hstsConfig', async () => { }, ); if (hstsConfig.error) { - throw new Error(hstsConfig.error.toString()); + const errorMessages = await hstsConfig.error.details.map( + (detail) => detail.message, + ); + throw new Error(buildErrorMessage(errorMessages)); } return hstsConfig.value; }); diff --git a/src/config/media.config.ts b/src/config/media.config.ts index 4a78a9ed0..cd890f670 100644 --- a/src/config/media.config.ts +++ b/src/config/media.config.ts @@ -7,6 +7,7 @@ import * as Joi from 'joi'; import { BackendType } from '../media/backends/backend-type.enum'; import { registerAs } from '@nestjs/config'; +import { buildErrorMessage } from './utils'; export interface MediaConfig { backend: { @@ -33,37 +34,41 @@ export interface MediaConfig { const mediaSchema = Joi.object({ backend: { - use: Joi.string().valid(...Object.values(BackendType)), + use: Joi.string() + .valid(...Object.values(BackendType)) + .label('HD_MEDIA_BACKEND'), filesystem: { uploadPath: Joi.when('...use', { is: Joi.valid(BackendType.FILESYSTEM), then: Joi.string(), otherwise: Joi.optional(), - }), + }).label('HD_MEDIA_BACKEND_FILESYSTEM_UPLOAD_PATH'), }, s3: Joi.when('...use', { is: Joi.valid(BackendType.S3), then: Joi.object({ - accessKey: Joi.string(), - secretKey: Joi.string(), - endPoint: Joi.string(), - secure: Joi.boolean(), - port: Joi.number(), + accessKey: Joi.string().label('HD_MEDIA_BACKEND_S3_ACCESS_KEY'), + secretKey: Joi.string().label('HD_MEDIA_BACKEND_S3_SECRET_KEY'), + endPoint: Joi.string().label('HD_MEDIA_BACKEND_S3_ENDPOINT'), + secure: Joi.boolean().label('HD_MEDIA_BACKEND_S3_SECURE'), + port: Joi.number().label('HD_MEDIA_BACKEND_S3_PORT'), }), otherwise: Joi.optional(), }), azure: Joi.when('...use', { is: Joi.valid(BackendType.AZURE), then: Joi.object({ - connectionString: Joi.string(), - container: Joi.string(), + connectionString: Joi.string().label( + 'HD_MEDIA_BACKEND_AZURE_CONNECTION_STRING', + ), + container: Joi.string().label('HD_MEDIA_BACKEND_AZURE_CONTAINER'), }), otherwise: Joi.optional(), }), imgur: Joi.when('...use', { is: Joi.valid(BackendType.IMGUR), then: Joi.object({ - clientID: Joi.string(), + clientID: Joi.string().label('HD_MEDIA_BACKEND_IMGUR_CLIENTID'), }), otherwise: Joi.optional(), }), @@ -80,7 +85,7 @@ export default registerAs('mediaConfig', async () => { }, s3: { accessKey: process.env.HD_MEDIA_BACKEND_S3_ACCESS_KEY, - secretKey: process.env.HD_MEDIA_BACKEND_S3_ACCESS_KEY, + secretKey: process.env.HD_MEDIA_BACKEND_S3_SECRET_KEY, endPoint: process.env.HD_MEDIA_BACKEND_S3_ENDPOINT, secure: process.env.HD_MEDIA_BACKEND_S3_SECURE, port: parseInt(process.env.HD_MEDIA_BACKEND_S3_PORT) || undefined, @@ -101,7 +106,10 @@ export default registerAs('mediaConfig', async () => { }, ); if (mediaConfig.error) { - throw new Error(mediaConfig.error.toString()); + const errorMessages = await mediaConfig.error.details.map( + (detail) => detail.message, + ); + throw new Error(buildErrorMessage(errorMessages)); } return mediaConfig.value; }); diff --git a/src/config/utils.spec.ts b/src/config/utils.spec.ts new file mode 100644 index 000000000..c611c53cf --- /dev/null +++ b/src/config/utils.spec.ts @@ -0,0 +1,43 @@ +/* + * SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file) + * + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { + replaceAuthErrorsWithEnvironmentVariables, + toArrayConfig, +} from './utils'; + +describe('config utils', () => { + describe('toArrayConfig', () => { + it('empty', () => { + expect(toArrayConfig('')).toEqual([]); + }); + it('one element', () => { + expect(toArrayConfig('one')).toEqual(['one']); + }); + it('multiple elements', () => { + expect(toArrayConfig('one, two, three')).toEqual(['one', 'two', 'three']); + }); + it('non default seperator', () => { + expect(toArrayConfig('one ; two ; three', ';')).toEqual([ + 'one', + 'two', + 'three', + ]); + }); + }); + describe('toArrayConfig', () => { + it('"gitlab[0].scope', () => { + expect( + replaceAuthErrorsWithEnvironmentVariables( + '"gitlab[0].scope', + 'gitlab', + 'HD_AUTH_GITLAB_', + ['test'], + ), + ).toEqual('"HD_AUTH_GITLAB_test_SCOPE'); + }); + }); +}); diff --git a/src/config/utils.ts b/src/config/utils.ts index 168a0e532..d6d02e2cf 100644 --- a/src/config/utils.ts +++ b/src/config/utils.ts @@ -15,3 +15,77 @@ export const toArrayConfig = (configValue: string, separator = ',') => { return configValue.split(separator).map((arrayItem) => arrayItem.trim()); }; + +export const buildErrorMessage = (errorMessages: string[]): string => { + let totalErrorMessage = 'There were some errors with your configuration:'; + for (const message of errorMessages) { + totalErrorMessage += '\n - '; + totalErrorMessage += message; + } + totalErrorMessage += + '\nFor further information, have a look at our configuration docs at https://docs.hedgedoc.org/configuration'; + return totalErrorMessage; +}; + +export const replaceAuthErrorsWithEnvironmentVariables = ( + message: string, + name: string, + replacement: string, + arrayOfNames: string[], +): string => { + // this builds a regex like /"gitlab\[(\d+)]\./ to extract the position in the arrayOfNames + const regex = new RegExp('"' + name + '\\[(\\d+)]\\.', 'g'); + message = message.replace( + regex, + (_, index) => `"${replacement}${arrayOfNames[index]}.`, + ); + message = message.replace('.providerName', '_PROVIDER_NAME'); + message = message.replace('.baseURL', '_BASE_URL'); + message = message.replace('.clientID', '_CLIENT_ID'); + message = message.replace('.clientSecret', '_CLIENT_SECRET'); + message = message.replace('.scope', '_SCOPE'); + message = message.replace('.version', '_GITLAB_VERSION'); + message = message.replace('.url', '_URL'); + message = message.replace('.bindDn', '_BIND_DN'); + message = message.replace('.bindCredentials', '_BIND_CREDENTIALS'); + message = message.replace('.searchBase', '_SEARCH_BASE'); + message = message.replace('.searchFilter', '_SEARCH_FILTER'); + message = message.replace('.searchAttributes', '_SEARCH_ATTRIBUTES'); + message = message.replace('.usernameField', '_USERNAME_FIELD'); + message = message.replace('.useridField', '_USERID_FIELD'); + message = message.replace('.tlsCa', '_TLS_CA'); + message = message.replace('.idpSsoUrl', '_IDPSSOURL'); + message = message.replace('.idpCert', '_IDPCERT'); + message = message.replace('.clientCert', '_CLIENTCERT'); + message = message.replace('.issuer', '_ISSUER'); + message = message.replace('.identifierFormat', '_IDENTIFIERFORMAT'); + message = message.replace( + '.disableRequestedAuthnContext', + '_DISABLEREQUESTEDAUTHNCONTEXT', + ); + message = message.replace('.groupAttribute', '_GROUPATTRIBUTE'); + message = message.replace('.requiredGroups', '_REQUIREDGROUPS'); + message = message.replace('.externalGroups', '_EXTERNALGROUPS'); + message = message.replace('.attribute.id', '_ATTRIBUTE_ID'); + message = message.replace('.attribute.username', '_ATTRIBUTE_USERNAME'); + message = message.replace('.attribute.email', '_ATTRIBUTE_USERNAME'); + message = message.replace('.userProfileURL', '_USER_PROFILE_URL'); + message = message.replace('.userProfileIdAttr', '_USER_PROFILE_ID_ATTR'); + message = message.replace( + '.userProfileUsernameAttr', + '_USER_PROFILE_USERNAME_ATTR', + ); + message = message.replace( + '.userProfileDisplayNameAttr', + '_USER_PROFILE_DISPLAY_NAME_ATTR', + ); + message = message.replace( + '.userProfileEmailAttr', + '_USER_PROFILE_EMAIL_ATTR', + ); + message = message.replace('.tokenURL', '_TOKEN_URL'); + message = message.replace('.authorizationURL', '_AUTHORIZATION_URL'); + message = message.replace('.rolesClaim', '_ROLES_CLAIM'); + message = message.replace('.accessRole', '_ACCESS_ROLE'); + return message; +};