[RAC] Fix index names used by RBAC, delete hardcoded map of Kibana features to index names (#109567) (#110068)

**Ticket:** https://github.com/elastic/kibana/issues/102089

🚨 **This PR is critical for Observability 7.15** 🚨

## Summary

This PR introduces changes that fix the usage of alerts-as-data index naming in RBAC. It builds on top of https://github.com/elastic/kibana/pull/109346 and replaces https://github.com/elastic/kibana/pull/108872.

TODO:

- [x] Address https://github.com/elastic/kibana/pull/109346#pullrequestreview-735158370
- [x] Make changes to `AlertsClient.getAuthorizedAlertsIndices()` so it starts using `RuleDataService` to get index names by feature ids.
- [x] Delete the hardcoded `mapConsumerToIndexName` where we had incorrect index names.
- [x] Close https://github.com/elastic/kibana/pull/108872

### Checklist

Delete any items that are not applicable to this PR.

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

Co-authored-by: Georgii Gorbachev <georgii.gorbachev@elastic.co>
This commit is contained in:
Kibana Machine 2021-08-25 12:50:18 -04:00 committed by GitHub
parent aa7680e3c1
commit 8db2bc1374
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
33 changed files with 162 additions and 126 deletions

View file

@ -26,17 +26,9 @@ export const AlertConsumers = {
export type AlertConsumers = typeof AlertConsumers[keyof typeof AlertConsumers];
export type STATUS_VALUES = 'open' | 'acknowledged' | 'closed' | 'in-progress'; // TODO: remove 'in-progress' after migration to 'acknowledged'
export const mapConsumerToIndexName: Record<AlertConsumers, string | string[]> = {
apm: '.alerts-observability-apm',
logs: '.alerts-observability.logs',
infrastructure: '.alerts-observability.metrics',
observability: '.alerts-observability',
siem: '.alerts-security.alerts',
uptime: '.alerts-observability.uptime',
};
export type ValidFeatureId = keyof typeof mapConsumerToIndexName;
export type ValidFeatureId = AlertConsumers;
export const validFeatureIds = Object.keys(mapConsumerToIndexName);
export const validFeatureIds = Object.values(AlertConsumers).map((v) => v as string);
export const isValidFeatureId = (a: unknown): a is ValidFeatureId =>
typeof a === 'string' && validFeatureIds.includes(a);

View file

@ -19,7 +19,7 @@ const testIndices = [
'.ds-metrics-system.process.summary-default-2021.05.25-00000',
'.kibana_shahzad_9',
'.kibana-felix-log-stream_8.0.0_001',
'.kibana_smith_alerts-observability-apm-000001',
'.kibana_smith_alerts-observability.apm.alerts-000001',
'.ds-logs-endpoint.events.process-default-2021.05.26-000001',
'.kibana_dominiqueclarke54_8.0.0_001',
'.kibana-cmarcondes-19_8.0.0_001',
@ -63,7 +63,7 @@ const onlySystemIndices = [
'.ds-metrics-system.process.summary-default-2021.05.25-00000',
'.kibana_shahzad_9',
'.kibana-felix-log-stream_8.0.0_001',
'.kibana_smith_alerts-observability-apm-000001',
'.kibana_smith_alerts-observability.apm.alerts-000001',
'.ds-logs-endpoint.events.process-default-2021.05.26-000001',
'.kibana_dominiqueclarke54_8.0.0_001',
'.kibana-cmarcondes-19_8.0.0_001',
@ -85,7 +85,7 @@ const kibanaNoTaskIndices = [
'.kibana_shahzad_1',
'.kibana_shahzad_9',
'.kibana-felix-log-stream_8.0.0_001',
'.kibana_smith_alerts-observability-apm-000001',
'.kibana_smith_alerts-observability.apm.alerts-000001',
'.kibana_dominiqueclarke54_8.0.0_001',
'.kibana-cmarcondes-19_8.0.0_001',
'.kibana_dominiqueclarke55-alerts-8.0.0-000001',

View file

@ -72,7 +72,6 @@ export function AlertsPage({ routeParams }: AlertsPageProps) {
registrationContexts: [
'observability.apm',
'observability.logs',
'observability.infrastructure',
'observability.metrics',
'observability.uptime',
],

View file

@ -6,6 +6,7 @@
*/
import * as t from 'io-ts';
import { Dataset } from '../../../rule_registry/server';
import { createObservabilityServerRoute } from './create_observability_server_route';
import { createObservabilityServerRouteRepository } from './create_observability_server_route_repository';
@ -24,7 +25,7 @@ const alertsDynamicIndexPatternRoute = createObservabilityServerRoute({
const { namespace, registrationContexts } = params.query;
const indexNames = registrationContexts.flatMap((registrationContext) => {
const indexName = ruleDataService
.getRegisteredIndexInfo(registrationContext)
.findIndexByName(registrationContext, Dataset.alerts)
?.getPrimaryAlias(namespace);
if (indexName != null) {

View file

@ -73,7 +73,7 @@ await plugins.ruleRegistry.createOrUpdateComponentTemplate({
await plugins.ruleRegistry.createOrUpdateIndexTemplate({
name: plugins.ruleRegistry.getFullAssetName('apm-index-template'),
body: {
index_patterns: [plugins.ruleRegistry.getFullAssetName('observability-apm*')],
index_patterns: [plugins.ruleRegistry.getFullAssetName('observability.apm*')],
composed_of: [
// Technical component template, required
plugins.ruleRegistry.getFullAssetName(TECHNICAL_COMPONENT_TEMPLATE_NAME),
@ -85,7 +85,7 @@ await plugins.ruleRegistry.createOrUpdateIndexTemplate({
// Finally, create the rule data client that can be injected into rule type
// executors and API endpoints
const ruleDataClient = new RuleDataClient({
alias: plugins.ruleRegistry.getFullAssetName('observability-apm'),
alias: plugins.ruleRegistry.getFullAssetName('observability.apm'),
getClusterClient: async () => {
const coreStart = await getCoreStart();
return coreStart.elasticsearch.client.asInternalUser;

View file

@ -13,14 +13,13 @@ import type {
getEsQueryConfig as getEsQueryConfigTyped,
getSafeSortIds as getSafeSortIdsTyped,
isValidFeatureId as isValidFeatureIdTyped,
mapConsumerToIndexName as mapConsumerToIndexNameTyped,
STATUS_VALUES,
ValidFeatureId,
} from '@kbn/rule-data-utils';
import {
getEsQueryConfig as getEsQueryConfigNonTyped,
getSafeSortIds as getSafeSortIdsNonTyped,
isValidFeatureId as isValidFeatureIdNonTyped,
mapConsumerToIndexName as mapConsumerToIndexNameNonTyped,
// @ts-expect-error
} from '@kbn/rule-data-utils/target_node/alerts_as_data_rbac';
@ -42,11 +41,11 @@ import {
SPACE_IDS,
} from '../../common/technical_rule_data_field_names';
import { ParsedTechnicalFields } from '../../common/parse_technical_fields';
import { Dataset, RuleDataPluginService } from '../rule_data_plugin_service';
const getEsQueryConfig: typeof getEsQueryConfigTyped = getEsQueryConfigNonTyped;
const getSafeSortIds: typeof getSafeSortIdsTyped = getSafeSortIdsNonTyped;
const isValidFeatureId: typeof isValidFeatureIdTyped = isValidFeatureIdNonTyped;
const mapConsumerToIndexName: typeof mapConsumerToIndexNameTyped = mapConsumerToIndexNameNonTyped;
// TODO: Fix typings https://github.com/elastic/kibana/issues/101776
type NonNullableProps<Obj extends {}, Props extends keyof Obj> = Omit<Obj, Props> &
@ -71,6 +70,7 @@ export interface ConstructorOptions {
authorization: PublicMethodsOf<AlertingAuthorization>;
auditLogger?: AuditLogger;
esClient: ElasticsearchClient;
ruleDataService: RuleDataPluginService;
}
export interface UpdateOptions<Params extends AlertTypeParams> {
@ -115,15 +115,17 @@ export class AlertsClient {
private readonly authorization: PublicMethodsOf<AlertingAuthorization>;
private readonly esClient: ElasticsearchClient;
private readonly spaceId: string | undefined;
private readonly ruleDataService: RuleDataPluginService;
constructor({ auditLogger, authorization, logger, esClient }: ConstructorOptions) {
this.logger = logger;
this.authorization = authorization;
this.esClient = esClient;
this.auditLogger = auditLogger;
constructor(options: ConstructorOptions) {
this.logger = options.logger;
this.authorization = options.authorization;
this.esClient = options.esClient;
this.auditLogger = options.auditLogger;
// If spaceId is undefined, it means that spaces is disabled
// Otherwise, if space is enabled and not specified, it is "default"
this.spaceId = this.authorization.getSpaceId();
this.ruleDataService = options.ruleDataService;
}
private getOutcome(
@ -666,15 +668,18 @@ export class AlertsClient {
authorizedFeatures.add(ruleType.producer);
}
const toReturn = Array.from(authorizedFeatures).flatMap((feature) => {
if (featureIds.includes(feature) && isValidFeatureId(feature)) {
if (feature === 'siem') {
return `${mapConsumerToIndexName[feature]}-${this.spaceId}`;
} else {
return `${mapConsumerToIndexName[feature]}`;
}
const validAuthorizedFeatures = Array.from(authorizedFeatures).filter(
(feature): feature is ValidFeatureId =>
featureIds.includes(feature) && isValidFeatureId(feature)
);
const toReturn = validAuthorizedFeatures.flatMap((feature) => {
const indices = this.ruleDataService.findIndicesByFeature(feature, Dataset.alerts);
if (feature === 'siem') {
return indices.map((i) => `${i.baseName}-${this.spaceId}`);
} else {
return indices.map((i) => i.baseName);
}
return [];
});
return toReturn;

View file

@ -13,6 +13,8 @@ import { loggingSystemMock } from 'src/core/server/mocks';
import { securityMock } from '../../../security/server/mocks';
import { AuditLogger } from '../../../security/server';
import { alertingAuthorizationMock } from '../../../alerting/server/authorization/alerting_authorization.mock';
import { ruleDataPluginServiceMock } from '../rule_data_plugin_service/rule_data_plugin_service.mock';
import { RuleDataPluginService } from '../rule_data_plugin_service';
jest.mock('./alerts_client');
@ -24,6 +26,7 @@ const alertsClientFactoryParams: AlertsClientFactoryProps = {
getAlertingAuthorization: (_: KibanaRequest) => alertingAuthMock,
securityPluginSetup,
esClient: {} as ElasticsearchClient,
ruleDataService: (ruleDataPluginServiceMock.create() as unknown) as RuleDataPluginService,
};
const fakeRequest = ({
@ -64,6 +67,7 @@ describe('AlertsClientFactory', () => {
logger: alertsClientFactoryParams.logger,
auditLogger,
esClient: {},
ruleDataService: alertsClientFactoryParams.ruleDataService,
});
});

View file

@ -5,10 +5,11 @@
* 2.0.
*/
import { ElasticsearchClient, KibanaRequest, Logger } from 'src/core/server';
import { PublicMethodsOf } from '@kbn/utility-types';
import { SecurityPluginSetup } from '../../../security/server';
import { ElasticsearchClient, KibanaRequest, Logger } from 'src/core/server';
import { AlertingAuthorization } from '../../../alerting/server';
import { SecurityPluginSetup } from '../../../security/server';
import { RuleDataPluginService } from '../rule_data_plugin_service';
import { AlertsClient } from './alerts_client';
export interface AlertsClientFactoryProps {
@ -16,6 +17,7 @@ export interface AlertsClientFactoryProps {
esClient: ElasticsearchClient;
getAlertingAuthorization: (request: KibanaRequest) => PublicMethodsOf<AlertingAuthorization>;
securityPluginSetup: SecurityPluginSetup | undefined;
ruleDataService: RuleDataPluginService | null;
}
export class AlertsClientFactory {
@ -26,6 +28,7 @@ export class AlertsClientFactory {
request: KibanaRequest
) => PublicMethodsOf<AlertingAuthorization>;
private securityPluginSetup!: SecurityPluginSetup | undefined;
private ruleDataService!: RuleDataPluginService | null;
public initialize(options: AlertsClientFactoryProps) {
/**
@ -40,6 +43,7 @@ export class AlertsClientFactory {
this.logger = options.logger;
this.esClient = options.esClient;
this.securityPluginSetup = options.securityPluginSetup;
this.ruleDataService = options.ruleDataService;
}
public async create(request: KibanaRequest): Promise<AlertsClient> {
@ -50,6 +54,7 @@ export class AlertsClientFactory {
authorization: getAlertingAuthorization(request),
auditLogger: securityPluginSetup?.audit.asScoped(request),
esClient: this.esClient,
ruleDataService: this.ruleDataService!,
});
}
}

View file

@ -18,6 +18,8 @@ import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mo
import { alertingAuthorizationMock } from '../../../../alerting/server/authorization/alerting_authorization.mock';
import { AuditLogger } from '../../../../security/server';
import { AlertingAuthorizationEntity } from '../../../../alerting/server';
import { ruleDataPluginServiceMock } from '../../rule_data_plugin_service/rule_data_plugin_service.mock';
import { RuleDataPluginService } from '../../rule_data_plugin_service';
const alertingAuthMock = alertingAuthorizationMock.create();
const esClientMock = elasticsearchClientMock.createElasticsearchClient();
@ -30,6 +32,7 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
authorization: alertingAuthMock,
esClient: esClientMock,
auditLogger,
ruleDataService: (ruleDataPluginServiceMock.create() as unknown) as RuleDataPluginService,
};
const DEFAULT_SPACE = 'test_default_space_id';
@ -78,7 +81,7 @@ describe('bulkUpdate()', () => {
describe('ids', () => {
describe('audit log', () => {
test('logs successful event in audit logger', async () => {
const indexName = '.alerts-observability-apm.alerts';
const indexName = '.alerts-observability.apm.alerts';
const alertsClient = new AlertsClient(alertsClientParams);
esClientMock.mget.mockResolvedValueOnce(
elasticsearchClientMock.createApiResponse({
@ -107,7 +110,7 @@ describe('bulkUpdate()', () => {
{
update: {
_id: fakeAlertId,
_index: '.alerts-observability-apm.alerts',
_index: '.alerts-observability.apm.alerts',
result: 'updated',
status: 200,
},
@ -135,7 +138,7 @@ describe('bulkUpdate()', () => {
});
test('audit error access if user is unauthorized for given alert', async () => {
const indexName = '.alerts-observability-apm.alerts';
const indexName = '.alerts-observability.apm.alerts';
const alertsClient = new AlertsClient(alertsClientParams);
esClientMock.mget.mockResolvedValueOnce(
elasticsearchClientMock.createApiResponse({
@ -181,7 +184,7 @@ describe('bulkUpdate()', () => {
});
test('logs multiple error events in audit logger', async () => {
const indexName = '.alerts-observability-apm.alerts';
const indexName = '.alerts-observability.apm.alerts';
const alertsClient = new AlertsClient(alertsClientParams);
esClientMock.mget.mockResolvedValueOnce(
elasticsearchClientMock.createApiResponse({
@ -257,7 +260,7 @@ describe('bulkUpdate()', () => {
describe('query', () => {
describe('audit log', () => {
test('logs successful event in audit logger', async () => {
const indexName = '.alerts-observability-apm.alerts';
const indexName = '.alerts-observability.apm.alerts';
const alertsClient = new AlertsClient(alertsClientParams);
esClientMock.search.mockResolvedValueOnce(
elasticsearchClientMock.createApiResponse({
@ -276,7 +279,7 @@ describe('bulkUpdate()', () => {
hits: [
{
_id: fakeAlertId,
_index: '.alerts-observability-apm.alerts',
_index: '.alerts-observability.apm.alerts',
_source: {
[ALERT_RULE_TYPE_ID]: 'apm.error_rate',
[ALERT_RULE_CONSUMER]: 'apm',
@ -317,7 +320,7 @@ describe('bulkUpdate()', () => {
});
test('audit error access if user is unauthorized for given alert', async () => {
const indexName = '.alerts-observability-apm.alerts';
const indexName = '.alerts-observability.apm.alerts';
const alertsClient = new AlertsClient(alertsClientParams);
esClientMock.search.mockResolvedValueOnce(
elasticsearchClientMock.createApiResponse({
@ -336,7 +339,7 @@ describe('bulkUpdate()', () => {
hits: [
{
_id: fakeAlertId,
_index: '.alerts-observability-apm.alerts',
_index: '.alerts-observability.apm.alerts',
_source: {
[ALERT_RULE_TYPE_ID]: fakeRuleTypeId,
[ALERT_RULE_CONSUMER]: 'apm',
@ -378,7 +381,7 @@ describe('bulkUpdate()', () => {
});
test('logs multiple error events in audit logger', async () => {
const indexName = '.alerts-observability-apm.alerts';
const indexName = '.alerts-observability.apm.alerts';
const alertsClient = new AlertsClient(alertsClientParams);
esClientMock.search.mockResolvedValueOnce(
elasticsearchClientMock.createApiResponse({
@ -397,7 +400,7 @@ describe('bulkUpdate()', () => {
hits: [
{
_id: successfulAuthzHit,
_index: '.alerts-observability-apm.alerts',
_index: '.alerts-observability.apm.alerts',
_source: {
[ALERT_RULE_TYPE_ID]: 'apm.error_rate',
[ALERT_RULE_CONSUMER]: 'apm',
@ -407,7 +410,7 @@ describe('bulkUpdate()', () => {
},
{
_id: unsuccessfulAuthzHit,
_index: '.alerts-observability-apm.alerts',
_index: '.alerts-observability.apm.alerts',
_source: {
[ALERT_RULE_TYPE_ID]: fakeRuleTypeId,
[ALERT_RULE_CONSUMER]: 'apm',

View file

@ -18,6 +18,8 @@ import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mo
import { alertingAuthorizationMock } from '../../../../alerting/server/authorization/alerting_authorization.mock';
import { AuditLogger } from '../../../../security/server';
import { AlertingAuthorizationEntity } from '../../../../alerting/server';
import { ruleDataPluginServiceMock } from '../../rule_data_plugin_service/rule_data_plugin_service.mock';
import { RuleDataPluginService } from '../../rule_data_plugin_service';
const alertingAuthMock = alertingAuthorizationMock.create();
const esClientMock = elasticsearchClientMock.createElasticsearchClient();
@ -30,6 +32,7 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
authorization: alertingAuthMock,
esClient: esClientMock,
auditLogger,
ruleDataService: (ruleDataPluginServiceMock.create() as unknown) as RuleDataPluginService,
};
const DEFAULT_SPACE = 'test_default_space_id';
@ -90,7 +93,7 @@ describe('find()', () => {
{
found: true,
_type: 'alert',
_index: '.alerts-observability-apm',
_index: '.alerts-observability.apm.alerts',
_id: 'NoxgpHkBqbdrfX07MqXV',
_version: 1,
_seq_no: 362,
@ -110,7 +113,7 @@ describe('find()', () => {
);
const result = await alertsClient.find({
query: { match: { [ALERT_WORKFLOW_STATUS]: 'open' } },
index: '.alerts-observability-apm',
index: '.alerts-observability.apm.alerts',
});
expect(result).toMatchInlineSnapshot(`
Object {
@ -124,7 +127,7 @@ describe('find()', () => {
"hits": Array [
Object {
"_id": "NoxgpHkBqbdrfX07MqXV",
"_index": ".alerts-observability-apm",
"_index": ".alerts-observability.apm.alerts",
"_primary_term": 2,
"_seq_no": 362,
"_source": Object {
@ -194,7 +197,7 @@ describe('find()', () => {
"track_total_hits": undefined,
},
"ignore_unavailable": true,
"index": ".alerts-observability-apm",
"index": ".alerts-observability.apm.alerts",
"seq_no_primary_term": true,
},
]
@ -221,7 +224,7 @@ describe('find()', () => {
{
found: true,
_type: 'alert',
_index: '.alerts-observability-apm',
_index: '.alerts-observability.apm.alerts',
_id: 'NoxgpHkBqbdrfX07MqXV',
_version: 1,
_seq_no: 362,
@ -241,7 +244,7 @@ describe('find()', () => {
);
await alertsClient.find({
query: { match: { [ALERT_WORKFLOW_STATUS]: 'open' } },
index: '.alerts-observability-apm',
index: '.alerts-observability.apm.alerts',
});
expect(auditLogger.log).toHaveBeenCalledWith({
@ -252,7 +255,7 @@ describe('find()', () => {
});
test('audit error access if user is unauthorized for given alert', async () => {
const indexName = '.alerts-observability-apm';
const indexName = '.alerts-observability.apm.alerts';
const fakeAlertId = 'myfakeid1';
// fakeRuleTypeId will cause authz to fail
const fakeRuleTypeId = 'fake.rule';
@ -296,7 +299,7 @@ describe('find()', () => {
await expect(
alertsClient.find({
query: { match: { [ALERT_WORKFLOW_STATUS]: 'open' } },
index: '.alerts-observability-apm',
index: '.alerts-observability.apm.alerts',
})
).rejects.toThrowErrorMatchingInlineSnapshot(`
"Unable to retrieve alert details for alert with id of \\"undefined\\" or with query \\"[object Object]\\" and operation find
@ -326,7 +329,7 @@ describe('find()', () => {
await expect(
alertsClient.find({
query: { match: { [ALERT_WORKFLOW_STATUS]: 'open' } },
index: '.alerts-observability-apm',
index: '.alerts-observability.apm.alerts',
})
).rejects.toThrowErrorMatchingInlineSnapshot(`
"Unable to retrieve alert details for alert with id of \\"undefined\\" or with query \\"[object Object]\\" and operation find
@ -354,7 +357,7 @@ describe('find()', () => {
{
found: true,
_type: 'alert',
_index: '.alerts-observability-apm',
_index: '.alerts-observability.apm.alerts',
_id: 'NoxgpHkBqbdrfX07MqXV',
_version: 1,
_seq_no: 362,
@ -378,7 +381,7 @@ describe('find()', () => {
const alertsClient = new AlertsClient(alertsClientParams);
const result = await alertsClient.find({
query: { match: { [ALERT_WORKFLOW_STATUS]: 'open' } },
index: '.alerts-observability-apm',
index: '.alerts-observability.apm.alerts',
});
expect(result).toMatchInlineSnapshot(`
@ -393,7 +396,7 @@ describe('find()', () => {
"hits": Array [
Object {
"_id": "NoxgpHkBqbdrfX07MqXV",
"_index": ".alerts-observability-apm",
"_index": ".alerts-observability.apm.alerts",
"_primary_term": 2,
"_seq_no": 362,
"_source": Object {

View file

@ -18,6 +18,8 @@ import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mo
import { alertingAuthorizationMock } from '../../../../alerting/server/authorization/alerting_authorization.mock';
import { AuditLogger } from '../../../../security/server';
import { AlertingAuthorizationEntity } from '../../../../alerting/server';
import { ruleDataPluginServiceMock } from '../../rule_data_plugin_service/rule_data_plugin_service.mock';
import { RuleDataPluginService } from '../../rule_data_plugin_service';
const alertingAuthMock = alertingAuthorizationMock.create();
const esClientMock = elasticsearchClientMock.createElasticsearchClient();
@ -30,6 +32,7 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
authorization: alertingAuthMock,
esClient: esClientMock,
auditLogger,
ruleDataService: (ruleDataPluginServiceMock.create() as unknown) as RuleDataPluginService,
};
const DEFAULT_SPACE = 'test_default_space_id';
@ -91,7 +94,7 @@ describe('get()', () => {
{
found: true,
_type: 'alert',
_index: '.alerts-observability-apm',
_index: '.alerts-observability.apm.alerts',
_id: 'NoxgpHkBqbdrfX07MqXV',
_version: 1,
_seq_no: 362,
@ -109,7 +112,7 @@ describe('get()', () => {
},
})
);
const result = await alertsClient.get({ id: '1', index: '.alerts-observability-apm' });
const result = await alertsClient.get({ id: '1', index: '.alerts-observability.apm.alerts' });
expect(result).toMatchInlineSnapshot(`
Object {
"kibana.alert.rule.consumer": "apm",
@ -173,7 +176,7 @@ describe('get()', () => {
"track_total_hits": undefined,
},
"ignore_unavailable": true,
"index": ".alerts-observability-apm",
"index": ".alerts-observability.apm.alerts",
"seq_no_primary_term": true,
},
]
@ -200,7 +203,7 @@ describe('get()', () => {
{
found: true,
_type: 'alert',
_index: '.alerts-observability-apm',
_index: '.alerts-observability.apm.alerts',
_id: 'NoxgpHkBqbdrfX07MqXV',
_version: 1,
_seq_no: 362,
@ -218,7 +221,10 @@ describe('get()', () => {
},
})
);
await alertsClient.get({ id: 'NoxgpHkBqbdrfX07MqXV', index: '.alerts-observability-apm' });
await alertsClient.get({
id: 'NoxgpHkBqbdrfX07MqXV',
index: '.alerts-observability.apm.alerts',
});
expect(auditLogger.log).toHaveBeenCalledWith({
error: undefined,
@ -228,7 +234,7 @@ describe('get()', () => {
});
test('audit error access if user is unauthorized for given alert', async () => {
const indexName = '.alerts-observability-apm.alerts';
const indexName = '.alerts-observability.apm.alerts';
const fakeAlertId = 'myfakeid1';
// fakeRuleTypeId will cause authz to fail
const fakeRuleTypeId = 'fake.rule';
@ -269,7 +275,7 @@ describe('get()', () => {
})
);
await expect(alertsClient.get({ id: fakeAlertId, index: '.alerts-observability-apm.alerts' }))
await expect(alertsClient.get({ id: fakeAlertId, index: '.alerts-observability.apm.alerts' }))
.rejects.toThrowErrorMatchingInlineSnapshot(`
"Unable to retrieve alert details for alert with id of \\"myfakeid1\\" or with query \\"undefined\\" and operation get
Error: Error: Unauthorized for fake.rule and apm"
@ -296,7 +302,7 @@ describe('get()', () => {
esClientMock.search.mockRejectedValue(error);
await expect(
alertsClient.get({ id: 'NoxgpHkBqbdrfX07MqXV', index: '.alerts-observability-apm' })
alertsClient.get({ id: 'NoxgpHkBqbdrfX07MqXV', index: '.alerts-observability.apm.alerts' })
).rejects.toThrowErrorMatchingInlineSnapshot(`
"Unable to retrieve alert details for alert with id of \\"NoxgpHkBqbdrfX07MqXV\\" or with query \\"undefined\\" and operation get
Error: Error: something went wrong"
@ -323,7 +329,7 @@ describe('get()', () => {
{
found: true,
_type: 'alert',
_index: '.alerts-observability-apm',
_index: '.alerts-observability.apm.alerts',
_id: 'NoxgpHkBqbdrfX07MqXV',
_version: 1,
_seq_no: 362,
@ -347,7 +353,7 @@ describe('get()', () => {
const alertsClient = new AlertsClient(alertsClientParams);
const result = await alertsClient.get({
id: 'NoxgpHkBqbdrfX07MqXV',
index: '.alerts-observability-apm',
index: '.alerts-observability.apm.alerts',
});
expect(result).toMatchInlineSnapshot(`

View file

@ -18,6 +18,8 @@ import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mo
import { alertingAuthorizationMock } from '../../../../alerting/server/authorization/alerting_authorization.mock';
import { AuditLogger } from '../../../../security/server';
import { AlertingAuthorizationEntity } from '../../../../alerting/server';
import { ruleDataPluginServiceMock } from '../../rule_data_plugin_service/rule_data_plugin_service.mock';
import { RuleDataPluginService } from '../../rule_data_plugin_service';
const alertingAuthMock = alertingAuthorizationMock.create();
const esClientMock = elasticsearchClientMock.createElasticsearchClient();
@ -30,6 +32,7 @@ const alertsClientParams: jest.Mocked<ConstructorOptions> = {
authorization: alertingAuthMock,
esClient: esClientMock,
auditLogger,
ruleDataService: (ruleDataPluginServiceMock.create() as unknown) as RuleDataPluginService,
};
const DEFAULT_SPACE = 'test_default_space_id';
@ -91,7 +94,7 @@ describe('update()', () => {
{
found: true,
_type: 'alert',
_index: '.alerts-observability-apm',
_index: '.alerts-observability.apm.alerts',
_id: 'NoxgpHkBqbdrfX07MqXV',
_source: {
[ALERT_RULE_TYPE_ID]: 'apm.error_rate',
@ -109,7 +112,7 @@ describe('update()', () => {
esClientMock.update.mockResolvedValueOnce(
elasticsearchClientMock.createApiResponse({
body: {
_index: '.alerts-observability-apm',
_index: '.alerts-observability.apm.alerts',
_id: 'NoxgpHkBqbdrfX07MqXV',
_version: 2,
result: 'updated',
@ -123,12 +126,12 @@ describe('update()', () => {
id: '1',
status: 'closed',
_version: undefined,
index: '.alerts-observability-apm',
index: '.alerts-observability.apm.alerts',
});
expect(result).toMatchInlineSnapshot(`
Object {
"_id": "NoxgpHkBqbdrfX07MqXV",
"_index": ".alerts-observability-apm",
"_index": ".alerts-observability.apm.alerts",
"_primary_term": 1,
"_seq_no": 1,
"_shards": Object {
@ -150,7 +153,7 @@ describe('update()', () => {
},
},
"id": "1",
"index": ".alerts-observability-apm",
"index": ".alerts-observability.apm.alerts",
"refresh": "wait_for",
},
]
@ -177,7 +180,7 @@ describe('update()', () => {
{
found: true,
_type: 'alert',
_index: '.alerts-observability-apm',
_index: '.alerts-observability.apm.alerts',
_id: 'NoxgpHkBqbdrfX07MqXV',
_source: {
[ALERT_RULE_TYPE_ID]: 'apm.error_rate',
@ -195,7 +198,7 @@ describe('update()', () => {
esClientMock.update.mockResolvedValueOnce(
elasticsearchClientMock.createApiResponse({
body: {
_index: '.alerts-observability-apm',
_index: '.alerts-observability.apm.alerts',
_id: 'NoxgpHkBqbdrfX07MqXV',
_version: 2,
result: 'updated',
@ -209,7 +212,7 @@ describe('update()', () => {
id: 'NoxgpHkBqbdrfX07MqXV',
status: 'closed',
_version: undefined,
index: '.alerts-observability-apm',
index: '.alerts-observability.apm.alerts',
});
expect(auditLogger.log).toHaveBeenCalledWith({
@ -225,7 +228,7 @@ describe('update()', () => {
});
test('audit error update if user is unauthorized for given alert', async () => {
const indexName = '.alerts-observability-apm.alerts';
const indexName = '.alerts-observability.apm.alerts';
const fakeAlertId = 'myfakeid1';
// fakeRuleTypeId will cause authz to fail
const fakeRuleTypeId = 'fake.rule';
@ -271,7 +274,7 @@ describe('update()', () => {
id: fakeAlertId,
status: 'closed',
_version: '1',
index: '.alerts-observability-apm',
index: '.alerts-observability.apm.alerts',
})
).rejects.toThrowErrorMatchingInlineSnapshot(`
"Unable to retrieve alert details for alert with id of \\"myfakeid1\\" or with query \\"undefined\\" and operation update
@ -303,7 +306,7 @@ describe('update()', () => {
id: 'NoxgpHkBqbdrfX07MqXV',
status: 'closed',
_version: undefined,
index: '.alerts-observability-apm',
index: '.alerts-observability.apm.alerts',
})
).rejects.toThrowErrorMatchingInlineSnapshot(`
"Unable to retrieve alert details for alert with id of \\"NoxgpHkBqbdrfX07MqXV\\" or with query \\"undefined\\" and operation update
@ -332,7 +335,7 @@ describe('update()', () => {
{
found: true,
_type: 'alert',
_index: '.alerts-observability-apm',
_index: '.alerts-observability.apm.alerts',
_id: 'NoxgpHkBqbdrfX07MqXV',
_source: {
[ALERT_RULE_TYPE_ID]: 'apm.error_rate',
@ -354,7 +357,7 @@ describe('update()', () => {
id: 'NoxgpHkBqbdrfX07MqXV',
status: 'closed',
_version: undefined,
index: '.alerts-observability-apm',
index: '.alerts-observability.apm.alerts',
})
).rejects.toThrowErrorMatchingInlineSnapshot(`"something went wrong on update"`);
expect(auditLogger.log).toHaveBeenCalledWith({
@ -389,7 +392,7 @@ describe('update()', () => {
{
found: true,
_type: 'alert',
_index: '.alerts-observability-apm',
_index: '.alerts-observability.apm.alerts',
_id: 'NoxgpHkBqbdrfX07MqXV',
_version: 2,
_seq_no: 362,
@ -411,7 +414,7 @@ describe('update()', () => {
esClientMock.update.mockResolvedValueOnce(
elasticsearchClientMock.createApiResponse({
body: {
_index: '.alerts-observability-apm',
_index: '.alerts-observability.apm.alerts',
_id: 'NoxgpHkBqbdrfX07MqXV',
_version: 2,
result: 'updated',
@ -429,13 +432,13 @@ describe('update()', () => {
id: 'NoxgpHkBqbdrfX07MqXV',
status: 'closed',
_version: undefined,
index: '.alerts-observability-apm',
index: '.alerts-observability.apm.alerts',
});
expect(result).toMatchInlineSnapshot(`
Object {
"_id": "NoxgpHkBqbdrfX07MqXV",
"_index": ".alerts-observability-apm",
"_index": ".alerts-observability.apm.alerts",
"_primary_term": 1,
"_seq_no": 1,
"_shards": Object {

View file

@ -125,7 +125,7 @@ export class RuleRegistryPlugin
core: CoreStart,
plugins: RuleRegistryPluginStartDependencies
): RuleRegistryPluginStartContract {
const { logger, alertsClientFactory, security } = this;
const { logger, alertsClientFactory, ruleDataService, security } = this;
alertsClientFactory.initialize({
logger,
@ -135,6 +135,7 @@ export class RuleRegistryPlugin
return plugins.alerting.getAlertingAuthorizationWithRequest(request);
},
securityPluginSetup: security,
ruleDataService,
});
const getRacClientWithRequest = (request: KibanaRequest) => {

View file

@ -29,6 +29,6 @@ export const getUpdateRequest = () =>
body: {
status: 'closed',
ids: ['alert-1'],
index: '.alerts-observability-apm*',
index: '.alerts-observability.apm.alerts*',
},
});

View file

@ -20,7 +20,7 @@ describe('updateAlertByIdRoute', () => {
({ clients, context } = requestContextMock.createTools());
clients.rac.update.mockResolvedValue({
_index: '.alerts-observability-apm',
_index: '.alerts-observability.apm.alerts',
_id: 'NoxgpHkBqbdrfX07MqXV',
_version: 'WzM2MiwyXQ==',
result: 'updated',
@ -37,7 +37,7 @@ describe('updateAlertByIdRoute', () => {
expect(response.status).toEqual(200);
expect(response.body).toEqual({
_index: '.alerts-observability-apm',
_index: '.alerts-observability.apm.alerts',
_id: 'NoxgpHkBqbdrfX07MqXV',
_version: 'WzM2MiwyXQ==',
result: 'updated',
@ -58,7 +58,7 @@ describe('updateAlertByIdRoute', () => {
body: {
status: 'closed',
ids: 'alert-1',
index: '.alerts-observability-apm*',
index: '.alerts-observability.apm.alerts*',
},
}),
context
@ -77,7 +77,7 @@ describe('updateAlertByIdRoute', () => {
body: {
notStatus: 'closed',
ids: ['alert-1'],
index: '.alerts-observability-apm*',
index: '.alerts-observability.apm.alerts*',
},
}),
context

View file

@ -12,18 +12,17 @@ type Schema = PublicMethodsOf<RuleDataPluginService>;
const createRuleDataPluginService = () => {
const mocked: jest.Mocked<Schema> = {
getRegisteredIndexInfo: jest.fn(),
getResourcePrefix: jest.fn(),
getResourceName: jest.fn(),
isWriteEnabled: jest.fn(),
initializeService: jest.fn(),
initializeIndex: jest.fn(),
findIndexByName: jest.fn(),
findIndicesByFeature: jest.fn(),
};
return mocked;
};
export const ruleDataPluginServiceMock: {
create: () => jest.Mocked<PublicMethodsOf<RuleDataPluginService>>;
} = {
export const ruleDataPluginServiceMock = {
create: createRuleDataPluginService,
};

View file

@ -6,12 +6,13 @@
*/
import { Either, isLeft, left, right } from 'fp-ts/lib/Either';
import { ValidFeatureId } from '@kbn/rule-data-utils';
import { ElasticsearchClient, Logger } from 'kibana/server';
import { IRuleDataClient, RuleDataClient, WaitResult } from '../rule_data_client';
import { IndexInfo } from './index_info';
import { IndexOptions } from './index_options';
import { Dataset, IndexOptions } from './index_options';
import { ResourceInstaller } from './resource_installer';
import { joinWithDash } from './utils';
@ -26,12 +27,16 @@ interface ConstructorOptions {
* A service for creating and using Elasticsearch indices for alerts-as-data.
*/
export class RuleDataPluginService {
private readonly indicesByBaseName: Map<string, IndexInfo>;
private readonly indicesByFeatureId: Map<string, IndexInfo[]>;
private readonly resourceInstaller: ResourceInstaller;
private installCommonResources: Promise<Either<Error, 'ok'>>;
private isInitialized: boolean;
private registeredIndices: Map<string, IndexInfo> = new Map();
constructor(private readonly options: ConstructorOptions) {
this.indicesByBaseName = new Map();
this.indicesByFeatureId = new Map();
this.resourceInstaller = new ResourceInstaller({
getResourceName: (name) => this.getResourceName(name),
getClusterClient: options.getClusterClient,
@ -106,7 +111,9 @@ export class RuleDataPluginService {
indexOptions,
});
this.registeredIndices.set(indexOptions.registrationContext, indexInfo);
const indicesAssociatedWithFeature = this.indicesByFeatureId.get(indexOptions.feature) ?? [];
this.indicesByFeatureId.set(indexOptions.feature, [...indicesAssociatedWithFeature, indexInfo]);
this.indicesByBaseName.set(indexInfo.baseName, indexInfo);
const waitUntilClusterClientAvailable = async (): Promise<WaitResult> => {
try {
@ -153,11 +160,19 @@ export class RuleDataPluginService {
}
/**
* Looks up the index information associated with the given `registrationContext`.
* @param registrationContext
* @returns the IndexInfo or undefined
* Looks up the index information associated with the given registration context and dataset.
*/
public getRegisteredIndexInfo(registrationContext: string): IndexInfo | undefined {
return this.registeredIndices.get(registrationContext);
public findIndexByName(registrationContext: string, dataset: Dataset): IndexInfo | null {
const baseName = this.getResourceName(`${registrationContext}.${dataset}`);
return this.indicesByBaseName.get(baseName) ?? null;
}
/**
* Looks up the index information associated with the given Kibana "feature".
* Note: features are used in RBAC.
*/
public findIndicesByFeature(featureId: ValidFeatureId, dataset?: Dataset): IndexInfo[] {
const foundIndices = this.indicesByFeatureId.get(featureId) ?? [];
return dataset ? foundIndices.filter((i) => i.indexOptions.dataset === dataset) : foundIndices;
}
}

View file

@ -25,6 +25,6 @@ curl -s -k \
-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 .
-d "{\"ids\": $IDS, \"status\":\"$STATUS\", \"index\":\".alerts-observability.apm.alerts\"}" | jq .
# -d "{\"ids\": $IDS, \"query\": \"kibana.rac.alert.status: open\", \"status\":\"$STATUS\", \"index\":\".alerts-observability.apm.alerts\"}" | jq .
# -d "{\"ids\": $IDS, \"status\":\"$STATUS\", \"index\":\".alerts-observability.apm.alerts\"}" | jq .

View file

@ -25,4 +25,4 @@ curl -s -k \
-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 .
-d "{\"query\": \"$QUERY\", \"status\":\"$STATUS\", \"index\":\".alerts-observability.apm.alerts\"}" | jq .

View file

@ -26,4 +26,4 @@ curl -v \
-H 'kbn-xsrf: 123' \
-u observer:changeme \
-X POST ${KIBANA_URL}${SPACE_URL}/internal/rac/alerts/find \
-d "{\"query\": { \"match\": { \"kibana.alert.status\": \"open\" }}, \"index\":\".alerts-observability-apm\"}" | jq .
-d "{\"query\": { \"match\": { \"kibana.alert.status\": \"open\" }}, \"index\":\".alerts-observability.apm.alerts\"}" | jq .

View file

@ -19,4 +19,4 @@ cd ..
# Example: ./get_observability_alert.sh hunter
curl -v -k \
-u $USER:changeme \
-X GET "${KIBANA_URL}${SPACE_URL}/internal/rac/alerts?id=$ID&index=.alerts-observability-apm" | jq .
-X GET "${KIBANA_URL}${SPACE_URL}/internal/rac/alerts?id=$ID&index=.alerts-observability.apm.alerts" | jq .

View file

@ -25,4 +25,4 @@ curl -s -k \
-H 'kbn-xsrf: 123' \
-u observer:changeme \
-X POST ${KIBANA_URL}${SPACE_URL}/internal/rac/alerts \
-d "{\"ids\": $IDS, \"status\":\"$STATUS\", \"index\":\".alerts-observability-apm\"}" | jq .
-d "{\"ids\": $IDS, \"status\":\"$STATUS\", \"index\":\".alerts-observability.apm.alerts\"}" | jq .

View file

@ -73,7 +73,7 @@ export const createRuleTypeMocks = (
};
},
isWriteEnabled: jest.fn(() => true),
indexName: '.alerts-observability.synthetics.alerts',
indexName: '.alerts-observability.uptime.alerts',
} as unknown) as IRuleDataClient,
},
services,

View file

@ -1,7 +1,7 @@
{
"type": "doc",
"value": {
"index": ".alerts-observability-apm",
"index": ".alerts-observability.apm.alerts",
"id": "NoxgpHkBqbdrfX07MqXV",
"source": {
"event.kind" : "signal",
@ -18,7 +18,7 @@
{
"type": "doc",
"value": {
"index": ".alerts-observability-apm",
"index": ".alerts-observability.apm.alerts",
"id": "space1alert",
"source": {
"event.kind" : "signal",
@ -35,7 +35,7 @@
{
"type": "doc",
"value": {
"index": ".alerts-observability-apm",
"index": ".alerts-observability.apm.alerts",
"id": "space2alert",
"source": {
"event.kind" : "signal",

View file

@ -1,7 +1,7 @@
{
"type": "index",
"value": {
"index": ".alerts-observability-apm",
"index": ".alerts-observability.apm.alerts",
"mappings": {
"properties": {
"message": {

View file

@ -55,7 +55,7 @@ export default ({ getService }: FtrProviderContext) => {
const SPACE1 = 'space1';
const SPACE2 = 'space2';
const APM_ALERT_ID = 'NoxgpHkBqbdrfX07MqXV';
const APM_ALERT_INDEX = '.alerts-observability-apm';
const APM_ALERT_INDEX = '.alerts-observability.apm.alerts';
const SECURITY_SOLUTION_ALERT_ID = '020202';
const SECURITY_SOLUTION_ALERT_INDEX = '.alerts-security.alerts';

View file

@ -54,7 +54,7 @@ export default ({ getService }: FtrProviderContext) => {
const SPACE1 = 'space1';
const SPACE2 = 'space2';
const APM_ALERT_ID = 'NoxgpHkBqbdrfX07MqXV';
const APM_ALERT_INDEX = '.alerts-observability-apm';
const APM_ALERT_INDEX = '.alerts-observability.apm.alerts';
const SECURITY_SOLUTION_ALERT_ID = '020202';
const SECURITY_SOLUTION_ALERT_INDEX = '.alerts-security.alerts';

View file

@ -54,7 +54,7 @@ export default ({ getService }: FtrProviderContext) => {
const SPACE1 = 'space1';
const SPACE2 = 'space2';
const APM_ALERT_ID = 'NoxgpHkBqbdrfX07MqXV';
const APM_ALERT_INDEX = '.alerts-observability-apm';
const APM_ALERT_INDEX = '.alerts-observability.apm.alerts';
const SECURITY_SOLUTION_ALERT_ID = '020202';
const SECURITY_SOLUTION_ALERT_INDEX = '.alerts-security.alerts';

View file

@ -53,7 +53,7 @@ export default ({ getService }: FtrProviderContext) => {
const SPACE1 = 'space1';
const SPACE2 = 'space2';
const APM_ALERT_ID = 'NoxgpHkBqbdrfX07MqXV';
const APM_ALERT_INDEX = '.alerts-observability-apm';
const APM_ALERT_INDEX = '.alerts-observability.apm.alerts';
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

View file

@ -38,9 +38,9 @@ export default ({ getService }: FtrProviderContext) => {
.set('kbn-xsrf', 'true')
.expect(200);
const observabilityIndex = indexNames?.index_name?.find(
(indexName) => indexName === '.alerts-observability-apm'
(indexName) => indexName === '.alerts-observability.apm.alerts'
);
expect(observabilityIndex).to.eql('.alerts-observability-apm');
expect(observabilityIndex).to.eql('.alerts-observability.apm.alerts');
return observabilityIndex;
};

View file

@ -36,9 +36,9 @@ export default ({ getService }: FtrProviderContext) => {
.set('kbn-xsrf', 'true')
.expect(200);
const observabilityIndex = indexNames?.index_name?.find(
(indexName) => indexName === '.alerts-observability-apm'
(indexName) => indexName === '.alerts-observability.apm.alerts'
);
expect(observabilityIndex).to.eql('.alerts-observability-apm');
expect(observabilityIndex).to.eql('.alerts-observability.apm.alerts');
return observabilityIndex;
};
@ -107,7 +107,7 @@ export default ({ getService }: FtrProviderContext) => {
.expect(200);
expect(omit(['_version', '_seq_no'], res.body)).to.eql({
success: true,
_index: '.alerts-observability-apm',
_index: '.alerts-observability.apm.alerts',
_id: 'NoxgpHkBqbdrfX07MqXV',
result: 'updated',
_shards: { total: 2, successful: 1, failed: 0 },

View file

@ -22,7 +22,7 @@ export default ({ getService }: FtrProviderContext) => {
const SPACE1 = 'space1';
const SPACE2 = 'space2';
const APM_ALERT_ID = 'NoxgpHkBqbdrfX07MqXV';
const APM_ALERT_INDEX = '.alerts-observability-apm';
const APM_ALERT_INDEX = '.alerts-observability.apm.alerts';
const SECURITY_SOLUTION_ALERT_INDEX = '.alerts-security.alerts';
const getAPMIndexName = async (user: User) => {

View file

@ -21,7 +21,7 @@ export default ({ getService }: FtrProviderContext) => {
const SPACE1 = 'space1';
const SPACE2 = 'space2';
const APM_ALERT_ID = 'NoxgpHkBqbdrfX07MqXV';
const APM_ALERT_INDEX = '.alerts-observability-apm';
const APM_ALERT_INDEX = '.alerts-observability.apm.alerts';
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