Invalidate alert API Key when generating a new one (#53732)
* Initial work to auto cleanup old API keys * Fix ESLint error * Rename confusing variables * Add test to ensure thrown errors are swallowed * Add more tests Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
47830c75d9
commit
8cc778a64a
|
@ -12,24 +12,33 @@ import { alertTypeRegistryMock } from './alert_type_registry.mock';
|
|||
import { TaskStatus } from '../../task_manager/server';
|
||||
import { IntervalSchedule } from './types';
|
||||
import { resolvable } from './test_utils';
|
||||
import { encryptedSavedObjectsMock } from '../../../../plugins/encrypted_saved_objects/server/mocks';
|
||||
|
||||
const taskManager = taskManagerMock.create();
|
||||
const alertTypeRegistry = alertTypeRegistryMock.create();
|
||||
const savedObjectsClient = savedObjectsClientMock.create();
|
||||
const encryptedSavedObjects = encryptedSavedObjectsMock.createStart();
|
||||
|
||||
const alertsClientParams = {
|
||||
taskManager,
|
||||
alertTypeRegistry,
|
||||
savedObjectsClient,
|
||||
spaceId: 'default',
|
||||
namespace: 'default',
|
||||
getUserName: jest.fn(),
|
||||
createAPIKey: jest.fn(),
|
||||
invalidateAPIKey: jest.fn(),
|
||||
logger: loggingServiceMock.create().get(),
|
||||
encryptedSavedObjectsPlugin: encryptedSavedObjects,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
alertsClientParams.createAPIKey.mockResolvedValue({ created: false });
|
||||
alertsClientParams.createAPIKey.mockResolvedValue({ apiKeysEnabled: false });
|
||||
alertsClientParams.invalidateAPIKey.mockResolvedValue({
|
||||
apiKeysEnabled: true,
|
||||
result: { error_count: 0 },
|
||||
});
|
||||
alertsClientParams.getUserName.mockResolvedValue('elastic');
|
||||
taskManager.runNow.mockResolvedValue({ id: '' });
|
||||
});
|
||||
|
@ -725,7 +734,7 @@ describe('create()', () => {
|
|||
async executor() {},
|
||||
});
|
||||
alertsClientParams.createAPIKey.mockResolvedValueOnce({
|
||||
created: true,
|
||||
apiKeysEnabled: true,
|
||||
result: { id: '123', api_key: 'abc' },
|
||||
});
|
||||
savedObjectsClient.bulkGet.mockResolvedValueOnce({
|
||||
|
@ -841,7 +850,7 @@ describe('create()', () => {
|
|||
describe('enable()', () => {
|
||||
test('enables an alert', async () => {
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
|
@ -900,7 +909,7 @@ describe('enable()', () => {
|
|||
|
||||
test(`doesn't enable already enabled alerts`, async () => {
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
|
@ -918,7 +927,7 @@ describe('enable()', () => {
|
|||
|
||||
test('calls the API key function', async () => {
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
|
@ -943,7 +952,7 @@ describe('enable()', () => {
|
|||
ownerId: null,
|
||||
});
|
||||
alertsClientParams.createAPIKey.mockResolvedValueOnce({
|
||||
created: true,
|
||||
apiKeysEnabled: true,
|
||||
result: { id: '123', api_key: 'abc' },
|
||||
});
|
||||
|
||||
|
@ -978,6 +987,41 @@ describe('enable()', () => {
|
|||
scope: ['alerting'],
|
||||
});
|
||||
});
|
||||
|
||||
test('swallows error when invalidate API key throws', async () => {
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail'));
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
schedule: { interval: '10s' },
|
||||
alertTypeId: '2',
|
||||
enabled: false,
|
||||
apiKey: Buffer.from('123:abc').toString('base64'),
|
||||
},
|
||||
version: '123',
|
||||
references: [],
|
||||
});
|
||||
taskManager.schedule.mockResolvedValueOnce({
|
||||
id: 'task-123',
|
||||
scheduledAt: new Date(),
|
||||
attempts: 0,
|
||||
status: TaskStatus.Idle,
|
||||
runAt: new Date(),
|
||||
state: {},
|
||||
params: {},
|
||||
taskType: '',
|
||||
startedAt: null,
|
||||
retryAt: null,
|
||||
ownerId: null,
|
||||
});
|
||||
|
||||
await alertsClient.enable({ id: '1' });
|
||||
expect(alertsClientParams.logger.error).toHaveBeenCalledWith(
|
||||
'Failed to invalidate API Key: Fail'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('disable()', () => {
|
||||
|
@ -1390,7 +1434,7 @@ describe('find()', () => {
|
|||
describe('delete()', () => {
|
||||
test('successfully removes an alert', async () => {
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
|
@ -1437,6 +1481,48 @@ describe('delete()', () => {
|
|||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('swallows error when invalidate API key throws', async () => {
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail'));
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
alertTypeId: '123',
|
||||
schedule: { interval: '10s' },
|
||||
params: {
|
||||
bar: true,
|
||||
},
|
||||
apiKey: Buffer.from('123:abc').toString('base64'),
|
||||
scheduledTaskId: 'task-123',
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
actionRef: 'action_0',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
references: [
|
||||
{
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
});
|
||||
savedObjectsClient.delete.mockResolvedValueOnce({
|
||||
success: true,
|
||||
});
|
||||
|
||||
await alertsClient.delete({ id: '1' });
|
||||
expect(alertsClientParams.logger.error).toHaveBeenCalledWith(
|
||||
'Failed to invalidate API Key: Fail'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update()', () => {
|
||||
|
@ -1448,7 +1534,7 @@ describe('update()', () => {
|
|||
actionGroups: ['default'],
|
||||
async executor() {},
|
||||
});
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
|
@ -1603,7 +1689,7 @@ describe('update()', () => {
|
|||
actionGroups: ['default'],
|
||||
async executor() {},
|
||||
});
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
|
@ -1786,7 +1872,7 @@ describe('update()', () => {
|
|||
actionGroups: ['default'],
|
||||
async executor() {},
|
||||
});
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
|
@ -1810,7 +1896,7 @@ describe('update()', () => {
|
|||
],
|
||||
});
|
||||
alertsClientParams.createAPIKey.mockResolvedValueOnce({
|
||||
created: true,
|
||||
apiKeysEnabled: true,
|
||||
result: { id: '123', api_key: 'abc' },
|
||||
});
|
||||
savedObjectsClient.update.mockResolvedValueOnce({
|
||||
|
@ -1952,7 +2038,7 @@ describe('update()', () => {
|
|||
},
|
||||
async executor() {},
|
||||
});
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
|
@ -1986,6 +2072,93 @@ describe('update()', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('swallows error when invalidate API key throws', async () => {
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail'));
|
||||
alertTypeRegistry.get.mockReturnValueOnce({
|
||||
id: '123',
|
||||
name: 'Test',
|
||||
actionGroups: ['default'],
|
||||
async executor() {},
|
||||
});
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
enabled: true,
|
||||
alertTypeId: '123',
|
||||
scheduledTaskId: 'task-123',
|
||||
apiKey: Buffer.from('123:abc').toString('base64'),
|
||||
},
|
||||
references: [],
|
||||
version: '123',
|
||||
});
|
||||
savedObjectsClient.bulkGet.mockResolvedValueOnce({
|
||||
saved_objects: [
|
||||
{
|
||||
id: '1',
|
||||
type: 'action',
|
||||
attributes: {
|
||||
actionTypeId: 'test',
|
||||
},
|
||||
references: [],
|
||||
},
|
||||
],
|
||||
});
|
||||
savedObjectsClient.update.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
enabled: true,
|
||||
schedule: { interval: '10s' },
|
||||
params: {
|
||||
bar: true,
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
actionRef: 'action_0',
|
||||
actionTypeId: 'test',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
scheduledTaskId: 'task-123',
|
||||
},
|
||||
references: [
|
||||
{
|
||||
name: 'action_0',
|
||||
type: 'action',
|
||||
id: '1',
|
||||
},
|
||||
],
|
||||
});
|
||||
await alertsClient.update({
|
||||
id: '1',
|
||||
data: {
|
||||
schedule: { interval: '10s' },
|
||||
name: 'abc',
|
||||
tags: ['foo'],
|
||||
params: {
|
||||
bar: true,
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: '1',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(alertsClientParams.logger.error).toHaveBeenCalledWith(
|
||||
'Failed to invalidate API Key: Fail'
|
||||
);
|
||||
});
|
||||
|
||||
describe('updating an alert schedule', () => {
|
||||
function mockApiCalls(
|
||||
alertId: string,
|
||||
|
@ -2012,7 +2185,7 @@ describe('update()', () => {
|
|||
},
|
||||
],
|
||||
});
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: alertId,
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
|
@ -2212,7 +2385,7 @@ describe('update()', () => {
|
|||
describe('updateApiKey()', () => {
|
||||
test('updates the API key for the alert', async () => {
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
|
@ -2224,7 +2397,7 @@ describe('updateApiKey()', () => {
|
|||
references: [],
|
||||
});
|
||||
alertsClientParams.createAPIKey.mockResolvedValueOnce({
|
||||
created: true,
|
||||
apiKeysEnabled: true,
|
||||
result: { id: '123', api_key: 'abc' },
|
||||
});
|
||||
|
||||
|
@ -2243,4 +2416,30 @@ describe('updateApiKey()', () => {
|
|||
{ version: '123' }
|
||||
);
|
||||
});
|
||||
|
||||
test('swallows error when invalidate API key throws', async () => {
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
alertsClientParams.invalidateAPIKey.mockRejectedValue(new Error('Fail'));
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
schedule: { interval: '10s' },
|
||||
alertTypeId: '2',
|
||||
enabled: true,
|
||||
apiKey: Buffer.from('123:abc').toString('base64'),
|
||||
},
|
||||
version: '123',
|
||||
references: [],
|
||||
});
|
||||
alertsClientParams.createAPIKey.mockResolvedValueOnce({
|
||||
apiKeysEnabled: true,
|
||||
result: { id: '123', api_key: 'abc' },
|
||||
});
|
||||
|
||||
await alertsClient.updateApiKey({ id: '1' });
|
||||
expect(alertsClientParams.logger.error).toHaveBeenCalledWith(
|
||||
'Failed to invalidate API Key: Fail'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -23,26 +23,32 @@ import {
|
|||
} from './types';
|
||||
import { TaskManagerStartContract } from './shim';
|
||||
import { validateAlertTypeParams } from './lib';
|
||||
import { CreateAPIKeyResult as SecurityPluginCreateAPIKeyResult } from '../../../../plugins/security/server';
|
||||
import {
|
||||
InvalidateAPIKeyParams,
|
||||
CreateAPIKeyResult as SecurityPluginCreateAPIKeyResult,
|
||||
InvalidateAPIKeyResult as SecurityPluginInvalidateAPIKeyResult,
|
||||
} from '../../../../plugins/security/server';
|
||||
import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../../plugins/encrypted_saved_objects/server';
|
||||
|
||||
interface FailedCreateAPIKeyResult {
|
||||
created: false;
|
||||
}
|
||||
interface SuccessCreateAPIKeyResult {
|
||||
created: true;
|
||||
result: SecurityPluginCreateAPIKeyResult;
|
||||
}
|
||||
export type CreateAPIKeyResult = FailedCreateAPIKeyResult | SuccessCreateAPIKeyResult;
|
||||
type NormalizedAlertAction = Omit<AlertAction, 'actionTypeId'>;
|
||||
export type CreateAPIKeyResult =
|
||||
| { apiKeysEnabled: false }
|
||||
| { apiKeysEnabled: true; result: SecurityPluginCreateAPIKeyResult };
|
||||
export type InvalidateAPIKeyResult =
|
||||
| { apiKeysEnabled: false }
|
||||
| { apiKeysEnabled: true; result: SecurityPluginInvalidateAPIKeyResult };
|
||||
|
||||
interface ConstructorOptions {
|
||||
logger: Logger;
|
||||
taskManager: TaskManagerStartContract;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
alertTypeRegistry: AlertTypeRegistry;
|
||||
encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract;
|
||||
spaceId?: string;
|
||||
namespace?: string;
|
||||
getUserName: () => Promise<string | null>;
|
||||
createAPIKey: () => Promise<CreateAPIKeyResult>;
|
||||
invalidateAPIKey: (params: InvalidateAPIKeyParams) => Promise<InvalidateAPIKeyResult>;
|
||||
}
|
||||
|
||||
export interface FindOptions {
|
||||
|
@ -106,10 +112,15 @@ export class AlertsClient {
|
|||
private readonly logger: Logger;
|
||||
private readonly getUserName: () => Promise<string | null>;
|
||||
private readonly spaceId?: string;
|
||||
private readonly namespace?: string;
|
||||
private readonly taskManager: TaskManagerStartContract;
|
||||
private readonly savedObjectsClient: SavedObjectsClientContract;
|
||||
private readonly alertTypeRegistry: AlertTypeRegistry;
|
||||
private readonly createAPIKey: () => Promise<CreateAPIKeyResult>;
|
||||
private readonly invalidateAPIKey: (
|
||||
params: InvalidateAPIKeyParams
|
||||
) => Promise<InvalidateAPIKeyResult>;
|
||||
encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract;
|
||||
|
||||
constructor({
|
||||
alertTypeRegistry,
|
||||
|
@ -117,16 +128,22 @@ export class AlertsClient {
|
|||
taskManager,
|
||||
logger,
|
||||
spaceId,
|
||||
namespace,
|
||||
getUserName,
|
||||
createAPIKey,
|
||||
invalidateAPIKey,
|
||||
encryptedSavedObjectsPlugin,
|
||||
}: ConstructorOptions) {
|
||||
this.logger = logger;
|
||||
this.getUserName = getUserName;
|
||||
this.spaceId = spaceId;
|
||||
this.namespace = namespace;
|
||||
this.taskManager = taskManager;
|
||||
this.alertTypeRegistry = alertTypeRegistry;
|
||||
this.savedObjectsClient = savedObjectsClient;
|
||||
this.createAPIKey = createAPIKey;
|
||||
this.invalidateAPIKey = invalidateAPIKey;
|
||||
this.encryptedSavedObjectsPlugin = encryptedSavedObjectsPlugin;
|
||||
}
|
||||
|
||||
public async create({ data, options }: CreateOptions) {
|
||||
|
@ -206,21 +223,26 @@ export class AlertsClient {
|
|||
}
|
||||
|
||||
public async delete({ id }: { id: string }) {
|
||||
const alertSavedObject = await this.savedObjectsClient.get('alert', id);
|
||||
const decryptedAlertSavedObject = await this.encryptedSavedObjectsPlugin.getDecryptedAsInternalUser<
|
||||
RawAlert
|
||||
>('alert', id, { namespace: this.namespace });
|
||||
const removeResult = await this.savedObjectsClient.delete('alert', id);
|
||||
if (alertSavedObject.attributes.scheduledTaskId) {
|
||||
await this.taskManager.remove(alertSavedObject.attributes.scheduledTaskId);
|
||||
if (decryptedAlertSavedObject.attributes.scheduledTaskId) {
|
||||
await this.taskManager.remove(decryptedAlertSavedObject.attributes.scheduledTaskId);
|
||||
}
|
||||
await this.invalidateApiKey({ apiKey: decryptedAlertSavedObject.attributes.apiKey });
|
||||
return removeResult;
|
||||
}
|
||||
|
||||
public async update({ id, data }: UpdateOptions) {
|
||||
const alert = await this.savedObjectsClient.get<RawAlert>('alert', id);
|
||||
const updateResult = await this.updateAlert({ id, data }, alert);
|
||||
const decryptedAlertSavedObject = await this.encryptedSavedObjectsPlugin.getDecryptedAsInternalUser<
|
||||
RawAlert
|
||||
>('alert', id, { namespace: this.namespace });
|
||||
const updateResult = await this.updateAlert({ id, data }, decryptedAlertSavedObject);
|
||||
|
||||
if (
|
||||
updateResult.scheduledTaskId &&
|
||||
!isEqual(alert.attributes.schedule, updateResult.schedule)
|
||||
!isEqual(decryptedAlertSavedObject.attributes.schedule, updateResult.schedule)
|
||||
) {
|
||||
this.taskManager.runNow(updateResult.scheduledTaskId).catch((err: Error) => {
|
||||
this.logger.error(
|
||||
|
@ -262,6 +284,9 @@ export class AlertsClient {
|
|||
references,
|
||||
}
|
||||
);
|
||||
|
||||
await this.invalidateApiKey({ apiKey: attributes.apiKey });
|
||||
|
||||
return this.getAlertFromRaw(
|
||||
id,
|
||||
updatedObject.attributes,
|
||||
|
@ -274,7 +299,7 @@ export class AlertsClient {
|
|||
apiKey: CreateAPIKeyResult,
|
||||
username: string | null
|
||||
): Pick<RawAlert, 'apiKey' | 'apiKeyOwner'> {
|
||||
return apiKey.created
|
||||
return apiKey.apiKeysEnabled
|
||||
? {
|
||||
apiKeyOwner: username,
|
||||
apiKey: Buffer.from(`${apiKey.result.id}:${apiKey.result.api_key}`).toString('base64'),
|
||||
|
@ -286,7 +311,12 @@ export class AlertsClient {
|
|||
}
|
||||
|
||||
public async updateApiKey({ id }: { id: string }) {
|
||||
const { version, attributes } = await this.savedObjectsClient.get('alert', id);
|
||||
const {
|
||||
version,
|
||||
attributes,
|
||||
} = await this.encryptedSavedObjectsPlugin.getDecryptedAsInternalUser<RawAlert>('alert', id, {
|
||||
namespace: this.namespace,
|
||||
});
|
||||
|
||||
const username = await this.getUserName();
|
||||
await this.savedObjectsClient.update(
|
||||
|
@ -299,10 +329,36 @@ export class AlertsClient {
|
|||
},
|
||||
{ version }
|
||||
);
|
||||
|
||||
await this.invalidateApiKey({ apiKey: attributes.apiKey });
|
||||
}
|
||||
|
||||
private async invalidateApiKey({ apiKey }: { apiKey: string | null }): Promise<void> {
|
||||
if (!apiKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const apiKeyId = Buffer.from(apiKey, 'base64')
|
||||
.toString()
|
||||
.split(':')[0];
|
||||
const response = await this.invalidateAPIKey({ id: apiKeyId });
|
||||
if (response.apiKeysEnabled === true && response.result.error_count > 0) {
|
||||
this.logger.error(`Failed to invalidate API Key [id="${apiKeyId}"]`);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.error(`Failed to invalidate API Key: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async enable({ id }: { id: string }) {
|
||||
const { attributes, version } = await this.savedObjectsClient.get('alert', id);
|
||||
const {
|
||||
version,
|
||||
attributes,
|
||||
} = await this.encryptedSavedObjectsPlugin.getDecryptedAsInternalUser<RawAlert>('alert', id, {
|
||||
namespace: this.namespace,
|
||||
});
|
||||
|
||||
if (attributes.enabled === false) {
|
||||
const scheduledTask = await this.scheduleAlert(id, attributes.alertTypeId);
|
||||
const username = await this.getUserName();
|
||||
|
@ -319,6 +375,7 @@ export class AlertsClient {
|
|||
},
|
||||
{ version }
|
||||
);
|
||||
await this.invalidateApiKey({ apiKey: attributes.apiKey });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import { alertTypeRegistryMock } from '../alert_type_registry.mock';
|
|||
import { taskManagerMock } from '../../../task_manager/server/task_manager.mock';
|
||||
import { KibanaRequest } from '../../../../../../src/core/server';
|
||||
import { loggingServiceMock } from '../../../../../../src/core/server/mocks';
|
||||
import { encryptedSavedObjectsMock } from '../../../../../plugins/encrypted_saved_objects/server/mocks';
|
||||
|
||||
jest.mock('../alerts_client');
|
||||
|
||||
|
@ -25,6 +26,8 @@ const alertsClientFactoryParams: jest.Mocked<ConstructorOpts> = {
|
|||
taskManager: taskManagerMock.create(),
|
||||
alertTypeRegistry: alertTypeRegistryMock.create(),
|
||||
getSpaceId: jest.fn(),
|
||||
spaceIdToNamespace: jest.fn(),
|
||||
encryptedSavedObjectsPlugin: encryptedSavedObjectsMock.createStart(),
|
||||
};
|
||||
const fakeRequest: Request = {
|
||||
headers: {},
|
||||
|
@ -45,6 +48,7 @@ const fakeRequest: Request = {
|
|||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
alertsClientFactoryParams.getSpaceId.mockReturnValue('default');
|
||||
alertsClientFactoryParams.spaceIdToNamespace.mockReturnValue('default');
|
||||
});
|
||||
|
||||
test('creates an alerts client with proper constructor arguments', async () => {
|
||||
|
@ -57,8 +61,11 @@ test('creates an alerts client with proper constructor arguments', async () => {
|
|||
taskManager: alertsClientFactoryParams.taskManager,
|
||||
alertTypeRegistry: alertsClientFactoryParams.alertTypeRegistry,
|
||||
spaceId: 'default',
|
||||
namespace: 'default',
|
||||
getUserName: expect.any(Function),
|
||||
createAPIKey: expect.any(Function),
|
||||
invalidateAPIKey: expect.any(Function),
|
||||
encryptedSavedObjectsPlugin: alertsClientFactoryParams.encryptedSavedObjectsPlugin,
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -84,23 +91,23 @@ test('getUserName() returns a name when security is enabled', async () => {
|
|||
expect(userNameResult).toEqual('bob');
|
||||
});
|
||||
|
||||
test('createAPIKey() returns { created: false } when security is disabled', async () => {
|
||||
test('createAPIKey() returns { apiKeysEnabled: false } when security is disabled', async () => {
|
||||
const factory = new AlertsClientFactory(alertsClientFactoryParams);
|
||||
factory.create(KibanaRequest.from(fakeRequest), fakeRequest);
|
||||
const constructorCall = jest.requireMock('../alerts_client').AlertsClient.mock.calls[0][0];
|
||||
|
||||
const createAPIKeyResult = await constructorCall.createAPIKey();
|
||||
expect(createAPIKeyResult).toEqual({ created: false });
|
||||
expect(createAPIKeyResult).toEqual({ apiKeysEnabled: false });
|
||||
});
|
||||
|
||||
test('createAPIKey() returns { created: false } when security is enabled but ES security is disabled', async () => {
|
||||
test('createAPIKey() returns { apiKeysEnabled: false } when security is enabled but ES security is disabled', async () => {
|
||||
const factory = new AlertsClientFactory(alertsClientFactoryParams);
|
||||
factory.create(KibanaRequest.from(fakeRequest), fakeRequest);
|
||||
const constructorCall = jest.requireMock('../alerts_client').AlertsClient.mock.calls[0][0];
|
||||
|
||||
securityPluginSetup.authc.createAPIKey.mockResolvedValueOnce(null);
|
||||
const createAPIKeyResult = await constructorCall.createAPIKey();
|
||||
expect(createAPIKeyResult).toEqual({ created: false });
|
||||
expect(createAPIKeyResult).toEqual({ apiKeysEnabled: false });
|
||||
});
|
||||
|
||||
test('createAPIKey() returns an API key when security is enabled', async () => {
|
||||
|
@ -113,7 +120,10 @@ test('createAPIKey() returns an API key when security is enabled', async () => {
|
|||
|
||||
securityPluginSetup.authc.createAPIKey.mockResolvedValueOnce({ api_key: '123', id: 'abc' });
|
||||
const createAPIKeyResult = await constructorCall.createAPIKey();
|
||||
expect(createAPIKeyResult).toEqual({ created: true, result: { api_key: '123', id: 'abc' } });
|
||||
expect(createAPIKeyResult).toEqual({
|
||||
apiKeysEnabled: true,
|
||||
result: { api_key: '123', id: 'abc' },
|
||||
});
|
||||
});
|
||||
|
||||
test('createAPIKey() throws when security plugin createAPIKey throws an error', async () => {
|
||||
|
|
|
@ -6,10 +6,12 @@
|
|||
|
||||
import Hapi from 'hapi';
|
||||
import uuid from 'uuid';
|
||||
import { AlertTypeRegistry } from '../types';
|
||||
import { AlertsClient } from '../alerts_client';
|
||||
import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from '../types';
|
||||
import { SecurityPluginStartContract, TaskManagerStartContract } from '../shim';
|
||||
import { KibanaRequest, Logger } from '../../../../../../src/core/server';
|
||||
import { InvalidateAPIKeyParams } from '../../../../../plugins/security/server';
|
||||
import { PluginStartContract as EncryptedSavedObjectsStartContract } from '../../../../../plugins/encrypted_saved_objects/server';
|
||||
|
||||
export interface ConstructorOpts {
|
||||
logger: Logger;
|
||||
|
@ -17,6 +19,8 @@ export interface ConstructorOpts {
|
|||
alertTypeRegistry: AlertTypeRegistry;
|
||||
securityPluginSetup?: SecurityPluginStartContract;
|
||||
getSpaceId: (request: Hapi.Request) => string | undefined;
|
||||
spaceIdToNamespace: SpaceIdToNamespaceFunction;
|
||||
encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract;
|
||||
}
|
||||
|
||||
export class AlertsClientFactory {
|
||||
|
@ -25,6 +29,8 @@ export class AlertsClientFactory {
|
|||
private readonly alertTypeRegistry: AlertTypeRegistry;
|
||||
private readonly securityPluginSetup?: SecurityPluginStartContract;
|
||||
private readonly getSpaceId: (request: Hapi.Request) => string | undefined;
|
||||
private readonly spaceIdToNamespace: SpaceIdToNamespaceFunction;
|
||||
private readonly encryptedSavedObjectsPlugin: EncryptedSavedObjectsStartContract;
|
||||
|
||||
constructor(options: ConstructorOpts) {
|
||||
this.logger = options.logger;
|
||||
|
@ -32,16 +38,21 @@ export class AlertsClientFactory {
|
|||
this.taskManager = options.taskManager;
|
||||
this.alertTypeRegistry = options.alertTypeRegistry;
|
||||
this.securityPluginSetup = options.securityPluginSetup;
|
||||
this.spaceIdToNamespace = options.spaceIdToNamespace;
|
||||
this.encryptedSavedObjectsPlugin = options.encryptedSavedObjectsPlugin;
|
||||
}
|
||||
|
||||
public create(request: KibanaRequest, legacyRequest: Hapi.Request): AlertsClient {
|
||||
const { securityPluginSetup } = this;
|
||||
const spaceId = this.getSpaceId(legacyRequest);
|
||||
return new AlertsClient({
|
||||
spaceId,
|
||||
logger: this.logger,
|
||||
taskManager: this.taskManager,
|
||||
alertTypeRegistry: this.alertTypeRegistry,
|
||||
savedObjectsClient: legacyRequest.getSavedObjectsClient(),
|
||||
spaceId: this.getSpaceId(legacyRequest),
|
||||
namespace: this.spaceIdToNamespace(spaceId),
|
||||
encryptedSavedObjectsPlugin: this.encryptedSavedObjectsPlugin,
|
||||
async getUserName() {
|
||||
if (!securityPluginSetup) {
|
||||
return null;
|
||||
|
@ -51,20 +62,37 @@ export class AlertsClientFactory {
|
|||
},
|
||||
async createAPIKey() {
|
||||
if (!securityPluginSetup) {
|
||||
return { created: false };
|
||||
return { apiKeysEnabled: false };
|
||||
}
|
||||
const createAPIKeyResult = await securityPluginSetup.authc.createAPIKey(request, {
|
||||
name: `source: alerting, generated uuid: "${uuid.v4()}"`,
|
||||
role_descriptors: {},
|
||||
});
|
||||
if (!createAPIKeyResult) {
|
||||
return { created: false };
|
||||
return { apiKeysEnabled: false };
|
||||
}
|
||||
return {
|
||||
created: true,
|
||||
apiKeysEnabled: true,
|
||||
result: createAPIKeyResult,
|
||||
};
|
||||
},
|
||||
async invalidateAPIKey(params: InvalidateAPIKeyParams) {
|
||||
if (!securityPluginSetup) {
|
||||
return { apiKeysEnabled: false };
|
||||
}
|
||||
const invalidateAPIKeyResult = await securityPluginSetup.authc.invalidateAPIKey(
|
||||
request,
|
||||
params
|
||||
);
|
||||
// Null when Elasticsearch security is disabled
|
||||
if (!invalidateAPIKeyResult) {
|
||||
return { apiKeysEnabled: false };
|
||||
}
|
||||
return {
|
||||
apiKeysEnabled: true,
|
||||
result: invalidateAPIKeyResult,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -107,11 +107,18 @@ export class Plugin {
|
|||
public start(core: AlertingCoreStart, plugins: AlertingPluginsStart): PluginStartContract {
|
||||
const { adminClient, serverBasePath } = this;
|
||||
|
||||
function spaceIdToNamespace(spaceId?: string): string | undefined {
|
||||
const spacesPlugin = plugins.spaces();
|
||||
return spacesPlugin && spaceId ? spacesPlugin.spaceIdToNamespace(spaceId) : undefined;
|
||||
}
|
||||
|
||||
const alertsClientFactory = new AlertsClientFactory({
|
||||
alertTypeRegistry: this.alertTypeRegistry!,
|
||||
logger: this.logger,
|
||||
taskManager: plugins.task_manager,
|
||||
securityPluginSetup: plugins.security,
|
||||
encryptedSavedObjectsPlugin: plugins.encryptedSavedObjects,
|
||||
spaceIdToNamespace,
|
||||
getSpaceId(request: Hapi.Request) {
|
||||
const spacesPlugin = plugins.spaces();
|
||||
return spacesPlugin ? spacesPlugin.getSpaceId(request) : undefined;
|
||||
|
@ -127,12 +134,9 @@ export class Plugin {
|
|||
savedObjectsClient: core.savedObjects.getScopedSavedObjectsClient(request),
|
||||
};
|
||||
},
|
||||
spaceIdToNamespace,
|
||||
executeAction: plugins.actions.execute,
|
||||
encryptedSavedObjectsPlugin: plugins.encryptedSavedObjects,
|
||||
spaceIdToNamespace(spaceId?: string): string | undefined {
|
||||
const spacesPlugin = plugins.spaces();
|
||||
return spacesPlugin && spaceId ? spacesPlugin.spaceIdToNamespace(spaceId) : undefined;
|
||||
},
|
||||
getBasePath(spaceId?: string): string {
|
||||
const spacesPlugin = plugins.spaces();
|
||||
return spacesPlugin && spaceId ? spacesPlugin.getBasePath(spaceId) : serverBasePath!;
|
||||
|
|
|
@ -10,7 +10,13 @@ import { Plugin } from './plugin';
|
|||
|
||||
// These exports are part of public Security plugin contract, any change in signature of exported
|
||||
// functions or removal of exports should be considered as a breaking change.
|
||||
export { AuthenticationResult, DeauthenticationResult, CreateAPIKeyResult } from './authentication';
|
||||
export {
|
||||
AuthenticationResult,
|
||||
DeauthenticationResult,
|
||||
CreateAPIKeyResult,
|
||||
InvalidateAPIKeyParams,
|
||||
InvalidateAPIKeyResult,
|
||||
} from './authentication';
|
||||
export { PluginSetupContract } from './plugin';
|
||||
|
||||
export const config = { schema: ConfigSchema };
|
||||
|
|
Loading…
Reference in a new issue