[ResponseOps] Allow users authenticated with an API keys to manage alerting rules (#154189)

Resolves https://github.com/elastic/kibana/issues/152140

## Summary
Updates the following functions in the Rules Client to re-use the API
key in context and avoid having the system invalidate them when no
longer in use:

- bulk_delete
- bulk_edit
- clone
- create
- delete
- update
- update_api_key

Also adds a new field to the rule SO to help determine when whether an
api key was created by a user or created by us.

### Checklist

- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

### To verify

- Follow these
[instructions](https://www.elastic.co/guide/en/kibana/master/api-keys.html#create-api-key)
to create an api key. Make sure to copy your api key
- Run the following 
```
curl -X POST "http://localhost:5601/api/alerting/rule/" -H 'Authorization: ApiKey ${API_KEY}' -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d'
{
  "rule_type_id": "example.pattern",
  "name": "pattern",
  "schedule": {
    "interval": "5s"
  },
  "actions": [
  ],
  "consumer": "alerts",
  "tags": [],
  "notify_when": "onActionGroupChange",
  "params": {
    "patterns": {
      "instA": " a - - a "
    }
  }
}'
```
- Verify that the request returns a rule
with`"api_key_created_by_user":true`
- Try this with the other rules clients functions listed above to verify
that you can manage alerting rules when authenticated with an api key
- Verify that `"api_key_created_by_user":false` when you remove the api
key header and add `-u ${USERNAME}:${PASSWORD}` to authenticate
This commit is contained in:
Alexi Doak 2023-04-11 10:29:56 -04:00 committed by GitHub
parent 60fe5af19c
commit 331eb60a8b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
70 changed files with 1191 additions and 101 deletions

View file

@ -57,7 +57,7 @@ describe('checking migration metadata changes on all registered SO types', () =>
Object {
"action": "6cfc277ed3211639e37546ac625f4a68f2494215",
"action_task_params": "5f419caba96dd8c77d0f94013e71d43890e3d5d6",
"alert": "1e4cd6941f1eb39c729c646e91fbfb9700de84b9",
"alert": "7bec97d7775a025ecf36a33baf17386b9e7b4c3c",
"api_key_pending_invalidation": "16e7bcf8e78764102d7f525542d5b616809a21ee",
"apm-indices": "d19dd7fb51f2d2cbc1f8769481721e0953f9a6d2",
"apm-server-schema": "1d42f17eff9ec6c16d3a9324d9539e2d123d0a9a",

View file

@ -168,6 +168,7 @@ export interface Rule<Params extends RuleTypeParams = never> {
updatedAt: Date;
apiKey: string | null;
apiKeyOwner: string | null;
apiKeyCreatedByUser?: boolean | null;
throttle?: string | null;
muteAll: boolean;
notifyWhen?: RuleNotifyWhenType | null;

View file

@ -109,6 +109,7 @@ export function transformRule(input: ApiRule): Rule {
updated_at: updatedAt,
api_key: apiKey,
api_key_owner: apiKeyOwner,
api_key_created_by_user: apiKeyCreatedByUser,
notify_when: notifyWhen,
mute_all: muteAll,
muted_alert_ids: mutedInstanceIds,
@ -140,6 +141,8 @@ export function transformRule(input: ApiRule): Rule {
...(nextRun ? { nextRun: new Date(nextRun) } : {}),
...(monitoring ? { monitoring: transformMonitoring(monitoring) } : {}),
...(lastRun ? { lastRun: transformLastRun(lastRun) } : {}),
...(apiKeyCreatedByUser !== undefined ? { apiKeyCreatedByUser } : {}),
...rest,
};
}

View file

@ -36,6 +36,7 @@ const rewriteBodyRes: RewriteResponseCase<PartialRule<RuleTypeParams>> = ({
createdAt,
updatedAt,
apiKeyOwner,
apiKeyCreatedByUser,
notifyWhen,
muteAll,
mutedInstanceIds,
@ -75,6 +76,7 @@ const rewriteBodyRes: RewriteResponseCase<PartialRule<RuleTypeParams>> = ({
: {}),
...(lastRun ? { last_run: rewriteRuleLastRun(lastRun) } : {}),
...(nextRun ? { next_run: nextRun } : {}),
...(apiKeyCreatedByUser !== undefined ? { api_key_created_by_user: apiKeyCreatedByUser } : {}),
});
export const cloneRuleRoute = (

View file

@ -74,6 +74,7 @@ const rewriteBodyRes: RewriteResponseCase<SanitizedRule<RuleTypeParams>> = ({
createdAt,
updatedAt,
apiKeyOwner,
apiKeyCreatedByUser,
notifyWhen,
muteAll,
mutedInstanceIds,
@ -103,6 +104,7 @@ const rewriteBodyRes: RewriteResponseCase<SanitizedRule<RuleTypeParams>> = ({
actions: rewriteActionsRes(actions),
...(lastRun ? { last_run: rewriteRuleLastRun(lastRun) } : {}),
...(nextRun ? { next_run: nextRun } : {}),
...(apiKeyCreatedByUser !== undefined ? { api_key_created_by_user: apiKeyCreatedByUser } : {}),
});
export const createRuleRoute = ({ router, licenseState, usageCounter }: RouteOptions) => {

View file

@ -29,6 +29,7 @@ const rewriteBodyRes: RewriteResponseCase<SanitizedRule<RuleTypeParams>> = ({
createdAt,
updatedAt,
apiKeyOwner,
apiKeyCreatedByUser,
notifyWhen,
muteAll,
mutedInstanceIds,
@ -78,6 +79,7 @@ const rewriteBodyRes: RewriteResponseCase<SanitizedRule<RuleTypeParams>> = ({
...(lastRun ? { last_run: rewriteRuleLastRun(lastRun) } : {}),
...(nextRun ? { next_run: nextRun } : {}),
...(viewInAppRelativeUrl ? { view_in_app_relative_url: viewInAppRelativeUrl } : {}),
...(apiKeyCreatedByUser !== undefined ? { api_key_created_by_user: apiKeyCreatedByUser } : {}),
});
interface BuildGetRulesRouteParams {

View file

@ -25,6 +25,7 @@ export const rewriteRule = ({
createdAt,
updatedAt,
apiKeyOwner,
apiKeyCreatedByUser,
notifyWhen,
muteAll,
mutedInstanceIds,
@ -76,4 +77,5 @@ export const rewriteRule = ({
})),
...(lastRun ? { last_run: rewriteRuleLastRun(lastRun) } : {}),
...(nextRun ? { next_run: nextRun } : {}),
...(apiKeyCreatedByUser !== undefined ? { api_key_created_by_user: apiKeyCreatedByUser } : {}),
});

View file

@ -33,6 +33,7 @@ const rewriteBodyRes: RewriteResponseCase<ResolvedSanitizedRule<RuleTypeParams>>
createdAt,
updatedAt,
apiKeyOwner,
apiKeyCreatedByUser,
notifyWhen,
muteAll,
mutedInstanceIds,
@ -62,6 +63,7 @@ const rewriteBodyRes: RewriteResponseCase<ResolvedSanitizedRule<RuleTypeParams>>
actions: rewriteActionsRes(actions),
...(lastRun ? { last_run: rewriteRuleLastRun(lastRun) } : {}),
...(nextRun ? { next_run: nextRun } : {}),
...(apiKeyCreatedByUser !== undefined ? { api_key_created_by_user: apiKeyCreatedByUser } : {}),
});
export const resolveRuleRoute = (

View file

@ -74,6 +74,7 @@ const rewriteBodyRes: RewriteResponseCase<PartialRule<RuleTypeParams>> = ({
createdAt,
updatedAt,
apiKeyOwner,
apiKeyCreatedByUser,
notifyWhen,
muteAll,
mutedInstanceIds,
@ -113,6 +114,7 @@ const rewriteBodyRes: RewriteResponseCase<PartialRule<RuleTypeParams>> = ({
: {}),
...(lastRun ? { last_run: rewriteRuleLastRun(lastRun) } : {}),
...(nextRun ? { next_run: nextRun } : {}),
...(apiKeyCreatedByUser !== undefined ? { api_key_created_by_user: apiKeyCreatedByUser } : {}),
});
export const updateRuleRoute = (

View file

@ -0,0 +1,68 @@
/*
* 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 { apiKeyAsAlertAttributes } from './api_key_as_alert_attributes';
describe('apiKeyAsAlertAttributes', () => {
test('return attributes', () => {
expect(
apiKeyAsAlertAttributes(
{
apiKeysEnabled: true,
result: {
id: '123',
name: '123',
api_key: 'abc',
},
},
'test',
false
)
).toEqual({
apiKey: 'MTIzOmFiYw==',
apiKeyOwner: 'test',
apiKeyCreatedByUser: false,
});
});
test('returns null attributes when api keys are not enabled', () => {
expect(
apiKeyAsAlertAttributes(
{
apiKeysEnabled: false,
},
'test',
false
)
).toEqual({
apiKey: null,
apiKeyOwner: null,
apiKeyCreatedByUser: null,
});
});
test('returns apiKeyCreatedByUser as true when createdByUser is passed in', () => {
expect(
apiKeyAsAlertAttributes(
{
apiKeysEnabled: true,
result: {
id: '123',
name: '123',
api_key: 'abc',
},
},
'test',
true
)
).toEqual({
apiKey: 'MTIzOmFiYw==',
apiKeyOwner: 'test',
apiKeyCreatedByUser: true,
});
});
});

View file

@ -10,15 +10,18 @@ import { CreateAPIKeyResult } from '../types';
export function apiKeyAsAlertAttributes(
apiKey: CreateAPIKeyResult | null,
username: string | null
): Pick<RawRule, 'apiKey' | 'apiKeyOwner'> {
username: string | null,
createdByUser: boolean
): Pick<RawRule, 'apiKey' | 'apiKeyOwner' | 'apiKeyCreatedByUser'> {
return apiKey && apiKey.apiKeysEnabled
? {
apiKeyOwner: username,
apiKey: Buffer.from(`${apiKey.result.id}:${apiKey.result.api_key}`).toString('base64'),
apiKeyCreatedByUser: createdByUser,
}
: {
apiKeyOwner: null,
apiKey: null,
apiKeyCreatedByUser: null,
};
}

View file

@ -0,0 +1,169 @@
/*
* 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 { savedObjectsClientMock, loggingSystemMock } from '@kbn/core/server/mocks';
import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks';
import { ruleTypeRegistryMock } from '../../rule_type_registry.mock';
import { alertingAuthorizationMock } from '../../authorization/alerting_authorization.mock';
import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
import { actionsAuthorizationMock } from '@kbn/actions-plugin/server/mocks';
import { AlertingAuthorization } from '../../authorization/alerting_authorization';
import { ActionsAuthorization } from '@kbn/actions-plugin/server';
import { RawRule } from '../../types';
import { getBeforeSetup, mockedDateString } from '../tests/lib';
import { createNewAPIKeySet } from './create_new_api_key_set';
import { RulesClientContext } from '../types';
const taskManager = taskManagerMock.createStart();
const ruleTypeRegistry = ruleTypeRegistryMock.create();
const unsecuredSavedObjectsClient = savedObjectsClientMock.create();
const encryptedSavedObjects = encryptedSavedObjectsMock.createClient();
const authorization = alertingAuthorizationMock.create();
const actionsAuthorization = actionsAuthorizationMock.create();
const kibanaVersion = 'v8.0.0';
const rulesClientParams: jest.Mocked<RulesClientContext> = {
taskManager,
ruleTypeRegistry,
unsecuredSavedObjectsClient,
authorization: authorization as unknown as AlertingAuthorization,
actionsAuthorization: actionsAuthorization as unknown as ActionsAuthorization,
spaceId: 'default',
getUserName: jest.fn(),
createAPIKey: jest.fn(),
logger: loggingSystemMock.create().get(),
encryptedSavedObjectsClient: encryptedSavedObjects,
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
minimumScheduleInterval: { value: '1m', enforce: false },
minimumScheduleIntervalInMs: 1,
fieldsToExcludeFromPublicApi: [],
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
};
const username = 'test';
const attributes: RawRule = {
enabled: true,
name: 'rule-name',
tags: ['tag-1', 'tag-2'],
alertTypeId: '123',
consumer: 'rule-consumer',
legacyId: null,
schedule: { interval: '1s' },
actions: [],
params: {},
createdBy: null,
updatedBy: null,
createdAt: mockedDateString,
updatedAt: mockedDateString,
apiKey: null,
apiKeyOwner: null,
throttle: null,
notifyWhen: null,
muteAll: false,
mutedInstanceIds: [],
executionStatus: {
status: 'unknown',
lastExecutionDate: '2020-08-20T19:23:38Z',
error: null,
warning: null,
},
revision: 0,
};
describe('createNewAPIKeySet', () => {
beforeEach(() => {
getBeforeSetup(rulesClientParams, taskManager, ruleTypeRegistry);
});
test('create new api keys', async () => {
rulesClientParams.createAPIKey.mockResolvedValueOnce({
apiKeysEnabled: true,
result: { id: '123', name: '123', api_key: 'abc' },
});
const apiKey = await createNewAPIKeySet(rulesClientParams, {
id: attributes.alertTypeId,
ruleName: attributes.name,
username,
shouldUpdateApiKey: true,
});
expect(apiKey).toEqual({
apiKey: 'MTIzOmFiYw==',
apiKeyCreatedByUser: undefined,
apiKeyOwner: 'test',
});
expect(rulesClientParams.createAPIKey).toHaveBeenCalledTimes(1);
});
test('should get api key from the request if the user is authenticated using api keys', async () => {
rulesClientParams.getAuthenticationAPIKey.mockReturnValueOnce({
apiKeysEnabled: true,
result: { id: '123', name: '123', api_key: 'abc' },
});
rulesClientParams.isAuthenticationTypeAPIKey.mockReturnValueOnce(true);
const apiKey = await createNewAPIKeySet(rulesClientParams, {
id: attributes.alertTypeId,
ruleName: attributes.name,
username,
shouldUpdateApiKey: true,
});
expect(apiKey).toEqual({
apiKey: 'MTIzOmFiYw==',
apiKeyCreatedByUser: true,
apiKeyOwner: 'test',
});
expect(rulesClientParams.getAuthenticationAPIKey).toHaveBeenCalledTimes(1);
expect(rulesClientParams.isAuthenticationTypeAPIKey).toHaveBeenCalledTimes(1);
});
test('should throw an error if getting the api key fails', async () => {
rulesClientParams.createAPIKey.mockRejectedValueOnce(new Error('Test failure'));
await expect(
async () =>
await createNewAPIKeySet(rulesClientParams, {
id: attributes.alertTypeId,
ruleName: attributes.name,
username,
shouldUpdateApiKey: true,
})
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Error creating API key for rule - Test failure"`
);
});
test('should throw an error if getting the api key fails and an error message is passed in', async () => {
rulesClientParams.createAPIKey.mockRejectedValueOnce(new Error('Test failure'));
await expect(
async () =>
await createNewAPIKeySet(rulesClientParams, {
id: attributes.alertTypeId,
ruleName: attributes.name,
username,
shouldUpdateApiKey: true,
errorMessage: 'Error updating rule: could not create API key',
})
).rejects.toThrowErrorMatchingInlineSnapshot(
`"Error updating rule: could not create API key - Test failure"`
);
});
test('should return null if shouldUpdateApiKey: false', async () => {
const apiKey = await createNewAPIKeySet(rulesClientParams, {
id: attributes.alertTypeId,
ruleName: attributes.name,
username,
shouldUpdateApiKey: false,
});
expect(apiKey).toEqual({
apiKey: null,
apiKeyCreatedByUser: null,
apiKeyOwner: null,
});
});
});

View file

@ -13,21 +13,33 @@ import { RulesClientContext } from '../types';
export async function createNewAPIKeySet(
context: RulesClientContext,
{
attributes,
id,
ruleName,
username,
shouldUpdateApiKey,
errorMessage,
}: {
attributes: RawRule;
id: string;
ruleName: string;
username: string | null;
shouldUpdateApiKey: boolean;
errorMessage?: string;
}
): Promise<Pick<RawRule, 'apiKey' | 'apiKeyOwner'>> {
): Promise<Pick<RawRule, 'apiKey' | 'apiKeyOwner' | 'apiKeyCreatedByUser'>> {
let createdAPIKey = null;
let isAuthTypeApiKey = false;
try {
createdAPIKey = await context.createAPIKey(
generateAPIKeyName(attributes.alertTypeId, attributes.name)
);
isAuthTypeApiKey = context.isAuthenticationTypeAPIKey();
const name = generateAPIKeyName(id, ruleName);
createdAPIKey = shouldUpdateApiKey
? isAuthTypeApiKey
? context.getAuthenticationAPIKey(`${name}-user-created`)
: await context.createAPIKey(name)
: null;
} catch (error) {
throw Boom.badRequest(`Error creating API key for rule: ${error.message}`);
const message = errorMessage ? errorMessage : 'Error creating API key for rule';
throw Boom.badRequest(`${message} - ${error.message}`);
}
return apiKeyAsAlertAttributes(createdAPIKey, username);
return apiKeyAsAlertAttributes(createdAPIKey, username, isAuthTypeApiKey);
}

View file

@ -54,7 +54,7 @@ export async function createRuleSavedObject<Params extends RuleTypeParams = neve
} catch (e) {
// Avoid unused API key
await bulkMarkApiKeysForInvalidation(
{ apiKeys: rawRule.apiKey ? [rawRule.apiKey] : [] },
{ apiKeys: rawRule.apiKey && !rawRule.apiKeyCreatedByUser ? [rawRule.apiKey] : [] },
context.logger,
context.unsecuredSavedObjectsClient
);

View file

@ -110,7 +110,7 @@ const bulkDeleteWithOCC = async (
async () => {
for await (const response of rulesFinder.find()) {
for (const rule of response.saved_objects) {
if (rule.attributes.apiKey) {
if (rule.attributes.apiKey && !rule.attributes.apiKeyCreatedByUser) {
apiKeyToRuleIdMapping[rule.id] = rule.attributes.apiKey;
}
if (rule.attributes.name) {

View file

@ -41,8 +41,6 @@ import {
applyBulkEditOperation,
buildKueryNodeFilter,
injectReferencesIntoActions,
generateAPIKeyName,
apiKeyAsAlertAttributes,
getBulkSnoozeAttributes,
getBulkUnsnoozeAttributes,
verifySnoozeScheduleLimit,
@ -61,13 +59,13 @@ import {
validateActions,
updateMeta,
addGeneratedActionValues,
createNewAPIKeySet,
} from '../lib';
import {
NormalizedAlertAction,
BulkOperationError,
RuleBulkOperationAggregation,
RulesClientContext,
CreateAPIKeyResult,
NormalizedAlertActionWithGeneratedValues,
} from '../types';
@ -121,9 +119,17 @@ export type BulkEditOperation =
value?: undefined;
};
type ApiKeysMap = Map<string, { oldApiKey?: string; newApiKey?: string }>;
type ApiKeysMap = Map<
string,
{
oldApiKey?: string;
newApiKey?: string;
oldApiKeyCreatedByUser?: boolean | null;
newApiKeyCreatedByUser?: boolean | null;
}
>;
type ApiKeyAttributes = Pick<RawRule, 'apiKey' | 'apiKeyOwner'>;
type ApiKeyAttributes = Pick<RawRule, 'apiKey' | 'apiKeyOwner' | 'apiKeyCreatedByUser'>;
type RuleType = ReturnType<RuleTypeRegistry['get']>;
@ -433,7 +439,10 @@ async function updateRuleAttributesAndParamsInMemory<Params extends RuleTypePara
}): Promise<void> {
try {
if (rule.attributes.apiKey) {
apiKeysMap.set(rule.id, { oldApiKey: rule.attributes.apiKey });
apiKeysMap.set(rule.id, {
oldApiKey: rule.attributes.apiKey,
oldApiKeyCreatedByUser: rule.attributes.apiKeyCreatedByUser,
});
}
const ruleType = context.ruleTypeRegistry.get(rule.attributes.alertTypeId);
@ -734,23 +743,20 @@ async function prepareApiKeys(
hasUpdateApiKeyOperation: boolean,
username: string | null
): Promise<{ apiKeyAttributes: ApiKeyAttributes }> {
const shouldUpdateApiKey = attributes.enabled || hasUpdateApiKeyOperation;
const apiKeyAttributes = await createNewAPIKeySet(context, {
id: ruleType.id,
ruleName: attributes.name,
username,
shouldUpdateApiKey: attributes.enabled || hasUpdateApiKeyOperation,
errorMessage: 'Error updating rule: could not create API key',
});
let createdAPIKey: CreateAPIKeyResult | null = null;
try {
createdAPIKey = shouldUpdateApiKey
? await context.createAPIKey(generateAPIKeyName(ruleType.id, attributes.name))
: null;
} catch (error) {
throw Error(`Error updating rule: could not create API key - ${error.message}`);
}
const apiKeyAttributes = apiKeyAsAlertAttributes(createdAPIKey, username);
// collect generated API keys
if (apiKeyAttributes.apiKey) {
apiKeysMap.set(rule.id, {
...apiKeysMap.get(rule.id),
newApiKey: apiKeyAttributes.apiKey,
newApiKeyCreatedByUser: apiKeyAttributes.apiKeyCreatedByUser,
});
}
@ -812,7 +818,7 @@ async function saveBulkUpdatedRules(
await bulkMarkApiKeysForInvalidation(
{
apiKeys: Array.from(apiKeysMap.values())
.filter((value) => value.newApiKey)
.filter((value) => value.newApiKey && !value.newApiKeyCreatedByUser)
.map((value) => value.newApiKey as string),
},
context.logger,
@ -824,13 +830,15 @@ async function saveBulkUpdatedRules(
result.saved_objects.map(({ id, error }) => {
const oldApiKey = apiKeysMap.get(id)?.oldApiKey;
const oldApiKeyCreatedByUser = apiKeysMap.get(id)?.oldApiKeyCreatedByUser;
const newApiKey = apiKeysMap.get(id)?.newApiKey;
const newApiKeyCreatedByUser = apiKeysMap.get(id)?.newApiKeyCreatedByUser;
// if SO wasn't saved and has new API key it will be invalidated
if (error && newApiKey) {
if (error && newApiKey && !newApiKeyCreatedByUser) {
apiKeysToInvalidate.push(newApiKey);
// if SO saved and has old Api Key it will be invalidate
} else if (!error && oldApiKey) {
} else if (!error && oldApiKey && !oldApiKeyCreatedByUser) {
apiKeysToInvalidate.push(oldApiKey);
}
});

View file

@ -146,7 +146,12 @@ const bulkEnableRulesWithOCC = async (
const updatedAttributes = updateMeta(context, {
...rule.attributes,
...(!rule.attributes.apiKey &&
(await createNewAPIKeySet(context, { attributes: rule.attributes, username }))),
(await createNewAPIKeySet(context, {
id: rule.attributes.alertTypeId,
ruleName: rule.attributes.name,
username,
shouldUpdateApiKey: true,
}))),
enabled: true,
updatedBy: username,
updatedAt: new Date().toISOString(),

View file

@ -17,8 +17,7 @@ import { parseDuration } from '../../../common/parse_duration';
import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
import { getRuleExecutionStatusPending } from '../../lib/rule_execution_status';
import { isDetectionEngineAADRuleType } from '../../saved_objects/migrations/utils';
import { generateAPIKeyName, apiKeyAsAlertAttributes } from '../common';
import { createRuleSavedObject } from '../lib';
import { createNewAPIKeySet, createRuleSavedObject } from '../lib';
import { RulesClientContext } from '../types';
export type CloneArguments = [string, { newId?: string }];
@ -95,18 +94,18 @@ export async function clone<Params extends RuleTypeParams = never>(
const createTime = Date.now();
const lastRunTimestamp = new Date();
const legacyId = Semver.lt(context.kibanaVersion, '8.0.0') ? id : null;
let createdAPIKey = null;
try {
createdAPIKey = ruleSavedObject.attributes.enabled
? await context.createAPIKey(generateAPIKeyName(ruleType.id, ruleName))
: null;
} catch (error) {
throw Boom.badRequest(`Error creating rule: could not create API key - ${error.message}`);
}
const apiKeyAttributes = await createNewAPIKeySet(context, {
id: ruleType.id,
ruleName,
username,
shouldUpdateApiKey: ruleSavedObject.attributes.enabled,
errorMessage: 'Error creating rule: could not create API key',
});
const rawRule: RawRule = {
...ruleSavedObject.attributes,
name: ruleName,
...apiKeyAsAlertAttributes(createdAPIKey, username),
...apiKeyAttributes,
legacyId,
createdBy: username,
updatedBy: username,

View file

@ -40,6 +40,7 @@ export interface CreateOptions<Params extends RuleTypeParams> {
| 'updatedAt'
| 'apiKey'
| 'apiKeyOwner'
| 'apiKeyCreatedByUser'
| 'muteAll'
| 'mutedInstanceIds'
| 'actions'
@ -91,11 +92,20 @@ export async function create<Params extends RuleTypeParams = never>(
const username = await context.getUserName();
let createdAPIKey = null;
let isAuthTypeApiKey = false;
try {
isAuthTypeApiKey = context.isAuthenticationTypeAPIKey();
const name = generateAPIKeyName(ruleType.id, data.name);
createdAPIKey = data.enabled
? await withSpan({ name: 'createAPIKey', type: 'rules' }, () =>
context.createAPIKey(generateAPIKeyName(ruleType.id, data.name))
)
? isAuthTypeApiKey
? context.getAuthenticationAPIKey(`${name}-user-created`)
: await withSpan(
{
name: 'createAPIKey',
type: 'rules',
},
() => context.createAPIKey(name)
)
: null;
} catch (error) {
throw Boom.badRequest(`Error creating rule: could not create API key - ${error.message}`);
@ -144,7 +154,7 @@ export async function create<Params extends RuleTypeParams = never>(
const rawRule: RawRule = {
...data,
...apiKeyAsAlertAttributes(createdAPIKey, username),
...apiKeyAsAlertAttributes(createdAPIKey, username, isAuthTypeApiKey),
legacyId,
actions,
createdBy: username,

View file

@ -23,6 +23,7 @@ export async function deleteRule(context: RulesClientContext, { id }: { id: stri
async function deleteWithOCC(context: RulesClientContext, { id }: { id: string }) {
let taskIdToRemove: string | undefined | null;
let apiKeyToInvalidate: string | null = null;
let apiKeyCreatedByUser: boolean | undefined | null = false;
let attributes: RawRule;
try {
@ -31,6 +32,7 @@ async function deleteWithOCC(context: RulesClientContext, { id }: { id: string }
namespace: context.namespace,
});
apiKeyToInvalidate = decryptedAlert.attributes.apiKey;
apiKeyCreatedByUser = decryptedAlert.attributes.apiKeyCreatedByUser;
taskIdToRemove = decryptedAlert.attributes.scheduledTaskId;
attributes = decryptedAlert.attributes;
} catch (e) {
@ -73,7 +75,7 @@ async function deleteWithOCC(context: RulesClientContext, { id }: { id: string }
await Promise.all([
taskIdToRemove ? context.taskManager.removeIfExists(taskIdToRemove) : null,
apiKeyToInvalidate
apiKeyToInvalidate && !apiKeyCreatedByUser
? bulkMarkApiKeysForInvalidation(
{ apiKeys: [apiKeyToInvalidate] },
context.logger,

View file

@ -83,7 +83,13 @@ async function enableWithOCC(context: RulesClientContext, { id }: { id: string }
const updateAttributes = updateMeta(context, {
...attributes,
...(!existingApiKey && (await createNewAPIKeySet(context, { attributes, username }))),
...(!existingApiKey &&
(await createNewAPIKeySet(context, {
id: attributes.alertTypeId,
ruleName: attributes.name,
username,
shouldUpdateApiKey: true,
}))),
...(attributes.monitoring && {
monitoring: resetMonitoringLastRun(attributes.monitoring),
}),

View file

@ -32,8 +32,8 @@ import {
getPartialRuleFromRaw,
addGeneratedActionValues,
incrementRevision,
createNewAPIKeySet,
} from '../lib';
import { generateAPIKeyName, apiKeyAsAlertAttributes } from '../common';
export interface UpdateOptions<Params extends RuleTypeParams> {
id: string;
@ -122,7 +122,7 @@ async function updateWithOCC<Params extends RuleTypeParams>(
);
await Promise.all([
alertSavedObject.attributes.apiKey
alertSavedObject.attributes.apiKey && !alertSavedObject.attributes.apiKeyCreatedByUser
? bulkMarkApiKeysForInvalidation(
{ apiKeys: [alertSavedObject.attributes.apiKey] },
context.logger,
@ -206,16 +206,13 @@ async function updateAlert<Params extends RuleTypeParams>(
const username = await context.getUserName();
let createdAPIKey = null;
try {
createdAPIKey = attributes.enabled
? await context.createAPIKey(generateAPIKeyName(ruleType.id, data.name))
: null;
} catch (error) {
throw Boom.badRequest(`Error updating rule: could not create API key - ${error.message}`);
}
const apiKeyAttributes = apiKeyAsAlertAttributes(createdAPIKey, username);
const apiKeyAttributes = await createNewAPIKeySet(context, {
id: ruleType.id,
ruleName: data.name,
username,
shouldUpdateApiKey: attributes.enabled,
errorMessage: 'Error updating rule: could not create API key',
});
const notifyWhen = getRuleNotifyWhenType(data.notifyWhen ?? null, data.throttle ?? null);
// Increment revision if applicable field has changed
@ -260,7 +257,12 @@ async function updateAlert<Params extends RuleTypeParams>(
} catch (e) {
// Avoid unused API key
await bulkMarkApiKeysForInvalidation(
{ apiKeys: createAttributes.apiKey ? [createAttributes.apiKey] : [] },
{
apiKeys:
createAttributes.apiKey && !createAttributes.apiKeyCreatedByUser
? [createAttributes.apiKey]
: [],
},
context.logger,
context.unsecuredSavedObjectsClient
);

View file

@ -5,14 +5,12 @@
* 2.0.
*/
import Boom from '@hapi/boom';
import { RawRule } from '../../types';
import { WriteOperations, AlertingAuthorizationEntity } from '../../authorization';
import { retryIfConflicts } from '../../lib/retry_if_conflicts';
import { bulkMarkApiKeysForInvalidation } from '../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation';
import { ruleAuditEvent, RuleAuditAction } from '../common/audit_events';
import { generateAPIKeyName, apiKeyAsAlertAttributes } from '../common';
import { updateMeta } from '../lib';
import { createNewAPIKeySet, updateMeta } from '../lib';
import { RulesClientContext } from '../types';
export async function updateApiKey(
@ -27,7 +25,8 @@ export async function updateApiKey(
}
async function updateApiKeyWithOCC(context: RulesClientContext, { id }: { id: string }) {
let apiKeyToInvalidate: string | null = null;
let oldApiKeyToInvalidate: string | null = null;
let oldApiKeyCreatedByUser: boolean | undefined | null = false;
let attributes: RawRule;
let version: string | undefined;
@ -36,7 +35,8 @@ async function updateApiKeyWithOCC(context: RulesClientContext, { id }: { id: st
await context.encryptedSavedObjectsClient.getDecryptedAsInternalUser<RawRule>('alert', id, {
namespace: context.namespace,
});
apiKeyToInvalidate = decryptedAlert.attributes.apiKey;
oldApiKeyToInvalidate = decryptedAlert.attributes.apiKey;
oldApiKeyCreatedByUser = decryptedAlert.attributes.apiKeyCreatedByUser;
attributes = decryptedAlert.attributes;
version = decryptedAlert.version;
} catch (e) {
@ -73,20 +73,17 @@ async function updateApiKeyWithOCC(context: RulesClientContext, { id }: { id: st
const username = await context.getUserName();
let createdAPIKey = null;
try {
createdAPIKey = await context.createAPIKey(
generateAPIKeyName(attributes.alertTypeId, attributes.name)
);
} catch (error) {
throw Boom.badRequest(
`Error updating API key for rule: could not create API key - ${error.message}`
);
}
const apiKeyAttributes = await createNewAPIKeySet(context, {
id: attributes.alertTypeId,
ruleName: attributes.name,
username,
shouldUpdateApiKey: true,
errorMessage: 'Error updating API key for rule: could not create API key',
});
const updateAttributes = updateMeta(context, {
...attributes,
...apiKeyAsAlertAttributes(createdAPIKey, username),
...apiKeyAttributes,
updatedAt: new Date().toISOString(),
updatedBy: username,
});
@ -106,16 +103,21 @@ async function updateApiKeyWithOCC(context: RulesClientContext, { id }: { id: st
} catch (e) {
// Avoid unused API key
await bulkMarkApiKeysForInvalidation(
{ apiKeys: updateAttributes.apiKey ? [updateAttributes.apiKey] : [] },
{
apiKeys:
updateAttributes.apiKey && !updateAttributes.apiKeyCreatedByUser
? [updateAttributes.apiKey]
: [],
},
context.logger,
context.unsecuredSavedObjectsClient
);
throw e;
}
if (apiKeyToInvalidate) {
if (oldApiKeyToInvalidate && !oldApiKeyCreatedByUser) {
await bulkMarkApiKeysForInvalidation(
{ apiKeys: [apiKeyToInvalidate] },
{ apiKeys: [oldApiKeyToInvalidate] },
context.logger,
context.unsecuredSavedObjectsClient
);

View file

@ -71,6 +71,7 @@ export const fieldsToExcludeFromRevisionUpdates: ReadonlySet<keyof RuleTypeParam
'alertTypeId',
'apiKey',
'apiKeyOwner',
'apiKeyCreatedByUser',
'consumer',
'createdAt',
'createdBy',

View file

@ -50,6 +50,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
};
beforeEach(() => {

View file

@ -19,7 +19,13 @@ 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';
import { loggerMock } from '@kbn/logging-mocks';
import { enabledRule1, enabledRule2, returnedRule1, returnedRule2 } from './test_helpers';
import {
defaultRule,
enabledRule1,
enabledRule2,
returnedRule1,
returnedRule2,
} from './test_helpers';
jest.mock('../../invalidate_pending_api_keys/bulk_mark_api_keys_for_invalidation', () => ({
bulkMarkApiKeysForInvalidation: jest.fn(),
@ -53,6 +59,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
kibanaVersion,
auditLogger,
minimumScheduleInterval: { value: '1m', enforce: false },
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
};
const getBulkOperationStatusErrorResponse = (statusCode: number) => ({
@ -66,6 +74,36 @@ const getBulkOperationStatusErrorResponse = (statusCode: number) => ({
},
});
export const enabledRule3 = {
...defaultRule,
id: 'id3',
attributes: {
...defaultRule.attributes,
enabled: true,
scheduledTaskId: 'id3',
apiKey: Buffer.from('789:ghi').toString('base64'),
apiKeyCreatedByUser: true,
},
};
export const returnedRule3 = {
actions: [],
alertTypeId: 'fakeType',
apiKey: 'Nzg5OmdoaQ==',
apiKeyCreatedByUser: true,
consumer: 'fakeConsumer',
enabled: true,
id: 'id3',
name: 'fakeName',
notifyWhen: undefined,
params: undefined,
schedule: {
interval: '5m',
},
scheduledTaskId: 'id3',
snoozeSchedule: [],
};
beforeEach(() => {
getBeforeSetup(rulesClientParams, taskManager, ruleTypeRegistry);
jest.clearAllMocks();
@ -77,7 +115,7 @@ describe('bulkDelete', () => {
let rulesClient: RulesClient;
const mockCreatePointInTimeFinderAsInternalUser = (
response = { saved_objects: [enabledRule1, enabledRule2] }
response = { saved_objects: [enabledRule1, enabledRule2, enabledRule3] }
) => {
encryptedSavedObjects.createPointInTimeFinderDecryptedAsInternalUser = jest
.fn()
@ -126,11 +164,12 @@ describe('bulkDelete', () => {
});
});
test('should try to delete rules, one successful and one with 500 error', async () => {
test('should try to delete rules, two successful and one with 500 error', async () => {
unsecuredSavedObjectsClient.bulkDelete.mockResolvedValue({
statuses: [
{ id: 'id1', type: 'alert', success: true },
getBulkOperationStatusErrorResponse(500),
{ id: 'id3', type: 'alert', success: true },
],
});
@ -140,9 +179,10 @@ describe('bulkDelete', () => {
expect(unsecuredSavedObjectsClient.bulkDelete).toHaveBeenCalledWith([
enabledRule1,
enabledRule2,
enabledRule3,
]);
expect(taskManager.bulkRemove).toHaveBeenCalledTimes(1);
expect(taskManager.bulkRemove).toHaveBeenCalledWith(['id1']);
expect(taskManager.bulkRemove).toHaveBeenCalledWith(['id1', 'id3']);
expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledTimes(1);
expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledWith(
{ apiKeys: ['MTIzOmFiYw=='] },
@ -150,7 +190,7 @@ describe('bulkDelete', () => {
expect.anything()
);
expect(result).toStrictEqual({
rules: [returnedRule1],
rules: [returnedRule1, returnedRule3],
errors: [{ message: 'UPS', rule: { id: 'id2', name: 'fakeName' }, status: 500 }],
total: 2,
taskIdsFailedToBeDeleted: [],
@ -313,6 +353,25 @@ describe('bulkDelete', () => {
);
});
test('should not mark API keys for invalidation if the user is authenticated using an api key', async () => {
unsecuredSavedObjectsClient.bulkDelete.mockResolvedValue({
statuses: [
{ id: 'id3', type: 'alert', success: true },
{ id: 'id1', type: 'alert', success: true },
{ id: 'id2', type: 'alert', success: true },
],
});
await rulesClient.bulkDeleteRules({ filter: 'fake_filter' });
expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledTimes(1);
expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledWith(
{ apiKeys: ['MTIzOmFiYw==', 'MzIxOmFiYw=='] },
expect.anything(),
expect.anything()
);
});
describe('taskManager', () => {
test('should return task id if deleting task failed', async () => {
unsecuredSavedObjectsClient.bulkDelete.mockResolvedValue({

View file

@ -73,6 +73,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
auditLogger,
eventLogger,
minimumScheduleInterval: { value: '1m', enforce: false },
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
};
beforeEach(() => {

View file

@ -47,6 +47,9 @@ const auditLogger = auditLoggerMock.create();
const kibanaVersion = 'v8.2.0';
const createAPIKeyMock = jest.fn();
const isAuthenticationTypeApiKeyMock = jest.fn();
const getAuthenticationApiKeyMock = jest.fn();
const rulesClientParams: jest.Mocked<ConstructorOptions> = {
taskManager,
ruleTypeRegistry,
@ -64,6 +67,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
kibanaVersion,
auditLogger,
minimumScheduleInterval: { value: '1m', enforce: false },
isAuthenticationTypeAPIKey: isAuthenticationTypeApiKeyMock,
getAuthenticationAPIKey: getAuthenticationApiKeyMock,
};
const paramsModifier = jest.fn();
@ -104,6 +109,7 @@ describe('bulkEdit()', () => {
attributes: {
...existingRule.attributes,
apiKey: MOCK_API_KEY,
apiKeyCreatedByUser: false,
},
};
@ -580,6 +586,7 @@ describe('bulkEdit()', () => {
],
apiKey: null,
apiKeyOwner: null,
apiKeyCreatedByUser: null,
meta: { versionApiKeyLastmodified: 'v8.2.0' },
name: 'my rule name',
enabled: false,
@ -781,6 +788,7 @@ describe('bulkEdit()', () => {
],
apiKey: null,
apiKeyOwner: null,
apiKeyCreatedByUser: null,
meta: { versionApiKeyLastmodified: 'v8.2.0' },
name: 'my rule name',
enabled: false,
@ -2070,6 +2078,142 @@ describe('bulkEdit()', () => {
});
expect(rulesClientParams.createAPIKey).toHaveBeenCalledWith('Alerting: myType/my rule name');
});
describe('set by the user when authenticated using api keys', () => {
beforeEach(() => {
isAuthenticationTypeApiKeyMock.mockReturnValue(true);
getAuthenticationApiKeyMock.mockReturnValue({
apiKeysEnabled: true,
result: { api_key: '111' },
});
mockCreatePointInTimeFinderAsInternalUser({
saved_objects: [
{
...existingDecryptedRule,
attributes: {
...existingDecryptedRule.attributes,
enabled: true,
apiKeyCreatedByUser: true,
},
},
],
});
});
test('should not call bulkMarkApiKeysForInvalidation', async () => {
await rulesClient.bulkEdit({
filter: 'alert.attributes.tags: "APM"',
operations: [
{
field: 'tags',
operation: 'add',
value: ['test-1'],
},
],
});
expect(bulkMarkApiKeysForInvalidation).not.toHaveBeenCalled();
});
test('should call bulkMarkApiKeysForInvalidation with empty array if bulkCreate failed', async () => {
unsecuredSavedObjectsClient.bulkCreate.mockImplementation(() => {
throw new Error('Fail');
});
await expect(
rulesClient.bulkEdit({
filter: 'alert.attributes.tags: "APM"',
operations: [
{
field: 'tags',
operation: 'add',
value: ['test-1'],
},
],
})
).rejects.toThrow('Fail');
expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledTimes(1);
expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledWith(
{ apiKeys: [] },
expect.any(Object),
expect.any(Object)
);
});
test('should call bulkMarkApiKeysForInvalidation with empty array if SO update failed', async () => {
unsecuredSavedObjectsClient.bulkCreate.mockResolvedValue({
saved_objects: [
{
id: '1',
type: 'alert',
attributes: {
enabled: true,
tags: ['foo'],
alertTypeId: 'myType',
schedule: { interval: '1m' },
consumer: 'myApp',
scheduledTaskId: 'task-123',
params: { index: ['test-index-*'] },
throttle: null,
notifyWhen: null,
actions: [],
},
references: [],
version: '123',
error: {
error: 'test failure',
statusCode: 500,
message: 'test failure',
},
},
],
});
await rulesClient.bulkEdit({
filter: 'alert.attributes.tags: "APM"',
operations: [
{
field: 'tags',
operation: 'add',
value: ['test-1'],
},
],
});
expect(bulkMarkApiKeysForInvalidation).not.toHaveBeenCalled();
});
test('should not call get apiKey if rule is disabled', async () => {
await rulesClient.bulkEdit({
filter: 'alert.attributes.tags: "APM"',
operations: [
{
field: 'tags',
operation: 'add',
value: ['test-1'],
},
],
});
expect(rulesClientParams.getAuthenticationAPIKey).not.toHaveBeenCalledWith();
});
test('should return error in rule errors if key is not generated', async () => {
await rulesClient.bulkEdit({
filter: 'alert.attributes.tags: "APM"',
operations: [
{
field: 'tags',
operation: 'add',
value: ['test-1'],
},
],
});
expect(rulesClientParams.getAuthenticationAPIKey).toHaveBeenCalledWith(
'Alerting: myType/my rule name-user-created'
);
});
});
});
describe('params validation', () => {

View file

@ -64,6 +64,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
kibanaVersion,
auditLogger,
minimumScheduleInterval: { value: '1m', enforce: false },
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
};
beforeEach(() => {

View file

@ -60,6 +60,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
kibanaVersion,
auditLogger,
eventLogger,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
};
describe('clearExpiredSnoozes()', () => {

View file

@ -68,6 +68,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
kibanaVersion,
auditLogger,
minimumScheduleInterval: { value: '1m', enforce: false },
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
};
beforeEach(() => {
@ -415,6 +417,7 @@ describe('create()', () => {
],
"alertTypeId": "123",
"apiKey": null,
"apiKeyCreatedByUser": null,
"apiKeyOwner": null,
"consumer": "bar",
"createdAt": "2019-02-12T21:01:22.479Z",
@ -636,6 +639,7 @@ describe('create()', () => {
],
"alertTypeId": "123",
"apiKey": null,
"apiKeyCreatedByUser": null,
"apiKeyOwner": null,
"consumer": "bar",
"createdAt": "2019-02-12T21:01:22.479Z",
@ -1087,6 +1091,7 @@ describe('create()', () => {
alertTypeId: '123',
apiKey: null,
apiKeyOwner: null,
apiKeyCreatedByUser: null,
consumer: 'bar',
createdAt: '2019-02-12T21:01:22.479Z',
createdBy: 'elastic',
@ -1297,6 +1302,7 @@ describe('create()', () => {
alertTypeId: '123',
apiKey: null,
apiKeyOwner: null,
apiKeyCreatedByUser: null,
consumer: 'bar',
createdAt: '2019-02-12T21:01:22.479Z',
createdBy: 'elastic',
@ -1477,6 +1483,7 @@ describe('create()', () => {
alertTypeId: '123',
apiKey: null,
apiKeyOwner: null,
apiKeyCreatedByUser: null,
legacyId: null,
consumer: 'bar',
createdAt: '2019-02-12T21:01:22.479Z',
@ -1650,6 +1657,7 @@ describe('create()', () => {
params: { bar: true },
apiKey: null,
apiKeyOwner: null,
apiKeyCreatedByUser: null,
createdBy: 'elastic',
createdAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
@ -1787,6 +1795,7 @@ describe('create()', () => {
params: { bar: true },
apiKey: null,
apiKeyOwner: null,
apiKeyCreatedByUser: null,
createdBy: 'elastic',
createdAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
@ -1924,6 +1933,7 @@ describe('create()', () => {
params: { bar: true },
apiKey: null,
apiKeyOwner: null,
apiKeyCreatedByUser: null,
createdBy: 'elastic',
createdAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
@ -2083,6 +2093,7 @@ describe('create()', () => {
],
apiKeyOwner: null,
apiKey: null,
apiKeyCreatedByUser: null,
legacyId: null,
createdBy: 'elastic',
updatedBy: 'elastic',
@ -2562,6 +2573,7 @@ describe('create()', () => {
params: { bar: true },
apiKey: null,
apiKeyOwner: null,
apiKeyCreatedByUser: null,
createdBy: 'elastic',
createdAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
@ -3309,4 +3321,137 @@ describe('create()', () => {
expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled();
expect(taskManager.schedule).not.toHaveBeenCalled();
});
test('calls the authentication API key function if the user is authenticated using an api key', async () => {
const data = getMockData();
rulesClientParams.getAuthenticationAPIKey.mockReturnValueOnce({
apiKeysEnabled: true,
result: { id: '123', name: '123', api_key: 'abc' },
});
rulesClientParams.isAuthenticationTypeAPIKey.mockReturnValueOnce(true);
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
alertTypeId: '123',
schedule: { interval: '1m' },
params: {
bar: true,
},
actions: [
{
group: 'default',
actionRef: 'action_0',
actionTypeId: 'test',
params: {
foo: true,
},
},
],
},
references: [
{
name: 'action_0',
type: 'action',
id: '1',
},
],
});
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
actions: [],
scheduledTaskId: 'task-123',
},
references: [
{
id: '1',
name: 'action_0',
type: 'action',
},
],
});
await rulesClient.create({ data });
expect(rulesClientParams.isAuthenticationTypeAPIKey).toHaveBeenCalledTimes(1);
expect(rulesClientParams.getAuthenticationAPIKey).toHaveBeenCalledTimes(1);
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledWith(
'alert',
{
actions: [
{
actionRef: 'action_0',
group: 'default',
actionTypeId: 'test',
params: { foo: true },
uuid: '151',
},
],
alertTypeId: '123',
consumer: 'bar',
name: 'abc',
legacyId: null,
params: { bar: true },
apiKey: Buffer.from('123:abc').toString('base64'),
apiKeyOwner: 'elastic',
apiKeyCreatedByUser: true,
createdBy: 'elastic',
createdAt: '2019-02-12T21:01:22.479Z',
updatedBy: 'elastic',
updatedAt: '2019-02-12T21:01:22.479Z',
enabled: true,
meta: {
versionApiKeyLastmodified: kibanaVersion,
},
schedule: { interval: '1m' },
throttle: null,
notifyWhen: null,
muteAll: false,
snoozeSchedule: [],
mutedInstanceIds: [],
tags: ['foo'],
executionStatus: {
lastExecutionDate: '2019-02-12T21:01:22.479Z',
status: 'pending',
error: null,
warning: null,
},
monitoring: getDefaultMonitoring('2019-02-12T21:01:22.479Z'),
revision: 0,
running: false,
},
{
id: 'mock-saved-object-id',
references: [
{
id: '1',
name: 'action_0',
type: 'action',
},
],
}
);
});
test('throws error and does not add API key to invalidatePendingApiKey SO when create saved object fails if the user is authenticated using an api key', async () => {
const data = getMockData();
rulesClientParams.getAuthenticationAPIKey.mockReturnValueOnce({
apiKeysEnabled: true,
result: { id: '123', name: '123', api_key: 'abc' },
});
rulesClientParams.isAuthenticationTypeAPIKey.mockReturnValueOnce(true);
unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Test failure'));
await expect(rulesClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot(
`"Test failure"`
);
expect(taskManager.schedule).not.toHaveBeenCalled();
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1);
expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledTimes(1);
expect(bulkMarkApiKeysForInvalidation).toHaveBeenCalledWith(
{ apiKeys: [] },
expect.any(Object),
expect.any(Object)
);
});
});

View file

@ -48,6 +48,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
getEventLogClient: jest.fn(),
kibanaVersion,
auditLogger,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
};
beforeEach(() => {
@ -161,6 +163,20 @@ describe('delete()', () => {
expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled();
});
test(`doesn't invalidate API key if set by the user when authenticated using api keys`, async () => {
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({
...existingAlert,
attributes: {
...existingAlert.attributes,
apiKeyCreatedByUser: true,
},
});
await rulesClient.delete({ id: '1' });
expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled();
expect(bulkMarkApiKeysForInvalidation).not.toHaveBeenCalled();
});
test('swallows error when invalidate API key throws', async () => {
unsecuredSavedObjectsClient.create.mockRejectedValueOnce(new Error('Fail'));
await rulesClient.delete({ id: '1' });

View file

@ -55,6 +55,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
kibanaVersion,
auditLogger,
eventLogger,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
};
beforeEach(() => {

View file

@ -48,6 +48,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
getEventLogClient: jest.fn(),
kibanaVersion,
auditLogger,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
};
setGlobalDate();
@ -375,7 +377,7 @@ describe('enable()', () => {
});
await expect(
async () => await rulesClient.enable({ id: '1' })
).rejects.toThrowErrorMatchingInlineSnapshot(`"Error creating API key for rule: no"`);
).rejects.toThrowErrorMatchingInlineSnapshot(`"Error creating API key for rule - no"`);
expect(taskManager.bulkEnable).not.toHaveBeenCalled();
});

View file

@ -45,6 +45,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
};
beforeEach(() => {

View file

@ -43,6 +43,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
};
beforeEach(() => {

View file

@ -50,6 +50,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
getEventLogClient: jest.fn(),
kibanaVersion,
auditLogger,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
};
beforeEach(() => {

View file

@ -42,6 +42,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
};
beforeEach(() => {

View file

@ -48,6 +48,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
};
beforeEach(() => {

View file

@ -51,6 +51,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
getEventLogClient: jest.fn(),
kibanaVersion,
auditLogger,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
};
beforeEach(() => {

View file

@ -46,6 +46,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
};
beforeEach(() => {

View file

@ -42,6 +42,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
};
beforeEach(() => {

View file

@ -42,6 +42,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
};
beforeEach(() => {

View file

@ -43,6 +43,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
};
beforeEach(() => {

View file

@ -44,6 +44,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
getEventLogClient: jest.fn(),
kibanaVersion,
auditLogger,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
};
setGlobalDate();

View file

@ -42,6 +42,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
};
beforeEach(() => {

View file

@ -42,6 +42,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
getActionsClient: jest.fn(),
getEventLogClient: jest.fn(),
kibanaVersion,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
};
beforeEach(() => {

View file

@ -69,6 +69,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
kibanaVersion,
auditLogger,
minimumScheduleInterval: { value: '1m', enforce: false },
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
};
beforeEach(() => {
@ -381,6 +383,7 @@ describe('update()', () => {
],
"alertTypeId": "myType",
"apiKey": null,
"apiKeyCreatedByUser": null,
"apiKeyOwner": null,
"consumer": "myApp",
"enabled": true,
@ -622,6 +625,7 @@ describe('update()', () => {
alertTypeId: 'myType',
apiKey: null,
apiKeyOwner: null,
apiKeyCreatedByUser: null,
consumer: 'myApp',
enabled: true,
meta: { versionApiKeyLastmodified: 'v7.10.0' },
@ -809,6 +813,7 @@ describe('update()', () => {
alertTypeId: 'myType',
apiKey: null,
apiKeyOwner: null,
apiKeyCreatedByUser: null,
consumer: 'myApp',
enabled: true,
meta: { versionApiKeyLastmodified: 'v7.10.0' },
@ -987,6 +992,7 @@ describe('update()', () => {
],
"alertTypeId": "myType",
"apiKey": "MTIzOmFiYw==",
"apiKeyCreatedByUser": undefined,
"apiKeyOwner": "elastic",
"consumer": "myApp",
"enabled": true,
@ -1139,6 +1145,7 @@ describe('update()', () => {
],
"alertTypeId": "myType",
"apiKey": null,
"apiKeyCreatedByUser": null,
"apiKeyOwner": null,
"consumer": "myApp",
"enabled": false,
@ -2155,6 +2162,7 @@ describe('update()', () => {
alertTypeId: 'myType',
apiKey: null,
apiKeyOwner: null,
apiKeyCreatedByUser: null,
consumer: 'myApp',
enabled: true,
meta: { versionApiKeyLastmodified: 'v7.10.0' },
@ -2702,6 +2710,7 @@ describe('update()', () => {
alertTypeId: 'myType',
apiKey: null,
apiKeyOwner: null,
apiKeyCreatedByUser: null,
consumer: 'myApp',
enabled: true,
mapped_params: { risk_score: 40, severity: '20-low' },
@ -2724,4 +2733,210 @@ describe('update()', () => {
}
);
});
it('calls the authentication API key function if the user is authenticated using an api key', async () => {
rulesClientParams.isAuthenticationTypeAPIKey.mockReturnValueOnce(true);
rulesClientParams.getAuthenticationAPIKey.mockReturnValueOnce({
apiKeysEnabled: true,
result: { id: '123', name: '123', api_key: 'abc' },
});
unsecuredSavedObjectsClient.create.mockResolvedValueOnce({
id: '1',
type: 'alert',
attributes: {
enabled: true,
schedule: { interval: '1m' },
params: {
bar: true,
},
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
notifyWhen: 'onThrottleInterval',
actions: [
{
group: 'default',
actionRef: 'action_0',
actionTypeId: 'test',
params: {
foo: true,
},
},
],
apiKey: Buffer.from('123:abc').toString('base64'),
apiKeyCreatedByUser: true,
revision: 1,
scheduledTaskId: 'task-123',
},
updated_at: new Date().toISOString(),
references: [
{
name: 'action_0',
type: 'action',
id: '1',
},
],
});
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({
...existingDecryptedAlert,
attributes: {
...existingDecryptedAlert.attributes,
apiKeyCreatedByUser: true,
},
});
const result = await rulesClient.update({
id: '1',
data: {
schedule: { interval: '1m' },
name: 'abc',
tags: ['foo'],
params: {
bar: true,
},
throttle: '5m',
notifyWhen: 'onThrottleInterval',
actions: [
{
group: 'default',
id: '1',
params: {
foo: true,
},
},
],
},
});
expect(result).toMatchInlineSnapshot(`
Object {
"actions": Array [
Object {
"actionTypeId": "test",
"group": "default",
"id": "1",
"params": Object {
"foo": true,
},
},
],
"apiKey": "MTIzOmFiYw==",
"apiKeyCreatedByUser": true,
"createdAt": 2019-02-12T21:01:22.479Z,
"enabled": true,
"id": "1",
"notifyWhen": "onThrottleInterval",
"params": Object {
"bar": true,
},
"revision": 1,
"schedule": Object {
"interval": "1m",
},
"scheduledTaskId": "task-123",
"updatedAt": 2019-02-12T21:01:22.479Z,
}
`);
expect(unsecuredSavedObjectsClient.create).toHaveBeenCalledTimes(1);
expect(bulkMarkApiKeysForInvalidationMock).not.toHaveBeenCalled();
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(`
Object {
"actions": Array [
Object {
"actionRef": "action_0",
"actionTypeId": "test",
"group": "default",
"params": Object {
"foo": true,
},
"uuid": "152",
},
],
"alertTypeId": "myType",
"apiKey": "MTIzOmFiYw==",
"apiKeyCreatedByUser": true,
"apiKeyOwner": "elastic",
"consumer": "myApp",
"enabled": true,
"meta": Object {
"versionApiKeyLastmodified": "v7.10.0",
},
"name": "abc",
"notifyWhen": "onThrottleInterval",
"params": Object {
"bar": true,
},
"revision": 1,
"schedule": Object {
"interval": "1m",
},
"scheduledTaskId": "task-123",
"tags": Array [
"foo",
],
"throttle": "5m",
"updatedAt": "2019-02-12T21:01:22.479Z",
"updatedBy": "elastic",
}
`);
expect(unsecuredSavedObjectsClient.create.mock.calls[0][2]).toMatchInlineSnapshot(`
Object {
"id": "1",
"overwrite": true,
"references": Array [
Object {
"id": "1",
"name": "action_0",
"type": "action",
},
],
"version": "123",
}
`);
});
test('throws when unsecuredSavedObjectsClient update fails and does not invalidate newly created API key if the user is authenticated using an api key', async () => {
rulesClientParams.isAuthenticationTypeAPIKey.mockReturnValueOnce(true);
rulesClientParams.getAuthenticationAPIKey.mockReturnValueOnce({
apiKeysEnabled: true,
result: { id: '123', name: '123', api_key: 'abc' },
});
unsecuredSavedObjectsClient.create.mockRejectedValue(new Error('Fail'));
await expect(
rulesClient.update({
id: '1',
data: {
schedule: { interval: '1m' },
name: 'abc',
tags: ['foo'],
params: {
bar: true,
},
actions: [
{
group: 'default',
id: '1',
params: {
foo: true,
},
frequency: {
summary: false,
notifyWhen: 'onActionGroupChange',
throttle: null,
},
},
],
},
})
).rejects.toThrowErrorMatchingInlineSnapshot(`"Fail"`);
expect(bulkMarkApiKeysForInvalidationMock).toHaveBeenCalledTimes(1);
expect(bulkMarkApiKeysForInvalidationMock).toHaveBeenCalledWith(
{
apiKeys: [],
},
expect.any(Object),
expect.any(Object)
);
});
});

View file

@ -49,6 +49,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
getEventLogClient: jest.fn(),
kibanaVersion,
auditLogger,
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
};
beforeEach(() => {
@ -99,6 +101,7 @@ describe('updateApiKey()', () => {
});
test('updates the API key for the alert', async () => {
rulesClientParams.isAuthenticationTypeAPIKey.mockReturnValueOnce(false);
rulesClientParams.createAPIKey.mockResolvedValueOnce({
apiKeysEnabled: true,
result: { id: '234', name: '123', api_key: 'abc' },
@ -118,6 +121,7 @@ describe('updateApiKey()', () => {
enabled: true,
apiKey: Buffer.from('234:abc').toString('base64'),
apiKeyOwner: 'elastic',
apiKeyCreatedByUser: false,
revision: 0,
updatedBy: 'elastic',
updatedAt: '2019-02-12T21:01:22.479Z',
@ -146,6 +150,110 @@ describe('updateApiKey()', () => {
);
});
test('updates the API key for the alert and does not invalidate the old api key if created by a user authenticated using an api key', async () => {
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({
...existingEncryptedAlert,
attributes: {
...existingEncryptedAlert.attributes,
apiKeyCreatedByUser: true,
},
});
rulesClientParams.isAuthenticationTypeAPIKey.mockReturnValueOnce(false);
rulesClientParams.createAPIKey.mockResolvedValueOnce({
apiKeysEnabled: true,
result: { id: '234', name: '123', api_key: 'abc' },
});
await rulesClient.updateApiKey({ id: '1' });
expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled();
expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', {
namespace: 'default',
});
expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith(
'alert',
'1',
{
schedule: { interval: '10s' },
alertTypeId: 'myType',
consumer: 'myApp',
enabled: true,
apiKey: Buffer.from('234:abc').toString('base64'),
apiKeyOwner: 'elastic',
apiKeyCreatedByUser: false,
revision: 0,
updatedBy: 'elastic',
updatedAt: '2019-02-12T21:01:22.479Z',
actions: [
{
group: 'default',
id: '1',
actionTypeId: '1',
actionRef: '1',
params: {
foo: true,
},
},
],
meta: {
versionApiKeyLastmodified: kibanaVersion,
},
},
{ version: '123' }
);
expect(bulkMarkApiKeysForInvalidation).not.toHaveBeenCalled();
});
test('calls the authentication API key function if the user is authenticated using an api key', async () => {
encryptedSavedObjects.getDecryptedAsInternalUser.mockResolvedValue({
...existingEncryptedAlert,
attributes: {
...existingEncryptedAlert.attributes,
apiKeyCreatedByUser: true,
},
});
rulesClientParams.isAuthenticationTypeAPIKey.mockReturnValueOnce(true);
rulesClientParams.getAuthenticationAPIKey.mockReturnValueOnce({
apiKeysEnabled: true,
result: { id: '234', name: '123', api_key: 'abc' },
});
await rulesClient.updateApiKey({ id: '1' });
expect(unsecuredSavedObjectsClient.get).not.toHaveBeenCalled();
expect(encryptedSavedObjects.getDecryptedAsInternalUser).toHaveBeenCalledWith('alert', '1', {
namespace: 'default',
});
expect(unsecuredSavedObjectsClient.update).toHaveBeenCalledWith(
'alert',
'1',
{
schedule: { interval: '10s' },
alertTypeId: 'myType',
consumer: 'myApp',
enabled: true,
apiKey: Buffer.from('234:abc').toString('base64'),
apiKeyOwner: 'elastic',
apiKeyCreatedByUser: true,
revision: 0,
updatedBy: 'elastic',
updatedAt: '2019-02-12T21:01:22.479Z',
actions: [
{
group: 'default',
id: '1',
actionTypeId: '1',
actionRef: '1',
params: {
foo: true,
},
},
],
meta: {
versionApiKeyLastmodified: kibanaVersion,
},
},
{ version: '123' }
);
expect(bulkMarkApiKeysForInvalidation).not.toHaveBeenCalled();
});
test('throws an error if API key creation throws', async () => {
rulesClientParams.createAPIKey.mockImplementation(() => {
throw new Error('no');

View file

@ -69,6 +69,8 @@ export interface RulesClientContext {
readonly auditLogger?: AuditLogger;
readonly eventLogger?: IEventLogger;
readonly fieldsToExcludeFromPublicApi: Array<keyof SanitizedRule>;
readonly isAuthenticationTypeAPIKey: () => boolean;
readonly getAuthenticationAPIKey: (name: string) => CreateAPIKeyResult;
}
export type NormalizedAlertAction = Omit<RuleAction, 'actionTypeId'>;

View file

@ -53,6 +53,8 @@ const rulesClientParams: jest.Mocked<ConstructorOptions> = {
getEventLogClient: jest.fn(),
kibanaVersion,
minimumScheduleInterval: { value: '1m', enforce: false },
isAuthenticationTypeAPIKey: jest.fn(),
getAuthenticationAPIKey: jest.fn(),
};
// this suite consists of two suites running tests against mutable RulesClient APIs:

View file

@ -122,6 +122,8 @@ test('creates a rules client with proper constructor arguments when security is
encryptedSavedObjectsClient: rulesClientFactoryParams.encryptedSavedObjectsClient,
kibanaVersion: '7.10.0',
minimumScheduleInterval: { value: '1m', enforce: false },
isAuthenticationTypeAPIKey: expect.any(Function),
getAuthenticationAPIKey: expect.any(Function),
});
});
@ -160,6 +162,8 @@ test('creates a rules client with proper constructor arguments', async () => {
getEventLogClient: expect.any(Function),
kibanaVersion: '7.10.0',
minimumScheduleInterval: { value: '1m', enforce: false },
isAuthenticationTypeAPIKey: expect.any(Function),
getAuthenticationAPIKey: expect.any(Function),
});
});

View file

@ -12,7 +12,11 @@ import {
PluginInitializerContext,
} from '@kbn/core/server';
import { PluginStartContract as ActionsPluginStartContract } from '@kbn/actions-plugin/server';
import { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/server';
import {
HTTPAuthorizationHeader,
SecurityPluginSetup,
SecurityPluginStart,
} from '@kbn/security-plugin/server';
import { EncryptedSavedObjectsClient } from '@kbn/encrypted-saved-objects-plugin/server';
import { TaskManagerStartContract } from '@kbn/task-manager-plugin/server';
import { IEventLogClientService, IEventLogger } from '@kbn/event-log-plugin/server';
@ -133,6 +137,30 @@ export class RulesClientFactory {
return eventLog.getClient(request);
},
eventLogger: this.eventLogger,
isAuthenticationTypeAPIKey() {
if (!securityPluginStart) {
return false;
}
const user = securityPluginStart.authc.getCurrentUser(request);
return user && user.authentication_type ? user.authentication_type === 'api_key' : false;
},
getAuthenticationAPIKey(name: string) {
const authorizationHeader = HTTPAuthorizationHeader.parseFromRequest(request);
if (authorizationHeader && authorizationHeader.credentials) {
const apiKey = Buffer.from(authorizationHeader.credentials, 'base64')
.toString()
.split(':');
return {
apiKeysEnabled: true,
result: {
name,
id: apiKey[0],
api_key: apiKey[1],
},
};
}
return { apiKeysEnabled: false };
},
});
}
}

View file

@ -8,6 +8,7 @@
import { SavedObjectsTypeMappingDefinition } from '@kbn/core/server';
export const alertMappings: SavedObjectsTypeMappingDefinition = {
dynamic: false,
properties: {
enabled: {
type: 'boolean',

View file

@ -34,6 +34,7 @@ describe('transform rule for export', () => {
updatedAt: date,
apiKey: '4tndskbuhewotw4klrhgjewrt9u',
apiKeyOwner: 'me',
apiKeyCreatedByUser: true,
throttle: null,
legacyId: '1',
notifyWhen: 'onActionGroupChange',
@ -66,6 +67,7 @@ describe('transform rule for export', () => {
updatedAt: date,
apiKey: null,
apiKeyOwner: null,
apiKeyCreatedByUser: null,
throttle: null,
legacyId: '2',
notifyWhen: 'onActionGroupChange',
@ -91,6 +93,7 @@ describe('transform rule for export', () => {
enabled: false,
apiKey: null,
apiKeyOwner: null,
apiKeyCreatedByUser: null,
scheduledTaskId: null,
legacyId: null,
executionStatus: {

View file

@ -26,6 +26,7 @@ function transformRuleForExport(
enabled: false,
apiKey: null,
apiKeyOwner: null,
apiKeyCreatedByUser: null,
scheduledTaskId: null,
executionStatus: getRuleExecutionStatusPending(exportDate),
},

View file

@ -357,6 +357,7 @@ export interface RawRule extends SavedObjectAttributes {
updatedAt: string;
apiKey: string | null;
apiKeyOwner: string | null;
apiKeyCreatedByUser?: boolean | null;
throttle?: string | null;
notifyWhen?: RuleNotifyWhenType | null;
muteAll: boolean;

View file

@ -20,13 +20,6 @@ jest.mock('../lib/alerts/fetch_licenses', () => ({
jest.mock('../lib/alerts/fetch_clusters', () => ({
fetchClusters: jest.fn(),
}));
jest.mock('moment', () => {
const actual = jest.requireActual('moment');
return {
...actual,
duration: () => ({ humanize: () => 'HUMANIZED_DURATION' }),
};
});
jest.mock('../static_globals', () => ({
Globals: {
@ -120,7 +113,12 @@ describe('LicenseExpirationRule', () => {
getState.mockReset();
});
afterAll(() => {
jest.useRealTimers();
});
it('should fire actions', async () => {
jest.useFakeTimers().setSystemTime(new Date('2023-03-30T00:00:00.000Z'));
const alert = new LicenseExpirationRule();
const type = alert.getRuleType();
await type.executor({
@ -169,7 +167,7 @@ describe('LicenseExpirationRule', () => {
],
},
severity: 'danger',
triggeredMS: 1,
triggeredMS: 1680134400000,
lastCheckedMS: 0,
},
},
@ -179,11 +177,11 @@ describe('LicenseExpirationRule', () => {
action: '[Please update your license.](elasticsearch/nodes)',
actionPlain: 'Please update your license.',
internalFullMessage:
'License expiration alert is firing for testCluster. Your license expires in HUMANIZED_DURATION. [Please update your license.](elasticsearch/nodes)',
'License expiration alert is firing for testCluster. Your license expires in 53 years. [Please update your license.](elasticsearch/nodes)',
internalShortMessage:
'License expiration alert is firing for testCluster. Your license expires in HUMANIZED_DURATION. Please update your license.',
'License expiration alert is firing for testCluster. Your license expires in 53 years. Please update your license.',
clusterName,
expiredDate: 'HUMANIZED_DURATION',
expiredDate: '53 years',
state: 'firing',
});
});

View file

@ -123,6 +123,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
notify_when: 'onThrottleInterval',
updated_by: user.username,
api_key_owner: user.username,
api_key_created_by_user: false,
mute_all: false,
muted_alert_ids: [],
execution_status: response.body.execution_status,

View file

@ -79,6 +79,7 @@ const getTestUtils = (
notify_when: 'onThrottleInterval',
updated_by: 'elastic',
api_key_owner: 'elastic',
api_key_created_by_user: false,
mute_all: false,
muted_alert_ids: [],
execution_status: response.body.execution_status,

View file

@ -119,6 +119,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
enabled: true,
updated_by: user.username,
api_key_owner: user.username,
api_key_created_by_user: false,
mute_all: false,
muted_alert_ids: [],
actions: [
@ -219,6 +220,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
enabled: true,
updated_by: user.username,
api_key_owner: user.username,
api_key_created_by_user: false,
mute_all: false,
muted_alert_ids: [],
scheduled_task_id: createdAlert.scheduled_task_id,
@ -321,6 +323,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
enabled: true,
updated_by: user.username,
api_key_owner: user.username,
api_key_created_by_user: false,
mute_all: false,
muted_alert_ids: [],
scheduled_task_id: createdAlert.scheduled_task_id,
@ -423,6 +426,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
enabled: true,
updated_by: user.username,
api_key_owner: user.username,
api_key_created_by_user: false,
mute_all: false,
muted_alert_ids: [],
scheduled_task_id: createdAlert.scheduled_task_id,
@ -523,6 +527,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
enabled: true,
updated_by: user.username,
api_key_owner: user.username,
api_key_created_by_user: false,
mute_all: false,
muted_alert_ids: [],
scheduled_task_id: createdAlert.scheduled_task_id,

View file

@ -13,6 +13,7 @@ import { getUrlPrefix, getTestRuleData, ObjectRemover } from '../../../../common
const getDefaultRules = (response: any) => ({
id: response.body.rules[0].id,
apiKey: response.body.rules[0].apiKey,
apiKeyCreatedByUser: false,
notifyWhen: 'onThrottleInterval',
enabled: true,
name: 'abc',
@ -46,6 +47,7 @@ const getThreeRules = (response: any) => {
rules.push({
id: response.body.rules[i].id,
apiKey: response.body.rules[i].apiKey,
apiKeyCreatedByUser: false,
notifyWhen: 'onThrottleInterval',
enabled: true,
name: 'abc',

View file

@ -19,6 +19,7 @@ const getDefaultRules = (response: any) => ({
consumer: 'alertsFixture',
throttle: '1m',
alertTypeId: 'test.noop',
apiKeyCreatedByUser: false,
apiKeyOwner: response.body.rules[0].apiKeyOwner,
createdBy: 'elastic',
updatedBy: response.body.rules[0].updatedBy,

View file

@ -83,6 +83,7 @@ export default ({ getService }: FtrProviderContext) => {
consumer: 'alertsFixture',
throttle: '1m',
alertTypeId: 'test.noop',
apiKeyCreatedByUser: false,
apiKeyOwner: response.body.rules[0].apiKeyOwner,
createdBy: 'elastic',
updatedBy: response.body.rules[0].updatedBy,

View file

@ -161,6 +161,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
throttle: '1m',
notify_when: 'onThrottleInterval',
updated_by: user.username,
api_key_created_by_user: false,
api_key_owner: user.username,
mute_all: false,
muted_alert_ids: [],

View file

@ -90,6 +90,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
scheduled_task_id: response.body.scheduled_task_id,
updated_by: null,
api_key_owner: null,
api_key_created_by_user: null,
throttle: '1m',
notify_when: 'onThrottleInterval',
mute_all: false,
@ -194,6 +195,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
scheduled_task_id: response.body.scheduled_task_id,
updated_by: null,
api_key_owner: null,
api_key_created_by_user: null,
throttle: '1m',
notify_when: 'onThrottleInterval',
mute_all: false,
@ -498,6 +500,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) {
scheduledTaskId: response.body.scheduledTaskId,
updatedBy: null,
apiKeyOwner: null,
apiKeyCreatedByUser: null,
throttle: '1m',
notifyWhen: 'onThrottleInterval',
muteAll: false,

View file

@ -109,6 +109,7 @@ const findTestUtils = (
params: {},
created_by: null,
api_key_owner: null,
api_key_created_by_user: null,
scheduled_task_id: match.scheduled_task_id,
updated_by: null,
throttle: null,
@ -361,6 +362,7 @@ export default function createFindTests({ getService }: FtrProviderContext) {
params: {},
createdBy: null,
apiKeyOwner: null,
apiKeyCreatedByUser: null,
scheduledTaskId: match.scheduledTaskId,
updatedBy: null,
throttle: '1m',

View file

@ -49,6 +49,7 @@ const getTestUtils = (
scheduled_task_id: response.body.scheduled_task_id,
updated_by: null,
api_key_owner: null,
api_key_created_by_user: null,
throttle: '1m',
notify_when: 'onThrottleInterval',
mute_all: false,
@ -149,6 +150,7 @@ export default function createGetTests({ getService }: FtrProviderContext) {
scheduledTaskId: response.body.scheduledTaskId,
updatedBy: null,
apiKeyOwner: null,
apiKeyCreatedByUser: null,
throttle: '1m',
notifyWhen: 'onThrottleInterval',
muteAll: false,

View file

@ -57,6 +57,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
enabled: true,
updated_by: null,
api_key_owner: null,
api_key_created_by_user: null,
mute_all: false,
muted_alert_ids: [],
notify_when: 'onThrottleInterval',
@ -163,6 +164,7 @@ export default function createUpdateTests({ getService }: FtrProviderContext) {
enabled: true,
updatedBy: null,
apiKeyOwner: null,
apiKeyCreatedByUser: null,
muteAll: false,
mutedInstanceIds: [],
notifyWhen: 'onThrottleInterval',

View file

@ -63,6 +63,7 @@ export default function ({ getService }: FtrProviderContext) {
notifyWhen: 'onActionGroupChange',
consumer: 'uptime',
alertTypeId: 'xpack.synthetics.alerts.monitorStatus',
apiKeyCreatedByUser: false,
tags: ['SYNTHETICS_DEFAULT_ALERT'],
name: 'Synthetics internal alert',
enabled: true,