fix(repository): Move backend code into subdirectory

Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
Tilman Vatteroth 2022-10-02 20:10:32 +02:00 committed by David Mehren
parent 86584e705f
commit bf30cbcf48
272 changed files with 87 additions and 67 deletions

View file

@ -0,0 +1,288 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import mockedEnv from 'mocked-env';
import appConfig from './app.config';
import { Loglevel } from './loglevel.enum';
describe('appConfig', () => {
const domain = 'https://example.com';
const invalidDomain = 'localhost';
const rendererBaseUrl = 'https://render.example.com';
const port = 3333;
const negativePort = -9000;
const floatPort = 3.14;
const outOfRangePort = 1000000;
const invalidPort = 'not-a-port';
const loglevel = Loglevel.TRACE;
const invalidLoglevel = 'not-a-loglevel';
const invalidPersistInterval = -1;
describe('correctly parses config', () => {
it('when given correct and complete environment variables', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_DOMAIN: domain,
HD_RENDERER_BASE_URL: rendererBaseUrl,
PORT: port.toString(),
HD_LOGLEVEL: loglevel,
HD_PERSIST_INTERVAL: '100',
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
const config = appConfig();
expect(config.domain).toEqual(domain);
expect(config.rendererBaseUrl).toEqual(rendererBaseUrl);
expect(config.port).toEqual(port);
expect(config.loglevel).toEqual(loglevel);
expect(config.persistInterval).toEqual(100);
restore();
});
it('when no HD_RENDER_ORIGIN is set', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_DOMAIN: domain,
PORT: port.toString(),
HD_LOGLEVEL: loglevel,
HD_PERSIST_INTERVAL: '100',
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
const config = appConfig();
expect(config.domain).toEqual(domain);
expect(config.rendererBaseUrl).toEqual(domain);
expect(config.port).toEqual(port);
expect(config.loglevel).toEqual(loglevel);
expect(config.persistInterval).toEqual(100);
restore();
});
it('when no PORT is set', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_DOMAIN: domain,
HD_RENDERER_BASE_URL: rendererBaseUrl,
HD_LOGLEVEL: loglevel,
HD_PERSIST_INTERVAL: '100',
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
const config = appConfig();
expect(config.domain).toEqual(domain);
expect(config.rendererBaseUrl).toEqual(rendererBaseUrl);
expect(config.port).toEqual(3000);
expect(config.loglevel).toEqual(loglevel);
expect(config.persistInterval).toEqual(100);
restore();
});
it('when no HD_LOGLEVEL is set', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_DOMAIN: domain,
HD_RENDERER_BASE_URL: rendererBaseUrl,
PORT: port.toString(),
HD_PERSIST_INTERVAL: '100',
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
const config = appConfig();
expect(config.domain).toEqual(domain);
expect(config.rendererBaseUrl).toEqual(rendererBaseUrl);
expect(config.port).toEqual(port);
expect(config.loglevel).toEqual(Loglevel.WARN);
expect(config.persistInterval).toEqual(100);
restore();
});
it('when no HD_PERSIST_INTERVAL is set', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_DOMAIN: domain,
HD_RENDERER_BASE_URL: rendererBaseUrl,
HD_LOGLEVEL: loglevel,
PORT: port.toString(),
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
const config = appConfig();
expect(config.domain).toEqual(domain);
expect(config.rendererBaseUrl).toEqual(rendererBaseUrl);
expect(config.port).toEqual(port);
expect(config.loglevel).toEqual(Loglevel.TRACE);
expect(config.persistInterval).toEqual(10);
restore();
});
it('when HD_PERSIST_INTERVAL is zero', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_DOMAIN: domain,
HD_RENDERER_BASE_URL: rendererBaseUrl,
HD_LOGLEVEL: loglevel,
PORT: port.toString(),
HD_PERSIST_INTERVAL: '0',
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
const config = appConfig();
expect(config.domain).toEqual(domain);
expect(config.rendererBaseUrl).toEqual(rendererBaseUrl);
expect(config.port).toEqual(port);
expect(config.loglevel).toEqual(Loglevel.TRACE);
expect(config.persistInterval).toEqual(0);
restore();
});
});
describe('throws error', () => {
it('when given a non-valid HD_DOMAIN', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_DOMAIN: invalidDomain,
PORT: port.toString(),
HD_LOGLEVEL: loglevel,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => appConfig()).toThrow('HD_DOMAIN');
restore();
});
it('when given a negative PORT', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_DOMAIN: domain,
PORT: negativePort.toString(),
HD_LOGLEVEL: loglevel,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => appConfig()).toThrow('"PORT" must be a positive number');
restore();
});
it('when given a out-of-range PORT', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_DOMAIN: domain,
PORT: outOfRangePort.toString(),
HD_LOGLEVEL: loglevel,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => appConfig()).toThrow(
'"PORT" must be less than or equal to 65535',
);
restore();
});
it('when given a non-integer PORT', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_DOMAIN: domain,
PORT: floatPort.toString(),
HD_LOGLEVEL: loglevel,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => appConfig()).toThrow('"PORT" must be an integer');
restore();
});
it('when given a non-number PORT', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_DOMAIN: domain,
PORT: invalidPort,
HD_LOGLEVEL: loglevel,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => appConfig()).toThrow('"PORT" must be a number');
restore();
});
it('when given a non-loglevel HD_LOGLEVEL', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_DOMAIN: domain,
PORT: port.toString(),
HD_LOGLEVEL: invalidLoglevel,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => appConfig()).toThrow('HD_LOGLEVEL');
restore();
});
it('when given a negative HD_PERSIST_INTERVAL', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_DOMAIN: domain,
PORT: port.toString(),
HD_LOGLEVEL: invalidLoglevel,
HD_PERSIST_INTERVAL: invalidPersistInterval.toString(),
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => appConfig()).toThrow('HD_PERSIST_INTERVAL');
restore();
});
});
});

View file

@ -0,0 +1,74 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { registerAs } from '@nestjs/config';
import * as Joi from 'joi';
import { Loglevel } from './loglevel.enum';
import { buildErrorMessage, parseOptionalNumber } from './utils';
export interface AppConfig {
domain: string;
rendererBaseUrl: string;
port: number;
loglevel: Loglevel;
persistInterval: number;
}
const schema = Joi.object({
domain: Joi.string()
.uri({
scheme: /https?/,
})
.label('HD_DOMAIN'),
rendererBaseUrl: Joi.string()
.uri({
scheme: /https?/,
})
.default(Joi.ref('domain'))
.optional()
.label('HD_RENDERER_BASE_URL'),
port: Joi.number()
.positive()
.integer()
.default(3000)
.max(65535)
.optional()
.label('PORT'),
loglevel: Joi.string()
.valid(...Object.values(Loglevel))
.default(Loglevel.WARN)
.optional()
.label('HD_LOGLEVEL'),
persistInterval: Joi.number()
.integer()
.min(0)
.default(10)
.optional()
.label('HD_PERSIST_INTERVAL'),
});
export default registerAs('appConfig', () => {
const appConfig = schema.validate(
{
domain: process.env.HD_DOMAIN,
rendererBaseUrl: process.env.HD_RENDERER_BASE_URL,
port: parseOptionalNumber(process.env.PORT),
loglevel: process.env.HD_LOGLEVEL,
persistInterval: process.env.HD_PERSIST_INTERVAL,
},
{
abortEarly: false,
presence: 'required',
},
);
if (appConfig.error) {
const errorMessages = appConfig.error.details.map(
(detail) => detail.message,
);
throw new Error(buildErrorMessage(errorMessages));
}
return appConfig.value as AppConfig;
});

View file

@ -0,0 +1,522 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import mockedEnv from 'mocked-env';
import authConfig from './auth.config';
describe('authConfig', () => {
const secret = 'this-is-a-secret';
const neededAuthConfig = {
/* eslint-disable @typescript-eslint/naming-convention */
HD_SESSION_SECRET: secret,
/* eslint-enable @typescript-eslint/naming-convention */
};
describe('local', () => {
const enableLogin = true;
const enableRegister = true;
const minimalPasswordStrength = 1;
const completeLocalConfig = {
/* eslint-disable @typescript-eslint/naming-convention */
HD_AUTH_LOCAL_ENABLE_LOGIN: String(enableLogin),
HD_AUTH_LOCAL_ENABLE_REGISTER: String(enableRegister),
HD_AUTH_LOCAL_MINIMAL_PASSWORD_STRENGTH: String(minimalPasswordStrength),
/* eslint-enable @typescript-eslint/naming-convention */
};
describe('is correctly parsed', () => {
it('when given correct and complete environment variables', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
...neededAuthConfig,
...completeLocalConfig,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
const config = authConfig();
expect(config.local.enableLogin).toEqual(enableLogin);
expect(config.local.enableRegister).toEqual(enableRegister);
expect(config.local.minimalPasswordStrength).toEqual(
minimalPasswordStrength,
);
restore();
});
it('when HD_AUTH_LOCAL_ENABLE_LOGIN is not set', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
...neededAuthConfig,
...completeLocalConfig,
HD_AUTH_LOCAL_ENABLE_LOGIN: undefined,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
const config = authConfig();
expect(config.local.enableLogin).toEqual(false);
expect(config.local.enableRegister).toEqual(enableRegister);
expect(config.local.minimalPasswordStrength).toEqual(
minimalPasswordStrength,
);
restore();
});
it('when HD_AUTH_LOCAL_ENABLE_REGISTER is not set', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
...neededAuthConfig,
...completeLocalConfig,
HD_AUTH_LOCAL_ENABLE_REGISTER: undefined,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
const config = authConfig();
expect(config.local.enableLogin).toEqual(enableLogin);
expect(config.local.enableRegister).toEqual(false);
expect(config.local.minimalPasswordStrength).toEqual(
minimalPasswordStrength,
);
restore();
});
it('when HD_AUTH_LOCAL_MINIMAL_PASSWORD_STRENGTH is not set', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
...neededAuthConfig,
...completeLocalConfig,
HD_AUTH_LOCAL_MINIMAL_PASSWORD_STRENGTH: undefined,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
const config = authConfig();
expect(config.local.enableLogin).toEqual(enableLogin);
expect(config.local.enableRegister).toEqual(enableRegister);
expect(config.local.minimalPasswordStrength).toEqual(2);
restore();
});
});
describe('fails to be parsed', () => {
it('when HD_AUTH_LOCAL_MINIMAL_PASSWORD_STRENGTH is 5', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
...neededAuthConfig,
...completeLocalConfig,
HD_AUTH_LOCAL_MINIMAL_PASSWORD_STRENGTH: '5',
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => authConfig()).toThrow(
'"HD_AUTH_LOCAL_MINIMAL_PASSWORD_STRENGTH" must be less than or equal to 4',
);
restore();
});
it('when HD_AUTH_LOCAL_MINIMAL_PASSWORD_STRENGTH is -1', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
...neededAuthConfig,
...completeLocalConfig,
HD_AUTH_LOCAL_MINIMAL_PASSWORD_STRENGTH: '-1',
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => authConfig()).toThrow(
'"HD_AUTH_LOCAL_MINIMAL_PASSWORD_STRENGTH" must be greater than or equal to 0',
);
restore();
});
});
});
describe('ldap', () => {
const ldapNames = ['futurama'];
const providerName = 'Futurama LDAP';
const url = 'ldap://localhost:389';
const searchBase = 'ou=people,dc=planetexpress,dc=com';
const searchFilter = '(mail={{username}})';
const searchAttributes = ['mail', 'uid'];
const userIdField = 'non_default_uid';
const displayNameField = 'non_default_display_name';
const profilePictureField = 'non_default_profile_picture';
const bindDn = 'cn=admin,dc=planetexpress,dc=com';
const bindCredentials = 'GoodNewsEveryone';
const tlsCa = ['./test/private-api/fixtures/hedgedoc.pem'];
const tlsCaContent = ['test-cert\n'];
const completeLdapConfig = {
/* eslint-disable @typescript-eslint/naming-convention */
HD_AUTH_LDAPS: ldapNames.join(','),
HD_AUTH_LDAP_FUTURAMA_PROVIDER_NAME: providerName,
HD_AUTH_LDAP_FUTURAMA_URL: url,
HD_AUTH_LDAP_FUTURAMA_SEARCH_BASE: searchBase,
HD_AUTH_LDAP_FUTURAMA_SEARCH_FILTER: searchFilter,
HD_AUTH_LDAP_FUTURAMA_SEARCH_ATTRIBUTES: searchAttributes.join(','),
HD_AUTH_LDAP_FUTURAMA_USER_ID_FIELD: userIdField,
HD_AUTH_LDAP_FUTURAMA_DISPLAY_NAME_FIELD: displayNameField,
HD_AUTH_LDAP_FUTURAMA_PROFILE_PICTURE_FIELD: profilePictureField,
HD_AUTH_LDAP_FUTURAMA_BIND_DN: bindDn,
HD_AUTH_LDAP_FUTURAMA_BIND_CREDENTIALS: bindCredentials,
HD_AUTH_LDAP_FUTURAMA_TLS_CERT_PATHS: tlsCa.join(','),
/* eslint-enable @typescript-eslint/naming-convention */
};
describe('is correctly parsed', () => {
it('when given correct and complete environment variables', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
...neededAuthConfig,
...completeLdapConfig,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
const config = authConfig();
expect(config.ldap).toHaveLength(1);
const firstLdap = config.ldap[0];
expect(firstLdap.identifier).toEqual(ldapNames[0].toUpperCase());
expect(firstLdap.url).toEqual(url);
expect(firstLdap.providerName).toEqual(providerName);
expect(firstLdap.searchBase).toEqual(searchBase);
expect(firstLdap.searchFilter).toEqual(searchFilter);
expect(firstLdap.searchAttributes).toEqual(searchAttributes);
expect(firstLdap.userIdField).toEqual(userIdField);
expect(firstLdap.displayNameField).toEqual(displayNameField);
expect(firstLdap.profilePictureField).toEqual(profilePictureField);
expect(firstLdap.bindDn).toEqual(bindDn);
expect(firstLdap.bindCredentials).toEqual(bindCredentials);
expect(firstLdap.tlsCaCerts).toEqual(tlsCaContent);
restore();
});
it('when no HD_AUTH_LDAP_FUTURAMA_PROVIDER_NAME is not set', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
...neededAuthConfig,
...completeLdapConfig,
HD_AUTH_LDAP_FUTURAMA_PROVIDER_NAME: undefined,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
const config = authConfig();
expect(config.ldap).toHaveLength(1);
const firstLdap = config.ldap[0];
expect(firstLdap.identifier).toEqual(ldapNames[0].toUpperCase());
expect(firstLdap.url).toEqual(url);
expect(firstLdap.providerName).toEqual('LDAP');
expect(firstLdap.searchBase).toEqual(searchBase);
expect(firstLdap.searchFilter).toEqual(searchFilter);
expect(firstLdap.searchAttributes).toEqual(searchAttributes);
expect(firstLdap.userIdField).toEqual(userIdField);
expect(firstLdap.displayNameField).toEqual(displayNameField);
expect(firstLdap.profilePictureField).toEqual(profilePictureField);
expect(firstLdap.bindDn).toEqual(bindDn);
expect(firstLdap.bindCredentials).toEqual(bindCredentials);
expect(firstLdap.tlsCaCerts).toEqual(tlsCaContent);
restore();
});
it('when no HD_AUTH_LDAP_FUTURAMA_SEARCH_FILTER is not set', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
...neededAuthConfig,
...completeLdapConfig,
HD_AUTH_LDAP_FUTURAMA_SEARCH_FILTER: undefined,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
const config = authConfig();
expect(config.ldap).toHaveLength(1);
const firstLdap = config.ldap[0];
expect(firstLdap.identifier).toEqual(ldapNames[0].toUpperCase());
expect(firstLdap.url).toEqual(url);
expect(firstLdap.providerName).toEqual(providerName);
expect(firstLdap.searchBase).toEqual(searchBase);
expect(firstLdap.searchFilter).toEqual('(uid={{username}})');
expect(firstLdap.searchAttributes).toEqual(searchAttributes);
expect(firstLdap.userIdField).toEqual(userIdField);
expect(firstLdap.displayNameField).toEqual(displayNameField);
expect(firstLdap.profilePictureField).toEqual(profilePictureField);
expect(firstLdap.bindDn).toEqual(bindDn);
expect(firstLdap.bindCredentials).toEqual(bindCredentials);
expect(firstLdap.tlsCaCerts).toEqual(tlsCaContent);
restore();
});
it('when no HD_AUTH_LDAP_FUTURAMA_USER_ID_FIELD is not set', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
...neededAuthConfig,
...completeLdapConfig,
HD_AUTH_LDAP_FUTURAMA_USER_ID_FIELD: undefined,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
const config = authConfig();
expect(config.ldap).toHaveLength(1);
const firstLdap = config.ldap[0];
expect(firstLdap.identifier).toEqual(ldapNames[0].toUpperCase());
expect(firstLdap.url).toEqual(url);
expect(firstLdap.providerName).toEqual(providerName);
expect(firstLdap.searchBase).toEqual(searchBase);
expect(firstLdap.searchFilter).toEqual(searchFilter);
expect(firstLdap.searchAttributes).toEqual(searchAttributes);
expect(firstLdap.userIdField).toBe('uid');
expect(firstLdap.displayNameField).toEqual(displayNameField);
expect(firstLdap.profilePictureField).toEqual(profilePictureField);
expect(firstLdap.bindDn).toEqual(bindDn);
expect(firstLdap.bindCredentials).toEqual(bindCredentials);
expect(firstLdap.tlsCaCerts).toEqual(tlsCaContent);
restore();
});
it('when no HD_AUTH_LDAP_FUTURAMA_DISPLAY_NAME_FIELD is not set', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
...neededAuthConfig,
...completeLdapConfig,
HD_AUTH_LDAP_FUTURAMA_DISPLAY_NAME_FIELD: undefined,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
const config = authConfig();
expect(config.ldap).toHaveLength(1);
const firstLdap = config.ldap[0];
expect(firstLdap.identifier).toEqual(ldapNames[0].toUpperCase());
expect(firstLdap.url).toEqual(url);
expect(firstLdap.providerName).toEqual(providerName);
expect(firstLdap.searchBase).toEqual(searchBase);
expect(firstLdap.searchFilter).toEqual(searchFilter);
expect(firstLdap.searchAttributes).toEqual(searchAttributes);
expect(firstLdap.userIdField).toEqual(userIdField);
expect(firstLdap.displayNameField).toEqual('displayName');
expect(firstLdap.profilePictureField).toEqual(profilePictureField);
expect(firstLdap.bindDn).toEqual(bindDn);
expect(firstLdap.bindCredentials).toEqual(bindCredentials);
expect(firstLdap.tlsCaCerts).toEqual(tlsCaContent);
restore();
});
it('when no HD_AUTH_LDAP_FUTURAMA_PROFILE_PICTURE_FIELD is not set', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
...neededAuthConfig,
...completeLdapConfig,
HD_AUTH_LDAP_FUTURAMA_PROFILE_PICTURE_FIELD: undefined,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
const config = authConfig();
expect(config.ldap).toHaveLength(1);
const firstLdap = config.ldap[0];
expect(firstLdap.identifier).toEqual(ldapNames[0].toUpperCase());
expect(firstLdap.url).toEqual(url);
expect(firstLdap.providerName).toEqual(providerName);
expect(firstLdap.searchBase).toEqual(searchBase);
expect(firstLdap.searchFilter).toEqual(searchFilter);
expect(firstLdap.searchAttributes).toEqual(searchAttributes);
expect(firstLdap.userIdField).toEqual(userIdField);
expect(firstLdap.displayNameField).toEqual(displayNameField);
expect(firstLdap.profilePictureField).toEqual('jpegPhoto');
expect(firstLdap.bindDn).toEqual(bindDn);
expect(firstLdap.bindCredentials).toEqual(bindCredentials);
expect(firstLdap.tlsCaCerts).toEqual(tlsCaContent);
restore();
});
it('when no HD_AUTH_LDAP_FUTURAMA_BIND_DN is not set', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
...neededAuthConfig,
...completeLdapConfig,
HD_AUTH_LDAP_FUTURAMA_BIND_DN: undefined,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
const config = authConfig();
expect(config.ldap).toHaveLength(1);
const firstLdap = config.ldap[0];
expect(firstLdap.identifier).toEqual(ldapNames[0].toUpperCase());
expect(firstLdap.url).toEqual(url);
expect(firstLdap.providerName).toEqual(providerName);
expect(firstLdap.searchBase).toEqual(searchBase);
expect(firstLdap.searchFilter).toEqual(searchFilter);
expect(firstLdap.searchAttributes).toEqual(searchAttributes);
expect(firstLdap.userIdField).toEqual(userIdField);
expect(firstLdap.displayNameField).toEqual(displayNameField);
expect(firstLdap.profilePictureField).toEqual(profilePictureField);
expect(firstLdap.bindDn).toBe(undefined);
expect(firstLdap.bindCredentials).toEqual(bindCredentials);
expect(firstLdap.tlsCaCerts).toEqual(tlsCaContent);
restore();
});
it('when no HD_AUTH_LDAP_FUTURAMA_BIND_CREDENTIALS is not set', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
...neededAuthConfig,
...completeLdapConfig,
HD_AUTH_LDAP_FUTURAMA_BIND_CREDENTIALS: undefined,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
const config = authConfig();
expect(config.ldap).toHaveLength(1);
const firstLdap = config.ldap[0];
expect(firstLdap.identifier).toEqual(ldapNames[0].toUpperCase());
expect(firstLdap.url).toEqual(url);
expect(firstLdap.providerName).toEqual(providerName);
expect(firstLdap.searchBase).toEqual(searchBase);
expect(firstLdap.searchFilter).toEqual(searchFilter);
expect(firstLdap.searchAttributes).toEqual(searchAttributes);
expect(firstLdap.userIdField).toEqual(userIdField);
expect(firstLdap.displayNameField).toEqual(displayNameField);
expect(firstLdap.profilePictureField).toEqual(profilePictureField);
expect(firstLdap.bindDn).toEqual(bindDn);
expect(firstLdap.bindCredentials).toBe(undefined);
expect(firstLdap.tlsCaCerts).toEqual(tlsCaContent);
restore();
});
it('when no HD_AUTH_LDAP_FUTURAMA_TLS_CERT_PATHS is not set', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
...neededAuthConfig,
...completeLdapConfig,
HD_AUTH_LDAP_FUTURAMA_TLS_CERT_PATHS: undefined,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
const config = authConfig();
expect(config.ldap).toHaveLength(1);
const firstLdap = config.ldap[0];
expect(firstLdap.identifier).toEqual(ldapNames[0].toUpperCase());
expect(firstLdap.url).toEqual(url);
expect(firstLdap.providerName).toEqual(providerName);
expect(firstLdap.searchBase).toEqual(searchBase);
expect(firstLdap.searchFilter).toEqual(searchFilter);
expect(firstLdap.searchAttributes).toEqual(searchAttributes);
expect(firstLdap.userIdField).toEqual(userIdField);
expect(firstLdap.displayNameField).toEqual(displayNameField);
expect(firstLdap.profilePictureField).toEqual(profilePictureField);
expect(firstLdap.bindDn).toEqual(bindDn);
expect(firstLdap.bindCredentials).toEqual(bindCredentials);
expect(firstLdap.tlsCaCerts).toBe(undefined);
restore();
});
});
describe('throws error', () => {
it('when HD_AUTH_LDAP_FUTURAMA_URL is wrong', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
...neededAuthConfig,
...completeLdapConfig,
HD_AUTH_LDAP_FUTURAMA_URL: undefined,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => authConfig()).toThrow(
'"HD_AUTH_LDAP_FUTURAMA_URL" is required',
);
restore();
});
it('when HD_AUTH_LDAP_FUTURAMA_SEARCH_BASE is wrong', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
...neededAuthConfig,
...completeLdapConfig,
HD_AUTH_LDAP_FUTURAMA_SEARCH_BASE: undefined,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => authConfig()).toThrow(
'"HD_AUTH_LDAP_FUTURAMA_SEARCH_BASE" is required',
);
restore();
});
it('when HD_AUTH_LDAP_FUTURAMA_TLS_CERT_PATHS is wrong', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
...neededAuthConfig,
...completeLdapConfig,
HD_AUTH_LDAP_FUTURAMA_TLS_CERT_PATHS: 'not-a-file.pem',
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => authConfig()).toThrow(
'"HD_AUTH_LDAP_FUTURAMA_TLS_CERT_PATHS[0]" must not be a sparse array item',
);
restore();
});
});
});
});

View file

@ -0,0 +1,452 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { registerAs } from '@nestjs/config';
import * as fs from 'fs';
import * as Joi from 'joi';
import { GitlabScope, GitlabVersion } from './gitlab.enum';
import {
buildErrorMessage,
parseOptionalNumber,
replaceAuthErrorsWithEnvironmentVariables,
toArrayConfig,
} from './utils';
export interface LDAPConfig {
identifier: string;
providerName: string;
url: string;
bindDn?: string;
bindCredentials?: string;
searchBase: string;
searchFilter: string;
searchAttributes: string[];
userIdField: string;
displayNameField: string;
profilePictureField: string;
tlsCaCerts?: string[];
}
export interface AuthConfig {
session: {
secret: string;
lifetime: number;
};
local: {
enableLogin: boolean;
enableRegister: boolean;
minimalPasswordStrength: number;
};
facebook: {
clientID: string;
clientSecret: string;
};
twitter: {
consumerKey: string;
consumerSecret: string;
};
github: {
clientID: string;
clientSecret: string;
};
dropbox: {
clientID: string;
clientSecret: string;
appKey: string;
};
google: {
clientID: string;
clientSecret: string;
apiKey: string;
};
gitlab: {
identifier: string;
providerName: string;
baseURL: string;
clientID: string;
clientSecret: string;
scope: GitlabScope;
version: GitlabVersion;
}[];
// ToDo: tlsOptions exist in config.json.example. See https://nodejs.org/api/tls.html#tls_tls_connect_options_callback
ldap: LDAPConfig[];
saml: {
identifier: string;
providerName: string;
idpSsoUrl: string;
idpCert: string;
clientCert: string;
issuer: string;
identifierFormat: string;
disableRequestedAuthnContext: string;
groupAttribute: string;
requiredGroups?: string[];
externalGroups?: string[];
attribute: {
id: string;
username: string;
email: string;
};
}[];
oauth2: {
identifier: string;
providerName: string;
baseURL: string;
userProfileURL: string;
userProfileIdAttr: string;
userProfileUsernameAttr: string;
userProfileDisplayNameAttr: string;
userProfileEmailAttr: string;
tokenURL: string;
authorizationURL: string;
clientID: string;
clientSecret: string;
scope: string;
rolesClaim: string;
accessRole: string;
}[];
}
const authSchema = Joi.object({
session: {
secret: Joi.string().label('HD_SESSION_SECRET'),
lifetime: Joi.number()
.default(1209600000) // 14 * 24 * 60 * 60 * 1000ms = 14 days
.optional()
.label('HD_SESSION_LIFETIME'),
},
local: {
enableLogin: Joi.boolean()
.default(false)
.optional()
.label('HD_AUTH_LOCAL_ENABLE_LOGIN'),
enableRegister: Joi.boolean()
.default(false)
.optional()
.label('HD_AUTH_LOCAL_ENABLE_REGISTER'),
minimalPasswordStrength: Joi.number()
.default(2)
.min(0)
.max(4)
.optional()
.label('HD_AUTH_LOCAL_MINIMAL_PASSWORD_STRENGTH'),
},
facebook: {
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().label('HD_AUTH_TWITTER_CONSUMER_KEY'),
consumerSecret: Joi.string()
.optional()
.label('HD_AUTH_TWITTER_CONSUMER_SECRET'),
},
github: {
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().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().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({
identifier: Joi.string(),
providerName: Joi.string().default('Gitlab').optional(),
baseURL: Joi.string(),
clientID: Joi.string(),
clientSecret: Joi.string(),
scope: Joi.string()
.valid(...Object.values(GitlabScope))
.default(GitlabScope.READ_USER)
.optional(),
version: Joi.string()
.valid(...Object.values(GitlabVersion))
.default(GitlabVersion.V4)
.optional(),
}).optional(),
)
.optional(),
// ToDo: should searchfilter have a default?
ldap: Joi.array()
.items(
Joi.object({
identifier: Joi.string(),
providerName: Joi.string().default('LDAP').optional(),
url: Joi.string(),
bindDn: Joi.string().optional(),
bindCredentials: Joi.string().optional(),
searchBase: Joi.string(),
searchFilter: Joi.string().default('(uid={{username}})').optional(),
searchAttributes: Joi.array().items(Joi.string()).optional(),
userIdField: Joi.string().default('uid').optional(),
displayNameField: Joi.string().default('displayName').optional(),
profilePictureField: Joi.string().default('jpegPhoto').optional(),
tlsCaCerts: Joi.array().items(Joi.string()).optional(),
}).optional(),
)
.optional(),
saml: Joi.array()
.items(
Joi.object({
identifier: Joi.string(),
providerName: Joi.string().default('SAML').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(),
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()).optional(),
externalGroups: Joi.array().items(Joi.string()).optional(),
attribute: {
id: Joi.string().default('NameId').optional(),
username: Joi.string().default('NameId').optional(),
local: Joi.string().default('NameId').optional(),
},
}).optional(),
)
.optional(),
oauth2: Joi.array()
.items(
Joi.object({
identifier: Joi.string(),
providerName: Joi.string().default('OAuth2').optional(),
baseURL: Joi.string(),
userProfileURL: Joi.string(),
userProfileIdAttr: 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(),
});
export default registerAs('authConfig', () => {
// 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 {
identifier: gitlabName,
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) => {
const caFiles = toArrayConfig(
process.env[`HD_AUTH_LDAP_${ldapName}_TLS_CERT_PATHS`],
',',
);
let tlsCaCerts = undefined;
if (caFiles) {
tlsCaCerts = caFiles.map((fileName) => {
if (fs.existsSync(fileName)) {
return fs.readFileSync(fileName, 'utf8');
}
});
}
return {
identifier: ldapName,
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`],
',',
),
userIdField: process.env[`HD_AUTH_LDAP_${ldapName}_USER_ID_FIELD`],
displayNameField:
process.env[`HD_AUTH_LDAP_${ldapName}_DISPLAY_NAME_FIELD`],
profilePictureField:
process.env[`HD_AUTH_LDAP_${ldapName}_PROFILE_PICTURE_FIELD`],
tlsCaCerts: tlsCaCerts,
};
});
const samls = samlNames.map((samlName) => {
return {
identifier: samlName,
providerName: process.env[`HD_AUTH_SAML_${samlName}_PROVIDER_NAME`],
idpSsoUrl: process.env[`HD_AUTH_SAML_${samlName}_IDP_SSO_URL`],
idpCert: process.env[`HD_AUTH_SAML_${samlName}_IDP_CERT`],
clientCert: process.env[`HD_AUTH_SAML_${samlName}_CLIENT_CERT`],
issuer: process.env[`HD_AUTH_SAML_${samlName}_ISSUER`],
identifierFormat:
process.env[`HD_AUTH_SAML_${samlName}_IDENTIFIER_FORMAT`],
disableRequestedAuthnContext:
process.env[`HD_AUTH_SAML_${samlName}_DISABLE_REQUESTED_AUTHN_CONTEXT`],
groupAttribute: process.env[`HD_AUTH_SAML_${samlName}_GROUP_ATTRIBUTE`],
requiredGroups: toArrayConfig(
process.env[`HD_AUTH_SAML_${samlName}_REQUIRED_GROUPS`],
'|',
),
externalGroups: toArrayConfig(
process.env[`HD_AUTH_SAML_${samlName}_EXTERNAL_GROUPS`],
'|',
),
attribute: {
id: process.env[`HD_AUTH_SAML_${samlName}_ATTRIBUTE_ID`],
username: process.env[`HD_AUTH_SAML_${samlName}_ATTRIBUTE_USERNAME`],
local: process.env[`HD_AUTH_SAML_${samlName}_ATTRIBUTE_LOCAL`],
},
};
});
const oauth2s = oauth2Names.map((oauth2Name) => {
return {
identifier: oauth2Name,
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(
{
session: {
secret: process.env.HD_SESSION_SECRET,
lifetime: parseOptionalNumber(process.env.HD_SESSION_LIFETIME),
},
local: {
enableLogin: process.env.HD_AUTH_LOCAL_ENABLE_LOGIN,
enableRegister: process.env.HD_AUTH_LOCAL_ENABLE_REGISTER,
minimalPasswordStrength: parseOptionalNumber(
process.env.HD_AUTH_LOCAL_MINIMAL_PASSWORD_STRENGTH,
),
},
facebook: {
clientID: process.env.HD_AUTH_FACEBOOK_CLIENT_ID,
clientSecret: process.env.HD_AUTH_FACEBOOK_CLIENT_SECRET,
},
twitter: {
consumerKey: process.env.HD_AUTH_TWITTER_CONSUMER_KEY,
consumerSecret: process.env.HD_AUTH_TWITTER_CONSUMER_SECRET,
},
github: {
clientID: process.env.HD_AUTH_GITHUB_CLIENT_ID,
clientSecret: process.env.HD_AUTH_GITHUB_CLIENT_SECRET,
},
dropbox: {
clientID: process.env.HD_AUTH_DROPBOX_CLIENT_ID,
clientSecret: process.env.HD_AUTH_DROPBOX_CLIENT_SECRET,
appKey: process.env.HD_AUTH_DROPBOX_APP_KEY,
},
google: {
clientID: process.env.HD_AUTH_GOOGLE_CLIENT_ID,
clientSecret: process.env.HD_AUTH_GOOGLE_CLIENT_SECRET,
apiKey: process.env.HD_AUTH_GOOGLE_APP_KEY,
},
gitlab: gitlabs,
ldap: ldaps,
saml: samls,
oauth2: oauth2s,
},
{
abortEarly: false,
presence: 'required',
},
);
if (authConfig.error) {
const errorMessages = authConfig.error.details
.map((detail) => detail.message)
.map((error) =>
replaceAuthErrorsWithEnvironmentVariables(
error,
'gitlab',
'HD_AUTH_GITLAB_',
gitlabNames,
),
)
.map((error) =>
replaceAuthErrorsWithEnvironmentVariables(
error,
'ldap',
'HD_AUTH_LDAP_',
ldapNames,
),
)
.map((error) =>
replaceAuthErrorsWithEnvironmentVariables(
error,
'saml',
'HD_AUTH_SAML_',
samlNames,
),
)
.map((error) =>
replaceAuthErrorsWithEnvironmentVariables(
error,
'oauth2',
'HD_AUTH_OAUTH2_',
oauth2Names,
),
);
throw new Error(buildErrorMessage(errorMessages));
}
return authConfig.value as AuthConfig;
});

View file

@ -0,0 +1,39 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { registerAs } from '@nestjs/config';
import * as Joi from 'joi';
import { buildErrorMessage } from './utils';
export interface CspConfig {
enable: boolean;
reportURI: string;
}
const cspSchema = Joi.object({
enable: Joi.boolean().default(true).optional().label('HD_CSP_ENABLE'),
reportURI: Joi.string().optional().label('HD_CSP_REPORT_URI'),
});
export default registerAs('cspConfig', () => {
const cspConfig = cspSchema.validate(
{
enable: process.env.HD_CSP_ENABLE || true,
reportURI: process.env.HD_CSP_REPORT_URI,
},
{
abortEarly: false,
presence: 'required',
},
);
if (cspConfig.error) {
const errorMessages = cspConfig.error.details.map(
(detail) => detail.message,
);
throw new Error(buildErrorMessage(errorMessages));
}
return cspConfig.value as CspConfig;
});

View file

@ -0,0 +1,80 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { registerAs } from '@nestjs/config';
import * as Joi from 'joi';
import { buildErrorMessage } from './utils';
export interface CustomizationConfig {
branding: {
customName: string;
customLogo: string;
};
specialUrls: {
privacy: string;
termsOfUse: string;
imprint: string;
};
}
const schema = Joi.object({
branding: Joi.object({
customName: Joi.string().optional().label('HD_CUSTOM_NAME'),
customLogo: Joi.string()
.uri({
scheme: [/https?/],
})
.optional()
.label('HD_CUSTOM_LOGO'),
}),
specialUrls: Joi.object({
privacy: Joi.string()
.uri({
scheme: /https?/,
})
.optional()
.label('HD_PRIVACY_URL'),
termsOfUse: Joi.string()
.uri({
scheme: /https?/,
})
.optional()
.label('HD_TERMS_OF_USE_URL'),
imprint: Joi.string()
.uri({
scheme: /https?/,
})
.optional()
.label('HD_IMPRINT_URL'),
}),
});
export default registerAs('customizationConfig', () => {
const customizationConfig = schema.validate(
{
branding: {
customName: process.env.HD_CUSTOM_NAME,
customLogo: process.env.HD_CUSTOM_LOGO,
},
specialUrls: {
privacy: process.env.HD_PRIVACY_URL,
termsOfUse: process.env.HD_TERMS_OF_USE_URL,
imprint: process.env.HD_IMPRINT_URL,
},
},
{
abortEarly: false,
presence: 'required',
},
);
if (customizationConfig.error) {
const errorMessages = customizationConfig.error.details.map(
(detail) => detail.message,
);
throw new Error(buildErrorMessage(errorMessages));
}
return customizationConfig.value as CustomizationConfig;
});

View file

@ -0,0 +1,12 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export enum DatabaseType {
POSTGRES = 'postgres',
MYSQL = 'mysql',
MARIADB = 'mariadb',
SQLITE = 'sqlite',
}

View file

@ -0,0 +1,73 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { registerAs } from '@nestjs/config';
import * as Joi from 'joi';
import { DatabaseType } from './database-type.enum';
import { buildErrorMessage, parseOptionalNumber } from './utils';
export interface DatabaseConfig {
username: string;
password: string;
database: string;
host: string;
port: number;
type: DatabaseType;
}
const databaseSchema = Joi.object({
type: Joi.string()
.valid(...Object.values(DatabaseType))
.label('HD_DATABASE_TYPE'),
// This is the database name, except for SQLite,
// where it is the path to the database file.
database: Joi.string().label('HD_DATABASE_NAME'),
username: Joi.when('type', {
is: Joi.invalid(DatabaseType.SQLITE),
then: Joi.string(),
otherwise: Joi.optional(),
}).label('HD_DATABASE_USER'),
password: Joi.when('type', {
is: Joi.invalid(DatabaseType.SQLITE),
then: Joi.string(),
otherwise: Joi.optional(),
}).label('HD_DATABASE_PASS'),
host: Joi.when('type', {
is: Joi.invalid(DatabaseType.SQLITE),
then: Joi.string(),
otherwise: Joi.optional(),
}).label('HD_DATABASE_HOST'),
port: Joi.when('type', {
is: Joi.invalid(DatabaseType.SQLITE),
then: Joi.number(),
otherwise: Joi.optional(),
}).label('HD_DATABASE_PORT'),
});
export default registerAs('databaseConfig', () => {
const databaseConfig = databaseSchema.validate(
{
type: process.env.HD_DATABASE_TYPE,
username: process.env.HD_DATABASE_USER,
password: process.env.HD_DATABASE_PASS,
database: process.env.HD_DATABASE_NAME,
host: process.env.HD_DATABASE_HOST,
port: parseOptionalNumber(process.env.HD_DATABASE_PORT),
},
{
abortEarly: false,
presence: 'required',
},
);
if (databaseConfig.error) {
const errorMessages = databaseConfig.error.details.map(
(detail) => detail.message,
);
throw new Error(buildErrorMessage(errorMessages));
}
return databaseConfig.value as DatabaseConfig;
});

View file

@ -0,0 +1,26 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export enum DefaultAccessPermission {
NONE = 'none',
READ = 'read',
WRITE = 'write',
}
export function getDefaultAccessPermissionOrdinal(
permission: DefaultAccessPermission,
): number {
switch (permission) {
case DefaultAccessPermission.NONE:
return 0;
case DefaultAccessPermission.READ:
return 1;
case DefaultAccessPermission.WRITE:
return 2;
default:
throw Error('Unknown permission');
}
}

View file

@ -0,0 +1,49 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { registerAs } from '@nestjs/config';
import * as Joi from 'joi';
import { buildErrorMessage } from './utils';
export interface ExternalServicesConfig {
plantUmlServer: string;
imageProxy: string;
}
const schema = Joi.object({
plantUmlServer: Joi.string()
.uri({
scheme: /https?/,
})
.optional()
.label('HD_PLANTUML_SERVER'),
imageProxy: Joi.string()
.uri({
scheme: /https?/,
})
.optional()
.label('HD_IMAGE_PROXY'),
});
export default registerAs('externalServicesConfig', () => {
const externalConfig = schema.validate(
{
plantUmlServer: process.env.HD_PLANTUML_SERVER,
imageProxy: process.env.HD_IMAGE_PROXY,
},
{
abortEarly: false,
presence: 'required',
},
);
if (externalConfig.error) {
const errorMessages = externalConfig.error.details.map(
(detail) => detail.message,
);
throw new Error(buildErrorMessage(errorMessages));
}
return externalConfig.value as ExternalServicesConfig;
});

View file

@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export enum GitlabScope {
READ_USER = 'read_user',
API = 'api',
}
// ToDo: Evaluate if V3 is really necessary anymore (it's deprecated since 2017)
export enum GitlabVersion {
V3 = 'v3',
V4 = 'v4',
}

View file

@ -0,0 +1,27 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export enum GuestAccess {
DENY = 'deny',
READ = 'read',
WRITE = 'write',
CREATE = 'create',
}
export function getGuestAccessOrdinal(guestAccess: GuestAccess): number {
switch (guestAccess) {
case GuestAccess.DENY:
return 0;
case GuestAccess.READ:
return 1;
case GuestAccess.WRITE:
return 2;
case GuestAccess.CREATE:
return 3;
default:
throw Error('Unknown permission');
}
}

View file

@ -0,0 +1,51 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { registerAs } from '@nestjs/config';
import * as Joi from 'joi';
import { buildErrorMessage, parseOptionalNumber } from './utils';
export interface HstsConfig {
enable: boolean;
maxAgeSeconds: number;
includeSubdomains: boolean;
preload: boolean;
}
const hstsSchema = Joi.object({
enable: Joi.boolean().default(true).optional().label('HD_HSTS_ENABLE'),
maxAgeSeconds: Joi.number()
.default(60 * 60 * 24 * 365)
.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', () => {
const hstsConfig = hstsSchema.validate(
{
enable: process.env.HD_HSTS_ENABLE,
maxAgeSeconds: parseOptionalNumber(process.env.HD_HSTS_MAX_AGE),
includeSubdomains: process.env.HD_HSTS_INCLUDE_SUBDOMAINS,
preload: process.env.HD_HSTS_PRELOAD,
},
{
abortEarly: false,
presence: 'required',
},
);
if (hstsConfig.error) {
const errorMessages = hstsConfig.error.details.map(
(detail) => detail.message,
);
throw new Error(buildErrorMessage(errorMessages));
}
return hstsConfig.value as HstsConfig;
});

View file

@ -0,0 +1,13 @@
/*
* SPDX-FileCopyrightText: 2021 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
export enum Loglevel {
TRACE = 'trace',
DEBUG = 'debug',
INFO = 'info',
WARN = 'warn',
ERROR = 'error',
}

View file

@ -0,0 +1,369 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import mockedEnv from 'mocked-env';
import { BackendType } from '../media/backends/backend-type.enum';
import mediaConfig from './media.config';
describe('mediaConfig', () => {
// Filesystem
const uploadPath = 'uploads';
// S3
const accessKeyId = 'accessKeyId';
const secretAccessKey = 'secretAccessKey';
const bucket = 'bucket';
const endPoint = 'endPoint';
// Azure
const azureConnectionString = 'connectionString';
const container = 'container';
// Imgur
const clientID = 'clientID';
// Webdav
const webdavConnectionString = 'https://example.com/webdav';
const uploadDir = 'uploadDir';
const publicUrl = 'https://example.com/images';
describe('correctly parses config', () => {
it('for backend filesystem', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_MEDIA_BACKEND: BackendType.FILESYSTEM,
HD_MEDIA_BACKEND_FILESYSTEM_UPLOAD_PATH: uploadPath,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
const config = mediaConfig();
expect(config.backend.use).toEqual(BackendType.FILESYSTEM);
expect(config.backend.filesystem.uploadPath).toEqual(uploadPath);
restore();
});
it('for backend s3', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_MEDIA_BACKEND: BackendType.S3,
HD_MEDIA_BACKEND_S3_ACCESS_KEY: accessKeyId,
HD_MEDIA_BACKEND_S3_SECRET_KEY: secretAccessKey,
HD_MEDIA_BACKEND_S3_BUCKET: bucket,
HD_MEDIA_BACKEND_S3_ENDPOINT: endPoint,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
const config = mediaConfig();
expect(config.backend.use).toEqual(BackendType.S3);
expect(config.backend.s3.accessKeyId).toEqual(accessKeyId);
expect(config.backend.s3.secretAccessKey).toEqual(secretAccessKey);
expect(config.backend.s3.bucket).toEqual(bucket);
expect(config.backend.s3.endPoint).toEqual(endPoint);
restore();
});
it('for backend azure', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_MEDIA_BACKEND: BackendType.AZURE,
HD_MEDIA_BACKEND_AZURE_CONNECTION_STRING: azureConnectionString,
HD_MEDIA_BACKEND_AZURE_CONTAINER: container,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
const config = mediaConfig();
expect(config.backend.use).toEqual(BackendType.AZURE);
expect(config.backend.azure.connectionString).toEqual(
azureConnectionString,
);
expect(config.backend.azure.container).toEqual(container);
restore();
});
it('for backend imgur', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_MEDIA_BACKEND: BackendType.IMGUR,
HD_MEDIA_BACKEND_IMGUR_CLIENT_ID: clientID,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
const config = mediaConfig();
expect(config.backend.use).toEqual(BackendType.IMGUR);
expect(config.backend.imgur.clientID).toEqual(clientID);
restore();
});
it('for backend webdav', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_MEDIA_BACKEND: BackendType.WEBDAV,
HD_MEDIA_BACKEND_WEBDAV_CONNECTION_STRING: webdavConnectionString,
HD_MEDIA_BACKEND_WEBDAV_UPLOAD_DIR: uploadDir,
HD_MEDIA_BACKEND_WEBDAV_PUBLIC_URL: publicUrl,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
const config = mediaConfig();
expect(config.backend.use).toEqual(BackendType.WEBDAV);
expect(config.backend.webdav.connectionString).toEqual(
webdavConnectionString,
);
expect(config.backend.webdav.uploadDir).toEqual(uploadDir);
expect(config.backend.webdav.publicUrl).toEqual(publicUrl);
restore();
});
});
describe('throws error', () => {
describe('for backend filesystem', () => {
it('when HD_MEDIA_BACKEND_FILESYSTEM_UPLOAD_PATH is not set', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_MEDIA_BACKEND: BackendType.FILESYSTEM,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => mediaConfig()).toThrow(
'"HD_MEDIA_BACKEND_FILESYSTEM_UPLOAD_PATH" is required',
);
restore();
});
});
describe('for backend s3', () => {
it('when HD_MEDIA_BACKEND_S3_ACCESS_KEY is not set', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_MEDIA_BACKEND: BackendType.S3,
HD_MEDIA_BACKEND_S3_SECRET_KEY: secretAccessKey,
HD_MEDIA_BACKEND_S3_BUCKET: bucket,
HD_MEDIA_BACKEND_S3_ENDPOINT: endPoint,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => mediaConfig()).toThrow(
'"HD_MEDIA_BACKEND_S3_ACCESS_KEY" is required',
);
restore();
});
it('when HD_MEDIA_BACKEND_S3_SECRET_KEY is not set', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_MEDIA_BACKEND: BackendType.S3,
HD_MEDIA_BACKEND_S3_ACCESS_KEY: accessKeyId,
HD_MEDIA_BACKEND_S3_BUCKET: bucket,
HD_MEDIA_BACKEND_S3_ENDPOINT: endPoint,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => mediaConfig()).toThrow(
'"HD_MEDIA_BACKEND_S3_SECRET_KEY" is required',
);
restore();
});
it('when HD_MEDIA_BACKEND_S3_BUCKET is not set', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_MEDIA_BACKEND: BackendType.S3,
HD_MEDIA_BACKEND_S3_ACCESS_KEY: accessKeyId,
HD_MEDIA_BACKEND_S3_SECRET_KEY: secretAccessKey,
HD_MEDIA_BACKEND_S3_ENDPOINT: endPoint,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => mediaConfig()).toThrow(
'"HD_MEDIA_BACKEND_S3_BUCKET" is required',
);
restore();
});
it('when HD_MEDIA_BACKEND_S3_ENDPOINT is not set', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_MEDIA_BACKEND: BackendType.S3,
HD_MEDIA_BACKEND_S3_ACCESS_KEY: accessKeyId,
HD_MEDIA_BACKEND_S3_SECRET_KEY: secretAccessKey,
HD_MEDIA_BACKEND_S3_BUCKET: bucket,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => mediaConfig()).toThrow(
'"HD_MEDIA_BACKEND_S3_ENDPOINT" is required',
);
restore();
});
});
describe('for backend azure', () => {
it('when HD_MEDIA_BACKEND_AZURE_CONNECTION_STRING is not set', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_MEDIA_BACKEND: BackendType.AZURE,
HD_MEDIA_BACKEND_AZURE_CONTAINER: container,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => mediaConfig()).toThrow(
'"HD_MEDIA_BACKEND_AZURE_CONNECTION_STRING" is required',
);
restore();
});
it('when HD_MEDIA_BACKEND_AZURE_CONTAINER is not set', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_MEDIA_BACKEND: BackendType.AZURE,
HD_MEDIA_BACKEND_AZURE_CONNECTION_STRING: azureConnectionString,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => mediaConfig()).toThrow(
'"HD_MEDIA_BACKEND_AZURE_CONTAINER" is required',
);
restore();
});
});
describe('for backend imgur', () => {
it('when HD_MEDIA_BACKEND_IMGUR_CLIENT_ID is not set', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_MEDIA_BACKEND: BackendType.IMGUR,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => mediaConfig()).toThrow(
'"HD_MEDIA_BACKEND_IMGUR_CLIENT_ID" is required',
);
restore();
});
});
describe('for backend webdav', () => {
it('when HD_MEDIA_BACKEND_WEBDAV_CONNECTION_STRING is not set', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_MEDIA_BACKEND: BackendType.WEBDAV,
HD_MEDIA_BACKEND_WEBDAV_UPLOAD_DIR: uploadDir,
HD_MEDIA_BACKEND_WEBDAV_PUBLIC_URL: publicUrl,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => mediaConfig()).toThrow(
'"HD_MEDIA_BACKEND_WEBDAV_CONNECTION_STRING" is required',
);
restore();
});
it('when HD_MEDIA_BACKEND_WEBDAV_CONNECTION_STRING is not set to an url', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_MEDIA_BACKEND: BackendType.WEBDAV,
HD_MEDIA_BACKEND_WEBDAV_CONNECTION_STRING: 'not-an-url',
HD_MEDIA_BACKEND_WEBDAV_UPLOAD_DIR: uploadDir,
HD_MEDIA_BACKEND_WEBDAV_PUBLIC_URL: publicUrl,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => mediaConfig()).toThrow(
'"HD_MEDIA_BACKEND_WEBDAV_CONNECTION_STRING" must be a valid uri',
);
restore();
});
it('when HD_MEDIA_BACKEND_WEBDAV_PUBLIC_URL is not set', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_MEDIA_BACKEND: BackendType.WEBDAV,
HD_MEDIA_BACKEND_WEBDAV_CONNECTION_STRING: webdavConnectionString,
HD_MEDIA_BACKEND_WEBDAV_UPLOAD_DIR: uploadDir,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => mediaConfig()).toThrow(
'"HD_MEDIA_BACKEND_WEBDAV_PUBLIC_URL" is required',
);
restore();
});
it('when HD_MEDIA_BACKEND_WEBDAV_PUBLIC_URL is not set to an url', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_MEDIA_BACKEND: BackendType.WEBDAV,
HD_MEDIA_BACKEND_WEBDAV_CONNECTION_STRING: webdavConnectionString,
HD_MEDIA_BACKEND_WEBDAV_UPLOAD_DIR: uploadDir,
HD_MEDIA_BACKEND_WEBDAV_PUBLIC_URL: 'not-an-url',
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => mediaConfig()).toThrow(
'"HD_MEDIA_BACKEND_WEBDAV_PUBLIC_URL" must be a valid uri',
);
restore();
});
});
});
});

View file

@ -0,0 +1,140 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { registerAs } from '@nestjs/config';
import * as Joi from 'joi';
import { BackendType } from '../media/backends/backend-type.enum';
import { buildErrorMessage } from './utils';
export interface MediaConfig {
backend: MediaBackendConfig;
}
export interface MediaBackendConfig {
use: BackendType;
filesystem: {
uploadPath: string;
};
s3: {
accessKeyId: string;
secretAccessKey: string;
bucket: string;
endPoint: string;
};
azure: {
connectionString: string;
container: string;
};
imgur: {
clientID: string;
};
webdav: {
connectionString: string;
uploadDir: string;
publicUrl: string;
};
}
const mediaSchema = Joi.object({
backend: {
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({
accessKeyId: Joi.string().label('HD_MEDIA_BACKEND_S3_ACCESS_KEY'),
secretAccessKey: Joi.string().label('HD_MEDIA_BACKEND_S3_SECRET_KEY'),
bucket: Joi.string().label('HD_MEDIA_BACKEND_S3_BUCKET'),
endPoint: Joi.string().label('HD_MEDIA_BACKEND_S3_ENDPOINT'),
}),
otherwise: Joi.optional(),
}),
azure: Joi.when('use', {
is: Joi.valid(BackendType.AZURE),
then: Joi.object({
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().label('HD_MEDIA_BACKEND_IMGUR_CLIENT_ID'),
}),
otherwise: Joi.optional(),
}),
webdav: Joi.when('use', {
is: Joi.valid(BackendType.WEBDAV),
then: Joi.object({
connectionString: Joi.string()
.uri()
.label('HD_MEDIA_BACKEND_WEBDAV_CONNECTION_STRING'),
uploadDir: Joi.string()
.optional()
.label('HD_MEDIA_BACKEND_WEBDAV_UPLOAD_DIR'),
publicUrl: Joi.string()
.uri()
.label('HD_MEDIA_BACKEND_WEBDAV_PUBLIC_URL'),
}),
otherwise: Joi.optional(),
}),
},
});
export default registerAs('mediaConfig', () => {
const mediaConfig = mediaSchema.validate(
{
backend: {
use: process.env.HD_MEDIA_BACKEND,
filesystem: {
uploadPath: process.env.HD_MEDIA_BACKEND_FILESYSTEM_UPLOAD_PATH,
},
s3: {
accessKeyId: process.env.HD_MEDIA_BACKEND_S3_ACCESS_KEY,
secretAccessKey: process.env.HD_MEDIA_BACKEND_S3_SECRET_KEY,
bucket: process.env.HD_MEDIA_BACKEND_S3_BUCKET,
endPoint: process.env.HD_MEDIA_BACKEND_S3_ENDPOINT,
},
azure: {
connectionString:
process.env.HD_MEDIA_BACKEND_AZURE_CONNECTION_STRING,
container: process.env.HD_MEDIA_BACKEND_AZURE_CONTAINER,
},
imgur: {
clientID: process.env.HD_MEDIA_BACKEND_IMGUR_CLIENT_ID,
},
webdav: {
connectionString:
process.env.HD_MEDIA_BACKEND_WEBDAV_CONNECTION_STRING,
uploadDir: process.env.HD_MEDIA_BACKEND_WEBDAV_UPLOAD_DIR,
publicUrl: process.env.HD_MEDIA_BACKEND_WEBDAV_PUBLIC_URL,
},
},
},
{
abortEarly: false,
presence: 'required',
},
);
if (mediaConfig.error) {
const errorMessages = mediaConfig.error.details.map(
(detail) => detail.message,
);
throw new Error(buildErrorMessage(errorMessages));
}
return mediaConfig.value as MediaConfig;
});

View file

@ -0,0 +1,28 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ConfigFactoryKeyHost, registerAs } from '@nestjs/config';
import { ConfigFactory } from '@nestjs/config/dist/interfaces';
import { AppConfig } from '../app.config';
import { Loglevel } from '../loglevel.enum';
export function createDefaultMockAppConfig(): AppConfig {
return {
domain: 'md.example.com',
rendererBaseUrl: 'md-renderer.example.com',
port: 3000,
loglevel: Loglevel.ERROR,
persistInterval: 10,
};
}
export function registerAppConfig(
appConfig: AppConfig,
): ConfigFactory<AppConfig> & ConfigFactoryKeyHost<AppConfig> {
return registerAs('appConfig', (): AppConfig => appConfig);
}
export default registerAppConfig(createDefaultMockAppConfig());

View file

@ -0,0 +1,57 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ConfigFactoryKeyHost, registerAs } from '@nestjs/config';
import { ConfigFactory } from '@nestjs/config/dist/interfaces';
import { AuthConfig } from '../auth.config';
export function createDefaultMockAuthConfig(): AuthConfig {
return {
session: {
secret: 'my_secret',
lifetime: 1209600000,
},
local: {
enableLogin: true,
enableRegister: true,
minimalPasswordStrength: 2,
},
facebook: {
clientID: '',
clientSecret: '',
},
twitter: {
consumerKey: '',
consumerSecret: '',
},
github: {
clientID: '',
clientSecret: '',
},
dropbox: {
clientID: '',
clientSecret: '',
appKey: '',
},
google: {
clientID: '',
clientSecret: '',
apiKey: '',
},
gitlab: [],
ldap: [],
saml: [],
oauth2: [],
};
}
export function registerAuthConfig(
authConfig: AuthConfig,
): ConfigFactory<AuthConfig> & ConfigFactoryKeyHost<AuthConfig> {
return registerAs('authConfig', (): AuthConfig => authConfig);
}
export default registerAuthConfig(createDefaultMockAuthConfig());

View file

@ -0,0 +1,37 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ConfigFactoryKeyHost, registerAs } from '@nestjs/config';
import { ConfigFactory } from '@nestjs/config/dist/interfaces';
import { CustomizationConfig } from '../customization.config';
export function createDefaultMockCustomizationConfig(): CustomizationConfig {
return {
branding: {
customName: 'ACME Corp',
customLogo: '',
},
specialUrls: {
privacy: '/test/privacy',
termsOfUse: '/test/termsOfUse',
imprint: '/test/imprint',
},
};
}
export function registerCustomizationConfig(
customizationConfig: CustomizationConfig,
): ConfigFactory<CustomizationConfig> &
ConfigFactoryKeyHost<CustomizationConfig> {
return registerAs(
'customizationConfig',
(): CustomizationConfig => customizationConfig,
);
}
export default registerCustomizationConfig(
createDefaultMockCustomizationConfig(),
);

View file

@ -0,0 +1,30 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ConfigFactoryKeyHost, registerAs } from '@nestjs/config';
import { ConfigFactory } from '@nestjs/config/dist/interfaces';
import { DatabaseType } from '../database-type.enum';
import { DatabaseConfig } from '../database.config';
export function createDefaultMockDatabaseConfig(): DatabaseConfig {
return {
type: (process.env.HEDGEDOC_TEST_DB_TYPE ||
DatabaseType.SQLITE) as DatabaseType,
database: 'hedgedoc',
password: 'hedgedoc',
host: 'localhost',
port: 0,
username: 'hedgedoc',
};
}
export function registerDatabaseConfig(
databaseConfig: DatabaseConfig,
): ConfigFactory<DatabaseConfig> & ConfigFactoryKeyHost<DatabaseConfig> {
return registerAs('databaseConfig', (): DatabaseConfig => databaseConfig);
}
export default registerDatabaseConfig(createDefaultMockDatabaseConfig());

View file

@ -0,0 +1,30 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ConfigFactoryKeyHost, registerAs } from '@nestjs/config';
import { ConfigFactory } from '@nestjs/config/dist/interfaces';
import { ExternalServicesConfig } from '../external-services.config';
export function createDefaultMockExternalServicesConfig(): ExternalServicesConfig {
return {
plantUmlServer: 'plantuml.example.com',
imageProxy: 'imageProxy.example.com',
};
}
export function registerExternalServiceConfig(
externalServicesConfig: ExternalServicesConfig,
): ConfigFactory<ExternalServicesConfig> &
ConfigFactoryKeyHost<ExternalServicesConfig> {
return registerAs(
'externalServicesConfig',
(): ExternalServicesConfig => externalServicesConfig,
);
}
export default registerExternalServiceConfig(
createDefaultMockExternalServicesConfig(),
);

View file

@ -0,0 +1,48 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ConfigFactoryKeyHost, registerAs } from '@nestjs/config';
import { ConfigFactory } from '@nestjs/config/dist/interfaces';
import { BackendType } from '../../media/backends/backend-type.enum';
import { MediaConfig } from '../media.config';
export function createDefaultMockMediaConfig(): MediaConfig {
return {
backend: {
use: BackendType.FILESYSTEM,
filesystem: {
uploadPath:
'test_uploads' + Math.floor(Math.random() * 100000).toString(),
},
s3: {
accessKeyId: '',
secretAccessKey: '',
bucket: '',
endPoint: '',
},
azure: {
connectionString: '',
container: '',
},
imgur: {
clientID: '',
},
webdav: {
connectionString: '',
uploadDir: '',
publicUrl: '',
},
},
};
}
export function registerMediaConfig(
appConfig: MediaConfig,
): ConfigFactory<MediaConfig> & ConfigFactoryKeyHost<MediaConfig> {
return registerAs('mediaConfig', (): MediaConfig => appConfig);
}
export default registerMediaConfig(createDefaultMockMediaConfig());

View file

@ -0,0 +1,33 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ConfigFactoryKeyHost, registerAs } from '@nestjs/config';
import { ConfigFactory } from '@nestjs/config/dist/interfaces';
import { DefaultAccessPermission } from '../default-access-permission.enum';
import { GuestAccess } from '../guest_access.enum';
import { NoteConfig } from '../note.config';
export function createDefaultMockNoteConfig(): NoteConfig {
return {
maxDocumentLength: 100000,
forbiddenNoteIds: ['forbiddenNoteId'],
permissions: {
default: {
everyone: DefaultAccessPermission.READ,
loggedIn: DefaultAccessPermission.WRITE,
},
},
guestAccess: GuestAccess.CREATE,
};
}
export function registerNoteConfig(
noteConfig: NoteConfig,
): ConfigFactory<NoteConfig> & ConfigFactoryKeyHost<NoteConfig> {
return registerAs('noteConfig', (): NoteConfig => noteConfig);
}
export default registerNoteConfig(createDefaultMockNoteConfig());

View file

@ -0,0 +1,458 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import mockedEnv from 'mocked-env';
import { DefaultAccessPermission } from './default-access-permission.enum';
import { GuestAccess } from './guest_access.enum';
import noteConfig from './note.config';
describe('noteConfig', () => {
const forbiddenNoteIds = ['forbidden_1', 'forbidden_2'];
const forbiddenNoteId = 'single_forbidden_id';
const invalidforbiddenNoteIds = ['', ''];
const maxDocumentLength = 1234;
const negativeMaxDocumentLength = -123;
const floatMaxDocumentLength = 2.71;
const invalidMaxDocumentLength = 'not-a-max-document-length';
const guestAccess = GuestAccess.CREATE;
const wrongDefaultPermission = 'wrong';
describe('correctly parses config', () => {
it('when given correct and complete environment variables', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessPermission.READ,
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessPermission.READ,
HD_GUEST_ACCESS: guestAccess,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
const config = noteConfig();
expect(config.forbiddenNoteIds).toHaveLength(forbiddenNoteIds.length);
expect(config.forbiddenNoteIds).toEqual(forbiddenNoteIds);
expect(config.maxDocumentLength).toEqual(maxDocumentLength);
expect(config.permissions.default.everyone).toEqual(
DefaultAccessPermission.READ,
);
expect(config.permissions.default.loggedIn).toEqual(
DefaultAccessPermission.READ,
);
expect(config.guestAccess).toEqual(guestAccess);
restore();
});
it('when no HD_FORBIDDEN_NOTE_IDS is set', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessPermission.READ,
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessPermission.READ,
HD_GUEST_ACCESS: guestAccess,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
const config = noteConfig();
expect(config.forbiddenNoteIds).toHaveLength(0);
expect(config.maxDocumentLength).toEqual(maxDocumentLength);
expect(config.permissions.default.everyone).toEqual(
DefaultAccessPermission.READ,
);
expect(config.permissions.default.loggedIn).toEqual(
DefaultAccessPermission.READ,
);
expect(config.guestAccess).toEqual(guestAccess);
restore();
});
it('when HD_FORBIDDEN_NOTE_IDS is a single item', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteId,
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessPermission.READ,
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessPermission.READ,
HD_GUEST_ACCESS: guestAccess,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
const config = noteConfig();
expect(config.forbiddenNoteIds).toHaveLength(1);
expect(config.forbiddenNoteIds[0]).toEqual(forbiddenNoteId);
expect(config.maxDocumentLength).toEqual(maxDocumentLength);
expect(config.permissions.default.everyone).toEqual(
DefaultAccessPermission.READ,
);
expect(config.permissions.default.loggedIn).toEqual(
DefaultAccessPermission.READ,
);
expect(config.guestAccess).toEqual(guestAccess);
restore();
});
it('when no HD_MAX_DOCUMENT_LENGTH is set', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessPermission.READ,
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessPermission.READ,
HD_GUEST_ACCESS: guestAccess,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
const config = noteConfig();
expect(config.forbiddenNoteIds).toHaveLength(forbiddenNoteIds.length);
expect(config.forbiddenNoteIds).toEqual(forbiddenNoteIds);
expect(config.maxDocumentLength).toEqual(100000);
expect(config.permissions.default.everyone).toEqual(
DefaultAccessPermission.READ,
);
expect(config.permissions.default.loggedIn).toEqual(
DefaultAccessPermission.READ,
);
expect(config.guestAccess).toEqual(guestAccess);
restore();
});
it('when no HD_PERMISSION_DEFAULT_EVERYONE is set', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessPermission.READ,
HD_GUEST_ACCESS: guestAccess,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
const config = noteConfig();
expect(config.forbiddenNoteIds).toHaveLength(forbiddenNoteIds.length);
expect(config.forbiddenNoteIds).toEqual(forbiddenNoteIds);
expect(config.maxDocumentLength).toEqual(maxDocumentLength);
expect(config.permissions.default.everyone).toEqual(
DefaultAccessPermission.READ,
);
expect(config.permissions.default.loggedIn).toEqual(
DefaultAccessPermission.READ,
);
expect(config.guestAccess).toEqual(guestAccess);
restore();
});
it('when no HD_PERMISSION_DEFAULT_LOGGED_IN is set', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessPermission.READ,
HD_GUEST_ACCESS: guestAccess,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
const config = noteConfig();
expect(config.forbiddenNoteIds).toHaveLength(forbiddenNoteIds.length);
expect(config.forbiddenNoteIds).toEqual(forbiddenNoteIds);
expect(config.maxDocumentLength).toEqual(maxDocumentLength);
expect(config.permissions.default.everyone).toEqual(
DefaultAccessPermission.READ,
);
expect(config.permissions.default.loggedIn).toEqual(
DefaultAccessPermission.WRITE,
);
expect(config.guestAccess).toEqual(guestAccess);
restore();
});
it('when no HD_GUEST_ACCESS is set', () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessPermission.READ,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
const config = noteConfig();
expect(config.forbiddenNoteIds).toHaveLength(forbiddenNoteIds.length);
expect(config.forbiddenNoteIds).toEqual(forbiddenNoteIds);
expect(config.maxDocumentLength).toEqual(maxDocumentLength);
expect(config.permissions.default.everyone).toEqual(
DefaultAccessPermission.READ,
);
expect(config.permissions.default.loggedIn).toEqual(
DefaultAccessPermission.WRITE,
);
expect(config.guestAccess).toEqual(GuestAccess.WRITE);
restore();
});
});
describe('throws error', () => {
it('when given a non-valid HD_FORBIDDEN_NOTE_IDS', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_FORBIDDEN_NOTE_IDS: invalidforbiddenNoteIds.join(' , '),
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessPermission.READ,
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessPermission.READ,
HD_GUEST_ACCESS: guestAccess,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => noteConfig()).toThrow(
'"forbiddenNoteIds[0]" is not allowed to be empty',
);
restore();
});
it('when given a negative HD_MAX_DOCUMENT_LENGTH', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
HD_MAX_DOCUMENT_LENGTH: negativeMaxDocumentLength.toString(),
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessPermission.READ,
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessPermission.READ,
HD_GUEST_ACCESS: guestAccess,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => noteConfig()).toThrow(
'"HD_MAX_DOCUMENT_LENGTH" must be a positive number',
);
restore();
});
it('when given a non-integer HD_MAX_DOCUMENT_LENGTH', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
HD_MAX_DOCUMENT_LENGTH: floatMaxDocumentLength.toString(),
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessPermission.READ,
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessPermission.READ,
HD_GUEST_ACCESS: guestAccess,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => noteConfig()).toThrow(
'"HD_MAX_DOCUMENT_LENGTH" must be an integer',
);
restore();
});
it('when given a non-number HD_MAX_DOCUMENT_LENGTH', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
HD_MAX_DOCUMENT_LENGTH: invalidMaxDocumentLength,
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessPermission.READ,
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessPermission.READ,
HD_GUEST_ACCESS: guestAccess,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => noteConfig()).toThrow(
'"HD_MAX_DOCUMENT_LENGTH" must be a number',
);
restore();
});
it('when given a non-valid HD_PERMISSION_DEFAULT_EVERYONE', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
HD_PERMISSION_DEFAULT_EVERYONE: wrongDefaultPermission,
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessPermission.READ,
HD_GUEST_ACCESS: guestAccess,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => noteConfig()).toThrow(
'"HD_PERMISSION_DEFAULT_EVERYONE" must be one of [none, read, write]',
);
restore();
});
it('when given a non-valid HD_PERMISSION_DEFAULT_LOGGED_IN', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessPermission.READ,
HD_PERMISSION_DEFAULT_LOGGED_IN: wrongDefaultPermission,
HD_GUEST_ACCESS: guestAccess,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => noteConfig()).toThrow(
'"HD_PERMISSION_DEFAULT_LOGGED_IN" must be one of [none, read, write]',
);
restore();
});
it('when given a non-valid HD_GUEST_ACCESS', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessPermission.READ,
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessPermission.READ,
HD_GUEST_ACCESS: wrongDefaultPermission,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => noteConfig()).toThrow(
'"HD_GUEST_ACCESS" must be one of [deny, read, write, create]',
);
restore();
});
it('when HD_GUEST_ACCESS is set to deny and HD_PERMISSION_DEFAULT_EVERYONE is set', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessPermission.READ,
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessPermission.READ,
HD_GUEST_ACCESS: 'deny',
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => noteConfig()).toThrow(
`'HD_GUEST_ACCESS' is set to 'deny', but 'HD_PERMISSION_DEFAULT_EVERYONE' is also configured. Please remove 'HD_PERMISSION_DEFAULT_EVERYONE'.`,
);
restore();
});
it('when HD_PERMISSION_DEFAULT_EVERYONE is set to write, but HD_PERMISSION_DEFAULT_LOGGED_IN is set to read', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessPermission.WRITE,
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessPermission.READ,
HD_GUEST_ACCESS: guestAccess,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => noteConfig()).toThrow(
`'HD_PERMISSION_DEFAULT_EVERYONE' is set to '${DefaultAccessPermission.WRITE}', but 'HD_PERMISSION_DEFAULT_LOGGED_IN' is set to '${DefaultAccessPermission.READ}'. This gives everyone greater permissions than logged-in users which is not allowed.`,
);
restore();
});
it('when HD_PERMISSION_DEFAULT_EVERYONE is set to write, but HD_PERMISSION_DEFAULT_LOGGED_IN is set to none', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessPermission.WRITE,
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessPermission.NONE,
HD_GUEST_ACCESS: guestAccess,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => noteConfig()).toThrow(
`'HD_PERMISSION_DEFAULT_EVERYONE' is set to '${DefaultAccessPermission.WRITE}', but 'HD_PERMISSION_DEFAULT_LOGGED_IN' is set to '${DefaultAccessPermission.NONE}'. This gives everyone greater permissions than logged-in users which is not allowed.`,
);
restore();
});
it('when HD_PERMISSION_DEFAULT_EVERYONE is set to read, but HD_PERMISSION_DEFAULT_LOGGED_IN is set to none', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessPermission.READ,
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessPermission.NONE,
HD_GUEST_ACCESS: guestAccess,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => noteConfig()).toThrow(
`'HD_PERMISSION_DEFAULT_EVERYONE' is set to '${DefaultAccessPermission.READ}', but 'HD_PERMISSION_DEFAULT_LOGGED_IN' is set to '${DefaultAccessPermission.NONE}'. This gives everyone greater permissions than logged-in users which is not allowed.`,
);
restore();
});
});
});

View file

@ -0,0 +1,116 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { registerAs } from '@nestjs/config';
import * as Joi from 'joi';
import {
DefaultAccessPermission,
getDefaultAccessPermissionOrdinal,
} from './default-access-permission.enum';
import { GuestAccess } from './guest_access.enum';
import { buildErrorMessage, parseOptionalNumber, toArrayConfig } from './utils';
export interface NoteConfig {
forbiddenNoteIds: string[];
maxDocumentLength: number;
guestAccess: GuestAccess;
permissions: {
default: {
everyone: DefaultAccessPermission;
loggedIn: DefaultAccessPermission;
};
};
}
const schema = Joi.object<NoteConfig>({
forbiddenNoteIds: Joi.array()
.items(Joi.string())
.optional()
.default([])
.label('HD_FORBIDDEN_NOTE_IDS'),
maxDocumentLength: Joi.number()
.default(100000)
.positive()
.integer()
.optional()
.label('HD_MAX_DOCUMENT_LENGTH'),
guestAccess: Joi.string()
.valid(...Object.values(GuestAccess))
.optional()
.default(GuestAccess.WRITE)
.label('HD_GUEST_ACCESS'),
permissions: {
default: {
everyone: Joi.string()
.valid(...Object.values(DefaultAccessPermission))
.optional()
.default(DefaultAccessPermission.READ)
.label('HD_PERMISSION_DEFAULT_EVERYONE'),
loggedIn: Joi.string()
.valid(...Object.values(DefaultAccessPermission))
.optional()
.default(DefaultAccessPermission.WRITE)
.label('HD_PERMISSION_DEFAULT_LOGGED_IN'),
},
},
});
function checkEveryoneConfigIsConsistent(config: NoteConfig): void {
const everyoneDefaultSet =
process.env.HD_PERMISSION_DEFAULT_EVERYONE !== undefined;
if (config.guestAccess === GuestAccess.DENY && everyoneDefaultSet) {
throw new Error(
`'HD_GUEST_ACCESS' is set to '${config.guestAccess}', but 'HD_PERMISSION_DEFAULT_EVERYONE' is also configured. Please remove 'HD_PERMISSION_DEFAULT_EVERYONE'.`,
);
}
}
function checkLoggedInUsersHaveHigherDefaultPermissionsThanGuests(
config: NoteConfig,
): void {
const everyone = config.permissions.default.everyone;
const loggedIn = config.permissions.default.loggedIn;
if (
getDefaultAccessPermissionOrdinal(everyone) >
getDefaultAccessPermissionOrdinal(loggedIn)
) {
throw new Error(
`'HD_PERMISSION_DEFAULT_EVERYONE' is set to '${everyone}', but 'HD_PERMISSION_DEFAULT_LOGGED_IN' is set to '${loggedIn}'. This gives everyone greater permissions than logged-in users which is not allowed.`,
);
}
}
export default registerAs('noteConfig', () => {
const noteConfig = schema.validate(
{
forbiddenNoteIds: toArrayConfig(process.env.HD_FORBIDDEN_NOTE_IDS, ','),
maxDocumentLength: parseOptionalNumber(
process.env.HD_MAX_DOCUMENT_LENGTH,
),
guestAccess: process.env.HD_GUEST_ACCESS,
permissions: {
default: {
everyone: process.env.HD_PERMISSION_DEFAULT_EVERYONE,
loggedIn: process.env.HD_PERMISSION_DEFAULT_LOGGED_IN,
},
},
} as NoteConfig,
{
abortEarly: false,
presence: 'required',
},
);
if (noteConfig.error) {
const errorMessages = noteConfig.error.details.map(
(detail) => detail.message,
);
throw new Error(buildErrorMessage(errorMessages));
}
const config = noteConfig.value;
checkEveryoneConfigIsConsistent(config);
checkLoggedInUsersHaveHigherDefaultPermissionsThanGuests(config);
return config;
});

View file

@ -0,0 +1,119 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Loglevel } from './loglevel.enum';
import {
needToLog,
parseOptionalNumber,
replaceAuthErrorsWithEnvironmentVariables,
toArrayConfig,
} from './utils';
describe('config utils', () => {
describe('toArrayConfig', () => {
it('empty', () => {
expect(toArrayConfig('')).toEqual(undefined);
expect(toArrayConfig(undefined)).toEqual(undefined);
});
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('replaceAuthErrorsWithEnvironmentVariables', () => {
it('"gitlab[0].scope', () => {
expect(
replaceAuthErrorsWithEnvironmentVariables(
'"gitlab[0].scope',
'gitlab',
'HD_AUTH_GITLAB_',
['test'],
),
).toEqual('"HD_AUTH_GITLAB_test_SCOPE');
});
it('"ldap[0].url', () => {
expect(
replaceAuthErrorsWithEnvironmentVariables(
'"ldap[0].url',
'ldap',
'HD_AUTH_LDAP_',
['test'],
),
).toEqual('"HD_AUTH_LDAP_test_URL');
});
it('"ldap[0].url is not changed by gitlab call', () => {
expect(
replaceAuthErrorsWithEnvironmentVariables(
'"ldap[0].url',
'gitlab',
'HD_AUTH_GITLAB_',
['test'],
),
).toEqual('"ldap[0].url');
});
});
describe('needToLog', () => {
it('currentLevel ERROR', () => {
const currentLevel = Loglevel.ERROR;
expect(needToLog(currentLevel, Loglevel.ERROR)).toBeTruthy();
expect(needToLog(currentLevel, Loglevel.WARN)).toBeFalsy();
expect(needToLog(currentLevel, Loglevel.INFO)).toBeFalsy();
expect(needToLog(currentLevel, Loglevel.DEBUG)).toBeFalsy();
expect(needToLog(currentLevel, Loglevel.TRACE)).toBeFalsy();
});
it('currentLevel WARN', () => {
const currentLevel = Loglevel.WARN;
expect(needToLog(currentLevel, Loglevel.ERROR)).toBeTruthy();
expect(needToLog(currentLevel, Loglevel.WARN)).toBeTruthy();
expect(needToLog(currentLevel, Loglevel.INFO)).toBeFalsy();
expect(needToLog(currentLevel, Loglevel.DEBUG)).toBeFalsy();
expect(needToLog(currentLevel, Loglevel.TRACE)).toBeFalsy();
});
it('currentLevel INFO', () => {
const currentLevel = Loglevel.INFO;
expect(needToLog(currentLevel, Loglevel.ERROR)).toBeTruthy();
expect(needToLog(currentLevel, Loglevel.WARN)).toBeTruthy();
expect(needToLog(currentLevel, Loglevel.INFO)).toBeTruthy();
expect(needToLog(currentLevel, Loglevel.DEBUG)).toBeFalsy();
expect(needToLog(currentLevel, Loglevel.TRACE)).toBeFalsy();
});
it('currentLevel DEBUG', () => {
const currentLevel = Loglevel.DEBUG;
expect(needToLog(currentLevel, Loglevel.ERROR)).toBeTruthy();
expect(needToLog(currentLevel, Loglevel.WARN)).toBeTruthy();
expect(needToLog(currentLevel, Loglevel.INFO)).toBeTruthy();
expect(needToLog(currentLevel, Loglevel.DEBUG)).toBeTruthy();
expect(needToLog(currentLevel, Loglevel.TRACE)).toBeFalsy();
});
it('currentLevel TRACE', () => {
const currentLevel = Loglevel.TRACE;
expect(needToLog(currentLevel, Loglevel.ERROR)).toBeTruthy();
expect(needToLog(currentLevel, Loglevel.WARN)).toBeTruthy();
expect(needToLog(currentLevel, Loglevel.INFO)).toBeTruthy();
expect(needToLog(currentLevel, Loglevel.DEBUG)).toBeTruthy();
expect(needToLog(currentLevel, Loglevel.TRACE)).toBeTruthy();
});
});
describe('parseOptionalNumber', () => {
it('returns undefined on undefined parameter', () => {
expect(parseOptionalNumber(undefined)).toEqual(undefined);
});
it('correctly parses a integer string', () => {
expect(parseOptionalNumber('42')).toEqual(42);
});
it('correctly parses a float string', () => {
expect(parseOptionalNumber('3.14')).toEqual(3.14);
});
});
});

136
backend/src/config/utils.ts Normal file
View file

@ -0,0 +1,136 @@
/*
* SPDX-FileCopyrightText: 2022 The HedgeDoc developers (see AUTHORS file)
*
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { Loglevel } from './loglevel.enum';
export function toArrayConfig(
configValue?: string,
separator = ',',
): string[] | undefined {
if (!configValue) {
return undefined;
}
if (!configValue.includes(separator)) {
return [configValue.trim()];
}
return configValue.split(separator).map((arrayItem) => arrayItem.trim());
}
export function 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 function 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');
let newMessage = message.replace(
regex,
(_, index: number) => `"${replacement}${arrayOfNames[index]}.`,
);
if (newMessage != message) {
newMessage = newMessage.replace('.providerName', '_PROVIDER_NAME');
newMessage = newMessage.replace('.baseURL', '_BASE_URL');
newMessage = newMessage.replace('.clientID', '_CLIENT_ID');
newMessage = newMessage.replace('.clientSecret', '_CLIENT_SECRET');
newMessage = newMessage.replace('.scope', '_SCOPE');
newMessage = newMessage.replace('.version', '_GITLAB_VERSION');
newMessage = newMessage.replace('.url', '_URL');
newMessage = newMessage.replace('.bindDn', '_BIND_DN');
newMessage = newMessage.replace('.bindCredentials', '_BIND_CREDENTIALS');
newMessage = newMessage.replace('.searchBase', '_SEARCH_BASE');
newMessage = newMessage.replace('.searchFilter', '_SEARCH_FILTER');
newMessage = newMessage.replace('.searchAttributes', '_SEARCH_ATTRIBUTES');
newMessage = newMessage.replace('.userIdField', '_USER_ID_FIELD');
newMessage = newMessage.replace('.displayNameField', '_DISPLAY_NAME_FIELD');
newMessage = newMessage.replace(
'.profilePictureField',
'_PROFILE_PICTURE_FIELD',
);
newMessage = newMessage.replace('.tlsCaCerts', '_TLS_CERT_PATHS');
newMessage = newMessage.replace('.idpSsoUrl', '_IDP_SSO_URL');
newMessage = newMessage.replace('.idpCert', '_IDP_CERT');
newMessage = newMessage.replace('.clientCert', '_CLIENT_CERT');
newMessage = newMessage.replace('.issuer', '_ISSUER');
newMessage = newMessage.replace('.identifierFormat', '_IDENTIFIER_FORMAT');
newMessage = newMessage.replace(
'.disableRequestedAuthnContext',
'_DISABLE_REQUESTED_AUTHN_CONTEXT',
);
newMessage = newMessage.replace('.groupAttribute', '_GROUP_ATTRIBUTE');
newMessage = newMessage.replace('.requiredGroups', '_REQUIRED_GROUPS');
newMessage = newMessage.replace('.externalGroups', '_EXTERNAL_GROUPS');
newMessage = newMessage.replace('.attribute.id', '_ATTRIBUTE_ID');
newMessage = newMessage.replace(
'.attribute.username',
'_ATTRIBUTE_USERNAME',
);
newMessage = newMessage.replace('.attribute.local', '_ATTRIBUTE_LOCAL');
newMessage = newMessage.replace('.userProfileURL', '_USER_PROFILE_URL');
newMessage = newMessage.replace(
'.userProfileIdAttr',
'_USER_PROFILE_ID_ATTR',
);
newMessage = newMessage.replace(
'.userProfileUsernameAttr',
'_USER_PROFILE_USERNAME_ATTR',
);
newMessage = newMessage.replace(
'.userProfileDisplayNameAttr',
'_USER_PROFILE_DISPLAY_NAME_ATTR',
);
newMessage = newMessage.replace(
'.userProfileEmailAttr',
'_USER_PROFILE_EMAIL_ATTR',
);
newMessage = newMessage.replace('.tokenURL', '_TOKEN_URL');
newMessage = newMessage.replace('.authorizationURL', '_AUTHORIZATION_URL');
newMessage = newMessage.replace('.rolesClaim', '_ROLES_CLAIM');
newMessage = newMessage.replace('.accessRole', '_ACCESS_ROLE');
}
return newMessage;
}
export function needToLog(
currentLoglevel: Loglevel,
requestedLoglevel: Loglevel,
): boolean {
const current = transformLoglevelToInt(currentLoglevel);
const requested = transformLoglevelToInt(requestedLoglevel);
return current >= requested;
}
function transformLoglevelToInt(loglevel: Loglevel): number {
switch (loglevel) {
case Loglevel.TRACE:
return 5;
case Loglevel.DEBUG:
return 4;
case Loglevel.INFO:
return 3;
case Loglevel.WARN:
return 2;
case Loglevel.ERROR:
return 1;
}
}
export function parseOptionalNumber(value?: string): number | undefined {
if (value === undefined) {
return undefined;
}
return Number(value);
}