Add alerting route to update API key (#45925) (#46274)

* Add route to update alert api key

* Allow updating an API key when the alert is disabled

* Fix typecheck failures

* Fix broken file references
This commit is contained in:
Mike Côté 2019-09-23 09:02:50 -04:00 committed by GitHub
parent 9a11739fdf
commit 979fd4686e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 278 additions and 0 deletions

View file

@ -17,6 +17,7 @@ const createAlertsClientMock = () => {
update: jest.fn(),
enable: jest.fn(),
disable: jest.fn(),
updateApiKey: jest.fn(),
};
return mocked;
};

View file

@ -1247,3 +1247,35 @@ describe('update()', () => {
);
});
});
describe('updateApiKey()', () => {
test('updates the API key for the alert', async () => {
const alertsClient = new AlertsClient(alertsClientParams);
savedObjectsClient.get.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
interval: '10s',
alertTypeId: '2',
enabled: true,
},
references: [],
});
alertsClientParams.createAPIKey.mockResolvedValueOnce({
created: true,
result: { id: '123', api_key: 'abc' },
});
await alertsClient.updateApiKey({ id: '1' });
expect(savedObjectsClient.update).toHaveBeenCalledWith(
'alert',
'1',
{
apiKey: Buffer.from('123:abc').toString('base64'),
apiKeyOwner: 'elastic',
updatedBy: 'elastic',
},
{ references: [] }
);
});
});

View file

@ -221,6 +221,27 @@ export class AlertsClient {
return this.getAlertFromRaw(id, updatedObject.attributes, updatedObject.references);
}
public async updateApiKey({ id }: { id: string }) {
const { references } = await this.savedObjectsClient.get('alert', id);
const apiKey = await this.createAPIKey();
const username = await this.getUserName();
await this.savedObjectsClient.update(
'alert',
id,
{
updatedBy: username,
apiKeyOwner: apiKey.created ? username : null,
apiKey: apiKey.created
? Buffer.from(`${apiKey.result.id}:${apiKey.result.api_key}`).toString('base64')
: null,
},
{
references,
}
);
}
public async enable({ id }: { id: string }) {
const existingObject = await this.savedObjectsClient.get('alert', id);
if (existingObject.attributes.enabled === false) {

View file

@ -27,6 +27,7 @@ import {
updateAlertRoute,
enableAlertRoute,
disableAlertRoute,
updateApiKeyRoute,
} from './routes';
// Extend PluginProperties to indicate which plugins are guaranteed to exist
@ -125,6 +126,7 @@ export function init(server: Server) {
updateAlertRoute(server);
enableAlertRoute(server);
disableAlertRoute(server);
updateApiKeyRoute(server);
// Expose functions
server.decorate('request', 'getAlertsClient', function() {

View file

@ -12,3 +12,4 @@ export { listAlertTypesRoute } from './list_alert_types';
export { updateAlertRoute } from './update';
export { enableAlertRoute } from './enable';
export { disableAlertRoute } from './disable';
export { updateApiKeyRoute } from './update_api_key';

View file

@ -0,0 +1,22 @@
/*
* 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 { createMockServer } from './_mock_server';
import { updateApiKeyRoute } from './update_api_key';
const { server, alertsClient } = createMockServer();
updateApiKeyRoute(server);
test('updates api key for an alert', async () => {
const request = {
method: 'POST',
url: '/api/alert/1/_update_api_key',
};
const { statusCode } = await server.inject(request);
expect(statusCode).toBe(204);
expect(alertsClient.updateApiKey).toHaveBeenCalledWith({ id: '1' });
});

View file

@ -0,0 +1,25 @@
/*
* 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 Hapi from 'hapi';
export function updateApiKeyRoute(server: Hapi.Server) {
server.route({
method: 'POST',
path: '/api/alert/{id}/_update_api_key',
options: {
tags: ['access:alerting-all'],
response: {
emptyStatusCode: 204,
},
},
async handler(request: Hapi.Request, h: Hapi.ResponseToolkit) {
const alertsClient = request.getAlertsClient!();
await alertsClient.updateApiKey({ id: request.params.id });
return h.response();
},
});
}

View file

@ -17,6 +17,7 @@ export default function alertingTests({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./get'));
loadTestFile(require.resolve('./list_alert_types'));
loadTestFile(require.resolve('./update'));
loadTestFile(require.resolve('./update_api_key'));
loadTestFile(require.resolve('./alerts'));
});
}

View file

@ -0,0 +1,109 @@
/*
* 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 expect from '@kbn/expect';
import { UserAtSpaceScenarios, Spaces } from '../../scenarios';
import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
// eslint-disable-next-line import/no-default-export
export default function createUpdateApiKeyTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const supertestWithoutAuth = getService('supertestWithoutAuth');
describe('update_api_key', () => {
const objectRemover = new ObjectRemover(supertest);
const OtherSpace = Spaces.find(space => space.id === 'other');
if (!OtherSpace) {
throw new Error('Space "other" not defined in scenarios');
}
after(() => objectRemover.removeAll());
for (const scenario of UserAtSpaceScenarios) {
const { user, space } = scenario;
describe(scenario.id, () => {
it('should handle update alert api key request appropriately', async () => {
const { body: createdAlert } = await supertest
.post(`${getUrlPrefix(space.id)}/api/alert`)
.set('kbn-xsrf', 'foo')
.send(getTestAlertData())
.expect(200);
objectRemover.add(space.id, createdAlert.id, 'alert');
const response = await supertestWithoutAuth
.post(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}/_update_api_key`)
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password);
switch (scenario.id) {
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'global_read at space1':
expect(response.statusCode).to.eql(404);
expect(response.body).to.eql({
statusCode: 404,
error: 'Not Found',
message: 'Not Found',
});
break;
case 'superuser at space1':
case 'space_1_all at space1':
expect(response.statusCode).to.eql(204);
expect(response.body).to.eql('');
const { body: updatedAlert } = await supertestWithoutAuth
.get(`${getUrlPrefix(space.id)}/api/alert/${createdAlert.id}`)
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password)
.expect(200);
expect(updatedAlert.apiKeyOwner).to.eql(user.username);
break;
default:
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
}
});
it(`shouldn't update alert api key from another space`, async () => {
const { body: createdAlert } = await supertest
.post(`${getUrlPrefix(space.id)}/api/alert`)
.set('kbn-xsrf', 'foo')
.send(getTestAlertData())
.expect(200);
objectRemover.add(space.id, createdAlert.id, 'alert');
const response = await supertestWithoutAuth
.post(`${getUrlPrefix(OtherSpace.id)}/api/alert/${createdAlert.id}/_update_api_key`)
.set('kbn-xsrf', 'foo')
.auth(user.username, user.password);
expect(response.statusCode).to.eql(404);
switch (scenario.id) {
case 'no_kibana_privileges at space1':
case 'space_1_all at space2':
case 'global_read at space1':
case 'space_1_all at space1':
expect(response.body).to.eql({
statusCode: 404,
error: 'Not Found',
message: 'Not Found',
});
break;
case 'superuser at space1':
expect(response.body).to.eql({
statusCode: 404,
error: 'Not Found',
message: `Saved object [alert/${createdAlert.id}] not found`,
});
break;
default:
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
}
});
});
}
});
}

View file

@ -17,6 +17,7 @@ export default function alertingTests({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./get'));
loadTestFile(require.resolve('./list_alert_types'));
loadTestFile(require.resolve('./update'));
loadTestFile(require.resolve('./update_api_key'));
loadTestFile(require.resolve('./alerts'));
});
}

View file

@ -0,0 +1,63 @@
/*
* 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 expect from '@kbn/expect';
import { Spaces } from '../../scenarios';
import { getUrlPrefix, getTestAlertData, ObjectRemover } from '../../../common/lib';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
/**
* Eventhough security is disabled, this test checks the API behavior.
*/
// eslint-disable-next-line import/no-default-export
export default function createUpdateApiKeyTests({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
describe('update_api_key', () => {
const objectRemover = new ObjectRemover(supertest);
after(() => objectRemover.removeAll());
it('should handle update alert api key appropriately', async () => {
const { body: createdAlert } = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`)
.set('kbn-xsrf', 'foo')
.send(getTestAlertData())
.expect(200);
objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert');
await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}/_update_api_key`)
.set('kbn-xsrf', 'foo')
.expect(204, '');
const { body: updatedAlert } = await supertest
.get(`${getUrlPrefix(Spaces.space1.id)}/api/alert/${createdAlert.id}`)
.set('kbn-xsrf', 'foo')
.expect(200);
expect(updatedAlert.apiKeyOwner).to.eql(null);
});
it(`shouldn't update alert api key from another space`, async () => {
const { body: createdAlert } = await supertest
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alert`)
.set('kbn-xsrf', 'foo')
.send(getTestAlertData())
.expect(200);
objectRemover.add(Spaces.space1.id, createdAlert.id, 'alert');
await supertest
.post(`${getUrlPrefix(Spaces.other.id)}/api/alert/${createdAlert.id}/_update_api_key`)
.set('kbn-xsrf', 'foo')
.expect(404, {
statusCode: 404,
error: 'Not Found',
message: `Saved object [alert/${createdAlert.id}] not found`,
});
});
});
}