Add double quotes wrapping and escaping on displating matcher form inputs
This commit is contained in:
-34
@@ -48,39 +48,6 @@ export interface AmRoutesExpandedFormProps {
|
||||
actionButtons: ReactNode;
|
||||
defaults?: Partial<FormAmRoute>;
|
||||
}
|
||||
|
||||
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 = /(?<!\\)"/g;
|
||||
const unescapedBackslashRegex = /(?<!\\)\\[^\\"]/g;
|
||||
const { unquoted } = unwrapQuotes(value);
|
||||
|
||||
return unescapedQuotesRegex.test(unquoted)
|
||||
? 'Contains unescaped double quotes.'
|
||||
: unescapedBackslashRegex.test(unquoted)
|
||||
? 'Contains unescaped backslash.'
|
||||
: true;
|
||||
},
|
||||
};
|
||||
|
||||
export const AmRoutesExpandedForm = ({
|
||||
actionButtons,
|
||||
receivers,
|
||||
@@ -170,7 +137,6 @@ export const AmRoutesExpandedForm = ({
|
||||
<Input
|
||||
{...register(`object_matchers.${index}.value`, {
|
||||
required: 'Field is required',
|
||||
validate: matcherValueValidator,
|
||||
})}
|
||||
defaultValue={field.value}
|
||||
placeholder="value"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Route } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { MatcherOperator, Route } from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { FormAmRoute } from '../types/amroutes';
|
||||
|
||||
@@ -53,6 +53,30 @@ describe('formAmRouteToAmRoute', () => {
|
||||
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"\\' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user