[alerting] encode rule/connector ids in http requests made from alerting UI (#97854)

resolves: https://github.com/elastic/kibana/issues/97852

Adds `encodeURIComponent()` wrappers around references to rule, alert, and
connector ids.  Without this fix, if an alert id (which can contain
customer-generated data) contains a character that needs to be URL
encoded, the resulting API call from the web UI will fail.
This commit is contained in:
Patrick Mueller 2021-04-23 15:50:24 -04:00 committed by GitHub
parent ff9eb3b6d5
commit 2ebb308753
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 168 additions and 125 deletions

View file

@ -99,10 +99,10 @@ describe('Jira API', () => {
test('should call get issue types API', async () => { test('should call get issue types API', async () => {
const abortCtrl = new AbortController(); const abortCtrl = new AbortController();
http.post.mockResolvedValueOnce(issueTypesResponse); http.post.mockResolvedValueOnce(issueTypesResponse);
const res = await getIssueTypes({ http, signal: abortCtrl.signal, connectorId: 'test' }); const res = await getIssueTypes({ http, signal: abortCtrl.signal, connectorId: 'te/st' });
expect(res).toEqual(issueTypesResponse); expect(res).toEqual(issueTypesResponse);
expect(http.post).toHaveBeenCalledWith('/api/actions/connector/test/_execute', { expect(http.post).toHaveBeenCalledWith('/api/actions/connector/te%2Fst/_execute', {
body: '{"params":{"subAction":"issueTypes","subActionParams":{}}}', body: '{"params":{"subAction":"issueTypes","subActionParams":{}}}',
signal: abortCtrl.signal, signal: abortCtrl.signal,
}); });
@ -116,12 +116,12 @@ describe('Jira API', () => {
const res = await getFieldsByIssueType({ const res = await getFieldsByIssueType({
http, http,
signal: abortCtrl.signal, signal: abortCtrl.signal,
connectorId: 'test', connectorId: 'te/st',
id: '10006', id: '10006',
}); });
expect(res).toEqual(fieldsResponse); expect(res).toEqual(fieldsResponse);
expect(http.post).toHaveBeenCalledWith('/api/actions/connector/test/_execute', { expect(http.post).toHaveBeenCalledWith('/api/actions/connector/te%2Fst/_execute', {
body: '{"params":{"subAction":"fieldsByIssueType","subActionParams":{"id":"10006"}}}', body: '{"params":{"subAction":"fieldsByIssueType","subActionParams":{"id":"10006"}}}',
signal: abortCtrl.signal, signal: abortCtrl.signal,
}); });
@ -135,12 +135,12 @@ describe('Jira API', () => {
const res = await getIssues({ const res = await getIssues({
http, http,
signal: abortCtrl.signal, signal: abortCtrl.signal,
connectorId: 'test', connectorId: 'te/st',
title: 'test issue', title: 'test issue',
}); });
expect(res).toEqual(issuesResponse); expect(res).toEqual(issuesResponse);
expect(http.post).toHaveBeenCalledWith('/api/actions/connector/test/_execute', { expect(http.post).toHaveBeenCalledWith('/api/actions/connector/te%2Fst/_execute', {
body: '{"params":{"subAction":"issues","subActionParams":{"title":"test issue"}}}', body: '{"params":{"subAction":"issues","subActionParams":{"title":"test issue"}}}',
signal: abortCtrl.signal, signal: abortCtrl.signal,
}); });
@ -154,12 +154,12 @@ describe('Jira API', () => {
const res = await getIssue({ const res = await getIssue({
http, http,
signal: abortCtrl.signal, signal: abortCtrl.signal,
connectorId: 'test', connectorId: 'te/st',
id: 'RJ-107', id: 'RJ-107',
}); });
expect(res).toEqual(issuesResponse); expect(res).toEqual(issuesResponse);
expect(http.post).toHaveBeenCalledWith('/api/actions/connector/test/_execute', { expect(http.post).toHaveBeenCalledWith('/api/actions/connector/te%2Fst/_execute', {
body: '{"params":{"subAction":"issue","subActionParams":{"id":"RJ-107"}}}', body: '{"params":{"subAction":"issue","subActionParams":{"id":"RJ-107"}}}',
signal: abortCtrl.signal, signal: abortCtrl.signal,
}); });

View file

@ -17,12 +17,15 @@ export async function getIssueTypes({
signal: AbortSignal; signal: AbortSignal;
connectorId: string; connectorId: string;
}): Promise<Record<string, any>> { }): Promise<Record<string, any>> {
return await http.post(`${BASE_ACTION_API_PATH}/connector/${connectorId}/_execute`, { return await http.post(
body: JSON.stringify({ `${BASE_ACTION_API_PATH}/connector/${encodeURIComponent(connectorId)}/_execute`,
params: { subAction: 'issueTypes', subActionParams: {} }, {
}), body: JSON.stringify({
signal, params: { subAction: 'issueTypes', subActionParams: {} },
}); }),
signal,
}
);
} }
export async function getFieldsByIssueType({ export async function getFieldsByIssueType({
@ -36,12 +39,15 @@ export async function getFieldsByIssueType({
connectorId: string; connectorId: string;
id: string; id: string;
}): Promise<Record<string, any>> { }): Promise<Record<string, any>> {
return await http.post(`${BASE_ACTION_API_PATH}/connector/${connectorId}/_execute`, { return await http.post(
body: JSON.stringify({ `${BASE_ACTION_API_PATH}/connector/${encodeURIComponent(connectorId)}/_execute`,
params: { subAction: 'fieldsByIssueType', subActionParams: { id } }, {
}), body: JSON.stringify({
signal, params: { subAction: 'fieldsByIssueType', subActionParams: { id } },
}); }),
signal,
}
);
} }
export async function getIssues({ export async function getIssues({
@ -55,12 +61,15 @@ export async function getIssues({
connectorId: string; connectorId: string;
title: string; title: string;
}): Promise<Record<string, any>> { }): Promise<Record<string, any>> {
return await http.post(`${BASE_ACTION_API_PATH}/connector/${connectorId}/_execute`, { return await http.post(
body: JSON.stringify({ `${BASE_ACTION_API_PATH}/connector/${encodeURIComponent(connectorId)}/_execute`,
params: { subAction: 'issues', subActionParams: { title } }, {
}), body: JSON.stringify({
signal, params: { subAction: 'issues', subActionParams: { title } },
}); }),
signal,
}
);
} }
export async function getIssue({ export async function getIssue({
@ -74,10 +83,13 @@ export async function getIssue({
connectorId: string; connectorId: string;
id: string; id: string;
}): Promise<Record<string, any>> { }): Promise<Record<string, any>> {
return await http.post(`${BASE_ACTION_API_PATH}/connector/${connectorId}/_execute`, { return await http.post(
body: JSON.stringify({ `${BASE_ACTION_API_PATH}/connector/${encodeURIComponent(connectorId)}/_execute`,
params: { subAction: 'issue', subActionParams: { id } }, {
}), body: JSON.stringify({
signal, params: { subAction: 'issue', subActionParams: { id } },
}); }),
signal,
}
);
} }

View file

@ -32,7 +32,7 @@ const incidentTypesResponse = {
{ id: 16, name: 'TBD / Unknown' }, { id: 16, name: 'TBD / Unknown' },
{ id: 15, name: 'Vendor / 3rd party error' }, { id: 15, name: 'Vendor / 3rd party error' },
], ],
actionId: 'test', actionId: 'te/st',
}; };
const severityResponse = { const severityResponse = {
@ -42,7 +42,7 @@ const severityResponse = {
{ id: 5, name: 'Medium' }, { id: 5, name: 'Medium' },
{ id: 6, name: 'High' }, { id: 6, name: 'High' },
], ],
actionId: 'test', actionId: 'te/st',
}; };
describe('Resilient API', () => { describe('Resilient API', () => {
@ -57,11 +57,11 @@ describe('Resilient API', () => {
const res = await getIncidentTypes({ const res = await getIncidentTypes({
http, http,
signal: abortCtrl.signal, signal: abortCtrl.signal,
connectorId: 'test', connectorId: 'te/st',
}); });
expect(res).toEqual(incidentTypesResponse); expect(res).toEqual(incidentTypesResponse);
expect(http.post).toHaveBeenCalledWith('/api/actions/connector/test/_execute', { expect(http.post).toHaveBeenCalledWith('/api/actions/connector/te%2Fst/_execute', {
body: '{"params":{"subAction":"incidentTypes","subActionParams":{}}}', body: '{"params":{"subAction":"incidentTypes","subActionParams":{}}}',
signal: abortCtrl.signal, signal: abortCtrl.signal,
}); });
@ -75,11 +75,11 @@ describe('Resilient API', () => {
const res = await getSeverity({ const res = await getSeverity({
http, http,
signal: abortCtrl.signal, signal: abortCtrl.signal,
connectorId: 'test', connectorId: 'te/st',
}); });
expect(res).toEqual(severityResponse); expect(res).toEqual(severityResponse);
expect(http.post).toHaveBeenCalledWith('/api/actions/connector/test/_execute', { expect(http.post).toHaveBeenCalledWith('/api/actions/connector/te%2Fst/_execute', {
body: '{"params":{"subAction":"severity","subActionParams":{}}}', body: '{"params":{"subAction":"severity","subActionParams":{}}}',
signal: abortCtrl.signal, signal: abortCtrl.signal,
}); });

View file

@ -17,12 +17,15 @@ export async function getIncidentTypes({
signal: AbortSignal; signal: AbortSignal;
connectorId: string; connectorId: string;
}): Promise<Record<string, any>> { }): Promise<Record<string, any>> {
return await http.post(`${BASE_ACTION_API_PATH}/connector/${connectorId}/_execute`, { return await http.post(
body: JSON.stringify({ `${BASE_ACTION_API_PATH}/connector/${encodeURIComponent(connectorId)}/_execute`,
params: { subAction: 'incidentTypes', subActionParams: {} }, {
}), body: JSON.stringify({
signal, params: { subAction: 'incidentTypes', subActionParams: {} },
}); }),
signal,
}
);
} }
export async function getSeverity({ export async function getSeverity({
@ -34,10 +37,13 @@ export async function getSeverity({
signal: AbortSignal; signal: AbortSignal;
connectorId: string; connectorId: string;
}): Promise<Record<string, any>> { }): Promise<Record<string, any>> {
return await http.post(`${BASE_ACTION_API_PATH}/connector/${connectorId}/_execute`, { return await http.post(
body: JSON.stringify({ `${BASE_ACTION_API_PATH}/connector/${encodeURIComponent(connectorId)}/_execute`,
params: { subAction: 'severity', subActionParams: {} }, {
}), body: JSON.stringify({
signal, params: { subAction: 'severity', subActionParams: {} },
}); }),
signal,
}
);
} }

View file

@ -56,12 +56,12 @@ describe('ServiceNow API', () => {
const res = await getChoices({ const res = await getChoices({
http, http,
signal: abortCtrl.signal, signal: abortCtrl.signal,
connectorId: 'test', connectorId: 'te/st',
fields: ['priority'], fields: ['priority'],
}); });
expect(res).toEqual(choicesResponse); expect(res).toEqual(choicesResponse);
expect(http.post).toHaveBeenCalledWith('/api/actions/connector/test/_execute', { expect(http.post).toHaveBeenCalledWith('/api/actions/connector/te%2Fst/_execute', {
body: '{"params":{"subAction":"getChoices","subActionParams":{"fields":["priority"]}}}', body: '{"params":{"subAction":"getChoices","subActionParams":{"fields":["priority"]}}}',
signal: abortCtrl.signal, signal: abortCtrl.signal,
}); });

View file

@ -19,10 +19,13 @@ export async function getChoices({
connectorId: string; connectorId: string;
fields: string[]; fields: string[];
}): Promise<Record<string, any>> { }): Promise<Record<string, any>> {
return await http.post(`${BASE_ACTION_API_PATH}/connector/${connectorId}/_execute`, { return await http.post(
body: JSON.stringify({ `${BASE_ACTION_API_PATH}/connector/${encodeURIComponent(connectorId)}/_execute`,
params: { subAction: 'getChoices', subActionParams: { fields } }, {
}), body: JSON.stringify({
signal, params: { subAction: 'getChoices', subActionParams: { fields } },
}); }),
signal,
}
);
} }

View file

@ -14,7 +14,7 @@ beforeEach(() => jest.resetAllMocks());
describe('deleteActions', () => { describe('deleteActions', () => {
test('should call delete API per action', async () => { test('should call delete API per action', async () => {
const ids = ['1', '2', '3']; const ids = ['1', '2', '/'];
const result = await deleteActions({ ids, http }); const result = await deleteActions({ ids, http });
expect(result).toEqual({ errors: [], successes: [undefined, undefined, undefined] }); expect(result).toEqual({ errors: [], successes: [undefined, undefined, undefined] });
@ -27,7 +27,7 @@ describe('deleteActions', () => {
"/api/actions/connector/2", "/api/actions/connector/2",
], ],
Array [ Array [
"/api/actions/connector/3", "/api/actions/connector/%2F",
], ],
] ]
`); `);

View file

@ -16,7 +16,9 @@ export async function deleteActions({
}): Promise<{ successes: string[]; errors: string[] }> { }): Promise<{ successes: string[]; errors: string[] }> {
const successes: string[] = []; const successes: string[] = [];
const errors: string[] = []; const errors: string[] = [];
await Promise.all(ids.map((id) => http.delete(`${BASE_ACTION_API_PATH}/connector/${id}`))).then( await Promise.all(
ids.map((id) => http.delete(`${BASE_ACTION_API_PATH}/connector/${encodeURIComponent(id)}`))
).then(
function (fulfilled) { function (fulfilled) {
successes.push(...fulfilled); successes.push(...fulfilled);
}, },

View file

@ -14,7 +14,7 @@ beforeEach(() => jest.resetAllMocks());
describe('executeAction', () => { describe('executeAction', () => {
test('should call execute API', async () => { test('should call execute API', async () => {
const id = '123'; const id = '12/3';
const params = { const params = {
stringParams: 'someString', stringParams: 'someString',
numericParams: 123, numericParams: 123,
@ -32,7 +32,7 @@ describe('executeAction', () => {
}); });
expect(http.post.mock.calls[0]).toMatchInlineSnapshot(` expect(http.post.mock.calls[0]).toMatchInlineSnapshot(`
Array [ Array [
"/api/actions/connector/123/_execute", "/api/actions/connector/12%2F3/_execute",
Object { Object {
"body": "{\\"params\\":{\\"stringParams\\":\\"someString\\",\\"numericParams\\":123}}", "body": "{\\"params\\":{\\"stringParams\\":\\"someString\\",\\"numericParams\\":123}}",
}, },

View file

@ -31,8 +31,11 @@ export async function executeAction({
http: HttpSetup; http: HttpSetup;
params: Record<string, unknown>; params: Record<string, unknown>;
}): Promise<ActionTypeExecutorResult<unknown>> { }): Promise<ActionTypeExecutorResult<unknown>> {
const res = await http.post(`${BASE_ACTION_API_PATH}/connector/${id}/_execute`, { const res = await http.post(
body: JSON.stringify({ params }), `${BASE_ACTION_API_PATH}/connector/${encodeURIComponent(id)}/_execute`,
}); {
body: JSON.stringify({ params }),
}
);
return rewriteBodyRes(res); return rewriteBodyRes(res);
} }

View file

@ -15,9 +15,9 @@ beforeEach(() => jest.resetAllMocks());
describe('updateActionConnector', () => { describe('updateActionConnector', () => {
test('should call the update API', async () => { test('should call the update API', async () => {
const id = '123'; const id = '12/3';
const apiResponse = { const apiResponse = {
connector_type_id: 'test', connector_type_id: 'te/st',
is_preconfigured: false, is_preconfigured: false,
name: 'My test', name: 'My test',
config: {}, config: {},
@ -27,7 +27,7 @@ describe('updateActionConnector', () => {
http.put.mockResolvedValueOnce(apiResponse); http.put.mockResolvedValueOnce(apiResponse);
const connector: ActionConnectorWithoutId<{}, {}> = { const connector: ActionConnectorWithoutId<{}, {}> = {
actionTypeId: 'test', actionTypeId: 'te/st',
isPreconfigured: false, isPreconfigured: false,
name: 'My test', name: 'My test',
config: {}, config: {},
@ -39,7 +39,7 @@ describe('updateActionConnector', () => {
expect(result).toEqual(resolvedValue); expect(result).toEqual(resolvedValue);
expect(http.put.mock.calls[0]).toMatchInlineSnapshot(` expect(http.put.mock.calls[0]).toMatchInlineSnapshot(`
Array [ Array [
"/api/actions/connector/123", "/api/actions/connector/12%2F3",
Object { Object {
"body": "{\\"name\\":\\"My test\\",\\"config\\":{},\\"secrets\\":{}}", "body": "{\\"name\\":\\"My test\\",\\"config\\":{},\\"secrets\\":{}}",
}, },

View file

@ -30,7 +30,7 @@ export async function updateActionConnector({
connector: Pick<ActionConnectorWithoutId, 'name' | 'config' | 'secrets'>; connector: Pick<ActionConnectorWithoutId, 'name' | 'config' | 'secrets'>;
id: string; id: string;
}): Promise<ActionConnector> { }): Promise<ActionConnector> {
const res = await http.put(`${BASE_ACTION_API_PATH}/connector/${id}`, { const res = await http.put(`${BASE_ACTION_API_PATH}/connector/${encodeURIComponent(id)}`, {
body: JSON.stringify({ body: JSON.stringify({
name: connector.name, name: connector.name,
config: connector.config, config: connector.config,

View file

@ -18,7 +18,7 @@ describe('loadAlertInstanceSummary', () => {
consumer: 'alerts', consumer: 'alerts',
enabled: true, enabled: true,
errorMessages: [], errorMessages: [],
id: 'test', id: 'te/st',
lastRun: '2021-04-01T22:18:27.609Z', lastRun: '2021-04-01T22:18:27.609Z',
muteAll: false, muteAll: false,
name: 'test', name: 'test',
@ -35,7 +35,7 @@ describe('loadAlertInstanceSummary', () => {
consumer: 'alerts', consumer: 'alerts',
enabled: true, enabled: true,
error_messages: [], error_messages: [],
id: 'test', id: 'te/st',
last_run: '2021-04-01T22:18:27.609Z', last_run: '2021-04-01T22:18:27.609Z',
mute_all: false, mute_all: false,
name: 'test', name: 'test',
@ -47,11 +47,11 @@ describe('loadAlertInstanceSummary', () => {
throttle: null, throttle: null,
}); });
const result = await loadAlertInstanceSummary({ http, alertId: 'test' }); const result = await loadAlertInstanceSummary({ http, alertId: 'te/st' });
expect(result).toEqual(resolvedValue); expect(result).toEqual(resolvedValue);
expect(http.get.mock.calls[0]).toMatchInlineSnapshot(` expect(http.get.mock.calls[0]).toMatchInlineSnapshot(`
Array [ Array [
"/internal/alerting/rule/test/_alert_summary", "/internal/alerting/rule/te%2Fst/_alert_summary",
] ]
`); `);
}); });

View file

@ -36,6 +36,8 @@ export async function loadAlertInstanceSummary({
http: HttpSetup; http: HttpSetup;
alertId: string; alertId: string;
}): Promise<AlertInstanceSummary> { }): Promise<AlertInstanceSummary> {
const res = await http.get(`${INTERNAL_BASE_ALERTING_API_PATH}/rule/${alertId}/_alert_summary`); const res = await http.get(
`${INTERNAL_BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(alertId)}/_alert_summary`
);
return rewriteBodyRes(res); return rewriteBodyRes(res);
} }

View file

@ -12,7 +12,7 @@ const http = httpServiceMock.createStartContract();
describe('deleteAlerts', () => { describe('deleteAlerts', () => {
test('should call delete API for each alert', async () => { test('should call delete API for each alert', async () => {
const ids = ['1', '2', '3']; const ids = ['1', '2', '/'];
const result = await deleteAlerts({ http, ids }); const result = await deleteAlerts({ http, ids });
expect(result).toEqual({ errors: [], successes: [undefined, undefined, undefined] }); expect(result).toEqual({ errors: [], successes: [undefined, undefined, undefined] });
expect(http.delete.mock.calls).toMatchInlineSnapshot(` expect(http.delete.mock.calls).toMatchInlineSnapshot(`
@ -24,7 +24,7 @@ describe('deleteAlerts', () => {
"/api/alerting/rule/2", "/api/alerting/rule/2",
], ],
Array [ Array [
"/api/alerting/rule/3", "/api/alerting/rule/%2F",
], ],
] ]
`); `);

View file

@ -16,7 +16,9 @@ export async function deleteAlerts({
}): Promise<{ successes: string[]; errors: string[] }> { }): Promise<{ successes: string[]; errors: string[] }> {
const successes: string[] = []; const successes: string[] = [];
const errors: string[] = []; const errors: string[] = [];
await Promise.all(ids.map((id) => http.delete(`${BASE_ALERTING_API_PATH}/rule/${id}`))).then( await Promise.all(
ids.map((id) => http.delete(`${BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(id)}`))
).then(
function (fulfilled) { function (fulfilled) {
successes.push(...fulfilled); successes.push(...fulfilled);
}, },

View file

@ -13,12 +13,12 @@ beforeEach(() => jest.resetAllMocks());
describe('disableAlert', () => { describe('disableAlert', () => {
test('should call disable alert API', async () => { test('should call disable alert API', async () => {
const result = await disableAlert({ http, id: '1' }); const result = await disableAlert({ http, id: '1/' });
expect(result).toEqual(undefined); expect(result).toEqual(undefined);
expect(http.post.mock.calls).toMatchInlineSnapshot(` expect(http.post.mock.calls).toMatchInlineSnapshot(`
Array [ Array [
Array [ Array [
"/api/alerting/rule/1/_disable", "/api/alerting/rule/1%2F/_disable",
], ],
] ]
`); `);
@ -27,7 +27,7 @@ describe('disableAlert', () => {
describe('disableAlerts', () => { describe('disableAlerts', () => {
test('should call disable alert API per alert', async () => { test('should call disable alert API per alert', async () => {
const ids = ['1', '2', '3']; const ids = ['1', '2', '/'];
const result = await disableAlerts({ http, ids }); const result = await disableAlerts({ http, ids });
expect(result).toEqual(undefined); expect(result).toEqual(undefined);
expect(http.post.mock.calls).toMatchInlineSnapshot(` expect(http.post.mock.calls).toMatchInlineSnapshot(`
@ -39,7 +39,7 @@ describe('disableAlerts', () => {
"/api/alerting/rule/2/_disable", "/api/alerting/rule/2/_disable",
], ],
Array [ Array [
"/api/alerting/rule/3/_disable", "/api/alerting/rule/%2F/_disable",
], ],
] ]
`); `);

View file

@ -8,7 +8,7 @@ import { HttpSetup } from 'kibana/public';
import { BASE_ALERTING_API_PATH } from '../../constants'; import { BASE_ALERTING_API_PATH } from '../../constants';
export async function disableAlert({ id, http }: { id: string; http: HttpSetup }): Promise<void> { export async function disableAlert({ id, http }: { id: string; http: HttpSetup }): Promise<void> {
await http.post(`${BASE_ALERTING_API_PATH}/rule/${id}/_disable`); await http.post(`${BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(id)}/_disable`);
} }
export async function disableAlerts({ export async function disableAlerts({

View file

@ -13,12 +13,12 @@ beforeEach(() => jest.resetAllMocks());
describe('enableAlert', () => { describe('enableAlert', () => {
test('should call enable alert API', async () => { test('should call enable alert API', async () => {
const result = await enableAlert({ http, id: '1' }); const result = await enableAlert({ http, id: '1/' });
expect(result).toEqual(undefined); expect(result).toEqual(undefined);
expect(http.post.mock.calls).toMatchInlineSnapshot(` expect(http.post.mock.calls).toMatchInlineSnapshot(`
Array [ Array [
Array [ Array [
"/api/alerting/rule/1/_enable", "/api/alerting/rule/1%2F/_enable",
], ],
] ]
`); `);
@ -27,7 +27,7 @@ describe('enableAlert', () => {
describe('enableAlerts', () => { describe('enableAlerts', () => {
test('should call enable alert API per alert', async () => { test('should call enable alert API per alert', async () => {
const ids = ['1', '2', '3']; const ids = ['1', '2', '/'];
const result = await enableAlerts({ http, ids }); const result = await enableAlerts({ http, ids });
expect(result).toEqual(undefined); expect(result).toEqual(undefined);
expect(http.post.mock.calls).toMatchInlineSnapshot(` expect(http.post.mock.calls).toMatchInlineSnapshot(`
@ -39,7 +39,7 @@ describe('enableAlerts', () => {
"/api/alerting/rule/2/_enable", "/api/alerting/rule/2/_enable",
], ],
Array [ Array [
"/api/alerting/rule/3/_enable", "/api/alerting/rule/%2F/_enable",
], ],
] ]
`); `);

View file

@ -8,7 +8,7 @@ import { HttpSetup } from 'kibana/public';
import { BASE_ALERTING_API_PATH } from '../../constants'; import { BASE_ALERTING_API_PATH } from '../../constants';
export async function enableAlert({ id, http }: { id: string; http: HttpSetup }): Promise<void> { export async function enableAlert({ id, http }: { id: string; http: HttpSetup }): Promise<void> {
await http.post(`${BASE_ALERTING_API_PATH}/rule/${id}/_enable`); await http.post(`${BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(id)}/_enable`);
} }
export async function enableAlerts({ export async function enableAlerts({

View file

@ -13,9 +13,10 @@ const http = httpServiceMock.createStartContract();
describe('loadAlert', () => { describe('loadAlert', () => {
test('should call get API with base parameters', async () => { test('should call get API with base parameters', async () => {
const alertId = uuid.v4(); const alertId = `${uuid.v4()}/`;
const alertIdEncoded = encodeURIComponent(alertId);
const resolvedValue = { const resolvedValue = {
id: '1', id: '1/',
params: { params: {
aggType: 'count', aggType: 'count',
termSize: 5, termSize: 5,
@ -56,7 +57,7 @@ describe('loadAlert', () => {
http.get.mockResolvedValueOnce(resolvedValue); http.get.mockResolvedValueOnce(resolvedValue);
expect(await loadAlert({ http, alertId })).toEqual({ expect(await loadAlert({ http, alertId })).toEqual({
id: '1', id: '1/',
params: { params: {
aggType: 'count', aggType: 'count',
termSize: 5, termSize: 5,
@ -94,6 +95,6 @@ describe('loadAlert', () => {
}, },
], ],
}); });
expect(http.get).toHaveBeenCalledWith(`/api/alerting/rule/${alertId}`); expect(http.get).toHaveBeenCalledWith(`/api/alerting/rule/${alertIdEncoded}`);
}); });
}); });

View file

@ -16,6 +16,6 @@ export async function loadAlert({
http: HttpSetup; http: HttpSetup;
alertId: string; alertId: string;
}): Promise<Alert> { }): Promise<Alert> {
const res = await http.get(`${BASE_ALERTING_API_PATH}/rule/${alertId}`); const res = await http.get(`${BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(alertId)}`);
return transformAlert(res); return transformAlert(res);
} }

View file

@ -13,12 +13,12 @@ beforeEach(() => jest.resetAllMocks());
describe('muteAlert', () => { describe('muteAlert', () => {
test('should call mute alert API', async () => { test('should call mute alert API', async () => {
const result = await muteAlert({ http, id: '1' }); const result = await muteAlert({ http, id: '1/' });
expect(result).toEqual(undefined); expect(result).toEqual(undefined);
expect(http.post.mock.calls).toMatchInlineSnapshot(` expect(http.post.mock.calls).toMatchInlineSnapshot(`
Array [ Array [
Array [ Array [
"/api/alerting/rule/1/_mute_all", "/api/alerting/rule/1%2F/_mute_all",
], ],
] ]
`); `);
@ -27,7 +27,7 @@ describe('muteAlert', () => {
describe('muteAlerts', () => { describe('muteAlerts', () => {
test('should call mute alert API per alert', async () => { test('should call mute alert API per alert', async () => {
const ids = ['1', '2', '3']; const ids = ['1', '2', '/'];
const result = await muteAlerts({ http, ids }); const result = await muteAlerts({ http, ids });
expect(result).toEqual(undefined); expect(result).toEqual(undefined);
expect(http.post.mock.calls).toMatchInlineSnapshot(` expect(http.post.mock.calls).toMatchInlineSnapshot(`
@ -39,7 +39,7 @@ describe('muteAlerts', () => {
"/api/alerting/rule/2/_mute_all", "/api/alerting/rule/2/_mute_all",
], ],
Array [ Array [
"/api/alerting/rule/3/_mute_all", "/api/alerting/rule/%2F/_mute_all",
], ],
] ]
`); `);

View file

@ -8,7 +8,7 @@ import { HttpSetup } from 'kibana/public';
import { BASE_ALERTING_API_PATH } from '../../constants'; import { BASE_ALERTING_API_PATH } from '../../constants';
export async function muteAlert({ id, http }: { id: string; http: HttpSetup }): Promise<void> { export async function muteAlert({ id, http }: { id: string; http: HttpSetup }): Promise<void> {
await http.post(`${BASE_ALERTING_API_PATH}/rule/${id}/_mute_all`); await http.post(`${BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(id)}/_mute_all`);
} }
export async function muteAlerts({ ids, http }: { ids: string[]; http: HttpSetup }): Promise<void> { export async function muteAlerts({ ids, http }: { ids: string[]; http: HttpSetup }): Promise<void> {

View file

@ -12,12 +12,12 @@ const http = httpServiceMock.createStartContract();
describe('muteAlertInstance', () => { describe('muteAlertInstance', () => {
test('should call mute instance alert API', async () => { test('should call mute instance alert API', async () => {
const result = await muteAlertInstance({ http, id: '1', instanceId: '123' }); const result = await muteAlertInstance({ http, id: '1/', instanceId: '12/3' });
expect(result).toEqual(undefined); expect(result).toEqual(undefined);
expect(http.post.mock.calls).toMatchInlineSnapshot(` expect(http.post.mock.calls).toMatchInlineSnapshot(`
Array [ Array [
Array [ Array [
"/api/alerting/rule/1/alert/123/_mute", "/api/alerting/rule/1%2F/alert/12%2F3/_mute",
], ],
] ]
`); `);

View file

@ -16,5 +16,9 @@ export async function muteAlertInstance({
instanceId: string; instanceId: string;
http: HttpSetup; http: HttpSetup;
}): Promise<void> { }): Promise<void> {
await http.post(`${BASE_ALERTING_API_PATH}/rule/${id}/alert/${instanceId}/_mute`); await http.post(
`${BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(id)}/alert/${encodeURIComponent(
instanceId
)}/_mute`
);
} }

View file

@ -13,7 +13,7 @@ beforeEach(() => jest.resetAllMocks());
describe('unmuteAlerts', () => { describe('unmuteAlerts', () => {
test('should call unmute alert API per alert', async () => { test('should call unmute alert API per alert', async () => {
const ids = ['1', '2', '3']; const ids = ['1', '2', '/'];
const result = await unmuteAlerts({ http, ids }); const result = await unmuteAlerts({ http, ids });
expect(result).toEqual(undefined); expect(result).toEqual(undefined);
expect(http.post.mock.calls).toMatchInlineSnapshot(` expect(http.post.mock.calls).toMatchInlineSnapshot(`
@ -25,7 +25,7 @@ describe('unmuteAlerts', () => {
"/api/alerting/rule/2/_unmute_all", "/api/alerting/rule/2/_unmute_all",
], ],
Array [ Array [
"/api/alerting/rule/3/_unmute_all", "/api/alerting/rule/%2F/_unmute_all",
], ],
] ]
`); `);
@ -34,12 +34,12 @@ describe('unmuteAlerts', () => {
describe('unmuteAlert', () => { describe('unmuteAlert', () => {
test('should call unmute alert API', async () => { test('should call unmute alert API', async () => {
const result = await unmuteAlert({ http, id: '1' }); const result = await unmuteAlert({ http, id: '1/' });
expect(result).toEqual(undefined); expect(result).toEqual(undefined);
expect(http.post.mock.calls).toMatchInlineSnapshot(` expect(http.post.mock.calls).toMatchInlineSnapshot(`
Array [ Array [
Array [ Array [
"/api/alerting/rule/1/_unmute_all", "/api/alerting/rule/1%2F/_unmute_all",
], ],
] ]
`); `);

View file

@ -8,7 +8,7 @@ import { HttpSetup } from 'kibana/public';
import { BASE_ALERTING_API_PATH } from '../../constants'; import { BASE_ALERTING_API_PATH } from '../../constants';
export async function unmuteAlert({ id, http }: { id: string; http: HttpSetup }): Promise<void> { export async function unmuteAlert({ id, http }: { id: string; http: HttpSetup }): Promise<void> {
await http.post(`${BASE_ALERTING_API_PATH}/rule/${id}/_unmute_all`); await http.post(`${BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(id)}/_unmute_all`);
} }
export async function unmuteAlerts({ export async function unmuteAlerts({

View file

@ -12,12 +12,12 @@ const http = httpServiceMock.createStartContract();
describe('unmuteAlertInstance', () => { describe('unmuteAlertInstance', () => {
test('should call mute instance alert API', async () => { test('should call mute instance alert API', async () => {
const result = await unmuteAlertInstance({ http, id: '1', instanceId: '123' }); const result = await unmuteAlertInstance({ http, id: '1/', instanceId: '12/3' });
expect(result).toEqual(undefined); expect(result).toEqual(undefined);
expect(http.post.mock.calls).toMatchInlineSnapshot(` expect(http.post.mock.calls).toMatchInlineSnapshot(`
Array [ Array [
Array [ Array [
"/api/alerting/rule/1/alert/123/_unmute", "/api/alerting/rule/1%2F/alert/12%2F3/_unmute",
], ],
] ]
`); `);

View file

@ -16,5 +16,9 @@ export async function unmuteAlertInstance({
instanceId: string; instanceId: string;
http: HttpSetup; http: HttpSetup;
}): Promise<void> { }): Promise<void> {
await http.post(`${BASE_ALERTING_API_PATH}/rule/${id}/alert/${instanceId}/_unmute`); await http.post(
`${BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(id)}/alert/${encodeURIComponent(
instanceId
)}/_unmute`
);
} }

View file

@ -32,7 +32,7 @@ describe('updateAlert', () => {
}; };
const resolvedValue: Alert = { const resolvedValue: Alert = {
...alertToUpdate, ...alertToUpdate,
id: '123', id: '12/3',
enabled: true, enabled: true,
alertTypeId: 'test', alertTypeId: 'test',
createdBy: null, createdBy: null,
@ -46,11 +46,11 @@ describe('updateAlert', () => {
}; };
http.put.mockResolvedValueOnce(resolvedValue); http.put.mockResolvedValueOnce(resolvedValue);
const result = await updateAlert({ http, id: '123', alert: alertToUpdate }); const result = await updateAlert({ http, id: '12/3', alert: alertToUpdate });
expect(result).toEqual(resolvedValue); expect(result).toEqual(resolvedValue);
expect(http.put.mock.calls[0]).toMatchInlineSnapshot(` expect(http.put.mock.calls[0]).toMatchInlineSnapshot(`
Array [ Array [
"/api/alerting/rule/123", "/api/alerting/rule/12%2F3",
Object { Object {
"body": "{\\"throttle\\":\\"1m\\",\\"name\\":\\"test\\",\\"tags\\":[\\"foo\\"],\\"schedule\\":{\\"interval\\":\\"1m\\"},\\"params\\":{},\\"notify_when\\":\\"onThrottleInterval\\",\\"actions\\":[]}", "body": "{\\"throttle\\":\\"1m\\",\\"name\\":\\"test\\",\\"tags\\":[\\"foo\\"],\\"schedule\\":{\\"interval\\":\\"1m\\"},\\"params\\":{},\\"notify_when\\":\\"onThrottleInterval\\",\\"actions\\":[]}",
}, },

View file

@ -41,7 +41,7 @@ export async function updateAlert({
>; >;
id: string; id: string;
}): Promise<Alert> { }): Promise<Alert> {
const res = await http.put(`${BASE_ALERTING_API_PATH}/rule/${id}`, { const res = await http.put(`${BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(id)}`, {
body: JSON.stringify( body: JSON.stringify(
rewriteBodyRequest( rewriteBodyRequest(
pick(alert, ['throttle', 'name', 'tags', 'schedule', 'params', 'actions', 'notifyWhen']) pick(alert, ['throttle', 'name', 'tags', 'schedule', 'params', 'actions', 'notifyWhen'])

View file

@ -93,14 +93,18 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
async function getAlertInstanceSummary(alertId: string) { async function getAlertInstanceSummary(alertId: string) {
const { body: summary } = await supertest const { body: summary } = await supertest
.get(`/internal/alerting/rule/${alertId}/_alert_summary`) .get(`/internal/alerting/rule/${encodeURIComponent(alertId)}/_alert_summary`)
.expect(200); .expect(200);
return summary; return summary;
} }
async function muteAlertInstance(alertId: string, alertInstanceId: string) { async function muteAlertInstance(alertId: string, alertInstanceId: string) {
const { body: response } = await supertest const { body: response } = await supertest
.post(`/api/alerting/rule/${alertId}/alert/${alertInstanceId}/_mute`) .post(
`/api/alerting/rule/${encodeURIComponent(alertId)}/alert/${encodeURIComponent(
alertInstanceId
)}/_mute`
)
.set('kbn-xsrf', 'foo') .set('kbn-xsrf', 'foo')
.expect(204); .expect(204);
@ -640,17 +644,17 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
it('renders the muted inactive alert instances', async () => { it('renders the muted inactive alert instances', async () => {
// mute an alert instance that doesn't exist // mute an alert instance that doesn't exist
await muteAlertInstance(alert.id, 'eu-east'); await muteAlertInstance(alert.id, 'eu/east');
// refresh to see alert // refresh to see alert
await browser.refresh(); await browser.refresh();
const instancesList: any[] = await pageObjects.alertDetailsUI.getAlertInstancesList(); const instancesList: any[] = await pageObjects.alertDetailsUI.getAlertInstancesList();
expect( expect(
instancesList.filter((alertInstance) => alertInstance.instance === 'eu-east') instancesList.filter((alertInstance) => alertInstance.instance === 'eu/east')
).to.eql([ ).to.eql([
{ {
instance: 'eu-east', instance: 'eu/east',
status: 'OK', status: 'OK',
start: '', start: '',
duration: '', duration: '',
@ -693,14 +697,14 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => {
}); });
it('allows the user unmute an inactive instance', async () => { it('allows the user unmute an inactive instance', async () => {
log.debug(`Ensuring eu-east is muted`); log.debug(`Ensuring eu/east is muted`);
await pageObjects.alertDetailsUI.ensureAlertInstanceMute('eu-east', true); await pageObjects.alertDetailsUI.ensureAlertInstanceMute('eu/east', true);
log.debug(`Unmuting eu-east`); log.debug(`Unmuting eu/east`);
await pageObjects.alertDetailsUI.clickAlertInstanceMuteButton('eu-east'); await pageObjects.alertDetailsUI.clickAlertInstanceMuteButton('eu/east');
log.debug(`Ensuring eu-east is removed from list`); log.debug(`Ensuring eu/east is removed from list`);
await pageObjects.alertDetailsUI.ensureAlertInstanceExistance('eu-east', false); await pageObjects.alertDetailsUI.ensureAlertInstanceExistance('eu/east', false);
}); });
}); });