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 <philip.molares@udo.edu>
This commit is contained in:
Philip Molares 2021-01-19 15:47:05 +01:00 committed by David Mehren
parent 4afc75912a
commit bc525633fc
No known key found for this signature in database
GPG key ID: 185982BA4C42B7C3
8 changed files with 372 additions and 168 deletions

View file

@ -7,6 +7,7 @@
import { registerAs } from '@nestjs/config'; import { registerAs } from '@nestjs/config';
import * as Joi from 'joi'; import * as Joi from 'joi';
import { Loglevel } from './loglevel.enum'; import { Loglevel } from './loglevel.enum';
import { buildErrorMessage } from './utils';
export interface AppConfig { export interface AppConfig {
domain: string; domain: string;
@ -15,12 +16,13 @@ export interface AppConfig {
} }
const schema = Joi.object({ const schema = Joi.object({
domain: Joi.string(), domain: Joi.string().label('HD_DOMAIN'),
port: Joi.number().default(3000).optional(), port: Joi.number().default(3000).optional().label('PORT'),
loglevel: Joi.string() loglevel: Joi.string()
.valid(...Object.values(Loglevel)) .valid(...Object.values(Loglevel))
.default(Loglevel.WARN) .default(Loglevel.WARN)
.optional(), .optional()
.label('HD_LOGLEVEL'),
}); });
export default registerAs('appConfig', async () => { export default registerAs('appConfig', async () => {
@ -36,7 +38,10 @@ export default registerAs('appConfig', async () => {
}, },
); );
if (appConfig.error) { 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; return appConfig.value;
}); });

View file

@ -6,7 +6,11 @@
import * as Joi from 'joi'; import * as Joi from 'joi';
import { GitlabScope, GitlabVersion } from './gitlab.enum'; import { GitlabScope, GitlabVersion } from './gitlab.enum';
import { toArrayConfig } from './utils'; import {
buildErrorMessage,
replaceAuthErrorsWithEnvironmentVariables,
toArrayConfig,
} from './utils';
import { registerAs } from '@nestjs/config'; import { registerAs } from '@nestjs/config';
export interface AuthConfig { export interface AuthConfig {
@ -102,38 +106,50 @@ export interface AuthConfig {
const authSchema = Joi.object({ const authSchema = Joi.object({
email: { email: {
enableLogin: Joi.boolean().default(false).optional(), enableLogin: Joi.boolean()
enableRegister: Joi.boolean().default(false).optional(), .default(false)
.optional()
.label('HD_AUTH_EMAIL_ENABLE_LOGIN'),
enableRegister: Joi.boolean()
.default(false)
.optional()
.label('HD_AUTH_EMAIL_ENABLE_REGISTER'),
}, },
facebook: { facebook: {
clientID: Joi.string().optional(), clientID: Joi.string().optional().label('HD_AUTH_FACEBOOK_CLIENT_ID'),
clientSecret: Joi.string().optional(), clientSecret: Joi.string()
.optional()
.label('HD_AUTH_FACEBOOK_CLIENT_SECRET'),
}, },
twitter: { twitter: {
consumerKey: Joi.string().optional(), consumerKey: Joi.string().optional().label('HD_AUTH_TWITTER_CONSUMER_KEY'),
consumerSecret: Joi.string().optional(), consumerSecret: Joi.string()
.optional()
.label('HD_AUTH_TWITTER_CONSUMER_SECRET'),
}, },
github: { github: {
clientID: Joi.string().optional(), clientID: Joi.string().optional().label('HD_AUTH_GITHUB_CLIENT_ID'),
clientSecret: Joi.string().optional(), clientSecret: Joi.string().optional().label('HD_AUTH_GITHUB_CLIENT_SECRET'),
}, },
dropbox: { dropbox: {
clientID: Joi.string().optional(), clientID: Joi.string().optional().label('HD_AUTH_DROPBOX_CLIENT_ID'),
clientSecret: Joi.string().optional(), clientSecret: Joi.string()
appKey: Joi.string().optional(), .optional()
.label('HD_AUTH_DROPBOX_CLIENT_SECRET'),
appKey: Joi.string().optional().label('HD_AUTH_DROPBOX_APP_KEY'),
}, },
google: { google: {
clientID: Joi.string().optional(), clientID: Joi.string().optional().label('HD_AUTH_GOOGLE_CLIENT_ID'),
clientSecret: Joi.string().optional(), clientSecret: Joi.string().optional().label('HD_AUTH_GOOGLE_CLIENT_SECRET'),
apiKey: Joi.string().optional(), apiKey: Joi.string().optional().label('HD_AUTH_GOOGLE_APP_KEY'),
}, },
gitlab: Joi.array() gitlab: Joi.array()
.items( .items(
Joi.object({ Joi.object({
providerName: Joi.string().default('Gitlab').optional(), providerName: Joi.string().default('Gitlab').optional(),
baseURL: Joi.string().optional(), baseURL: Joi.string(),
clientID: Joi.string().optional(), clientID: Joi.string(),
clientSecret: Joi.string().optional(), clientSecret: Joi.string(),
scope: Joi.string() scope: Joi.string()
.valid(...Object.values(GitlabScope)) .valid(...Object.values(GitlabScope))
.default(GitlabScope.READ_USER) .default(GitlabScope.READ_USER)
@ -142,7 +158,7 @@ const authSchema = Joi.object({
.valid(...Object.values(GitlabVersion)) .valid(...Object.values(GitlabVersion))
.default(GitlabVersion.V4) .default(GitlabVersion.V4)
.optional(), .optional(),
}), }).optional(),
) )
.optional(), .optional(),
// ToDo: should searchfilter have a default? // ToDo: should searchfilter have a default?
@ -150,83 +166,96 @@ const authSchema = Joi.object({
.items( .items(
Joi.object({ Joi.object({
providerName: Joi.string().default('LDAP').optional(), providerName: Joi.string().default('LDAP').optional(),
url: Joi.string().optional(), url: Joi.string(),
bindDn: Joi.string().optional(), bindDn: Joi.string().optional(),
bindCredentials: Joi.string().optional(), bindCredentials: Joi.string().optional(),
searchBase: Joi.string().optional(), searchBase: Joi.string(),
searchFilter: Joi.string().default('(uid={{username}})').optional(), searchFilter: Joi.string().default('(uid={{username}})').optional(),
searchAttributes: Joi.array().items(Joi.string()), searchAttributes: Joi.array()
usernameField: Joi.string().default('userid').optional(), .items(Joi.string())
useridField: Joi.string().optional(), .default(['displayName', 'mail'])
tlsCa: Joi.array().items(Joi.string()), .optional(),
}), usernameField: Joi.string().optional(),
useridField: Joi.string(),
tlsCa: Joi.array().items(Joi.string()).optional(),
}).optional(),
) )
.optional(), .optional(),
saml: Joi.array() saml: Joi.array()
.items( .items(
Joi.object({ Joi.object({
providerName: Joi.string().default('SAML').optional(), providerName: Joi.string().default('SAML').optional(),
idpSsoUrl: Joi.string().optional(), idpSsoUrl: Joi.string(),
idpCert: Joi.string().optional(), idpCert: Joi.string(),
clientCert: Joi.string().optional(), clientCert: Joi.string().optional(),
// ToDo: (default: config.serverURL) will be build on-the-fly in the config/index.js from domain, urlAddPort and urlPath. // 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() identifierFormat: Joi.string()
.default('urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress') .default('urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress')
.optional(), .optional(),
disableRequestedAuthnContext: Joi.boolean().default(false).optional(), disableRequestedAuthnContext: Joi.boolean().default(false).optional(),
groupAttribute: Joi.string().optional(), groupAttribute: Joi.string().optional(),
requiredGroups: Joi.array().items(Joi.string()), requiredGroups: Joi.array().items(Joi.string()).optional(),
externalGroups: Joi.array().items(Joi.string()), externalGroups: Joi.array().items(Joi.string()).optional(),
attribute: { attribute: {
id: Joi.string().default('NameId').optional(), id: Joi.string().default('NameId').optional(),
username: Joi.string().default('NameId').optional(), username: Joi.string().default('NameId').optional(),
email: Joi.string().default('NameId').optional(), email: Joi.string().default('NameId').optional(),
}, },
}), }).optional(),
) )
.optional(), .optional(),
oauth2: Joi.array() oauth2: Joi.array()
.items( .items(
Joi.object({ Joi.object({
providerName: Joi.string().default('OAuth2').optional(), providerName: Joi.string().default('OAuth2').optional(),
baseURL: Joi.string().optional(), baseURL: Joi.string(),
userProfileURL: Joi.string().optional(), userProfileURL: Joi.string(),
userProfileIdAttr: Joi.string().optional(), userProfileIdAttr: Joi.string().optional(),
userProfileUsernameAttr: Joi.string().optional(), userProfileUsernameAttr: Joi.string(),
userProfileDisplayNameAttr: Joi.string().optional(), userProfileDisplayNameAttr: Joi.string(),
userProfileEmailAttr: Joi.string().optional(), userProfileEmailAttr: Joi.string(),
tokenURL: Joi.string().optional(), tokenURL: Joi.string(),
authorizationURL: Joi.string().optional(), authorizationURL: Joi.string(),
clientID: Joi.string().optional(), clientID: Joi.string(),
clientSecret: Joi.string().optional(), clientSecret: Joi.string(),
scope: Joi.string().optional(), scope: Joi.string().optional(),
rolesClaim: Joi.string().optional(), rolesClaim: Joi.string().optional(),
accessRole: Joi.string().optional(), accessRole: Joi.string().optional(),
}), }).optional(),
) )
.optional(), .optional(),
}); });
// ToDo: Validate these with Joi to prevent duplicate entries? 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 gitlabNames = toArrayConfig(process.env.HD_AUTH_GITLABS, ','); const gitlabs = gitlabNames.map((gitlabName) => {
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 { return {
providerName: process.env[`HD_AUTH_GITLAB_${gitlabName}_PROVIDER_NAME`], providerName: process.env[`HD_AUTH_GITLAB_${gitlabName}_PROVIDER_NAME`],
baseURL: process.env[`HD_AUTH_GITLAB_${gitlabName}_BASE_URL`], baseURL: process.env[`HD_AUTH_GITLAB_${gitlabName}_BASE_URL`],
clientID: process.env[`HD_AUTH_GITLAB_${gitlabName}_CLIENT_ID`], clientID: process.env[`HD_AUTH_GITLAB_${gitlabName}_CLIENT_ID`],
clientSecret: process.env[`HD_AUTH_GITLAB_${gitlabName}_CLIENT_SECRET`], clientSecret: process.env[`HD_AUTH_GITLAB_${gitlabName}_CLIENT_SECRET`],
scope: process.env[`HD_AUTH_GITLAB_${gitlabName}_GITLAB_SCOPE`], scope: process.env[`HD_AUTH_GITLAB_${gitlabName}_SCOPE`],
version: process.env[`HD_AUTH_GITLAB_${gitlabName}_GITLAB_VERSION`], version: process.env[`HD_AUTH_GITLAB_${gitlabName}_GITLAB_VERSION`],
}; };
}); });
const ldaps = ldapNames.map((ldapName) => { const ldaps = ldapNames.map((ldapName) => {
return { return {
providerName: process.env[`HD_AUTH_LDAP_${ldapName}_PROVIDER_NAME`], providerName: process.env[`HD_AUTH_LDAP_${ldapName}_PROVIDER_NAME`],
url: process.env[`HD_AUTH_LDAP_${ldapName}_URL`], url: process.env[`HD_AUTH_LDAP_${ldapName}_URL`],
@ -242,16 +271,17 @@ const ldaps = ldapNames.map((ldapName) => {
useridField: process.env[`HD_AUTH_LDAP_${ldapName}_USERID_FIELD`], useridField: process.env[`HD_AUTH_LDAP_${ldapName}_USERID_FIELD`],
tlsCa: toArrayConfig(process.env[`HD_AUTH_LDAP_${ldapName}_TLS_CA`], ','), tlsCa: toArrayConfig(process.env[`HD_AUTH_LDAP_${ldapName}_TLS_CA`], ','),
}; };
}); });
const samls = samlNames.map((samlName) => { const samls = samlNames.map((samlName) => {
return { return {
providerName: process.env[`HD_AUTH_SAML_${samlName}_PROVIDER_NAME`], providerName: process.env[`HD_AUTH_SAML_${samlName}_PROVIDER_NAME`],
idpSsoUrl: process.env[`HD_AUTH_SAML_${samlName}_IDPSSOURL`], idpSsoUrl: process.env[`HD_AUTH_SAML_${samlName}_IDPSSOURL`],
idpCert: process.env[`HD_AUTH_SAML_${samlName}_IDPCERT`], idpCert: process.env[`HD_AUTH_SAML_${samlName}_IDPCERT`],
clientCert: process.env[`HD_AUTH_SAML_${samlName}_CLIENTCERT`], clientCert: process.env[`HD_AUTH_SAML_${samlName}_CLIENTCERT`],
issuer: process.env[`HD_AUTH_SAML_${samlName}_ISSUER`], issuer: process.env[`HD_AUTH_SAML_${samlName}_ISSUER`],
identifierFormat: process.env[`HD_AUTH_SAML_${samlName}_IDENTIFIERFORMAT`], identifierFormat:
process.env[`HD_AUTH_SAML_${samlName}_IDENTIFIERFORMAT`],
disableRequestedAuthnContext: disableRequestedAuthnContext:
process.env[`HD_AUTH_SAML_${samlName}_DISABLEREQUESTEDAUTHNCONTEXT`], process.env[`HD_AUTH_SAML_${samlName}_DISABLEREQUESTEDAUTHNCONTEXT`],
groupAttribute: process.env[`HD_AUTH_SAML_${samlName}_GROUPATTRIBUTE`], groupAttribute: process.env[`HD_AUTH_SAML_${samlName}_GROUPATTRIBUTE`],
@ -269,12 +299,12 @@ const samls = samlNames.map((samlName) => {
email: process.env[`HD_AUTH_SAML_${samlName}_ATTRIBUTE_USERNAME`], email: process.env[`HD_AUTH_SAML_${samlName}_ATTRIBUTE_USERNAME`],
}, },
}; };
}); });
const oauth2s = oauth2Names.map((oauth2Name) => { const oauth2s = oauth2Names.map((oauth2Name) => {
return { return {
providerName: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_PROVIDER_NAME`], providerName: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_PROVIDER_NAME`],
baseURL: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_BASEURL`], baseURL: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_BASE_URL`],
userProfileURL: userProfileURL:
process.env[`HD_AUTH_OAUTH2_${oauth2Name}_USER_PROFILE_URL`], process.env[`HD_AUTH_OAUTH2_${oauth2Name}_USER_PROFILE_URL`],
userProfileIdAttr: userProfileIdAttr:
@ -293,12 +323,11 @@ const oauth2s = oauth2Names.map((oauth2Name) => {
clientID: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_CLIENT_ID`], clientID: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_CLIENT_ID`],
clientSecret: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_CLIENT_SECRET`], clientSecret: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_CLIENT_SECRET`],
scope: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_SCOPE`], scope: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_SCOPE`],
rolesClaim: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_ROLES_CLAIM`], rolesClaim: process.env[`HD_AUTH_OAUTH2_${oauth2Name}`],
accessRole: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_ACCESS_ROLE`], accessRole: process.env[`HD_AUTH_OAUTH2_${oauth2Name}_ACCESS_ROLE`],
}; };
}); });
export default registerAs('authConfig', async () => {
const authConfig = authSchema.validate( const authConfig = authSchema.validate(
{ {
email: { email: {
@ -338,7 +367,36 @@ export default registerAs('authConfig', async () => {
}, },
); );
if (authConfig.error) { 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; return authConfig.value;
}); });

View file

@ -6,17 +6,16 @@
import * as Joi from 'joi'; import * as Joi from 'joi';
import { registerAs } from '@nestjs/config'; import { registerAs } from '@nestjs/config';
import { buildErrorMessage } from './utils';
export interface CspConfig { export interface CspConfig {
enable: boolean; enable: boolean;
maxAgeSeconds: number; reportURI: string;
includeSubdomains: boolean;
preload: boolean;
} }
const cspSchema = Joi.object({ const cspSchema = Joi.object({
enable: Joi.boolean().default(true).optional(), enable: Joi.boolean().default(true).optional().label('HD_CSP_ENABLE'),
reportURI: Joi.string().optional(), reportURI: Joi.string().optional().label('HD_CSP_REPORTURI'),
}); });
export default registerAs('cspConfig', async () => { export default registerAs('cspConfig', async () => {
@ -31,7 +30,10 @@ export default registerAs('cspConfig', async () => {
}, },
); );
if (cspConfig.error) { 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; return cspConfig.value;
}); });

View file

@ -7,6 +7,7 @@
import * as Joi from 'joi'; import * as Joi from 'joi';
import { DatabaseDialect } from './database-dialect.enum'; import { DatabaseDialect } from './database-dialect.enum';
import { registerAs } from '@nestjs/config'; import { registerAs } from '@nestjs/config';
import { buildErrorMessage } from './utils';
export interface DatabaseConfig { export interface DatabaseConfig {
username: string; username: string;
@ -23,33 +24,35 @@ const databaseSchema = Joi.object({
is: Joi.invalid(DatabaseDialect.SQLITE), is: Joi.invalid(DatabaseDialect.SQLITE),
then: Joi.string(), then: Joi.string(),
otherwise: Joi.optional(), otherwise: Joi.optional(),
}), }).label('HD_DATABASE_USER'),
password: Joi.when('dialect', { password: Joi.when('dialect', {
is: Joi.invalid(DatabaseDialect.SQLITE), is: Joi.invalid(DatabaseDialect.SQLITE),
then: Joi.string(), then: Joi.string(),
otherwise: Joi.optional(), otherwise: Joi.optional(),
}), }).label('HD_DATABASE_PASS'),
database: Joi.when('dialect', { database: Joi.when('dialect', {
is: Joi.invalid(DatabaseDialect.SQLITE), is: Joi.invalid(DatabaseDialect.SQLITE),
then: Joi.string(), then: Joi.string(),
otherwise: Joi.optional(), otherwise: Joi.optional(),
}), }).label('HD_DATABASE_NAME'),
host: Joi.when('dialect', { host: Joi.when('dialect', {
is: Joi.invalid(DatabaseDialect.SQLITE), is: Joi.invalid(DatabaseDialect.SQLITE),
then: Joi.string(), then: Joi.string(),
otherwise: Joi.optional(), otherwise: Joi.optional(),
}), }).label('HD_DATABASE_HOST'),
port: Joi.when('dialect', { port: Joi.when('dialect', {
is: Joi.invalid(DatabaseDialect.SQLITE), is: Joi.invalid(DatabaseDialect.SQLITE),
then: Joi.number(), then: Joi.number(),
otherwise: Joi.optional(), otherwise: Joi.optional(),
}), }).label('HD_DATABASE_PORT'),
storage: Joi.when('dialect', { storage: Joi.when('dialect', {
is: Joi.valid(DatabaseDialect.SQLITE), is: Joi.valid(DatabaseDialect.SQLITE),
then: Joi.string(), then: Joi.string(),
otherwise: Joi.optional(), otherwise: Joi.optional(),
}), }).label('HD_DATABASE_STORAGE'),
dialect: Joi.string().valid(...Object.values(DatabaseDialect)), dialect: Joi.string()
.valid(...Object.values(DatabaseDialect))
.label('HD_DATABASE_DIALECT'),
}); });
export default registerAs('databaseConfig', async () => { export default registerAs('databaseConfig', async () => {
@ -69,7 +72,10 @@ export default registerAs('databaseConfig', async () => {
}, },
); );
if (databaseConfig.error) { 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; return databaseConfig.value;
}); });

View file

@ -6,6 +6,7 @@
import * as Joi from 'joi'; import * as Joi from 'joi';
import { registerAs } from '@nestjs/config'; import { registerAs } from '@nestjs/config';
import { buildErrorMessage } from './utils';
export interface HstsConfig { export interface HstsConfig {
enable: boolean; enable: boolean;
@ -15,12 +16,16 @@ export interface HstsConfig {
} }
const hstsSchema = Joi.object({ const hstsSchema = Joi.object({
enable: Joi.boolean().default(true).optional(), enable: Joi.boolean().default(true).optional().label('HD_HSTS_ENABLE'),
maxAgeSeconds: Joi.number() maxAgeSeconds: Joi.number()
.default(60 * 60 * 24 * 365) .default(60 * 60 * 24 * 365)
.optional(), .optional()
includeSubdomains: Joi.boolean().default(true).optional(), .label('HD_HSTS_MAX_AGE'),
preload: Joi.boolean().default(true).optional(), 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 () => { export default registerAs('hstsConfig', async () => {
@ -37,7 +42,10 @@ export default registerAs('hstsConfig', async () => {
}, },
); );
if (hstsConfig.error) { 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; return hstsConfig.value;
}); });

View file

@ -7,6 +7,7 @@
import * as Joi from 'joi'; import * as Joi from 'joi';
import { BackendType } from '../media/backends/backend-type.enum'; import { BackendType } from '../media/backends/backend-type.enum';
import { registerAs } from '@nestjs/config'; import { registerAs } from '@nestjs/config';
import { buildErrorMessage } from './utils';
export interface MediaConfig { export interface MediaConfig {
backend: { backend: {
@ -33,37 +34,41 @@ export interface MediaConfig {
const mediaSchema = Joi.object({ const mediaSchema = Joi.object({
backend: { backend: {
use: Joi.string().valid(...Object.values(BackendType)), use: Joi.string()
.valid(...Object.values(BackendType))
.label('HD_MEDIA_BACKEND'),
filesystem: { filesystem: {
uploadPath: Joi.when('...use', { uploadPath: Joi.when('...use', {
is: Joi.valid(BackendType.FILESYSTEM), is: Joi.valid(BackendType.FILESYSTEM),
then: Joi.string(), then: Joi.string(),
otherwise: Joi.optional(), otherwise: Joi.optional(),
}), }).label('HD_MEDIA_BACKEND_FILESYSTEM_UPLOAD_PATH'),
}, },
s3: Joi.when('...use', { s3: Joi.when('...use', {
is: Joi.valid(BackendType.S3), is: Joi.valid(BackendType.S3),
then: Joi.object({ then: Joi.object({
accessKey: Joi.string(), accessKey: Joi.string().label('HD_MEDIA_BACKEND_S3_ACCESS_KEY'),
secretKey: Joi.string(), secretKey: Joi.string().label('HD_MEDIA_BACKEND_S3_SECRET_KEY'),
endPoint: Joi.string(), endPoint: Joi.string().label('HD_MEDIA_BACKEND_S3_ENDPOINT'),
secure: Joi.boolean(), secure: Joi.boolean().label('HD_MEDIA_BACKEND_S3_SECURE'),
port: Joi.number(), port: Joi.number().label('HD_MEDIA_BACKEND_S3_PORT'),
}), }),
otherwise: Joi.optional(), otherwise: Joi.optional(),
}), }),
azure: Joi.when('...use', { azure: Joi.when('...use', {
is: Joi.valid(BackendType.AZURE), is: Joi.valid(BackendType.AZURE),
then: Joi.object({ then: Joi.object({
connectionString: Joi.string(), connectionString: Joi.string().label(
container: Joi.string(), 'HD_MEDIA_BACKEND_AZURE_CONNECTION_STRING',
),
container: Joi.string().label('HD_MEDIA_BACKEND_AZURE_CONTAINER'),
}), }),
otherwise: Joi.optional(), otherwise: Joi.optional(),
}), }),
imgur: Joi.when('...use', { imgur: Joi.when('...use', {
is: Joi.valid(BackendType.IMGUR), is: Joi.valid(BackendType.IMGUR),
then: Joi.object({ then: Joi.object({
clientID: Joi.string(), clientID: Joi.string().label('HD_MEDIA_BACKEND_IMGUR_CLIENTID'),
}), }),
otherwise: Joi.optional(), otherwise: Joi.optional(),
}), }),
@ -80,7 +85,7 @@ export default registerAs('mediaConfig', async () => {
}, },
s3: { s3: {
accessKey: process.env.HD_MEDIA_BACKEND_S3_ACCESS_KEY, 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, endPoint: process.env.HD_MEDIA_BACKEND_S3_ENDPOINT,
secure: process.env.HD_MEDIA_BACKEND_S3_SECURE, secure: process.env.HD_MEDIA_BACKEND_S3_SECURE,
port: parseInt(process.env.HD_MEDIA_BACKEND_S3_PORT) || undefined, port: parseInt(process.env.HD_MEDIA_BACKEND_S3_PORT) || undefined,
@ -101,7 +106,10 @@ export default registerAs('mediaConfig', async () => {
}, },
); );
if (mediaConfig.error) { 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; return mediaConfig.value;
}); });

43
src/config/utils.spec.ts Normal file
View file

@ -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');
});
});
});

View file

@ -15,3 +15,77 @@ export const toArrayConfig = (configValue: string, separator = ',') => {
return configValue.split(separator).map((arrayItem) => arrayItem.trim()); 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;
};