mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
# Backport This will backport the following commits from `main` to `8.10`: - [[RAM] Use ruletype to determine alert indices (#163574)](https://github.com/elastic/kibana/pull/163574) <!--- Backport version: 8.9.7 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Xavier Mouligneau","email":"xavier.mouligneau@elastic.co"},"sourceCommit":{"committedDate":"2023-08-21T21:52:36Z","message":"[RAM] Use ruletype to determine alert indices (#163574)\n\n## Summary\r\n\r\nWe were using the feature Id to determine the alert indices, but we\r\nrealized that we should use the rule type id instead. Meaning that we\r\ncheck which rule type does the user have access and then we get the\r\nindices related to this rule type.\r\n\r\nWe also took advantage of the new suggestion abstraction of the search\r\nbar components to remove the toaster of hell ->\r\nhttps://github.com/elastic/kibana/issues/163003\r\n\r\n\r\n### Checklist\r\n\r\n- [ ] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"d7bf7efcdfb46ef8d44e9b554d670e13010bfab6","branchLabelMapping":{"^v8.11.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["bug","release_note:skip","impact:high","Team:ResponseOps","v8.10.0","v8.11.0"],"number":163574,"url":"https://github.com/elastic/kibana/pull/163574","mergeCommit":{"message":"[RAM] Use ruletype to determine alert indices (#163574)\n\n## Summary\r\n\r\nWe were using the feature Id to determine the alert indices, but we\r\nrealized that we should use the rule type id instead. Meaning that we\r\ncheck which rule type does the user have access and then we get the\r\nindices related to this rule type.\r\n\r\nWe also took advantage of the new suggestion abstraction of the search\r\nbar components to remove the toaster of hell ->\r\nhttps://github.com/elastic/kibana/issues/163003\r\n\r\n\r\n### Checklist\r\n\r\n- [ ] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"d7bf7efcdfb46ef8d44e9b554d670e13010bfab6"}},"sourceBranch":"main","suggestedTargetBranches":["8.10"],"targetPullRequestStates":[{"branch":"8.10","label":"v8.10.0","labelRegex":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v8.11.0","labelRegex":"^v8.11.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/163574","number":163574,"mergeCommit":{"message":"[RAM] Use ruletype to determine alert indices (#163574)\n\n## Summary\r\n\r\nWe were using the feature Id to determine the alert indices, but we\r\nrealized that we should use the rule type id instead. Meaning that we\r\ncheck which rule type does the user have access and then we get the\r\nindices related to this rule type.\r\n\r\nWe also took advantage of the new suggestion abstraction of the search\r\nbar components to remove the toaster of hell ->\r\nhttps://github.com/elastic/kibana/issues/163003\r\n\r\n\r\n### Checklist\r\n\r\n- [ ] [Unit or functional\r\ntests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)\r\nwere updated or added to match the most common scenarios\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>","sha":"d7bf7efcdfb46ef8d44e9b554d670e13010bfab6"}}]}] BACKPORT--> Co-authored-by: Xavier Mouligneau <xavier.mouligneau@elastic.co>
This commit is contained in:
parent
723cdf73c5
commit
e38772f48b
25 changed files with 765 additions and 304 deletions
|
@ -16,6 +16,7 @@ const createAlertingAuthorizationMock = () => {
|
|||
ensureAuthorized: jest.fn(),
|
||||
filterByRuleTypeAuthorization: jest.fn(),
|
||||
getAuthorizationFilter: jest.fn(),
|
||||
getAuthorizedRuleTypes: jest.fn(),
|
||||
getFindAuthorizationFilter: jest.fn(),
|
||||
getAugmentedRuleTypesWithAuthorization: jest.fn(),
|
||||
getSpaceId: jest.fn(),
|
||||
|
|
|
@ -52,27 +52,28 @@ function mockSecurity() {
|
|||
return { authorization };
|
||||
}
|
||||
|
||||
function mockFeature(appName: string, typeName?: string) {
|
||||
function mockFeature(appName: string, typeName?: string | string[]) {
|
||||
const typeNameArray = typeName ? (Array.isArray(typeName) ? typeName : [typeName]) : undefined;
|
||||
return new KibanaFeature({
|
||||
id: appName,
|
||||
name: appName,
|
||||
app: [],
|
||||
category: { id: 'foo', label: 'foo' },
|
||||
...(typeName
|
||||
...(typeNameArray
|
||||
? {
|
||||
alerting: [typeName],
|
||||
alerting: typeNameArray,
|
||||
}
|
||||
: {}),
|
||||
privileges: {
|
||||
all: {
|
||||
...(typeName
|
||||
...(typeNameArray
|
||||
? {
|
||||
alerting: {
|
||||
rule: {
|
||||
all: [typeName],
|
||||
all: typeNameArray,
|
||||
},
|
||||
alert: {
|
||||
all: [typeName],
|
||||
all: typeNameArray,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -84,14 +85,14 @@ function mockFeature(appName: string, typeName?: string) {
|
|||
ui: [],
|
||||
},
|
||||
read: {
|
||||
...(typeName
|
||||
...(typeNameArray
|
||||
? {
|
||||
alerting: {
|
||||
rule: {
|
||||
read: [typeName],
|
||||
read: typeNameArray,
|
||||
},
|
||||
alert: {
|
||||
read: [typeName],
|
||||
read: typeNameArray,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -815,6 +816,12 @@ describe('AlertingAuthorization', () => {
|
|||
ensureRuleTypeIsAuthorized('someMadeUpType', 'myApp', 'rule');
|
||||
});
|
||||
test('creates a filter based on the privileged types', async () => {
|
||||
features.getKibanaFeatures.mockReturnValue([
|
||||
mockFeature('myApp', ['myAppAlertType', 'mySecondAppAlertType']),
|
||||
mockFeature('alerts', 'myOtherAppAlertType'),
|
||||
myOtherAppFeature,
|
||||
myAppWithSubFeature,
|
||||
]);
|
||||
const { authorization } = mockSecurity();
|
||||
const checkPrivileges: jest.MockedFunction<
|
||||
ReturnType<typeof authorization.checkPrivilegesDynamicallyWithRequest>
|
||||
|
@ -846,7 +853,7 @@ describe('AlertingAuthorization', () => {
|
|||
).filter
|
||||
).toEqual(
|
||||
fromKueryExpression(
|
||||
`((path.to.rule_type_id:myAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule_type_id:myOtherAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule_type_id:mySecondAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)))`
|
||||
`((path.to.rule_type_id:myAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule_type_id:mySecondAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule_type_id:myOtherAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)))`
|
||||
)
|
||||
);
|
||||
});
|
||||
|
@ -894,6 +901,10 @@ describe('AlertingAuthorization', () => {
|
|||
);
|
||||
});
|
||||
test('creates an `ensureRuleTypeIsAuthorized` function which throws if type is unauthorized', async () => {
|
||||
features.getKibanaFeatures.mockReturnValue([
|
||||
mockFeature('myApp', ['myOtherAppAlertType', 'myAppAlertType']),
|
||||
mockFeature('myOtherApp', ['myOtherAppAlertType', 'myAppAlertType']),
|
||||
]);
|
||||
const { authorization } = mockSecurity();
|
||||
const checkPrivileges: jest.MockedFunction<
|
||||
ReturnType<typeof authorization.checkPrivilegesDynamicallyWithRequest>
|
||||
|
@ -954,6 +965,10 @@ describe('AlertingAuthorization', () => {
|
|||
);
|
||||
});
|
||||
test('creates an `ensureRuleTypeIsAuthorized` function which is no-op if type is authorized', async () => {
|
||||
features.getKibanaFeatures.mockReturnValue([
|
||||
mockFeature('myApp', ['myOtherAppAlertType', 'myAppAlertType']),
|
||||
mockFeature('myOtherApp', 'myAppAlertType'),
|
||||
]);
|
||||
const { authorization } = mockSecurity();
|
||||
const checkPrivileges: jest.MockedFunction<
|
||||
ReturnType<typeof authorization.checkPrivilegesDynamicallyWithRequest>
|
||||
|
@ -1012,6 +1027,10 @@ describe('AlertingAuthorization', () => {
|
|||
}).not.toThrow();
|
||||
});
|
||||
test('creates an `logSuccessfulAuthorization` function which logs every authorized type', async () => {
|
||||
features.getKibanaFeatures.mockReturnValue([
|
||||
mockFeature('myApp', ['myOtherAppAlertType', 'myAppAlertType', 'mySecondAppAlertType']),
|
||||
mockFeature('myOtherApp', ['mySecondAppAlertType', 'myAppAlertType']),
|
||||
]);
|
||||
const { authorization } = mockSecurity();
|
||||
const checkPrivileges: jest.MockedFunction<
|
||||
ReturnType<typeof authorization.checkPrivilegesDynamicallyWithRequest>
|
||||
|
@ -1140,8 +1159,19 @@ describe('AlertingAuthorization', () => {
|
|||
enabledInLicense: true,
|
||||
};
|
||||
const setOfAlertTypes = new Set([myAppAlertType, myOtherAppAlertType]);
|
||||
|
||||
beforeEach(() => {
|
||||
features.getKibanaFeatures.mockReturnValue([
|
||||
mockFeature('myApp', ['myOtherAppAlertType', 'myAppAlertType']),
|
||||
mockFeature('myOtherApp', ['myAppAlertType', 'myOtherAppAlertType']),
|
||||
]);
|
||||
});
|
||||
test('augments a list of types with all features when there is no authorization api', async () => {
|
||||
features.getKibanaFeatures.mockReturnValue([
|
||||
myAppFeature,
|
||||
myOtherAppFeature,
|
||||
myAppWithSubFeature,
|
||||
myFeatureWithoutAlerting,
|
||||
]);
|
||||
const alertAuthorization = new AlertingAuthorization({
|
||||
request,
|
||||
ruleTypeRegistry,
|
||||
|
@ -1670,7 +1700,9 @@ describe('AlertingAuthorization', () => {
|
|||
isExportable: true,
|
||||
};
|
||||
const setOfAlertTypes = new Set([myAppAlertType, myOtherAppAlertType, mySecondAppAlertType]);
|
||||
|
||||
beforeEach(() => {
|
||||
features.getKibanaFeatures.mockReturnValue([mockFeature('myApp', ['myOtherAppAlertType'])]);
|
||||
});
|
||||
test('it returns authorized rule types given a set of feature ids', async () => {
|
||||
const { authorization } = mockSecurity();
|
||||
const checkPrivileges: jest.MockedFunction<
|
||||
|
|
|
@ -92,7 +92,7 @@ export class AlertingAuthorization {
|
|||
private readonly featuresIds: Promise<Set<string>>;
|
||||
private readonly allPossibleConsumers: Promise<AuthorizedConsumers>;
|
||||
private readonly spaceId: string | undefined;
|
||||
|
||||
private readonly features: FeaturesPluginStart;
|
||||
constructor({
|
||||
ruleTypeRegistry,
|
||||
request,
|
||||
|
@ -104,7 +104,7 @@ export class AlertingAuthorization {
|
|||
this.request = request;
|
||||
this.authorization = authorization;
|
||||
this.ruleTypeRegistry = ruleTypeRegistry;
|
||||
|
||||
this.features = features;
|
||||
this.spaceId = getSpaceId(request);
|
||||
|
||||
this.featuresIds = getSpace(request)
|
||||
|
@ -262,6 +262,19 @@ export class AlertingAuthorization {
|
|||
return this.getAuthorizationFilter(authorizationEntity, filterOpts, ReadOperations.Find);
|
||||
}
|
||||
|
||||
public async getAuthorizedRuleTypes(
|
||||
authorizationEntity: AlertingAuthorizationEntity,
|
||||
featuresIds?: Set<string>
|
||||
): Promise<RegistryAlertTypeWithAuth[]> {
|
||||
const { authorizedRuleTypes } = await this.augmentRuleTypesWithAuthorization(
|
||||
this.ruleTypeRegistry.list(),
|
||||
[ReadOperations.Find],
|
||||
authorizationEntity,
|
||||
featuresIds
|
||||
);
|
||||
return Array.from(authorizedRuleTypes);
|
||||
}
|
||||
|
||||
public async getAuthorizationFilter(
|
||||
authorizationEntity: AlertingAuthorizationEntity,
|
||||
filterOpts: AlertingAuthorizationFilterOpts,
|
||||
|
@ -355,28 +368,44 @@ export class AlertingAuthorization {
|
|||
);
|
||||
|
||||
// add an empty `authorizedConsumers` array on each ruleType
|
||||
const ruleTypesWithAuthorization = this.augmentWithAuthorizedConsumers(ruleTypes, {});
|
||||
|
||||
const ruleTypesWithAuthorization = Array.from(
|
||||
this.augmentWithAuthorizedConsumers(ruleTypes, {})
|
||||
);
|
||||
const ruleTypesAuthorized: Map<string, RegistryRuleType> = new Map();
|
||||
// map from privilege to ruleType which we can refer back to when analyzing the result
|
||||
// of checkPrivileges
|
||||
const privilegeToRuleType = new Map<
|
||||
string,
|
||||
[RegistryAlertTypeWithAuth, string, HasPrivileges, IsAuthorizedAtProducerLevel]
|
||||
>();
|
||||
// as we can't ask ES for the user's individual privileges we need to ask for each feature
|
||||
// and ruleType in the system whether this user has this privilege
|
||||
for (const ruleType of ruleTypesWithAuthorization) {
|
||||
for (const feature of fIds) {
|
||||
for (const operation of operations) {
|
||||
privilegeToRuleType.set(
|
||||
this.authorization!.actions.alerting.get(
|
||||
ruleType.id,
|
||||
feature,
|
||||
authorizationEntity,
|
||||
operation
|
||||
),
|
||||
[ruleType, feature, hasPrivilegeByOperation(operation), ruleType.producer === feature]
|
||||
);
|
||||
for (const feature of fIds) {
|
||||
const featureDef = this.features
|
||||
.getKibanaFeatures()
|
||||
.find((kFeature) => kFeature.id === feature);
|
||||
for (const ruleTypeId of featureDef?.alerting ?? []) {
|
||||
const ruleTypeAuth = ruleTypesWithAuthorization.find((rtwa) => rtwa.id === ruleTypeId);
|
||||
if (ruleTypeAuth) {
|
||||
if (!ruleTypesAuthorized.has(ruleTypeId)) {
|
||||
const { authorizedConsumers, hasAlertsMappings, hasFieldsForAAD, ...ruleType } =
|
||||
ruleTypeAuth;
|
||||
ruleTypesAuthorized.set(ruleTypeId, ruleType);
|
||||
}
|
||||
for (const operation of operations) {
|
||||
privilegeToRuleType.set(
|
||||
this.authorization!.actions.alerting.get(
|
||||
ruleTypeId,
|
||||
feature,
|
||||
authorizationEntity,
|
||||
operation
|
||||
),
|
||||
[
|
||||
ruleTypeAuth,
|
||||
feature,
|
||||
hasPrivilegeByOperation(operation),
|
||||
ruleTypeAuth.producer === feature,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -388,30 +417,36 @@ export class AlertingAuthorization {
|
|||
return {
|
||||
username,
|
||||
hasAllRequested,
|
||||
authorizedRuleTypes: hasAllRequested
|
||||
? // has access to all features
|
||||
this.augmentWithAuthorizedConsumers(ruleTypes, await this.allPossibleConsumers)
|
||||
: // only has some of the required privileges
|
||||
privileges.kibana.reduce((authorizedRuleTypes, { authorized, privilege }) => {
|
||||
if (authorized && privilegeToRuleType.has(privilege)) {
|
||||
const [ruleType, feature, hasPrivileges, isAuthorizedAtProducerLevel] =
|
||||
privilegeToRuleType.get(privilege)!;
|
||||
ruleType.authorizedConsumers[feature] = mergeHasPrivileges(
|
||||
hasPrivileges,
|
||||
ruleType.authorizedConsumers[feature]
|
||||
);
|
||||
authorizedRuleTypes:
|
||||
hasAllRequested && featuresIds === undefined
|
||||
? // has access to all features
|
||||
this.augmentWithAuthorizedConsumers(
|
||||
new Set(ruleTypesAuthorized.values()),
|
||||
await this.allPossibleConsumers
|
||||
)
|
||||
: // only has some of the required privileges
|
||||
privileges.kibana.reduce((authorizedRuleTypes, { authorized, privilege }) => {
|
||||
if (authorized && privilegeToRuleType.has(privilege)) {
|
||||
const [ruleType, feature, hasPrivileges, isAuthorizedAtProducerLevel] =
|
||||
privilegeToRuleType.get(privilege)!;
|
||||
if (fIds.has(feature)) {
|
||||
ruleType.authorizedConsumers[feature] = mergeHasPrivileges(
|
||||
hasPrivileges,
|
||||
ruleType.authorizedConsumers[feature]
|
||||
);
|
||||
|
||||
if (isAuthorizedAtProducerLevel) {
|
||||
// granting privileges under the producer automatically authorized the Rules Management UI as well
|
||||
ruleType.authorizedConsumers[ALERTS_FEATURE_ID] = mergeHasPrivileges(
|
||||
hasPrivileges,
|
||||
ruleType.authorizedConsumers[ALERTS_FEATURE_ID]
|
||||
);
|
||||
if (isAuthorizedAtProducerLevel) {
|
||||
// granting privileges under the producer automatically authorized the Rules Management UI as well
|
||||
ruleType.authorizedConsumers[ALERTS_FEATURE_ID] = mergeHasPrivileges(
|
||||
hasPrivileges,
|
||||
ruleType.authorizedConsumers[ALERTS_FEATURE_ID]
|
||||
);
|
||||
}
|
||||
authorizedRuleTypes.add(ruleType);
|
||||
}
|
||||
}
|
||||
authorizedRuleTypes.add(ruleType);
|
||||
}
|
||||
return authorizedRuleTypes;
|
||||
}, new Set<RegistryAlertTypeWithAuth>()),
|
||||
return authorizedRuleTypes;
|
||||
}, new Set<RegistryAlertTypeWithAuth>()),
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* 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 { loggingSystemMock } from '@kbn/core-logging-server-mocks';
|
||||
import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks';
|
||||
import { licensingMock } from '@kbn/licensing-plugin/server/mocks';
|
||||
import { inMemoryMetricsMock } from '../monitoring/in_memory_metrics.mock';
|
||||
import { ConstructorOptions, RuleTypeRegistry } from '../rule_type_registry';
|
||||
import { TaskRunnerFactory } from '../task_runner/task_runner_factory';
|
||||
import { ILicenseState } from './license_state';
|
||||
import { licenseStateMock } from './license_state.mock';
|
||||
import { schema } from '@kbn/config-schema';
|
||||
import { createGetAlertIndicesAliasFn } from './create_get_alert_indices_alias';
|
||||
|
||||
describe('createGetAlertIndicesAliasFn', () => {
|
||||
const logger = loggingSystemMock.create().get();
|
||||
const mockedLicenseState: jest.Mocked<ILicenseState> = licenseStateMock.create();
|
||||
const taskManager = taskManagerMock.createSetup();
|
||||
const inMemoryMetrics = inMemoryMetricsMock.create();
|
||||
|
||||
const ruleTypeRegistryParams: ConstructorOptions = {
|
||||
logger,
|
||||
taskManager,
|
||||
taskRunnerFactory: new TaskRunnerFactory(),
|
||||
alertsService: null,
|
||||
licenseState: mockedLicenseState,
|
||||
licensing: licensingMock.createSetup(),
|
||||
minimumScheduleInterval: { value: '1m', enforce: false },
|
||||
inMemoryMetrics,
|
||||
};
|
||||
const registry = new RuleTypeRegistry(ruleTypeRegistryParams);
|
||||
registry.register({
|
||||
id: 'test',
|
||||
name: 'Test',
|
||||
actionGroups: [
|
||||
{
|
||||
id: 'default',
|
||||
name: 'Default',
|
||||
},
|
||||
],
|
||||
defaultActionGroupId: 'default',
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: true,
|
||||
executor: jest.fn(),
|
||||
producer: 'alerts',
|
||||
alerts: {
|
||||
context: 'test',
|
||||
mappings: { fieldMap: { field: { type: 'keyword', required: false } } },
|
||||
},
|
||||
validate: {
|
||||
params: { validate: (params) => params },
|
||||
},
|
||||
});
|
||||
registry.register({
|
||||
id: 'spaceAware',
|
||||
name: 'Space Aware',
|
||||
actionGroups: [
|
||||
{
|
||||
id: 'default',
|
||||
name: 'Default',
|
||||
},
|
||||
],
|
||||
defaultActionGroupId: 'default',
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: true,
|
||||
executor: jest.fn(),
|
||||
producer: 'alerts',
|
||||
alerts: {
|
||||
context: 'spaceAware',
|
||||
isSpaceAware: true,
|
||||
mappings: { fieldMap: { field: { type: 'keyword', required: false } } },
|
||||
},
|
||||
validate: {
|
||||
params: { validate: (params) => params },
|
||||
},
|
||||
});
|
||||
registry.register({
|
||||
id: 'foo',
|
||||
name: 'Foo',
|
||||
actionGroups: [
|
||||
{
|
||||
id: 'default',
|
||||
name: 'Default',
|
||||
},
|
||||
],
|
||||
defaultActionGroupId: 'default',
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: true,
|
||||
executor: jest.fn(),
|
||||
producer: 'alerts',
|
||||
validate: {
|
||||
params: schema.any(),
|
||||
},
|
||||
});
|
||||
const getAlertIndicesAlias = createGetAlertIndicesAliasFn(registry);
|
||||
|
||||
test('getAlertIndicesAlias for the rule type with alert context', () => {
|
||||
expect(getAlertIndicesAlias(['test'])).toEqual(['.alerts-test.alerts-default']);
|
||||
});
|
||||
test('getAlertIndicesAlias for the rule type with alert context with space I', () => {
|
||||
expect(getAlertIndicesAlias(['spaceAware'], 'space-1')).toEqual([
|
||||
'.alerts-spaceAware.alerts-space-1',
|
||||
]);
|
||||
});
|
||||
test('getAlertIndicesAlias for the rule type with NO alert context', () => {
|
||||
expect(getAlertIndicesAlias(['foo'])).toEqual([]);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 { DEFAULT_NAMESPACE_STRING } from '@kbn/core-saved-objects-utils-server';
|
||||
import { getIndexTemplateAndPattern } from '../alerts_service/resource_installer_utils';
|
||||
import { RuleTypeRegistry } from '../rule_type_registry';
|
||||
|
||||
export type GetAlertIndicesAlias = (rulesTypes: string[], spaceId?: string) => string[];
|
||||
|
||||
export function createGetAlertIndicesAliasFn(ruleTypeRegistry: RuleTypeRegistry) {
|
||||
return (rulesTypes: string[], spaceId?: string): string[] => {
|
||||
const aliases = new Set<string>();
|
||||
rulesTypes.forEach((ruleTypeId) => {
|
||||
const ruleType = ruleTypeRegistry.get(ruleTypeId);
|
||||
if (ruleType.alerts?.context) {
|
||||
const indexTemplateAndPattern = getIndexTemplateAndPattern({
|
||||
context: ruleType.alerts?.context,
|
||||
namespace: ruleType.alerts?.isSpaceAware && spaceId ? spaceId : DEFAULT_NAMESPACE_STRING,
|
||||
});
|
||||
aliases.add(indexTemplateAndPattern.alias);
|
||||
}
|
||||
});
|
||||
return Array.from(aliases);
|
||||
};
|
||||
}
|
|
@ -46,3 +46,5 @@ export { determineAlertsToReturn } from './determine_alerts_to_return';
|
|||
export { updateFlappingHistory, isFlapping } from './flapping_utils';
|
||||
export { getAlertsForNotification } from './get_alerts_for_notification';
|
||||
export { trimRecoveredAlerts } from './trim_recovered_alerts';
|
||||
export { createGetAlertIndicesAliasFn } from './create_get_alert_indices_alias';
|
||||
export type { GetAlertIndicesAlias } from './create_get_alert_indices_alias';
|
||||
|
|
|
@ -59,6 +59,7 @@ const createStartMock = () => {
|
|||
listTypes: jest.fn(),
|
||||
getType: jest.fn(),
|
||||
getAllTypes: jest.fn(),
|
||||
getAlertIndicesAlias: jest.fn(),
|
||||
getAlertingAuthorizationWithRequest: jest.fn(),
|
||||
getRulesClientWithRequest: jest.fn().mockResolvedValue(rulesClientMock.create()),
|
||||
getFrameworkHealth: jest.fn(),
|
||||
|
|
|
@ -97,6 +97,7 @@ import {
|
|||
} from './alerts_service';
|
||||
import { rulesSettingsFeature } from './rules_settings_feature';
|
||||
import { maintenanceWindowFeature } from './maintenance_window_feature';
|
||||
import { createGetAlertIndicesAliasFn, GetAlertIndicesAlias } from './lib';
|
||||
|
||||
export const EVENT_LOG_PROVIDER = 'alerting';
|
||||
export const EVENT_LOG_ACTIONS = {
|
||||
|
@ -145,6 +146,7 @@ export interface PluginStartContract {
|
|||
|
||||
getAllTypes: RuleTypeRegistry['getAllTypes'];
|
||||
getType: RuleTypeRegistry['get'];
|
||||
getAlertIndicesAlias: GetAlertIndicesAlias;
|
||||
|
||||
getRulesClientWithRequest(request: KibanaRequest): RulesClientApi;
|
||||
|
||||
|
@ -345,6 +347,7 @@ export class AlertingPlugin {
|
|||
router,
|
||||
licenseState: this.licenseState,
|
||||
usageCounter: this.usageCounter,
|
||||
getAlertIndicesAlias: createGetAlertIndicesAliasFn(this.ruleTypeRegistry!),
|
||||
encryptedSavedObjects: plugins.encryptedSavedObjects,
|
||||
config$: plugins.unifiedSearch.autocomplete.getInitializerContextConfig().create(),
|
||||
});
|
||||
|
@ -556,6 +559,7 @@ export class AlertingPlugin {
|
|||
listTypes: ruleTypeRegistry!.list.bind(this.ruleTypeRegistry!),
|
||||
getType: ruleTypeRegistry!.get.bind(this.ruleTypeRegistry),
|
||||
getAllTypes: ruleTypeRegistry!.getAllTypes.bind(this.ruleTypeRegistry!),
|
||||
getAlertIndicesAlias: createGetAlertIndicesAliasFn(this.ruleTypeRegistry!),
|
||||
getAlertingAuthorizationWithRequest,
|
||||
getRulesClientWithRequest,
|
||||
getFrameworkHealth: async () =>
|
||||
|
|
|
@ -10,7 +10,7 @@ import { UsageCounter } from '@kbn/usage-collection-plugin/server';
|
|||
import { EncryptedSavedObjectsPluginSetup } from '@kbn/encrypted-saved-objects-plugin/server';
|
||||
import type { ConfigSchema } from '@kbn/unified-search-plugin/config';
|
||||
import { Observable } from 'rxjs';
|
||||
import { ILicenseState } from '../lib';
|
||||
import { GetAlertIndicesAlias, ILicenseState } from '../lib';
|
||||
import { defineLegacyRoutes } from './legacy';
|
||||
import { AlertingRequestHandlerContext } from '../types';
|
||||
import { createRuleRoute } from './rule/apis/create';
|
||||
|
@ -56,20 +56,29 @@ import { findMaintenanceWindowsRoute } from './maintenance_window/find_maintenan
|
|||
import { archiveMaintenanceWindowRoute } from './maintenance_window/archive_maintenance_window';
|
||||
import { finishMaintenanceWindowRoute } from './maintenance_window/finish_maintenance_window';
|
||||
import { activeMaintenanceWindowsRoute } from './maintenance_window/active_maintenance_windows';
|
||||
import { registerValueSuggestionsRoute } from './suggestions/values_suggestion_rules';
|
||||
import { registerRulesValueSuggestionsRoute } from './suggestions/values_suggestion_rules';
|
||||
import { registerFieldsRoute } from './suggestions/fields_rules';
|
||||
import { bulkGetMaintenanceWindowRoute } from './maintenance_window/bulk_get_maintenance_windows';
|
||||
import { registerAlertsValueSuggestionsRoute } from './suggestions/values_suggestion_alerts';
|
||||
|
||||
export interface RouteOptions {
|
||||
router: IRouter<AlertingRequestHandlerContext>;
|
||||
licenseState: ILicenseState;
|
||||
encryptedSavedObjects: EncryptedSavedObjectsPluginSetup;
|
||||
getAlertIndicesAlias?: GetAlertIndicesAlias;
|
||||
usageCounter?: UsageCounter;
|
||||
config$?: Observable<ConfigSchema>;
|
||||
}
|
||||
|
||||
export function defineRoutes(opts: RouteOptions) {
|
||||
const { router, licenseState, encryptedSavedObjects, usageCounter, config$ } = opts;
|
||||
const {
|
||||
router,
|
||||
licenseState,
|
||||
encryptedSavedObjects,
|
||||
usageCounter,
|
||||
config$,
|
||||
getAlertIndicesAlias,
|
||||
} = opts;
|
||||
|
||||
defineLegacyRoutes(opts);
|
||||
createRuleRoute(opts);
|
||||
|
@ -116,7 +125,8 @@ export function defineRoutes(opts: RouteOptions) {
|
|||
archiveMaintenanceWindowRoute(router, licenseState);
|
||||
finishMaintenanceWindowRoute(router, licenseState);
|
||||
activeMaintenanceWindowsRoute(router, licenseState);
|
||||
registerValueSuggestionsRoute(router, licenseState, config$!);
|
||||
registerAlertsValueSuggestionsRoute(router, licenseState, config$!, getAlertIndicesAlias);
|
||||
registerRulesValueSuggestionsRoute(router, licenseState, config$!);
|
||||
registerFieldsRoute(router, licenseState);
|
||||
bulkGetMaintenanceWindowRoute(router, licenseState);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* 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 { httpServerMock, httpServiceMock } from '@kbn/core-http-server-mocks';
|
||||
import type { ConfigSchema } from '@kbn/unified-search-plugin/config';
|
||||
import { dataPluginMock } from '@kbn/unified-search-plugin/server/mocks';
|
||||
import { termsAggSuggestions } from '@kbn/unified-search-plugin/server/autocomplete/terms_agg';
|
||||
import { Observable } from 'rxjs';
|
||||
import { licenseStateMock } from '../../lib/license_state.mock';
|
||||
import { rulesClientMock } from '../../rules_client.mock';
|
||||
import { mockHandlerArguments } from '../_mock_handler_arguments';
|
||||
import { registerAlertsValueSuggestionsRoute } from './values_suggestion_alerts';
|
||||
|
||||
jest.mock('@kbn/unified-search-plugin/server/autocomplete/terms_agg', () => {
|
||||
return {
|
||||
termsAggSuggestions: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
const termsAggSuggestionsMock = termsAggSuggestions as jest.Mock;
|
||||
|
||||
jest.mock('../../lib/license_api_access', () => ({
|
||||
verifyApiAccess: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('registerAlertsValueSuggestionsRoute', () => {
|
||||
const rulesClient = rulesClientMock.create();
|
||||
let config$: Observable<ConfigSchema>;
|
||||
|
||||
beforeEach(() => {
|
||||
termsAggSuggestionsMock.mockClear();
|
||||
rulesClient.getSpaceId.mockReturnValue('space-x');
|
||||
config$ = dataPluginMock
|
||||
.createSetupContract()
|
||||
.autocomplete.getInitializerContextConfig()
|
||||
.create();
|
||||
});
|
||||
|
||||
test('happy path route registered', async () => {
|
||||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
const getAlertIndicesAliasMock = jest.fn().mockReturnValue(['alert-index']);
|
||||
registerAlertsValueSuggestionsRoute(router, licenseState, config$, getAlertIndicesAliasMock);
|
||||
|
||||
const [config, handler] = router.post.mock.calls[0];
|
||||
|
||||
expect(config.path).toMatchInlineSnapshot(`"/internal/alerts/suggestions/values"`);
|
||||
|
||||
const mockRequest = httpServerMock.createKibanaRequest<never, never, never>({
|
||||
body: {
|
||||
field: 'alert.tags',
|
||||
query: 'test-query',
|
||||
filters: 'test-filters',
|
||||
fieldMeta: 'test-field-meta',
|
||||
},
|
||||
});
|
||||
|
||||
const [context, req, res] = mockHandlerArguments({ rulesClient }, mockRequest, ['ok']);
|
||||
|
||||
await handler(
|
||||
{
|
||||
...context,
|
||||
core: { elasticsearch: { client: { asInternalUser: {} } }, savedObjects: { client: {} } },
|
||||
},
|
||||
req,
|
||||
res
|
||||
);
|
||||
|
||||
expect(rulesClient.getAuthorization).toHaveBeenCalledTimes(1);
|
||||
expect(rulesClient.getSpaceId).toHaveBeenCalledTimes(1);
|
||||
expect(termsAggSuggestionsMock).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
expect.any(Object),
|
||||
expect.any(Object),
|
||||
expect.any(Object),
|
||||
'alert-index',
|
||||
'alert.tags',
|
||||
'test-query',
|
||||
[{ term: { 'kibana.space_ids': 'space-x' } }],
|
||||
'test-field-meta',
|
||||
expect.any(Object)
|
||||
);
|
||||
expect(res.ok).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
* 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 { schema } from '@kbn/config-schema';
|
||||
import { IRouter } from '@kbn/core/server';
|
||||
import { firstValueFrom, Observable } from 'rxjs';
|
||||
import { getRequestAbortedSignal } from '@kbn/data-plugin/server';
|
||||
import { termsAggSuggestions } from '@kbn/unified-search-plugin/server/autocomplete/terms_agg';
|
||||
import type { ConfigSchema } from '@kbn/unified-search-plugin/config';
|
||||
import { UsageCounter } from '@kbn/usage-collection-plugin/server';
|
||||
import { getKbnServerError, reportServerError } from '@kbn/kibana-utils-plugin/server';
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import {
|
||||
AlertConsumers,
|
||||
ALERT_RULE_CONSUMER,
|
||||
ALERT_RULE_TYPE_ID,
|
||||
SPACE_IDS,
|
||||
} from '@kbn/rule-data-utils';
|
||||
|
||||
import { verifyAccessAndContext } from '../lib';
|
||||
import { RuleAuditAction, ruleAuditEvent } from '../../rules_client/common/audit_events';
|
||||
import {
|
||||
AlertingAuthorizationEntity,
|
||||
AlertingAuthorizationFilterOpts,
|
||||
AlertingAuthorizationFilterType,
|
||||
} from '../../authorization';
|
||||
import { AlertingRequestHandlerContext } from '../../types';
|
||||
import { GetAlertIndicesAlias, ILicenseState } from '../../lib';
|
||||
|
||||
const alertingAuthorizationFilterOpts: AlertingAuthorizationFilterOpts = {
|
||||
type: AlertingAuthorizationFilterType.ESDSL,
|
||||
fieldNames: { ruleTypeId: ALERT_RULE_TYPE_ID, consumer: ALERT_RULE_CONSUMER },
|
||||
};
|
||||
|
||||
export const AlertsSuggestionsSchema = {
|
||||
body: schema.object({
|
||||
field: schema.string(),
|
||||
query: schema.string(),
|
||||
filters: schema.maybe(schema.any()),
|
||||
fieldMeta: schema.maybe(schema.any()),
|
||||
}),
|
||||
};
|
||||
|
||||
const VALID_FEATURE_IDS = new Set([
|
||||
AlertConsumers.APM,
|
||||
AlertConsumers.INFRASTRUCTURE,
|
||||
AlertConsumers.LOGS,
|
||||
AlertConsumers.SLO,
|
||||
AlertConsumers.UPTIME,
|
||||
]);
|
||||
|
||||
export function registerAlertsValueSuggestionsRoute(
|
||||
router: IRouter<AlertingRequestHandlerContext>,
|
||||
licenseState: ILicenseState,
|
||||
config$: Observable<ConfigSchema>,
|
||||
getAlertIndicesAlias?: GetAlertIndicesAlias,
|
||||
usageCounter?: UsageCounter
|
||||
) {
|
||||
router.post(
|
||||
{
|
||||
path: '/internal/alerts/suggestions/values',
|
||||
validate: AlertsSuggestionsSchema,
|
||||
},
|
||||
router.handleLegacyErrors(
|
||||
verifyAccessAndContext(licenseState, async function (context, request, response) {
|
||||
const config = await firstValueFrom(config$);
|
||||
const { field: fieldName, query, fieldMeta } = request.body;
|
||||
const abortSignal = getRequestAbortedSignal(request.events.aborted$);
|
||||
const { savedObjects, elasticsearch } = await context.core;
|
||||
|
||||
const rulesClient = (await context.alerting).getRulesClient();
|
||||
let authorizationTuple;
|
||||
let authorizedRuleType = [];
|
||||
try {
|
||||
const authorization = rulesClient.getAuthorization();
|
||||
authorizationTuple = await authorization.getFindAuthorizationFilter(
|
||||
AlertingAuthorizationEntity.Alert,
|
||||
alertingAuthorizationFilterOpts
|
||||
);
|
||||
authorizedRuleType = await authorization.getAuthorizedRuleTypes(
|
||||
AlertingAuthorizationEntity.Alert,
|
||||
VALID_FEATURE_IDS
|
||||
);
|
||||
} catch (error) {
|
||||
rulesClient.getAuditLogger()?.log(
|
||||
ruleAuditEvent({
|
||||
action: RuleAuditAction.FIND,
|
||||
error,
|
||||
})
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
const spaceId = rulesClient.getSpaceId();
|
||||
const { filter: authorizationFilter } = authorizationTuple;
|
||||
const filters = [
|
||||
...(authorizationFilter != null ? [authorizationFilter] : []),
|
||||
{ term: { [SPACE_IDS]: spaceId } },
|
||||
] as estypes.QueryDslQueryContainer[];
|
||||
|
||||
const index = getAlertIndicesAlias!(
|
||||
authorizedRuleType.map((art) => art.id),
|
||||
spaceId
|
||||
).join(',');
|
||||
try {
|
||||
const body = await termsAggSuggestions(
|
||||
config,
|
||||
savedObjects.client,
|
||||
elasticsearch.client.asInternalUser,
|
||||
index,
|
||||
fieldName,
|
||||
query,
|
||||
filters,
|
||||
fieldMeta,
|
||||
abortSignal
|
||||
);
|
||||
return response.ok({ body });
|
||||
} catch (e) {
|
||||
const kbnErr = getKbnServerError(e);
|
||||
return reportServerError(response, kbnErr);
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
|
@ -13,7 +13,7 @@ import { Observable } from 'rxjs';
|
|||
import { licenseStateMock } from '../../lib/license_state.mock';
|
||||
import { rulesClientMock } from '../../rules_client.mock';
|
||||
import { mockHandlerArguments } from '../_mock_handler_arguments';
|
||||
import { registerValueSuggestionsRoute } from './values_suggestion_rules';
|
||||
import { registerRulesValueSuggestionsRoute } from './values_suggestion_rules';
|
||||
|
||||
jest.mock('@kbn/unified-search-plugin/server/autocomplete/terms_agg', () => {
|
||||
return {
|
||||
|
@ -27,7 +27,7 @@ jest.mock('../../lib/license_api_access', () => ({
|
|||
verifyApiAccess: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('registerValueSuggestionsRoute', () => {
|
||||
describe('registerRulesValueSuggestionsRoute', () => {
|
||||
const rulesClient = rulesClientMock.create();
|
||||
let config$: Observable<ConfigSchema>;
|
||||
|
||||
|
@ -43,7 +43,7 @@ describe('registerValueSuggestionsRoute', () => {
|
|||
const licenseState = licenseStateMock.create();
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
registerValueSuggestionsRoute(router, licenseState, config$);
|
||||
registerRulesValueSuggestionsRoute(router, licenseState, config$);
|
||||
|
||||
const [config, handler] = router.post.mock.calls[0];
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ export const RulesSuggestionsSchema = {
|
|||
}),
|
||||
};
|
||||
|
||||
export function registerValueSuggestionsRoute(
|
||||
export function registerRulesValueSuggestionsRoute(
|
||||
router: IRouter<AlertingRequestHandlerContext>,
|
||||
licenseState: ILicenseState,
|
||||
config$: Observable<ConfigSchema>,
|
||||
|
|
|
@ -33,6 +33,7 @@ const createRulesClientMock = () => {
|
|||
getAuditLogger: jest.fn(),
|
||||
getAuthorization: jest.fn().mockImplementation(() => ({
|
||||
getFindAuthorizationFilter: jest.fn().mockReturnValue({ filter: null }),
|
||||
getAuthorizedRuleTypes: jest.fn().mockResolvedValue([]),
|
||||
})),
|
||||
getExecutionLogForRule: jest.fn(),
|
||||
getRuleExecutionKPI: jest.fn(),
|
||||
|
|
|
@ -119,7 +119,6 @@ export class RuleRegistryPlugin
|
|||
core.getStartServices().then(([_, depsStart]) => {
|
||||
const ruleRegistrySearchStrategy = ruleRegistrySearchStrategyProvider(
|
||||
depsStart.data,
|
||||
this.ruleDataService!,
|
||||
depsStart.alerting,
|
||||
logger,
|
||||
plugins.security,
|
||||
|
|
|
@ -9,14 +9,12 @@ import { merge } from 'lodash';
|
|||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import { ruleRegistrySearchStrategyProvider, EMPTY_RESPONSE } from './search_strategy';
|
||||
import { ruleDataServiceMock } from '../rule_data_plugin_service/rule_data_plugin_service.mock';
|
||||
import { dataPluginMock } from '@kbn/data-plugin/server/mocks';
|
||||
import { SearchStrategyDependencies } from '@kbn/data-plugin/server';
|
||||
import { alertsMock } from '@kbn/alerting-plugin/server/mocks';
|
||||
import { securityMock } from '@kbn/security-plugin/server/mocks';
|
||||
import { spacesMock } from '@kbn/spaces-plugin/server/mocks';
|
||||
import { RuleRegistrySearchRequest } from '../../common/search_strategy';
|
||||
import { IndexInfo } from '../rule_data_plugin_service/index_info';
|
||||
import * as getAuthzFilterImport from '../lib/get_authz_filter';
|
||||
import { getIsKibanaRequest } from '../lib/get_is_kibana_request';
|
||||
|
||||
|
@ -50,12 +48,12 @@ const getBasicResponse = (overwrites = {}) => {
|
|||
|
||||
describe('ruleRegistrySearchStrategyProvider()', () => {
|
||||
const data = dataPluginMock.createStartContract();
|
||||
const ruleDataService = ruleDataServiceMock.create();
|
||||
const alerting = alertsMock.createStart();
|
||||
const security = securityMock.createSetup();
|
||||
const spaces = spacesMock.createStart();
|
||||
const logger = loggerMock.create();
|
||||
|
||||
const getAuthorizedRuleTypesMock = jest.fn();
|
||||
const getAlertIndicesAliasMock = jest.fn();
|
||||
const response = getBasicResponse({
|
||||
rawResponse: {
|
||||
hits: {
|
||||
|
@ -74,11 +72,13 @@ describe('ruleRegistrySearchStrategyProvider()', () => {
|
|||
const searchStrategySearch = jest.fn().mockImplementation(() => of(response));
|
||||
|
||||
beforeEach(() => {
|
||||
ruleDataService.findIndexByFeature.mockImplementation(() => {
|
||||
return {
|
||||
baseName: 'test',
|
||||
} as IndexInfo;
|
||||
});
|
||||
getAuthorizedRuleTypesMock.mockResolvedValue([]);
|
||||
getAlertIndicesAliasMock.mockReturnValue(['test']);
|
||||
const authorizationMock = {
|
||||
getAuthorizedRuleTypes: getAuthorizedRuleTypesMock,
|
||||
} as never;
|
||||
alerting.getAlertingAuthorizationWithRequest.mockResolvedValue(authorizationMock);
|
||||
alerting.getAlertIndicesAlias = getAlertIndicesAliasMock;
|
||||
|
||||
data.search.getSearchStrategy.mockImplementation(() => {
|
||||
return {
|
||||
|
@ -102,7 +102,8 @@ describe('ruleRegistrySearchStrategyProvider()', () => {
|
|||
});
|
||||
|
||||
afterEach(() => {
|
||||
ruleDataService.findIndexByFeature.mockClear();
|
||||
getAuthorizedRuleTypesMock.mockClear();
|
||||
getAlertIndicesAliasMock.mockClear();
|
||||
data.search.getSearchStrategy.mockClear();
|
||||
(data.search.searchAsInternalUser.search as jest.Mock).mockClear();
|
||||
getAuthzFilterSpy.mockClear();
|
||||
|
@ -110,6 +111,8 @@ describe('ruleRegistrySearchStrategyProvider()', () => {
|
|||
});
|
||||
|
||||
it('should handle a basic search request', async () => {
|
||||
getAuthorizedRuleTypesMock.mockResolvedValue([]);
|
||||
getAlertIndicesAliasMock.mockReturnValue(['observability-logs']);
|
||||
const request: RuleRegistrySearchRequest = {
|
||||
featureIds: [AlertConsumers.LOGS],
|
||||
};
|
||||
|
@ -118,68 +121,13 @@ describe('ruleRegistrySearchStrategyProvider()', () => {
|
|||
request: {},
|
||||
};
|
||||
|
||||
const strategy = ruleRegistrySearchStrategyProvider(
|
||||
data,
|
||||
ruleDataService,
|
||||
alerting,
|
||||
logger,
|
||||
security,
|
||||
spaces
|
||||
);
|
||||
const strategy = ruleRegistrySearchStrategyProvider(data, alerting, logger, security, spaces);
|
||||
|
||||
const result = await strategy
|
||||
.search(request, options, deps as unknown as SearchStrategyDependencies)
|
||||
.toPromise();
|
||||
expect(result).toBe(response);
|
||||
});
|
||||
|
||||
it('should use the active space in siem queries', async () => {
|
||||
const request: RuleRegistrySearchRequest = {
|
||||
featureIds: [AlertConsumers.SIEM],
|
||||
};
|
||||
const options = {};
|
||||
const deps = {
|
||||
request: {},
|
||||
};
|
||||
|
||||
spaces.spacesService.getActiveSpace.mockImplementation(async () => {
|
||||
return {
|
||||
id: 'testSpace',
|
||||
name: 'Test Space',
|
||||
disabledFeatures: [],
|
||||
};
|
||||
});
|
||||
|
||||
ruleDataService.findIndexByFeature.mockImplementation(() => {
|
||||
return {
|
||||
baseName: 'myTestIndex',
|
||||
} as unknown as IndexInfo;
|
||||
});
|
||||
|
||||
let searchRequest: RuleRegistrySearchRequest = {} as unknown as RuleRegistrySearchRequest;
|
||||
data.search.getSearchStrategy.mockImplementation(() => {
|
||||
return {
|
||||
search: (_request) => {
|
||||
searchRequest = _request as unknown as RuleRegistrySearchRequest;
|
||||
return of(response);
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const strategy = ruleRegistrySearchStrategyProvider(
|
||||
data,
|
||||
ruleDataService,
|
||||
alerting,
|
||||
logger,
|
||||
security,
|
||||
spaces
|
||||
);
|
||||
|
||||
await strategy
|
||||
.search(request, options, deps as unknown as SearchStrategyDependencies)
|
||||
.toPromise();
|
||||
spaces.spacesService.getActiveSpace.mockClear();
|
||||
expect(searchRequest?.params?.index).toStrictEqual(['myTestIndex-testSpace*']);
|
||||
expect(result).toEqual(response);
|
||||
});
|
||||
|
||||
it('should return an empty response if no valid indices are found', async () => {
|
||||
|
@ -191,18 +139,10 @@ describe('ruleRegistrySearchStrategyProvider()', () => {
|
|||
request: {},
|
||||
};
|
||||
|
||||
ruleDataService.findIndexByFeature.mockImplementationOnce(() => {
|
||||
return null;
|
||||
});
|
||||
getAuthorizedRuleTypesMock.mockResolvedValue([]);
|
||||
getAlertIndicesAliasMock.mockReturnValue([]);
|
||||
|
||||
const strategy = ruleRegistrySearchStrategyProvider(
|
||||
data,
|
||||
ruleDataService,
|
||||
alerting,
|
||||
logger,
|
||||
security,
|
||||
spaces
|
||||
);
|
||||
const strategy = ruleRegistrySearchStrategyProvider(data, alerting, logger, security, spaces);
|
||||
|
||||
const result = await strategy
|
||||
.search(request, options, deps as unknown as SearchStrategyDependencies)
|
||||
|
@ -219,14 +159,10 @@ describe('ruleRegistrySearchStrategyProvider()', () => {
|
|||
request: {},
|
||||
};
|
||||
|
||||
const strategy = ruleRegistrySearchStrategyProvider(
|
||||
data,
|
||||
ruleDataService,
|
||||
alerting,
|
||||
logger,
|
||||
security,
|
||||
spaces
|
||||
);
|
||||
getAuthorizedRuleTypesMock.mockResolvedValue([]);
|
||||
getAlertIndicesAliasMock.mockReturnValue(['security-siem']);
|
||||
|
||||
const strategy = ruleRegistrySearchStrategyProvider(data, alerting, logger, security, spaces);
|
||||
|
||||
await strategy
|
||||
.search(request, options, deps as unknown as SearchStrategyDependencies)
|
||||
|
@ -243,14 +179,10 @@ describe('ruleRegistrySearchStrategyProvider()', () => {
|
|||
request: {},
|
||||
};
|
||||
|
||||
const strategy = ruleRegistrySearchStrategyProvider(
|
||||
data,
|
||||
ruleDataService,
|
||||
alerting,
|
||||
logger,
|
||||
security,
|
||||
spaces
|
||||
);
|
||||
getAuthorizedRuleTypesMock.mockResolvedValue([]);
|
||||
getAlertIndicesAliasMock.mockReturnValue(['security-siem', 'o11y-logs']);
|
||||
|
||||
const strategy = ruleRegistrySearchStrategyProvider(data, alerting, logger, security, spaces);
|
||||
|
||||
let err;
|
||||
try {
|
||||
|
@ -271,15 +203,10 @@ describe('ruleRegistrySearchStrategyProvider()', () => {
|
|||
const deps = {
|
||||
request: {},
|
||||
};
|
||||
getAuthorizedRuleTypesMock.mockResolvedValue([]);
|
||||
getAlertIndicesAliasMock.mockReturnValue(['o11y-logs']);
|
||||
|
||||
const strategy = ruleRegistrySearchStrategyProvider(
|
||||
data,
|
||||
ruleDataService,
|
||||
alerting,
|
||||
logger,
|
||||
security,
|
||||
spaces
|
||||
);
|
||||
const strategy = ruleRegistrySearchStrategyProvider(data, alerting, logger, security, spaces);
|
||||
|
||||
await strategy
|
||||
.search(request, options, deps as unknown as SearchStrategyDependencies)
|
||||
|
@ -297,14 +224,10 @@ describe('ruleRegistrySearchStrategyProvider()', () => {
|
|||
request: {},
|
||||
};
|
||||
|
||||
const strategy = ruleRegistrySearchStrategyProvider(
|
||||
data,
|
||||
ruleDataService,
|
||||
alerting,
|
||||
logger,
|
||||
security,
|
||||
spaces
|
||||
);
|
||||
getAuthorizedRuleTypesMock.mockResolvedValue([]);
|
||||
getAlertIndicesAliasMock.mockReturnValue(['security-siem']);
|
||||
|
||||
const strategy = ruleRegistrySearchStrategyProvider(data, alerting, logger, security, spaces);
|
||||
|
||||
await strategy
|
||||
.search(request, options, deps as unknown as SearchStrategyDependencies)
|
||||
|
@ -325,15 +248,10 @@ describe('ruleRegistrySearchStrategyProvider()', () => {
|
|||
const deps = {
|
||||
request: {},
|
||||
};
|
||||
getAuthorizedRuleTypesMock.mockResolvedValue([]);
|
||||
getAlertIndicesAliasMock.mockReturnValue(['o11y-logs']);
|
||||
|
||||
const strategy = ruleRegistrySearchStrategyProvider(
|
||||
data,
|
||||
ruleDataService,
|
||||
alerting,
|
||||
logger,
|
||||
security,
|
||||
spaces
|
||||
);
|
||||
const strategy = ruleRegistrySearchStrategyProvider(data, alerting, logger, security, spaces);
|
||||
|
||||
await strategy
|
||||
.search(request, options, deps as unknown as SearchStrategyDependencies)
|
||||
|
@ -362,15 +280,10 @@ describe('ruleRegistrySearchStrategyProvider()', () => {
|
|||
const deps = {
|
||||
request: {},
|
||||
};
|
||||
getAuthorizedRuleTypesMock.mockResolvedValue([]);
|
||||
getAlertIndicesAliasMock.mockReturnValue(['o11y-logs']);
|
||||
|
||||
const strategy = ruleRegistrySearchStrategyProvider(
|
||||
data,
|
||||
ruleDataService,
|
||||
alerting,
|
||||
logger,
|
||||
security,
|
||||
spaces
|
||||
);
|
||||
const strategy = ruleRegistrySearchStrategyProvider(data, alerting, logger, security, spaces);
|
||||
|
||||
await strategy
|
||||
.search(request, options, deps as unknown as SearchStrategyDependencies)
|
||||
|
@ -392,15 +305,10 @@ describe('ruleRegistrySearchStrategyProvider()', () => {
|
|||
const deps = {
|
||||
request: {},
|
||||
};
|
||||
getAuthorizedRuleTypesMock.mockResolvedValue([]);
|
||||
getAlertIndicesAliasMock.mockReturnValue(['security-siem']);
|
||||
|
||||
const strategy = ruleRegistrySearchStrategyProvider(
|
||||
data,
|
||||
ruleDataService,
|
||||
alerting,
|
||||
logger,
|
||||
security,
|
||||
spaces
|
||||
);
|
||||
const strategy = ruleRegistrySearchStrategyProvider(data, alerting, logger, security, spaces);
|
||||
|
||||
await strategy
|
||||
.search(request, options, deps as unknown as SearchStrategyDependencies)
|
||||
|
@ -427,7 +335,7 @@ describe('ruleRegistrySearchStrategyProvider()', () => {
|
|||
sort: [],
|
||||
},
|
||||
ignore_unavailable: true,
|
||||
index: ['test-testSpace*'],
|
||||
index: ['security-siem'],
|
||||
},
|
||||
},
|
||||
{},
|
||||
|
@ -447,15 +355,10 @@ describe('ruleRegistrySearchStrategyProvider()', () => {
|
|||
const deps = {
|
||||
request: {},
|
||||
};
|
||||
getAuthorizedRuleTypesMock.mockResolvedValue([]);
|
||||
getAlertIndicesAliasMock.mockReturnValue(['security-siem']);
|
||||
|
||||
const strategy = ruleRegistrySearchStrategyProvider(
|
||||
data,
|
||||
ruleDataService,
|
||||
alerting,
|
||||
logger,
|
||||
security,
|
||||
spaces
|
||||
);
|
||||
const strategy = ruleRegistrySearchStrategyProvider(data, alerting, logger, security, spaces);
|
||||
|
||||
await strategy
|
||||
.search(request, options, deps as unknown as SearchStrategyDependencies)
|
||||
|
@ -477,7 +380,7 @@ describe('ruleRegistrySearchStrategyProvider()', () => {
|
|||
sort: [],
|
||||
},
|
||||
ignore_unavailable: true,
|
||||
index: ['test-testSpace*'],
|
||||
index: ['security-siem'],
|
||||
},
|
||||
},
|
||||
{},
|
||||
|
|
|
@ -12,15 +12,17 @@ import { isEmpty } from 'lodash';
|
|||
import { isValidFeatureId, AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import { ENHANCED_ES_SEARCH_STRATEGY } from '@kbn/data-plugin/common';
|
||||
import { ISearchStrategy, PluginStart } from '@kbn/data-plugin/server';
|
||||
import { ReadOperations, PluginStartContract as AlertingStart } from '@kbn/alerting-plugin/server';
|
||||
import {
|
||||
ReadOperations,
|
||||
PluginStartContract as AlertingStart,
|
||||
AlertingAuthorizationEntity,
|
||||
} from '@kbn/alerting-plugin/server';
|
||||
import { SecurityPluginSetup } from '@kbn/security-plugin/server';
|
||||
import { SpacesPluginStart } from '@kbn/spaces-plugin/server';
|
||||
import {
|
||||
RuleRegistrySearchRequest,
|
||||
RuleRegistrySearchResponse,
|
||||
} from '../../common/search_strategy';
|
||||
import { IRuleDataService } from '..';
|
||||
import { Dataset } from '../rule_data_plugin_service/index_options';
|
||||
import { MAX_ALERT_SEARCH_SIZE } from '../../common/constants';
|
||||
import { AlertAuditAction, alertAuditEvent } from '..';
|
||||
import { getSpacesFilter, getAuthzFilter } from '../lib';
|
||||
|
@ -35,7 +37,6 @@ export const RULE_SEARCH_STRATEGY_NAME = 'privateRuleRegistryAlertsSearchStrateg
|
|||
|
||||
export const ruleRegistrySearchStrategyProvider = (
|
||||
data: PluginStart,
|
||||
ruleDataService: IRuleDataService,
|
||||
alerting: AlertingStart,
|
||||
logger: Logger,
|
||||
security?: SecurityPluginSetup,
|
||||
|
@ -57,10 +58,17 @@ export const ruleRegistrySearchStrategyProvider = (
|
|||
`The ${RULE_SEARCH_STRATEGY_NAME} search strategy is unable to accommodate requests containing multiple feature IDs and one of those IDs is SIEM.`
|
||||
);
|
||||
}
|
||||
request.featureIds.forEach((featureId) => {
|
||||
if (!isValidFeatureId(featureId)) {
|
||||
logger.warn(
|
||||
`Found invalid feature '${featureId}' while using ${RULE_SEARCH_STRATEGY_NAME} search strategy. No alert data from this feature will be searched.`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const securityAuditLogger = security?.audit.asScoped(deps.request);
|
||||
const getActiveSpace = async () => spaces?.spacesService.getActiveSpace(deps.request);
|
||||
const getAsync = async () => {
|
||||
const getAsync = async (featureIds: string[]) => {
|
||||
const [space, authorization] = await Promise.all([
|
||||
getActiveSpace(),
|
||||
alerting.getAlertingAuthorizationWithRequest(deps.request),
|
||||
|
@ -73,28 +81,22 @@ export const ruleRegistrySearchStrategyProvider = (
|
|||
ReadOperations.Find
|
||||
)) as estypes.QueryDslQueryContainer;
|
||||
}
|
||||
return { space, authzFilter };
|
||||
};
|
||||
return from(getAsync()).pipe(
|
||||
mergeMap(({ space, authzFilter }) => {
|
||||
const indices: string[] = request.featureIds.reduce((accum: string[], featureId) => {
|
||||
if (!isValidFeatureId(featureId)) {
|
||||
logger.warn(
|
||||
`Found invalid feature '${featureId}' while using ${RULE_SEARCH_STRATEGY_NAME} search strategy. No alert data from this feature will be searched.`
|
||||
);
|
||||
return accum;
|
||||
}
|
||||
const alertIndexInfo = ruleDataService.findIndexByFeature(featureId, Dataset.alerts);
|
||||
if (alertIndexInfo) {
|
||||
accum.push(
|
||||
featureId === 'siem'
|
||||
? `${alertIndexInfo.baseName}-${space?.id ?? ''}*`
|
||||
: `${alertIndexInfo.baseName}*`
|
||||
);
|
||||
}
|
||||
return accum;
|
||||
}, []);
|
||||
|
||||
const authorizedRuleTypes =
|
||||
featureIds.length > 0
|
||||
? await authorization.getAuthorizedRuleTypes(
|
||||
AlertingAuthorizationEntity.Alert,
|
||||
new Set(featureIds)
|
||||
)
|
||||
: [];
|
||||
return { space, authzFilter, authorizedRuleTypes };
|
||||
};
|
||||
return from(getAsync(request.featureIds)).pipe(
|
||||
mergeMap(({ space, authzFilter, authorizedRuleTypes }) => {
|
||||
const indices = alerting.getAlertIndicesAlias(
|
||||
authorizedRuleTypes.map((art: { id: any }) => art.id),
|
||||
space?.id
|
||||
);
|
||||
if (indices.length === 0) {
|
||||
return of(EMPTY_RESPONSE);
|
||||
}
|
||||
|
|
|
@ -18,7 +18,7 @@ export const allowedExperimentalValues = Object.freeze({
|
|||
ruleStatusFilter: true,
|
||||
rulesDetailLogs: true,
|
||||
ruleUseExecutionStatus: false,
|
||||
ruleKqlBar: true,
|
||||
ruleKqlBar: false,
|
||||
});
|
||||
|
||||
type ExperimentalConfigKeys = Array<keyof ExperimentalFeatures>;
|
||||
|
|
|
@ -5,13 +5,11 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DataView } from '@kbn/data-views-plugin/common';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import { createStartServicesMock } from '../../common/lib/kibana/kibana_react.mock';
|
||||
import type { ValidFeatureId } from '@kbn/rule-data-utils';
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { AsyncState } from 'react-use/lib/useAsync';
|
||||
import { useAlertDataView } from './use_alert_data_view';
|
||||
import { useAlertDataView, UserAlertDataView } from './use_alert_data_view';
|
||||
|
||||
const mockUseKibanaReturnValue = createStartServicesMock();
|
||||
|
||||
|
@ -23,7 +21,6 @@ jest.mock('@kbn/kibana-react-plugin/public', () => ({
|
|||
}));
|
||||
|
||||
describe('useAlertDataView', () => {
|
||||
const mockedDataView = 'dataView';
|
||||
const observabilityAlertFeatureIds: ValidFeatureId[] = [
|
||||
AlertConsumers.APM,
|
||||
AlertConsumers.INFRASTRUCTURE,
|
||||
|
@ -40,7 +37,6 @@ describe('useAlertDataView', () => {
|
|||
'.alerts-observability.apm.alerts-*',
|
||||
],
|
||||
});
|
||||
mockUseKibanaReturnValue.data.dataViews.create = jest.fn().mockReturnValue(mockedDataView);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -51,9 +47,10 @@ describe('useAlertDataView', () => {
|
|||
await act(async () => {
|
||||
const mockedAsyncDataView = {
|
||||
loading: true,
|
||||
error: undefined,
|
||||
};
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook<ValidFeatureId[], AsyncState<DataView>>(() =>
|
||||
const { result, waitForNextUpdate } = renderHook<ValidFeatureId[], UserAlertDataView>(() =>
|
||||
useAlertDataView(observabilityAlertFeatureIds)
|
||||
);
|
||||
|
||||
|
@ -65,19 +62,26 @@ describe('useAlertDataView', () => {
|
|||
|
||||
it('returns dataView for the provided featureIds', async () => {
|
||||
await act(async () => {
|
||||
const mockedAsyncDataView = {
|
||||
loading: false,
|
||||
value: mockedDataView,
|
||||
};
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook<ValidFeatureId[], AsyncState<DataView>>(() =>
|
||||
const { result, waitForNextUpdate } = renderHook<ValidFeatureId[], UserAlertDataView>(() =>
|
||||
useAlertDataView(observabilityAlertFeatureIds)
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current).toEqual(mockedAsyncDataView);
|
||||
expect(result.current).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": undefined,
|
||||
"loading": false,
|
||||
"value": Array [
|
||||
Object {
|
||||
"fieldFormatMap": Object {},
|
||||
"fields": Array [],
|
||||
"title": ".alerts-observability.uptime.alerts-*,.alerts-observability.metrics.alerts-*,.alerts-observability.logs.alerts-*,.alerts-observability.apm.alerts-*",
|
||||
},
|
||||
],
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -88,19 +92,20 @@ describe('useAlertDataView', () => {
|
|||
});
|
||||
|
||||
await act(async () => {
|
||||
const mockedAsyncDataView = {
|
||||
loading: false,
|
||||
error,
|
||||
};
|
||||
|
||||
const { result, waitForNextUpdate } = renderHook<ValidFeatureId[], AsyncState<DataView>>(() =>
|
||||
const { result, waitForNextUpdate } = renderHook<ValidFeatureId[], UserAlertDataView>(() =>
|
||||
useAlertDataView(observabilityAlertFeatureIds)
|
||||
);
|
||||
|
||||
await waitForNextUpdate();
|
||||
await waitForNextUpdate();
|
||||
|
||||
expect(result.current).toEqual(mockedAsyncDataView);
|
||||
expect(result.current).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"error": [Error: http error],
|
||||
"loading": false,
|
||||
"value": undefined,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -5,28 +5,72 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DataView } from '@kbn/data-views-plugin/common';
|
||||
import { DataView, FieldSpec } from '@kbn/data-views-plugin/common';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { BASE_RAC_ALERTS_API_PATH } from '@kbn/rule-registry-plugin/common';
|
||||
import type { ValidFeatureId } from '@kbn/rule-data-utils';
|
||||
import useAsync from 'react-use/lib/useAsync';
|
||||
import type { AsyncState } from 'react-use/lib/useAsync';
|
||||
import { useMemo } from 'react';
|
||||
import { TriggersAndActionsUiServices } from '../..';
|
||||
|
||||
export function useAlertDataView(featureIds: ValidFeatureId[]): AsyncState<DataView> {
|
||||
const { http, data: dataService } = useKibana<TriggersAndActionsUiServices>().services;
|
||||
export interface UserAlertDataView {
|
||||
value?: DataView[];
|
||||
loading: boolean;
|
||||
error?: Error;
|
||||
}
|
||||
|
||||
export function useAlertDataView(featureIds: ValidFeatureId[]): UserAlertDataView {
|
||||
const { http } = useKibana<TriggersAndActionsUiServices>().services;
|
||||
const features = featureIds.sort().join(',');
|
||||
|
||||
const dataView = useAsync(async () => {
|
||||
const { index_name: indexNames } = await http.get<{ index_name: string[] }>(
|
||||
const indexNames = useAsync(async () => {
|
||||
const { index_name: indexNamesStr } = await http.get<{ index_name: string[] }>(
|
||||
`${BASE_RAC_ALERTS_API_PATH}/index`,
|
||||
{
|
||||
query: { features },
|
||||
}
|
||||
);
|
||||
|
||||
return dataService.dataViews.create({ title: indexNames.join(','), allowNoIndex: true });
|
||||
return indexNamesStr;
|
||||
}, [features]);
|
||||
|
||||
return dataView;
|
||||
const fields = useAsync(async () => {
|
||||
const { fields: alertFields } = await http.get<{ fields: FieldSpec[] }>(
|
||||
`${BASE_RAC_ALERTS_API_PATH}/browser_fields`,
|
||||
{
|
||||
query: { featureIds },
|
||||
}
|
||||
);
|
||||
return alertFields;
|
||||
}, [features]);
|
||||
|
||||
const dataview = useMemo(
|
||||
() =>
|
||||
!fields.loading &&
|
||||
!indexNames.loading &&
|
||||
fields.error === undefined &&
|
||||
indexNames.error === undefined
|
||||
? ([
|
||||
{
|
||||
title: (indexNames.value ?? []).join(','),
|
||||
fieldFormatMap: {},
|
||||
fields: (fields.value ?? [])?.map((field) => {
|
||||
return {
|
||||
...field,
|
||||
...(field.esTypes && field.esTypes.includes('flattened')
|
||||
? { type: 'string' }
|
||||
: {}),
|
||||
};
|
||||
}),
|
||||
},
|
||||
] as unknown as DataView[])
|
||||
: undefined,
|
||||
[fields, indexNames]
|
||||
);
|
||||
|
||||
return {
|
||||
value: dataview,
|
||||
loading: fields.loading || indexNames.loading,
|
||||
error: fields.error ? fields.error : indexNames.error,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
import React, { useCallback, useState } from 'react';
|
||||
import { useKibana } from '@kbn/kibana-react-plugin/public';
|
||||
import { Query, TimeRange } from '@kbn/es-query';
|
||||
import { SuggestionsAbstraction } from '@kbn/unified-search-plugin/public/typeahead/suggestions_component';
|
||||
import { NO_INDEX_PATTERNS } from './constants';
|
||||
import { SEARCH_BAR_PLACEHOLDER } from './translations';
|
||||
import { AlertsSearchBarProps, QueryLanguageType } from './types';
|
||||
|
@ -15,6 +16,8 @@ import { useAlertDataView } from '../../hooks/use_alert_data_view';
|
|||
import { TriggersAndActionsUiServices } from '../../..';
|
||||
import { useRuleAADFields } from '../../hooks/use_rule_aad_fields';
|
||||
|
||||
const SA_ALERTS = { type: 'alerts', fields: {} } as SuggestionsAbstraction;
|
||||
|
||||
// TODO Share buildEsQuery to be used between AlertsSearchBar and AlertsStateTable component https://github.com/elastic/kibana/issues/144615
|
||||
export function AlertsSearchBar({
|
||||
appName,
|
||||
|
@ -49,7 +52,7 @@ export function AlertsSearchBar({
|
|||
} = useRuleAADFields(ruleTypeId);
|
||||
|
||||
const indexPatterns =
|
||||
ruleTypeId && aadFields?.length ? [{ title: ruleTypeId, fields: aadFields }] : [dataView!];
|
||||
ruleTypeId && aadFields?.length ? [{ title: ruleTypeId, fields: aadFields }] : dataView;
|
||||
|
||||
const onSearchQuerySubmit = useCallback(
|
||||
({ dateRange, query: nextQuery }: { dateRange: TimeRange; query?: Query }) => {
|
||||
|
@ -102,6 +105,7 @@ export function AlertsSearchBar({
|
|||
showSubmitButton={showSubmitButton}
|
||||
submitOnBlur={submitOnBlur}
|
||||
onQueryChange={onSearchQueryChange}
|
||||
suggestionsAbstraction={SA_ALERTS}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* 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 'expect';
|
||||
import { Spaces } from '../../../scenarios';
|
||||
import { getUrlPrefix } from '../../../../common/lib';
|
||||
import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function createRuleSuggestionValuesTests({ getService }: FtrProviderContext) {
|
||||
const space1 = Spaces[0].id;
|
||||
|
||||
describe('alerts/suggestions/values', async () => {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
|
||||
before(async () => {
|
||||
await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts');
|
||||
await esArchiver.load('x-pack/test/functional/es_archives/security_solution/alerts/8.1.0');
|
||||
});
|
||||
after(async () => {
|
||||
await esArchiver.unload('x-pack/test/functional/es_archives/observability/alerts');
|
||||
await esArchiver.unload('x-pack/test/functional/es_archives/security_solution/alerts/8.1.0');
|
||||
});
|
||||
|
||||
it('Get service.name value suggestion in default space for super user', async () => {
|
||||
const response = await supertest
|
||||
.post(`${getUrlPrefix('default')}/internal/alerts/suggestions/values`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
field: 'service.name',
|
||||
filters: [],
|
||||
query: 'op',
|
||||
});
|
||||
expect(response.body).toEqual(expect.arrayContaining(['opbeans-python', 'opbeans-java']));
|
||||
});
|
||||
|
||||
it('Get service.name value suggestion in space 1 for super user', async () => {
|
||||
const response = await supertest
|
||||
.post(`${getUrlPrefix(space1)}/internal/alerts/suggestions/values`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
field: 'service.name',
|
||||
filters: [],
|
||||
query: 'op',
|
||||
});
|
||||
expect(response.body).toEqual([]);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -191,6 +191,28 @@ export const securitySolutionOnlyAllSpacesAll: Role = {
|
|||
},
|
||||
};
|
||||
|
||||
export const securitySolutionOnlyAllSpacesAllWithReadESIndices: Role = {
|
||||
name: 'sec_only_all_spaces_all_with_read_es_indices',
|
||||
privileges: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['all'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
siem: ['all'],
|
||||
},
|
||||
spaces: ['*'],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const securitySolutionOnlyReadSpacesAll: Role = {
|
||||
name: 'sec_only_read_spaces_all',
|
||||
privileges: {
|
||||
|
@ -468,6 +490,7 @@ export const allRoles = [
|
|||
observabilityOnlyAll,
|
||||
observabilityOnlyRead,
|
||||
securitySolutionOnlyAllSpacesAll,
|
||||
securitySolutionOnlyAllSpacesAllWithReadESIndices,
|
||||
securitySolutionOnlyReadSpacesAll,
|
||||
observabilityOnlyAllSpacesAll,
|
||||
logsOnlyAllSpacesAll,
|
||||
|
|
|
@ -29,6 +29,7 @@ import {
|
|||
observabilityOnlyReadSpace2,
|
||||
observabilityMinReadAlertsAllSpacesAll,
|
||||
observabilityOnlyAllSpacesAllWithReadESIndices,
|
||||
securitySolutionOnlyAllSpacesAllWithReadESIndices,
|
||||
} from './roles';
|
||||
import { User } from './types';
|
||||
|
||||
|
@ -157,6 +158,12 @@ export const secOnlyReadSpacesAll: User = {
|
|||
roles: [securitySolutionOnlyReadSpacesAll.name],
|
||||
};
|
||||
|
||||
export const secOnlySpacesAllEsReadAll: User = {
|
||||
username: 'sec_only_all_spaces_all_with_read_es_indices',
|
||||
password: 'sec_only_all_spaces_all_with_read_es_indices',
|
||||
roles: [securitySolutionOnlyAllSpacesAllWithReadESIndices.name],
|
||||
};
|
||||
|
||||
export const obsOnlySpacesAll: User = {
|
||||
username: 'obs_only_all_spaces_all',
|
||||
password: 'obs_only_all_spaces_all',
|
||||
|
@ -279,6 +286,7 @@ export const allUsers = [
|
|||
noKibanaPrivileges,
|
||||
obsOnlyReadSpacesAll,
|
||||
secOnlySpacesAll,
|
||||
secOnlySpacesAllEsReadAll,
|
||||
secOnlyReadSpacesAll,
|
||||
obsOnlySpacesAll,
|
||||
logsOnlySpacesAll,
|
||||
|
|
|
@ -8,21 +8,11 @@ import expect from '@kbn/expect';
|
|||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
|
||||
import { RuleRegistrySearchResponse } from '@kbn/rule-registry-plugin/common/search_strategy';
|
||||
import { QueryRuleCreateProps } from '@kbn/security-solution-plugin/common/api/detection_engine';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
import {
|
||||
deleteAllAlerts,
|
||||
createSignalsIndex,
|
||||
deleteAllRules,
|
||||
getRuleForSignalTesting,
|
||||
createRule,
|
||||
waitForSignalsToBePresent,
|
||||
waitForRuleSuccess,
|
||||
} from '../../../../detection_engine_api_integration/utils';
|
||||
import {
|
||||
obsOnlySpacesAllEsRead,
|
||||
obsOnlySpacesAll,
|
||||
logsOnlySpacesAll,
|
||||
secOnlySpacesAllEsReadAll,
|
||||
} from '../../../common/lib/authentication/users';
|
||||
|
||||
type RuleRegistrySearchResponseWithErrors = RuleRegistrySearchResponse & {
|
||||
|
@ -30,19 +20,12 @@ type RuleRegistrySearchResponseWithErrors = RuleRegistrySearchResponse & {
|
|||
message: string;
|
||||
};
|
||||
|
||||
const ID = 'BhbXBmkBR346wHgn4PeZ';
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default ({ getService }: FtrProviderContext) => {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const secureBsearch = getService('secureBsearch');
|
||||
const log = getService('log');
|
||||
const kbnClient = getService('kibanaServer');
|
||||
const es = getService('es');
|
||||
|
||||
const SPACE1 = 'space1';
|
||||
|
||||
describe('ruleRegistryAlertsSearchStrategy', () => {
|
||||
let kibanaVersion: string;
|
||||
|
@ -116,23 +99,14 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
|
||||
describe('siem', () => {
|
||||
before(async () => {
|
||||
await createSignalsIndex(supertest, log);
|
||||
await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts');
|
||||
await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts');
|
||||
|
||||
const rule: QueryRuleCreateProps = {
|
||||
...getRuleForSignalTesting(['auditbeat-*']),
|
||||
query: `_id:${ID}`,
|
||||
};
|
||||
const { id: createdId } = await createRule(supertest, log, rule);
|
||||
await waitForRuleSuccess({ supertest, log, id: createdId });
|
||||
await waitForSignalsToBePresent(supertest, log, 1, [createdId]);
|
||||
await esArchiver.load('x-pack/test/functional/es_archives/security_solution/alerts/8.1.0');
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await deleteAllAlerts(supertest, log, es);
|
||||
await deleteAllRules(supertest, log);
|
||||
await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts');
|
||||
await esArchiver.unload(
|
||||
'x-pack/test/functional/es_archives/security_solution/alerts/8.1.0'
|
||||
);
|
||||
await esArchiver.unload('x-pack/test/functional/es_archives/observability/alerts');
|
||||
});
|
||||
|
||||
|
@ -140,8 +114,8 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
const result = await secureBsearch.send<RuleRegistrySearchResponse>({
|
||||
supertestWithoutAuth,
|
||||
auth: {
|
||||
username: obsOnlySpacesAllEsRead.username,
|
||||
password: obsOnlySpacesAllEsRead.password,
|
||||
username: secOnlySpacesAllEsReadAll.username,
|
||||
password: secOnlySpacesAllEsReadAll.password,
|
||||
},
|
||||
referer: 'test',
|
||||
kibanaVersion,
|
||||
|
@ -151,7 +125,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
},
|
||||
strategy: 'privateRuleRegistryAlertsSearchStrategy',
|
||||
});
|
||||
expect(result.rawResponse.hits.total).to.eql(1);
|
||||
expect(result.rawResponse.hits.total).to.eql(50);
|
||||
const consumers = result.rawResponse.hits.hits.map(
|
||||
(hit) => hit.fields?.['kibana.alert.rule.consumer']
|
||||
);
|
||||
|
@ -162,8 +136,8 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
const result = await secureBsearch.send<RuleRegistrySearchResponseWithErrors>({
|
||||
supertestWithoutAuth,
|
||||
auth: {
|
||||
username: obsOnlySpacesAllEsRead.username,
|
||||
password: obsOnlySpacesAllEsRead.password,
|
||||
username: secOnlySpacesAllEsReadAll.username,
|
||||
password: secOnlySpacesAllEsReadAll.password,
|
||||
},
|
||||
referer: 'test',
|
||||
kibanaVersion,
|
||||
|
@ -185,8 +159,8 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
const result = await secureBsearch.send<RuleRegistrySearchResponse>({
|
||||
supertestWithoutAuth,
|
||||
auth: {
|
||||
username: obsOnlySpacesAllEsRead.username,
|
||||
password: obsOnlySpacesAllEsRead.password,
|
||||
username: secOnlySpacesAllEsReadAll.username,
|
||||
password: secOnlySpacesAllEsReadAll.password,
|
||||
},
|
||||
referer: 'test',
|
||||
kibanaVersion,
|
||||
|
@ -204,7 +178,7 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
},
|
||||
strategy: 'privateRuleRegistryAlertsSearchStrategy',
|
||||
});
|
||||
expect(result.rawResponse.hits.total).to.eql(1);
|
||||
expect(result.rawResponse.hits.total).to.eql(50);
|
||||
const runtimeFields = result.rawResponse.hits.hits.map(
|
||||
(hit) => hit.fields?.[runtimeFieldKey]
|
||||
);
|
||||
|
@ -214,10 +188,10 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
|
||||
describe('apm', () => {
|
||||
before(async () => {
|
||||
await esArchiver.load('x-pack/test/functional/es_archives/rule_registry/alerts');
|
||||
await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts');
|
||||
});
|
||||
after(async () => {
|
||||
await esArchiver.unload('x-pack/test/functional/es_archives/rule_registry/alerts');
|
||||
await esArchiver.unload('x-pack/test/functional/es_archives/observability/alerts');
|
||||
});
|
||||
|
||||
it('should return alerts from apm rules', async () => {
|
||||
|
@ -234,9 +208,9 @@ export default ({ getService }: FtrProviderContext) => {
|
|||
featureIds: [AlertConsumers.APM],
|
||||
},
|
||||
strategy: 'privateRuleRegistryAlertsSearchStrategy',
|
||||
space: SPACE1,
|
||||
space: 'default',
|
||||
});
|
||||
expect(result.rawResponse.hits.total).to.eql(2);
|
||||
expect(result.rawResponse.hits.total).to.eql(9);
|
||||
const consumers = result.rawResponse.hits.hits.map(
|
||||
(hit) => hit.fields?.['kibana.alert.rule.consumer']
|
||||
);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue