feat(config): add config vars for default permissions for special groups

Co-authored-by: Tilman Vatteroth <git@tilmanvatteroth.de>
Signed-off-by: Tilman Vatteroth <git@tilmanvatteroth.de>
Signed-off-by: Philip Molares <philip.molares@udo.edu>
This commit is contained in:
Philip Molares 2022-08-21 21:09:24 +02:00 committed by Yannick Bungers
parent 7dd093a44f
commit df976b5fe1
10 changed files with 518 additions and 36 deletions

View file

@ -19,20 +19,30 @@ We also provide an `.env.example` file containing a minimal configuration in the
## General
| environment variable | default | example | description |
|--------------------------|-----------|------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
| `HD_DOMAIN` | - | `https://md.example.com` | The URL the HedgeDoc instance runs on. |
| `PORT` | 3000 | | The port the HedgeDoc instance runs on. |
| `HD_RENDERER_ORIGIN` | HD_DOMAIN | | The URL the renderer runs on. If omitted this will be same as `HD_DOMAIN`. |
| `HD_LOGLEVEL` | warn | | The loglevel that should be used. Options are `error`, `warn`, `info`, `debug` or `trace`. |
| `HD_FORBIDDEN_NOTE_IDS` | - | `notAllowed, alsoNotAllowed` | A list of note ids (separated by `,`), that are not allowed to be created or requested by anyone. |
| `HD_MAX_DOCUMENT_LENGTH` | 100000 | | The maximum length of any one document. Changes to this will impact performance for your users. |
| `HD_PERSIST_INTERVAL` | 10 | `0`, `5`, `10`, `20` | The time interval in **minutes** for the periodic note revision creation during realtime editing. `0` deactivates the periodic note revision creation. |
| environment variable | default | example | description |
|--------------------------|-----------|-----------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
| `HD_DOMAIN` | - | `https://md.example.com` | The URL the HedgeDoc instance runs on. |
| `PORT` | 3000 | | The port the HedgeDoc instance runs on. |
| `HD_RENDERER_ORIGIN` | HD_DOMAIN | | The URL the renderer runs on. If omitted this will be same as `HD_DOMAIN`. |
| `HD_LOGLEVEL` | warn | | The loglevel that should be used. Options are `error`, `warn`, `info`, `debug` or `trace`. |
| `HD_FORBIDDEN_NOTE_IDS` | - | `notAllowed,alsoNotAllowed` | A list of note ids (separated by `,`), that are not allowed to be created or requested by anyone. |
| `HD_MAX_DOCUMENT_LENGTH` | 100000 | | The maximum length of any one document. Changes to this will impact performance for your users. |
| `HD_PERSIST_INTERVAL` | 10 | `0`, `5`, `10`, `20` | The time interval in **minutes** for the periodic note revision creation during realtime editing. `0` deactivates the periodic note revision creation. |
### Why should I want to run my renderer on a different (sub-)domain?
If the renderer is provided by another domain, it's way harder to manipulate HedgeDoc or steal credentials from the rendered note content, because renderer and editor are more isolated. This increases the security of the software and greatly mitigates [XSS attacks](https://en.wikipedia.org/wiki/Cross-site_scripting). However, you can run HedgeDoc without this extra security, but we recommend using it if possible.
## Notes
| environment variable | default | example | description |
|------------------------------------------|---------|-----------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `HD_FORBIDDEN_NOTE_IDS` | - | `notAllowed, alsoNotAllowed` | A list of note ids (separated by `,`), that are not allowed to be created or requested by anyone. |
| `HD_MAX_DOCUMENT_LENGTH` | 100000 | | The maximum length of any one document. Changes to this will impact performance for your users. |
| `HD_GUEST_ACCESS` | `write` | `deny`, `read`, `write`, `create` | Defines the maximum access level for guest users to the instance. If guest access is set lower than the "everyone" permission of a note then the note permission will be overridden. |
| `HD_PERMISSION_LOGGED_IN_DEFAULT_ACCESS` | `write` | `none, read, write` | The default permission for the "logged-in" group that is set on new notes. |
| `HD_PERMISSION_EVERYONE_DEFAULT_ACCESS` | `read` | `none, read, write` | The default permission for the "everyone" group (logged-in & guest users), that is set on new notes created by logged-in users. Notes created by guests always set this to "write". |
## Authentication
**ToDo:** Add Authentication docs

View file

@ -8,7 +8,7 @@ NestJS - the framework we use - is reading the variables from the environment an
## How the config code works
The config of HedgeDoc is split up into **eight** different modules:
The config of HedgeDoc is split up into **nine** different modules:
`app.config.ts`
: General configuration of the app
@ -34,6 +34,9 @@ The config of HedgeDoc is split up into **eight** different modules:
`media.config.ts`
: Where media files are being stored
`note.config.ts`
: Configuration for notes
Each of those files (except `auth.config.ts` which is discussed later) consists of three parts:
1. An interface

View file

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

View file

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

View file

@ -6,12 +6,21 @@
import { ConfigFactoryKeyHost, registerAs } from '@nestjs/config';
import { ConfigFactory } from '@nestjs/config/dist/interfaces';
import { DefaultAccessPermission } from '../default-access-permission.enum';
import { DefaultAccessPermission } from '../default-access-permission.enum';
import { NoteConfig } from '../note.config';
import { GuestAccess } from '../guest_access.enum';
export function createDefaultMockNoteConfig(): NoteConfig {
return {
maxDocumentLength: 100000,
forbiddenNoteIds: ['forbiddenNoteId'],
permissions: {
default: {
everyone: DefaultAccessPermission.READ,
loggedIn: DefaultAccessPermission.WRITE,
},
},
};
}

View file

@ -5,6 +5,8 @@
*/
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', () => {
@ -15,14 +17,19 @@ describe('noteConfig', () => {
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', async () => {
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 */
},
{
@ -33,6 +40,13 @@ describe('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();
});
@ -41,6 +55,9 @@ describe('noteConfig', () => {
{
/* 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 */
},
{
@ -50,24 +67,13 @@ describe('noteConfig', () => {
const config = noteConfig();
expect(config.forbiddenNoteIds).toHaveLength(0);
expect(config.maxDocumentLength).toEqual(maxDocumentLength);
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(' , '),
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
expect(config.permissions.default.everyone).toEqual(
DefaultAccessPermission.READ,
);
const config = noteConfig();
expect(config.forbiddenNoteIds).toHaveLength(forbiddenNoteIds.length);
expect(config.forbiddenNoteIds).toEqual(forbiddenNoteIds);
expect(config.maxDocumentLength).toEqual(100000);
expect(config.permissions.default.loggedIn).toEqual(
DefaultAccessPermission.READ,
);
expect(config.guestAccess).toEqual(guestAccess);
restore();
});
@ -77,6 +83,9 @@ describe('noteConfig', () => {
/* 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 */
},
{
@ -87,9 +96,133 @@ describe('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(
@ -97,6 +230,9 @@ describe('noteConfig', () => {
/* 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 */
},
{
@ -115,6 +251,9 @@ describe('noteConfig', () => {
/* 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 */
},
{
@ -133,6 +272,9 @@ describe('noteConfig', () => {
/* 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 */
},
{
@ -151,6 +293,9 @@ describe('noteConfig', () => {
/* 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 */
},
{
@ -162,5 +307,152 @@ describe('noteConfig', () => {
);
restore();
});
it('when given a non-valid HD_PERMISSION_DEFAULT_EVERYONE', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
HD_PERMISSION_DEFAULT_EVERYONE: wrongDefaultPermission,
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessPermission.READ,
HD_GUEST_ACCESS: guestAccess,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => noteConfig()).toThrow(
'"HD_PERMISSION_DEFAULT_EVERYONE" must be one of [none, read, write]',
);
restore();
});
it('when given a non-valid HD_PERMISSION_DEFAULT_LOGGED_IN', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessPermission.READ,
HD_PERMISSION_DEFAULT_LOGGED_IN: wrongDefaultPermission,
HD_GUEST_ACCESS: guestAccess,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => noteConfig()).toThrow(
'"HD_PERMISSION_DEFAULT_LOGGED_IN" must be one of [none, read, write]',
);
restore();
});
it('when given a non-valid HD_GUEST_ACCESS', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessPermission.READ,
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessPermission.READ,
HD_GUEST_ACCESS: wrongDefaultPermission,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => noteConfig()).toThrow(
'"HD_GUEST_ACCESS" must be one of [deny, read, write, create]',
);
restore();
});
it('when HD_GUEST_ACCESS is set to deny and HD_PERMISSION_DEFAULT_EVERYONE is set', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessPermission.READ,
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessPermission.READ,
HD_GUEST_ACCESS: 'deny',
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => noteConfig()).toThrow(
`'HD_GUEST_ACCESS' is set to 'deny', but 'HD_PERMISSION_DEFAULT_EVERYONE' is also configured. Please remove 'HD_PERMISSION_DEFAULT_EVERYONE'.`,
);
restore();
});
it('when HD_PERMISSION_DEFAULT_EVERYONE is set to write, but HD_PERMISSION_DEFAULT_LOGGED_IN is set to read', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessPermission.WRITE,
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessPermission.READ,
HD_GUEST_ACCESS: guestAccess,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => noteConfig()).toThrow(
`'HD_PERMISSION_DEFAULT_EVERYONE' is set to '${DefaultAccessPermission.WRITE}', but 'HD_PERMISSION_DEFAULT_LOGGED_IN' is set to '${DefaultAccessPermission.READ}'. This gives everyone greater permissions than logged-in users which is not allowed.`,
);
restore();
});
it('when HD_PERMISSION_DEFAULT_EVERYONE is set to write, but HD_PERMISSION_DEFAULT_LOGGED_IN is set to none', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessPermission.WRITE,
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessPermission.NONE,
HD_GUEST_ACCESS: guestAccess,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => noteConfig()).toThrow(
`'HD_PERMISSION_DEFAULT_EVERYONE' is set to '${DefaultAccessPermission.WRITE}', but 'HD_PERMISSION_DEFAULT_LOGGED_IN' is set to '${DefaultAccessPermission.NONE}'. This gives everyone greater permissions than logged-in users which is not allowed.`,
);
restore();
});
it('when HD_PERMISSION_DEFAULT_EVERYONE is set to read, but HD_PERMISSION_DEFAULT_LOGGED_IN is set to none', async () => {
const restore = mockedEnv(
{
/* eslint-disable @typescript-eslint/naming-convention */
HD_FORBIDDEN_NOTE_IDS: forbiddenNoteIds.join(' , '),
HD_MAX_DOCUMENT_LENGTH: maxDocumentLength.toString(),
HD_PERMISSION_DEFAULT_EVERYONE: DefaultAccessPermission.READ,
HD_PERMISSION_DEFAULT_LOGGED_IN: DefaultAccessPermission.NONE,
HD_GUEST_ACCESS: guestAccess,
/* eslint-enable @typescript-eslint/naming-convention */
},
{
clear: true,
},
);
expect(() => noteConfig()).toThrow(
`'HD_PERMISSION_DEFAULT_EVERYONE' is set to '${DefaultAccessPermission.READ}', but 'HD_PERMISSION_DEFAULT_LOGGED_IN' is set to '${DefaultAccessPermission.NONE}'. This gives everyone greater permissions than logged-in users which is not allowed.`,
);
restore();
});
});
});

View file

@ -6,14 +6,26 @@
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({
const schema = Joi.object<NoteConfig>({
forbiddenNoteIds: Joi.array()
.items(Joi.string())
.optional()
@ -25,8 +37,52 @@ const schema = Joi.object({
.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(
{
@ -34,7 +90,14 @@ export default registerAs('noteConfig', () => {
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',
@ -46,5 +109,8 @@ export default registerAs('noteConfig', () => {
);
throw new Error(buildErrorMessage(errorMessages));
}
return noteConfig.value as NoteConfig;
const config = noteConfig.value;
checkEveryoneConfigIsConsistent(config);
checkLoggedInUsersHaveHigherDefaultPermissionsThanGuests(config);
return config;
});

View file

@ -10,6 +10,7 @@ import { URL } from 'url';
import { AppConfig } from '../config/app.config';
import { AuthConfig } from '../config/auth.config';
import { CustomizationConfig } from '../config/customization.config';
import { DefaultAccessPermission } from '../config/default-access-permission.enum';
import { ExternalServicesConfig } from '../config/external-services.config';
import { GitlabScope, GitlabVersion } from '../config/gitlab.enum';
import { Loglevel } from '../config/loglevel.enum';
@ -191,7 +192,14 @@ describe('FrontendConfigService', () => {
return {
forbiddenNoteIds: [],
maxDocumentLength: 200,
};
guestAccess: true,
permissions: {
default: {
everyone: DefaultAccessPermission.READ,
loggedIn: DefaultAccessPermission.WRITE,
},
},
} as NoteConfig;
}),
],
}),
@ -350,6 +358,13 @@ describe('FrontendConfigService', () => {
const noteConfig: NoteConfig = {
forbiddenNoteIds: [],
maxDocumentLength: maxDocumentLength,
guestAccess: true,
permissions: {
default: {
everyone: DefaultAccessPermission.READ,
loggedIn: DefaultAccessPermission.WRITE,
},
},
};
const module: TestingModule = await Test.createTestingModule({
imports: [
@ -377,8 +392,7 @@ describe('FrontendConfigService', () => {
const service = module.get(FrontendConfigService);
const config = await service.getFrontendConfig();
expect(config.allowRegister).toEqual(enableRegister);
expect(config.allowAnonymous).toEqual(false);
expect(config.allowAnonymous).toEqual(noteConfig.guestAccess);
expect(config.branding.name).toEqual(customName);
expect(config.branding.logo).toEqual(
customLogo ? new URL(customLogo) : undefined,

View file

@ -46,8 +46,7 @@ export class FrontendConfigService {
async getFrontendConfig(): Promise<FrontendConfigDto> {
return {
// ToDo: use actual value here
allowAnonymous: false,
allowAnonymous: this.noteConfig.guestAccess,
allowRegister: this.authConfig.local.enableRegister,
authProviders: this.getAuthProviders(),
branding: this.getBranding(),

View file

@ -96,6 +96,26 @@ export class NotesService {
HistoryEntry.create(owner, newNote as Note) as HistoryEntry,
]);
}
const everyonePermission = this.createGroupPermission(
newNote as Note,
await this.groupsService.getEveryoneGroup(),
owner === null
? DefaultAccessPermission.WRITE
: this.noteConfig.permissions.default.everyone,
);
const loggedInPermission = this.createGroupPermission(
newNote as Note,
await this.groupsService.getLoggedInGroup(),
this.noteConfig.permissions.default.loggedIn,
);
newNote.groupPermissions = Promise.resolve([
...Optional.ofNullable(everyonePermission).wrapInArray(),
...Optional.ofNullable(loggedInPermission).wrapInArray(),
]);
try {
return await this.noteRepository.save(newNote);
} catch (e) {
@ -113,6 +133,20 @@ export class NotesService {
}
}
private createGroupPermission(
note: Note,
group: Group,
permission: DefaultAccessPermission,
): NoteGroupPermission | null {
return permission === DefaultAccessPermission.NONE
? null
: NoteGroupPermission.create(
group,
note,
permission === DefaultAccessPermission.WRITE,
);
}
/**
* @async
* Get the current content of the note.