Skip to content

Commit

Permalink
[Alerting] notifies user when security is enabled but TLS is not (#60270
Browse files Browse the repository at this point in the history
)

This PR:
1. Adds a callout on the Alerting UI when security is enabled but TLS is not
2. Cleans up displayed error message when creation fails due to TLS being switched off
  • Loading branch information
gmmorris authored Mar 24, 2020
1 parent ec8ac30 commit 18793db
Show file tree
Hide file tree
Showing 33 changed files with 809 additions and 103 deletions.
5 changes: 4 additions & 1 deletion examples/alerting_example/public/application.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
AppMountParameters,
CoreStart,
IUiSettingsClient,
DocLinksStart,
ToastsSetup,
} from '../../../src/core/public';
import { DataPublicPluginStart } from '../../../src/plugins/data/public';
Expand All @@ -45,6 +46,7 @@ export interface AlertingExampleComponentParams {
data: DataPublicPluginStart;
charts: ChartsPluginStart;
uiSettings: IUiSettingsClient;
docLinks: DocLinksStart;
toastNotifications: ToastsSetup;
}

Expand Down Expand Up @@ -88,7 +90,7 @@ const AlertingExampleApp = (deps: AlertingExampleComponentParams) => {
};

export const renderApp = (
{ application, notifications, http, uiSettings }: CoreStart,
{ application, notifications, http, uiSettings, docLinks }: CoreStart,
deps: AlertingExamplePublicStartDeps,
{ appBasePath, element }: AppMountParameters
) => {
Expand All @@ -99,6 +101,7 @@ export const renderApp = (
toastNotifications={notifications.toasts}
http={http}
uiSettings={uiSettings}
docLinks={docLinks}
{...deps}
/>,
element
Expand Down
2 changes: 2 additions & 0 deletions examples/alerting_example/public/components/create_alert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export const CreateAlert = ({
triggers_actions_ui,
charts,
uiSettings,
docLinks,
data,
toastNotifications,
}: AlertingExampleComponentParams) => {
Expand All @@ -56,6 +57,7 @@ export const CreateAlert = ({
alertTypeRegistry: triggers_actions_ui.alertTypeRegistry,
toastNotifications,
uiSettings,
docLinks,
charts,
dataFieldsFormats: data.fieldFormats,
}}
Expand Down
1 change: 1 addition & 0 deletions x-pack/legacy/plugins/apm/public/new-platform/plugin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ export class ApmPlugin
<AlertsContextProvider
value={{
http: core.http,
docLinks: core.docLinks,
toastNotifications: core.notifications.toasts,
actionTypeRegistry: plugins.triggers_actions_ui.actionTypeRegistry,
alertTypeRegistry: plugins.triggers_actions_ui.alertTypeRegistry
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const UptimeAlertsContextProvider: React.FC = ({ children }) => {
notifications,
triggers_actions_ui: { actionTypeRegistry, alertTypeRegistry },
uiSettings,
docLinks,
},
} = useKibana();

Expand All @@ -26,6 +27,7 @@ export const UptimeAlertsContextProvider: React.FC = ({ children }) => {
actionTypeRegistry,
alertTypeRegistry,
charts,
docLinks,
dataFieldsFormats: fieldFormats,
http,
toastNotifications: notifications?.toasts,
Expand Down
9 changes: 9 additions & 0 deletions x-pack/plugins/alerting/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,13 @@ export * from './alert_instance';
export * from './alert_task_instance';
export * from './alert_navigation';

export interface ActionGroup {
id: string;
name: string;
}

export interface AlertingFrameworkHealth {
isSufficientlySecure: boolean;
}

export const BASE_ALERT_API_PATH = '/api/alert';
2 changes: 2 additions & 0 deletions x-pack/plugins/alerting/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ import {
unmuteAllAlertRoute,
muteAlertInstanceRoute,
unmuteAlertInstanceRoute,
healthRoute,
} from './routes';
import { LicensingPluginSetup } from '../../licensing/server';
import {
Expand Down Expand Up @@ -173,6 +174,7 @@ export class AlertingPlugin {
unmuteAllAlertRoute(router, this.licenseState);
muteAlertInstanceRoute(router, this.licenseState);
unmuteAlertInstanceRoute(router, this.licenseState);
healthRoute(router, this.licenseState);

return {
registerType: alertTypeRegistry.register.bind(alertTypeRegistry),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@ import { httpServerMock } from '../../../../../src/core/server/mocks';
import { alertsClientMock } from '../alerts_client.mock';

export function mockHandlerArguments(
{ alertsClient, listTypes: listTypesRes = [] }: any,
{ alertsClient, listTypes: listTypesRes = [], elasticsearch }: any,
req: any,
res?: Array<MethodKeysOf<KibanaResponseFactory>>
): [RequestHandlerContext, KibanaRequest<any, any, any, any>, KibanaResponseFactory] {
const listTypes = jest.fn(() => listTypesRes);
return [
({
core: { elasticsearch },
alerting: {
listTypes,
getAlertsClient() {
Expand Down
35 changes: 19 additions & 16 deletions x-pack/plugins/alerting/server/routes/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { LicenseState } from '../lib/license_state';
import { verifyApiAccess } from '../lib/license_api_access';
import { validateDurationSchema } from '../lib';
import { handleDisabledApiKeysError } from './lib/error_handler';
import { Alert, BASE_ALERT_API_PATH } from '../types';

export const bodySchema = schema.object({
Expand Down Expand Up @@ -50,22 +51,24 @@ export const createAlertRoute = (router: IRouter, licenseState: LicenseState) =>
tags: ['access:alerting-all'],
},
},
router.handleLegacyErrors(async function(
context: RequestHandlerContext,
req: KibanaRequest<any, any, TypeOf<typeof bodySchema>, any>,
res: KibanaResponseFactory
): Promise<IKibanaResponse<any>> {
verifyApiAccess(licenseState);
handleDisabledApiKeysError(
router.handleLegacyErrors(async function(
context: RequestHandlerContext,
req: KibanaRequest<any, any, TypeOf<typeof bodySchema>, any>,
res: KibanaResponseFactory
): Promise<IKibanaResponse<any>> {
verifyApiAccess(licenseState);

if (!context.alerting) {
return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' });
}
const alertsClient = context.alerting.getAlertsClient();
const alert = req.body;
const alertRes: Alert = await alertsClient.create({ data: alert });
return res.ok({
body: alertRes,
});
})
if (!context.alerting) {
return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' });
}
const alertsClient = context.alerting.getAlertsClient();
const alert = req.body;
const alertRes: Alert = await alertsClient.create({ data: alert });
return res.ok({
body: alertRes,
});
})
)
);
};
31 changes: 17 additions & 14 deletions x-pack/plugins/alerting/server/routes/enable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
import { LicenseState } from '../lib/license_state';
import { verifyApiAccess } from '../lib/license_api_access';
import { BASE_ALERT_API_PATH } from '../../common';
import { handleDisabledApiKeysError } from './lib/error_handler';

const paramSchema = schema.object({
id: schema.string(),
Expand All @@ -31,19 +32,21 @@ export const enableAlertRoute = (router: IRouter, licenseState: LicenseState) =>
tags: ['access:alerting-all'],
},
},
router.handleLegacyErrors(async function(
context: RequestHandlerContext,
req: KibanaRequest<TypeOf<typeof paramSchema>, any, any, any>,
res: KibanaResponseFactory
): Promise<IKibanaResponse<any>> {
verifyApiAccess(licenseState);
if (!context.alerting) {
return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' });
}
const alertsClient = context.alerting.getAlertsClient();
const { id } = req.params;
await alertsClient.enable({ id });
return res.noContent();
})
handleDisabledApiKeysError(
router.handleLegacyErrors(async function(
context: RequestHandlerContext,
req: KibanaRequest<TypeOf<typeof paramSchema>, any, any, any>,
res: KibanaResponseFactory
): Promise<IKibanaResponse<any>> {
verifyApiAccess(licenseState);
if (!context.alerting) {
return res.badRequest({ body: 'RouteHandlerContext is not registered for alerting' });
}
const alertsClient = context.alerting.getAlertsClient();
const { id } = req.params;
await alertsClient.enable({ id });
return res.noContent();
})
)
);
};
171 changes: 171 additions & 0 deletions x-pack/plugins/alerting/server/routes/health.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { healthRoute } from './health';
import { mockRouter, RouterMock } from '../../../../../src/core/server/http/router/router.mock';
import { mockHandlerArguments } from './_mock_handler_arguments';
import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks';
import { verifyApiAccess } from '../lib/license_api_access';
import { mockLicenseState } from '../lib/license_state.mock';

jest.mock('../lib/license_api_access.ts', () => ({
verifyApiAccess: jest.fn(),
}));

beforeEach(() => {
jest.resetAllMocks();
});

describe('healthRoute', () => {
it('registers the route', async () => {
const router: RouterMock = mockRouter.create();

const licenseState = mockLicenseState();
healthRoute(router, licenseState);

const [config] = router.get.mock.calls[0];

expect(config.path).toMatchInlineSnapshot(`"/api/alert/_health"`);
});

it('queries the usage api', async () => {
const router: RouterMock = mockRouter.create();

const licenseState = mockLicenseState();
healthRoute(router, licenseState);
const [, handler] = router.get.mock.calls[0];

const elasticsearch = elasticsearchServiceMock.createSetup();
elasticsearch.adminClient.callAsInternalUser.mockReturnValue(Promise.resolve({}));

const [context, req, res] = mockHandlerArguments({ elasticsearch }, {}, ['ok']);

await handler(context, req, res);

expect(verifyApiAccess).toHaveBeenCalledWith(licenseState);

expect(elasticsearch.adminClient.callAsInternalUser.mock.calls[0]).toMatchInlineSnapshot(`
Array [
"transport.request",
Object {
"method": "GET",
"path": "/_xpack/usage",
},
]
`);
});

it('evaluates missing security info from the usage api to mean that the security plugin is disbled', async () => {
const router: RouterMock = mockRouter.create();

const licenseState = mockLicenseState();
healthRoute(router, licenseState);
const [, handler] = router.get.mock.calls[0];

const elasticsearch = elasticsearchServiceMock.createSetup();
elasticsearch.adminClient.callAsInternalUser.mockReturnValue(Promise.resolve({}));

const [context, req, res] = mockHandlerArguments({ elasticsearch }, {}, ['ok']);

expect(await handler(context, req, res)).toMatchInlineSnapshot(`
Object {
"body": Object {
"isSufficientlySecure": true,
},
}
`);
});

it('evaluates missing security http info from the usage api to mean that the security plugin is disbled', async () => {
const router: RouterMock = mockRouter.create();

const licenseState = mockLicenseState();
healthRoute(router, licenseState);
const [, handler] = router.get.mock.calls[0];

const elasticsearch = elasticsearchServiceMock.createSetup();
elasticsearch.adminClient.callAsInternalUser.mockReturnValue(Promise.resolve({ security: {} }));

const [context, req, res] = mockHandlerArguments({ elasticsearch }, {}, ['ok']);

expect(await handler(context, req, res)).toMatchInlineSnapshot(`
Object {
"body": Object {
"isSufficientlySecure": true,
},
}
`);
});

it('evaluates security enabled, and missing ssl info from the usage api to mean that the user cannot generate keys', async () => {
const router: RouterMock = mockRouter.create();

const licenseState = mockLicenseState();
healthRoute(router, licenseState);
const [, handler] = router.get.mock.calls[0];

const elasticsearch = elasticsearchServiceMock.createSetup();
elasticsearch.adminClient.callAsInternalUser.mockReturnValue(
Promise.resolve({ security: { enabled: true } })
);

const [context, req, res] = mockHandlerArguments({ elasticsearch }, {}, ['ok']);

expect(await handler(context, req, res)).toMatchInlineSnapshot(`
Object {
"body": Object {
"isSufficientlySecure": false,
},
}
`);
});

it('evaluates security enabled, SSL info present but missing http info from the usage api to mean that the user cannot generate keys', async () => {
const router: RouterMock = mockRouter.create();

const licenseState = mockLicenseState();
healthRoute(router, licenseState);
const [, handler] = router.get.mock.calls[0];

const elasticsearch = elasticsearchServiceMock.createSetup();
elasticsearch.adminClient.callAsInternalUser.mockReturnValue(
Promise.resolve({ security: { enabled: true, ssl: {} } })
);

const [context, req, res] = mockHandlerArguments({ elasticsearch }, {}, ['ok']);

expect(await handler(context, req, res)).toMatchInlineSnapshot(`
Object {
"body": Object {
"isSufficientlySecure": false,
},
}
`);
});

it('evaluates security and tls enabled to mean that the user can generate keys', async () => {
const router: RouterMock = mockRouter.create();

const licenseState = mockLicenseState();
healthRoute(router, licenseState);
const [, handler] = router.get.mock.calls[0];

const elasticsearch = elasticsearchServiceMock.createSetup();
elasticsearch.adminClient.callAsInternalUser.mockReturnValue(
Promise.resolve({ security: { enabled: true, ssl: { http: { enabled: true } } } })
);

const [context, req, res] = mockHandlerArguments({ elasticsearch }, {}, ['ok']);

expect(await handler(context, req, res)).toMatchInlineSnapshot(`
Object {
"body": Object {
"isSufficientlySecure": true,
},
}
`);
});
});
Loading

0 comments on commit 18793db

Please sign in to comment.