Skip to content

Commit

Permalink
[TGrid] Alerts status update use RAC api (#108092)
Browse files Browse the repository at this point in the history
Co-authored-by: Devin Hurley <[email protected]>
  • Loading branch information
2 people authored and kibanamachine committed Aug 14, 2021
1 parent d96cffa commit 70b6098
Show file tree
Hide file tree
Showing 23 changed files with 465 additions and 93 deletions.
2 changes: 1 addition & 1 deletion packages/kbn-rule-data-utils/src/alerts_as_data_rbac.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const AlertConsumers = {
SYNTHETICS: 'synthetics',
} as const;
export type AlertConsumers = typeof AlertConsumers[keyof typeof AlertConsumers];
export type STATUS_VALUES = 'open' | 'acknowledged' | 'closed';
export type STATUS_VALUES = 'open' | 'acknowledged' | 'closed' | 'in-progress'; // TODO: remove 'in-progress' after migration to 'acknowledged'

export const mapConsumerToIndexName: Record<AlertConsumers, string | string[]> = {
apm: '.alerts-observability-apm',
Expand Down
101 changes: 71 additions & 30 deletions x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
* 2.0.
*/
import Boom from '@hapi/boom';
import { estypes } from '@elastic/elasticsearch';
import { PublicMethodsOf } from '@kbn/utility-types';
import { Filter, buildEsQuery, EsQueryConfig } from '@kbn/es-query';
import { decodeVersion, encodeHitVersion } from '@kbn/securitysolution-es-utils';
Expand Down Expand Up @@ -35,7 +36,7 @@ import { Logger, ElasticsearchClient, EcsEventOutcome } from '../../../../../src
import { alertAuditEvent, operationAlertAuditActionMap } from './audit_events';
import { AuditLogger } from '../../../security/server';
import {
ALERT_STATUS,
ALERT_WORKFLOW_STATUS,
ALERT_RULE_CONSUMER,
ALERT_RULE_TYPE_ID,
SPACE_IDS,
Expand All @@ -50,16 +51,19 @@ const mapConsumerToIndexName: typeof mapConsumerToIndexNameTyped = mapConsumerTo
// TODO: Fix typings https://github.com/elastic/kibana/issues/101776
type NonNullableProps<Obj extends {}, Props extends keyof Obj> = Omit<Obj, Props> &
{ [K in Props]-?: NonNullable<Obj[K]> };
type AlertType = NonNullableProps<
type AlertType = { _index: string; _id: string } & NonNullableProps<
ParsedTechnicalFields,
typeof ALERT_RULE_TYPE_ID | typeof ALERT_RULE_CONSUMER | typeof SPACE_IDS
>;

const isValidAlert = (source?: ParsedTechnicalFields): source is AlertType => {
const isValidAlert = (source?: estypes.SearchHit<any>): source is AlertType => {
return (
source?.[ALERT_RULE_TYPE_ID] != null &&
source?.[ALERT_RULE_CONSUMER] != null &&
source?.[SPACE_IDS] != null
(source?._source?.[ALERT_RULE_TYPE_ID] != null &&
source?._source?.[ALERT_RULE_CONSUMER] != null &&
source?._source?.[SPACE_IDS] != null) ||
(source?.fields?.[ALERT_RULE_TYPE_ID][0] != null &&
source?.fields?.[ALERT_RULE_CONSUMER][0] != null &&
source?.fields?.[SPACE_IDS][0] != null)
);
};
export interface ConstructorOptions {
Expand All @@ -80,7 +84,7 @@ export interface BulkUpdateOptions<Params extends AlertTypeParams> {
ids: string[] | undefined | null;
status: STATUS_VALUES;
index: string;
query: string | undefined | null;
query: object | string | undefined | null;
}

interface GetAlertParams {
Expand All @@ -90,7 +94,7 @@ interface GetAlertParams {

interface SingleSearchAfterAndAudit {
id: string | null | undefined;
query: string | null | undefined;
query: object | string | null | undefined;
index?: string;
operation: WriteOperations.Update | ReadOperations.Find | ReadOperations.Get;
lastSortIds: Array<string | number> | undefined;
Expand Down Expand Up @@ -126,6 +130,15 @@ export class AlertsClient {
};
}

private getAlertStatusFieldUpdate(
source: ParsedTechnicalFields | undefined,
status: STATUS_VALUES
) {
return source?.[ALERT_WORKFLOW_STATUS] == null
? { signal: { status } }
: { [ALERT_WORKFLOW_STATUS]: status };
}

/**
* Accepts an array of ES documents and executes ensureAuthorized for the given operation
* @param items
Expand Down Expand Up @@ -218,6 +231,7 @@ export class AlertsClient {
const config = getEsQueryConfig();

let queryBody = {
fields: [ALERT_RULE_TYPE_ID, ALERT_RULE_CONSUMER, ALERT_WORKFLOW_STATUS, SPACE_IDS],
query: await this.buildEsQueryWithAuthz(query, id, alertSpaceId, operation, config),
sort: [
{
Expand Down Expand Up @@ -245,7 +259,7 @@ export class AlertsClient {
seq_no_primary_term: true,
});

if (!result?.body.hits.hits.every((hit) => isValidAlert(hit._source))) {
if (!result?.body.hits.hits.every((hit) => isValidAlert(hit))) {
const errorMessage = `Invalid alert found with id of "${id}" or with query "${query}" and operation ${operation}`;
this.logger.error(errorMessage);
throw Boom.badData(errorMessage);
Expand Down Expand Up @@ -307,19 +321,25 @@ export class AlertsClient {
);
}

const bulkUpdateRequest = mgetRes.body.docs.flatMap((item) => [
{
update: {
_index: item._index,
_id: item._id,
const bulkUpdateRequest = mgetRes.body.docs.flatMap((item) => {
const fieldToUpdate = this.getAlertStatusFieldUpdate(item?._source, status);
return [
{
update: {
_index: item._index,
_id: item._id,
},
},
},
{
doc: { [ALERT_STATUS]: status },
},
]);
{
doc: {
...fieldToUpdate,
},
},
];
});

const bulkUpdateResponse = await this.esClient.bulk({
refresh: 'wait_for',
body: bulkUpdateRequest,
});
return bulkUpdateResponse;
Expand All @@ -330,7 +350,7 @@ export class AlertsClient {
}

private async buildEsQueryWithAuthz(
query: string | null | undefined,
query: object | string | null | undefined,
id: string | null | undefined,
alertSpaceId: string,
operation: WriteOperations.Update | ReadOperations.Get | ReadOperations.Find,
Expand All @@ -345,15 +365,33 @@ export class AlertsClient {
},
operation
);
return buildEsQuery(
let esQuery;
if (id != null) {
esQuery = { query: `_id:${id}`, language: 'kuery' };
} else if (typeof query === 'string') {
esQuery = { query, language: 'kuery' };
} else if (query != null && typeof query === 'object') {
esQuery = [];
}
const builtQuery = buildEsQuery(
undefined,
{ query: query == null ? `_id:${id}` : query, language: 'kuery' },
esQuery == null ? { query: ``, language: 'kuery' } : esQuery,
[
(authzFilter as unknown) as Filter,
({ term: { [SPACE_IDS]: alertSpaceId } } as unknown) as Filter,
],
config
);
if (query != null && typeof query === 'object') {
return {
...builtQuery,
bool: {
...builtQuery.bool,
must: [...builtQuery.bool.must, query],
},
};
}
return builtQuery;
} catch (exc) {
this.logger.error(exc);
throw Boom.expectationFailed(
Expand All @@ -373,7 +411,7 @@ export class AlertsClient {
operation,
}: {
index: string;
query: string;
query: object | string;
operation: WriteOperations.Update | ReadOperations.Find | ReadOperations.Get;
}) {
let lastSortIds;
Expand Down Expand Up @@ -436,7 +474,7 @@ export class AlertsClient {
// first search for the alert by id, then use the alert info to check if user has access to it
const alert = await this.singleSearchAfterAndAudit({
id,
query: null,
query: undefined,
index,
operation: ReadOperations.Get,
lastSortIds: undefined,
Expand Down Expand Up @@ -476,14 +514,17 @@ export class AlertsClient {
this.logger.error(errorMessage);
throw Boom.notFound(errorMessage);
}

const fieldToUpdate = this.getAlertStatusFieldUpdate(
alert?.hits.hits[0]._source,
status as STATUS_VALUES
);
const { body: response } = await this.esClient.update<ParsedTechnicalFields>({
...decodeVersion(_version),
id,
index,
body: {
doc: {
[ALERT_STATUS]: status,
...fieldToUpdate,
},
},
refresh: 'wait_for',
Expand Down Expand Up @@ -535,11 +576,11 @@ export class AlertsClient {
refresh: true,
body: {
script: {
source: `if (ctx._source['${ALERT_STATUS}'] != null) {
ctx._source['${ALERT_STATUS}'] = '${status}'
source: `if (ctx._source['${ALERT_WORKFLOW_STATUS}'] != null) {
ctx._source['${ALERT_WORKFLOW_STATUS}'] = '${status}'
}
if (ctx._source['signal.status'] != null) {
ctx._source['signal.status'] = '${status}'
if (ctx._source.signal != null && ctx._source.signal.status != null) {
ctx._source.signal.status = '${status}'
}`,
lang: 'painless',
} as InlineScript,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ describe('get()', () => {
Array [
Object {
"body": Object {
"fields": Array [
"kibana.alert.rule.rule_type_id",
"kibana.alert.rule.consumer",
"kibana.alert.workflow_status",
"kibana.space_ids",
],
"query": Object {
"bool": Object {
"filter": Array [
Expand Down Expand Up @@ -254,7 +260,7 @@ describe('get()', () => {

await expect(alertsClient.get({ id: fakeAlertId, index: '.alerts-observability-apm' })).rejects
.toThrowErrorMatchingInlineSnapshot(`
"Unable to retrieve alert details for alert with id of \\"myfakeid1\\" or with query \\"null\\" and operation get
"Unable to retrieve alert details for alert with id of \\"myfakeid1\\" or with query \\"undefined\\" and operation get
Error: Error: Unauthorized for fake.rule and apm"
`);

Expand All @@ -281,7 +287,7 @@ describe('get()', () => {
await expect(
alertsClient.get({ id: 'NoxgpHkBqbdrfX07MqXV', index: '.alerts-observability-apm' })
).rejects.toThrowErrorMatchingInlineSnapshot(`
"Unable to retrieve alert details for alert with id of \\"NoxgpHkBqbdrfX07MqXV\\" or with query \\"null\\" and operation get
"Unable to retrieve alert details for alert with id of \\"NoxgpHkBqbdrfX07MqXV\\" or with query \\"undefined\\" and operation get
Error: Error: something went wrong"
`);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

import {
ALERT_RULE_CONSUMER,
ALERT_STATUS,
ALERT_WORKFLOW_STATUS,
SPACE_IDS,
ALERT_RULE_TYPE_ID,
} from '@kbn/rule-data-utils';
Expand Down Expand Up @@ -89,8 +89,8 @@ describe('update()', () => {
_source: {
[ALERT_RULE_TYPE_ID]: 'apm.error_rate',
message: 'hello world 1',
[ALERT_WORKFLOW_STATUS]: 'open',
[ALERT_RULE_CONSUMER]: 'apm',
[ALERT_STATUS]: 'open',
[SPACE_IDS]: [DEFAULT_SPACE],
},
},
Expand Down Expand Up @@ -139,7 +139,7 @@ describe('update()', () => {
Object {
"body": Object {
"doc": Object {
"${ALERT_STATUS}": "closed",
"${ALERT_WORKFLOW_STATUS}": "closed",
},
},
"id": "1",
Expand Down Expand Up @@ -175,8 +175,8 @@ describe('update()', () => {
_source: {
[ALERT_RULE_TYPE_ID]: 'apm.error_rate',
message: 'hello world 1',
[ALERT_WORKFLOW_STATUS]: 'open',
[ALERT_RULE_CONSUMER]: 'apm',
[ALERT_STATUS]: 'open',
[SPACE_IDS]: [DEFAULT_SPACE],
},
},
Expand Down Expand Up @@ -249,7 +249,7 @@ describe('update()', () => {
_source: {
[ALERT_RULE_TYPE_ID]: fakeRuleTypeId,
[ALERT_RULE_CONSUMER]: 'apm',
[ALERT_STATUS]: 'open',
[ALERT_WORKFLOW_STATUS]: 'open',
[SPACE_IDS]: [DEFAULT_SPACE],
},
},
Expand Down Expand Up @@ -330,8 +330,8 @@ describe('update()', () => {
_source: {
[ALERT_RULE_TYPE_ID]: 'apm.error_rate',
message: 'hello world 1',
[ALERT_WORKFLOW_STATUS]: 'open',
[ALERT_RULE_CONSUMER]: 'apm',
[ALERT_STATUS]: 'open',
[SPACE_IDS]: [DEFAULT_SPACE],
},
},
Expand Down Expand Up @@ -391,7 +391,7 @@ describe('update()', () => {
[ALERT_RULE_TYPE_ID]: 'apm.error_rate',
message: 'hello world 1',
[ALERT_RULE_CONSUMER]: 'apm',
[ALERT_STATUS]: 'open',
[ALERT_WORKFLOW_STATUS]: 'open',
[SPACE_IDS]: [DEFAULT_SPACE],
},
},
Expand Down
16 changes: 13 additions & 3 deletions x-pack/plugins/rule_registry/server/routes/bulk_update_alerts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,26 @@ export const bulkUpdateAlertsRoute = (router: IRouter<RacRequestHandlerContext>)
body: buildRouteValidation(
t.union([
t.strict({
status: t.union([t.literal('open'), t.literal('closed')]),
status: t.union([
t.literal('open'),
t.literal('closed'),
t.literal('in-progress'), // TODO: remove after migration to acknowledged
t.literal('acknowledged'),
]),
index: t.string,
ids: t.array(t.string),
query: t.undefined,
}),
t.strict({
status: t.union([t.literal('open'), t.literal('closed')]),
status: t.union([
t.literal('open'),
t.literal('closed'),
t.literal('in-progress'), // TODO: remove after migration to acknowledged
t.literal('acknowledged'),
]),
index: t.string,
ids: t.undefined,
query: t.string,
query: t.union([t.object, t.string]),
}),
])
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
timelineId,
}) => {
const [isPopoverOpen, setPopover] = useState(false);

const ruleId = get(0, ecsRowData?.signal?.rule?.id);
const ruleName = get(0, ecsRowData?.signal?.rule?.name);

Expand Down Expand Up @@ -116,6 +115,7 @@ const AlertContextMenuComponent: React.FC<AlertContextMenuProps> = ({
const { actionItems } = useAlertsActions({
alertStatus,
eventId: ecsRowData?._id,
indexName: ecsRowData?._index ?? '',
timelineId,
closePopover,
});
Expand Down
Loading

0 comments on commit 70b6098

Please sign in to comment.