From 044e13fe686941786bc6a5efd73a5c823eedda08 Mon Sep 17 00:00:00 2001 From: gnuxie Date: Wed, 12 Jun 2024 16:43:54 +0100 Subject: [PATCH] Report validation errors. This has failed miserably because for some reason transform errors are reported differently to schema validation errors. They aren't treated like a TypeBox `ValueError`. `Config.util.extendDeep` also for some obscure reason introduces `null`s when objects such as `sentry` are undefined, so the schema had to be changed for that. --- src/config.ts | 149 ++++++++++++-------------------------------------- 1 file changed, 36 insertions(+), 113 deletions(-) diff --git a/src/config.ts b/src/config.ts index ed5f9d24..5e916e89 100644 --- a/src/config.ts +++ b/src/config.ts @@ -30,6 +30,11 @@ import { load } from "js-yaml"; import { MatrixClient, LogService } from "matrix-bot-sdk"; import Config from "config"; import path from "path"; +import { ConfigSchema } from "./ConfigSchema"; +import { ActionResult, DecodeException, Logger, StringRoomID, Value, isError } from "matrix-protection-suite"; +import { ValueError } from "@sinclair/typebox/errors" + +const log = new Logger('config'); /** * The configuration, as read from production.yaml @@ -39,106 +44,7 @@ import path from "path"; // The object is magically generated by external lib `config` // from the file specified by `NODE_ENV`, e.g. production.yaml // or harness.yaml. -export interface IConfig { - homeserverUrl: string; - rawHomeserverUrl: string; - accessToken: string; - pantalaimon: { - use: boolean; - username: string; - password: string; - }; - dataPath: string; - /** - * If true, Mjolnir will only accept invites from users present in managementRoom. - * Otherwise a space must be provided to `acceptInvitesFromSpace`. - */ - autojoinOnlyIfManager: boolean; - /** Mjolnir will accept invites from members of this space if `autojoinOnlyIfManager` is false. */ - acceptInvitesFromSpace: string; - recordIgnoredInvites: boolean; - managementRoom: string; - verboseLogging: boolean; - logLevel: "DEBUG" | "INFO" | "WARN" | "ERROR"; - logMutedModules: string[], - syncOnStartup: boolean; - verifyPermissionsOnStartup: boolean; - disableServerACL: boolean; - noop: boolean; - protectedRooms: string[]; // matrix.to urls - fasterMembershipChecks: boolean; - automaticallyRedactForReasons: string[]; // case-insensitive globs - protectAllJoinedRooms: boolean; - /** - * Backgrounded tasks: number of milliseconds to wait between the completion - * of one background task and the start of the next one. - */ - backgroundDelayMS: number; - pollReports: boolean; - /** - * Whether or not new reports, received either by webapi or polling, - * should be printed to our managementRoom. - */ - displayReports: boolean; - admin?: { - enableMakeRoomAdminCommand?: boolean; - } - commands: { - allowNoPrefix: boolean; - additionalPrefixes: string[]; - confirmWildcardBan: boolean; - features: string[]; - ban: { - defaultReasons: string[] - } - }; - protections: { - wordlist: { - words: string[]; - minutesBeforeTrusting: number; - }; - }; - health: { - healthz: { - enabled: boolean; - port: number; - address: string; - endpoint: string; - healthyStatus: number; - unhealthyStatus: number; - }; - // If specified, attempt to upload any crash statistics to sentry. - sentry?: { - dsn: string; - - // Frequency of performance monitoring. - // - // A number in [0.0, 1.0], where 0.0 means "don't bother with tracing" - // and 1.0 means "trace performance at every opportunity". - tracesSampleRate: number; - }; - }; - web: { - enabled: boolean; - port: number; - address: string; - abuseReporting: { - enabled: boolean; - } - }; - // Store room state using sqlite to improve startup time when Synapse responds - // slowly to requests for `/state`. - roomStateBackingStore: { - enabled?: boolean; - }; - // Experimental usage of the matrix-bot-sdk rust crypto. - // This can not be used with Pantalaimon. - experimentalRustCrypto: boolean; - - /** - * Config options only set at runtime. Try to avoid using the objects - * here as much as possible. - */ +export interface IConfig extends ConfigSchema { RUNTIME: { client?: MatrixClient; }; @@ -154,19 +60,14 @@ const defaultConfig: IConfig = { password: "", }, dataPath: "/data/storage", - acceptInvitesFromSpace: '!noop:example.org', + managementRoom: '!noop:example.org' as StringRoomID, + acceptInvitesFromSpace: '!noop:example.org' as StringRoomID, autojoinOnlyIfManager: true, recordIgnoredInvites: false, - managementRoom: "!noop:example.org", - verboseLogging: false, logLevel: "INFO", logMutedModules: ['MatrixHttpClient', 'MatrixClientLite'], - syncOnStartup: true, - verifyPermissionsOnStartup: true, noop: false, disableServerACL: false, - protectedRooms: [], - fasterMembershipChecks: false, automaticallyRedactForReasons: ["spam", "advertising"], protectAllJoinedRooms: false, backgroundDelayMS: 500, @@ -176,9 +77,6 @@ const defaultConfig: IConfig = { allowNoPrefix: false, additionalPrefixes: ["draupnir"], confirmWildcardBan: true, - features: [ - "synapse admin", - ], ban: { defaultReasons: [ "spam", @@ -203,6 +101,7 @@ const defaultConfig: IConfig = { healthyStatus: 200, unhealthyStatus: 418, }, + sentry: undefined, }, web: { enabled: false, @@ -229,19 +128,43 @@ export function getDefaultConfig(): IConfig { /** * @returns The users's raw config, deep copied over the `defaultConfig`. */ -function readConfigSource(): IConfig { +function readConfigSource(): Record { const explicitConfigPath = getCommandLineOption(process.argv, "--draupnir-config"); if (explicitConfigPath !== undefined) { const content = fs.readFileSync(explicitConfigPath, "utf8"); const parsed = load(content); return Config.util.extendDeep({}, defaultConfig, parsed); } else { - return Config.util.extendDeep({}, defaultConfig, Config.util.toObject()) as IConfig; + return Config.util.extendDeep({}, defaultConfig, Config.util.toObject()); } } +function readConfigSourceAndValidate(): ActionResult { + const source = readConfigSource(); + const decodedSource = Value.Decode(ConfigSchema, source, {suppressLogOnError: true}); + return decodedSource as ActionResult; +} + +function renderValidationError(error: ValueError): void { + log.error( + `Config value at path ${error.path} failed to validate with value:`, + error.value, + `and the following message:`, + error.message + ); + log.info(`Here is the description for ${error.path}:`, error.schema.description); +} + export function read(): IConfig { - const config = readConfigSource(); + const configResult = readConfigSourceAndValidate(); + if (isError(configResult)) { + configResult.error.errors.forEach(renderValidationError) + if (configResult.error.errors.length === 0) { + log.error(`Config error:`, configResult.error.exception); + } + throw new TypeError(`Config file failed validation`); + } + const config = configResult.ok; const explicitAccessTokenPath = getCommandLineOption(process.argv, "--access-token-path"); const explicitPantalaimonPasswordPath = getCommandLineOption(process.argv, "--pantalaimon-password-path"); if (explicitAccessTokenPath !== undefined) {