[alerting] add mustache variable kibanaBaseUrl for Kibana's publicly exposed base URL (#90525)

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

Adds the top-level mustache variable `kibanaBaseUrl` for action parameter
mustache templates.  The value comes from Kibana config, which, if not set
will result in this variable having the value `undefined` which will be rendered
as an empty string.
This commit is contained in:
Patrick Mueller 2021-02-16 15:47:56 -05:00 committed by GitHub
parent 073cd4d508
commit 20e16bd9a4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 252 additions and 1 deletions

View file

@ -161,6 +161,7 @@ export class AlertingPlugin {
private eventLogService?: IEventLogService;
private eventLogger?: IEventLogger;
private readonly kibanaIndexConfig: Observable<{ kibana: { index: string } }>;
private kibanaBaseUrl: string | undefined;
constructor(initializerContext: PluginInitializerContext) {
this.config = initializerContext.config.create<AlertsConfig>().pipe(first()).toPromise();
@ -176,6 +177,7 @@ export class AlertingPlugin {
core: CoreSetup<AlertingPluginsStart, unknown>,
plugins: AlertingPluginsSetup
): PluginSetupContract {
this.kibanaBaseUrl = core.http.basePath.publicBaseUrl;
this.licenseState = new LicenseState(plugins.licensing.license$);
this.security = plugins.security;
@ -371,6 +373,7 @@ export class AlertingPlugin {
eventLogger: this.eventLogger!,
internalSavedObjectsRepository: core.savedObjects.createInternalRepository(['alert']),
alertTypeRegistry: this.alertTypeRegistry!,
kibanaBaseUrl: this.kibanaBaseUrl,
});
this.eventLogService!.registerSavedObjectProvider('alert', (request) => {

View file

@ -72,6 +72,7 @@ const createExecutionHandlerParams: jest.Mocked<
alertName: 'name-of-alert',
tags: ['tag-A', 'tag-B'],
apiKey: 'MTIzOmFiYw==',
kibanaBaseUrl: 'http://localhost:5601',
alertType,
logger: loggingSystemMock.create().get(),
eventLogger: mockEventLogger,

View file

@ -39,6 +39,7 @@ export interface CreateExecutionHandlerOptions<
actions: AlertAction[];
spaceId: string;
apiKey: RawAlert['apiKey'];
kibanaBaseUrl: string | undefined;
alertType: NormalizedAlertType<
Params,
State,
@ -82,6 +83,7 @@ export function createExecutionHandler<
spaceId,
apiKey,
alertType,
kibanaBaseUrl,
eventLogger,
request,
alertParams,
@ -126,6 +128,7 @@ export function createExecutionHandler<
context,
actionParams: action.params,
state,
kibanaBaseUrl,
alertParams,
}),
};

View file

@ -97,6 +97,7 @@ describe('Task Runner', () => {
eventLogger: eventLoggerMock.create(),
internalSavedObjectsRepository: savedObjectsRepositoryMock.create(),
alertTypeRegistry,
kibanaBaseUrl: 'https://localhost:5601',
};
const mockedAlertTypeSavedObject: Alert<AlertTypeParams> = {

View file

@ -160,6 +160,7 @@ export class TaskRunner<
tags: string[] | undefined,
spaceId: string,
apiKey: RawAlert['apiKey'],
kibanaBaseUrl: string | undefined,
actions: Alert<Params>['actions'],
alertParams: Params
) {
@ -180,6 +181,7 @@ export class TaskRunner<
actions,
spaceId,
alertType: this.alertType,
kibanaBaseUrl,
eventLogger: this.context.eventLogger,
request: this.getFakeKibanaRequest(spaceId, apiKey),
alertParams,
@ -388,6 +390,7 @@ export class TaskRunner<
alert.tags,
spaceId,
apiKey,
this.context.kibanaBaseUrl,
alert.actions,
alert.params
);

View file

@ -77,6 +77,7 @@ describe('Task Runner Factory', () => {
eventLogger: eventLoggerMock.create(),
internalSavedObjectsRepository: savedObjectsRepositoryMock.create(),
alertTypeRegistry: alertTypeRegistryMock.create(),
kibanaBaseUrl: 'https://localhost:5601',
};
beforeEach(() => {

View file

@ -40,6 +40,7 @@ export interface TaskRunnerContext {
basePathService: IBasePath;
internalSavedObjectsRepository: ISavedObjectsRepository;
alertTypeRegistry: AlertTypeRegistry;
kibanaBaseUrl: string | undefined;
}
export class TaskRunnerFactory {

View file

@ -27,6 +27,7 @@ interface TransformActionParamsOptions {
actionParams: AlertActionParams;
alertParams: AlertTypeParams;
state: AlertInstanceState;
kibanaBaseUrl?: string;
context: AlertInstanceContext;
}
@ -44,6 +45,7 @@ export function transformActionParams({
context,
actionParams,
state,
kibanaBaseUrl,
alertParams,
}: TransformActionParamsOptions): AlertActionParams {
// when the list of variables we pass in here changes,
@ -61,6 +63,7 @@ export function transformActionParams({
context,
date: new Date().toISOString(),
state,
kibanaBaseUrl,
params: alertParams,
};
return actionsPlugin.renderActionParameterTemplates(actionTypeId, actionParams, variables);

View file

@ -44,10 +44,18 @@ describe('transformActionVariables', () => {
"description": "The alert action group that was used to scheduled actions for the alert.",
"name": "alertActionGroup",
},
Object {
"description": "The alert action subgroup that was used to scheduled actions for the alert.",
"name": "alertActionSubgroup",
},
Object {
"description": "The human readable name of the alert action group that was used to scheduled actions for the alert.",
"name": "alertActionGroupName",
},
Object {
"description": "The configured server.publicBaseUrl value or empty string if not configured.",
"name": "kibanaBaseUrl",
},
]
`);
});
@ -91,10 +99,18 @@ describe('transformActionVariables', () => {
"description": "The alert action group that was used to scheduled actions for the alert.",
"name": "alertActionGroup",
},
Object {
"description": "The alert action subgroup that was used to scheduled actions for the alert.",
"name": "alertActionSubgroup",
},
Object {
"description": "The human readable name of the alert action group that was used to scheduled actions for the alert.",
"name": "alertActionGroupName",
},
Object {
"description": "The configured server.publicBaseUrl value or empty string if not configured.",
"name": "kibanaBaseUrl",
},
Object {
"description": "foo-description",
"name": "context.foo",
@ -146,10 +162,18 @@ describe('transformActionVariables', () => {
"description": "The alert action group that was used to scheduled actions for the alert.",
"name": "alertActionGroup",
},
Object {
"description": "The alert action subgroup that was used to scheduled actions for the alert.",
"name": "alertActionSubgroup",
},
Object {
"description": "The human readable name of the alert action group that was used to scheduled actions for the alert.",
"name": "alertActionGroupName",
},
Object {
"description": "The configured server.publicBaseUrl value or empty string if not configured.",
"name": "kibanaBaseUrl",
},
Object {
"description": "foo-description",
"name": "state.foo",
@ -204,10 +228,18 @@ describe('transformActionVariables', () => {
"description": "The alert action group that was used to scheduled actions for the alert.",
"name": "alertActionGroup",
},
Object {
"description": "The alert action subgroup that was used to scheduled actions for the alert.",
"name": "alertActionSubgroup",
},
Object {
"description": "The human readable name of the alert action group that was used to scheduled actions for the alert.",
"name": "alertActionGroupName",
},
Object {
"description": "The configured server.publicBaseUrl value or empty string if not configured.",
"name": "kibanaBaseUrl",
},
Object {
"description": "fooC-description",
"name": "context.fooC",
@ -280,10 +312,18 @@ describe('transformActionVariables', () => {
"description": "The alert action group that was used to scheduled actions for the alert.",
"name": "alertActionGroup",
},
Object {
"description": "The alert action subgroup that was used to scheduled actions for the alert.",
"name": "alertActionSubgroup",
},
Object {
"description": "The human readable name of the alert action group that was used to scheduled actions for the alert.",
"name": "alertActionGroupName",
},
Object {
"description": "The configured server.publicBaseUrl value or empty string if not configured.",
"name": "kibanaBaseUrl",
},
Object {
"description": "fooC-description",
"name": "context.fooC",

View file

@ -88,6 +88,17 @@ function getAlwaysProvidedActionVariables(): ActionVariable[] {
}),
});
result.push({
name: 'alertActionSubgroup',
description: i18n.translate(
'xpack.triggersActionsUI.actionVariables.alertActionSubgroupLabel',
{
defaultMessage:
'The alert action subgroup that was used to scheduled actions for the alert.',
}
),
});
result.push({
name: 'alertActionGroupName',
description: i18n.translate(
@ -99,5 +110,13 @@ function getAlwaysProvidedActionVariables(): ActionVariable[] {
),
});
result.push({
name: 'kibanaBaseUrl',
description: i18n.translate('xpack.triggersActionsUI.actionVariables.kibanaBaseUrlLabel', {
defaultMessage:
'The configured server.publicBaseUrl value or empty string if not configured.',
}),
});
return result;
}

View file

@ -19,6 +19,7 @@ interface CreateTestConfigOptions {
ssl?: boolean;
enableActionsProxy: boolean;
rejectUnauthorized?: boolean;
publicBaseUrl?: boolean;
}
// test.not-enabled is specifically not enabled
@ -97,7 +98,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
...xPackApiIntegrationTestsConfig.get('kbnTestServer'),
serverArgs: [
...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'),
'--server.publicBaseUrl=https://localhost:5601',
...(options.publicBaseUrl ? ['--server.publicBaseUrl=https://localhost:5601'] : []),
`--xpack.actions.allowedHosts=${JSON.stringify(['localhost', 'some.non.existent.com'])}`,
'--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"',
'--xpack.alerts.invalidateApiKeysTask.interval="15s"',

View file

@ -13,4 +13,5 @@ export default createTestConfig('security_and_spaces', {
license: 'trial',
ssl: true,
enableActionsProxy: true,
publicBaseUrl: true,
});

View file

@ -51,6 +51,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC
loadTestFile(require.resolve('./update_api_key'));
loadTestFile(require.resolve('./alerts'));
loadTestFile(require.resolve('./event_log'));
loadTestFile(require.resolve('./mustache_templates'));
});
});
}

View file

@ -0,0 +1,125 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
/*
* These tests ensure that the per-action mustache template escaping works
* for actions we have simulators for. It arranges to have an alert that
* schedules an action that will contain "escapable" characters in it, and
* then validates that the simulator receives the escaped versions.
*/
import http from 'http';
import getPort from 'get-port';
import axios from 'axios';
import httpProxy from 'http-proxy';
import expect from '@kbn/expect';
import { Spaces } from '../../scenarios';
import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
import { getSlackServer } from '../../../common/fixtures/plugins/actions_simulators/server/plugin';
import { getHttpProxyServer } from '../../../common/lib/get_proxy_server';
// eslint-disable-next-line import/no-default-export
export default function executionStatusAlertTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const retry = getService('retry');
const configService = getService('config');
describe('mustacheTemplates', () => {
const objectRemover = new ObjectRemover(supertest);
let slackSimulatorURL: string = '';
let slackServer: http.Server;
let proxyServer: httpProxy | undefined;
before(async () => {
slackServer = await getSlackServer();
const availablePort = await getPort({ port: getPort.makeRange(9000, 9100) });
if (!slackServer.listening) {
slackServer.listen(availablePort);
}
slackSimulatorURL = `http://localhost:${availablePort}`;
proxyServer = await getHttpProxyServer(
slackSimulatorURL,
configService.get('kbnTestServer.serverArgs'),
() => {}
);
});
after(async () => {
await objectRemover.removeAll();
slackServer.close();
if (proxyServer) {
proxyServer.close();
}
});
it('should render kibanaBaseUrl as non-empty string since configured', async () => {
const actionResponse = await supertest
.post(`${getUrlPrefix(Spaces[0].id)}/api/actions/action`)
.set('kbn-xsrf', 'test')
.send({
name: 'testing context variable expansion',
actionTypeId: '.slack',
secrets: {
webhookUrl: slackSimulatorURL,
},
});
expect(actionResponse.status).to.eql(200);
const createdAction = actionResponse.body;
objectRemover.add(Spaces[0].id, createdAction.id, 'action', 'actions');
const varsTemplate = 'kibanaBaseUrl: "{{kibanaBaseUrl}}"';
const alertResponse = await supertest
.post(`${getUrlPrefix(Spaces[0].id)}/api/alerts/alert`)
.set('kbn-xsrf', 'foo')
.send(
getTestAlertData({
name: 'testing context variable kibanaBaseUrl',
alertTypeId: 'test.patternFiring',
params: {
pattern: { instance: [true, true] },
},
actions: [
{
id: createdAction.id,
group: 'default',
params: {
message: `message {{alertId}} - ${varsTemplate}`,
},
},
],
})
);
expect(alertResponse.status).to.eql(200);
const createdAlert = alertResponse.body;
objectRemover.add(Spaces[0].id, createdAlert.id, 'alert', 'alerts');
const body = await retry.try(async () =>
waitForActionBody(slackSimulatorURL, createdAlert.id)
);
expect(body).to.be('kibanaBaseUrl: "https://localhost:5601"');
});
});
async function waitForActionBody(url: string, id: string): Promise<string> {
const response = await axios.get<string[]>(url);
expect(response.status).to.eql(200);
for (const datum of response.data) {
const match = datum.match(/^(.*) - (.*)$/);
if (match == null) continue;
if (match[1] === id) return match[2];
}
throw new Error(`no action body posted yet for id ${id}`);
}
}

View file

@ -217,6 +217,54 @@ export default function executionStatusAlertTests({ getService }: FtrProviderCon
'{"objectA":{"stringB":"B","arrayC":[{"stringD":"D1","numberE":42},{"stringD":"D2","numberE":43}],"objectF":{"stringG":"G","nullG":null}},"stringH":"H","arrayI":[44,45],"nullJ":null}'
);
});
it('should render kibanaBaseUrl as empty string since not configured', async () => {
const actionResponse = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`)
.set('kbn-xsrf', 'test')
.send({
name: 'testing context variable expansion',
actionTypeId: '.slack',
secrets: {
webhookUrl: slackSimulatorURL,
},
});
expect(actionResponse.status).to.eql(200);
const createdAction = actionResponse.body;
objectRemover.add(Spaces.space1.id, createdAction.id, 'action', 'actions');
const varsTemplate = 'kibanaBaseUrl: "{{kibanaBaseUrl}}"';
const alertResponse = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`)
.set('kbn-xsrf', 'foo')
.send(
getTestAlertData({
name: 'testing context variable kibanaBaseUrl',
alertTypeId: 'test.patternFiring',
params: {
pattern: { instance: [true, true] },
},
actions: [
{
id: createdAction.id,
group: 'default',
params: {
message: `message {{alertId}} - ${varsTemplate}`,
},
},
],
})
);
expect(alertResponse.status).to.eql(200);
const createdAlert = alertResponse.body;
objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert', 'alerts');
const body = await retry.try(async () =>
waitForActionBody(slackSimulatorURL, createdAlert.id)
);
expect(body).to.be('kibanaBaseUrl: ""');
});
});
async function waitForActionBody(url: string, id: string): Promise<string> {