mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 09:48:58 -04:00
[RAC] [RBAC] Adds bulk update route to rule registry and bulk update function to alerts client (#106297)
Adds a bulk update route (POST /internal/rac/alerts/bulk_update) to the rule registry and bulkUpdate function to the alerts as data client.
This commit is contained in:
parent
bc171418d2
commit
ab43afab88
33 changed files with 1903 additions and 321 deletions
|
@ -22,8 +22,10 @@ NPM_MODULE_EXTRA_FILES = [
|
|||
]
|
||||
|
||||
SRC_DEPS = [
|
||||
"//packages/kbn-es-query",
|
||||
"@npm//tslib",
|
||||
"@npm//utility-types",
|
||||
"@npm//@elastic/elasticsearch",
|
||||
]
|
||||
|
||||
TYPES_DEPS = [
|
||||
|
|
|
@ -6,6 +6,9 @@
|
|||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { estypes } from '@elastic/elasticsearch';
|
||||
import type { EsQueryConfig } from '@kbn/es-query';
|
||||
|
||||
/**
|
||||
* registering a new instance of the rule data client
|
||||
* in a new plugin will require updating the below data structure
|
||||
|
@ -24,6 +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 const mapConsumerToIndexName: Record<AlertConsumers, string | string[]> = {
|
||||
apm: '.alerts-observability-apm',
|
||||
|
@ -38,3 +42,57 @@ export type ValidFeatureId = keyof typeof mapConsumerToIndexName;
|
|||
export const validFeatureIds = Object.keys(mapConsumerToIndexName);
|
||||
export const isValidFeatureId = (a: unknown): a is ValidFeatureId =>
|
||||
typeof a === 'string' && validFeatureIds.includes(a);
|
||||
|
||||
/**
|
||||
* Prevent javascript from returning Number.MAX_SAFE_INTEGER when Elasticsearch expects
|
||||
* Java's Long.MAX_VALUE. This happens when sorting fields by date which are
|
||||
* unmapped in the provided index
|
||||
*
|
||||
* Ref: https://github.com/elastic/elasticsearch/issues/28806#issuecomment-369303620
|
||||
*
|
||||
* return stringified Long.MAX_VALUE if we receive Number.MAX_SAFE_INTEGER
|
||||
* @param sortIds estypes.SearchSortResults | undefined
|
||||
* @returns SortResults
|
||||
*/
|
||||
export const getSafeSortIds = (sortIds: estypes.SearchSortResults | null | undefined) => {
|
||||
if (sortIds == null) {
|
||||
return sortIds;
|
||||
}
|
||||
return sortIds.map((sortId) => {
|
||||
// haven't determined when we would receive a null value for a sort id
|
||||
// but in case we do, default to sending the stringified Java max_int
|
||||
if (sortId == null || sortId === '' || sortId >= Number.MAX_SAFE_INTEGER) {
|
||||
return '9223372036854775807';
|
||||
}
|
||||
return sortId;
|
||||
});
|
||||
};
|
||||
|
||||
interface GetEsQueryConfigParamType {
|
||||
allowLeadingWildcards?: EsQueryConfig['allowLeadingWildcards'];
|
||||
queryStringOptions?: EsQueryConfig['queryStringOptions'];
|
||||
ignoreFilterIfFieldNotInIndex?: EsQueryConfig['ignoreFilterIfFieldNotInIndex'];
|
||||
dateFormatTZ?: EsQueryConfig['dateFormatTZ'];
|
||||
}
|
||||
|
||||
type ConfigKeys = keyof GetEsQueryConfigParamType;
|
||||
|
||||
export const getEsQueryConfig = (params?: GetEsQueryConfigParamType): EsQueryConfig => {
|
||||
const defaultConfigValues = {
|
||||
allowLeadingWildcards: true,
|
||||
queryStringOptions: { analyze_wildcard: true },
|
||||
ignoreFilterIfFieldNotInIndex: false,
|
||||
dateFormatTZ: 'Zulu',
|
||||
};
|
||||
if (params == null) {
|
||||
return defaultConfigValues;
|
||||
}
|
||||
const paramKeysWithValues = Object.keys(params).reduce((acc: EsQueryConfig, key) => {
|
||||
const configKey = key as ConfigKeys;
|
||||
if (params[configKey] != null) {
|
||||
return { [key]: params[configKey], ...acc };
|
||||
}
|
||||
return { [key]: defaultConfigValues[configKey], ...acc };
|
||||
}, {} as EsQueryConfig);
|
||||
return paramKeysWithValues;
|
||||
};
|
||||
|
|
|
@ -15,6 +15,7 @@ const createAlertingAuthorizationMock = () => {
|
|||
const mocked: AlertingAuthorizationMock = {
|
||||
ensureAuthorized: jest.fn(),
|
||||
filterByRuleTypeAuthorization: jest.fn(),
|
||||
getAuthorizationFilter: jest.fn(),
|
||||
getFindAuthorizationFilter: jest.fn(),
|
||||
getAugmentedRuleTypesWithAuthorization: jest.fn(),
|
||||
getSpaceId: jest.fn(),
|
||||
|
|
|
@ -281,11 +281,23 @@ export class AlertingAuthorization {
|
|||
filter?: KueryNode | JsonObject;
|
||||
ensureRuleTypeIsAuthorized: (ruleTypeId: string, consumer: string, auth: string) => void;
|
||||
logSuccessfulAuthorization: () => void;
|
||||
}> {
|
||||
return this.getAuthorizationFilter(authorizationEntity, filterOpts, ReadOperations.Find);
|
||||
}
|
||||
|
||||
public async getAuthorizationFilter(
|
||||
authorizationEntity: AlertingAuthorizationEntity,
|
||||
filterOpts: AlertingAuthorizationFilterOpts,
|
||||
operation: WriteOperations | ReadOperations
|
||||
): Promise<{
|
||||
filter?: KueryNode | JsonObject;
|
||||
ensureRuleTypeIsAuthorized: (ruleTypeId: string, consumer: string, auth: string) => void;
|
||||
logSuccessfulAuthorization: () => void;
|
||||
}> {
|
||||
if (this.authorization && this.shouldCheckAuthorization()) {
|
||||
const { username, authorizedRuleTypes } = await this.augmentRuleTypesWithAuthorization(
|
||||
this.ruleTypeRegistry.list(),
|
||||
[ReadOperations.Find],
|
||||
[operation],
|
||||
authorizationEntity
|
||||
);
|
||||
|
||||
|
|
|
@ -41,6 +41,7 @@ const alert: Alert = {
|
|||
'rule.name': ['Latency threshold | frontend-rum'],
|
||||
[ALERT_DURATION]: [62879000],
|
||||
[ALERT_STATUS]: ['open'],
|
||||
[SPACE_IDS]: ['myfakespaceid'],
|
||||
tags: ['apm', 'service.name:frontend-rum'],
|
||||
'transaction.type': ['page-load'],
|
||||
[ALERT_PRODUCER]: ['apm'],
|
||||
|
|
|
@ -10,5 +10,6 @@ Alerts as data client API Interface
|
|||
|
||||
### Interfaces
|
||||
|
||||
- [BulkUpdateOptions](interfaces/bulkupdateoptions.md)
|
||||
- [ConstructorOptions](interfaces/constructoroptions.md)
|
||||
- [UpdateOptions](interfaces/updateoptions.md)
|
||||
|
|
|
@ -22,10 +22,14 @@ on alerts as data.
|
|||
|
||||
### Methods
|
||||
|
||||
- [fetchAlert](alertsclient.md#fetchalert)
|
||||
- [buildEsQueryWithAuthz](alertsclient.md#buildesquerywithauthz)
|
||||
- [bulkUpdate](alertsclient.md#bulkupdate)
|
||||
- [ensureAllAuthorized](alertsclient.md#ensureallauthorized)
|
||||
- [get](alertsclient.md#get)
|
||||
- [getAlertsIndex](alertsclient.md#getalertsindex)
|
||||
- [getAuthorizedAlertsIndices](alertsclient.md#getauthorizedalertsindices)
|
||||
- [mgetAlertsAuditOperate](alertsclient.md#mgetalertsauditoperate)
|
||||
- [queryAndAuditAllAlerts](alertsclient.md#queryandauditallalerts)
|
||||
- [singleSearchAfterAndAudit](alertsclient.md#singlesearchafterandaudit)
|
||||
- [update](alertsclient.md#update)
|
||||
|
||||
## Constructors
|
||||
|
@ -42,7 +46,7 @@ on alerts as data.
|
|||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:66](https://github.com/elastic/kibana/blob/48e1b91d751/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L66)
|
||||
[alerts_client.ts:93](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L93)
|
||||
|
||||
## Properties
|
||||
|
||||
|
@ -52,7 +56,7 @@ on alerts as data.
|
|||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:63](https://github.com/elastic/kibana/blob/48e1b91d751/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L63)
|
||||
[alerts_client.ts:90](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L90)
|
||||
|
||||
___
|
||||
|
||||
|
@ -62,7 +66,7 @@ ___
|
|||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:64](https://github.com/elastic/kibana/blob/48e1b91d751/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L64)
|
||||
[alerts_client.ts:91](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L91)
|
||||
|
||||
___
|
||||
|
||||
|
@ -72,7 +76,7 @@ ___
|
|||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:65](https://github.com/elastic/kibana/blob/48e1b91d751/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L65)
|
||||
[alerts_client.ts:92](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L92)
|
||||
|
||||
___
|
||||
|
||||
|
@ -82,70 +86,33 @@ ___
|
|||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:62](https://github.com/elastic/kibana/blob/48e1b91d751/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L62)
|
||||
[alerts_client.ts:89](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L89)
|
||||
|
||||
___
|
||||
|
||||
### spaceId
|
||||
|
||||
• `Private` `Readonly` **spaceId**: `Promise`<undefined \| string\>
|
||||
• `Private` `Readonly` **spaceId**: `undefined` \| `string`
|
||||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:66](https://github.com/elastic/kibana/blob/48e1b91d751/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L66)
|
||||
[alerts_client.ts:93](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L93)
|
||||
|
||||
## Methods
|
||||
|
||||
### fetchAlert
|
||||
### buildEsQueryWithAuthz
|
||||
|
||||
▸ `Private` **fetchAlert**(`__namedParameters`): `Promise`<undefined \| ``null`` \| `Omit`<OutputOf<SetOptional<`Object`\>\>, ``"kibana.alert.owner"`` \| ``"rule.id"``\> & { `kibana.alert.owner`: `string` ; `rule.id`: `string` } & { `_version`: `undefined` \| `string` }\>
|
||||
▸ `Private` **buildEsQueryWithAuthz**(`query`, `id`, `alertSpaceId`, `operation`, `config`): `Promise`<`Object`\>
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type |
|
||||
| :------ | :------ |
|
||||
| `__namedParameters` | `GetAlertParams` |
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`<undefined \| ``null`` \| `Omit`<OutputOf<SetOptional<`Object`\>\>, ``"kibana.alert.owner"`` \| ``"rule.id"``\> & { `kibana.alert.owner`: `string` ; `rule.id`: `string` } & { `_version`: `undefined` \| `string` }\>
|
||||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:87](https://github.com/elastic/kibana/blob/48e1b91d751/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L87)
|
||||
|
||||
___
|
||||
|
||||
### get
|
||||
|
||||
▸ **get**(`__namedParameters`): `Promise`<undefined \| ``null`` \| OutputOf<SetOptional<`Object`\>\>\>
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type |
|
||||
| :------ | :------ |
|
||||
| `__namedParameters` | `GetAlertParams` |
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`<undefined \| ``null`` \| OutputOf<SetOptional<`Object`\>\>\>
|
||||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:134](https://github.com/elastic/kibana/blob/48e1b91d751/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L134)
|
||||
|
||||
___
|
||||
|
||||
### getAlertsIndex
|
||||
|
||||
▸ **getAlertsIndex**(`featureIds`, `operations`): `Promise`<`Object`\>
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type |
|
||||
| :------ | :------ |
|
||||
| `featureIds` | `string`[] |
|
||||
| `operations` | (`ReadOperations` \| `WriteOperations`)[] |
|
||||
| `query` | `undefined` \| ``null`` \| `string` |
|
||||
| `id` | `undefined` \| ``null`` \| `string` |
|
||||
| `alertSpaceId` | `string` |
|
||||
| `operation` | `Get` \| `Find` \| `Update` |
|
||||
| `config` | `EsQueryConfig` |
|
||||
|
||||
#### Returns
|
||||
|
||||
|
@ -153,7 +120,76 @@ ___
|
|||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:76](https://github.com/elastic/kibana/blob/48e1b91d751/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L76)
|
||||
[alerts_client.ts:305](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L305)
|
||||
|
||||
___
|
||||
|
||||
### bulkUpdate
|
||||
|
||||
▸ **bulkUpdate**<Params\>(`__namedParameters`): `Promise`<ApiResponse<BulkResponse, unknown\> \| ApiResponse<UpdateByQueryResponse, unknown\>\>
|
||||
|
||||
#### Type parameters
|
||||
|
||||
| Name | Type |
|
||||
| :------ | :------ |
|
||||
| `Params` | `Params`: `AlertTypeParams` = `never` |
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type |
|
||||
| :------ | :------ |
|
||||
| `__namedParameters` | [BulkUpdateOptions](../interfaces/bulkupdateoptions.md)<Params\> |
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`<ApiResponse<BulkResponse, unknown\> \| ApiResponse<UpdateByQueryResponse, unknown\>\>
|
||||
|
||||
#### Defined in
|
||||
|
||||
[alerts_client.ts:475](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L475)
|
||||
|
||||
___
|
||||
|
||||
### ensureAllAuthorized
|
||||
|
||||
▸ `Private` **ensureAllAuthorized**(`items`, `operation`): `Promise`<(undefined \| void)[]\>
|
||||
|
||||
Accepts an array of ES documents and executes ensureAuthorized for the given operation
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type |
|
||||
| :------ | :------ |
|
||||
| `items` | { `_id`: `string` ; `_source?`: ``null`` \| { `kibana.alert.owner?`: ``null`` \| `string` ; `rule.id?`: ``null`` \| `string` } }[] |
|
||||
| `operation` | `Get` \| `Find` \| `Update` |
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`<(undefined \| void)[]\>
|
||||
|
||||
#### Defined in
|
||||
|
||||
[alerts_client.ts:111](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L111)
|
||||
|
||||
___
|
||||
|
||||
### get
|
||||
|
||||
▸ **get**(`__namedParameters`): `Promise`<undefined \| OutputOf<SetOptional<`Object`\>\>\>
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type |
|
||||
| :------ | :------ |
|
||||
| `__namedParameters` | `GetAlertParams` |
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`<undefined \| OutputOf<SetOptional<`Object`\>\>\>
|
||||
|
||||
#### Defined in
|
||||
|
||||
[alerts_client.ts:407](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L407)
|
||||
|
||||
___
|
||||
|
||||
|
@ -173,13 +209,87 @@ ___
|
|||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:238](https://github.com/elastic/kibana/blob/48e1b91d751/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L238)
|
||||
[alerts_client.ts:533](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L533)
|
||||
|
||||
___
|
||||
|
||||
### mgetAlertsAuditOperate
|
||||
|
||||
▸ `Private` **mgetAlertsAuditOperate**(`__namedParameters`): `Promise`<ApiResponse<BulkResponse, unknown\>\>
|
||||
|
||||
When an update by ids is requested, do a multi-get, ensure authz and audit alerts, then execute bulk update
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type |
|
||||
| :------ | :------ |
|
||||
| `__namedParameters` | `Object` |
|
||||
| `__namedParameters.ids` | `string`[] |
|
||||
| `__namedParameters.indexName` | `string` |
|
||||
| `__namedParameters.operation` | `Get` \| `Find` \| `Update` |
|
||||
| `__namedParameters.status` | `STATUS\_VALUES` |
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`<ApiResponse<BulkResponse, unknown\>\>
|
||||
|
||||
#### Defined in
|
||||
|
||||
[alerts_client.ts:252](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L252)
|
||||
|
||||
___
|
||||
|
||||
### queryAndAuditAllAlerts
|
||||
|
||||
▸ `Private` **queryAndAuditAllAlerts**(`__namedParameters`): `Promise`<undefined \| { `auditedAlerts`: `boolean` = true; `authorizedQuery`: {} }\>
|
||||
|
||||
executes a search after to find alerts with query (+ authz filter)
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type |
|
||||
| :------ | :------ |
|
||||
| `__namedParameters` | `Object` |
|
||||
| `__namedParameters.index` | `string` |
|
||||
| `__namedParameters.operation` | `Get` \| `Find` \| `Update` |
|
||||
| `__namedParameters.query` | `string` |
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`<undefined \| { `auditedAlerts`: `boolean` = true; `authorizedQuery`: {} }\>
|
||||
|
||||
#### Defined in
|
||||
|
||||
[alerts_client.ts:343](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L343)
|
||||
|
||||
___
|
||||
|
||||
### singleSearchAfterAndAudit
|
||||
|
||||
▸ `Private` **singleSearchAfterAndAudit**(`__namedParameters`): `Promise`<SearchResponse<OutputOf<SetOptional<`Object`\>\>\>\>
|
||||
|
||||
This will be used as a part of the "find" api
|
||||
In the future we will add an "aggs" param
|
||||
|
||||
#### Parameters
|
||||
|
||||
| Name | Type |
|
||||
| :------ | :------ |
|
||||
| `__namedParameters` | `SingleSearchAfterAndAudit` |
|
||||
|
||||
#### Returns
|
||||
|
||||
`Promise`<SearchResponse<OutputOf<SetOptional<`Object`\>\>\>\>
|
||||
|
||||
#### Defined in
|
||||
|
||||
[alerts_client.ts:176](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L176)
|
||||
|
||||
___
|
||||
|
||||
### update
|
||||
|
||||
▸ **update**<Params\>(`__namedParameters`): `Promise`<undefined \| { `_version`: `undefined` \| `string` }\>
|
||||
▸ **update**<Params\>(`__namedParameters`): `Promise`<`Object`\>
|
||||
|
||||
#### Type parameters
|
||||
|
||||
|
@ -195,8 +305,8 @@ ___
|
|||
|
||||
#### Returns
|
||||
|
||||
`Promise`<undefined \| { `_version`: `undefined` \| `string` }\>
|
||||
`Promise`<`Object`\>
|
||||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:179](https://github.com/elastic/kibana/blob/48e1b91d751/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L179)
|
||||
[alerts_client.ts:432](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L432)
|
||||
|
|
|
@ -0,0 +1,58 @@
|
|||
[Alerts as data client API Interface](../alerts_client_api.md) / BulkUpdateOptions
|
||||
|
||||
# Interface: BulkUpdateOptions<Params\>
|
||||
|
||||
## Type parameters
|
||||
|
||||
| Name | Type |
|
||||
| :------ | :------ |
|
||||
| `Params` | `Params`: `AlertTypeParams` |
|
||||
|
||||
## Table of contents
|
||||
|
||||
### Properties
|
||||
|
||||
- [ids](bulkupdateoptions.md#ids)
|
||||
- [index](bulkupdateoptions.md#index)
|
||||
- [query](bulkupdateoptions.md#query)
|
||||
- [status](bulkupdateoptions.md#status)
|
||||
|
||||
## Properties
|
||||
|
||||
### ids
|
||||
|
||||
• **ids**: `undefined` \| ``null`` \| `string`[]
|
||||
|
||||
#### Defined in
|
||||
|
||||
[alerts_client.ts:64](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L64)
|
||||
|
||||
___
|
||||
|
||||
### index
|
||||
|
||||
• **index**: `string`
|
||||
|
||||
#### Defined in
|
||||
|
||||
[alerts_client.ts:66](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L66)
|
||||
|
||||
___
|
||||
|
||||
### query
|
||||
|
||||
• **query**: `undefined` \| ``null`` \| `string`
|
||||
|
||||
#### Defined in
|
||||
|
||||
[alerts_client.ts:67](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L67)
|
||||
|
||||
___
|
||||
|
||||
### status
|
||||
|
||||
• **status**: `STATUS\_VALUES`
|
||||
|
||||
#### Defined in
|
||||
|
||||
[alerts_client.ts:65](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L65)
|
|
@ -19,7 +19,7 @@
|
|||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:40](https://github.com/elastic/kibana/blob/48e1b91d751/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L40)
|
||||
[alerts_client.ts:52](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L52)
|
||||
|
||||
___
|
||||
|
||||
|
@ -29,7 +29,7 @@ ___
|
|||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:39](https://github.com/elastic/kibana/blob/48e1b91d751/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L39)
|
||||
[alerts_client.ts:51](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L51)
|
||||
|
||||
___
|
||||
|
||||
|
@ -39,7 +39,7 @@ ___
|
|||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:41](https://github.com/elastic/kibana/blob/48e1b91d751/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L41)
|
||||
[alerts_client.ts:53](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L53)
|
||||
|
||||
___
|
||||
|
||||
|
@ -49,4 +49,4 @@ ___
|
|||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:38](https://github.com/elastic/kibana/blob/48e1b91d751/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L38)
|
||||
[alerts_client.ts:50](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L50)
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:47](https://github.com/elastic/kibana/blob/48e1b91d751/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L47)
|
||||
[alerts_client.ts:59](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L59)
|
||||
|
||||
___
|
||||
|
||||
|
@ -35,7 +35,7 @@ ___
|
|||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:45](https://github.com/elastic/kibana/blob/48e1b91d751/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L45)
|
||||
[alerts_client.ts:57](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L57)
|
||||
|
||||
___
|
||||
|
||||
|
@ -45,7 +45,7 @@ ___
|
|||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:48](https://github.com/elastic/kibana/blob/48e1b91d751/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L48)
|
||||
[alerts_client.ts:60](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L60)
|
||||
|
||||
___
|
||||
|
||||
|
@ -55,4 +55,4 @@ ___
|
|||
|
||||
#### Defined in
|
||||
|
||||
[rule_registry/server/alert_data_client/alerts_client.ts:46](https://github.com/elastic/kibana/blob/48e1b91d751/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L46)
|
||||
[alerts_client.ts:58](https://github.com/elastic/kibana/blob/daf6871ba4b/x-pack/plugins/rule_registry/server/alert_data_client/alerts_client.ts#L58)
|
||||
|
|
|
@ -14,9 +14,9 @@ export type AlertsClientMock = jest.Mocked<Schema>;
|
|||
const createAlertsClientMock = () => {
|
||||
const mocked: AlertsClientMock = {
|
||||
get: jest.fn(),
|
||||
getAlertsIndex: jest.fn(),
|
||||
update: jest.fn(),
|
||||
getAuthorizedAlertsIndices: jest.fn(),
|
||||
bulkUpdate: jest.fn(),
|
||||
};
|
||||
return mocked;
|
||||
};
|
||||
|
|
|
@ -4,23 +4,28 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import Boom from '@hapi/boom';
|
||||
import { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import { Filter, buildEsQuery, EsQueryConfig } from '@kbn/es-query';
|
||||
import { decodeVersion, encodeHitVersion } from '@kbn/securitysolution-es-utils';
|
||||
import {
|
||||
mapConsumerToIndexName,
|
||||
validFeatureIds,
|
||||
isValidFeatureId,
|
||||
getSafeSortIds,
|
||||
STATUS_VALUES,
|
||||
getEsQueryConfig,
|
||||
} from '@kbn/rule-data-utils/target/alerts_as_data_rbac';
|
||||
|
||||
import { AlertTypeParams } from '../../../alerting/server';
|
||||
import { InlineScript, QueryDslQueryContainer } from '@elastic/elasticsearch/api/types';
|
||||
import { AlertTypeParams, AlertingAuthorizationFilterType } from '../../../alerting/server';
|
||||
import {
|
||||
ReadOperations,
|
||||
AlertingAuthorization,
|
||||
WriteOperations,
|
||||
AlertingAuthorizationEntity,
|
||||
} from '../../../alerting/server';
|
||||
import { Logger, ElasticsearchClient } from '../../../../../src/core/server';
|
||||
import { alertAuditEvent, AlertAuditAction } from './audit_events';
|
||||
import { Logger, ElasticsearchClient, EcsEventOutcome } from '../../../../../src/core/server';
|
||||
import { alertAuditEvent, operationAlertAuditActionMap } from './audit_events';
|
||||
import { AuditLogger } from '../../../security/server';
|
||||
import {
|
||||
ALERT_STATUS,
|
||||
|
@ -33,10 +38,13 @@ import { ParsedTechnicalFields } from '../../common/parse_technical_fields';
|
|||
// 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<ParsedTechnicalFields, 'rule.id' | 'kibana.alert.owner'>;
|
||||
type AlertType = NonNullableProps<
|
||||
ParsedTechnicalFields,
|
||||
typeof RULE_ID | typeof ALERT_OWNER | typeof SPACE_IDS
|
||||
>;
|
||||
|
||||
const isValidAlert = (source?: ParsedTechnicalFields): source is AlertType => {
|
||||
return source?.[RULE_ID] != null && source?.[ALERT_OWNER] != null;
|
||||
return source?.[RULE_ID] != null && source?.[ALERT_OWNER] != null && source?.[SPACE_IDS] != null;
|
||||
};
|
||||
export interface ConstructorOptions {
|
||||
logger: Logger;
|
||||
|
@ -52,11 +60,26 @@ export interface UpdateOptions<Params extends AlertTypeParams> {
|
|||
index: string;
|
||||
}
|
||||
|
||||
export interface BulkUpdateOptions<Params extends AlertTypeParams> {
|
||||
ids: string[] | undefined | null;
|
||||
status: STATUS_VALUES;
|
||||
index: string;
|
||||
query: string | undefined | null;
|
||||
}
|
||||
|
||||
interface GetAlertParams {
|
||||
id: string;
|
||||
index?: string;
|
||||
}
|
||||
|
||||
interface SingleSearchAfterAndAudit {
|
||||
id: string | null | undefined;
|
||||
query: string | null | undefined;
|
||||
index?: string;
|
||||
operation: WriteOperations.Update | ReadOperations.Find | ReadOperations.Get;
|
||||
lastSortIds: Array<string | number> | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides apis to interact with alerts as data
|
||||
* ensures the request is authorized to perform read / write actions
|
||||
|
@ -79,105 +102,337 @@ export class AlertsClient {
|
|||
this.spaceId = this.authorization.getSpaceId();
|
||||
}
|
||||
|
||||
public async getAlertsIndex(
|
||||
featureIds: string[],
|
||||
operations: Array<ReadOperations | WriteOperations>
|
||||
) {
|
||||
return this.authorization.getAugmentedRuleTypesWithAuthorization(
|
||||
featureIds.length !== 0 ? featureIds : validFeatureIds,
|
||||
operations,
|
||||
AlertingAuthorizationEntity.Alert
|
||||
);
|
||||
private getOutcome(
|
||||
operation: WriteOperations.Update | ReadOperations.Find | ReadOperations.Get
|
||||
): { outcome: EcsEventOutcome } {
|
||||
return {
|
||||
outcome: operation === WriteOperations.Update ? 'unknown' : 'success',
|
||||
};
|
||||
}
|
||||
|
||||
private async fetchAlert({
|
||||
/**
|
||||
* Accepts an array of ES documents and executes ensureAuthorized for the given operation
|
||||
* @param items
|
||||
* @param operation
|
||||
* @returns
|
||||
*/
|
||||
private async ensureAllAuthorized(
|
||||
items: Array<{
|
||||
_id: string;
|
||||
// this is typed kind of crazy to fit the output of es api response to this
|
||||
_source?:
|
||||
| { [RULE_ID]?: string | null | undefined; [ALERT_OWNER]?: string | null | undefined }
|
||||
| null
|
||||
| undefined;
|
||||
}>,
|
||||
operation: ReadOperations.Find | ReadOperations.Get | WriteOperations.Update
|
||||
) {
|
||||
const { hitIds, ownersAndRuleTypeIds } = items.reduce(
|
||||
(acc, hit) => ({
|
||||
hitIds: [hit._id, ...acc.hitIds],
|
||||
ownersAndRuleTypeIds: [
|
||||
{
|
||||
[RULE_ID]: hit?._source?.[RULE_ID],
|
||||
[ALERT_OWNER]: hit?._source?.[ALERT_OWNER],
|
||||
},
|
||||
],
|
||||
}),
|
||||
{ hitIds: [], ownersAndRuleTypeIds: [] } as {
|
||||
hitIds: string[];
|
||||
ownersAndRuleTypeIds: Array<{
|
||||
[RULE_ID]: string | null | undefined;
|
||||
[ALERT_OWNER]: string | null | undefined;
|
||||
}>;
|
||||
}
|
||||
);
|
||||
|
||||
const assertString = (hit: unknown): hit is string => hit !== null && hit !== undefined;
|
||||
|
||||
return Promise.all(
|
||||
ownersAndRuleTypeIds.map((hit) => {
|
||||
const alertOwner = hit?.[ALERT_OWNER];
|
||||
const ruleId = hit?.[RULE_ID];
|
||||
if (hit != null && assertString(alertOwner) && assertString(ruleId)) {
|
||||
return this.authorization.ensureAuthorized({
|
||||
ruleTypeId: ruleId,
|
||||
consumer: alertOwner,
|
||||
operation,
|
||||
entity: AlertingAuthorizationEntity.Alert,
|
||||
});
|
||||
}
|
||||
})
|
||||
).catch((error) => {
|
||||
for (const hitId of hitIds) {
|
||||
this.auditLogger?.log(
|
||||
alertAuditEvent({
|
||||
action: operationAlertAuditActionMap[operation],
|
||||
id: hitId,
|
||||
error,
|
||||
})
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This will be used as a part of the "find" api
|
||||
* In the future we will add an "aggs" param
|
||||
* @param param0
|
||||
* @returns
|
||||
*/
|
||||
private async singleSearchAfterAndAudit({
|
||||
id,
|
||||
query,
|
||||
index,
|
||||
}: GetAlertParams): Promise<(AlertType & { _version: string | undefined }) | null | undefined> {
|
||||
operation,
|
||||
lastSortIds = [],
|
||||
}: SingleSearchAfterAndAudit) {
|
||||
try {
|
||||
const alertSpaceId = this.spaceId;
|
||||
if (alertSpaceId == null) {
|
||||
this.logger.error('Failed to acquire spaceId from authorization client');
|
||||
return;
|
||||
const errorMessage = 'Failed to acquire spaceId from authorization client';
|
||||
this.logger.error(`fetchAlertAndAudit threw an error: ${errorMessage}`);
|
||||
throw Boom.failedDependency(`fetchAlertAndAudit threw an error: ${errorMessage}`);
|
||||
}
|
||||
const result = await this.esClient.search<ParsedTechnicalFields>({
|
||||
// Context: Originally thought of always just searching `.alerts-*` but that could
|
||||
// result in a big performance hit. If the client already knows which index the alert
|
||||
// belongs to, passing in the index will speed things up
|
||||
index: index ?? '.alerts-*',
|
||||
ignore_unavailable: true,
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
filter: [{ term: { _id: id } }, { term: { [SPACE_IDS]: alertSpaceId } }],
|
||||
|
||||
const config = getEsQueryConfig();
|
||||
|
||||
let queryBody = {
|
||||
query: await this.buildEsQueryWithAuthz(query, id, alertSpaceId, operation, config),
|
||||
sort: [
|
||||
{
|
||||
'@timestamp': {
|
||||
order: 'asc',
|
||||
unmapped_type: 'date',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
if (lastSortIds.length > 0) {
|
||||
queryBody = {
|
||||
...queryBody,
|
||||
// @ts-expect-error
|
||||
search_after: lastSortIds,
|
||||
};
|
||||
}
|
||||
|
||||
const result = await this.esClient.search<ParsedTechnicalFields>({
|
||||
index: index ?? '.alerts-*',
|
||||
ignore_unavailable: true,
|
||||
// @ts-expect-error
|
||||
body: queryBody,
|
||||
seq_no_primary_term: true,
|
||||
});
|
||||
|
||||
if (result == null || result.body == null || result.body.hits.hits.length === 0) {
|
||||
return;
|
||||
if (!result?.body.hits.hits.every((hit) => isValidAlert(hit._source))) {
|
||||
const errorMessage = `Invalid alert found with id of "${id}" or with query "${query}" and operation ${operation}`;
|
||||
this.logger.error(errorMessage);
|
||||
throw Boom.badData(errorMessage);
|
||||
}
|
||||
|
||||
if (!isValidAlert(result.body.hits.hits[0]._source)) {
|
||||
const errorMessage = `Unable to retrieve alert details for alert with id of "${id}".`;
|
||||
this.logger.debug(errorMessage);
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
await this.ensureAllAuthorized(result.body.hits.hits, operation);
|
||||
|
||||
return {
|
||||
...result.body.hits.hits[0]._source,
|
||||
_version: encodeHitVersion(result.body.hits.hits[0]),
|
||||
};
|
||||
result?.body.hits.hits.map((item) =>
|
||||
this.auditLogger?.log(
|
||||
alertAuditEvent({
|
||||
action: operationAlertAuditActionMap[operation],
|
||||
id: item._id,
|
||||
...this.getOutcome(operation),
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
return result.body;
|
||||
} catch (error) {
|
||||
const errorMessage = `Unable to retrieve alert with id of "${id}".`;
|
||||
this.logger.debug(errorMessage);
|
||||
throw error;
|
||||
const errorMessage = `Unable to retrieve alert details for alert with id of "${id}" or with query "${query}" and operation ${operation} \nError: ${error}`;
|
||||
this.logger.error(errorMessage);
|
||||
throw Boom.notFound(errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
public async get({
|
||||
id,
|
||||
index,
|
||||
}: GetAlertParams): Promise<ParsedTechnicalFields | null | undefined> {
|
||||
/**
|
||||
* When an update by ids is requested, do a multi-get, ensure authz and audit alerts, then execute bulk update
|
||||
* @param param0
|
||||
* @returns
|
||||
*/
|
||||
private async mgetAlertsAuditOperate({
|
||||
ids,
|
||||
status,
|
||||
indexName,
|
||||
operation,
|
||||
}: {
|
||||
ids: string[];
|
||||
status: STATUS_VALUES;
|
||||
indexName: string;
|
||||
operation: ReadOperations.Find | ReadOperations.Get | WriteOperations.Update;
|
||||
}) {
|
||||
try {
|
||||
// first search for the alert by id, then use the alert info to check if user has access to it
|
||||
const alert = await this.fetchAlert({
|
||||
id,
|
||||
index,
|
||||
const mgetRes = await this.esClient.mget<ParsedTechnicalFields>({
|
||||
index: indexName,
|
||||
body: {
|
||||
ids,
|
||||
},
|
||||
});
|
||||
|
||||
if (alert == null) {
|
||||
return;
|
||||
await this.ensureAllAuthorized(mgetRes.body.docs, operation);
|
||||
|
||||
for (const id of ids) {
|
||||
this.auditLogger?.log(
|
||||
alertAuditEvent({
|
||||
action: operationAlertAuditActionMap[operation],
|
||||
id,
|
||||
...this.getOutcome(operation),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// this.authorization leverages the alerting plugin's authorization
|
||||
// client exposed to us for reuse
|
||||
await this.authorization.ensureAuthorized({
|
||||
ruleTypeId: alert[RULE_ID],
|
||||
consumer: alert[ALERT_OWNER],
|
||||
const bulkUpdateRequest = mgetRes.body.docs.flatMap((item) => [
|
||||
{
|
||||
update: {
|
||||
_index: item._index,
|
||||
_id: item._id,
|
||||
},
|
||||
},
|
||||
{
|
||||
doc: { [ALERT_STATUS]: status },
|
||||
},
|
||||
]);
|
||||
|
||||
const bulkUpdateResponse = await this.esClient.bulk({
|
||||
body: bulkUpdateRequest,
|
||||
});
|
||||
return bulkUpdateResponse;
|
||||
} catch (exc) {
|
||||
this.logger.error(`error in mgetAlertsAuditOperate ${exc}`);
|
||||
throw exc;
|
||||
}
|
||||
}
|
||||
|
||||
private async buildEsQueryWithAuthz(
|
||||
query: string | null | undefined,
|
||||
id: string | null | undefined,
|
||||
alertSpaceId: string,
|
||||
operation: WriteOperations.Update | ReadOperations.Get | ReadOperations.Find,
|
||||
config: EsQueryConfig
|
||||
) {
|
||||
try {
|
||||
const { filter: authzFilter } = await this.authorization.getAuthorizationFilter(
|
||||
AlertingAuthorizationEntity.Alert,
|
||||
{
|
||||
type: AlertingAuthorizationFilterType.ESDSL,
|
||||
fieldNames: { consumer: ALERT_OWNER, ruleTypeId: RULE_ID },
|
||||
},
|
||||
operation
|
||||
);
|
||||
return buildEsQuery(
|
||||
undefined,
|
||||
{ query: query == null ? `_id:${id}` : query, language: 'kuery' },
|
||||
[
|
||||
(authzFilter as unknown) as Filter,
|
||||
({ term: { [SPACE_IDS]: alertSpaceId } } as unknown) as Filter,
|
||||
],
|
||||
config
|
||||
);
|
||||
} catch (exc) {
|
||||
this.logger.error(exc);
|
||||
throw Boom.expectationFailed(
|
||||
`buildEsQueryWithAuthz threw an error: unable to get authorization filter \n ${exc}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* executes a search after to find alerts with query (+ authz filter)
|
||||
* @param param0
|
||||
* @returns
|
||||
*/
|
||||
private async queryAndAuditAllAlerts({
|
||||
index,
|
||||
query,
|
||||
operation,
|
||||
}: {
|
||||
index: string;
|
||||
query: string;
|
||||
operation: WriteOperations.Update | ReadOperations.Find | ReadOperations.Get;
|
||||
}) {
|
||||
let lastSortIds;
|
||||
let hasSortIds = true;
|
||||
const alertSpaceId = this.spaceId;
|
||||
if (alertSpaceId == null) {
|
||||
this.logger.error('Failed to acquire spaceId from authorization client');
|
||||
return;
|
||||
}
|
||||
|
||||
const config = getEsQueryConfig();
|
||||
|
||||
const authorizedQuery = await this.buildEsQueryWithAuthz(
|
||||
query,
|
||||
null,
|
||||
alertSpaceId,
|
||||
operation,
|
||||
config
|
||||
);
|
||||
|
||||
while (hasSortIds) {
|
||||
try {
|
||||
const result = await this.singleSearchAfterAndAudit({
|
||||
id: null,
|
||||
query,
|
||||
index,
|
||||
operation,
|
||||
lastSortIds,
|
||||
});
|
||||
|
||||
if (lastSortIds != null && result?.hits.hits.length === 0) {
|
||||
return { auditedAlerts: true, authorizedQuery };
|
||||
}
|
||||
if (result == null) {
|
||||
this.logger.error('RESULT WAS EMPTY');
|
||||
return { auditedAlerts: false, authorizedQuery };
|
||||
}
|
||||
if (result.hits.hits.length === 0) {
|
||||
this.logger.error('Search resulted in no hits');
|
||||
return { auditedAlerts: true, authorizedQuery };
|
||||
}
|
||||
|
||||
lastSortIds = getSafeSortIds(result.hits.hits[result.hits.hits.length - 1]?.sort);
|
||||
if (lastSortIds != null && lastSortIds.length !== 0) {
|
||||
hasSortIds = true;
|
||||
} else {
|
||||
hasSortIds = false;
|
||||
return { auditedAlerts: true, authorizedQuery };
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = `queryAndAuditAllAlerts threw an error: Unable to retrieve alerts with query "${query}" and operation ${operation} \n ${error}`;
|
||||
this.logger.error(errorMessage);
|
||||
throw Boom.notFound(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async get({ id, index }: GetAlertParams) {
|
||||
try {
|
||||
// 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,
|
||||
index,
|
||||
operation: ReadOperations.Get,
|
||||
entity: AlertingAuthorizationEntity.Alert,
|
||||
lastSortIds: undefined,
|
||||
});
|
||||
|
||||
this.auditLogger?.log(
|
||||
alertAuditEvent({
|
||||
action: AlertAuditAction.GET,
|
||||
id,
|
||||
})
|
||||
);
|
||||
if (alert == null || alert.hits.hits.length === 0) {
|
||||
const errorMessage = `Unable to retrieve alert details for alert with id of "${id}" and operation ${ReadOperations.Get}`;
|
||||
this.logger.error(errorMessage);
|
||||
throw Boom.notFound(errorMessage);
|
||||
}
|
||||
|
||||
return alert;
|
||||
// move away from pulling data from _source in the future
|
||||
return alert.hits.hits[0]._source;
|
||||
} catch (error) {
|
||||
this.logger.debug(`Error fetching alert with id of "${id}"`);
|
||||
this.auditLogger?.log(
|
||||
alertAuditEvent({
|
||||
action: AlertAuditAction.GET,
|
||||
id,
|
||||
error,
|
||||
})
|
||||
);
|
||||
this.logger.error(`get threw an error: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
@ -189,29 +444,19 @@ export class AlertsClient {
|
|||
index,
|
||||
}: UpdateOptions<Params>) {
|
||||
try {
|
||||
const alert = await this.fetchAlert({
|
||||
const alert = await this.singleSearchAfterAndAudit({
|
||||
id,
|
||||
query: null,
|
||||
index,
|
||||
});
|
||||
|
||||
if (alert == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.authorization.ensureAuthorized({
|
||||
ruleTypeId: alert[RULE_ID],
|
||||
consumer: alert[ALERT_OWNER],
|
||||
operation: WriteOperations.Update,
|
||||
entity: AlertingAuthorizationEntity.Alert,
|
||||
lastSortIds: undefined,
|
||||
});
|
||||
|
||||
this.auditLogger?.log(
|
||||
alertAuditEvent({
|
||||
action: AlertAuditAction.UPDATE,
|
||||
id,
|
||||
outcome: 'unknown',
|
||||
})
|
||||
);
|
||||
if (alert == null || alert.hits.hits.length === 0) {
|
||||
const errorMessage = `Unable to retrieve alert details for alert with id of "${id}" and operation ${ReadOperations.Get}`;
|
||||
this.logger.error(errorMessage);
|
||||
throw Boom.notFound(errorMessage);
|
||||
}
|
||||
|
||||
const { body: response } = await this.esClient.update<ParsedTechnicalFields>({
|
||||
...decodeVersion(_version),
|
||||
|
@ -230,39 +475,97 @@ export class AlertsClient {
|
|||
_version: encodeHitVersion(response),
|
||||
};
|
||||
} catch (error) {
|
||||
this.auditLogger?.log(
|
||||
alertAuditEvent({
|
||||
action: AlertAuditAction.UPDATE,
|
||||
id,
|
||||
error,
|
||||
})
|
||||
);
|
||||
this.logger.error(`update threw an error: ${error}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
public async getAuthorizedAlertsIndices(featureIds: string[]): Promise<string[] | undefined> {
|
||||
const augmentedRuleTypes = await this.authorization.getAugmentedRuleTypesWithAuthorization(
|
||||
featureIds,
|
||||
[ReadOperations.Find, ReadOperations.Get, WriteOperations.Update],
|
||||
AlertingAuthorizationEntity.Alert
|
||||
);
|
||||
public async bulkUpdate<Params extends AlertTypeParams = never>({
|
||||
ids,
|
||||
query,
|
||||
index,
|
||||
status,
|
||||
}: BulkUpdateOptions<Params>) {
|
||||
// rejects at the route level if more than 1000 id's are passed in
|
||||
if (ids != null) {
|
||||
return this.mgetAlertsAuditOperate({
|
||||
ids,
|
||||
status,
|
||||
indexName: index,
|
||||
operation: WriteOperations.Update,
|
||||
});
|
||||
} else if (query != null) {
|
||||
try {
|
||||
// execute search after with query + authorization filter
|
||||
// audit results of that query
|
||||
const fetchAndAuditResponse = await this.queryAndAuditAllAlerts({
|
||||
query,
|
||||
index,
|
||||
operation: WriteOperations.Update,
|
||||
});
|
||||
|
||||
// As long as the user can read a minimum of one type of rule type produced by the provided feature,
|
||||
// the user should be provided that features' alerts index.
|
||||
// Limiting which alerts that user can read on that index will be done via the findAuthorizationFilter
|
||||
const authorizedFeatures = new Set<string>();
|
||||
for (const ruleType of augmentedRuleTypes.authorizedRuleTypes) {
|
||||
authorizedFeatures.add(ruleType.producer);
|
||||
}
|
||||
if (!fetchAndAuditResponse?.auditedAlerts) {
|
||||
throw Boom.unauthorized('Failed to audit alerts');
|
||||
}
|
||||
|
||||
const toReturn = Array.from(authorizedFeatures).flatMap((feature) => {
|
||||
if (isValidFeatureId(feature)) {
|
||||
return mapConsumerToIndexName[feature];
|
||||
// executes updateByQuery with query + authorization filter
|
||||
// used in the queryAndAuditAllAlerts function
|
||||
const result = await this.esClient.updateByQuery({
|
||||
index,
|
||||
conflicts: 'proceed',
|
||||
refresh: true,
|
||||
body: {
|
||||
script: {
|
||||
source: `if (ctx._source['${ALERT_STATUS}'] != null) {
|
||||
ctx._source['${ALERT_STATUS}'] = '${status}'
|
||||
}
|
||||
if (ctx._source['signal.status'] != null) {
|
||||
ctx._source['signal.status'] = '${status}'
|
||||
}`,
|
||||
lang: 'painless',
|
||||
} as InlineScript,
|
||||
query: fetchAndAuditResponse.authorizedQuery as Omit<QueryDslQueryContainer, 'script'>,
|
||||
},
|
||||
ignore_unavailable: true,
|
||||
});
|
||||
return result;
|
||||
} catch (err) {
|
||||
this.logger.error(`bulkUpdate threw an error: ${err}`);
|
||||
throw err;
|
||||
}
|
||||
return [];
|
||||
});
|
||||
} else {
|
||||
throw Boom.badRequest('no ids or query were provided for updating');
|
||||
}
|
||||
}
|
||||
|
||||
return toReturn;
|
||||
public async getAuthorizedAlertsIndices(featureIds: string[]): Promise<string[] | undefined> {
|
||||
try {
|
||||
const augmentedRuleTypes = await this.authorization.getAugmentedRuleTypesWithAuthorization(
|
||||
featureIds,
|
||||
[ReadOperations.Find, ReadOperations.Get, WriteOperations.Update],
|
||||
AlertingAuthorizationEntity.Alert
|
||||
);
|
||||
|
||||
// As long as the user can read a minimum of one type of rule type produced by the provided feature,
|
||||
// the user should be provided that features' alerts index.
|
||||
// Limiting which alerts that user can read on that index will be done via the findAuthorizationFilter
|
||||
const authorizedFeatures = new Set<string>();
|
||||
for (const ruleType of augmentedRuleTypes.authorizedRuleTypes) {
|
||||
authorizedFeatures.add(ruleType.producer);
|
||||
}
|
||||
|
||||
const toReturn = Array.from(authorizedFeatures).flatMap((feature) => {
|
||||
if (isValidFeatureId(feature)) {
|
||||
return mapConsumerToIndexName[feature];
|
||||
}
|
||||
return [];
|
||||
});
|
||||
|
||||
return toReturn;
|
||||
} catch (exc) {
|
||||
const errMessage = `getAuthorizedAlertsIndices failed to get authorized rule types: ${exc}`;
|
||||
this.logger.error(errMessage);
|
||||
throw Boom.failedDependency(errMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { EcsEventOutcome, EcsEventType } from 'src/core/server';
|
||||
import { AuditEvent } from '../../../security/server';
|
||||
import { ReadOperations, WriteOperations } from '../../../alerting/server';
|
||||
|
||||
export enum AlertAuditAction {
|
||||
GET = 'alert_get',
|
||||
|
@ -14,6 +15,12 @@ export enum AlertAuditAction {
|
|||
FIND = 'alert_find',
|
||||
}
|
||||
|
||||
export const operationAlertAuditActionMap = {
|
||||
[WriteOperations.Update]: AlertAuditAction.UPDATE,
|
||||
[ReadOperations.Find]: AlertAuditAction.FIND,
|
||||
[ReadOperations.Get]: AlertAuditAction.GET,
|
||||
};
|
||||
|
||||
type VerbsTuple = [string, string, string];
|
||||
|
||||
const eventVerbs: Record<AlertAuditAction, VerbsTuple> = {
|
||||
|
|
|
@ -0,0 +1,458 @@
|
|||
/*
|
||||
* 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 { ALERT_OWNER, ALERT_STATUS, SPACE_IDS, RULE_ID } from '@kbn/rule-data-utils';
|
||||
import { AlertsClient, ConstructorOptions } from '../alerts_client';
|
||||
import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks';
|
||||
import { alertingAuthorizationMock } from '../../../../alerting/server/authorization/alerting_authorization.mock';
|
||||
import { AuditLogger } from '../../../../security/server';
|
||||
import { AlertingAuthorizationEntity } from '../../../../alerting/server';
|
||||
|
||||
const alertingAuthMock = alertingAuthorizationMock.create();
|
||||
const esClientMock = elasticsearchClientMock.createElasticsearchClient();
|
||||
const auditLogger = {
|
||||
log: jest.fn(),
|
||||
} as jest.Mocked<AuditLogger>;
|
||||
|
||||
const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
||||
logger: loggingSystemMock.create().get(),
|
||||
authorization: alertingAuthMock,
|
||||
esClient: esClientMock,
|
||||
auditLogger,
|
||||
};
|
||||
|
||||
const DEFAULT_SPACE = 'test_default_space_id';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
alertingAuthMock.getSpaceId.mockImplementation(() => 'test_default_space_id');
|
||||
// @ts-expect-error
|
||||
alertingAuthMock.getAuthorizationFilter.mockImplementation(async () =>
|
||||
Promise.resolve({ filter: [] })
|
||||
);
|
||||
alertingAuthMock.ensureAuthorized.mockImplementation(
|
||||
// @ts-expect-error
|
||||
async ({
|
||||
ruleTypeId,
|
||||
consumer,
|
||||
operation,
|
||||
entity,
|
||||
}: {
|
||||
ruleTypeId: string;
|
||||
consumer: string;
|
||||
operation: string;
|
||||
entity: typeof AlertingAuthorizationEntity.Alert;
|
||||
}) => {
|
||||
if (ruleTypeId === 'apm.error_rate' && consumer === 'apm') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error(`Unauthorized for ${ruleTypeId} and ${consumer}`));
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const fakeAlertId = 'myfakeid1';
|
||||
const successfulAuthzHit = 'successfulAuthzHit';
|
||||
const unsuccessfulAuthzHit = 'unsuccessfulAuthzHit';
|
||||
// fakeRuleTypeId will cause authz to fail
|
||||
const fakeRuleTypeId = 'fake.rule';
|
||||
|
||||
describe('bulkUpdate()', () => {
|
||||
describe('ids', () => {
|
||||
describe('audit log', () => {
|
||||
test('logs successful event in audit logger', async () => {
|
||||
const indexName = '.alerts-observability-apm.alerts';
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
esClientMock.mget.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createApiResponse({
|
||||
body: {
|
||||
docs: [
|
||||
{
|
||||
_id: fakeAlertId,
|
||||
_index: indexName,
|
||||
_source: {
|
||||
[RULE_ID]: 'apm.error_rate',
|
||||
[ALERT_OWNER]: 'apm',
|
||||
[ALERT_STATUS]: 'open',
|
||||
[SPACE_IDS]: [DEFAULT_SPACE],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
);
|
||||
esClientMock.bulk.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createApiResponse({
|
||||
body: {
|
||||
errors: false,
|
||||
took: 1,
|
||||
items: [
|
||||
{
|
||||
update: {
|
||||
_id: fakeAlertId,
|
||||
_index: '.alerts-observability-apm.alerts',
|
||||
result: 'updated',
|
||||
status: 200,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
);
|
||||
await alertsClient.bulkUpdate({
|
||||
ids: [fakeAlertId],
|
||||
query: undefined,
|
||||
index: indexName,
|
||||
status: 'closed',
|
||||
});
|
||||
expect(auditLogger.log).toHaveBeenNthCalledWith(1, {
|
||||
message: `User is updating alert [id=${fakeAlertId}]`,
|
||||
event: {
|
||||
action: 'alert_update',
|
||||
category: ['database'],
|
||||
outcome: 'unknown',
|
||||
type: ['change'],
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test('audit error access if user is unauthorized for given alert', async () => {
|
||||
const indexName = '.alerts-observability-apm.alerts';
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
esClientMock.mget.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createApiResponse({
|
||||
body: {
|
||||
docs: [
|
||||
{
|
||||
_id: fakeAlertId,
|
||||
_index: indexName,
|
||||
_source: {
|
||||
[RULE_ID]: fakeRuleTypeId,
|
||||
[ALERT_OWNER]: 'apm',
|
||||
[ALERT_STATUS]: 'open',
|
||||
[SPACE_IDS]: [DEFAULT_SPACE],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await expect(
|
||||
alertsClient.bulkUpdate({
|
||||
ids: [fakeAlertId],
|
||||
query: undefined,
|
||||
index: indexName,
|
||||
status: 'closed',
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"Unauthorized for fake.rule and apm"`);
|
||||
|
||||
expect(auditLogger.log).toHaveBeenNthCalledWith(1, {
|
||||
message: `Failed attempt to update alert [id=${fakeAlertId}]`,
|
||||
event: {
|
||||
action: 'alert_update',
|
||||
category: ['database'],
|
||||
outcome: 'failure',
|
||||
type: ['change'],
|
||||
},
|
||||
error: {
|
||||
code: 'Error',
|
||||
message: 'Unauthorized for fake.rule and apm',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('logs multiple error events in audit logger', async () => {
|
||||
const indexName = '.alerts-observability-apm.alerts';
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
esClientMock.mget.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createApiResponse({
|
||||
body: {
|
||||
docs: [
|
||||
{
|
||||
_id: successfulAuthzHit,
|
||||
_index: indexName,
|
||||
_source: {
|
||||
[RULE_ID]: 'apm.error_rate',
|
||||
[ALERT_OWNER]: 'apm',
|
||||
[ALERT_STATUS]: 'open',
|
||||
[SPACE_IDS]: [DEFAULT_SPACE],
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: unsuccessfulAuthzHit,
|
||||
_index: indexName,
|
||||
_source: {
|
||||
[RULE_ID]: fakeRuleTypeId,
|
||||
[ALERT_OWNER]: 'apm',
|
||||
[ALERT_STATUS]: 'open',
|
||||
[SPACE_IDS]: [DEFAULT_SPACE],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await expect(
|
||||
alertsClient.bulkUpdate({
|
||||
ids: [successfulAuthzHit, unsuccessfulAuthzHit],
|
||||
query: undefined,
|
||||
index: indexName,
|
||||
status: 'closed',
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"Unauthorized for fake.rule and apm"`);
|
||||
expect(auditLogger.log).toHaveBeenCalledTimes(2);
|
||||
expect(auditLogger.log).toHaveBeenNthCalledWith(1, {
|
||||
message: `Failed attempt to update alert [id=${unsuccessfulAuthzHit}]`,
|
||||
event: {
|
||||
action: 'alert_update',
|
||||
category: ['database'],
|
||||
outcome: 'failure',
|
||||
type: ['change'],
|
||||
},
|
||||
error: {
|
||||
code: 'Error',
|
||||
message: 'Unauthorized for fake.rule and apm',
|
||||
},
|
||||
});
|
||||
expect(auditLogger.log).toHaveBeenNthCalledWith(2, {
|
||||
message: `Failed attempt to update alert [id=${successfulAuthzHit}]`,
|
||||
event: {
|
||||
action: 'alert_update',
|
||||
category: ['database'],
|
||||
outcome: 'failure',
|
||||
type: ['change'],
|
||||
},
|
||||
error: {
|
||||
code: 'Error',
|
||||
message: 'Unauthorized for fake.rule and apm',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// test('throws an error if ES client fetch fails', async () => {});
|
||||
// test('throws an error if ES client bulk update fails', async () => {});
|
||||
// test('throws an error if ES client updateByQuery fails', async () => {});
|
||||
});
|
||||
describe('query', () => {
|
||||
describe('audit log', () => {
|
||||
test('logs successful event in audit logger', async () => {
|
||||
const indexName = '.alerts-observability-apm.alerts';
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
esClientMock.search.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createApiResponse({
|
||||
body: {
|
||||
took: 5,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
total: 1,
|
||||
successful: 1,
|
||||
failed: 0,
|
||||
skipped: 0,
|
||||
},
|
||||
hits: {
|
||||
total: 1,
|
||||
max_score: 999,
|
||||
hits: [
|
||||
{
|
||||
_id: fakeAlertId,
|
||||
_index: '.alerts-observability-apm.alerts',
|
||||
_source: {
|
||||
[RULE_ID]: 'apm.error_rate',
|
||||
[ALERT_OWNER]: 'apm',
|
||||
[ALERT_STATUS]: 'open',
|
||||
[SPACE_IDS]: [DEFAULT_SPACE],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
esClientMock.updateByQuery.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createApiResponse({
|
||||
body: {
|
||||
updated: 1,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await alertsClient.bulkUpdate({
|
||||
ids: undefined,
|
||||
query: `${ALERT_STATUS}: open`,
|
||||
index: indexName,
|
||||
status: 'closed',
|
||||
});
|
||||
expect(auditLogger.log).toHaveBeenCalledWith({
|
||||
message: `User is updating alert [id=${fakeAlertId}]`,
|
||||
event: {
|
||||
action: 'alert_update',
|
||||
category: ['database'],
|
||||
outcome: 'unknown',
|
||||
type: ['change'],
|
||||
},
|
||||
error: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
test('audit error access if user is unauthorized for given alert', async () => {
|
||||
const indexName = '.alerts-observability-apm.alerts';
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
esClientMock.search.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createApiResponse({
|
||||
body: {
|
||||
took: 5,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
total: 1,
|
||||
successful: 1,
|
||||
failed: 0,
|
||||
skipped: 0,
|
||||
},
|
||||
hits: {
|
||||
total: 1,
|
||||
max_score: 999,
|
||||
hits: [
|
||||
{
|
||||
_id: fakeAlertId,
|
||||
_index: '.alerts-observability-apm.alerts',
|
||||
_source: {
|
||||
[RULE_ID]: fakeRuleTypeId,
|
||||
[ALERT_OWNER]: 'apm',
|
||||
[ALERT_STATUS]: 'open',
|
||||
[SPACE_IDS]: [DEFAULT_SPACE],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
await expect(
|
||||
alertsClient.bulkUpdate({
|
||||
ids: undefined,
|
||||
query: `${ALERT_STATUS}: open`,
|
||||
index: indexName,
|
||||
status: 'closed',
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||
"queryAndAuditAllAlerts threw an error: Unable to retrieve alerts with query \\"kibana.alert.status: open\\" and operation update
|
||||
Error: Unable to retrieve alert details for alert with id of \\"null\\" or with query \\"kibana.alert.status: open\\" and operation update
|
||||
Error: Error: Unauthorized for fake.rule and apm"
|
||||
`);
|
||||
|
||||
expect(auditLogger.log).toHaveBeenNthCalledWith(1, {
|
||||
message: `Failed attempt to update alert [id=${fakeAlertId}]`,
|
||||
event: {
|
||||
action: 'alert_update',
|
||||
category: ['database'],
|
||||
outcome: 'failure',
|
||||
type: ['change'],
|
||||
},
|
||||
error: {
|
||||
code: 'Error',
|
||||
message: 'Unauthorized for fake.rule and apm',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('logs multiple error events in audit logger', async () => {
|
||||
const indexName = '.alerts-observability-apm.alerts';
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
esClientMock.search.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createApiResponse({
|
||||
body: {
|
||||
took: 5,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
total: 1,
|
||||
successful: 1,
|
||||
failed: 0,
|
||||
skipped: 0,
|
||||
},
|
||||
hits: {
|
||||
total: 2,
|
||||
max_score: 999,
|
||||
hits: [
|
||||
{
|
||||
_id: successfulAuthzHit,
|
||||
_index: '.alerts-observability-apm.alerts',
|
||||
_source: {
|
||||
[RULE_ID]: 'apm.error_rate',
|
||||
[ALERT_OWNER]: 'apm',
|
||||
[ALERT_STATUS]: 'open',
|
||||
[SPACE_IDS]: [DEFAULT_SPACE],
|
||||
},
|
||||
},
|
||||
{
|
||||
_id: unsuccessfulAuthzHit,
|
||||
_index: '.alerts-observability-apm.alerts',
|
||||
_source: {
|
||||
[RULE_ID]: fakeRuleTypeId,
|
||||
[ALERT_OWNER]: 'apm',
|
||||
[ALERT_STATUS]: 'open',
|
||||
[SPACE_IDS]: [DEFAULT_SPACE],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
await expect(
|
||||
alertsClient.bulkUpdate({
|
||||
ids: undefined,
|
||||
query: `${ALERT_STATUS}: open`,
|
||||
index: indexName,
|
||||
status: 'closed',
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||
"queryAndAuditAllAlerts threw an error: Unable to retrieve alerts with query \\"kibana.alert.status: open\\" and operation update
|
||||
Error: Unable to retrieve alert details for alert with id of \\"null\\" or with query \\"kibana.alert.status: open\\" and operation update
|
||||
Error: Error: Unauthorized for fake.rule and apm"
|
||||
`);
|
||||
|
||||
expect(auditLogger.log).toHaveBeenCalledTimes(2);
|
||||
expect(auditLogger.log).toHaveBeenNthCalledWith(1, {
|
||||
message: `Failed attempt to update alert [id=${unsuccessfulAuthzHit}]`,
|
||||
event: {
|
||||
action: 'alert_update',
|
||||
category: ['database'],
|
||||
outcome: 'failure',
|
||||
type: ['change'],
|
||||
},
|
||||
error: {
|
||||
code: 'Error',
|
||||
message: 'Unauthorized for fake.rule and apm',
|
||||
},
|
||||
});
|
||||
expect(auditLogger.log).toHaveBeenNthCalledWith(2, {
|
||||
message: `Failed attempt to update alert [id=${successfulAuthzHit}]`,
|
||||
event: {
|
||||
action: 'alert_update',
|
||||
category: ['database'],
|
||||
outcome: 'failure',
|
||||
type: ['change'],
|
||||
},
|
||||
error: {
|
||||
code: 'Error',
|
||||
message: 'Unauthorized for fake.rule and apm',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
// test('throws an error if ES client fetch fails', async () => {});
|
||||
// test('throws an error if ES client bulk update fails', async () => {});
|
||||
// test('throws an error if ES client updateByQuery fails', async () => {});
|
||||
});
|
||||
});
|
|
@ -5,13 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ALERT_OWNER, ALERT_STATUS, SPACE_IDS } from '@kbn/rule-data-utils';
|
||||
import { ALERT_OWNER, ALERT_STATUS, RULE_ID, SPACE_IDS } from '@kbn/rule-data-utils';
|
||||
import { AlertsClient, ConstructorOptions } from '../alerts_client';
|
||||
import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks';
|
||||
import { alertingAuthorizationMock } from '../../../../alerting/server/authorization/alerting_authorization.mock';
|
||||
import { AuditLogger } from '../../../../security/server';
|
||||
import { AlertingAuthorizationEntity } from '../../../../alerting/server';
|
||||
|
||||
const alertingAuthMock = alertingAuthorizationMock.create();
|
||||
const esClientMock = elasticsearchClientMock.createElasticsearchClient();
|
||||
|
@ -26,9 +27,35 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
auditLogger,
|
||||
};
|
||||
|
||||
const DEFAULT_SPACE = 'test_default_space_id';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
alertingAuthMock.getSpaceId.mockImplementation(() => 'test_default_space_id');
|
||||
alertingAuthMock.getSpaceId.mockImplementation(() => DEFAULT_SPACE);
|
||||
// @ts-expect-error
|
||||
alertingAuthMock.getAuthorizationFilter.mockImplementation(async () =>
|
||||
Promise.resolve({ filter: [] })
|
||||
);
|
||||
|
||||
alertingAuthMock.ensureAuthorized.mockImplementation(
|
||||
// @ts-expect-error
|
||||
async ({
|
||||
ruleTypeId,
|
||||
consumer,
|
||||
operation,
|
||||
entity,
|
||||
}: {
|
||||
ruleTypeId: string;
|
||||
consumer: string;
|
||||
operation: string;
|
||||
entity: typeof AlertingAuthorizationEntity.Alert;
|
||||
}) => {
|
||||
if (ruleTypeId === 'apm.error_rate' && consumer === 'apm') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error(`Unauthorized for ${ruleTypeId} and ${consumer}`));
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('get()', () => {
|
||||
|
@ -73,10 +100,9 @@ describe('get()', () => {
|
|||
const result = await alertsClient.get({ id: '1', index: '.alerts-observability-apm' });
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"_version": "WzM2MiwyXQ==",
|
||||
"${ALERT_OWNER}": "apm",
|
||||
"${ALERT_STATUS}": "open",
|
||||
"${SPACE_IDS}": Array [
|
||||
"kibana.alert.owner": "apm",
|
||||
"kibana.alert.status": "open",
|
||||
"kibana.space_ids": Array [
|
||||
"test_default_space_id",
|
||||
],
|
||||
"message": "hello world 1",
|
||||
|
@ -92,18 +118,37 @@ describe('get()', () => {
|
|||
"bool": Object {
|
||||
"filter": Array [
|
||||
Object {
|
||||
"term": Object {
|
||||
"_id": "1",
|
||||
"bool": Object {
|
||||
"minimum_should_match": 1,
|
||||
"should": Array [
|
||||
Object {
|
||||
"match": Object {
|
||||
"_id": "1",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
Object {},
|
||||
Object {
|
||||
"term": Object {
|
||||
"kibana.space_ids": "test_default_space_id",
|
||||
},
|
||||
},
|
||||
],
|
||||
"must": Array [],
|
||||
"must_not": Array [],
|
||||
"should": Array [],
|
||||
},
|
||||
},
|
||||
"sort": Array [
|
||||
Object {
|
||||
"@timestamp": Object {
|
||||
"order": "asc",
|
||||
"unmapped_type": "date",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
"ignore_unavailable": true,
|
||||
"index": ".alerts-observability-apm",
|
||||
|
@ -151,12 +196,75 @@ describe('get()', () => {
|
|||
},
|
||||
})
|
||||
);
|
||||
await alertsClient.get({ id: '1', index: '.alerts-observability-apm' });
|
||||
await alertsClient.get({ id: 'NoxgpHkBqbdrfX07MqXV', index: '.alerts-observability-apm' });
|
||||
|
||||
expect(auditLogger.log).toHaveBeenCalledWith({
|
||||
error: undefined,
|
||||
event: { action: 'alert_get', category: ['database'], outcome: 'success', type: ['access'] },
|
||||
message: 'User has accessed alert [id=1]',
|
||||
message: 'User has accessed alert [id=NoxgpHkBqbdrfX07MqXV]',
|
||||
});
|
||||
});
|
||||
|
||||
test('audit error access if user is unauthorized for given alert', async () => {
|
||||
const indexName = '.alerts-observability-apm.alerts';
|
||||
const fakeAlertId = 'myfakeid1';
|
||||
// fakeRuleTypeId will cause authz to fail
|
||||
const fakeRuleTypeId = 'fake.rule';
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
esClientMock.search.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createApiResponse({
|
||||
body: {
|
||||
took: 5,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
total: 1,
|
||||
successful: 1,
|
||||
failed: 0,
|
||||
skipped: 0,
|
||||
},
|
||||
hits: {
|
||||
total: 1,
|
||||
max_score: 999,
|
||||
hits: [
|
||||
{
|
||||
found: true,
|
||||
_type: 'alert',
|
||||
_version: 1,
|
||||
_seq_no: 362,
|
||||
_primary_term: 2,
|
||||
_id: fakeAlertId,
|
||||
_index: indexName,
|
||||
_source: {
|
||||
[RULE_ID]: fakeRuleTypeId,
|
||||
[ALERT_OWNER]: 'apm',
|
||||
[ALERT_STATUS]: 'open',
|
||||
[SPACE_IDS]: [DEFAULT_SPACE],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
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
|
||||
Error: Error: Unauthorized for fake.rule and apm"
|
||||
`);
|
||||
|
||||
expect(auditLogger.log).toHaveBeenNthCalledWith(1, {
|
||||
message: `Failed attempt to access alert [id=${fakeAlertId}]`,
|
||||
event: {
|
||||
action: 'alert_get',
|
||||
category: ['database'],
|
||||
outcome: 'failure',
|
||||
type: ['access'],
|
||||
},
|
||||
error: {
|
||||
code: 'Error',
|
||||
message: 'Unauthorized for fake.rule and apm',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -166,13 +274,11 @@ describe('get()', () => {
|
|||
esClientMock.search.mockRejectedValue(error);
|
||||
|
||||
await expect(
|
||||
alertsClient.get({ id: '1', index: '.alerts-observability-apm' })
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"something went wrong"`);
|
||||
expect(auditLogger.log).toHaveBeenCalledWith({
|
||||
error: { code: 'Error', message: 'something went wrong' },
|
||||
event: { action: 'alert_get', category: ['database'], outcome: 'failure', type: ['access'] },
|
||||
message: 'Failed attempt to access alert [id=1]',
|
||||
});
|
||||
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
|
||||
Error: Error: something went wrong"
|
||||
`);
|
||||
});
|
||||
|
||||
describe('authorization', () => {
|
||||
|
@ -217,20 +323,16 @@ describe('get()', () => {
|
|||
|
||||
test('returns alert if user is authorized to read alert under the consumer', async () => {
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
const result = await alertsClient.get({ id: '1', index: '.alerts-observability-apm' });
|
||||
|
||||
expect(alertingAuthMock.ensureAuthorized).toHaveBeenCalledWith({
|
||||
entity: 'alert',
|
||||
consumer: 'apm',
|
||||
operation: 'get',
|
||||
ruleTypeId: 'apm.error_rate',
|
||||
const result = await alertsClient.get({
|
||||
id: 'NoxgpHkBqbdrfX07MqXV',
|
||||
index: '.alerts-observability-apm',
|
||||
});
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"_version": "WzM2MiwyXQ==",
|
||||
"${ALERT_OWNER}": "apm",
|
||||
"${ALERT_STATUS}": "open",
|
||||
"${SPACE_IDS}": Array [
|
||||
"kibana.alert.owner": "apm",
|
||||
"kibana.alert.status": "open",
|
||||
"kibana.space_ids": Array [
|
||||
"test_default_space_id",
|
||||
],
|
||||
"message": "hello world 1",
|
||||
|
@ -238,25 +340,5 @@ describe('get()', () => {
|
|||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('throws when user is not authorized to get this type of alert', async () => {
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
alertingAuthMock.ensureAuthorized.mockRejectedValue(
|
||||
new Error(`Unauthorized to get a "apm.error_rate" alert for "apm"`)
|
||||
);
|
||||
|
||||
await expect(
|
||||
alertsClient.get({ id: '1', index: '.alerts-observability-apm' })
|
||||
).rejects.toMatchInlineSnapshot(
|
||||
`[Error: Unauthorized to get a "apm.error_rate" alert for "apm"]`
|
||||
);
|
||||
|
||||
expect(alertingAuthMock.ensureAuthorized).toHaveBeenCalledWith({
|
||||
entity: 'alert',
|
||||
consumer: 'apm',
|
||||
operation: 'get',
|
||||
ruleTypeId: 'apm.error_rate',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,13 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ALERT_OWNER, ALERT_STATUS, SPACE_IDS } from '@kbn/rule-data-utils';
|
||||
import { ALERT_OWNER, ALERT_STATUS, SPACE_IDS, RULE_ID } from '@kbn/rule-data-utils';
|
||||
import { AlertsClient, ConstructorOptions } from '../alerts_client';
|
||||
import { loggingSystemMock } from '../../../../../../src/core/server/mocks';
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks';
|
||||
import { alertingAuthorizationMock } from '../../../../alerting/server/authorization/alerting_authorization.mock';
|
||||
import { AuditLogger } from '../../../../security/server';
|
||||
import { AlertingAuthorizationEntity } from '../../../../alerting/server';
|
||||
|
||||
const alertingAuthMock = alertingAuthorizationMock.create();
|
||||
const esClientMock = elasticsearchClientMock.createElasticsearchClient();
|
||||
|
@ -26,9 +27,35 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
|
|||
auditLogger,
|
||||
};
|
||||
|
||||
const DEFAULT_SPACE = 'test_default_space_id';
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
alertingAuthMock.getSpaceId.mockImplementation(() => 'test_default_space_id');
|
||||
alertingAuthMock.getSpaceId.mockImplementation(() => DEFAULT_SPACE);
|
||||
// @ts-expect-error
|
||||
alertingAuthMock.getAuthorizationFilter.mockImplementation(async () =>
|
||||
Promise.resolve({ filter: [] })
|
||||
);
|
||||
|
||||
alertingAuthMock.ensureAuthorized.mockImplementation(
|
||||
// @ts-expect-error
|
||||
async ({
|
||||
ruleTypeId,
|
||||
consumer,
|
||||
operation,
|
||||
entity,
|
||||
}: {
|
||||
ruleTypeId: string;
|
||||
consumer: string;
|
||||
operation: string;
|
||||
entity: typeof AlertingAuthorizationEntity.Alert;
|
||||
}) => {
|
||||
if (ruleTypeId === 'apm.error_rate' && consumer === 'apm') {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return Promise.reject(new Error(`Unauthorized for ${ruleTypeId} and ${consumer}`));
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
describe('update()', () => {
|
||||
|
@ -55,11 +82,11 @@ describe('update()', () => {
|
|||
_index: '.alerts-observability-apm',
|
||||
_id: 'NoxgpHkBqbdrfX07MqXV',
|
||||
_source: {
|
||||
'rule.id': 'apm.error_rate',
|
||||
[RULE_ID]: 'apm.error_rate',
|
||||
message: 'hello world 1',
|
||||
[ALERT_OWNER]: 'apm',
|
||||
[ALERT_STATUS]: 'open',
|
||||
[SPACE_IDS]: ['test_default_space_id'],
|
||||
[SPACE_IDS]: [DEFAULT_SPACE],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -145,7 +172,7 @@ describe('update()', () => {
|
|||
message: 'hello world 1',
|
||||
[ALERT_OWNER]: 'apm',
|
||||
[ALERT_STATUS]: 'open',
|
||||
[SPACE_IDS]: ['test_default_space_id'],
|
||||
[SPACE_IDS]: [DEFAULT_SPACE],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -167,7 +194,7 @@ describe('update()', () => {
|
|||
})
|
||||
);
|
||||
await alertsClient.update({
|
||||
id: '1',
|
||||
id: 'NoxgpHkBqbdrfX07MqXV',
|
||||
status: 'closed',
|
||||
_version: undefined,
|
||||
index: '.alerts-observability-apm',
|
||||
|
@ -181,35 +208,97 @@ describe('update()', () => {
|
|||
outcome: 'unknown',
|
||||
type: ['change'],
|
||||
},
|
||||
message: 'User is updating alert [id=1]',
|
||||
message: 'User is updating alert [id=NoxgpHkBqbdrfX07MqXV]',
|
||||
});
|
||||
});
|
||||
|
||||
test(`throws an error if ES client get fails`, async () => {
|
||||
const error = new Error('something went wrong on get');
|
||||
test('audit error update if user is unauthorized for given alert', async () => {
|
||||
const indexName = '.alerts-observability-apm.alerts';
|
||||
const fakeAlertId = 'myfakeid1';
|
||||
// fakeRuleTypeId will cause authz to fail
|
||||
const fakeRuleTypeId = 'fake.rule';
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
esClientMock.search.mockRejectedValue(error);
|
||||
esClientMock.search.mockResolvedValueOnce(
|
||||
elasticsearchClientMock.createApiResponse({
|
||||
body: {
|
||||
took: 5,
|
||||
timed_out: false,
|
||||
_shards: {
|
||||
total: 1,
|
||||
successful: 1,
|
||||
failed: 0,
|
||||
skipped: 0,
|
||||
},
|
||||
hits: {
|
||||
total: 1,
|
||||
max_score: 999,
|
||||
hits: [
|
||||
{
|
||||
found: true,
|
||||
_type: 'alert',
|
||||
_version: 1,
|
||||
_seq_no: 362,
|
||||
_primary_term: 2,
|
||||
_id: fakeAlertId,
|
||||
_index: indexName,
|
||||
_source: {
|
||||
[RULE_ID]: fakeRuleTypeId,
|
||||
[ALERT_OWNER]: 'apm',
|
||||
[ALERT_STATUS]: 'open',
|
||||
[SPACE_IDS]: [DEFAULT_SPACE],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
await expect(
|
||||
alertsClient.update({
|
||||
id: '1',
|
||||
id: fakeAlertId,
|
||||
status: 'closed',
|
||||
_version: undefined,
|
||||
_version: '1',
|
||||
index: '.alerts-observability-apm',
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"something went wrong on get"`);
|
||||
expect(auditLogger.log).toHaveBeenCalledWith({
|
||||
error: { code: 'Error', message: 'something went wrong on get' },
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||
"Unable to retrieve alert details for alert with id of \\"myfakeid1\\" or with query \\"null\\" and operation update
|
||||
Error: Error: Unauthorized for fake.rule and apm"
|
||||
`);
|
||||
|
||||
expect(auditLogger.log).toHaveBeenNthCalledWith(1, {
|
||||
message: `Failed attempt to update alert [id=${fakeAlertId}]`,
|
||||
event: {
|
||||
action: 'alert_update',
|
||||
category: ['database'],
|
||||
outcome: 'failure',
|
||||
type: ['change'],
|
||||
},
|
||||
message: 'Failed attempt to update alert [id=1]',
|
||||
error: {
|
||||
code: 'Error',
|
||||
message: 'Unauthorized for fake.rule and apm',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test(`throws an error if ES client get fails`, async () => {
|
||||
const error = new Error('something went wrong on update');
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
esClientMock.search.mockRejectedValue(error);
|
||||
|
||||
await expect(
|
||||
alertsClient.update({
|
||||
id: 'NoxgpHkBqbdrfX07MqXV',
|
||||
status: 'closed',
|
||||
_version: undefined,
|
||||
index: '.alerts-observability-apm',
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`
|
||||
"Unable to retrieve alert details for alert with id of \\"NoxgpHkBqbdrfX07MqXV\\" or with query \\"null\\" and operation update
|
||||
Error: Error: something went wrong on update"
|
||||
`);
|
||||
});
|
||||
|
||||
test(`throws an error if ES client update fails`, async () => {
|
||||
const error = new Error('something went wrong on update');
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
|
@ -238,7 +327,7 @@ describe('update()', () => {
|
|||
message: 'hello world 1',
|
||||
[ALERT_OWNER]: 'apm',
|
||||
[ALERT_STATUS]: 'open',
|
||||
[SPACE_IDS]: ['test_default_space_id'],
|
||||
[SPACE_IDS]: [DEFAULT_SPACE],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -250,21 +339,21 @@ describe('update()', () => {
|
|||
|
||||
await expect(
|
||||
alertsClient.update({
|
||||
id: '1',
|
||||
id: 'NoxgpHkBqbdrfX07MqXV',
|
||||
status: 'closed',
|
||||
_version: undefined,
|
||||
index: '.alerts-observability-apm',
|
||||
})
|
||||
).rejects.toThrowErrorMatchingInlineSnapshot(`"something went wrong on update"`);
|
||||
expect(auditLogger.log).toHaveBeenCalledWith({
|
||||
error: { code: 'Error', message: 'something went wrong on update' },
|
||||
error: undefined,
|
||||
event: {
|
||||
action: 'alert_update',
|
||||
category: ['database'],
|
||||
outcome: 'failure',
|
||||
outcome: 'unknown',
|
||||
type: ['change'],
|
||||
},
|
||||
message: 'Failed attempt to update alert [id=1]',
|
||||
message: 'User is updating alert [id=NoxgpHkBqbdrfX07MqXV]',
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -298,7 +387,7 @@ describe('update()', () => {
|
|||
message: 'hello world 1',
|
||||
[ALERT_OWNER]: 'apm',
|
||||
[ALERT_STATUS]: 'open',
|
||||
[SPACE_IDS]: ['test_default_space_id'],
|
||||
[SPACE_IDS]: [DEFAULT_SPACE],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
@ -325,18 +414,12 @@ describe('update()', () => {
|
|||
test('returns alert if user is authorized to update alert under the consumer', async () => {
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
const result = await alertsClient.update({
|
||||
id: '1',
|
||||
id: 'NoxgpHkBqbdrfX07MqXV',
|
||||
status: 'closed',
|
||||
_version: undefined,
|
||||
index: '.alerts-observability-apm',
|
||||
});
|
||||
|
||||
expect(alertingAuthMock.ensureAuthorized).toHaveBeenCalledWith({
|
||||
entity: 'alert',
|
||||
consumer: 'apm',
|
||||
operation: 'update',
|
||||
ruleTypeId: 'apm.error_rate',
|
||||
});
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"_id": "NoxgpHkBqbdrfX07MqXV",
|
||||
|
@ -353,30 +436,5 @@ describe('update()', () => {
|
|||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('throws when user is not authorized to update this type of alert', async () => {
|
||||
const alertsClient = new AlertsClient(alertsClientParams);
|
||||
alertingAuthMock.ensureAuthorized.mockRejectedValue(
|
||||
new Error(`Unauthorized to get a "apm.error_rate" alert for "apm"`)
|
||||
);
|
||||
|
||||
await expect(
|
||||
alertsClient.update({
|
||||
id: '1',
|
||||
status: 'closed',
|
||||
_version: undefined,
|
||||
index: '.alerts-observability-apm',
|
||||
})
|
||||
).rejects.toMatchInlineSnapshot(
|
||||
`[Error: Unauthorized to get a "apm.error_rate" alert for "apm"]`
|
||||
);
|
||||
|
||||
expect(alertingAuthMock.ensureAuthorized).toHaveBeenCalledWith({
|
||||
entity: 'alert',
|
||||
consumer: 'apm',
|
||||
operation: 'update',
|
||||
ruleTypeId: 'apm.error_rate',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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 { IRouter } from 'kibana/server';
|
||||
import * as t from 'io-ts';
|
||||
import { id as _id } from '@kbn/securitysolution-io-ts-list-types';
|
||||
import { transformError } from '@kbn/securitysolution-es-utils';
|
||||
|
||||
import { buildRouteValidation } from './utils/route_validation';
|
||||
import { RacRequestHandlerContext } from '../types';
|
||||
import { BASE_RAC_ALERTS_API_PATH } from '../../common/constants';
|
||||
|
||||
export const bulkUpdateAlertsRoute = (router: IRouter<RacRequestHandlerContext>) => {
|
||||
router.post(
|
||||
{
|
||||
path: `${BASE_RAC_ALERTS_API_PATH}/bulk_update`,
|
||||
validate: {
|
||||
body: buildRouteValidation(
|
||||
t.union([
|
||||
t.strict({
|
||||
status: t.union([t.literal('open'), t.literal('closed')]),
|
||||
index: t.string,
|
||||
ids: t.array(t.string),
|
||||
query: t.undefined,
|
||||
}),
|
||||
t.strict({
|
||||
status: t.union([t.literal('open'), t.literal('closed')]),
|
||||
index: t.string,
|
||||
ids: t.undefined,
|
||||
query: t.string,
|
||||
}),
|
||||
])
|
||||
),
|
||||
},
|
||||
options: {
|
||||
tags: ['access:rac'],
|
||||
},
|
||||
},
|
||||
async (context, req, response) => {
|
||||
try {
|
||||
const alertsClient = await context.rac.getAlertsClient();
|
||||
const { status, ids, index, query } = req.body;
|
||||
|
||||
if (ids != null && ids.length > 1000) {
|
||||
return response.badRequest({
|
||||
body: {
|
||||
message: 'cannot use more than 1000 ids',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const updatedAlert = await alertsClient.bulkUpdate({
|
||||
ids,
|
||||
status,
|
||||
query,
|
||||
index,
|
||||
});
|
||||
|
||||
if (updatedAlert == null) {
|
||||
return response.notFound({
|
||||
body: { message: `alerts with ids ${ids} and index ${index} not found` },
|
||||
});
|
||||
}
|
||||
|
||||
return response.ok({ body: { success: true, ...updatedAlert } });
|
||||
} catch (exc) {
|
||||
const err = transformError(exc);
|
||||
|
||||
const contentType = {
|
||||
'content-type': 'application/json',
|
||||
};
|
||||
const defaultedHeaders = {
|
||||
...contentType,
|
||||
};
|
||||
|
||||
return response.customError({
|
||||
headers: defaultedHeaders,
|
||||
statusCode: err.statusCode,
|
||||
body: {
|
||||
message: err.message,
|
||||
attributes: {
|
||||
success: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
|
@ -10,9 +10,11 @@ import { RacRequestHandlerContext } from '../types';
|
|||
import { getAlertByIdRoute } from './get_alert_by_id';
|
||||
import { updateAlertByIdRoute } from './update_alert_by_id';
|
||||
import { getAlertsIndexRoute } from './get_alert_index';
|
||||
import { bulkUpdateAlertsRoute } from './bulk_update_alerts';
|
||||
|
||||
export function defineRoutes(router: IRouter<RacRequestHandlerContext>) {
|
||||
getAlertByIdRoute(router);
|
||||
updateAlertByIdRoute(router);
|
||||
getAlertsIndexRoute(router);
|
||||
bulkUpdateAlertsRoute(router);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
#!/bin/sh
|
||||
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
IDS=${1:-[\"Do4JnHoBqkRSppNZ6vre\"]}
|
||||
STATUS=${2}
|
||||
|
||||
echo $IDS
|
||||
echo "'"$STATUS"'"
|
||||
|
||||
cd ./hunter && sh ./post_detections_role.sh && sh ./post_detections_user.sh
|
||||
cd ../observer && sh ./post_detections_role.sh && sh ./post_detections_user.sh
|
||||
cd ..
|
||||
|
||||
# Example: ./update_observability_alert.sh [\"my-alert-id\",\"another-alert-id\"] <closed | open>
|
||||
curl -s -k \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'kbn-xsrf: 123' \
|
||||
-u observer:changeme \
|
||||
-X POST ${KIBANA_URL}${SPACE_URL}/internal/rac/alerts/bulk_update \
|
||||
-d "{\"ids\": $IDS, \"status\":\"$STATUS\", \"index\":\".alerts-observability-apm\"}" | jq .
|
||||
# -d "{\"ids\": $IDS, \"query\": \"kibana.rac.alert.status: open\", \"status\":\"$STATUS\", \"index\":\".alerts-observability-apm\"}" | jq .
|
||||
# -d "{\"ids\": $IDS, \"status\":\"$STATUS\", \"index\":\".alerts-observability-apm\"}" | jq .
|
|
@ -0,0 +1,28 @@
|
|||
#!/bin/sh
|
||||
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
QUERY=${1:-"kibana.rac.alert.status: open"}
|
||||
STATUS=${2}
|
||||
|
||||
echo $IDS
|
||||
echo "'"$STATUS"'"
|
||||
|
||||
cd ./hunter && sh ./post_detections_role.sh && sh ./post_detections_user.sh
|
||||
cd ../observer && sh ./post_detections_role.sh && sh ./post_detections_user.sh
|
||||
cd ..
|
||||
|
||||
# Example: ./update_observability_alert.sh "kibana.rac.alert.stats: open" <closed | open>
|
||||
curl -s -k \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'kbn-xsrf: 123' \
|
||||
-u observer:changeme \
|
||||
-X POST ${KIBANA_URL}${SPACE_URL}/internal/rac/alerts/bulk_update \
|
||||
-d "{\"query\": \"$QUERY\", \"status\":\"$STATUS\", \"index\":\".alerts-observability-apm\"}" | jq .
|
|
@ -0,0 +1,28 @@
|
|||
#!/bin/sh
|
||||
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
QUERY=${1:-"signal.status: open"}
|
||||
STATUS=${2}
|
||||
|
||||
echo $IDS
|
||||
echo "'"$STATUS"'"
|
||||
|
||||
cd ./hunter && sh ./post_detections_role.sh && sh ./post_detections_user.sh
|
||||
cd ../observer && sh ./post_detections_role.sh && sh ./post_detections_user.sh
|
||||
cd ..
|
||||
|
||||
# Example: ./update_observability_alert.sh "kibana.rac.alert.stats: open" <closed | open>
|
||||
curl -s -k \
|
||||
-H 'Content-Type: application/json' \
|
||||
-H 'kbn-xsrf: 123' \
|
||||
-u hunter:changeme \
|
||||
-X POST ${KIBANA_URL}${SPACE_URL}/internal/rac/alerts/bulk_update \
|
||||
-d "{\"query\": \"$QUERY\", \"status\":\"$STATUS\", \"index\":\".siem-signals*\"}" | jq .
|
|
@ -10,7 +10,7 @@
|
|||
set -e
|
||||
|
||||
USER=${1:-'observer'}
|
||||
ID=${2:-'DHEnOXoB8br9Z2X1fq_l'}
|
||||
ID=${2:-'Do4JnHoBqkRSppNZ6vre'}
|
||||
|
||||
cd ./hunter && sh ./post_detections_role.sh && sh ./post_detections_user.sh
|
||||
cd ../observer && sh ./post_detections_role.sh && sh ./post_detections_user.sh
|
||||
|
|
|
@ -6,12 +6,7 @@
|
|||
"kibana": [
|
||||
{
|
||||
"feature": {
|
||||
"ml": ["read"],
|
||||
"siem": ["all"],
|
||||
"actions": ["read"],
|
||||
"ruleRegistry": ["all"],
|
||||
"builtInAlerts": ["all"],
|
||||
"alerting": ["all"]
|
||||
"siem": ["all"]
|
||||
},
|
||||
"spaces": ["*"]
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
"kibana": [
|
||||
{
|
||||
"feature": {
|
||||
"apm": ["read", "alerts_all"]
|
||||
"apm": ["all"]
|
||||
},
|
||||
"spaces": ["*"]
|
||||
}
|
||||
|
|
|
@ -18,6 +18,9 @@ import {
|
|||
* @deprecated ruleExecutionFieldMap is kept here only as a reference. It will be superseded with EventLog implementation
|
||||
*/
|
||||
export const ruleExecutionFieldMap = {
|
||||
// [ALERT_OWNER]: { type: 'keyword', required: true },
|
||||
// [SPACE_IDS]: { type: 'keyword', array: true, required: true },
|
||||
// [RULE_ID]: { type: 'keyword', required: true },
|
||||
[MESSAGE]: { type: 'keyword' },
|
||||
[EVENT_SEQUENCE]: { type: 'long' },
|
||||
[EVENT_END]: { type: 'date' },
|
||||
|
|
|
@ -213,6 +213,14 @@ export class RuleRegistryLogClient implements IRuleRegistryLogClient {
|
|||
);
|
||||
}
|
||||
|
||||
// { [x: string]: string | string[] | ExecutionMetricValue<T>;
|
||||
// [x: number]: string;
|
||||
// "kibana.space_ids": string[];
|
||||
// "event.action": T;
|
||||
// "event.kind": string;
|
||||
// "rule.id": string;
|
||||
// "@timestamp": string; }
|
||||
|
||||
public async logExecutionMetric<T extends ExecutionMetric>({
|
||||
ruleId,
|
||||
namespace,
|
||||
|
|
|
@ -82,3 +82,35 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"index": ".alerts-security.alerts",
|
||||
"id": "space1securityalert",
|
||||
"source": {
|
||||
"@timestamp": "2020-12-16T15:16:18.570Z",
|
||||
"rule.id": "siem.signals",
|
||||
"message": "hello world security",
|
||||
"kibana.alert.owner": "siem",
|
||||
"kibana.alert.status": "open",
|
||||
"kibana.space_ids": ["space1"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"type": "doc",
|
||||
"value": {
|
||||
"index": ".alerts-security.alerts",
|
||||
"id": "space2securityalert",
|
||||
"source": {
|
||||
"@timestamp": "2020-12-16T15:16:18.570Z",
|
||||
"rule.id": "siem.signals",
|
||||
"message": "hello world security",
|
||||
"kibana.alert.owner": "siem",
|
||||
"kibana.alert.status": "open",
|
||||
"kibana.space_ids": ["space2"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,225 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
import {
|
||||
superUser,
|
||||
globalRead,
|
||||
obsOnly,
|
||||
obsOnlyRead,
|
||||
obsSec,
|
||||
obsSecRead,
|
||||
secOnly,
|
||||
secOnlyRead,
|
||||
secOnlySpace2,
|
||||
secOnlyReadSpace2,
|
||||
obsSecAllSpace2,
|
||||
obsSecReadSpace2,
|
||||
obsOnlySpace2,
|
||||
obsOnlyReadSpace2,
|
||||
obsOnlySpacesAll,
|
||||
obsSecSpacesAll,
|
||||
secOnlySpacesAll,
|
||||
noKibanaPrivileges,
|
||||
} from '../../../common/lib/authentication/users';
|
||||
import type { User } from '../../../common/lib/authentication/types';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
import { getSpaceUrlPrefix } from '../../../common/lib/authentication/spaces';
|
||||
|
||||
interface TestCase {
|
||||
/** The space where the alert exists */
|
||||
space: string;
|
||||
/** The ID of the alert */
|
||||
alertId: string;
|
||||
/** The index of the alert */
|
||||
index: string;
|
||||
/** Authorized users */
|
||||
authorizedUsers: User[];
|
||||
/** Unauthorized users */
|
||||
unauthorizedUsers: User[];
|
||||
}
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
const TEST_URL = '/internal/rac/alerts';
|
||||
const ALERTS_INDEX_URL = `${TEST_URL}/index`;
|
||||
const SPACE1 = 'space1';
|
||||
const SPACE2 = 'space2';
|
||||
const APM_ALERT_ID = 'NoxgpHkBqbdrfX07MqXV';
|
||||
const APM_ALERT_INDEX = '.alerts-observability-apm';
|
||||
const SECURITY_SOLUTION_ALERT_ID = '020202';
|
||||
const SECURITY_SOLUTION_ALERT_INDEX = '.alerts-security.alerts';
|
||||
// const ALERT_VERSION = Buffer.from(JSON.stringify([0, 1]), 'utf8').toString('base64'); // required for optimistic concurrency control
|
||||
|
||||
const getAPMIndexName = async (user: User) => {
|
||||
const {
|
||||
body: indexNames,
|
||||
}: { body: { index_name: string[] | undefined } } = await supertestWithoutAuth
|
||||
.get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`)
|
||||
.auth(user.username, user.password)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(200);
|
||||
const observabilityIndex = indexNames?.index_name?.find(
|
||||
(indexName) => indexName === APM_ALERT_INDEX
|
||||
);
|
||||
expect(observabilityIndex).to.eql(APM_ALERT_INDEX); // assert this here so we can use constants in the dynamically-defined test cases below
|
||||
};
|
||||
|
||||
const getSecuritySolutionIndexName = async (user: User) => {
|
||||
const {
|
||||
body: indexNames,
|
||||
}: { body: { index_name: string[] | undefined } } = await supertestWithoutAuth
|
||||
.get(`${getSpaceUrlPrefix(SPACE1)}${ALERTS_INDEX_URL}`)
|
||||
.auth(user.username, user.password)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(200);
|
||||
const securitySolution = indexNames?.index_name?.find(
|
||||
(indexName) => indexName === SECURITY_SOLUTION_ALERT_INDEX
|
||||
);
|
||||
expect(securitySolution).to.eql(SECURITY_SOLUTION_ALERT_INDEX); // assert this here so we can use constants in the dynamically-defined test cases below
|
||||
};
|
||||
|
||||
describe('Alert - Bulk Update - RBAC - spaces', () => {
|
||||
before(async () => {
|
||||
await getSecuritySolutionIndexName(superUser);
|
||||
await getAPMIndexName(superUser);
|
||||
});
|
||||
|
||||
before(async () => {
|
||||
await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts');
|
||||
});
|
||||
|
||||
function addTests({ space, authorizedUsers, unauthorizedUsers, alertId, index }: TestCase) {
|
||||
authorizedUsers.forEach(({ username, password }) => {
|
||||
it(`${username} should bulk update alert with given id ${alertId} in ${space}/${index}`, async () => {
|
||||
await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts'); // since this is a success case, reload the test data immediately beforehand
|
||||
const { body: updated } = await supertestWithoutAuth
|
||||
.post(`${getSpaceUrlPrefix(space)}${TEST_URL}/bulk_update`)
|
||||
.auth(username, password)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
ids: [alertId],
|
||||
status: 'closed',
|
||||
index,
|
||||
});
|
||||
expect(updated.statusCode).to.eql(200);
|
||||
const items = updated.body.items;
|
||||
// @ts-expect-error
|
||||
items.map((item) => expect(item.update.result).to.eql('updated'));
|
||||
});
|
||||
|
||||
it(`${username} should bulk update alerts which match query in ${space}/${index}`, async () => {
|
||||
const { body: updated } = await supertestWithoutAuth
|
||||
.post(`${getSpaceUrlPrefix(space)}${TEST_URL}/bulk_update`)
|
||||
.auth(username, password)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
status: 'closed',
|
||||
query: 'kibana.alert.status: open',
|
||||
index,
|
||||
});
|
||||
expect(updated.statusCode).to.eql(200);
|
||||
expect(updated.body.updated).to.greaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
unauthorizedUsers.forEach(({ username, password }) => {
|
||||
it(`${username} should NOT be able to update alert ${alertId} in ${space}/${index}`, async () => {
|
||||
const res = await supertestWithoutAuth
|
||||
.post(`${getSpaceUrlPrefix(space)}${TEST_URL}/bulk_update`)
|
||||
.auth(username, password)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
ids: [alertId],
|
||||
status: 'closed',
|
||||
index,
|
||||
});
|
||||
expect([403, 404]).to.contain(res.statusCode);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Alert - Update - RBAC - spaces Security Solution superuser should bulk update alerts which match query in space1/.alerts-security.alerts
|
||||
// Alert - Update - RBAC - spaces superuser should bulk update alert with given id 020202 in space1/.alerts-security.alerts
|
||||
describe('Security Solution', () => {
|
||||
const authorizedInAllSpaces = [superUser, secOnlySpacesAll, obsSecSpacesAll];
|
||||
const authorizedOnlyInSpace1 = [secOnly, obsSec];
|
||||
const authorizedOnlyInSpace2 = [secOnlySpace2, obsSecAllSpace2];
|
||||
const unauthorized = [
|
||||
// these users are not authorized to update alerts for the Security Solution in any space
|
||||
globalRead,
|
||||
secOnlyRead,
|
||||
obsSecRead,
|
||||
secOnlyReadSpace2,
|
||||
obsSecReadSpace2,
|
||||
obsOnly,
|
||||
obsOnlyRead,
|
||||
obsOnlySpace2,
|
||||
obsOnlyReadSpace2,
|
||||
obsOnlySpacesAll,
|
||||
noKibanaPrivileges,
|
||||
];
|
||||
|
||||
addTests({
|
||||
space: SPACE1,
|
||||
alertId: SECURITY_SOLUTION_ALERT_ID,
|
||||
index: SECURITY_SOLUTION_ALERT_INDEX,
|
||||
authorizedUsers: [...authorizedInAllSpaces, ...authorizedOnlyInSpace1],
|
||||
unauthorizedUsers: [...authorizedOnlyInSpace2, ...unauthorized],
|
||||
});
|
||||
addTests({
|
||||
space: SPACE2,
|
||||
alertId: SECURITY_SOLUTION_ALERT_ID,
|
||||
index: SECURITY_SOLUTION_ALERT_INDEX,
|
||||
authorizedUsers: [...authorizedInAllSpaces, ...authorizedOnlyInSpace2],
|
||||
unauthorizedUsers: [...authorizedOnlyInSpace1, ...unauthorized],
|
||||
});
|
||||
});
|
||||
|
||||
describe('APM', () => {
|
||||
const authorizedInAllSpaces = [superUser, obsOnlySpacesAll, obsSecSpacesAll];
|
||||
const authorizedOnlyInSpace1 = [obsOnly, obsSec];
|
||||
const authorizedOnlyInSpace2 = [obsOnlySpace2, obsSecAllSpace2];
|
||||
const unauthorized = [
|
||||
// these users are not authorized to update alerts for APM in any space
|
||||
globalRead,
|
||||
obsOnlyRead,
|
||||
obsSecRead,
|
||||
obsOnlyReadSpace2,
|
||||
obsSecReadSpace2,
|
||||
secOnly,
|
||||
secOnlyRead,
|
||||
secOnlySpace2,
|
||||
secOnlyReadSpace2,
|
||||
secOnlySpacesAll,
|
||||
noKibanaPrivileges,
|
||||
];
|
||||
|
||||
addTests({
|
||||
space: SPACE1,
|
||||
alertId: APM_ALERT_ID,
|
||||
index: APM_ALERT_INDEX,
|
||||
authorizedUsers: [...authorizedInAllSpaces, ...authorizedOnlyInSpace1],
|
||||
unauthorizedUsers: [...authorizedOnlyInSpace2, ...unauthorized],
|
||||
});
|
||||
addTests({
|
||||
space: SPACE2,
|
||||
alertId: APM_ALERT_ID,
|
||||
index: APM_ALERT_INDEX,
|
||||
authorizedUsers: [...authorizedInAllSpaces, ...authorizedOnlyInSpace2],
|
||||
unauthorizedUsers: [...authorizedOnlyInSpace1, ...unauthorized],
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
|
@ -152,11 +152,11 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
|
||||
unauthorizedUsers.forEach(({ username, password }) => {
|
||||
it(`${username} should NOT be able to access alert ${alertId} in ${space}/${index}`, async () => {
|
||||
await supertestWithoutAuth
|
||||
const res = await supertestWithoutAuth
|
||||
.get(`${getSpaceUrlPrefix(space)}${TEST_URL}?id=${alertId}&index=${index}`)
|
||||
.auth(username, password)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(403);
|
||||
.set('kbn-xsrf', 'true');
|
||||
expect([403, 404]).to.contain(res.statusCode);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -25,5 +25,6 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => {
|
|||
// Basic
|
||||
loadTestFile(require.resolve('./get_alert_by_id'));
|
||||
loadTestFile(require.resolve('./update_alert'));
|
||||
loadTestFile(require.resolve('./bulk_update_alerts'));
|
||||
});
|
||||
};
|
||||
|
|
|
@ -145,25 +145,11 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
})
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it(`${username} should return a 404 when superuser accesses not-existent alerts as data index`, async () => {
|
||||
await supertestWithoutAuth
|
||||
.get(`${getSpaceUrlPrefix(space)}${TEST_URL}?id=${APM_ALERT_ID}&index=myfakeindex`)
|
||||
.auth(username, password)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.send({
|
||||
ids: [APM_ALERT_ID],
|
||||
status: 'closed',
|
||||
index: 'this index does not exist',
|
||||
_version: ALERT_VERSION,
|
||||
})
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
|
||||
unauthorizedUsers.forEach(({ username, password }) => {
|
||||
it(`${username} should NOT be able to update alert ${alertId} in ${space}/${index}`, async () => {
|
||||
await supertestWithoutAuth
|
||||
const res = await supertestWithoutAuth
|
||||
.post(`${getSpaceUrlPrefix(space)}${TEST_URL}`)
|
||||
.auth(username, password)
|
||||
.set('kbn-xsrf', 'true')
|
||||
|
@ -172,8 +158,8 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
status: 'closed',
|
||||
index,
|
||||
_version: ALERT_VERSION,
|
||||
})
|
||||
.expect(403);
|
||||
});
|
||||
expect([403, 404]).to.contain(res.statusCode);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -73,7 +73,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
.get(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&index=${apmIndex}`)
|
||||
.auth(obsMinRead.username, obsMinRead.password)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(403);
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it(`${obsMinReadSpacesAll.username} should NOT be able to access the APM alert in ${SPACE1}`, async () => {
|
||||
|
@ -82,7 +82,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
.get(`${getSpaceUrlPrefix(SPACE1)}${TEST_URL}?id=NoxgpHkBqbdrfX07MqXV&index=${apmIndex}`)
|
||||
.auth(obsMinReadSpacesAll.username, obsMinReadSpacesAll.password)
|
||||
.set('kbn-xsrf', 'true')
|
||||
.expect(403);
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -166,7 +166,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
index: apmIndex,
|
||||
_version: Buffer.from(JSON.stringify([0, 1]), 'utf8').toString('base64'),
|
||||
})
|
||||
.expect(403);
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it(`${obsMinAllSpacesAll.username} should NOT be able to update the APM alert in ${SPACE1}`, async () => {
|
||||
|
@ -181,7 +181,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
index: apmIndex,
|
||||
_version: Buffer.from(JSON.stringify([0, 1]), 'utf8').toString('base64'),
|
||||
})
|
||||
.expect(403);
|
||||
.expect(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue