From 454a883f179ec4d88de43cc6770d28657b8bb286 Mon Sep 17 00:00:00 2001 From: Philip Molares Date: Tue, 19 Jan 2021 15:47:05 +0100 Subject: [PATCH] config: Improve error messages Add labels to most Joi objects Convert all auth variable insert names to upper case to prevent inconsistent naming of the variables Rewrite auth errors to correctly point out the problematic variable Add tests for the config utils functions Signed-off-by: Philip Molares --- src/config/app.config.ts | 13 +- src/config/auth.config.ts | 324 ++++++++++++++++++++-------------- src/config/csp.config.ts | 14 +- src/config/database.config.ts | 22 ++- src/config/hsts.config.ts | 18 +- src/config/media.config.ts | 32 ++-- src/config/utils.spec.ts | 43 +++++ src/config/utils.ts | 74 ++++++++ 8 files changed, 372 insertions(+), 168 deletions(-) create mode 100644 src/config/utils.spec.ts 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; +};