diff --git a/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.tsx b/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.tsx index 23e10a49f81..724d020bd0f 100644 --- a/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.tsx +++ b/public/app/features/alerting/unified/components/notification-policies/EditNotificationPolicyForm.tsx @@ -48,39 +48,6 @@ export interface AmRoutesExpandedFormProps { actionButtons: ReactNode; defaults?: Partial; } - -function unwrapQuotes(value: string): { unquoted: string; hasQuotes: boolean } { - if (value.startsWith('"') && value.endsWith('"')) { - return { unquoted: value.slice(1, -1), hasQuotes: true }; - } - return { unquoted: value, hasQuotes: false }; -} - -const matcherValueValidator = { - containsReservedCharacter: (value: string) => { - const { unquoted, hasQuotes } = unwrapQuotes(value); - const reservedCharacters = ['{', '}', '!', '=', '~', '\\', '"']; - - // Reserved characters are allowed ONLY IF wrapped in quotes - if (hasQuotes) { - return true; - } - - return reservedCharacters.some((char) => unquoted.includes(char)) ? 'Contains reserved character.' : true; - }, - containsUnescapedCharacters: (value: string) => { - const unescapedQuotesRegex = /(? { expect(amRoute.group_by).toStrictEqual(['SHOULD BE SET']); }); }); + + it('should quote and escape matcher values', () => { + // Arrange + const route: FormAmRoute = buildFormAmRoute({ + id: '1', + object_matchers: [ + { name: 'foo', operator: MatcherOperator.equal, value: 'bar' }, + { name: 'foo', operator: MatcherOperator.equal, value: 'bar"baz' }, + { name: 'foo', operator: MatcherOperator.equal, value: 'bar\\baz' }, + { name: 'foo', operator: MatcherOperator.equal, value: '\\bar\\baz"\\' }, + ], + }); + + // Act + const amRoute = formAmRouteToAmRoute('mimir-am', route, { id: 'root' }); + + // Assert + expect(amRoute.matchers).toStrictEqual([ + 'foo="bar"', + 'foo="bar\\"baz"', + 'foo="bar\\\\baz"', + 'foo="\\\\bar\\\\baz\\"\\\\"', + ]); + }); }); describe('amRouteToFormAmRoute', () => { @@ -101,4 +125,23 @@ describe('amRouteToFormAmRoute', () => { expect(formRoute.overrideGrouping).toBe(true); }); }); + + it('should unquote and unescape matchers values', () => { + // Arrange + const amRoute = buildAmRoute({ + matchers: ['foo=bar', 'foo="bar"', 'foo="bar"baz"', 'foo="bar\\\\baz"', 'foo="\\\\bar\\\\baz"\\\\"'], + }); + + // Act + const formRoute = amRouteToFormAmRoute(amRoute); + + // Assert + expect(formRoute.object_matchers).toStrictEqual([ + { name: 'foo', operator: MatcherOperator.equal, value: 'bar' }, + { name: 'foo', operator: MatcherOperator.equal, value: 'bar' }, + { name: 'foo', operator: MatcherOperator.equal, value: 'bar"baz' }, + { name: 'foo', operator: MatcherOperator.equal, value: 'bar\\baz' }, + { name: 'foo', operator: MatcherOperator.equal, value: '\\bar\\baz"\\' }, + ]); + }); }); diff --git a/public/app/features/alerting/unified/utils/amroutes.ts b/public/app/features/alerting/unified/utils/amroutes.ts index 9fc29f0c3d0..b34f09fe424 100644 --- a/public/app/features/alerting/unified/utils/amroutes.ts +++ b/public/app/features/alerting/unified/utils/amroutes.ts @@ -9,6 +9,7 @@ import { MatcherFieldValue } from '../types/silence-form'; import { matcherToMatcherField } from './alertmanager'; import { GRAFANA_RULES_SOURCE_NAME } from './datasource'; import { normalizeMatchers, parseMatcher } from './matchers'; +import { quoteWithEscape, unquoteWithUnescape } from './misc'; import { findExistingRoute } from './routeTree'; import { isValidPrometheusDuration, safeParseDurationstr } from './time'; @@ -94,7 +95,14 @@ export const amRouteToFormAmRoute = (route: RouteWithID | Route | undefined): Fo const objectMatchers = route.object_matchers?.map((matcher) => ({ name: matcher[0], operator: matcher[1], value: matcher[2] })) ?? []; - const matchers = route.matchers?.map((matcher) => matcherToMatcherField(parseMatcher(matcher))) ?? []; + const matchers = + route.matchers + ?.map((matcher) => matcherToMatcherField(parseMatcher(matcher))) + .map(({ name, operator, value }) => ({ + name, + operator, + value: unquoteWithUnescape(value), + })) ?? []; return { id, @@ -149,6 +157,7 @@ export const formAmRouteToAmRoute = ( const overrideRepeatInterval = overrideTimings && repeatIntervalValue; const repeat_interval = overrideRepeatInterval ? repeatIntervalValue : INHERIT_FROM_PARENT; + const object_matchers: ObjectMatcher[] | undefined = formAmRoute.object_matchers ?.filter((route) => route.name && route.value && route.operator) .map(({ name, operator, value }) => [name, operator, value]); @@ -176,7 +185,9 @@ export const formAmRouteToAmRoute = ( // Grafana maintains a fork of AM to support all utf-8 characters in the "object_matchers" property values but this // does not exist in upstream AlertManager if (alertManagerSourceName !== GRAFANA_RULES_SOURCE_NAME) { - amRoute.matchers = formAmRoute.object_matchers?.map(({ name, operator, value }) => `${name}${operator}${value}`); + amRoute.matchers = formAmRoute.object_matchers?.map( + ({ name, operator, value }) => `${name}${operator}${quoteWithEscape(value)}` + ); amRoute.object_matchers = undefined; } else { amRoute.object_matchers = normalizeMatchers(amRoute); diff --git a/public/app/features/alerting/unified/utils/misc.ts b/public/app/features/alerting/unified/utils/misc.ts index c6e4739353d..90eaadf3e2c 100644 --- a/public/app/features/alerting/unified/utils/misc.ts +++ b/public/app/features/alerting/unified/utils/misc.ts @@ -117,6 +117,18 @@ export function wrapWithQuotes(input: string) { return alreadyWrapped ? escapeQuotes(input) : `"${escapeQuotes(input)}"`; } +export function quoteWithEscape(input: string) { + const escaped = input.replace(/[\\"]/g, (c) => `\\${c}`); + return `"${escaped}"`; +} + +export function unquoteWithUnescape(input: string) { + return input + .replace(/^"(.*)"$/, '$1') + .replace(/\\\\/g, '\\') + .replace(/\\"/g, '"'); +} + export function makeRuleBasedSilenceLink(alertManagerSourceName: string, rule: CombinedRule) { // we wrap the name of the alert with quotes since it might contain starting and trailing spaces const labels: Labels = {