mirror of
https://github.com/hedgedoc/hedgedoc.git
synced 2025-05-24 03:57:06 -04:00
fix(repository): Move backend code into subdirectory
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
This commit is contained in:
parent
86584e705f
commit
bf30cbcf48
272 changed files with 87 additions and 67 deletions
288
backend/src/config/app.config.spec.ts
Normal file
288
backend/src/config/app.config.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
74
backend/src/config/app.config.ts
Normal file
74
backend/src/config/app.config.ts
Normal 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;
|
||||
});
|
522
backend/src/config/auth.config.spec.ts
Normal file
522
backend/src/config/auth.config.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
452
backend/src/config/auth.config.ts
Normal file
452
backend/src/config/auth.config.ts
Normal 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;
|
||||
});
|
39
backend/src/config/csp.config.ts
Normal file
39
backend/src/config/csp.config.ts
Normal 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;
|
||||
});
|
80
backend/src/config/customization.config.ts
Normal file
80
backend/src/config/customization.config.ts
Normal 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;
|
||||
});
|
12
backend/src/config/database-type.enum.ts
Normal file
12
backend/src/config/database-type.enum.ts
Normal 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',
|
||||
}
|
73
backend/src/config/database.config.ts
Normal file
73
backend/src/config/database.config.ts
Normal 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;
|
||||
});
|
26
backend/src/config/default-access-permission.enum.ts
Normal file
26
backend/src/config/default-access-permission.enum.ts
Normal 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');
|
||||
}
|
||||
}
|
49
backend/src/config/external-services.config.ts
Normal file
49
backend/src/config/external-services.config.ts
Normal 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;
|
||||
});
|
16
backend/src/config/gitlab.enum.ts
Normal file
16
backend/src/config/gitlab.enum.ts
Normal 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',
|
||||
}
|
27
backend/src/config/guest_access.enum.ts
Normal file
27
backend/src/config/guest_access.enum.ts
Normal 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');
|
||||
}
|
||||
}
|
51
backend/src/config/hsts.config.ts
Normal file
51
backend/src/config/hsts.config.ts
Normal 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;
|
||||
});
|
13
backend/src/config/loglevel.enum.ts
Normal file
13
backend/src/config/loglevel.enum.ts
Normal 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',
|
||||
}
|
369
backend/src/config/media.config.spec.ts
Normal file
369
backend/src/config/media.config.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
140
backend/src/config/media.config.ts
Normal file
140
backend/src/config/media.config.ts
Normal 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;
|
||||
});
|
28
backend/src/config/mock/app.config.mock.ts
Normal file
28
backend/src/config/mock/app.config.mock.ts
Normal 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());
|
57
backend/src/config/mock/auth.config.mock.ts
Normal file
57
backend/src/config/mock/auth.config.mock.ts
Normal 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());
|
37
backend/src/config/mock/customization.config.mock.ts
Normal file
37
backend/src/config/mock/customization.config.mock.ts
Normal 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(),
|
||||
);
|
30
backend/src/config/mock/database.config.mock.ts
Normal file
30
backend/src/config/mock/database.config.mock.ts
Normal 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());
|
30
backend/src/config/mock/external-services.config.mock.ts
Normal file
30
backend/src/config/mock/external-services.config.mock.ts
Normal 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(),
|
||||
);
|
48
backend/src/config/mock/media.config.mock.ts
Normal file
48
backend/src/config/mock/media.config.mock.ts
Normal 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());
|
33
backend/src/config/mock/note.config.mock.ts
Normal file
33
backend/src/config/mock/note.config.mock.ts
Normal 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());
|
458
backend/src/config/note.config.spec.ts
Normal file
458
backend/src/config/note.config.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
116
backend/src/config/note.config.ts
Normal file
116
backend/src/config/note.config.ts
Normal 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;
|
||||
});
|
119
backend/src/config/utils.spec.ts
Normal file
119
backend/src/config/utils.spec.ts
Normal 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
136
backend/src/config/utils.ts
Normal 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);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue