diff --git a/backend/src/configValidator.ts b/backend/src/configValidator.ts index 5650238d..9ea4e804 100644 --- a/backend/src/configValidator.ts +++ b/backend/src/configValidator.ts @@ -37,7 +37,7 @@ export async function validateGuildConfig(config: any): Promise { const plugin = pluginNameToPlugin.get(pluginName)!; try { const mergedOptions = configUtils.mergeConfig(plugin.defaultOptions || {}, pluginOptions); - await plugin.configPreprocessor?.((mergedOptions as unknown) as PluginOptions); + await plugin.configPreprocessor?.((mergedOptions as unknown) as PluginOptions, true); } catch (err) { if (err instanceof ConfigValidationError || err instanceof StrictValidationError) { return `${pluginName}: ${err.message}`; diff --git a/backend/src/pluginUtils.ts b/backend/src/pluginUtils.ts index 7bc0fbb0..f1ea4616 100644 --- a/backend/src/pluginUtils.ts +++ b/backend/src/pluginUtils.ts @@ -54,6 +54,19 @@ const PluginOverrideCriteriaType: t.Type> = t.re }), ); +const validTopLevelOverrideKeys = [ + "channel", + "category", + "level", + "user", + "role", + "all", + "any", + "not", + "extra", + "config", +]; + const BasicPluginStructureType = t.type({ enabled: tNullable(t.boolean), config: tNullable(t.unknown), @@ -74,7 +87,7 @@ export function getPluginConfigPreprocessor( blueprint: ZeppelinPlugin, customPreprocessor?: ZeppelinPlugin["configPreprocessor"], ) { - return async (options: PluginOptions) => { + return async (options: PluginOptions, strict?: boolean) => { // 1. Validate the basic structure of plugin config const basicOptionsValidation = validate(BasicPluginStructureType, options); if (basicOptionsValidation instanceof StrictValidationError) { @@ -93,8 +106,32 @@ export function getPluginConfigPreprocessor( if (options.overrides) { for (const override of options.overrides) { - const partialOverrideConfigValidation = validate(partialConfigSchema, override.config || {}); - if (partialOverrideConfigValidation) { + // Validate criteria and extra criteria + // FIXME: This is ugly + for (const key of Object.keys(override)) { + if (!validTopLevelOverrideKeys.includes(key)) { + if (strict) { + throw new ConfigValidationError(`Unknown override criterion '${key}'`); + } + + delete override[key]; + } + } + if (override.extra != null) { + for (const extraCriterion of Object.keys(override.extra)) { + if (!blueprint.customOverrideCriteriaFunctions?.[extraCriterion]) { + if (strict) { + throw new ConfigValidationError(`Unknown override extra criterion '${extraCriterion}'`); + } + + delete override.extra[extraCriterion]; + } + } + } + + // Validate override config + const partialOverrideConfigValidation = decodeAndValidateStrict(partialConfigSchema, override.config || {}); + if (partialOverrideConfigValidation instanceof StrictValidationError) { throw strictValidationErrorToConfigValidationError(partialOverrideConfigValidation); } } diff --git a/backend/src/plugins/ZeppelinPluginBlueprint.ts b/backend/src/plugins/ZeppelinPluginBlueprint.ts index 1f289e7a..f55bd0c6 100644 --- a/backend/src/plugins/ZeppelinPluginBlueprint.ts +++ b/backend/src/plugins/ZeppelinPluginBlueprint.ts @@ -10,6 +10,8 @@ import { import * as t from "io-ts"; import { getPluginConfigPreprocessor } from "../pluginUtils"; import { TMarkdown } from "../types"; +import { Awaitable } from "knub/dist/utils"; +import { PluginOptions } from "knub/dist/config/configTypes"; /** * GUILD PLUGINS @@ -26,6 +28,11 @@ export interface ZeppelinGuildPluginBlueprint, + strict?: boolean, + ) => Awaitable>; } export function zeppelinGuildPlugin( @@ -59,6 +66,7 @@ export function zeppelinGuildPlugin(...args) { export interface ZeppelinGlobalPluginBlueprint extends GlobalPluginBlueprint> { configSchema: t.TypeC; + configPreprocessor?: (options: PluginOptions, strict?: boolean) => Awaitable>; } export function zeppelinGlobalPlugin( diff --git a/backend/src/validatorUtils.ts b/backend/src/validatorUtils.ts index 5292ca96..afc1297f 100644 --- a/backend/src/validatorUtils.ts +++ b/backend/src/validatorUtils.ts @@ -111,7 +111,11 @@ export function validate(schema: t.Type, value: any): StrictValidationError * Decodes and validates the given value against the given schema while also disallowing extra properties * See: https://github.com/gcanti/io-ts/issues/322 */ -export function decodeAndValidateStrict(schema: T, value: any): StrictValidationError | any { +export function decodeAndValidateStrict( + schema: T, + value: any, + debug = false, +): StrictValidationError | any { const validationResult = t.exact(schema).decode(value); return pipe( validationResult, @@ -119,6 +123,14 @@ export function decodeAndValidateStrict(schema: T, value: err => report(validationResult), result => { // Make sure there are no extra properties + if (debug) + console.log( + "JSON.stringify() check:", + JSON.stringify(value) === JSON.stringify(result) + ? "they are the same, no excess" + : "they are not the same, might have excess", + result, + ); if (JSON.stringify(value) !== JSON.stringify(result)) { const diff = deepDiff(result, value); const errors = diff.filter(d => d.kind === "N").map(d => `Unknown property <${d.path.join(".")}>`);