mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
Retain APIKey when disabling/enabling a rule (#131581)
* Retain APIKey when disabling/enabling a rule
This commit is contained in:
parent
d828e9b799
commit
de29010c43
24 changed files with 2057 additions and 1165 deletions
|
@ -65,10 +65,9 @@ Rules and connectors are isolated to the {kib} space in which they were created.
|
|||
Rules are authorized using an <<api-keys,API key>> associated with the last user to edit the rule. This API key captures a snapshot of the user's privileges at the time of edit and is subsequently used to run all background tasks associated with the rule, including condition checks like {es} queries and triggered actions. The following rule actions will re-generate the API key:
|
||||
|
||||
* Creating a rule
|
||||
* Enabling a disabled rule
|
||||
* Updating a rule
|
||||
|
||||
[IMPORTANT]
|
||||
==============================================
|
||||
If a rule requires certain privileges, such as index privileges, to run, and a user without those privileges updates, disables, or re-enables the rule, the rule will no longer function. Conversely, if a user with greater or administrator privileges modifies the rule, it will begin running with increased privileges.
|
||||
If a rule requires certain privileges, such as index privileges, to run, and a user without those privileges updates the rule, the rule will no longer function. Conversely, if a user with greater or administrator privileges modifies the rule, it will begin running with increased privileges.
|
||||
==============================================
|
||||
|
|
|
@ -1828,7 +1828,7 @@ export class RulesClient {
|
|||
}
|
||||
|
||||
private async enableWithOCC({ id }: { id: string }) {
|
||||
let apiKeyToInvalidate: string | null = null;
|
||||
let existingApiKey: string | null = null;
|
||||
let attributes: RawRule;
|
||||
let version: string | undefined;
|
||||
|
||||
|
@ -1837,14 +1837,11 @@ export class RulesClient {
|
|||
await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser<RawRule>('alert', id, {
|
||||
namespace: this.namespace,
|
||||
});
|
||||
apiKeyToInvalidate = decryptedAlert.attributes.apiKey;
|
||||
existingApiKey = decryptedAlert.attributes.apiKey;
|
||||
attributes = decryptedAlert.attributes;
|
||||
version = decryptedAlert.version;
|
||||
} catch (e) {
|
||||
// We'll skip invalidating the API key since we failed to load the decrypted saved object
|
||||
this.logger.error(
|
||||
`enable(): Failed to load API key to invalidate on alert ${id}: ${e.message}`
|
||||
);
|
||||
this.logger.error(`enable(): Failed to load API key of alert ${id}: ${e.message}`);
|
||||
// Still attempt to load the attributes and version using SOC
|
||||
const alert = await this.unsecuredSavedObjectsClient.get<RawRule>('alert', id);
|
||||
attributes = alert.attributes;
|
||||
|
@ -1886,19 +1883,10 @@ export class RulesClient {
|
|||
if (attributes.enabled === false) {
|
||||
const username = await this.getUserName();
|
||||
|
||||
let createdAPIKey = null;
|
||||
try {
|
||||
createdAPIKey = await this.createAPIKey(
|
||||
this.generateAPIKeyName(attributes.alertTypeId, attributes.name)
|
||||
);
|
||||
} catch (error) {
|
||||
throw Boom.badRequest(`Error enabling rule: could not create API key - ${error.message}`);
|
||||
}
|
||||
|
||||
const updateAttributes = this.updateMeta({
|
||||
...attributes,
|
||||
...(!existingApiKey && (await this.createNewAPIKeySet({ attributes, username }))),
|
||||
enabled: true,
|
||||
...this.apiKeyAsAlertAttributes(createdAPIKey, username),
|
||||
updatedBy: username,
|
||||
updatedAt: new Date().toISOString(),
|
||||
executionStatus: {
|
||||
|
@ -1909,15 +1897,10 @@ export class RulesClient {
|
|||
warning: null,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await this.unsecuredSavedObjectsClient.update('alert', id, updateAttributes, { version });
|
||||
} catch (e) {
|
||||
// Avoid unused API key
|
||||
await bulkMarkApiKeysForInvalidation(
|
||||
{ apiKeys: updateAttributes.apiKey ? [updateAttributes.apiKey] : [] },
|
||||
this.logger,
|
||||
this.unsecuredSavedObjectsClient
|
||||
);
|
||||
throw e;
|
||||
}
|
||||
const scheduledTask = await this.scheduleRule({
|
||||
|
@ -1930,16 +1913,28 @@ export class RulesClient {
|
|||
await this.unsecuredSavedObjectsClient.update('alert', id, {
|
||||
scheduledTaskId: scheduledTask.id,
|
||||
});
|
||||
if (apiKeyToInvalidate) {
|
||||
await bulkMarkApiKeysForInvalidation(
|
||||
{ apiKeys: [apiKeyToInvalidate] },
|
||||
this.logger,
|
||||
this.unsecuredSavedObjectsClient
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async createNewAPIKeySet({
|
||||
attributes,
|
||||
username,
|
||||
}: {
|
||||
attributes: RawRule;
|
||||
username: string | null;
|
||||
}): Promise<Pick<RawRule, 'apiKey' | 'apiKeyOwner'>> {
|
||||
let createdAPIKey = null;
|
||||
try {
|
||||
createdAPIKey = await this.createAPIKey(
|
||||
this.generateAPIKeyName(attributes.alertTypeId, attributes.name)
|
||||
);
|
||||
} catch (error) {
|
||||
throw Boom.badRequest(`Error creating API key for rule: ${error.message}`);
|
||||
}
|
||||
|
||||
return this.apiKeyAsAlertAttributes(createdAPIKey, username);
|
||||
}
|
||||
|
||||
public async disable({ id }: { id: string }): Promise<void> {
|
||||
return await retryIfConflicts(
|
||||
this.logger,
|
||||
|
@ -1949,7 +1944,6 @@ export class RulesClient {
|
|||
}
|
||||
|
||||
private async disableWithOCC({ id }: { id: string }) {
|
||||
let apiKeyToInvalidate: string | null = null;
|
||||
let attributes: RawRule;
|
||||
let version: string | undefined;
|
||||
|
||||
|
@ -1958,14 +1952,10 @@ export class RulesClient {
|
|||
await this.encryptedSavedObjectsClient.getDecryptedAsInternalUser<RawRule>('alert', id, {
|
||||
namespace: this.namespace,
|
||||
});
|
||||
apiKeyToInvalidate = decryptedAlert.attributes.apiKey;
|
||||
attributes = decryptedAlert.attributes;
|
||||
version = decryptedAlert.version;
|
||||
} catch (e) {
|
||||
// We'll skip invalidating the API key since we failed to load the decrypted saved object
|
||||
this.logger.error(
|
||||
`disable(): Failed to load API key to invalidate on alert ${id}: ${e.message}`
|
||||
);
|
||||
this.logger.error(`disable(): Failed to load API key of alert ${id}: ${e.message}`);
|
||||
// Still attempt to load the attributes and version using SOC
|
||||
const alert = await this.unsecuredSavedObjectsClient.get<RawRule>('alert', id);
|
||||
attributes = alert.attributes;
|
||||
|
@ -2058,26 +2048,14 @@ export class RulesClient {
|
|||
...attributes,
|
||||
enabled: false,
|
||||
scheduledTaskId: null,
|
||||
apiKey: null,
|
||||
apiKeyOwner: null,
|
||||
updatedBy: await this.getUserName(),
|
||||
updatedAt: new Date().toISOString(),
|
||||
}),
|
||||
{ version }
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
attributes.scheduledTaskId
|
||||
? this.taskManager.removeIfExists(attributes.scheduledTaskId)
|
||||
: null,
|
||||
apiKeyToInvalidate
|
||||
? await bulkMarkApiKeysForInvalidation(
|
||||
{ apiKeys: [apiKeyToInvalidate] },
|
||||
this.logger,
|
||||
this.unsecuredSavedObjectsClient
|
||||
)
|
||||
: null,
|
||||
]);
|
||||
if (attributes.scheduledTaskId) {
|
||||
await this.taskManager.removeIfExists(attributes.scheduledTaskId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -18,7 +18,6 @@ import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
|
|||
import { getBeforeSetup, setGlobalDate } from './lib';
|
||||
import { eventLoggerMock } from '@kbn/event-log-plugin/server/event_logger.mock';
|
||||
import { TaskStatus } from '@kbn/task-manager-plugin/server';
|
||||
import { bulkMarkApiKeysForInvalidation } from '../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation';
|
||||
|
||||
jest.mock('../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation', () => ({
|
||||
bulkMarkApiKeysForInvalidation: jest.fn(),
|
||||
|
@ -111,6 +110,7 @@ describe('disable()', () => {
|
|||
attributes: {
|
||||
...existingAlert.attributes,
|
||||
apiKey: Buffer.from('123:abc').toString('base64'),
|
||||
apiKeyOwner: 'elastic',
|
||||
},
|
||||
version: '123',
|
||||
references: [],
|
||||
|
@ -206,11 +206,11 @@ describe('disable()', () => {
|
|||
alertTypeId: 'myType',
|
||||
enabled: false,
|
||||
meta: {
|
||||
versionApiKeyLastmodified: kibanaVersion,
|
||||
versionApiKeyLastmodified: 'v7.10.0',
|
||||
},
|
||||
scheduledTaskId: null,
|
||||
apiKey: null,
|
||||
apiKeyOwner: null,
|
||||
apiKey: 'MTIzOmFiYw==',
|
||||
apiKeyOwner: 'elastic',
|
||||
updatedAt: '2019-02-12T21:01:22.479Z',
|
||||
updatedBy: 'elastic',
|
||||
actions: [
|
||||
|
@ -230,12 +230,6 @@ describe('disable()', () => {
|
|||
}
|
||||
);
|
||||
expect(taskManager.removeIfExists).toHaveBeenCalledWith('task-123');
|
||||
expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledTimes(1);
|
||||
expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledWith(
|
||||
{ apiKeys: ['MTIzOmFiYw=='] },
|
||||
expect.any(Object),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
test('disables the rule with calling event log to "recover" the alert instances from the task state', async () => {
|
||||
|
@ -282,11 +276,11 @@ describe('disable()', () => {
|
|||
alertTypeId: 'myType',
|
||||
enabled: false,
|
||||
meta: {
|
||||
versionApiKeyLastmodified: kibanaVersion,
|
||||
versionApiKeyLastmodified: 'v7.10.0',
|
||||
},
|
||||
scheduledTaskId: null,
|
||||
apiKey: null,
|
||||
apiKeyOwner: null,
|
||||
apiKey: 'MTIzOmFiYw==',
|
||||
apiKeyOwner: 'elastic',
|
||||
updatedAt: '2019-02-12T21:01:22.479Z',
|
||||
updatedBy: 'elastic',
|
||||
actions: [
|
||||
|
@ -306,12 +300,6 @@ describe('disable()', () => {
|
|||
}
|
||||
);
|
||||
expect(taskManager.removeIfExists).toHaveBeenCalledWith('task-123');
|
||||
expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledTimes(1);
|
||||
expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledWith(
|
||||
{ apiKeys: ['MTIzOmFiYw=='] },
|
||||
expect.any(Object),
|
||||
expect.any(Object)
|
||||
);
|
||||
|
||||
expect(eventLogger.logEvent).toHaveBeenCalledTimes(1);
|
||||
expect(eventLogger.logEvent.mock.calls[0][0]).toStrictEqual({
|
||||
|
@ -369,11 +357,11 @@ describe('disable()', () => {
|
|||
alertTypeId: 'myType',
|
||||
enabled: false,
|
||||
meta: {
|
||||
versionApiKeyLastmodified: kibanaVersion,
|
||||
versionApiKeyLastmodified: 'v7.10.0',
|
||||
},
|
||||
scheduledTaskId: null,
|
||||
apiKey: null,
|
||||
apiKeyOwner: null,
|
||||
apiKey: 'MTIzOmFiYw==',
|
||||
apiKeyOwner: 'elastic',
|
||||
updatedAt: '2019-02-12T21:01:22.479Z',
|
||||
updatedBy: 'elastic',
|
||||
actions: [
|
||||
|
@ -393,12 +381,6 @@ describe('disable()', () => {
|
|||
}
|
||||
);
|
||||
expect(taskManager.removeIfExists).toHaveBeenCalledWith('task-123');
|
||||
expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledTimes(1);
|
||||
expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledWith(
|
||||
{ apiKeys: ['MTIzOmFiYw=='] },
|
||||
expect.any(Object),
|
||||
expect.any(Object)
|
||||
);
|
||||
|
||||
expect(eventLogger.logEvent).toHaveBeenCalledTimes(0);
|
||||
expect(rulesClientParams.logger.warn).toHaveBeenCalledWith(
|
||||
|
@ -408,7 +390,6 @@ describe('disable()', () => {
|
|||
|
||||
test('falls back when getDecryptedAsInternalUser throws an error', async () => {
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockRejectedValueOnce(new Error('Fail'));
|
||||
|
||||
await rulesClient.disable({ id: '1' });
|
||||
expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1');
|
||||
expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', {
|
||||
|
@ -422,12 +403,7 @@ describe('disable()', () => {
|
|||
schedule: { interval: '10s' },
|
||||
alertTypeId: 'myType',
|
||||
enabled: false,
|
||||
meta: {
|
||||
versionApiKeyLastmodified: kibanaVersion,
|
||||
},
|
||||
scheduledTaskId: null,
|
||||
apiKey: null,
|
||||
apiKeyOwner: null,
|
||||
updatedAt: '2019-02-12T21:01:22.479Z',
|
||||
updatedBy: 'elastic',
|
||||
actions: [
|
||||
|
@ -447,7 +423,6 @@ describe('disable()', () => {
|
|||
}
|
||||
);
|
||||
expect(taskManager.removeIfExists).toHaveBeenCalledWith('task-123');
|
||||
expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test(`doesn't disable already disabled alerts`, async () => {
|
||||
|
@ -463,14 +438,6 @@ describe('disable()', () => {
|
|||
await rulesClient.disable({ id: '1' });
|
||||
expect(unsecuredSavedObjectsClient.update).not.toHaveBeenCalled();
|
||||
expect(taskManager.removeIfExists).not.toHaveBeenCalled();
|
||||
expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test(`doesn't invalidate when no API key is used`, async () => {
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce(existingAlert);
|
||||
|
||||
await rulesClient.disable({ id: '1' });
|
||||
expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('swallows error when failing to load decrypted saved object', async () => {
|
||||
|
@ -479,9 +446,8 @@ describe('disable()', () => {
|
|||
await rulesClient.disable({ id: '1' });
|
||||
expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled();
|
||||
expect(taskManager.removeIfExists).toHaveBeenCalled();
|
||||
expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled();
|
||||
expect(rulesClientParams.logger.error).toHaveBeenCalledWith(
|
||||
'disable(): Failed to load API key to invalidate on alert 1: Fail'
|
||||
'disable(): Failed to load API key of alert 1: Fail'
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -493,17 +459,6 @@ describe('disable()', () => {
|
|||
);
|
||||
});
|
||||
|
||||
test('swallows error when invalidate API key throws', async () => {
|
||||
unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Fail'));
|
||||
await rulesClient.disable({ id: '1' });
|
||||
expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledTimes(1);
|
||||
expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledWith(
|
||||
{ apiKeys: ['MTIzOmFiYw=='] },
|
||||
expect.any(Object),
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
|
||||
test('throws when failing to remove task from task manager', async () => {
|
||||
taskManager.removeIfExists.mockRejectedValueOnce(new Error('Failed to remove task'));
|
||||
|
||||
|
|
|
@ -17,7 +17,6 @@ import { ActionsAuthorization } from '@kbn/actions-plugin/server';
|
|||
import { TaskStatus } from '@kbn/task-manager-plugin/server';
|
||||
import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
|
||||
import { getBeforeSetup, setGlobalDate } from './lib';
|
||||
import { bulkMarkApiKeysForInvalidation } from '../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation';
|
||||
|
||||
jest.mock('../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation', () => ({
|
||||
bulkMarkApiKeysForInvalidation: jest.fn(),
|
||||
|
@ -51,23 +50,22 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
auditLogger,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
getBeforeSetup(rulesClientParams, taskManager, ruleTypeRegistry);
|
||||
(auditLogger.log as jest.Mock).mockClear();
|
||||
});
|
||||
|
||||
setGlobalDate();
|
||||
|
||||
describe('enable()', () => {
|
||||
let rulesClient: RulesClient;
|
||||
const existingAlert = {
|
||||
|
||||
const existingRule = {
|
||||
id: '1',
|
||||
type: 'alert',
|
||||
attributes: {
|
||||
name: 'name',
|
||||
consumer: 'myApp',
|
||||
schedule: { interval: '10s' },
|
||||
alertTypeId: 'myType',
|
||||
enabled: false,
|
||||
apiKey: 'MTIzOmFiYw==',
|
||||
apiKeyOwner: 'elastic',
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
|
@ -84,23 +82,24 @@ describe('enable()', () => {
|
|||
references: [],
|
||||
};
|
||||
|
||||
const existingRuleWithoutApiKey = {
|
||||
...existingRule,
|
||||
attributes: {
|
||||
...existingRule.attributes,
|
||||
apiKey: null,
|
||||
apiKeyOwner: null,
|
||||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
getBeforeSetup(rulesClientParams, taskManager, ruleTypeRegistry);
|
||||
(auditLogger.log as jest.Mock).mockClear();
|
||||
rulesClient = new RulesClient(rulesClientParams);
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingAlert);
|
||||
unsecuredSavedObjectsClient.get.mockResolvedValue(existingAlert);
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingRule);
|
||||
unsecuredSavedObjectsClient.get.mockResolvedValue(existingRule);
|
||||
rulesClientParams.createAPIKey.mockResolvedValue({
|
||||
apiKeysEnabled: false,
|
||||
});
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
...existingAlert,
|
||||
attributes: {
|
||||
...existingAlert.attributes,
|
||||
enabled: true,
|
||||
apiKey: null,
|
||||
apiKeyOwner: null,
|
||||
updatedBy: 'elastic',
|
||||
},
|
||||
});
|
||||
taskManager.schedule.mockResolvedValue({
|
||||
id: '1',
|
||||
scheduledAt: new Date(),
|
||||
|
@ -187,27 +186,17 @@ describe('enable()', () => {
|
|||
});
|
||||
|
||||
test('enables a rule', async () => {
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
...existingAlert,
|
||||
attributes: {
|
||||
...existingAlert.attributes,
|
||||
enabled: true,
|
||||
apiKey: null,
|
||||
apiKeyOwner: null,
|
||||
updatedBy: 'elastic',
|
||||
},
|
||||
});
|
||||
|
||||
await rulesClient.enable({ id: '1' });
|
||||
expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled();
|
||||
expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', {
|
||||
namespace: 'default',
|
||||
});
|
||||
expect(rulesClientParams.createAPIKey).toHaveBeenCalled();
|
||||
expect(unsecuredSavedObjectsClient.create).not.toBeCalledWith('api_key_pending_invalidation');
|
||||
expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith(
|
||||
'alert',
|
||||
'1',
|
||||
{
|
||||
name: 'name',
|
||||
schedule: { interval: '10s' },
|
||||
alertTypeId: 'myType',
|
||||
consumer: 'myApp',
|
||||
|
@ -217,8 +206,8 @@ describe('enable()', () => {
|
|||
},
|
||||
updatedAt: '2019-02-12T21:01:22.479Z',
|
||||
updatedBy: 'elastic',
|
||||
apiKey: null,
|
||||
apiKeyOwner: null,
|
||||
apiKey: 'MTIzOmFiYw==',
|
||||
apiKeyOwner: 'elastic',
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
|
@ -265,33 +254,65 @@ describe('enable()', () => {
|
|||
});
|
||||
});
|
||||
|
||||
test('invalidates API key if ever one existed prior to updating', async () => {
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({
|
||||
...existingAlert,
|
||||
attributes: {
|
||||
...existingAlert.attributes,
|
||||
apiKey: Buffer.from('123:abc').toString('base64'),
|
||||
},
|
||||
test('enables a rule that does not have an apiKey', async () => {
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingRuleWithoutApiKey);
|
||||
rulesClientParams.createAPIKey.mockResolvedValueOnce({
|
||||
apiKeysEnabled: true,
|
||||
result: { id: '123', name: '123', api_key: 'abc' },
|
||||
});
|
||||
|
||||
await rulesClient.enable({ id: '1' });
|
||||
expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled();
|
||||
expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', {
|
||||
namespace: 'default',
|
||||
});
|
||||
expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledTimes(1);
|
||||
expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledWith(
|
||||
{ apiKeys: ['MTIzOmFiYw=='] },
|
||||
expect.any(Object),
|
||||
expect.any(Object)
|
||||
expect(unsecuredSavedObjectsClient.create).not.toBeCalledWith('api_key_pending_invalidation');
|
||||
expect(rulesClientParams.createAPIKey).toHaveBeenCalledWith('Alerting: myType/name');
|
||||
expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith(
|
||||
'alert',
|
||||
'1',
|
||||
{
|
||||
name: 'name',
|
||||
schedule: { interval: '10s' },
|
||||
alertTypeId: 'myType',
|
||||
consumer: 'myApp',
|
||||
enabled: true,
|
||||
meta: {
|
||||
versionApiKeyLastmodified: kibanaVersion,
|
||||
},
|
||||
updatedAt: '2019-02-12T21:01:22.479Z',
|
||||
updatedBy: 'elastic',
|
||||
apiKey: 'MTIzOmFiYw==',
|
||||
apiKeyOwner: 'elastic',
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
id: '1',
|
||||
actionTypeId: '1',
|
||||
actionRef: '1',
|
||||
params: {
|
||||
foo: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
executionStatus: {
|
||||
status: 'pending',
|
||||
lastDuration: 0,
|
||||
lastExecutionDate: '2019-02-12T21:01:22.479Z',
|
||||
error: null,
|
||||
warning: null,
|
||||
},
|
||||
},
|
||||
{
|
||||
version: '123',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test(`doesn't enable already enabled alerts`, async () => {
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValueOnce({
|
||||
...existingAlert,
|
||||
...existingRuleWithoutApiKey,
|
||||
attributes: {
|
||||
...existingAlert.attributes,
|
||||
...existingRuleWithoutApiKey.attributes,
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
|
@ -314,6 +335,7 @@ describe('enable()', () => {
|
|||
'alert',
|
||||
'1',
|
||||
{
|
||||
name: 'name',
|
||||
schedule: { interval: '10s' },
|
||||
alertTypeId: 'myType',
|
||||
consumer: 'myApp',
|
||||
|
@ -351,14 +373,14 @@ describe('enable()', () => {
|
|||
});
|
||||
|
||||
test('throws an error if API key creation throws', async () => {
|
||||
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue(existingRuleWithoutApiKey);
|
||||
|
||||
rulesClientParams.createAPIKey.mockImplementation(() => {
|
||||
throw new Error('no');
|
||||
});
|
||||
expect(
|
||||
await expect(
|
||||
async () => await rulesClient.enable({ id: '1' })
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"Error enabling rule: could not create API key - no"`
|
||||
);
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"Error creating API key for rule: no"`);
|
||||
});
|
||||
|
||||
test('falls back when failing to getDecryptedAsInternalUser', async () => {
|
||||
|
@ -367,7 +389,7 @@ describe('enable()', () => {
|
|||
await rulesClient.enable({ id: '1' });
|
||||
expect(unsecuredSavedObjectsClient.get).toHaveBeenCalledWith('alert', '1');
|
||||
expect(rulesClientParams.logger.error).toHaveBeenCalledWith(
|
||||
'enable(): Failed to load API key to invalidate on alert 1: Fail'
|
||||
'enable(): Failed to load API key of alert 1: Fail'
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -396,13 +418,6 @@ describe('enable()', () => {
|
|||
`"Fail to update"`
|
||||
);
|
||||
expect(rulesClientParams.getUserName).toHaveBeenCalled();
|
||||
expect(rulesClientParams.createAPIKey).toHaveBeenCalled();
|
||||
expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledTimes(1);
|
||||
expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledWith(
|
||||
{ apiKeys: ['MTIzOmFiYw=='] },
|
||||
expect.any(Object),
|
||||
expect.any(Object)
|
||||
);
|
||||
expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(1);
|
||||
expect(taskManager.schedule).not.toHaveBeenCalled();
|
||||
});
|
||||
|
@ -410,9 +425,9 @@ describe('enable()', () => {
|
|||
test('throws error when failing to update the second time', async () => {
|
||||
unsecuredSavedObjectsClient.update.mockReset();
|
||||
unsecuredSavedObjectsClient.update.mockResolvedValueOnce({
|
||||
...existingAlert,
|
||||
...existingRuleWithoutApiKey,
|
||||
attributes: {
|
||||
...existingAlert.attributes,
|
||||
...existingRuleWithoutApiKey.attributes,
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
|
@ -424,7 +439,6 @@ describe('enable()', () => {
|
|||
`"Fail to update second time"`
|
||||
);
|
||||
expect(rulesClientParams.getUserName).toHaveBeenCalled();
|
||||
expect(rulesClientParams.createAPIKey).toHaveBeenCalled();
|
||||
expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledTimes(2);
|
||||
expect(taskManager.schedule).toHaveBeenCalled();
|
||||
});
|
||||
|
@ -436,15 +450,14 @@ describe('enable()', () => {
|
|||
`"Fail to schedule"`
|
||||
);
|
||||
expect(rulesClientParams.getUserName).toHaveBeenCalled();
|
||||
expect(rulesClientParams.createAPIKey).toHaveBeenCalled();
|
||||
expect(unsecuredSavedObjectsClient.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('enables a rule if conflict errors received when scheduling a task', async () => {
|
||||
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
|
||||
...existingAlert,
|
||||
...existingRuleWithoutApiKey,
|
||||
attributes: {
|
||||
...existingAlert.attributes,
|
||||
...existingRuleWithoutApiKey.attributes,
|
||||
enabled: true,
|
||||
apiKey: null,
|
||||
apiKeyOwner: null,
|
||||
|
@ -460,11 +473,12 @@ describe('enable()', () => {
|
|||
expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', {
|
||||
namespace: 'default',
|
||||
});
|
||||
expect(rulesClientParams.createAPIKey).toHaveBeenCalled();
|
||||
expect(unsecuredSavedObjectsClient.create).not.toBeCalledWith('api_key_pending_invalidation');
|
||||
expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith(
|
||||
'alert',
|
||||
'1',
|
||||
{
|
||||
name: 'name',
|
||||
schedule: { interval: '10s' },
|
||||
alertTypeId: 'myType',
|
||||
consumer: 'myApp',
|
||||
|
@ -474,8 +488,8 @@ describe('enable()', () => {
|
|||
},
|
||||
updatedAt: '2019-02-12T21:01:22.479Z',
|
||||
updatedBy: 'elastic',
|
||||
apiKey: null,
|
||||
apiKeyOwner: null,
|
||||
apiKey: 'MTIzOmFiYw==',
|
||||
apiKeyOwner: 'elastic',
|
||||
actions: [
|
||||
{
|
||||
group: 'default',
|
||||
|
|
|
@ -54,7 +54,7 @@ export const DeleteModalConfirmation = ({
|
|||
'xpack.triggersActionsUI.deleteSelectedIdsConfirmModal.descriptionText',
|
||||
{
|
||||
defaultMessage:
|
||||
"You can't recover {numIdsToDelete, plural, one {a deleted {singleTitle}} other {deleted {multipleTitle}}}.",
|
||||
"You won't be able to recover {numIdsToDelete, plural, one {a deleted {singleTitle}} other {deleted {multipleTitle}}}.",
|
||||
values: { numIdsToDelete, singleTitle, multipleTitle },
|
||||
}
|
||||
);
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiConfirmModal } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { HttpSetup } from '@kbn/core/public';
|
||||
import { useKibana } from '../../common/lib/kibana';
|
||||
export const UpdateApiKeyModalConfirmation = ({
|
||||
onCancel,
|
||||
idsToUpdate,
|
||||
apiUpdateApiKeyCall,
|
||||
setIsLoadingState,
|
||||
onUpdated,
|
||||
}: {
|
||||
onCancel: () => void;
|
||||
idsToUpdate: string[];
|
||||
apiUpdateApiKeyCall: ({ id, http }: { id: string; http: HttpSetup }) => Promise<string>;
|
||||
setIsLoadingState: (isLoading: boolean) => void;
|
||||
onUpdated: () => void;
|
||||
}) => {
|
||||
const {
|
||||
http,
|
||||
notifications: { toasts },
|
||||
} = useKibana().services;
|
||||
|
||||
const [updateModalFlyoutVisible, setUpdateModalVisibility] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => {
|
||||
setUpdateModalVisibility(idsToUpdate.length > 0);
|
||||
}, [idsToUpdate]);
|
||||
|
||||
return updateModalFlyoutVisible ? (
|
||||
<EuiConfirmModal
|
||||
buttonColor="primary"
|
||||
data-test-subj="updateApiKeyIdsConfirmation"
|
||||
title={i18n.translate('xpack.triggersActionsUI.updateApiKeyConfirmModal.title', {
|
||||
defaultMessage: 'Update API key',
|
||||
})}
|
||||
onCancel={() => {
|
||||
setUpdateModalVisibility(false);
|
||||
onCancel();
|
||||
}}
|
||||
onConfirm={async () => {
|
||||
setUpdateModalVisibility(false);
|
||||
setIsLoadingState(true);
|
||||
try {
|
||||
await Promise.all(idsToUpdate.map((id) => apiUpdateApiKeyCall({ id, http })));
|
||||
toasts.addSuccess(
|
||||
i18n.translate('xpack.triggersActionsUI.updateApiKeyConfirmModal.successMessage', {
|
||||
defaultMessage:
|
||||
'API {idsToUpdate, plural, one {key} other {keys}} {idsToUpdate, plural, one {has} other {have}} been updated',
|
||||
values: { idsToUpdate: idsToUpdate.length },
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
toasts.addError(e, {
|
||||
title: i18n.translate(
|
||||
'xpack.triggersActionsUI.updateApiKeyConfirmModal.failureMessage',
|
||||
{
|
||||
defaultMessage:
|
||||
'Failed to update the API {idsToUpdate, plural, one {key} other {keys}}',
|
||||
values: { idsToUpdate: idsToUpdate.length },
|
||||
}
|
||||
),
|
||||
});
|
||||
}
|
||||
setIsLoadingState(false);
|
||||
onUpdated();
|
||||
}}
|
||||
cancelButtonText={i18n.translate(
|
||||
'xpack.triggersActionsUI.updateApiKeyConfirmModal.cancelButton',
|
||||
{
|
||||
defaultMessage: 'Cancel',
|
||||
}
|
||||
)}
|
||||
confirmButtonText={i18n.translate(
|
||||
'xpack.triggersActionsUI.updateApiKeyConfirmModal.confirmButton',
|
||||
{
|
||||
defaultMessage: 'Update',
|
||||
}
|
||||
)}
|
||||
>
|
||||
{i18n.translate('xpack.triggersActionsUI.updateApiKeyConfirmModal.description', {
|
||||
defaultMessage:
|
||||
'You will not be able to recover the old API {idsToUpdate, plural, one {key} other {keys}}',
|
||||
values: { idsToUpdate: idsToUpdate.length },
|
||||
})}
|
||||
</EuiConfirmModal>
|
||||
) : null;
|
||||
};
|
|
@ -27,3 +27,4 @@ export { updateRule } from './update';
|
|||
export { resolveRule } from './resolve_rule';
|
||||
export { snoozeRule } from './snooze';
|
||||
export { unsnoozeRule } from './unsnooze';
|
||||
export { updateAPIKey } from './update_api_key';
|
||||
|
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { httpServiceMock } from '@kbn/core/public/mocks';
|
||||
import { updateAPIKey } from './update_api_key';
|
||||
|
||||
const http = httpServiceMock.createStartContract();
|
||||
beforeEach(() => jest.resetAllMocks());
|
||||
|
||||
describe('updateAPIKey', () => {
|
||||
test('should call _update_api_key rule API', async () => {
|
||||
const result = await updateAPIKey({ http, id: '1/' });
|
||||
expect(result).toEqual(undefined);
|
||||
expect(http.post.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
"/internal/alerting/rule/1%2F/_update_api_key",
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { HttpSetup } from '@kbn/core/public';
|
||||
import { INTERNAL_BASE_ALERTING_API_PATH } from '../../constants';
|
||||
|
||||
export async function updateAPIKey({ id, http }: { id: string; http: HttpSetup }): Promise<string> {
|
||||
return http.post<string>(
|
||||
`${INTERNAL_BASE_ALERTING_API_PATH}/rule/${encodeURIComponent(id)}/_update_api_key`
|
||||
);
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.ruleActionsPopover__deleteButton {
|
||||
color: $euiColorDangerText;
|
||||
}
|
|
@ -0,0 +1,201 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
||||
import * as React from 'react';
|
||||
import { RuleActionsPopover } from './rule_actions_popover';
|
||||
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
|
||||
import { Rule } from '../../../..';
|
||||
|
||||
describe('rule_actions_popover', () => {
|
||||
const onDeleteMock = jest.fn();
|
||||
const onApiKeyUpdateMock = jest.fn();
|
||||
const onEnableDisableMock = jest.fn();
|
||||
|
||||
function mockRule(overloads: Partial<Rule> = {}): Rule {
|
||||
return {
|
||||
id: '12345',
|
||||
enabled: true,
|
||||
name: `rule-12345`,
|
||||
tags: [],
|
||||
ruleTypeId: '.noop',
|
||||
consumer: 'consumer',
|
||||
schedule: { interval: '1m' },
|
||||
actions: [],
|
||||
params: {},
|
||||
createdBy: null,
|
||||
updatedBy: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
apiKeyOwner: null,
|
||||
throttle: null,
|
||||
notifyWhen: null,
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
executionStatus: {
|
||||
status: 'unknown',
|
||||
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
|
||||
},
|
||||
...overloads,
|
||||
};
|
||||
}
|
||||
|
||||
it('renders all the buttons', () => {
|
||||
const rule = mockRule();
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<RuleActionsPopover
|
||||
rule={rule}
|
||||
onDelete={onDeleteMock}
|
||||
onApiKeyUpdate={onApiKeyUpdateMock}
|
||||
canSaveRule={true}
|
||||
onEnableDisable={onEnableDisableMock}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
const actionButton = screen.getByTestId('ruleActionsButton');
|
||||
expect(actionButton).toBeInTheDocument();
|
||||
fireEvent.click(actionButton);
|
||||
expect(screen.getByText('Update API key')).toBeInTheDocument();
|
||||
expect(screen.getByText('Delete rule')).toBeInTheDocument();
|
||||
expect(screen.getByText('Disable')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onDelete', async () => {
|
||||
const rule = mockRule();
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<RuleActionsPopover
|
||||
rule={rule}
|
||||
onDelete={onDeleteMock}
|
||||
onApiKeyUpdate={onApiKeyUpdateMock}
|
||||
canSaveRule={true}
|
||||
onEnableDisable={onEnableDisableMock}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
const actionButton = screen.getByTestId('ruleActionsButton');
|
||||
expect(actionButton).toBeInTheDocument();
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const deleteButton = screen.getByText('Delete rule');
|
||||
expect(deleteButton).toBeInTheDocument();
|
||||
fireEvent.click(deleteButton);
|
||||
|
||||
expect(onDeleteMock).toHaveBeenCalledWith('12345');
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Delete rule')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('disables the rule', async () => {
|
||||
const rule = mockRule();
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<RuleActionsPopover
|
||||
rule={rule}
|
||||
onDelete={onDeleteMock}
|
||||
onApiKeyUpdate={onApiKeyUpdateMock}
|
||||
canSaveRule={true}
|
||||
onEnableDisable={onEnableDisableMock}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
const actionButton = screen.getByTestId('ruleActionsButton');
|
||||
expect(actionButton).toBeInTheDocument();
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const disableButton = screen.getByText('Disable');
|
||||
expect(disableButton).toBeInTheDocument();
|
||||
fireEvent.click(disableButton);
|
||||
|
||||
expect(onEnableDisableMock).toHaveBeenCalledWith(false);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Disable')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
it('enables the rule', async () => {
|
||||
const rule = mockRule({ enabled: false });
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<RuleActionsPopover
|
||||
rule={rule}
|
||||
onDelete={onDeleteMock}
|
||||
onApiKeyUpdate={onApiKeyUpdateMock}
|
||||
canSaveRule={true}
|
||||
onEnableDisable={onEnableDisableMock}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
const actionButton = screen.getByTestId('ruleActionsButton');
|
||||
expect(actionButton).toBeInTheDocument();
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const enableButton = screen.getByText('Enable');
|
||||
expect(enableButton).toBeInTheDocument();
|
||||
fireEvent.click(enableButton);
|
||||
|
||||
expect(onEnableDisableMock).toHaveBeenCalledWith(true);
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Disable')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('calls onApiKeyUpdate', async () => {
|
||||
const rule = mockRule();
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<RuleActionsPopover
|
||||
rule={rule}
|
||||
onDelete={onDeleteMock}
|
||||
onApiKeyUpdate={onApiKeyUpdateMock}
|
||||
canSaveRule={true}
|
||||
onEnableDisable={onEnableDisableMock}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
const actionButton = screen.getByTestId('ruleActionsButton');
|
||||
expect(actionButton).toBeInTheDocument();
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
const deleteButton = screen.getByText('Update API key');
|
||||
expect(deleteButton).toBeInTheDocument();
|
||||
fireEvent.click(deleteButton);
|
||||
|
||||
expect(onApiKeyUpdateMock).toHaveBeenCalledWith('12345');
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Update API key')).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it('disables buttons when the user does not have enough permission', async () => {
|
||||
const rule = mockRule();
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<RuleActionsPopover
|
||||
rule={rule}
|
||||
onDelete={onDeleteMock}
|
||||
onApiKeyUpdate={onApiKeyUpdateMock}
|
||||
canSaveRule={false}
|
||||
onEnableDisable={onEnableDisableMock}
|
||||
/>
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
const actionButton = screen.getByTestId('ruleActionsButton');
|
||||
expect(actionButton).toBeInTheDocument();
|
||||
fireEvent.click(actionButton);
|
||||
|
||||
expect(screen.getByText('Delete rule').closest('button')).toBeDisabled();
|
||||
expect(screen.getByText('Update API key').closest('button')).toBeDisabled();
|
||||
expect(screen.getByText('Disable').closest('button')).toBeDisabled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useState } from 'react';
|
||||
import { EuiButtonEmpty, EuiContextMenu, EuiPopover } from '@elastic/eui';
|
||||
import './rule_actions_popopver.scss';
|
||||
import { Rule } from '../../../..';
|
||||
|
||||
export interface RuleActionsPopoverProps {
|
||||
rule: Rule;
|
||||
canSaveRule: boolean;
|
||||
onDelete: (ruleId: string) => void;
|
||||
onApiKeyUpdate: (ruleId: string) => void;
|
||||
onEnableDisable: (enable: boolean) => void;
|
||||
}
|
||||
|
||||
export const RuleActionsPopover: React.FunctionComponent<RuleActionsPopoverProps> = ({
|
||||
rule,
|
||||
canSaveRule,
|
||||
onDelete,
|
||||
onApiKeyUpdate,
|
||||
onEnableDisable,
|
||||
}) => {
|
||||
const [isPopoverOpen, setIsPopoverOpen] = useState<boolean>(false);
|
||||
|
||||
return (
|
||||
<EuiPopover
|
||||
button={
|
||||
<EuiButtonEmpty
|
||||
disabled={false}
|
||||
data-test-subj="ruleActionsButton"
|
||||
data-testid="ruleActionsButton"
|
||||
iconType="boxesHorizontal"
|
||||
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
|
||||
aria-label={i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.popoverButtonTitle',
|
||||
{ defaultMessage: 'Actions' }
|
||||
)}
|
||||
/>
|
||||
}
|
||||
isOpen={isPopoverOpen}
|
||||
closePopover={() => setIsPopoverOpen(false)}
|
||||
ownFocus
|
||||
panelPaddingSize="none"
|
||||
>
|
||||
<EuiContextMenu
|
||||
initialPanelId={0}
|
||||
panels={[
|
||||
{
|
||||
id: 0,
|
||||
items: [
|
||||
{
|
||||
disabled: !canSaveRule,
|
||||
'data-test-subj': 'disableButton',
|
||||
onClick: async () => {
|
||||
setIsPopoverOpen(false);
|
||||
onEnableDisable(!rule.enabled);
|
||||
},
|
||||
name: !rule.enabled
|
||||
? i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.enableRuleButtonLabel',
|
||||
{ defaultMessage: 'Enable' }
|
||||
)
|
||||
: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.disableRuleButtonLabel',
|
||||
{ defaultMessage: 'Disable' }
|
||||
),
|
||||
},
|
||||
{
|
||||
disabled: !canSaveRule,
|
||||
'data-test-subj': 'updateAPIKeyButton',
|
||||
onClick: () => {
|
||||
setIsPopoverOpen(false);
|
||||
onApiKeyUpdate(rule.id);
|
||||
},
|
||||
name: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.updateAPIKeyButtonLabel',
|
||||
{ defaultMessage: 'Update API key' }
|
||||
),
|
||||
},
|
||||
{
|
||||
disabled: !canSaveRule,
|
||||
className: 'ruleActionsPopover__deleteButton',
|
||||
'data-test-subj': 'deleteRuleButton',
|
||||
onClick: () => {
|
||||
setIsPopoverOpen(false);
|
||||
onDelete(rule.id);
|
||||
},
|
||||
name: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.ruleDetails.deleteRuleButtonLabel',
|
||||
{ defaultMessage: 'Delete rule' }
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
]}
|
||||
className="ruleActionsPopover"
|
||||
data-test-subj="ruleActionsPopover"
|
||||
data-testid="ruleActionsPopover"
|
||||
/>
|
||||
</EuiPopover>
|
||||
);
|
||||
};
|
File diff suppressed because it is too large
Load diff
|
@ -27,6 +27,10 @@ import {
|
|||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
import { toMountPoint } from '@kbn/kibana-react-plugin/public';
|
||||
import { RuleExecutionStatusErrorReasons, parseDuration } from '@kbn/alerting-plugin/common';
|
||||
import { UpdateApiKeyModalConfirmation } from '../../../components/update_api_key_modal_confirmation';
|
||||
import { updateAPIKey, deleteRules } from '../../../lib/rule_api';
|
||||
import { DeleteModalConfirmation } from '../../../components/delete_modal_confirmation';
|
||||
import { RuleActionsPopover } from './rule_actions_popover';
|
||||
import {
|
||||
hasAllPrivilege,
|
||||
hasExecuteActionsCapability,
|
||||
|
@ -49,7 +53,7 @@ import {
|
|||
import { RuleRouteWithApi } from './rule_route';
|
||||
import { ViewInApp } from './view_in_app';
|
||||
import { RuleEdit } from '../../rule_form';
|
||||
import { routeToRuleDetails } from '../../../constants';
|
||||
import { routeToRuleDetails, routeToRules } from '../../../constants';
|
||||
import {
|
||||
rulesErrorReasonTranslationsMapping,
|
||||
rulesWarningReasonTranslationsMapping,
|
||||
|
@ -94,6 +98,9 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
|
|||
dispatch({ command: { type: 'setRule' }, payload: { key: 'rule', value } });
|
||||
};
|
||||
|
||||
const [rulesToDelete, setRulesToDelete] = useState<string[]>([]);
|
||||
const [rulesToUpdateAPIKey, setRulesToUpdateAPIKey] = useState<string[]>([]);
|
||||
|
||||
const [hasActionsWithBrokenConnector, setHasActionsWithBrokenConnector] =
|
||||
useState<boolean>(false);
|
||||
|
||||
|
@ -207,6 +214,10 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
|
|||
history.push(routeToRuleDetails.replace(`:ruleId`, rule.id));
|
||||
};
|
||||
|
||||
const goToRulesList = () => {
|
||||
history.push(routeToRules);
|
||||
};
|
||||
|
||||
const getRuleStatusErrorReasonText = () => {
|
||||
if (rule.executionStatus.error && rule.executionStatus.error.reason) {
|
||||
return rulesErrorReasonTranslationsMapping[rule.executionStatus.error.reason];
|
||||
|
@ -223,40 +234,71 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
|
|||
}
|
||||
};
|
||||
|
||||
const rightPageHeaderButtons = hasEditButton
|
||||
? [
|
||||
<>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="openEditRuleFlyoutButton"
|
||||
iconType="pencil"
|
||||
onClick={() => setEditFlyoutVisibility(true)}
|
||||
name="edit"
|
||||
disabled={!ruleType.enabledInLicense}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.ruleDetails.editRuleButtonLabel"
|
||||
defaultMessage="Edit"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
{editFlyoutVisible && (
|
||||
<RuleEdit
|
||||
initialRule={rule}
|
||||
onClose={() => {
|
||||
setInitialRule(rule);
|
||||
setEditFlyoutVisibility(false);
|
||||
}}
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
ruleTypeRegistry={ruleTypeRegistry}
|
||||
ruleType={ruleType}
|
||||
onSave={setRule}
|
||||
/>
|
||||
)}
|
||||
</>,
|
||||
]
|
||||
: [];
|
||||
const editButton = hasEditButton ? (
|
||||
<>
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="openEditRuleFlyoutButton"
|
||||
iconType="pencil"
|
||||
onClick={() => setEditFlyoutVisibility(true)}
|
||||
name="edit"
|
||||
disabled={!ruleType.enabledInLicense}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="xpack.triggersActionsUI.sections.ruleDetails.editRuleButtonLabel"
|
||||
defaultMessage="Edit"
|
||||
/>
|
||||
</EuiButtonEmpty>
|
||||
{editFlyoutVisible && (
|
||||
<RuleEdit
|
||||
initialRule={rule}
|
||||
onClose={() => {
|
||||
setInitialRule(rule);
|
||||
setEditFlyoutVisibility(false);
|
||||
}}
|
||||
actionTypeRegistry={actionTypeRegistry}
|
||||
ruleTypeRegistry={ruleTypeRegistry}
|
||||
ruleType={ruleType}
|
||||
onSave={setRule}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<DeleteModalConfirmation
|
||||
onDeleted={async () => {
|
||||
setRulesToDelete([]);
|
||||
goToRulesList();
|
||||
}}
|
||||
onErrors={async () => {
|
||||
// Refresh the rule from the server, it may have been deleted
|
||||
await requestRefresh();
|
||||
setRulesToDelete([]);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setRulesToDelete([]);
|
||||
}}
|
||||
apiDeleteCall={deleteRules}
|
||||
idsToDelete={rulesToDelete}
|
||||
singleTitle={i18n.translate('xpack.triggersActionsUI.sections.rulesList.singleTitle', {
|
||||
defaultMessage: 'rule',
|
||||
})}
|
||||
multipleTitle=""
|
||||
setIsLoadingState={() => {}}
|
||||
/>
|
||||
<UpdateApiKeyModalConfirmation
|
||||
onCancel={() => {
|
||||
setRulesToUpdateAPIKey([]);
|
||||
}}
|
||||
idsToUpdate={rulesToUpdateAPIKey}
|
||||
apiUpdateApiKeyCall={updateAPIKey}
|
||||
setIsLoadingState={() => {}}
|
||||
onUpdated={async () => {
|
||||
setRulesToUpdateAPIKey([]);
|
||||
requestRefresh();
|
||||
}}
|
||||
/>
|
||||
<EuiPageHeader
|
||||
data-test-subj="ruleDetailsTitle"
|
||||
bottomBorder
|
||||
|
@ -378,7 +420,25 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
|
|||
</EuiFlexGroup>
|
||||
}
|
||||
rightSideItems={[
|
||||
<ViewInApp rule={rule} />,
|
||||
<RuleActionsPopover
|
||||
canSaveRule={canSaveRule}
|
||||
rule={rule}
|
||||
onDelete={(ruleId) => {
|
||||
setRulesToDelete([ruleId]);
|
||||
}}
|
||||
onApiKeyUpdate={(ruleId) => {
|
||||
setRulesToUpdateAPIKey([ruleId]);
|
||||
}}
|
||||
onEnableDisable={async (enable) => {
|
||||
if (enable) {
|
||||
await enableRule(rule);
|
||||
} else {
|
||||
await disableRule(rule);
|
||||
}
|
||||
requestRefresh();
|
||||
}}
|
||||
/>,
|
||||
editButton,
|
||||
<EuiButtonEmpty
|
||||
data-test-subj="refreshRulesButton"
|
||||
iconType="refresh"
|
||||
|
@ -391,7 +451,7 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
|
|||
defaultMessage="Refresh"
|
||||
/>
|
||||
</EuiButtonEmpty>,
|
||||
...rightPageHeaderButtons,
|
||||
<ViewInApp rule={rule} />,
|
||||
]}
|
||||
/>
|
||||
<EuiSpacer size="l" />
|
||||
|
@ -422,7 +482,6 @@ export const RuleDetails: React.FunctionComponent<RuleDetailsProps> = ({
|
|||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
) : null}
|
||||
|
||||
{rule.enabled && rule.executionStatus.status === 'warning' ? (
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem>
|
||||
|
|
|
@ -1,9 +1,3 @@
|
|||
.actCollapsedItemActions {
|
||||
.euiContextMenuItem:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
button[data-test-subj='deleteRule'] {
|
||||
color: $euiColorDanger;
|
||||
.collapsedItemActions__deleteButton {
|
||||
color: $euiColorDangerText;
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ const disableRule = jest.fn();
|
|||
const enableRule = jest.fn();
|
||||
const unmuteRule = jest.fn();
|
||||
const muteRule = jest.fn();
|
||||
const onUpdateAPIKey = jest.fn();
|
||||
|
||||
export const tick = (ms = 0) =>
|
||||
new Promise((resolve) => {
|
||||
|
@ -91,6 +92,7 @@ describe('CollapsedItemActions', () => {
|
|||
enableRule,
|
||||
unmuteRule,
|
||||
muteRule,
|
||||
onUpdateAPIKey,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -118,6 +120,7 @@ describe('CollapsedItemActions', () => {
|
|||
expect(wrapper.find('[data-test-subj="disableButton"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="editRule"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="deleteRule"]').exists()).toBeFalsy();
|
||||
expect(wrapper.find('[data-test-subj="updateApiKey"]').exists()).toBeFalsy();
|
||||
|
||||
wrapper.find('[data-test-subj="selectActionButton"]').first().simulate('click');
|
||||
await act(async () => {
|
||||
|
@ -130,6 +133,7 @@ describe('CollapsedItemActions', () => {
|
|||
expect(wrapper.find('[data-test-subj="disableButton"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="editRule"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="deleteRule"]').exists()).toBeTruthy();
|
||||
expect(wrapper.find('[data-test-subj="updateApiKey"]').exists()).toBeTruthy();
|
||||
|
||||
expect(
|
||||
wrapper.find('[data-test-subj="selectActionButton"]').first().props().disabled
|
||||
|
@ -143,6 +147,7 @@ describe('CollapsedItemActions', () => {
|
|||
expect(wrapper.find(`[data-test-subj="editRule"] button`).text()).toEqual('Edit rule');
|
||||
expect(wrapper.find(`[data-test-subj="deleteRule"] button`).prop('disabled')).toBeFalsy();
|
||||
expect(wrapper.find(`[data-test-subj="deleteRule"] button`).text()).toEqual('Delete rule');
|
||||
expect(wrapper.find(`[data-test-subj="updateApiKey"] button`).text()).toEqual('Update API key');
|
||||
});
|
||||
|
||||
test('handles case when rule is unmuted and enabled and mute is clicked', async () => {
|
||||
|
|
|
@ -23,6 +23,7 @@ export type ComponentOpts = {
|
|||
onRuleChanged: () => void;
|
||||
setRulesToDelete: React.Dispatch<React.SetStateAction<string[]>>;
|
||||
onEditRule: (item: RuleTableItem) => void;
|
||||
onUpdateAPIKey: (id: string[]) => void;
|
||||
} & Pick<BulkOperationsComponentOpts, 'disableRule' | 'enableRule' | 'unmuteRule' | 'muteRule'>;
|
||||
|
||||
export const CollapsedItemActions: React.FunctionComponent<ComponentOpts> = ({
|
||||
|
@ -34,6 +35,7 @@ export const CollapsedItemActions: React.FunctionComponent<ComponentOpts> = ({
|
|||
muteRule,
|
||||
setRulesToDelete,
|
||||
onEditRule,
|
||||
onUpdateAPIKey,
|
||||
}: ComponentOpts) => {
|
||||
const { ruleTypeRegistry } = useKibana().services;
|
||||
|
||||
|
@ -53,6 +55,7 @@ export const CollapsedItemActions: React.FunctionComponent<ComponentOpts> = ({
|
|||
<EuiButtonIcon
|
||||
disabled={!item.isEditable}
|
||||
data-test-subj="selectActionButton"
|
||||
data-testid="selectActionButton"
|
||||
iconType="boxesHorizontal"
|
||||
onClick={() => setIsPopoverOpen(!isPopoverOpen)}
|
||||
aria-label={i18n.translate(
|
||||
|
@ -133,6 +136,19 @@ export const CollapsedItemActions: React.FunctionComponent<ComponentOpts> = ({
|
|||
},
|
||||
{
|
||||
disabled: !item.isEditable,
|
||||
'data-test-subj': 'updateApiKey',
|
||||
onClick: () => {
|
||||
setIsPopoverOpen(!isPopoverOpen);
|
||||
onUpdateAPIKey([item.id]);
|
||||
},
|
||||
name: i18n.translate(
|
||||
'xpack.triggersActionsUI.sections.rulesList.collapsedItemActions.updateApiKey',
|
||||
{ defaultMessage: 'Update API key' }
|
||||
),
|
||||
},
|
||||
{
|
||||
disabled: !item.isEditable,
|
||||
className: 'collapsedItemActions__deleteButton',
|
||||
'data-test-subj': 'deleteRule',
|
||||
onClick: () => {
|
||||
setIsPopoverOpen(!isPopoverOpen);
|
||||
|
@ -161,6 +177,7 @@ export const CollapsedItemActions: React.FunctionComponent<ComponentOpts> = ({
|
|||
panels={panels}
|
||||
className="actCollapsedItemActions"
|
||||
data-test-subj="collapsedActionPanel"
|
||||
data-testid="collapsedActionPanel"
|
||||
/>
|
||||
</EuiPopover>
|
||||
);
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
*/
|
||||
|
||||
import * as React from 'react';
|
||||
import { fireEvent, act, render, screen } from '@testing-library/react';
|
||||
import { mountWithIntl, nextTick } from '@kbn/test-jest-helpers';
|
||||
import { ReactWrapper } from 'enzyme';
|
||||
import { act } from 'react-dom/test-utils';
|
||||
import { actionTypeRegistryMock } from '../../../action_type_registry.mock';
|
||||
import { ruleTypeRegistryMock } from '../../../rule_type_registry.mock';
|
||||
import { RulesList, percentileFields } from './rules_list';
|
||||
|
@ -23,8 +23,10 @@ import { getFormattedDuration, getFormattedMilliseconds } from '../../../lib/mon
|
|||
import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features';
|
||||
|
||||
import { useKibana } from '../../../../common/lib/kibana';
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
import { __IntlProvider as IntlProvider } from '@kbn/i18n-react';
|
||||
import { IToasts } from '@kbn/core/public';
|
||||
|
||||
jest.mock('../../../../common/lib/kibana');
|
||||
jest.mock('../../../lib/action_connector_api', () => ({
|
||||
loadActionTypes: jest.fn(),
|
||||
loadAllActions: jest.fn(),
|
||||
|
@ -33,6 +35,7 @@ jest.mock('../../../lib/rule_api', () => ({
|
|||
loadRules: jest.fn(),
|
||||
loadRuleTypes: jest.fn(),
|
||||
loadRuleAggregations: jest.fn(),
|
||||
updateAPIKey: jest.fn(),
|
||||
loadRuleTags: jest.fn(),
|
||||
alertingFrameworkHealth: jest.fn(() => ({
|
||||
isSufficientlySecure: true,
|
||||
|
@ -67,12 +70,12 @@ jest.mock('../../../../common/get_experimental_features', () => ({
|
|||
|
||||
const ruleTags = ['a', 'b', 'c', 'd'];
|
||||
|
||||
const { loadRules, loadRuleTypes, loadRuleAggregations, loadRuleTags } =
|
||||
const { loadRules, loadRuleTypes, loadRuleAggregations, updateAPIKey, loadRuleTags } =
|
||||
jest.requireMock('../../../lib/rule_api');
|
||||
const { loadActionTypes, loadAllActions } = jest.requireMock('../../../lib/action_connector_api');
|
||||
|
||||
const actionTypeRegistry = actionTypeRegistryMock.create();
|
||||
const ruleTypeRegistry = ruleTypeRegistryMock.create();
|
||||
|
||||
const ruleType = {
|
||||
id: 'test_rule_type',
|
||||
description: 'test',
|
||||
|
@ -101,11 +104,299 @@ const ruleTypeFromApi = {
|
|||
};
|
||||
ruleTypeRegistry.list.mockReturnValue([ruleType]);
|
||||
actionTypeRegistry.list.mockReturnValue([]);
|
||||
|
||||
const useKibanaMock = useKibana as jest.Mocked<typeof useKibana>;
|
||||
|
||||
const mockedRulesData = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'test rule',
|
||||
tags: ['tag1'],
|
||||
enabled: true,
|
||||
ruleTypeId: 'test_rule_type',
|
||||
schedule: { interval: '1s' },
|
||||
actions: [],
|
||||
params: { name: 'test rule type name' },
|
||||
scheduledTaskId: null,
|
||||
createdBy: null,
|
||||
updatedBy: null,
|
||||
apiKeyOwner: null,
|
||||
throttle: '1m',
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
executionStatus: {
|
||||
status: 'active',
|
||||
lastDuration: 500,
|
||||
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
|
||||
error: null,
|
||||
},
|
||||
monitoring: {
|
||||
execution: {
|
||||
history: [
|
||||
{
|
||||
success: true,
|
||||
duration: 1000000,
|
||||
},
|
||||
{
|
||||
success: true,
|
||||
duration: 200000,
|
||||
},
|
||||
{
|
||||
success: false,
|
||||
duration: 300000,
|
||||
},
|
||||
],
|
||||
calculated_metrics: {
|
||||
success_ratio: 0.66,
|
||||
p50: 200000,
|
||||
p95: 300000,
|
||||
p99: 300000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'test rule ok',
|
||||
tags: ['tag1'],
|
||||
enabled: true,
|
||||
ruleTypeId: 'test_rule_type',
|
||||
schedule: { interval: '5d' },
|
||||
actions: [],
|
||||
params: { name: 'test rule type name' },
|
||||
scheduledTaskId: null,
|
||||
createdBy: null,
|
||||
updatedBy: null,
|
||||
apiKeyOwner: null,
|
||||
throttle: '1m',
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
executionStatus: {
|
||||
status: 'ok',
|
||||
lastDuration: 61000,
|
||||
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
|
||||
error: null,
|
||||
},
|
||||
monitoring: {
|
||||
execution: {
|
||||
history: [
|
||||
{
|
||||
success: true,
|
||||
duration: 100000,
|
||||
},
|
||||
{
|
||||
success: true,
|
||||
duration: 500000,
|
||||
},
|
||||
],
|
||||
calculated_metrics: {
|
||||
success_ratio: 1,
|
||||
p50: 0,
|
||||
p95: 100000,
|
||||
p99: 500000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'test rule pending',
|
||||
tags: ['tag1'],
|
||||
enabled: true,
|
||||
ruleTypeId: 'test_rule_type',
|
||||
schedule: { interval: '5d' },
|
||||
actions: [],
|
||||
params: { name: 'test rule type name' },
|
||||
scheduledTaskId: null,
|
||||
createdBy: null,
|
||||
updatedBy: null,
|
||||
apiKeyOwner: null,
|
||||
throttle: '1m',
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
executionStatus: {
|
||||
status: 'pending',
|
||||
lastDuration: 30234,
|
||||
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
|
||||
error: null,
|
||||
},
|
||||
monitoring: {
|
||||
execution: {
|
||||
history: [{ success: false, duration: 100 }],
|
||||
calculated_metrics: {
|
||||
success_ratio: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'test rule error',
|
||||
tags: ['tag1'],
|
||||
enabled: true,
|
||||
ruleTypeId: 'test_rule_type',
|
||||
schedule: { interval: '5d' },
|
||||
actions: [{ id: 'test', group: 'rule', params: { message: 'test' } }],
|
||||
params: { name: 'test rule type name' },
|
||||
scheduledTaskId: null,
|
||||
createdBy: null,
|
||||
updatedBy: null,
|
||||
apiKeyOwner: null,
|
||||
throttle: '1m',
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
executionStatus: {
|
||||
status: 'error',
|
||||
lastDuration: 122000,
|
||||
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
|
||||
error: {
|
||||
reason: RuleExecutionStatusErrorReasons.Unknown,
|
||||
message: 'test',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'test rule license error',
|
||||
tags: [],
|
||||
enabled: true,
|
||||
ruleTypeId: 'test_rule_type',
|
||||
schedule: { interval: '5d' },
|
||||
actions: [{ id: 'test', group: 'rule', params: { message: 'test' } }],
|
||||
params: { name: 'test rule type name' },
|
||||
scheduledTaskId: null,
|
||||
createdBy: null,
|
||||
updatedBy: null,
|
||||
apiKeyOwner: null,
|
||||
throttle: '1m',
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
executionStatus: {
|
||||
status: 'error',
|
||||
lastDuration: 500,
|
||||
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
|
||||
error: {
|
||||
reason: RuleExecutionStatusErrorReasons.License,
|
||||
message: 'test',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
name: 'test rule warning',
|
||||
tags: [],
|
||||
enabled: true,
|
||||
ruleTypeId: 'test_rule_type',
|
||||
schedule: { interval: '5d' },
|
||||
actions: [{ id: 'test', group: 'rule', params: { message: 'test' } }],
|
||||
params: { name: 'test rule type name' },
|
||||
scheduledTaskId: null,
|
||||
createdBy: null,
|
||||
updatedBy: null,
|
||||
apiKeyOwner: null,
|
||||
throttle: '1m',
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
executionStatus: {
|
||||
status: 'warning',
|
||||
lastDuration: 500,
|
||||
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
|
||||
warning: {
|
||||
reason: RuleExecutionStatusWarningReasons.MAX_EXECUTABLE_ACTIONS,
|
||||
message: 'test',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
(getIsExperimentalFeatureEnabled as jest.Mock<any, any>).mockImplementation(() => false);
|
||||
});
|
||||
describe('Update Api Key', () => {
|
||||
const addSuccess = jest.fn();
|
||||
const addError = jest.fn();
|
||||
|
||||
beforeAll(() => {
|
||||
loadRules.mockResolvedValue({
|
||||
page: 1,
|
||||
perPage: 10000,
|
||||
total: 0,
|
||||
data: mockedRulesData,
|
||||
});
|
||||
loadActionTypes.mockResolvedValue([]);
|
||||
loadRuleTypes.mockResolvedValue([ruleTypeFromApi]);
|
||||
loadAllActions.mockResolvedValue([]);
|
||||
useKibanaMock().services.notifications.toasts = {
|
||||
addSuccess,
|
||||
addError,
|
||||
} as unknown as IToasts;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('Updates the Api Key successfully', async () => {
|
||||
updateAPIKey.mockResolvedValueOnce(204);
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<RulesList />
|
||||
</IntlProvider>
|
||||
);
|
||||
expect(await screen.findByText('test rule ok')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getAllByTestId('selectActionButton')[1]);
|
||||
expect(screen.getByTestId('collapsedActionPanel')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByText('Update API key'));
|
||||
expect(screen.getByText('You will not be able to recover the old API key')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByText('Cancel'));
|
||||
expect(
|
||||
screen.queryByText('You will not be able to recover the old API key')
|
||||
).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getAllByTestId('selectActionButton')[1]);
|
||||
expect(screen.getByTestId('collapsedActionPanel')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByText('Update API key'));
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('Update'));
|
||||
});
|
||||
expect(updateAPIKey).toHaveBeenCalledWith(expect.objectContaining({ id: '2' }));
|
||||
expect(loadRules).toHaveBeenCalledTimes(2);
|
||||
expect(screen.queryByText("You can't recover the old API key")).not.toBeInTheDocument();
|
||||
expect(addSuccess).toHaveBeenCalledWith('API key has been updated');
|
||||
});
|
||||
|
||||
it('Update API key fails', async () => {
|
||||
updateAPIKey.mockRejectedValueOnce(500);
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<RulesList />
|
||||
</IntlProvider>
|
||||
);
|
||||
|
||||
expect(await screen.findByText('test rule ok')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getAllByTestId('selectActionButton')[1]);
|
||||
expect(screen.getByTestId('collapsedActionPanel')).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByText('Update API key'));
|
||||
expect(screen.getByText('You will not be able to recover the old API key')).toBeInTheDocument();
|
||||
|
||||
await act(async () => {
|
||||
fireEvent.click(screen.getByText('Update'));
|
||||
});
|
||||
expect(updateAPIKey).toHaveBeenCalledWith(expect.objectContaining({ id: '2' }));
|
||||
expect(loadRules).toHaveBeenCalledTimes(2);
|
||||
expect(
|
||||
screen.queryByText('You will not be able to recover the old API key')
|
||||
).not.toBeInTheDocument();
|
||||
expect(addError).toHaveBeenCalledWith(500, { title: 'Failed to update the API key' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('rules_list component empty', () => {
|
||||
let wrapper: ReactWrapper<any>;
|
||||
|
@ -174,208 +465,6 @@ describe('rules_list component empty', () => {
|
|||
describe('rules_list component with items', () => {
|
||||
let wrapper: ReactWrapper<any>;
|
||||
|
||||
const mockedRulesData = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'test rule',
|
||||
tags: ['tag1'],
|
||||
enabled: true,
|
||||
ruleTypeId: 'test_rule_type',
|
||||
schedule: { interval: '1s' },
|
||||
actions: [],
|
||||
params: { name: 'test rule type name' },
|
||||
scheduledTaskId: null,
|
||||
createdBy: null,
|
||||
updatedBy: null,
|
||||
apiKeyOwner: null,
|
||||
throttle: '1m',
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
executionStatus: {
|
||||
status: 'active',
|
||||
lastDuration: 500,
|
||||
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
|
||||
error: null,
|
||||
},
|
||||
monitoring: {
|
||||
execution: {
|
||||
history: [
|
||||
{
|
||||
success: true,
|
||||
duration: 1000000,
|
||||
},
|
||||
{
|
||||
success: true,
|
||||
duration: 200000,
|
||||
},
|
||||
{
|
||||
success: false,
|
||||
duration: 300000,
|
||||
},
|
||||
],
|
||||
calculated_metrics: {
|
||||
success_ratio: 0.66,
|
||||
p50: 200000,
|
||||
p95: 300000,
|
||||
p99: 300000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'test rule ok',
|
||||
tags: ['tag1'],
|
||||
enabled: true,
|
||||
ruleTypeId: 'test_rule_type',
|
||||
schedule: { interval: '5d' },
|
||||
actions: [],
|
||||
params: { name: 'test rule type name' },
|
||||
scheduledTaskId: null,
|
||||
createdBy: null,
|
||||
updatedBy: null,
|
||||
apiKeyOwner: null,
|
||||
throttle: '1m',
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
executionStatus: {
|
||||
status: 'ok',
|
||||
lastDuration: 61000,
|
||||
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
|
||||
error: null,
|
||||
},
|
||||
monitoring: {
|
||||
execution: {
|
||||
history: [
|
||||
{
|
||||
success: true,
|
||||
duration: 100000,
|
||||
},
|
||||
{
|
||||
success: true,
|
||||
duration: 500000,
|
||||
},
|
||||
],
|
||||
calculated_metrics: {
|
||||
success_ratio: 1,
|
||||
p50: 0,
|
||||
p95: 100000,
|
||||
p99: 500000,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'test rule pending',
|
||||
tags: ['tag1'],
|
||||
enabled: true,
|
||||
ruleTypeId: 'test_rule_type',
|
||||
schedule: { interval: '5d' },
|
||||
actions: [],
|
||||
params: { name: 'test rule type name' },
|
||||
scheduledTaskId: null,
|
||||
createdBy: null,
|
||||
updatedBy: null,
|
||||
apiKeyOwner: null,
|
||||
throttle: '1m',
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
executionStatus: {
|
||||
status: 'pending',
|
||||
lastDuration: 30234,
|
||||
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
|
||||
error: null,
|
||||
},
|
||||
monitoring: {
|
||||
execution: {
|
||||
history: [{ success: false, duration: 100 }],
|
||||
calculated_metrics: {
|
||||
success_ratio: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'test rule error',
|
||||
tags: ['tag1'],
|
||||
enabled: true,
|
||||
ruleTypeId: 'test_rule_type',
|
||||
schedule: { interval: '5d' },
|
||||
actions: [{ id: 'test', group: 'rule', params: { message: 'test' } }],
|
||||
params: { name: 'test rule type name' },
|
||||
scheduledTaskId: null,
|
||||
createdBy: null,
|
||||
updatedBy: null,
|
||||
apiKeyOwner: null,
|
||||
throttle: '1m',
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
executionStatus: {
|
||||
status: 'error',
|
||||
lastDuration: 122000,
|
||||
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
|
||||
error: {
|
||||
reason: RuleExecutionStatusErrorReasons.Unknown,
|
||||
message: 'test',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'test rule license error',
|
||||
tags: [],
|
||||
enabled: true,
|
||||
ruleTypeId: 'test_rule_type',
|
||||
schedule: { interval: '5d' },
|
||||
actions: [{ id: 'test', group: 'rule', params: { message: 'test' } }],
|
||||
params: { name: 'test rule type name' },
|
||||
scheduledTaskId: null,
|
||||
createdBy: null,
|
||||
updatedBy: null,
|
||||
apiKeyOwner: null,
|
||||
throttle: '1m',
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
executionStatus: {
|
||||
status: 'error',
|
||||
lastDuration: 500,
|
||||
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
|
||||
error: {
|
||||
reason: RuleExecutionStatusErrorReasons.License,
|
||||
message: 'test',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
name: 'test rule warning',
|
||||
tags: [],
|
||||
enabled: true,
|
||||
ruleTypeId: 'test_rule_type',
|
||||
schedule: { interval: '5d' },
|
||||
actions: [{ id: 'test', group: 'rule', params: { message: 'test' } }],
|
||||
params: { name: 'test rule type name' },
|
||||
scheduledTaskId: null,
|
||||
createdBy: null,
|
||||
updatedBy: null,
|
||||
apiKeyOwner: null,
|
||||
throttle: '1m',
|
||||
muteAll: false,
|
||||
mutedInstanceIds: [],
|
||||
executionStatus: {
|
||||
status: 'warning',
|
||||
lastDuration: 500,
|
||||
lastExecutionDate: new Date('2020-08-20T19:23:38Z'),
|
||||
warning: {
|
||||
reason: RuleExecutionStatusWarningReasons.MAX_EXECUTABLE_ACTIONS,
|
||||
message: 'test',
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
async function setup(editable: boolean = true) {
|
||||
loadRules.mockResolvedValue({
|
||||
page: 1,
|
||||
|
|
|
@ -80,6 +80,7 @@ import {
|
|||
snoozeRule,
|
||||
unsnoozeRule,
|
||||
deleteRules,
|
||||
updateAPIKey,
|
||||
} from '../../../lib/rule_api';
|
||||
import { loadActionTypes } from '../../../lib/action_connector_api';
|
||||
import { hasAllPrivilege, hasExecuteActionsCapability } from '../../../lib/capabilities';
|
||||
|
@ -103,6 +104,7 @@ import { triggersActionsUiConfig } from '../../../../common/lib/config_api';
|
|||
import { RuleTagFilter } from './rule_tag_filter';
|
||||
import { RuleStatusFilter } from './rule_status_filter';
|
||||
import { getIsExperimentalFeatureEnabled } from '../../../../common/get_experimental_features';
|
||||
import { UpdateApiKeyModalConfirmation } from '../../../components/update_api_key_modal_confirmation';
|
||||
|
||||
const ENTER_KEY = 13;
|
||||
|
||||
|
@ -219,6 +221,7 @@ export const RulesList: React.FunctionComponent = () => {
|
|||
totalItemCount: 0,
|
||||
});
|
||||
const [rulesToDelete, setRulesToDelete] = useState<string[]>([]);
|
||||
const [rulesToUpdateAPIKey, setRulesToUpdateAPIKey] = useState<string[]>([]);
|
||||
const onRuleEdit = (ruleItem: RuleTableItem) => {
|
||||
setEditFlyoutVisibility(true);
|
||||
setCurrentRuleToEdit(ruleItem);
|
||||
|
@ -903,6 +906,7 @@ export const RulesList: React.FunctionComponent = () => {
|
|||
onRuleChanged={() => loadRulesData()}
|
||||
setRulesToDelete={setRulesToDelete}
|
||||
onEditRule={() => onRuleEdit(item)}
|
||||
onUpdateAPIKey={setRulesToUpdateAPIKey}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
@ -1330,7 +1334,7 @@ export const RulesList: React.FunctionComponent = () => {
|
|||
await loadRulesData();
|
||||
}}
|
||||
onErrors={async () => {
|
||||
// Refresh the rules from the server, some rules may have beend deleted
|
||||
// Refresh the rules from the server, some rules may have been deleted
|
||||
await loadRulesData();
|
||||
setRulesToDelete([]);
|
||||
}}
|
||||
|
@ -1349,6 +1353,20 @@ export const RulesList: React.FunctionComponent = () => {
|
|||
setRulesState({ ...rulesState, isLoading });
|
||||
}}
|
||||
/>
|
||||
<UpdateApiKeyModalConfirmation
|
||||
onCancel={() => {
|
||||
setRulesToUpdateAPIKey([]);
|
||||
}}
|
||||
idsToUpdate={rulesToUpdateAPIKey}
|
||||
apiUpdateApiKeyCall={updateAPIKey}
|
||||
setIsLoadingState={(isLoading: boolean) => {
|
||||
setRulesState({ ...rulesState, isLoading });
|
||||
}}
|
||||
onUpdated={async () => {
|
||||
setRulesToUpdateAPIKey([]);
|
||||
await loadRulesData();
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer size="xs" />
|
||||
{getRulesList()}
|
||||
{ruleFlyoutVisible && (
|
||||
|
|
|
@ -13,6 +13,7 @@ import {
|
|||
KibanaResponseFactory,
|
||||
IKibanaResponse,
|
||||
Logger,
|
||||
SavedObject,
|
||||
} from '@kbn/core/server';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { InvalidatePendingApiKey } from '@kbn/alerting-plugin/server/types';
|
||||
|
@ -364,4 +365,46 @@ export function defineRoutes(
|
|||
}
|
||||
}
|
||||
);
|
||||
|
||||
router.get(
|
||||
{
|
||||
path: '/api/alerts_fixture/rule/{id}/_get_api_key',
|
||||
validate: {
|
||||
params: schema.object({
|
||||
id: schema.string(),
|
||||
}),
|
||||
},
|
||||
},
|
||||
async function (
|
||||
context: RequestHandlerContext,
|
||||
req: KibanaRequest<any, any, any, any>,
|
||||
res: KibanaResponseFactory
|
||||
): Promise<IKibanaResponse<any>> {
|
||||
const { id } = req.params;
|
||||
const [, { encryptedSavedObjects, spaces }] = await core.getStartServices();
|
||||
|
||||
const spaceId = spaces ? spaces.spacesService.getSpaceId(req) : 'default';
|
||||
|
||||
let namespace: string | undefined;
|
||||
if (spaces && spaceId) {
|
||||
namespace = spaces.spacesService.spaceIdToNamespace(spaceId);
|
||||
}
|
||||
|
||||
try {
|
||||
const {
|
||||
attributes: { apiKey, apiKeyOwner },
|
||||
}: SavedObject<RawRule> = await encryptedSavedObjects
|
||||
.getClient({
|
||||
includedHiddenTypes: ['alert'],
|
||||
})
|
||||
.getDecryptedAsInternalUser('alert', id, {
|
||||
namespace,
|
||||
});
|
||||
|
||||
return res.ok({ body: { apiKey, apiKeyOwner } });
|
||||
} catch (err) {
|
||||
return res.badRequest({ body: err });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
|
|
@ -76,6 +76,16 @@ export class AlertUtils {
|
|||
return request;
|
||||
}
|
||||
|
||||
public getAPIKeyRequest(ruleId: string) {
|
||||
const request = this.supertestWithoutAuth.get(
|
||||
`${getUrlPrefix(this.space.id)}/api/alerts_fixture/rule/${ruleId}/_get_api_key`
|
||||
);
|
||||
if (this.user) {
|
||||
return request.auth(this.user.username, this.user.password);
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
public getDisableRequest(alertId: string) {
|
||||
const request = this.supertestWithoutAuth
|
||||
.post(`${getUrlPrefix(this.space.id)}/api/alerting/rule/${alertId}/_disable`)
|
||||
|
|
|
@ -334,13 +334,6 @@ export default function createDisableAlertTests({ getService }: FtrProviderConte
|
|||
} catch (e) {
|
||||
expect(e.meta.statusCode).to.eql(404);
|
||||
}
|
||||
// Ensure AAD isn't broken
|
||||
await checkAAD({
|
||||
supertest,
|
||||
spaceId: space.id,
|
||||
type: 'alert',
|
||||
id: createdAlert.id,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
|
|
|
@ -31,6 +31,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC
|
|||
loadTestFile(require.resolve('./get_alert_summary'));
|
||||
loadTestFile(require.resolve('./rule_types'));
|
||||
loadTestFile(require.resolve('./bulk_edit'));
|
||||
loadTestFile(require.resolve('./retain_api_key'));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { UserAtSpaceScenarios } from '../../../scenarios';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
import { AlertUtils, getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common/lib';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function retainAPIKeyTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
|
||||
describe('retain api key', () => {
|
||||
const objectRemover = new ObjectRemover(supertest);
|
||||
|
||||
after(() => objectRemover.removeAll());
|
||||
|
||||
for (const scenario of UserAtSpaceScenarios) {
|
||||
const { user, space } = scenario;
|
||||
const alertUtils = new AlertUtils({ user, space, supertestWithoutAuth });
|
||||
|
||||
describe(scenario.id, () => {
|
||||
it('should retain the api key when a rule is disabled and then enabled', async () => {
|
||||
const { body: createdAction } = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/api/actions/connector`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'MY action',
|
||||
connector_type_id: 'test.noop',
|
||||
config: {},
|
||||
secrets: {},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
const { body: createdRule } = await supertest
|
||||
.post(`${getUrlPrefix(space.id)}/api/alerting/rule`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestRuleData({
|
||||
actions: [
|
||||
{
|
||||
id: createdAction.id,
|
||||
group: 'default',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
.expect(200);
|
||||
objectRemover.add(space.id, createdRule.id, 'rule', 'alerting');
|
||||
objectRemover.add(space.id, createdAction.id, 'action', 'actions');
|
||||
const {
|
||||
body: { apiKey, apiKeyOwner },
|
||||
} = await alertUtils.getAPIKeyRequest(createdRule.id);
|
||||
|
||||
expect(apiKey).not.to.be(null);
|
||||
expect(apiKey).not.to.be(undefined);
|
||||
expect(apiKeyOwner).not.to.be(null);
|
||||
expect(apiKeyOwner).not.to.be(undefined);
|
||||
|
||||
await alertUtils.getDisableRequest(createdRule.id);
|
||||
|
||||
const {
|
||||
body: { apiKey: apiKeyAfterDisable, apiKeyOwner: apiKeyOwnerAfterDisable },
|
||||
} = await alertUtils.getAPIKeyRequest(createdRule.id);
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'space_1_all at space2':
|
||||
case 'global_read at space1':
|
||||
case 'space_1_all_alerts_none_actions at space1':
|
||||
case 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
case 'space_1_all_with_restricted_fixture at space1':
|
||||
expect(apiKey).to.be(apiKeyAfterDisable);
|
||||
expect(apiKeyOwner).to.be(apiKeyOwnerAfterDisable);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
|
||||
await alertUtils.getEnableRequest(createdRule.id);
|
||||
|
||||
const {
|
||||
body: { apiKey: apiKeyAfterEnable, apiKeyOwner: apiKeyOwnerAfterEnable },
|
||||
} = await alertUtils.getAPIKeyRequest(createdRule.id);
|
||||
|
||||
switch (scenario.id) {
|
||||
case 'no_kibana_privileges at space1':
|
||||
case 'space_1_all at space2':
|
||||
case 'global_read at space1':
|
||||
case 'space_1_all_alerts_none_actions at space1':
|
||||
case 'superuser at space1':
|
||||
case 'space_1_all at space1':
|
||||
case 'space_1_all_with_restricted_fixture at space1':
|
||||
expect(apiKey).to.be(apiKeyAfterEnable);
|
||||
expect(apiKeyOwner).to.be(apiKeyOwnerAfterEnable);
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue