mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
Used SO for saving the API key IDs that should be deleted (#82211)
* Used SO for saving the API key IDs that should be deleted and create a configuration option where can set an execution interval for a TM task which will get the data from this SO and remove marked for delete keys. * removed invalidateApiKey from AlertsClient * Fixed type checks * Fixed jest tests * Removed test code * Changed SO name * fixed type cheks * Moved invalidate logic out of alerts client * fixed type check * Added functional tests * Fixed due to comments * added configurable delay for invalidation task * added interval to the task response * Fixed jest tests * Fixed due to comments * Fixed task * fixed paging * Fixed date filter * Fixed jest tests * fixed due to comments * fixed due to comments * Fixed e2e test * Fixed e2e test * Fixed due to comments. Changed api key invalidation task to use SavedObjectClient * Use encryptedSavedObjectClient * set back flaky test comment
This commit is contained in:
parent
2fb04a6d41
commit
8b658fbcd2
36 changed files with 847 additions and 126 deletions
|
@ -31,7 +31,6 @@ import {
|
|||
} from '../types';
|
||||
import { validateAlertTypeParams, alertExecutionStatusFromRaw } from '../lib';
|
||||
import {
|
||||
InvalidateAPIKeyParams,
|
||||
GrantAPIKeyResult as SecurityPluginGrantAPIKeyResult,
|
||||
InvalidateAPIKeyResult as SecurityPluginInvalidateAPIKeyResult,
|
||||
} from '../../../security/server';
|
||||
|
@ -48,6 +47,7 @@ import { IEvent } from '../../../event_log/server';
|
|||
import { parseDuration } from '../../common/parse_duration';
|
||||
import { retryIfConflicts } from '../lib/retry_if_conflicts';
|
||||
import { partiallyUpdateAlert } from '../saved_objects';
|
||||
import { markApiKeyForInvalidation } from '../invalidate_pending_api_keys/mark_api_key_for_invalidation';
|
||||
|
||||
export interface RegistryAlertTypeWithAuth extends RegistryAlertType {
|
||||
authorizedConsumers: string[];
|
||||
|
@ -72,7 +72,6 @@ export interface ConstructorOptions {
|
|||
namespace?: string;
|
||||
getUserName: () => Promise<string | null>;
|
||||
createAPIKey: (name: string) => Promise<CreateAPIKeyResult>;
|
||||
invalidateAPIKey: (params: InvalidateAPIKeyParams) => Promise<InvalidateAPIKeyResult>;
|
||||
getActionsClient: () => Promise<ActionsClient>;
|
||||
getEventLogClient: () => Promise<IEventLogClient>;
|
||||
kibanaVersion: PluginInitializerContext['env']['packageInfo']['version'];
|
||||
|
@ -172,9 +171,6 @@ export class AlertsClient {
|
|||
private readonly authorization: AlertsAuthorization;
|
||||
private readonly alertTypeRegistry: AlertTypeRegistry;
|
||||
private readonly createAPIKey: (name: string) => Promise<CreateAPIKeyResult>;
|
||||
private readonly invalidateAPIKey: (
|
||||
params: InvalidateAPIKeyParams
|
||||
) => Promise<InvalidateAPIKeyResult>;
|
||||
private readonly getActionsClient: () => Promise<ActionsClient>;
|
||||
private readonly actionsAuthorization: ActionsAuthorization;
|
||||
private readonly getEventLogClient: () => Promise<IEventLogClient>;
|
||||
|
@ -191,7 +187,6 @@ export class AlertsClient {
|
|||
namespace,
|
||||
getUserName,
|
||||
createAPIKey,
|
||||
invalidateAPIKey,
|
||||
encryptedSavedObjectsClient,
|
||||
getActionsClient,
|
||||
actionsAuthorization,
|
||||
|
@ -207,7 +202,6 @@ export class AlertsClient {
|
|||
this.unsecuredSavedObjectsClient = unsecuredSavedObjectsClient;
|
||||
this.authorization = authorization;
|
||||
this.createAPIKey = createAPIKey;
|
||||
this.invalidateAPIKey = invalidateAPIKey;
|
||||
this.encryptedSavedObjectsClient = encryptedSavedObjectsClient;
|
||||
this.getActionsClient = getActionsClient;
|
||||
this.actionsAuthorization = actionsAuthorization;
|
||||
|
@ -263,7 +257,11 @@ export class AlertsClient {
|
|||
);
|
||||
} catch (e) {
|
||||
// Avoid unused API key
|
||||
this.invalidateApiKey({ apiKey: rawAlert.apiKey });
|
||||
markApiKeyForInvalidation(
|
||||
{ apiKey: rawAlert.apiKey },
|
||||
this.logger,
|
||||
this.unsecuredSavedObjectsClient
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
if (data.enabled) {
|
||||
|
@ -487,7 +485,13 @@ export class AlertsClient {
|
|||
|
||||
await Promise.all([
|
||||
taskIdToRemove ? deleteTaskIfItExists(this.taskManager, taskIdToRemove) : null,
|
||||
apiKeyToInvalidate ? this.invalidateApiKey({ apiKey: apiKeyToInvalidate }) : null,
|
||||
apiKeyToInvalidate
|
||||
? markApiKeyForInvalidation(
|
||||
{ apiKey: apiKeyToInvalidate },
|
||||
this.logger,
|
||||
this.unsecuredSavedObjectsClient
|
||||
)
|
||||
: null,
|
||||
]);
|
||||
|
||||
return removeResult;
|
||||
|
@ -526,7 +530,11 @@ export class AlertsClient {
|
|||
|
||||
await Promise.all([
|
||||
alertSavedObject.attributes.apiKey
|
||||
? this.invalidateApiKey({ apiKey: alertSavedObject.attributes.apiKey })
|
||||
? markApiKeyForInvalidation(
|
||||
{ apiKey: alertSavedObject.attributes.apiKey },
|
||||
this.logger,
|
||||
this.unsecuredSavedObjectsClient
|
||||
)
|
||||
: null,
|
||||
(async () => {
|
||||
if (
|
||||
|
@ -591,7 +599,11 @@ export class AlertsClient {
|
|||
);
|
||||
} catch (e) {
|
||||
// Avoid unused API key
|
||||
this.invalidateApiKey({ apiKey: createAttributes.apiKey });
|
||||
markApiKeyForInvalidation(
|
||||
{ apiKey: createAttributes.apiKey },
|
||||
this.logger,
|
||||
this.unsecuredSavedObjectsClient
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
|
||||
|
@ -671,28 +683,20 @@ export class AlertsClient {
|
|||
await this.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, { version });
|
||||
} catch (e) {
|
||||
// Avoid unused API key
|
||||
this.invalidateApiKey({ apiKey: updateAttributes.apiKey });
|
||||
markApiKeyForInvalidation(
|
||||
{ apiKey: updateAttributes.apiKey },
|
||||
this.logger,
|
||||
this.unsecuredSavedObjectsClient
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
|
||||
if (apiKeyToInvalidate) {
|
||||
await this.invalidateApiKey({ apiKey: apiKeyToInvalidate });
|
||||
}
|
||||
}
|
||||
|
||||
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}`);
|
||||
await markApiKeyForInvalidation(
|
||||
{ apiKey: apiKeyToInvalidate },
|
||||
this.logger,
|
||||
this.unsecuredSavedObjectsClient
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -752,7 +756,11 @@ export class AlertsClient {
|
|||
await this.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, { version });
|
||||
} catch (e) {
|
||||
// Avoid unused API key
|
||||
this.invalidateApiKey({ apiKey: updateAttributes.apiKey });
|
||||
markApiKeyForInvalidation(
|
||||
{ apiKey: updateAttributes.apiKey },
|
||||
this.logger,
|
||||
this.unsecuredSavedObjectsClient
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
const scheduledTask = await this.scheduleAlert(
|
||||
|
@ -764,7 +772,11 @@ export class AlertsClient {
|
|||
scheduledTaskId: scheduledTask.id,
|
||||
});
|
||||
if (apiKeyToInvalidate) {
|
||||
await this.invalidateApiKey({ apiKey: apiKeyToInvalidate });
|
||||
await markApiKeyForInvalidation(
|
||||
{ apiKey: apiKeyToInvalidate },
|
||||
this.logger,
|
||||
this.unsecuredSavedObjectsClient
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -825,7 +837,13 @@ export class AlertsClient {
|
|||
attributes.scheduledTaskId
|
||||
? deleteTaskIfItExists(this.taskManager, attributes.scheduledTaskId)
|
||||
: null,
|
||||
apiKeyToInvalidate ? this.invalidateApiKey({ apiKey: apiKeyToInvalidate }) : null,
|
||||
apiKeyToInvalidate
|
||||
? await markApiKeyForInvalidation(
|
||||
{ apiKey: apiKeyToInvalidate },
|
||||
this.logger,
|
||||
this.unsecuredSavedObjectsClient
|
||||
)
|
||||
: null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,7 +34,6 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
namespace: 'default',
|
||||
getUserName: jest.fn(),
|
||||
createAPIKey: jest.fn(),
|
||||
invalidateAPIKey: jest.fn(),
|
||||
logger: loggingSystemMock.create().get(),
|
||||
encryptedSavedObjectsClient: encryptedSavedObjects,
|
||||
getActionsClient: jest.fn(),
|
||||
|
|
|
@ -34,7 +34,6 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
namespace: 'default',
|
||||
getUserName: jest.fn(),
|
||||
createAPIKey: jest.fn(),
|
||||
invalidateAPIKey: jest.fn(),
|
||||
logger: loggingSystemMock.create().get(),
|
||||
encryptedSavedObjectsClient: encryptedSavedObjects,
|
||||
getActionsClient: jest.fn(),
|
||||
|
@ -711,7 +710,7 @@ describe('create()', () => {
|
|||
expect(taskManager.schedule).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('throws error and invalidates API key when create saved object fails', async () => {
|
||||
test('throws error and add API key to invalidatePendingApiKey SO when create saved object fails', async () => {
|
||||
const data = getMockData();
|
||||
alertsClientParams.createAPIKey.mockResolvedValueOnce({
|
||||
apiKeysEnabled: true,
|
||||
|
@ -731,11 +730,25 @@ describe('create()', () => {
|
|||
],
|
||||
});
|
||||
unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Test failure'));
|
||||
const createdAt = new Date().toISOString();
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'api_key_pending_invalidation',
|
||||
attributes: {
|
||||
apiKeyId: '123',
|
||||
createdAt,
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
await expect(alertsClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Test failure"`
|
||||
);
|
||||
expect(taskManager.schedule).not.toHaveBeenCalled();
|
||||
expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' });
|
||||
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(2);
|
||||
expect(unsecuredSavedObjectsClient.create.mock.calls[1][1]).toStrictEqual({
|
||||
apiKeyId: '123',
|
||||
createdAt,
|
||||
});
|
||||
});
|
||||
|
||||
test('attempts to remove saved object if scheduling failed', async () => {
|
||||
|
|
|
@ -32,7 +32,6 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
namespace: 'default',
|
||||
getUserName: jest.fn(),
|
||||
createAPIKey: jest.fn(),
|
||||
invalidateAPIKey: jest.fn(),
|
||||
logger: loggingSystemMock.create().get(),
|
||||
encryptedSavedObjectsClient: encryptedSavedObjects,
|
||||
getActionsClient: jest.fn(),
|
||||
|
@ -94,11 +93,22 @@ describe('delete()', () => {
|
|||
});
|
||||
|
||||
test('successfully removes an alert', async () => {
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'api_key_pending_invalidation',
|
||||
attributes: {
|
||||
apiKeyId: '123',
|
||||
createdAt: '2019-02-12T21:01:22.479Z',
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
const result = await alertsClient.delete({ id: '1' });
|
||||
expect(result).toEqual({ success: true });
|
||||
expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledWith('alert', '1');
|
||||
expect(taskManager.remove).toHaveBeenCalledWith('task-123');
|
||||
expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' });
|
||||
expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toBe(
|
||||
'api_key_pending_invalidation'
|
||||
);
|
||||
expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', {
|
||||
namespace: 'default',
|
||||
});
|
||||
|
@ -107,12 +117,21 @@ describe('delete()', () => {
|
|||
|
||||
test('falls back to SOC.get when getDecryptedAsInternalUser throws an error', async () => {
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail'));
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'api_key_pending_invalidation',
|
||||
attributes: {
|
||||
apiKeyId: '123',
|
||||
createdAt: '2019-02-12T21:01:22.479Z',
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
|
||||
const result = await alertsClient.delete({ id: '1' });
|
||||
expect(result).toEqual({ success: true });
|
||||
expect(unsecuredSavedObjectsClient.delete).toHaveBeenCalledWith('alert', '1');
|
||||
expect(taskManager.remove).toHaveBeenCalledWith('task-123');
|
||||
expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled();
|
||||
expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled();
|
||||
expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1');
|
||||
expect(alertsClientParams.logger.error).toHaveBeenCalledWith(
|
||||
'delete(): Failed to load API key to invalidate on alert 1: Fail'
|
||||
|
@ -133,6 +152,15 @@ describe('delete()', () => {
|
|||
});
|
||||
|
||||
test(`doesn't invalidate API key when apiKey is null`, async () => {
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'api_key_pending_invalidation',
|
||||
attributes: {
|
||||
apiKeyId: '123',
|
||||
createdAt: '2019-02-12T21:01:22.479Z',
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({
|
||||
...existingAlert,
|
||||
attributes: {
|
||||
|
@ -142,24 +170,34 @@ describe('delete()', () => {
|
|||
});
|
||||
|
||||
await alertsClient.delete({ id: '1' });
|
||||
expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled();
|
||||
expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('swallows error when invalidate API key throws', async () => {
|
||||
alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail'));
|
||||
|
||||
unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Fail'));
|
||||
await alertsClient.delete({ id: '1' });
|
||||
expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' });
|
||||
expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toBe(
|
||||
'api_key_pending_invalidation'
|
||||
);
|
||||
expect(alertsClientParams.logger.error).toHaveBeenCalledWith(
|
||||
'Failed to invalidate API Key: Fail'
|
||||
'Failed to mark for API key [id="MTIzOmFiYw=="] for invalidation: Fail'
|
||||
);
|
||||
});
|
||||
|
||||
test('swallows error when getDecryptedAsInternalUser throws an error', async () => {
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'api_key_pending_invalidation',
|
||||
attributes: {
|
||||
apiKeyId: '123',
|
||||
createdAt: '2019-02-12T21:01:22.479Z',
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValue(new Error('Fail'));
|
||||
|
||||
await alertsClient.delete({ id: '1' });
|
||||
expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled();
|
||||
expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled();
|
||||
expect(alertsClientParams.logger.error).toHaveBeenCalledWith(
|
||||
'delete(): Failed to load API key to invalidate on alert 1: Fail'
|
||||
);
|
||||
|
|
|
@ -13,6 +13,7 @@ import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
|
|||
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
|
||||
import { ActionsAuthorization } from '../../../../actions/server';
|
||||
import { getBeforeSetup } from './lib';
|
||||
import { InvalidatePendingApiKey } from '../../types';
|
||||
|
||||
const taskManager = taskManagerMock.createStart();
|
||||
const alertTypeRegistry = alertTypeRegistryMock.create();
|
||||
|
@ -33,7 +34,6 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
namespace: 'default',
|
||||
getUserName: jest.fn(),
|
||||
createAPIKey: jest.fn(),
|
||||
invalidateAPIKey: jest.fn(),
|
||||
logger: loggingSystemMock.create().get(),
|
||||
encryptedSavedObjectsClient: encryptedSavedObjects,
|
||||
getActionsClient: jest.fn(),
|
||||
|
@ -108,6 +108,15 @@ describe('disable()', () => {
|
|||
});
|
||||
|
||||
test('disables an alert', async () => {
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'api_key_pending_invalidation',
|
||||
attributes: {
|
||||
apiKeyId: '123',
|
||||
createdAt: '2019-02-12T21:01:22.479Z',
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
await alertsClient.disable({ id: '1' });
|
||||
expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled();
|
||||
expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', {
|
||||
|
@ -145,11 +154,22 @@ describe('disable()', () => {
|
|||
}
|
||||
);
|
||||
expect(taskManager.remove).toHaveBeenCalledWith('task-123');
|
||||
expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' });
|
||||
expect(
|
||||
(unsecuredSavedObjectsClient.create.mock.calls[0][1] as InvalidatePendingApiKey).apiKeyId
|
||||
).toBe('123');
|
||||
});
|
||||
|
||||
test('falls back when getDecryptedAsInternalUser throws an error', async () => {
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail'));
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'api_key_pending_invalidation',
|
||||
attributes: {
|
||||
apiKeyId: '123',
|
||||
createdAt: '2019-02-12T21:01:22.479Z',
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
|
||||
await alertsClient.disable({ id: '1' });
|
||||
expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1');
|
||||
|
@ -188,7 +208,7 @@ describe('disable()', () => {
|
|||
}
|
||||
);
|
||||
expect(taskManager.remove).toHaveBeenCalledWith('task-123');
|
||||
expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled();
|
||||
expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test(`doesn't disable already disabled alerts`, async () => {
|
||||
|
@ -201,26 +221,54 @@ describe('disable()', () => {
|
|||
},
|
||||
});
|
||||
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'api_key_pending_invalidation',
|
||||
attributes: {
|
||||
apiKeyId: '123',
|
||||
createdAt: '2019-02-12T21:01:22.479Z',
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
|
||||
await alertsClient.disable({ id: '1' });
|
||||
expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled();
|
||||
expect(taskManager.remove).not.toHaveBeenCalled();
|
||||
expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled();
|
||||
expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test(`doesn't invalidate when no API key is used`, async () => {
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'api_key_pending_invalidation',
|
||||
attributes: {
|
||||
apiKeyId: '123',
|
||||
createdAt: '2019-02-12T21:01:22.479Z',
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce(existingAlert);
|
||||
|
||||
await alertsClient.disable({ id: '1' });
|
||||
expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled();
|
||||
expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('swallows error when failing to load decrypted saved object', async () => {
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'api_key_pending_invalidation',
|
||||
attributes: {
|
||||
apiKeyId: '123',
|
||||
createdAt: '2019-02-12T21:01:22.479Z',
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail'));
|
||||
|
||||
await alertsClient.disable({ id: '1' });
|
||||
expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled();
|
||||
expect(taskManager.remove).toHaveBeenCalled();
|
||||
expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled();
|
||||
expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled();
|
||||
expect(alertsClientParams.logger.error).toHaveBeenCalledWith(
|
||||
'disable(): Failed to load API key to invalidate on alert 1: Fail'
|
||||
);
|
||||
|
@ -235,11 +283,10 @@ describe('disable()', () => {
|
|||
});
|
||||
|
||||
test('swallows error when invalidate API key throws', async () => {
|
||||
alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail'));
|
||||
|
||||
unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Fail'));
|
||||
await alertsClient.disable({ id: '1' });
|
||||
expect(alertsClientParams.logger.error).toHaveBeenCalledWith(
|
||||
'Failed to invalidate API Key: Fail'
|
||||
'Failed to mark for API key [id="MTIzOmFiYw=="] for invalidation: Fail'
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ import { AlertsAuthorization } from '../../authorization/alerts_authorization';
|
|||
import { ActionsAuthorization } from '../../../../actions/server';
|
||||
import { TaskStatus } from '../../../../task_manager/server';
|
||||
import { getBeforeSetup } from './lib';
|
||||
import { InvalidatePendingApiKey } from '../../types';
|
||||
|
||||
const taskManager = taskManagerMock.createStart();
|
||||
const alertTypeRegistry = alertTypeRegistryMock.create();
|
||||
|
@ -34,7 +35,6 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
namespace: 'default',
|
||||
getUserName: jest.fn(),
|
||||
createAPIKey: jest.fn(),
|
||||
invalidateAPIKey: jest.fn(),
|
||||
logger: loggingSystemMock.create().get(),
|
||||
encryptedSavedObjectsClient: encryptedSavedObjects,
|
||||
getActionsClient: jest.fn(),
|
||||
|
@ -147,6 +147,7 @@ describe('enable()', () => {
|
|||
});
|
||||
|
||||
test('enables an alert', async () => {
|
||||
const createdAt = new Date().toISOString();
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
...existingAlert,
|
||||
attributes: {
|
||||
|
@ -157,13 +158,22 @@ describe('enable()', () => {
|
|||
updatedBy: 'elastic',
|
||||
},
|
||||
});
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'api_key_pending_invalidation',
|
||||
attributes: {
|
||||
apiKeyId: '123',
|
||||
createdAt,
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
|
||||
await alertsClient.enable({ id: '1' });
|
||||
expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled();
|
||||
expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', {
|
||||
namespace: 'default',
|
||||
});
|
||||
expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled();
|
||||
expect(unsecuredSavedObjectsClient.create).not.toBeCalledWith('api_key_pending_invalidation');
|
||||
expect(alertsClientParams.createAPIKey).toHaveBeenCalled();
|
||||
expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith(
|
||||
'alert',
|
||||
|
@ -217,6 +227,7 @@ describe('enable()', () => {
|
|||
});
|
||||
|
||||
test('invalidates API key if ever one existed prior to updating', async () => {
|
||||
const createdAt = new Date().toISOString();
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({
|
||||
...existingAlert,
|
||||
attributes: {
|
||||
|
@ -224,13 +235,24 @@ describe('enable()', () => {
|
|||
apiKey: Buffer.from('123:abc').toString('base64'),
|
||||
},
|
||||
});
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'api_key_pending_invalidation',
|
||||
attributes: {
|
||||
apiKeyId: '123',
|
||||
createdAt,
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
|
||||
await alertsClient.enable({ id: '1' });
|
||||
expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled();
|
||||
expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', {
|
||||
namespace: 'default',
|
||||
});
|
||||
expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' });
|
||||
expect(
|
||||
(unsecuredSavedObjectsClient.create.mock.calls[0][1] as InvalidatePendingApiKey).apiKeyId
|
||||
).toBe('123');
|
||||
});
|
||||
|
||||
test(`doesn't enable already enabled alerts`, async () => {
|
||||
|
@ -312,19 +334,31 @@ describe('enable()', () => {
|
|||
});
|
||||
|
||||
test('throws error when failing to update the first time', async () => {
|
||||
const createdAt = new Date().toISOString();
|
||||
alertsClientParams.createAPIKey.mockResolvedValueOnce({
|
||||
apiKeysEnabled: true,
|
||||
result: { id: '123', name: '123', api_key: 'abc' },
|
||||
});
|
||||
unsecuredSavedObjectsClient.update.mockReset();
|
||||
unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Fail to update'));
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'api_key_pending_invalidation',
|
||||
attributes: {
|
||||
apiKeyId: '123',
|
||||
createdAt,
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
|
||||
await expect(alertsClient.enable({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Fail to update"`
|
||||
);
|
||||
expect(alertsClientParams.getUserName).toHaveBeenCalled();
|
||||
expect(alertsClientParams.createAPIKey).toHaveBeenCalled();
|
||||
expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' });
|
||||
expect(
|
||||
(unsecuredSavedObjectsClient.create.mock.calls[0][1] as InvalidatePendingApiKey).apiKeyId
|
||||
).toBe('123');
|
||||
expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1);
|
||||
expect(taskManager.schedule).not.toHaveBeenCalled();
|
||||
});
|
||||
|
|
|
@ -35,7 +35,6 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
namespace: 'default',
|
||||
getUserName: jest.fn(),
|
||||
createAPIKey: jest.fn(),
|
||||
invalidateAPIKey: jest.fn(),
|
||||
logger: loggingSystemMock.create().get(),
|
||||
encryptedSavedObjectsClient: encryptedSavedObjects,
|
||||
getActionsClient: jest.fn(),
|
||||
|
|
|
@ -33,7 +33,6 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
namespace: 'default',
|
||||
getUserName: jest.fn(),
|
||||
createAPIKey: jest.fn(),
|
||||
invalidateAPIKey: jest.fn(),
|
||||
logger: loggingSystemMock.create().get(),
|
||||
encryptedSavedObjectsClient: encryptedSavedObjects,
|
||||
getActionsClient: jest.fn(),
|
||||
|
|
|
@ -39,7 +39,6 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
namespace: 'default',
|
||||
getUserName: jest.fn(),
|
||||
createAPIKey: jest.fn(),
|
||||
invalidateAPIKey: jest.fn(),
|
||||
logger: loggingSystemMock.create().get(),
|
||||
encryptedSavedObjectsClient: encryptedSavedObjects,
|
||||
getActionsClient: jest.fn(),
|
||||
|
|
|
@ -34,7 +34,6 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
namespace: 'default',
|
||||
getUserName: jest.fn(),
|
||||
createAPIKey: jest.fn(),
|
||||
invalidateAPIKey: jest.fn(),
|
||||
logger: loggingSystemMock.create().get(),
|
||||
encryptedSavedObjectsClient: encryptedSavedObjects,
|
||||
getActionsClient: jest.fn(),
|
||||
|
|
|
@ -46,14 +46,6 @@ export function getBeforeSetup(
|
|||
) {
|
||||
jest.resetAllMocks();
|
||||
alertsClientParams.createAPIKey.mockResolvedValue({ apiKeysEnabled: false });
|
||||
alertsClientParams.invalidateAPIKey.mockResolvedValue({
|
||||
apiKeysEnabled: true,
|
||||
result: {
|
||||
invalidated_api_keys: [],
|
||||
previously_invalidated_api_keys: [],
|
||||
error_count: 0,
|
||||
},
|
||||
});
|
||||
alertsClientParams.getUserName.mockResolvedValue('elastic');
|
||||
taskManager.runNow.mockResolvedValue({ id: '' });
|
||||
const actionsClient = actionsClientMock.create();
|
||||
|
|
|
@ -33,7 +33,6 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
namespace: 'default',
|
||||
getUserName: jest.fn(),
|
||||
createAPIKey: jest.fn(),
|
||||
invalidateAPIKey: jest.fn(),
|
||||
logger: loggingSystemMock.create().get(),
|
||||
encryptedSavedObjectsClient: encryptedSavedObjects,
|
||||
getActionsClient: jest.fn(),
|
||||
|
|
|
@ -32,7 +32,6 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
namespace: 'default',
|
||||
getUserName: jest.fn(),
|
||||
createAPIKey: jest.fn(),
|
||||
invalidateAPIKey: jest.fn(),
|
||||
logger: loggingSystemMock.create().get(),
|
||||
encryptedSavedObjectsClient: encryptedSavedObjects,
|
||||
getActionsClient: jest.fn(),
|
||||
|
|
|
@ -33,7 +33,6 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
namespace: 'default',
|
||||
getUserName: jest.fn(),
|
||||
createAPIKey: jest.fn(),
|
||||
invalidateAPIKey: jest.fn(),
|
||||
logger: loggingSystemMock.create().get(),
|
||||
encryptedSavedObjectsClient: encryptedSavedObjects,
|
||||
getActionsClient: jest.fn(),
|
||||
|
|
|
@ -33,7 +33,6 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
namespace: 'default',
|
||||
getUserName: jest.fn(),
|
||||
createAPIKey: jest.fn(),
|
||||
invalidateAPIKey: jest.fn(),
|
||||
logger: loggingSystemMock.create().get(),
|
||||
encryptedSavedObjectsClient: encryptedSavedObjects,
|
||||
getActionsClient: jest.fn(),
|
||||
|
|
|
@ -33,7 +33,6 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
namespace: 'default',
|
||||
getUserName: jest.fn(),
|
||||
createAPIKey: jest.fn(),
|
||||
invalidateAPIKey: jest.fn(),
|
||||
logger: loggingSystemMock.create().get(),
|
||||
encryptedSavedObjectsClient: encryptedSavedObjects,
|
||||
getActionsClient: jest.fn(),
|
||||
|
|
|
@ -10,7 +10,7 @@ import { savedObjectsClientMock, loggingSystemMock } from '../../../../../../src
|
|||
import { taskManagerMock } from '../../../../task_manager/server/mocks';
|
||||
import { alertTypeRegistryMock } from '../../alert_type_registry.mock';
|
||||
import { alertsAuthorizationMock } from '../../authorization/alerts_authorization.mock';
|
||||
import { IntervalSchedule } from '../../types';
|
||||
import { IntervalSchedule, InvalidatePendingApiKey } from '../../types';
|
||||
import { encryptedSavedObjectsMock } from '../../../../encrypted_saved_objects/server/mocks';
|
||||
import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
|
||||
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
|
||||
|
@ -38,7 +38,6 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
namespace: 'default',
|
||||
getUserName: jest.fn(),
|
||||
createAPIKey: jest.fn(),
|
||||
invalidateAPIKey: jest.fn(),
|
||||
logger: loggingSystemMock.create().get(),
|
||||
encryptedSavedObjectsClient: encryptedSavedObjects,
|
||||
getActionsClient: jest.fn(),
|
||||
|
@ -161,6 +160,15 @@ describe('update()', () => {
|
|||
},
|
||||
],
|
||||
});
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'api_key_pending_invalidation',
|
||||
attributes: {
|
||||
apiKeyId: '234',
|
||||
createdAt: '2019-02-12T21:01:22.479Z',
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
const result = await alertsClient.update({
|
||||
id: '1',
|
||||
data: {
|
||||
|
@ -241,7 +249,7 @@ describe('update()', () => {
|
|||
namespace: 'default',
|
||||
});
|
||||
expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled();
|
||||
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1);
|
||||
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(2);
|
||||
expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3);
|
||||
expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert');
|
||||
expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(`
|
||||
|
@ -376,6 +384,24 @@ describe('update()', () => {
|
|||
},
|
||||
],
|
||||
});
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'api_key_pending_invalidation',
|
||||
attributes: {
|
||||
apiKeyId: '234',
|
||||
createdAt: '2019-02-12T21:01:22.479Z',
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'api_key_pending_invalidation',
|
||||
attributes: {
|
||||
apiKeyId: '234',
|
||||
createdAt: '2019-02-12T21:01:22.479Z',
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
const result = await alertsClient.update({
|
||||
id: '1',
|
||||
data: {
|
||||
|
@ -423,7 +449,7 @@ describe('update()', () => {
|
|||
"updatedAt": 2019-02-12T21:01:22.479Z,
|
||||
}
|
||||
`);
|
||||
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1);
|
||||
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(2);
|
||||
expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3);
|
||||
expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert');
|
||||
expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(`
|
||||
|
@ -530,6 +556,15 @@ describe('update()', () => {
|
|||
},
|
||||
],
|
||||
});
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'api_key_pending_invalidation',
|
||||
attributes: {
|
||||
apiKeyId: '234',
|
||||
createdAt: '2019-02-12T21:01:22.479Z',
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
const result = await alertsClient.update({
|
||||
id: '1',
|
||||
data: {
|
||||
|
@ -578,7 +613,7 @@ describe('update()', () => {
|
|||
"updatedAt": 2019-02-12T21:01:22.479Z,
|
||||
}
|
||||
`);
|
||||
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1);
|
||||
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(2);
|
||||
expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(3);
|
||||
expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual('alert');
|
||||
expect(unsecuredSavedObjectsClient.create.mock.calls[0][1]).toMatchInlineSnapshot(`
|
||||
|
@ -732,7 +767,6 @@ describe('update()', () => {
|
|||
});
|
||||
|
||||
it('swallows error when invalidate API key throws', async () => {
|
||||
alertsClientParams.invalidateAPIKey.mockRejectedValueOnce(new Error('Fail'));
|
||||
unsecuredSavedObjectsClient.bulkGet.mockResolvedValueOnce({
|
||||
saved_objects: [
|
||||
{
|
||||
|
@ -775,6 +809,7 @@ describe('update()', () => {
|
|||
},
|
||||
],
|
||||
});
|
||||
unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Fail')); // add ApiKey to invalidate
|
||||
await alertsClient.update({
|
||||
id: '1',
|
||||
data: {
|
||||
|
@ -797,7 +832,7 @@ describe('update()', () => {
|
|||
},
|
||||
});
|
||||
expect(alertsClientParams.logger.error).toHaveBeenCalledWith(
|
||||
'Failed to invalidate API Key: Fail'
|
||||
'Failed to mark for API key [id="MTIzOmFiYw=="] for invalidation: Fail'
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -965,8 +1000,9 @@ describe('update()', () => {
|
|||
},
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"Fail"`);
|
||||
expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalledWith({ id: '123' });
|
||||
expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '234' });
|
||||
expect(
|
||||
(unsecuredSavedObjectsClient.create.mock.calls[1][1] as InvalidatePendingApiKey).apiKeyId
|
||||
).toBe('234');
|
||||
});
|
||||
|
||||
describe('updating an alert schedule', () => {
|
||||
|
|
|
@ -13,6 +13,7 @@ import { actionsAuthorizationMock } from '../../../../actions/server/mocks';
|
|||
import { AlertsAuthorization } from '../../authorization/alerts_authorization';
|
||||
import { ActionsAuthorization } from '../../../../actions/server';
|
||||
import { getBeforeSetup } from './lib';
|
||||
import { InvalidatePendingApiKey } from '../../types';
|
||||
|
||||
const taskManager = taskManagerMock.createStart();
|
||||
const alertTypeRegistry = alertTypeRegistryMock.create();
|
||||
|
@ -32,7 +33,6 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
namespace: 'default',
|
||||
getUserName: jest.fn(),
|
||||
createAPIKey: jest.fn(),
|
||||
invalidateAPIKey: jest.fn(),
|
||||
logger: loggingSystemMock.create().get(),
|
||||
encryptedSavedObjectsClient: encryptedSavedObjects,
|
||||
getActionsClient: jest.fn(),
|
||||
|
@ -80,6 +80,15 @@ describe('updateApiKey()', () => {
|
|||
beforeEach(() => {
|
||||
alertsClient = new AlertsClient(alertsClientParams);
|
||||
unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert);
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'api_key_pending_invalidation',
|
||||
attributes: {
|
||||
apiKeyId: '234',
|
||||
createdAt: '2019-02-12T21:01:22.479Z',
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingEncryptedAlert);
|
||||
alertsClientParams.createAPIKey.mockResolvedValueOnce({
|
||||
apiKeysEnabled: true,
|
||||
|
@ -121,11 +130,22 @@ describe('updateApiKey()', () => {
|
|||
},
|
||||
{ version: '123' }
|
||||
);
|
||||
expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '123' });
|
||||
expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toBe(
|
||||
'api_key_pending_invalidation'
|
||||
);
|
||||
});
|
||||
|
||||
test('falls back to SOC when getDecryptedAsInternalUser throws an error', async () => {
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail'));
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'api_key_pending_invalidation',
|
||||
attributes: {
|
||||
apiKeyId: '123',
|
||||
createdAt: '2019-02-12T21:01:22.479Z',
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
|
||||
await alertsClient.updateApiKey({ id: '1' });
|
||||
expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1');
|
||||
|
@ -160,28 +180,37 @@ describe('updateApiKey()', () => {
|
|||
},
|
||||
{ version: '123' }
|
||||
);
|
||||
expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled();
|
||||
expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('swallows error when invalidate API key throws', async () => {
|
||||
alertsClientParams.invalidateAPIKey.mockRejectedValue(new Error('Fail'));
|
||||
unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Fail'));
|
||||
|
||||
await alertsClient.updateApiKey({ id: '1' });
|
||||
expect(alertsClientParams.logger.error).toHaveBeenCalledWith(
|
||||
'Failed to invalidate API Key: Fail'
|
||||
);
|
||||
expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled();
|
||||
expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toBe(
|
||||
'api_key_pending_invalidation'
|
||||
);
|
||||
});
|
||||
|
||||
test('swallows error when getting decrypted object throws', async () => {
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail'));
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'api_key_pending_invalidation',
|
||||
attributes: {
|
||||
apiKeyId: '234',
|
||||
createdAt: '2019-02-12T21:01:22.479Z',
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
|
||||
await alertsClient.updateApiKey({ id: '1' });
|
||||
expect(alertsClientParams.logger.error).toHaveBeenCalledWith(
|
||||
'updateApiKey(): Failed to load API key to invalidate on alert 1: Fail'
|
||||
);
|
||||
expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled();
|
||||
expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalled();
|
||||
expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('throws when unsecuredSavedObjectsClient update fails and invalidates newly created API key', async () => {
|
||||
|
@ -190,12 +219,22 @@ describe('updateApiKey()', () => {
|
|||
result: { id: '234', name: '234', api_key: 'abc' },
|
||||
});
|
||||
unsecuredSavedObjectsClient.update.mockRejectedValueOnce(new Error('Fail'));
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'api_key_pending_invalidation',
|
||||
attributes: {
|
||||
apiKeyId: '234',
|
||||
createdAt: '2019-02-12T21:01:22.479Z',
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
|
||||
await expect(alertsClient.updateApiKey({ id: '1' })).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Fail"`
|
||||
);
|
||||
expect(alertsClientParams.invalidateAPIKey).not.toHaveBeenCalledWith({ id: '123' });
|
||||
expect(alertsClientParams.invalidateAPIKey).toHaveBeenCalledWith({ id: '234' });
|
||||
expect(
|
||||
(unsecuredSavedObjectsClient.create.mock.calls[0][1] as InvalidatePendingApiKey).apiKeyId
|
||||
).toBe('234');
|
||||
});
|
||||
|
||||
describe('authorization', () => {
|
||||
|
|
|
@ -45,7 +45,6 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
namespace: 'default',
|
||||
getUserName: jest.fn(),
|
||||
createAPIKey: jest.fn(),
|
||||
invalidateAPIKey: jest.fn(),
|
||||
logger,
|
||||
encryptedSavedObjectsClient: encryptedSavedObjects,
|
||||
getActionsClient: jest.fn(),
|
||||
|
@ -115,7 +114,7 @@ async function update(success: boolean) {
|
|||
);
|
||||
return expectConflict(success, err, 'create');
|
||||
}
|
||||
expectSuccess(success, 2, 'create');
|
||||
expectSuccess(success, 3, 'create');
|
||||
|
||||
// only checking the debug messages in this test
|
||||
expect(logger.debug).nthCalledWith(1, `alertsClient.update('alert-id') conflict, retrying ...`);
|
||||
|
@ -306,14 +305,6 @@ beforeEach(() => {
|
|||
jest.resetAllMocks();
|
||||
|
||||
alertsClientParams.createAPIKey.mockResolvedValue({ apiKeysEnabled: false });
|
||||
alertsClientParams.invalidateAPIKey.mockResolvedValue({
|
||||
apiKeysEnabled: true,
|
||||
result: {
|
||||
invalidated_api_keys: [],
|
||||
previously_invalidated_api_keys: [],
|
||||
error_count: 0,
|
||||
},
|
||||
});
|
||||
alertsClientParams.getUserName.mockResolvedValue('elastic');
|
||||
|
||||
taskManager.runNow.mockResolvedValue({ id: '' });
|
||||
|
|
|
@ -92,7 +92,7 @@ test('creates an alerts client with proper constructor arguments when security i
|
|||
|
||||
expect(savedObjectsService.getScopedClient).toHaveBeenCalledWith(request, {
|
||||
excludedWrappers: ['security'],
|
||||
includedHiddenTypes: ['alert'],
|
||||
includedHiddenTypes: ['alert', 'api_key_pending_invalidation'],
|
||||
});
|
||||
|
||||
const { AlertsAuthorization } = jest.requireMock('./authorization/alerts_authorization');
|
||||
|
@ -125,7 +125,6 @@ test('creates an alerts client with proper constructor arguments when security i
|
|||
getActionsClient: expect.any(Function),
|
||||
getEventLogClient: expect.any(Function),
|
||||
createAPIKey: expect.any(Function),
|
||||
invalidateAPIKey: expect.any(Function),
|
||||
encryptedSavedObjectsClient: alertsClientFactoryParams.encryptedSavedObjectsClient,
|
||||
kibanaVersion: '7.10.0',
|
||||
});
|
||||
|
@ -142,7 +141,7 @@ test('creates an alerts client with proper constructor arguments', async () => {
|
|||
|
||||
expect(savedObjectsService.getScopedClient).toHaveBeenCalledWith(request, {
|
||||
excludedWrappers: ['security'],
|
||||
includedHiddenTypes: ['alert'],
|
||||
includedHiddenTypes: ['alert', 'api_key_pending_invalidation'],
|
||||
});
|
||||
|
||||
const { AlertsAuthorization } = jest.requireMock('./authorization/alerts_authorization');
|
||||
|
@ -167,7 +166,6 @@ test('creates an alerts client with proper constructor arguments', async () => {
|
|||
namespace: 'default',
|
||||
getUserName: expect.any(Function),
|
||||
createAPIKey: expect.any(Function),
|
||||
invalidateAPIKey: expect.any(Function),
|
||||
encryptedSavedObjectsClient: alertsClientFactoryParams.encryptedSavedObjectsClient,
|
||||
getActionsClient: expect.any(Function),
|
||||
getEventLogClient: expect.any(Function),
|
||||
|
|
|
@ -14,7 +14,7 @@ import { PluginStartContract as ActionsPluginStartContract } from '../../actions
|
|||
import { AlertsClient } from './alerts_client';
|
||||
import { ALERTS_FEATURE_ID } from '../common';
|
||||
import { AlertTypeRegistry, SpaceIdToNamespaceFunction } from './types';
|
||||
import { InvalidateAPIKeyParams, SecurityPluginSetup } from '../../security/server';
|
||||
import { SecurityPluginSetup } from '../../security/server';
|
||||
import { EncryptedSavedObjectsClient } from '../../encrypted_saved_objects/server';
|
||||
import { TaskManagerStartContract } from '../../task_manager/server';
|
||||
import { PluginStartContract as FeaturesPluginStart } from '../../features/server';
|
||||
|
@ -94,7 +94,7 @@ export class AlertsClientFactory {
|
|||
alertTypeRegistry: this.alertTypeRegistry,
|
||||
unsecuredSavedObjectsClient: savedObjects.getScopedClient(request, {
|
||||
excludedWrappers: ['security'],
|
||||
includedHiddenTypes: ['alert'],
|
||||
includedHiddenTypes: ['alert', 'api_key_pending_invalidation'],
|
||||
}),
|
||||
authorization,
|
||||
actionsAuthorization: actions.getActionsAuthorizationWithRequest(request),
|
||||
|
@ -129,22 +129,6 @@ export class AlertsClientFactory {
|
|||
result: createAPIKeyResult,
|
||||
};
|
||||
},
|
||||
async invalidateAPIKey(params: InvalidateAPIKeyParams) {
|
||||
if (!securityPluginSetup) {
|
||||
return { apiKeysEnabled: false };
|
||||
}
|
||||
const invalidateAPIKeyResult = await securityPluginSetup.authc.invalidateAPIKeyAsInternalUser(
|
||||
params
|
||||
);
|
||||
// Null when Elasticsearch security is disabled
|
||||
if (!invalidateAPIKeyResult) {
|
||||
return { apiKeysEnabled: false };
|
||||
}
|
||||
return {
|
||||
apiKeysEnabled: true,
|
||||
result: invalidateAPIKeyResult,
|
||||
};
|
||||
},
|
||||
async getActionsClient() {
|
||||
return actions.getActionsClientWithRequest(request);
|
||||
},
|
||||
|
|
|
@ -13,6 +13,10 @@ describe('config validation', () => {
|
|||
"healthCheck": Object {
|
||||
"interval": "60m",
|
||||
},
|
||||
"invalidateApiKeysTask": Object {
|
||||
"interval": "5m",
|
||||
"removalDelay": "5m",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -11,6 +11,10 @@ export const configSchema = schema.object({
|
|||
healthCheck: schema.object({
|
||||
interval: schema.string({ validate: validateDurationSchema, defaultValue: '60m' }),
|
||||
}),
|
||||
invalidateApiKeysTask: schema.object({
|
||||
interval: schema.string({ validate: validateDurationSchema, defaultValue: '5m' }),
|
||||
removalDelay: schema.string({ validate: validateDurationSchema, defaultValue: '5m' }),
|
||||
}),
|
||||
});
|
||||
|
||||
export type AlertsConfig = TypeOf<typeof configSchema>;
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 { loggingSystemMock, savedObjectsClientMock } from '../../../../../src/core/server/mocks';
|
||||
import { markApiKeyForInvalidation } from './mark_api_key_for_invalidation';
|
||||
|
||||
describe('markApiKeyForInvalidation', () => {
|
||||
test('should call savedObjectsClient create with the proper params', async () => {
|
||||
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
id: '1',
|
||||
type: 'api_key_pending_invalidation',
|
||||
attributes: {
|
||||
apiKeyId: '123',
|
||||
createdAt: '2019-02-12T21:01:22.479Z',
|
||||
},
|
||||
references: [],
|
||||
});
|
||||
await markApiKeyForInvalidation(
|
||||
{ apiKey: Buffer.from('123:abc').toString('base64') },
|
||||
loggingSystemMock.create().get(),
|
||||
unsecuredSavedObjectsClient
|
||||
);
|
||||
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1);
|
||||
expect(unsecuredSavedObjectsClient.create.mock.calls[0]).toHaveLength(2);
|
||||
expect(unsecuredSavedObjectsClient.create.mock.calls[0][0]).toEqual(
|
||||
'api_key_pending_invalidation'
|
||||
);
|
||||
});
|
||||
|
||||
test('should log the proper error when savedObjectsClient create failed', async () => {
|
||||
const logger = loggingSystemMock.create().get();
|
||||
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
|
||||
unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Fail'));
|
||||
await markApiKeyForInvalidation(
|
||||
{ apiKey: Buffer.from('123').toString('base64') },
|
||||
logger,
|
||||
unsecuredSavedObjectsClient
|
||||
);
|
||||
expect(logger.error).toHaveBeenCalledWith(
|
||||
'Failed to mark for API key [id="MTIz"] for invalidation: Fail'
|
||||
);
|
||||
});
|
||||
});
|
|
@ -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 { Logger, SavedObjectsClientContract } from 'src/core/server';
|
||||
|
||||
export const markApiKeyForInvalidation = async (
|
||||
{ apiKey }: { apiKey: string | null },
|
||||
logger: Logger,
|
||||
savedObjectsClient: SavedObjectsClientContract
|
||||
): Promise<void> => {
|
||||
if (!apiKey) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const apiKeyId = Buffer.from(apiKey, 'base64').toString().split(':')[0];
|
||||
await savedObjectsClient.create('api_key_pending_invalidation', {
|
||||
apiKeyId,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(`Failed to mark for API key [id="${apiKey}"] for invalidation: ${e.message}`);
|
||||
}
|
||||
};
|
226
x-pack/plugins/alerts/server/invalidate_pending_api_keys/task.ts
Normal file
226
x-pack/plugins/alerts/server/invalidate_pending_api_keys/task.ts
Normal file
|
@ -0,0 +1,226 @@
|
|||
/*
|
||||
* 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 {
|
||||
Logger,
|
||||
CoreStart,
|
||||
SavedObjectsFindResponse,
|
||||
KibanaRequest,
|
||||
SavedObjectsClientContract,
|
||||
} from 'kibana/server';
|
||||
import { EncryptedSavedObjectsClient } from '../../../encrypted_saved_objects/server';
|
||||
import { InvalidateAPIKeyParams, SecurityPluginSetup } from '../../../security/server';
|
||||
import {
|
||||
RunContext,
|
||||
TaskManagerSetupContract,
|
||||
TaskManagerStartContract,
|
||||
} from '../../../task_manager/server';
|
||||
import { InvalidateAPIKeyResult } from '../alerts_client';
|
||||
import { AlertsConfig } from '../config';
|
||||
import { timePeriodBeforeDate } from '../lib/get_cadence';
|
||||
import { AlertingPluginsStart } from '../plugin';
|
||||
import { InvalidatePendingApiKey } from '../types';
|
||||
|
||||
const TASK_TYPE = 'alerts_invalidate_api_keys';
|
||||
export const TASK_ID = `Alerts-${TASK_TYPE}`;
|
||||
|
||||
const invalidateAPIKey = async (
|
||||
params: InvalidateAPIKeyParams,
|
||||
securityPluginSetup?: SecurityPluginSetup
|
||||
): Promise<InvalidateAPIKeyResult> => {
|
||||
if (!securityPluginSetup) {
|
||||
return { apiKeysEnabled: false };
|
||||
}
|
||||
const invalidateAPIKeyResult = await securityPluginSetup.authc.invalidateAPIKeyAsInternalUser(
|
||||
params
|
||||
);
|
||||
// Null when Elasticsearch security is disabled
|
||||
if (!invalidateAPIKeyResult) {
|
||||
return { apiKeysEnabled: false };
|
||||
}
|
||||
return {
|
||||
apiKeysEnabled: true,
|
||||
result: invalidateAPIKeyResult,
|
||||
};
|
||||
};
|
||||
|
||||
export function initializeApiKeyInvalidator(
|
||||
logger: Logger,
|
||||
coreStartServices: Promise<[CoreStart, AlertingPluginsStart, unknown]>,
|
||||
taskManager: TaskManagerSetupContract,
|
||||
config: Promise<AlertsConfig>,
|
||||
securityPluginSetup?: SecurityPluginSetup
|
||||
) {
|
||||
registerApiKeyInvalitorTaskDefinition(
|
||||
logger,
|
||||
coreStartServices,
|
||||
taskManager,
|
||||
config,
|
||||
securityPluginSetup
|
||||
);
|
||||
}
|
||||
|
||||
export async function scheduleApiKeyInvalidatorTask(
|
||||
logger: Logger,
|
||||
config: Promise<AlertsConfig>,
|
||||
taskManager: TaskManagerStartContract
|
||||
) {
|
||||
const interval = (await config).invalidateApiKeysTask.interval;
|
||||
try {
|
||||
await taskManager.ensureScheduled({
|
||||
id: TASK_ID,
|
||||
taskType: TASK_TYPE,
|
||||
schedule: {
|
||||
interval,
|
||||
},
|
||||
state: {},
|
||||
params: {},
|
||||
});
|
||||
} catch (e) {
|
||||
logger.debug(`Error scheduling task, received ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
function registerApiKeyInvalitorTaskDefinition(
|
||||
logger: Logger,
|
||||
coreStartServices: Promise<[CoreStart, AlertingPluginsStart, unknown]>,
|
||||
taskManager: TaskManagerSetupContract,
|
||||
config: Promise<AlertsConfig>,
|
||||
securityPluginSetup?: SecurityPluginSetup
|
||||
) {
|
||||
taskManager.registerTaskDefinitions({
|
||||
[TASK_TYPE]: {
|
||||
title: 'Invalidate alert API Keys',
|
||||
createTaskRunner: taskRunner(logger, coreStartServices, config, securityPluginSetup),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function getFakeKibanaRequest(basePath: string) {
|
||||
const requestHeaders: Record<string, string> = {};
|
||||
return ({
|
||||
headers: requestHeaders,
|
||||
getBasePath: () => basePath,
|
||||
path: '/',
|
||||
route: { settings: {} },
|
||||
url: {
|
||||
href: '/',
|
||||
},
|
||||
raw: {
|
||||
req: {
|
||||
url: '/',
|
||||
},
|
||||
},
|
||||
} as unknown) as KibanaRequest;
|
||||
}
|
||||
|
||||
function taskRunner(
|
||||
logger: Logger,
|
||||
coreStartServices: Promise<[CoreStart, AlertingPluginsStart, unknown]>,
|
||||
config: Promise<AlertsConfig>,
|
||||
securityPluginSetup?: SecurityPluginSetup
|
||||
) {
|
||||
return ({ taskInstance }: RunContext) => {
|
||||
const { state } = taskInstance;
|
||||
return {
|
||||
async run() {
|
||||
let totalInvalidated = 0;
|
||||
const configResult = await config;
|
||||
try {
|
||||
const [{ savedObjects, http }, { encryptedSavedObjects }] = await coreStartServices;
|
||||
const savedObjectsClient = savedObjects.getScopedClient(
|
||||
getFakeKibanaRequest(http.basePath.serverBasePath),
|
||||
{
|
||||
includedHiddenTypes: ['api_key_pending_invalidation'],
|
||||
excludedWrappers: ['security'],
|
||||
}
|
||||
);
|
||||
const encryptedSavedObjectsClient = encryptedSavedObjects.getClient({
|
||||
includedHiddenTypes: ['api_key_pending_invalidation'],
|
||||
});
|
||||
const configuredDelay = configResult.invalidateApiKeysTask.removalDelay;
|
||||
const delay = timePeriodBeforeDate(new Date(), configuredDelay).toISOString();
|
||||
|
||||
let hasApiKeysPendingInvalidation = true;
|
||||
const PAGE_SIZE = 100;
|
||||
do {
|
||||
const apiKeysToInvalidate = await savedObjectsClient.find<InvalidatePendingApiKey>({
|
||||
type: 'api_key_pending_invalidation',
|
||||
filter: `api_key_pending_invalidation.attributes.createdAt <= "${delay}"`,
|
||||
page: 1,
|
||||
sortField: 'createdAt',
|
||||
sortOrder: 'asc',
|
||||
perPage: PAGE_SIZE,
|
||||
});
|
||||
totalInvalidated += await invalidateApiKeys(
|
||||
logger,
|
||||
savedObjectsClient,
|
||||
apiKeysToInvalidate,
|
||||
encryptedSavedObjectsClient,
|
||||
securityPluginSetup
|
||||
);
|
||||
|
||||
hasApiKeysPendingInvalidation = apiKeysToInvalidate.total > PAGE_SIZE;
|
||||
} while (hasApiKeysPendingInvalidation);
|
||||
|
||||
return {
|
||||
state: {
|
||||
runs: (state.runs || 0) + 1,
|
||||
total_invalidated: totalInvalidated,
|
||||
},
|
||||
schedule: {
|
||||
interval: configResult.invalidateApiKeysTask.interval,
|
||||
},
|
||||
};
|
||||
} catch (e) {
|
||||
logger.warn(`Error executing alerting apiKey invalidation task: ${e.message}`);
|
||||
return {
|
||||
state: {
|
||||
runs: (state.runs || 0) + 1,
|
||||
total_invalidated: totalInvalidated,
|
||||
},
|
||||
schedule: {
|
||||
interval: configResult.invalidateApiKeysTask.interval,
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
async function invalidateApiKeys(
|
||||
logger: Logger,
|
||||
savedObjectsClient: SavedObjectsClientContract,
|
||||
apiKeysToInvalidate: SavedObjectsFindResponse<InvalidatePendingApiKey>,
|
||||
encryptedSavedObjectsClient: EncryptedSavedObjectsClient,
|
||||
securityPluginSetup?: SecurityPluginSetup
|
||||
) {
|
||||
let totalInvalidated = 0;
|
||||
await Promise.all(
|
||||
apiKeysToInvalidate.saved_objects.map(async (apiKeyObj) => {
|
||||
const decryptedApiKey = await encryptedSavedObjectsClient.getDecryptedAsInternalUser<
|
||||
InvalidatePendingApiKey
|
||||
>('api_key_pending_invalidation', apiKeyObj.id);
|
||||
const apiKeyId = decryptedApiKey.attributes.apiKeyId;
|
||||
const response = await invalidateAPIKey({ id: apiKeyId }, securityPluginSetup);
|
||||
if (response.apiKeysEnabled === true && response.result.error_count > 0) {
|
||||
logger.error(`Failed to invalidate API Key [id="${apiKeyObj.attributes.apiKeyId}"]`);
|
||||
} else {
|
||||
try {
|
||||
await savedObjectsClient.delete('api_key_pending_invalidation', apiKeyObj.id);
|
||||
totalInvalidated++;
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`Failed to cleanup api key "${apiKeyObj.attributes.apiKeyId}". Error: ${err.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
logger.debug(`Total invalidated api keys "${totalInvalidated}"`);
|
||||
return totalInvalidated;
|
||||
}
|
53
x-pack/plugins/alerts/server/lib/get_cadence.ts
Normal file
53
x-pack/plugins/alerts/server/lib/get_cadence.ts
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 { memoize } from 'lodash';
|
||||
|
||||
export enum TimeUnit {
|
||||
Minute = 'm',
|
||||
Second = 's',
|
||||
Hour = 'h',
|
||||
Day = 'd',
|
||||
}
|
||||
const VALID_CADENCE = new Set(Object.values(TimeUnit));
|
||||
const CADENCE_IN_MS: Record<TimeUnit, number> = {
|
||||
[TimeUnit.Second]: 1000,
|
||||
[TimeUnit.Minute]: 60 * 1000,
|
||||
[TimeUnit.Hour]: 60 * 60 * 1000,
|
||||
[TimeUnit.Day]: 24 * 60 * 60 * 1000,
|
||||
};
|
||||
|
||||
const isNumeric = (numAsStr: string) => /^\d+$/.test(numAsStr);
|
||||
|
||||
export const parseIntervalAsMillisecond = memoize((value: string): number => {
|
||||
const numericAsStr: string = value.slice(0, -1);
|
||||
const numeric: number = parseInt(numericAsStr, 10);
|
||||
const cadence: TimeUnit | string = value.slice(-1);
|
||||
if (
|
||||
!VALID_CADENCE.has(cadence as TimeUnit) ||
|
||||
isNaN(numeric) ||
|
||||
numeric <= 0 ||
|
||||
!isNumeric(numericAsStr)
|
||||
) {
|
||||
throw new Error(
|
||||
`Invalid time value "${value}". Time must be of the form {number}m. Example: 5m.`
|
||||
);
|
||||
}
|
||||
return numeric * CADENCE_IN_MS[cadence as TimeUnit];
|
||||
});
|
||||
|
||||
/**
|
||||
* Returns a date that is the specified interval from given date.
|
||||
*
|
||||
* @param {Date} date - The date to add interval to
|
||||
* @param {string} interval - THe time of the form `Nm` such as `5m`
|
||||
*/
|
||||
export function timePeriodBeforeDate(date: Date, timePeriod: string): Date {
|
||||
const result = new Date(date.valueOf());
|
||||
const milisecFromTime = parseIntervalAsMillisecond(timePeriod);
|
||||
result.setMilliseconds(result.getMilliseconds() - milisecFromTime);
|
||||
return result;
|
||||
}
|
|
@ -22,6 +22,10 @@ describe('Alerting Plugin', () => {
|
|||
healthCheck: {
|
||||
interval: '5m',
|
||||
},
|
||||
invalidateApiKeysTask: {
|
||||
interval: '5m',
|
||||
removalDelay: '5m',
|
||||
},
|
||||
});
|
||||
const plugin = new AlertingPlugin(context);
|
||||
|
||||
|
@ -67,6 +71,10 @@ describe('Alerting Plugin', () => {
|
|||
healthCheck: {
|
||||
interval: '5m',
|
||||
},
|
||||
invalidateApiKeysTask: {
|
||||
interval: '5m',
|
||||
removalDelay: '5m',
|
||||
},
|
||||
});
|
||||
const plugin = new AlertingPlugin(context);
|
||||
|
||||
|
@ -114,6 +122,10 @@ describe('Alerting Plugin', () => {
|
|||
healthCheck: {
|
||||
interval: '5m',
|
||||
},
|
||||
invalidateApiKeysTask: {
|
||||
interval: '5m',
|
||||
removalDelay: '5m',
|
||||
},
|
||||
});
|
||||
const plugin = new AlertingPlugin(context);
|
||||
|
||||
|
|
|
@ -65,6 +65,10 @@ import { initializeAlertingTelemetry, scheduleAlertingTelemetry } from './usage/
|
|||
import { IEventLogger, IEventLogService, IEventLogClientService } from '../../event_log/server';
|
||||
import { PluginStartContract as FeaturesPluginStart } from '../../features/server';
|
||||
import { setupSavedObjects } from './saved_objects';
|
||||
import {
|
||||
initializeApiKeyInvalidator,
|
||||
scheduleApiKeyInvalidatorTask,
|
||||
} from './invalidate_pending_api_keys/task';
|
||||
import {
|
||||
getHealthStatusStream,
|
||||
scheduleAlertingHealthCheck,
|
||||
|
@ -200,6 +204,14 @@ export class AlertingPlugin {
|
|||
});
|
||||
}
|
||||
|
||||
initializeApiKeyInvalidator(
|
||||
this.logger,
|
||||
core.getStartServices(),
|
||||
plugins.taskManager,
|
||||
this.config,
|
||||
this.security
|
||||
);
|
||||
|
||||
core.getStartServices().then(async ([, startPlugins]) => {
|
||||
core.status.set(
|
||||
combineLatest([
|
||||
|
@ -308,7 +320,9 @@ export class AlertingPlugin {
|
|||
});
|
||||
|
||||
scheduleAlertingTelemetry(this.telemetryLogger, plugins.taskManager);
|
||||
|
||||
scheduleAlertingHealthCheck(this.logger, this.config, plugins.taskManager);
|
||||
scheduleApiKeyInvalidatorTask(this.telemetryLogger, this.config, plugins.taskManager);
|
||||
|
||||
return {
|
||||
listTypes: alertTypeRegistry!.list.bind(this.alertTypeRegistry!),
|
||||
|
|
|
@ -42,10 +42,32 @@ export function setupSavedObjects(
|
|||
mappings: mappings.alert,
|
||||
});
|
||||
|
||||
savedObjects.registerType({
|
||||
name: 'api_key_pending_invalidation',
|
||||
hidden: true,
|
||||
namespaceType: 'agnostic',
|
||||
mappings: {
|
||||
properties: {
|
||||
apiKeyId: {
|
||||
type: 'keyword',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'date',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Encrypted attributes
|
||||
encryptedSavedObjects.registerType({
|
||||
type: 'alert',
|
||||
attributesToEncrypt: new Set(['apiKey']),
|
||||
attributesToExcludeFromAAD: new Set(AlertAttributesExcludedFromAAD),
|
||||
});
|
||||
|
||||
// Encrypted attributes
|
||||
encryptedSavedObjects.registerType({
|
||||
type: 'api_key_pending_invalidation',
|
||||
attributesToEncrypt: new Set(['apiKeyId']),
|
||||
});
|
||||
}
|
||||
|
|
|
@ -180,4 +180,16 @@ export interface AlertsConfigType {
|
|||
};
|
||||
}
|
||||
|
||||
export interface AlertsConfigType {
|
||||
invalidateApiKeysTask: {
|
||||
interval: string;
|
||||
removalDelay: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface InvalidatePendingApiKey {
|
||||
apiKeyId: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export type AlertTypeRegistry = PublicMethodsOf<OrigAlertTypeRegistry>;
|
||||
|
|
|
@ -92,6 +92,7 @@ export function createTestConfig(name: string, options: CreateTestConfigOptions)
|
|||
...xPackApiIntegrationTestsConfig.get('kbnTestServer.serverArgs'),
|
||||
`--xpack.actions.allowedHosts=${JSON.stringify(['localhost', 'some.non.existent.com'])}`,
|
||||
'--xpack.encryptedSavedObjects.encryptionKey="wuGNaIhoMpk5sO4UBxgr3NyW1sFcLgIf"',
|
||||
'--xpack.alerts.invalidateApiKeysTask.interval="15s"',
|
||||
`--xpack.actions.enabledActionTypes=${JSON.stringify(enabledActionTypes)}`,
|
||||
...actionsProxyUrl,
|
||||
|
||||
|
|
|
@ -437,6 +437,21 @@ export function defineAlertTypes(
|
|||
throw new Error('this alert is intended to fail');
|
||||
},
|
||||
};
|
||||
const longRunningAlertType: AlertType = {
|
||||
id: 'test.longRunning',
|
||||
name: 'Test: Long Running',
|
||||
actionGroups: [
|
||||
{
|
||||
id: 'default',
|
||||
name: 'Default',
|
||||
},
|
||||
],
|
||||
producer: 'alertsFixture',
|
||||
defaultActionGroupId: 'default',
|
||||
async executor() {
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
},
|
||||
};
|
||||
|
||||
alerts.registerType(getAlwaysFiringAlertType());
|
||||
alerts.registerType(getCumulativeFiringAlertType());
|
||||
|
@ -449,4 +464,5 @@ export function defineAlertTypes(
|
|||
alerts.registerType(onlyStateVariablesAlertType);
|
||||
alerts.registerType(getPatternFiringAlertType());
|
||||
alerts.registerType(throwAlertType);
|
||||
alerts.registerType(longRunningAlertType);
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
|
|||
'test.unrestricted-noop',
|
||||
'test.patternFiring',
|
||||
'test.throw',
|
||||
'test.longRunning',
|
||||
],
|
||||
privileges: {
|
||||
all: {
|
||||
|
@ -72,6 +73,7 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
|
|||
'test.unrestricted-noop',
|
||||
'test.patternFiring',
|
||||
'test.throw',
|
||||
'test.longRunning',
|
||||
],
|
||||
},
|
||||
ui: [],
|
||||
|
@ -96,6 +98,7 @@ export class FixturePlugin implements Plugin<void, void, FixtureSetupDeps, Fixtu
|
|||
'test.unrestricted-noop',
|
||||
'test.patternFiring',
|
||||
'test.throw',
|
||||
'test.longRunning',
|
||||
],
|
||||
},
|
||||
ui: [],
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
IKibanaResponse,
|
||||
} from 'kibana/server';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { InvalidatePendingApiKey } from '../../../../../../../plugins/alerts/server/types';
|
||||
import { RawAlert } from '../../../../../../../plugins/alerts/server/types';
|
||||
import { TaskInstance } from '../../../../../../../plugins/task_manager/server';
|
||||
import { FixtureSetupDeps, FixtureStartDeps } from './plugin';
|
||||
|
@ -184,4 +185,31 @@ export function defineRoutes(
|
|||
return res.ok({ body: result });
|
||||
}
|
||||
);
|
||||
|
||||
router.get(
|
||||
{
|
||||
path: '/api/alerts_fixture/api_keys_pending_invalidation',
|
||||
validate: {},
|
||||
},
|
||||
async (
|
||||
context: RequestHandlerContext,
|
||||
req: KibanaRequest<any, any, any, any>,
|
||||
res: KibanaResponseFactory
|
||||
): Promise<IKibanaResponse<any>> => {
|
||||
try {
|
||||
const [{ savedObjects }] = await core.getStartServices();
|
||||
const savedObjectsWithTasksAndAlerts = await savedObjects.getScopedClient(req, {
|
||||
includedHiddenTypes: ['api_key_pending_invalidation'],
|
||||
});
|
||||
const findResult = await savedObjectsWithTasksAndAlerts.find<InvalidatePendingApiKey>({
|
||||
type: 'api_key_pending_invalidation',
|
||||
});
|
||||
return res.ok({
|
||||
body: { apiKeysToInvalidate: findResult.saved_objects },
|
||||
});
|
||||
} catch (err) {
|
||||
return res.badRequest({ body: err });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -836,6 +836,80 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
|
|||
}
|
||||
});
|
||||
|
||||
it('should handle updates for a long running alert type without failing the underlying tasks due to invalidated ApiKey', async () => {
|
||||
const { body: createdAlert } = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
enabled: true,
|
||||
name: 'abc',
|
||||
tags: ['foo'],
|
||||
alertTypeId: 'test.longRunning',
|
||||
consumer: 'alertsFixture',
|
||||
schedule: { interval: '1s' },
|
||||
throttle: '1m',
|
||||
actions: [],
|
||||
params: {},
|
||||
})
|
||||
.expect(200);
|
||||
objectRemover.add(space.id, createdAlert.id, 'alert', 'alerts');
|
||||
const updatedData = {
|
||||
name: 'bcd',
|
||||
tags: ['bar'],
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
schedule: { interval: '1m' },
|
||||
actions: [],
|
||||
throttle: '1m',
|
||||
};
|
||||
const response = await supertestWithoutAuth
|
||||
.put(`${getUrlPrefix(space.id)}/api/alerts/alert/${createdAlert.id}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.auth(user.username, user.password)
|
||||
.send(updatedData);
|
||||
|
||||
const statusUpdates: string[] = [];
|
||||
await retry.try(async () => {
|
||||
const alertTask = (await getAlertingTaskById(createdAlert.scheduledTaskId)).docs[0];
|
||||
statusUpdates.push(alertTask.status);
|
||||
expect(alertTask.status).to.eql('idle');
|
||||
});
|
||||
|
||||
expect(statusUpdates.find((status) => status === 'failed')).to.be(undefined);
|
||||
|
||||
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(403);
|
||||
expect(response.body).to.eql({
|
||||
error: 'Forbidden',
|
||||
message: getConsumerUnauthorizedErrorMessage(
|
||||
'update',
|
||||
'test.longRunning',
|
||||
'alertsFixture'
|
||||
),
|
||||
statusCode: 403,
|
||||
});
|
||||
break;
|
||||
case 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
case 'space_1_all_alerts_none_actions at space1':
|
||||
case 'space_1_all_with_restricted_fixture at space1':
|
||||
expect(response.statusCode).to.eql(200);
|
||||
await retry.try(async () => {
|
||||
const alertTask = (await getAlertingTaskById(createdAlert.scheduledTaskId)).docs[0];
|
||||
expect(alertTask.status).to.eql('idle');
|
||||
// ensure the alert is rescheduled to a minute from now
|
||||
ensureDatetimeIsWithinRange(Date.parse(alertTask.runAt), 60 * 1000);
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle updates to an alert schedule by setting the new schedule for the underlying task', async () => {
|
||||
const { body: createdAlert } = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/api/alerts/alert`)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue