mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2024-11-26 11:43:59 -05:00
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:
parent
4afc75912a
commit
bc525633fc
8 changed files with 372 additions and 168 deletions
|
@ -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;
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
});
|
});
|
||||||
|
|
|
@ -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;
|
||||||
});
|
});
|
||||||
|
|
|
@ -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
43
src/config/utils.spec.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -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;
|
||||||
|
};
|
||||||
|
|
Loading…
Reference in a new issue