Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions x-pack/plugins/alerting/server/rules_client.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const createRulesClientMock = () => {
clearExpiredSnoozes: jest.fn(),
runSoon: jest.fn(),
clone: jest.fn(),
getAlertFromRaw: jest.fn(),
};
return mocked;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ import {
} from '../common';
import { RulesClientContext } from '../types';

export interface GetAlertFromRawParams {
id: string;
ruleTypeId: string;
rawRule: RawRule;
references: SavedObjectReference[] | undefined;
includeLegacyId?: boolean;
excludeFromPublicApi?: boolean;
includeSnoozeData?: boolean;
}

export function getAlertFromRaw<Params extends RuleTypeParams>(
context: RulesClientContext,
id: string,
Expand Down
13 changes: 13 additions & 0 deletions x-pack/plugins/alerting/server/rules_client/rules_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import { muteInstance } from './methods/mute_instance';
import { unmuteInstance } from './methods/unmute_instance';
import { runSoon } from './methods/run_soon';
import { listAlertTypes } from './methods/list_alert_types';
import { getAlertFromRaw, GetAlertFromRawParams } from './lib/get_alert_from_raw';

export type ConstructorOptions = Omit<
RulesClientContext,
Expand Down Expand Up @@ -135,4 +136,16 @@ export class RulesClient {
public getSpaceId(): string | undefined {
return this.context.spaceId;
}

public getAlertFromRaw = (params: GetAlertFromRawParams) =>
getAlertFromRaw(
this.context,
params.id,
params.ruleTypeId,
params.rawRule,
params.references,
params.includeLegacyId,
params.excludeFromPublicApi,
params.includeSnoozeData
);
}
57 changes: 21 additions & 36 deletions x-pack/plugins/alerting/server/task_runner/rule_loader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/s
import { CoreKibanaRequest } from '@kbn/core/server';
import { schema } from '@kbn/config-schema';

import { getDecryptedAttributes, getFakeKibanaRequest, loadRule } from './rule_loader';
import { getRuleAttributes, getFakeKibanaRequest, loadRule } from './rule_loader';
import { TaskRunnerContext } from './task_runner_factory';
import { ruleTypeRegistryMock } from '../rule_type_registry.mock';
import { rulesClientMock } from '../rules_client.mock';
import { Rule, RulesClientApi } from '../types';
import { Rule } from '../types';
import { MONITORING_HISTORY_LIMIT, RuleExecutionStatusErrorReasons } from '../../common';
import { getReasonFromError } from '../lib/error_with_reason';
import { alertingEventLoggerMock } from '../lib/alerting_event_logger/alerting_event_logger.mock';
Expand Down Expand Up @@ -137,25 +137,6 @@ describe('rule_loader', () => {
expect(outcome).toBe('failure');
});

test('throws when user cannot read rule', async () => {
context.getRulesClientWithRequest = function (fakeRequest: unknown): RulesClientApi {
rulesClient.get.mockImplementation(async (args: unknown) => {
throw new Error('rule-client-error: 1001');
});
return rulesClient;
};

let outcome = 'success';
try {
await loadRule({ ...DefaultLoadRuleParams, context });
} catch (err) {
outcome = 'failure';
expect(err.message).toBe('rule-client-error: 1001');
expect(getReasonFromError(err)).toBe(RuleExecutionStatusErrorReasons.Read);
}
expect(outcome).toBe('failure');
});

test('throws when rule type is not enabled', async () => {
ruleTypeRegistry.ensureRuleTypeEnabled.mockImplementation(() => {
throw new Error('rule-type-not-enabled: 2112');
Expand Down Expand Up @@ -196,11 +177,14 @@ describe('rule_loader', () => {
describe('getDecryptedAttributes()', () => {
test('succeeds with default space', async () => {
contextMock.spaceIdToNamespace.mockReturnValue(undefined);
const result = await getDecryptedAttributes(context, ruleId, 'default');
const result = await getRuleAttributes(context, ruleId, 'default');

expect(result.apiKey).toBe(apiKey);
expect(result.consumer).toBe(consumer);
expect(result.enabled).toBe(true);
expect(result.fakeRequest).toEqual(expect.any(CoreKibanaRequest));
expect(result.rule.alertTypeId).toBe(ruleTypeId);
expect(result.rulesClient).toBeTruthy();
expect(contextMock.spaceIdToNamespace.mock.calls[0]).toEqual(['default']);

const esoArgs = encryptedSavedObjects.getDecryptedAsInternalUser.mock.calls[0];
Expand All @@ -209,11 +193,14 @@ describe('rule_loader', () => {

test('succeeds with non-default space', async () => {
contextMock.spaceIdToNamespace.mockReturnValue(spaceId);
const result = await getDecryptedAttributes(context, ruleId, spaceId);
const result = await getRuleAttributes(context, ruleId, spaceId);

expect(result.apiKey).toBe(apiKey);
expect(result.consumer).toBe(consumer);
expect(result.enabled).toBe(true);
expect(result.fakeRequest).toEqual(expect.any(CoreKibanaRequest));
expect(result.rule.alertTypeId).toBe(ruleTypeId);
expect(result.rulesClient).toBeTruthy();
expect(contextMock.spaceIdToNamespace.mock.calls[0]).toEqual([spaceId]);

const esoArgs = encryptedSavedObjects.getDecryptedAsInternalUser.mock.calls[0];
Expand All @@ -227,7 +214,7 @@ describe('rule_loader', () => {
}
);

const promise = getDecryptedAttributes(context, ruleId, spaceId);
const promise = getRuleAttributes(context, ruleId, spaceId);
await expect(promise).rejects.toThrow('wops');
});
});
Expand Down Expand Up @@ -340,20 +327,18 @@ function getTaskRunnerContext(ruleParameters: unknown, historyElements: number)
getRulesClientWithRequest,
};

function getRulesClientWithRequest(fakeRequest: unknown) {
function getRulesClientWithRequest() {
// only need get() mocked
rulesClient.get.mockImplementation(async (args: unknown) => {
return {
name: ruleName,
alertTypeId: ruleTypeId,
params: ruleParameters,
monitoring: {
run: {
history: new Array(historyElements),
},
rulesClient.getAlertFromRaw.mockReturnValue({
name: ruleName,
alertTypeId: ruleTypeId,
params: ruleParameters,
monitoring: {
run: {
history: new Array(historyElements),
},
} as Rule;
});
},
} as Rule);
return rulesClient;
}
}
61 changes: 38 additions & 23 deletions x-pack/plugins/alerting/server/task_runner/rule_loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
RuleTypeRegistry,
RuleTypeParamsValidator,
SanitizedRule,
RulesClientApi,
} from '../types';
import { MONITORING_HISTORY_LIMIT, RuleTypeParams } from '../../common';
import { AlertingEventLogger } from '../lib/alerting_event_logger/alerting_event_logger';
Expand All @@ -35,11 +36,17 @@ export async function loadRule<Params extends RuleTypeParams>(params: LoadRulePa
params;
let enabled: boolean;
let apiKey: string | null;
let rule: SanitizedRule<Params>;
let fakeRequest: CoreKibanaRequest;
let rulesClient: RulesClientApi;

try {
const decryptedAttributes = await getDecryptedAttributes(context, ruleId, spaceId);
apiKey = decryptedAttributes.apiKey;
enabled = decryptedAttributes.enabled;
const attributes = await getRuleAttributes<Params>(context, ruleId, spaceId);
apiKey = attributes.apiKey;
enabled = attributes.enabled;
rule = attributes.rule;
fakeRequest = attributes.fakeRequest;
rulesClient = attributes.rulesClient;
} catch (err) {
throw new ErrorWithReason(RuleExecutionStatusErrorReasons.Decrypt, err);
}
Expand All @@ -51,18 +58,6 @@ export async function loadRule<Params extends RuleTypeParams>(params: LoadRulePa
);
}

const fakeRequest = getFakeKibanaRequest(context, spaceId, apiKey);
const rulesClient = context.getRulesClientWithRequest(fakeRequest);

let rule: SanitizedRule<Params>;

// Ensure API key is still valid and user has access
try {
rule = await rulesClient.get<Params>({ id: ruleId });
} catch (err) {
throw new ErrorWithReason(RuleExecutionStatusErrorReasons.Read, err);
}

alertingEventLogger.setRuleName(rule.name);

try {
Expand Down Expand Up @@ -94,24 +89,44 @@ export async function loadRule<Params extends RuleTypeParams>(params: LoadRulePa
};
}

export async function getDecryptedAttributes(
export async function getRuleAttributes<Params extends RuleTypeParams>(
context: TaskRunnerContext,
ruleId: string,
spaceId: string
): Promise<{ apiKey: string | null; enabled: boolean; consumer: string }> {
): Promise<{
apiKey: string | null;
enabled: boolean;
consumer: string;
rule: SanitizedRule<Params>;
fakeRequest: CoreKibanaRequest;
rulesClient: RulesClientApi;
}> {
const namespace = context.spaceIdToNamespace(spaceId);

// Only fetch encrypted attributes here, we'll create a saved objects client
// scoped with the API key to fetch the remaining data.
const {
attributes: { apiKey, enabled, consumer },
} = await context.encryptedSavedObjectsClient.getDecryptedAsInternalUser<RawRule>(
const rawRule = await context.encryptedSavedObjectsClient.getDecryptedAsInternalUser<RawRule>(
'alert',
ruleId,
{ namespace }
);

return { apiKey, enabled, consumer };
const fakeRequest = getFakeKibanaRequest(context, spaceId, rawRule.attributes.apiKey);
const rulesClient = context.getRulesClientWithRequest(fakeRequest);
const rule = rulesClient.getAlertFromRaw({
id: ruleId,
ruleTypeId: rawRule.attributes.alertTypeId as string,
rawRule: rawRule.attributes as RawRule,
references: rawRule.references,
includeLegacyId: false,
});

return {
rule,
apiKey: rawRule.attributes.apiKey,
enabled: rawRule.attributes.enabled,
consumer: rawRule.attributes.consumer,
fakeRequest,
rulesClient,
};
}

export function getFakeKibanaRequest(
Expand Down
Loading