mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 10:40:07 -04:00
[Observability] [Serverless] Introduce custom roles (#219861)
## Summary Closes https://github.com/elastic/observability-dev/issues/4539 Fixes https://github.com/elastic/kibana/issues/221035 Enables custom roles for Observability projects in serverless. The following is a summary of the changes: ## Feature renaming 1. Renamed `Uptime and Synthetics` to `Synthetics` 2. Renamed `APM and User Experience` to `Applications` 3. Renamed `Metrics` to `Infrastructure` ## Category reassignment 1. Changed `Dashboard` category from `Analytics` to `Observability` 2. Changed `Discover` category from `Analytics` to `Observability` 3. Changed `ML` category from `Analytics` to `Observability` ## Feature hiding 1. Hides the `Stack Alerts` feature. 2. Provides backwards compatibility for alerts created via Stack Alerts. This enables our users to import rules created within Stack Alerts and expect to see them in the Observability rules table. ## Navigation updates 1. Adds a `Custom Roles` link under the `Access` section in the management navigation 2. Adds a `Manage Organization Members` link under the `Access` section in the management navigation 3. Removes the `Users and Roles` link from the navigation footer (in favor of the `Manage Organization Members link) ## Bug fixes 1. Fixes a bug where the `Alerts` link was not shown for Synthetics only user (in stateful and serverless) 2. Fixes a bug where the `Alerts` link was not shown for Logs only user (in stateful and serverless) ## Alert Override Removal In the alerting framework, each rule is assigned a `consumer` value. This `consumer` value changes depending on where the rule is created in Kibana. However, in serverless we introduced an override that caused the `consumer` value to be `Observability` in nearly every case. This logic branched from stateful causing complexity and a large mental burden for our engineers. Ultimately, this override became the source of bugs, uncertainty, and unintended user experiences. Because of this, we've removed this overrides. If we kept this override, it would have the unfortunate side effect of making all rules created in serverless visible from all custom roles (an APM only user would have been can see Synthetics rules, and vice versus). To make things more unpredictable, when users import their rules from stateful the behavior would be different (access would be properly mapped to the specific feature). To address these specific user experience issues, and remove the source of complexity, branching logic, and bugs, we removed this override logic and restored the rule access behavior to match with stateful. We did this while introducing backwards compatibility logic, ensuring rules created in earlier versions of an oblt stateful cluster continue to work and are accessible by a user with the right role access. # Testing 1. Run local ES ``` yarn es serverless --projectType=oblt -E xpack.security.authc.native_roles.enabled=true ``` 2. Run local Kibana ``` yarn start --serverless=oblt --xpack.security.roleManagementEnabled=true --xpack.cloud.users_and_roles_url="https://test_users_and_roles_url" ``` 3. Login to Kibana with the admin role. Navigate to the Custom Roles page via the management navigation. 4. Create a custom role 5. Log out of Kibana 6. Log back in with your custom role. You can do so by typing the custom role name into the mock saml auth <img width="460" alt="Screenshot 2025-05-22 at 9 23 13 PM" src="https://github.com/user-attachments/assets/8e7f659b-5fe9-4e74-8c57-b420467d309e" /> --------- Co-authored-by: Jason Rhodes <jason.rhodes@elastic.co> Co-authored-by: Faisal Kanout <faisal.kanout@elastic.co> Co-authored-by: “jeramysoucy” <jeramy.soucy@elastic.co>
This commit is contained in:
parent
af7ed3f2a3
commit
f15d325e3c
51 changed files with 5524 additions and 21681 deletions
|
@ -44,4 +44,4 @@ enabled:
|
|||
- x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.apm.serverless.config.ts
|
||||
- x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.synthetics.serverless.config.ts
|
||||
- x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.streams.serverless.config.ts
|
||||
- x-pack/test/api_integration/deployment_agnostic/feature_flag_configs/serverless/oblt.synthetics.serverless.config.ts
|
||||
- x-pack/test/api_integration/deployment_agnostic/feature_flag_configs/serverless/oblt.serverless.config.ts
|
||||
|
|
|
@ -48,4 +48,4 @@ enabled:
|
|||
- x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts
|
||||
- x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.apm.stateful.config.ts
|
||||
- x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.ai_assistant.stateful.config.ts
|
||||
- x-pack/test/api_integration/deployment_agnostic/feature_flag_configs/stateful/oblt.synthetics.stateful.config.ts
|
||||
- x-pack/test/api_integration/deployment_agnostic/feature_flag_configs/stateful/oblt.stateful.config.ts
|
||||
|
|
|
@ -5,59 +5,5 @@ xpack.infra.enabled: true
|
|||
xpack.slo.enabled: true
|
||||
|
||||
xpack.features.overrides:
|
||||
### Applications feature privileges are fine-tuned to grant access to Logs, and Observability apps.
|
||||
apm:
|
||||
### By default, this feature named as `APM and User Experience`, but should be renamed to `Applications`.
|
||||
name: "Applications"
|
||||
privileges:
|
||||
# Infrastructure's `All` feature privilege should implicitly grant `All` access to Logs and Observability apps.
|
||||
all.composedOf:
|
||||
- feature: "logs"
|
||||
privileges: [ "all" ]
|
||||
- feature: "observability"
|
||||
privileges: [ "all" ]
|
||||
# Infrastructure's `Read` feature privilege should implicitly grant `Read` access to Logs and Observability apps.
|
||||
read.composedOf:
|
||||
- feature: "logs"
|
||||
privileges: [ "read" ]
|
||||
- feature: "observability"
|
||||
privileges: [ "read" ]
|
||||
### Fleet feature privileges are fine-tuned to grant access to Logs app.
|
||||
fleetv2:
|
||||
privileges:
|
||||
# Fleet `All` feature privilege should implicitly grant `All` access to Logs app.
|
||||
all.composedOf:
|
||||
- feature: "logs"
|
||||
privileges: [ "all" ]
|
||||
# Fleet `Read` feature privilege should implicitly grant `Read` access to Logs app.
|
||||
read.composedOf:
|
||||
- feature: "logs"
|
||||
privileges: [ "read" ]
|
||||
infrastructure:
|
||||
### By default, this feature named as `Metrics`, but should be renamed to `Infrastructure`.
|
||||
name: "Infrastructure"
|
||||
privileges:
|
||||
# Infrastructure's `All` feature privilege should implicitly grant `All` access to Logs and Observability apps.
|
||||
all.composedOf:
|
||||
- feature: "logs"
|
||||
privileges: [ "all" ]
|
||||
- feature: "observability"
|
||||
privileges: [ "all" ]
|
||||
# Infrastructure's `Read` feature privilege should implicitly grant `Read` access to Logs and Observability apps.
|
||||
read.composedOf:
|
||||
- feature: "logs"
|
||||
privileges: [ "read" ]
|
||||
- feature: "observability"
|
||||
privileges: [ "read" ]
|
||||
### Logs feature is hidden in Role management since it's automatically granted by either Infrastructure, or Applications features.
|
||||
logs.hidden: true
|
||||
slo:
|
||||
privileges:
|
||||
# SLOs `All` feature privilege should implicitly grant `All` access to Observability app.
|
||||
all.composedOf:
|
||||
- feature: "observability"
|
||||
privileges: [ "all" ]
|
||||
# SLOs `Read` feature privilege should implicitly grant `Read` access to Observability app.
|
||||
read.composedOf:
|
||||
- feature: "observability"
|
||||
privileges: [ "read" ]
|
||||
### By default, this feature named as `Metrics`, but should be renamed to `Infrastructure`.
|
||||
infrastructure.name: 'Infrastructure'
|
||||
|
|
|
@ -14,54 +14,21 @@ xpack.ux.enabled: false
|
|||
xpack.legacy_uptime.enabled: false
|
||||
|
||||
xpack.features.overrides:
|
||||
### By default, this feature named as `APM and User Experience`, but should be renamed to `Applications`.
|
||||
apm.name: 'Applications'
|
||||
### Dashboards feature should be moved from Analytics category to the Observability one.
|
||||
dashboard_v2.category: "observability"
|
||||
dashboard_v2.category: 'observability'
|
||||
### Discover feature should be moved from Analytics category to the Observability one and its privileges are
|
||||
### fine-tuned to grant access to Observability app.
|
||||
discover:
|
||||
privileges:
|
||||
# Discover `All` feature privilege should implicitly grant `All` access to Observability app.
|
||||
all.composedOf:
|
||||
- feature: "observability"
|
||||
privileges: [ "all" ]
|
||||
# Discover `Read` feature privilege should implicitly grant `Read` access to Observability app.
|
||||
read.composedOf:
|
||||
- feature: "observability"
|
||||
privileges: [ "read" ]
|
||||
discover_v2:
|
||||
category: "observability"
|
||||
privileges:
|
||||
# Discover `All` feature privilege should implicitly grant `All` access to Observability app.
|
||||
all.composedOf:
|
||||
- feature: "observability"
|
||||
privileges: [ "all" ]
|
||||
# Discover `Read` feature privilege should implicitly grant `Read` access to Observability app.
|
||||
read.composedOf:
|
||||
- feature: "observability"
|
||||
privileges: [ "read" ]
|
||||
discover_v2.category: 'observability'
|
||||
### Machine Learning feature should be moved from Analytics category to the Observability one and renamed to `AI Ops`.
|
||||
ml:
|
||||
category: "observability"
|
||||
category: 'observability'
|
||||
order: 1200
|
||||
### Observability feature is hidden in Role management since it's automatically granted by either Discover,
|
||||
### Infrastructure, Applications, Synthetics, or SLOs features.
|
||||
observability.hidden: true
|
||||
### Stack alerts is hidden in Role management since it's not needed.
|
||||
stackAlerts.hidden: true
|
||||
### Synthetics feature privileges are fine-tuned to grant access to Observability app.
|
||||
uptime:
|
||||
### By default, this feature named as `Synthetics and Uptime`, but should be renamed to `Synthetics` since `Uptime` is not available.
|
||||
name: "Synthetics"
|
||||
privileges:
|
||||
# Synthetics `All` feature privilege should implicitly grant `All` access to Observability app.
|
||||
all.composedOf:
|
||||
- feature: "observability"
|
||||
privileges: [ "all" ]
|
||||
# Synthetics `Read` feature privilege should implicitly grant `Read` access to Observability app.
|
||||
read.composedOf:
|
||||
- feature: "observability"
|
||||
privileges: [ "read" ]
|
||||
|
||||
### By default, this feature named as `Synthetics and Uptime`, but should be renamed to `Synthetics` since `Uptime` is not available.
|
||||
uptime.name: 'Synthetics'
|
||||
|
||||
## Cloud settings
|
||||
xpack.cloud.serverless.project_type: observability
|
||||
|
@ -81,11 +48,6 @@ xpack.fleet.agentIdVerificationEnabled: false
|
|||
## Enable event.ingested separately because agentIdVerification is disabled
|
||||
xpack.fleet.eventIngestedEnabled: true
|
||||
|
||||
## Enable the capability for the observability feature ID in the serverless environment to take ownership of the rules.
|
||||
## The value need to be a featureId observability Or stackAlerts Or siem
|
||||
xpack.alerting.rules.overwriteProducer: 'observability'
|
||||
xpack.observability.createO11yGenericFeatureId: true
|
||||
|
||||
## APM Serverless Onboarding flow
|
||||
xpack.apm.serverlessOnboarding: true
|
||||
|
||||
|
@ -140,8 +102,8 @@ xpack.apm.featureFlags.sourcemapApiAvailable: false
|
|||
xpack.apm.featureFlags.storageExplorerAvailable: false
|
||||
|
||||
## Set the AI Assistant type
|
||||
aiAssistantManagementSelection.preferredAIAssistantType: "observability"
|
||||
xpack.observabilityAIAssistant.scope: "observability"
|
||||
aiAssistantManagementSelection.preferredAIAssistantType: 'observability'
|
||||
xpack.observabilityAIAssistant.scope: 'observability'
|
||||
|
||||
# Specify in telemetry the project type
|
||||
telemetry.labels.serverless: observability
|
||||
|
|
|
@ -33,6 +33,7 @@ export const AlertConsumers = {
|
|||
DISCOVER: 'discover',
|
||||
} as const;
|
||||
export type AlertConsumers = (typeof AlertConsumers)[keyof typeof AlertConsumers];
|
||||
export const DEPRECATED_ALERTING_CONSUMERS = [AlertConsumers.OBSERVABILITY];
|
||||
export type STATUS_VALUES = 'open' | 'acknowledged' | 'closed' | 'in-progress'; // TODO: remove 'in-progress' after migration to 'acknowledged'
|
||||
|
||||
export type ValidFeatureId = AlertConsumers;
|
||||
|
|
|
@ -46,7 +46,6 @@ export interface CreateRuleFormProps {
|
|||
canShowConsumerSelection?: boolean;
|
||||
showMustacheAutocompleteSwitch?: boolean;
|
||||
isFlyout?: boolean;
|
||||
isServerless?: boolean;
|
||||
onCancel?: () => void;
|
||||
onSubmit?: (ruleId: string) => void;
|
||||
onChangeMetaData?: (metadata?: RuleTypeMetaData) => void;
|
||||
|
@ -67,7 +66,6 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => {
|
|||
canShowConsumerSelection = true,
|
||||
showMustacheAutocompleteSwitch = false,
|
||||
isFlyout,
|
||||
isServerless = false,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
onChangeMetaData,
|
||||
|
@ -213,7 +211,6 @@ export const CreateRuleForm = (props: CreateRuleFormProps) => {
|
|||
validConsumers,
|
||||
ruleType,
|
||||
ruleTypes,
|
||||
isServerless,
|
||||
}),
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -41,7 +41,6 @@ export interface RuleFormProps<MetaData extends RuleTypeMetaData = RuleTypeMetaD
|
|||
showMustacheAutocompleteSwitch?: boolean;
|
||||
initialValues?: Partial<Omit<RuleFormData, 'ruleTypeId'>>;
|
||||
initialMetadata?: MetaData;
|
||||
isServerless?: boolean;
|
||||
}
|
||||
|
||||
export const RuleForm = <MetaData extends RuleTypeMetaData = RuleTypeMetaData>(
|
||||
|
@ -66,7 +65,6 @@ export const RuleForm = <MetaData extends RuleTypeMetaData = RuleTypeMetaData>(
|
|||
showMustacheAutocompleteSwitch,
|
||||
initialValues,
|
||||
initialMetadata,
|
||||
isServerless,
|
||||
} = props;
|
||||
|
||||
const {
|
||||
|
@ -147,7 +145,6 @@ export const RuleForm = <MetaData extends RuleTypeMetaData = RuleTypeMetaData>(
|
|||
showMustacheAutocompleteSwitch={showMustacheAutocompleteSwitch}
|
||||
initialValues={initialValues}
|
||||
initialMetadata={initialMetadata}
|
||||
isServerless={isServerless}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -179,7 +176,6 @@ export const RuleForm = <MetaData extends RuleTypeMetaData = RuleTypeMetaData>(
|
|||
ruleTypeRegistry,
|
||||
actionTypeRegistry,
|
||||
contentManagement,
|
||||
isServerless,
|
||||
id,
|
||||
ruleTypeId,
|
||||
validConsumers,
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { AlertConsumers, DEPRECATED_ALERTING_CONSUMERS } from '@kbn/rule-data-utils';
|
||||
import { getAuthorizedConsumers } from './get_authorized_consumers';
|
||||
import type { RuleTypeWithDescription } from '../common/types';
|
||||
import type { RuleCreationValidConsumer } from '@kbn/rule-data-utils';
|
||||
|
||||
describe('getAuthorizedConsumers', () => {
|
||||
// @ts-ignore
|
||||
const mockValidConsumers = ['consumer1', 'consumer2'] as RuleCreationValidConsumer[];
|
||||
|
||||
it('should return authorized consumers that have "all" privileges and are valid', () => {
|
||||
const mockRuleType: RuleTypeWithDescription = {
|
||||
authorizedConsumers: {
|
||||
consumer1: { all: true },
|
||||
consumer2: { all: false },
|
||||
consumer3: { all: true },
|
||||
},
|
||||
} as unknown as RuleTypeWithDescription;
|
||||
|
||||
const result = getAuthorizedConsumers({
|
||||
ruleType: mockRuleType,
|
||||
validConsumers: mockValidConsumers,
|
||||
});
|
||||
|
||||
expect(result).toEqual(['consumer1']);
|
||||
});
|
||||
|
||||
it('should filter out deprecated consumers', () => {
|
||||
const mockRuleType: RuleTypeWithDescription = {
|
||||
authorizedConsumers: {
|
||||
[AlertConsumers.OBSERVABILITY]: { all: true },
|
||||
consumer1: { all: true },
|
||||
},
|
||||
} as unknown as RuleTypeWithDescription;
|
||||
|
||||
const result = getAuthorizedConsumers({
|
||||
ruleType: mockRuleType,
|
||||
validConsumers: [...mockValidConsumers, ...DEPRECATED_ALERTING_CONSUMERS],
|
||||
});
|
||||
|
||||
expect(result).toEqual(['consumer1']);
|
||||
});
|
||||
|
||||
it('should return an empty array if no consumers have "all" privileges', () => {
|
||||
const mockRuleType: RuleTypeWithDescription = {
|
||||
authorizedConsumers: {
|
||||
consumer1: { all: false },
|
||||
consumer2: { all: false },
|
||||
},
|
||||
} as unknown as RuleTypeWithDescription;
|
||||
|
||||
const result = getAuthorizedConsumers({
|
||||
ruleType: mockRuleType,
|
||||
validConsumers: mockValidConsumers,
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return an empty array if no valid consumers are authorized', () => {
|
||||
const mockRuleType: RuleTypeWithDescription = {
|
||||
authorizedConsumers: {
|
||||
consumer3: { all: true },
|
||||
},
|
||||
} as unknown as RuleTypeWithDescription;
|
||||
|
||||
const result = getAuthorizedConsumers({
|
||||
ruleType: mockRuleType,
|
||||
validConsumers: mockValidConsumers,
|
||||
});
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
|
@ -8,6 +8,7 @@
|
|||
*/
|
||||
|
||||
import type { RuleCreationValidConsumer } from '@kbn/rule-data-utils';
|
||||
import { DEPRECATED_ALERTING_CONSUMERS } from '@kbn/rule-data-utils';
|
||||
import type { RuleTypeWithDescription } from '../common/types';
|
||||
|
||||
export const getAuthorizedConsumers = ({
|
||||
|
@ -17,8 +18,8 @@ export const getAuthorizedConsumers = ({
|
|||
ruleType: RuleTypeWithDescription;
|
||||
validConsumers: RuleCreationValidConsumer[];
|
||||
}) => {
|
||||
return Object.entries(ruleType.authorizedConsumers).reduce<RuleCreationValidConsumer[]>(
|
||||
(result, [authorizedConsumer, privilege]) => {
|
||||
return Object.entries(ruleType.authorizedConsumers)
|
||||
.reduce<RuleCreationValidConsumer[]>((result, [authorizedConsumer, privilege]) => {
|
||||
if (
|
||||
privilege.all &&
|
||||
validConsumers.includes(authorizedConsumer as RuleCreationValidConsumer)
|
||||
|
@ -26,7 +27,11 @@ export const getAuthorizedConsumers = ({
|
|||
result.push(authorizedConsumer as RuleCreationValidConsumer);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
[]
|
||||
);
|
||||
}, [])
|
||||
.filter((consumer) => {
|
||||
// Filter out deprecated alerting consumers
|
||||
return !(DEPRECATED_ALERTING_CONSUMERS as RuleCreationValidConsumer[]).includes(
|
||||
consumer as RuleCreationValidConsumer
|
||||
);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -47,37 +47,7 @@ describe('getInitialMultiConsumer', () => {
|
|||
} as RuleTypeWithDescription;
|
||||
|
||||
const ruleTypes = [
|
||||
{
|
||||
id: '.es-query',
|
||||
name: 'Test',
|
||||
actionGroups: [
|
||||
{
|
||||
id: 'testActionGroup',
|
||||
name: 'Test Action Group',
|
||||
},
|
||||
{
|
||||
id: 'recovered',
|
||||
name: 'Recovered',
|
||||
},
|
||||
],
|
||||
defaultActionGroupId: 'testActionGroup',
|
||||
minimumLicenseRequired: 'basic',
|
||||
recoveryActionGroup: {
|
||||
id: 'recovered',
|
||||
},
|
||||
producer: 'logs',
|
||||
authorizedConsumers: {
|
||||
alerting: { read: true, all: true },
|
||||
test: { read: true, all: true },
|
||||
stackAlerts: { read: true, all: true },
|
||||
logs: { read: true, all: true },
|
||||
},
|
||||
actionVariables: {
|
||||
params: [],
|
||||
state: [],
|
||||
},
|
||||
enabledInLicense: true,
|
||||
},
|
||||
ruleType,
|
||||
{
|
||||
enabledInLicense: true,
|
||||
recoveryActionGroup: {
|
||||
|
@ -108,13 +78,12 @@ describe('getInitialMultiConsumer', () => {
|
|||
test('should return null when rule type id does not match', () => {
|
||||
const res = getInitialMultiConsumer({
|
||||
multiConsumerSelection: null,
|
||||
validConsumers: ['logs', 'observability'],
|
||||
validConsumers: ['logs'],
|
||||
ruleType: {
|
||||
...ruleType,
|
||||
id: 'test',
|
||||
},
|
||||
ruleTypes,
|
||||
isServerless: false,
|
||||
});
|
||||
|
||||
expect(res).toBe(null);
|
||||
|
@ -126,7 +95,6 @@ describe('getInitialMultiConsumer', () => {
|
|||
validConsumers: [],
|
||||
ruleType,
|
||||
ruleTypes,
|
||||
isServerless: false,
|
||||
});
|
||||
|
||||
expect(res).toBe(null);
|
||||
|
@ -138,7 +106,6 @@ describe('getInitialMultiConsumer', () => {
|
|||
validConsumers: ['alerts'],
|
||||
ruleType,
|
||||
ruleTypes,
|
||||
isServerless: false,
|
||||
});
|
||||
|
||||
expect(res).toBe('alerts');
|
||||
|
@ -147,25 +114,49 @@ describe('getInitialMultiConsumer', () => {
|
|||
test('should not return observability consumer for non serverless', () => {
|
||||
const res = getInitialMultiConsumer({
|
||||
multiConsumerSelection: null,
|
||||
validConsumers: ['logs', 'infrastructure', 'observability'],
|
||||
validConsumers: ['logs', 'infrastructure'],
|
||||
ruleType,
|
||||
ruleTypes,
|
||||
isServerless: false,
|
||||
});
|
||||
|
||||
expect(res).toBe('logs');
|
||||
});
|
||||
|
||||
test('should return observability consumer for serverless', () => {
|
||||
test('should return valid consumer when user has only logs privileges', () => {
|
||||
const res = getInitialMultiConsumer({
|
||||
multiConsumerSelection: null,
|
||||
validConsumers: ['logs', 'infrastructure', 'observability'],
|
||||
ruleType,
|
||||
validConsumers: ['infrastructure', 'logs'],
|
||||
ruleType: {
|
||||
...ruleType,
|
||||
authorizedConsumers: {
|
||||
logs: { read: true, all: true },
|
||||
},
|
||||
},
|
||||
ruleTypes,
|
||||
isServerless: true,
|
||||
});
|
||||
|
||||
expect(res).toBe('observability');
|
||||
expect(res).toBe('logs');
|
||||
});
|
||||
|
||||
test('should return valid consumer when user has only infrastructure privileges', () => {
|
||||
const res = getInitialMultiConsumer({
|
||||
multiConsumerSelection: null,
|
||||
validConsumers: ['infrastructure', 'logs'],
|
||||
ruleType: {
|
||||
...ruleType,
|
||||
authorizedConsumers: {
|
||||
infrastructure: { read: true, all: true },
|
||||
},
|
||||
},
|
||||
ruleTypes: ruleTypes.map((rule) => ({
|
||||
...rule,
|
||||
authorizedConsumers: {
|
||||
infrastructure: { read: true, all: true },
|
||||
},
|
||||
})),
|
||||
});
|
||||
|
||||
expect(res).toBe('infrastructure');
|
||||
});
|
||||
|
||||
test('should return null when there is no authorized consumers', () => {
|
||||
|
@ -177,7 +168,6 @@ describe('getInitialMultiConsumer', () => {
|
|||
authorizedConsumers: {},
|
||||
},
|
||||
ruleTypes,
|
||||
isServerless: false,
|
||||
});
|
||||
|
||||
expect(res).toBe(null);
|
||||
|
@ -194,7 +184,6 @@ describe('getInitialMultiConsumer', () => {
|
|||
},
|
||||
},
|
||||
ruleTypes,
|
||||
isServerless: false,
|
||||
});
|
||||
|
||||
expect(res).toBe(null);
|
||||
|
@ -211,7 +200,6 @@ describe('getInitialMultiConsumer', () => {
|
|||
},
|
||||
},
|
||||
ruleTypes,
|
||||
isServerless: false,
|
||||
});
|
||||
|
||||
expect(res).toBe('logs');
|
||||
|
@ -226,7 +214,6 @@ describe('getInitialMultiConsumer', () => {
|
|||
authorizedConsumers: {},
|
||||
},
|
||||
ruleTypes,
|
||||
isServerless: false,
|
||||
});
|
||||
|
||||
expect(res).toBe('stackAlerts');
|
||||
|
@ -241,7 +228,6 @@ describe('getInitialMultiConsumer', () => {
|
|||
authorizedConsumers: {},
|
||||
},
|
||||
ruleTypes: [],
|
||||
isServerless: false,
|
||||
});
|
||||
|
||||
expect(res).toBe(null);
|
||||
|
|
|
@ -36,13 +36,11 @@ export const getInitialMultiConsumer = ({
|
|||
validConsumers,
|
||||
ruleType,
|
||||
ruleTypes,
|
||||
isServerless,
|
||||
}: {
|
||||
multiConsumerSelection?: RuleCreationValidConsumer | null;
|
||||
validConsumers: RuleCreationValidConsumer[];
|
||||
ruleType: RuleTypeWithDescription;
|
||||
ruleTypes: RuleTypeWithDescription[];
|
||||
isServerless?: boolean;
|
||||
}): RuleCreationValidConsumer | null => {
|
||||
// If rule type doesn't support multi-consumer or no valid consumers exists,
|
||||
// return nothing
|
||||
|
@ -55,11 +53,6 @@ export const getInitialMultiConsumer = ({
|
|||
return validConsumers[0];
|
||||
}
|
||||
|
||||
// If o11y is in the valid consumers and it is serverless, just use that
|
||||
if (isServerless && validConsumers.includes(AlertConsumers.OBSERVABILITY)) {
|
||||
return AlertConsumers.OBSERVABILITY;
|
||||
}
|
||||
|
||||
const selectedAvailableRuleType = ruleTypes.find((availableRuleType) => {
|
||||
return availableRuleType.id === ruleType.id;
|
||||
});
|
||||
|
@ -87,7 +80,7 @@ export const getInitialMultiConsumer = ({
|
|||
validConsumers,
|
||||
});
|
||||
|
||||
// If validated consumer exists and no o11y in valid consumers, just use that
|
||||
// If validated consumer exists just use that
|
||||
if (validatedConsumer) {
|
||||
return validatedConsumer;
|
||||
}
|
||||
|
|
|
@ -32005,7 +32005,6 @@
|
|||
"xpack.observability.metric.iconSelect.tagIconLabel": "Balise",
|
||||
"xpack.observability.metric.iconSelect.temperatureLabel": "Température",
|
||||
"xpack.observability.metricWithSparkline.nATextColorLabel": "S. O.",
|
||||
"xpack.observability.nameFeatureTitle": "Observabilité",
|
||||
"xpack.observability.news.readFullStory": "Lire toute l'histoire",
|
||||
"xpack.observability.news.title": "Nouveautés",
|
||||
"xpack.observability.noDataConfig.beatsCard.description": "Utilisez des agents Beats et APM pour envoyer des données d'observabilité à Elasticsearch. Nous facilitons les choses en proposant un support technique pour un grand nombre de langues, d'applications et de systèmes courants.",
|
||||
|
|
|
@ -31983,7 +31983,6 @@
|
|||
"xpack.observability.metric.iconSelect.tagIconLabel": "タグ",
|
||||
"xpack.observability.metric.iconSelect.temperatureLabel": "温度",
|
||||
"xpack.observability.metricWithSparkline.nATextColorLabel": "N/A",
|
||||
"xpack.observability.nameFeatureTitle": "Observability",
|
||||
"xpack.observability.news.readFullStory": "詳細なストーリーを読む",
|
||||
"xpack.observability.news.title": "新機能",
|
||||
"xpack.observability.noDataConfig.beatsCard.description": "BeatsとAPMエージェントを使用して、オブザーバビリティデータをElasticsearchに送信します。多数の一般的なシステム、アプリ、言語では、サポートによって処理が簡単になりました。",
|
||||
|
|
|
@ -32038,7 +32038,6 @@
|
|||
"xpack.observability.metric.iconSelect.tagIconLabel": "标签",
|
||||
"xpack.observability.metric.iconSelect.temperatureLabel": "温度",
|
||||
"xpack.observability.metricWithSparkline.nATextColorLabel": "不可用",
|
||||
"xpack.observability.nameFeatureTitle": "Observability",
|
||||
"xpack.observability.news.readFullStory": "详细了解",
|
||||
"xpack.observability.news.title": "最新动态",
|
||||
"xpack.observability.noDataConfig.beatsCard.description": "使用 Beats 和 APM 代理将 Observability 数据发送到 Elasticsearch。我们使许多流行系统、应用和语言都可以轻松获取支持。",
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import type { KibanaRequest } from '@kbn/core/server';
|
||||
import { DEPRECATED_ALERTING_CONSUMERS } from '@kbn/rule-data-utils';
|
||||
import { ALERTING_FEATURE_ID } from '@kbn/alerting-plugin/common';
|
||||
import { PLUGIN_ID } from '../constants/app';
|
||||
import {
|
||||
|
@ -110,9 +111,9 @@ export function getDefaultCapabilities(): MlCapabilities {
|
|||
};
|
||||
}
|
||||
|
||||
const alertingFeatures = Object.values(ML_ALERT_TYPES).map((ruleTypeId) => ({
|
||||
export const alertingFeatures = Object.values(ML_ALERT_TYPES).map((ruleTypeId) => ({
|
||||
ruleTypeId,
|
||||
consumers: [PLUGIN_ID, ALERTING_FEATURE_ID],
|
||||
consumers: [PLUGIN_ID, ALERTING_FEATURE_ID, ...DEPRECATED_ALERTING_CONSUMERS],
|
||||
}));
|
||||
|
||||
export function getPluginPrivileges() {
|
||||
|
|
|
@ -26,10 +26,9 @@ import type { SpacesPluginSetup } from '@kbn/spaces-plugin/server';
|
|||
import type { FieldFormatsStart } from '@kbn/field-formats-plugin/server';
|
||||
import type { HomeServerPluginSetup } from '@kbn/home-plugin/server';
|
||||
import type { CasesServerSetup } from '@kbn/cases-plugin/server';
|
||||
import { ALERTING_FEATURE_ID } from '@kbn/alerting-plugin/common';
|
||||
import { KibanaFeatureScope } from '@kbn/features-plugin/common';
|
||||
import type { PluginsSetup, PluginsStart, RouteInitialization } from './types';
|
||||
import type { MlCapabilities } from '../common/types/capabilities';
|
||||
import { type MlCapabilities, alertingFeatures } from '../common/types/capabilities';
|
||||
import { notificationsRoutes } from './routes/notifications';
|
||||
import {
|
||||
type MlFeatures,
|
||||
|
@ -70,7 +69,6 @@ import {
|
|||
} from './saved_objects';
|
||||
import { RouteGuard } from './lib/route_guard';
|
||||
import { registerMlAlerts } from './lib/alerts/register_ml_alerts';
|
||||
import { ML_ALERT_TYPES } from '../common/constants/alerts';
|
||||
import { alertingRoutes } from './routes/alerting';
|
||||
import { registerCollector } from './usage';
|
||||
import { SavedObjectsSyncService } from './saved_objects/sync_task';
|
||||
|
@ -143,10 +141,7 @@ export class MlServerPlugin
|
|||
management: {
|
||||
insightsAndAlerting: ['jobsListLink', 'triggersActions'],
|
||||
},
|
||||
alerting: Object.values(ML_ALERT_TYPES).map((ruleTypeId) => ({
|
||||
ruleTypeId,
|
||||
consumers: [PLUGIN_ID, ALERTING_FEATURE_ID],
|
||||
})),
|
||||
alerting: alertingFeatures,
|
||||
privileges: {
|
||||
all: admin,
|
||||
read: user,
|
||||
|
|
|
@ -29,7 +29,6 @@ export const RuleFormRoute = () => {
|
|||
actionTypeRegistry,
|
||||
contentManagement,
|
||||
chrome,
|
||||
isServerless,
|
||||
setBreadcrumbs,
|
||||
...startServices
|
||||
} = useKibana().services;
|
||||
|
@ -78,7 +77,6 @@ export const RuleFormRoute = () => {
|
|||
contentManagement,
|
||||
...startServices,
|
||||
}}
|
||||
isServerless={isServerless}
|
||||
id={id}
|
||||
ruleTypeId={ruleTypeId}
|
||||
onCancel={() => {
|
||||
|
|
|
@ -14,7 +14,7 @@ import type {
|
|||
} from '@kbn/licensing-plugin/server';
|
||||
|
||||
import { APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE } from '@kbn/apm-sources-access-plugin/server/saved_objects/apm_indices';
|
||||
import { ApmRuleType } from '@kbn/rule-data-utils';
|
||||
import { ApmRuleType, DEPRECATED_ALERTING_CONSUMERS } from '@kbn/rule-data-utils';
|
||||
import { ALERTING_FEATURE_ID } from '@kbn/alerting-plugin/common';
|
||||
import type { KibanaFeatureConfig } from '@kbn/features-plugin/common';
|
||||
import { KibanaFeatureScope } from '@kbn/features-plugin/common';
|
||||
|
@ -22,7 +22,7 @@ import { APM_SERVER_FEATURE_ID } from '../common/rules/apm_rule_types';
|
|||
|
||||
const alertingFeatures = Object.values(ApmRuleType).map((ruleTypeId) => ({
|
||||
ruleTypeId,
|
||||
consumers: [APM_SERVER_FEATURE_ID, ALERTING_FEATURE_ID],
|
||||
consumers: [APM_SERVER_FEATURE_ID, ALERTING_FEATURE_ID, ...DEPRECATED_ALERTING_CONSUMERS],
|
||||
}));
|
||||
|
||||
export const APM_FEATURE: KibanaFeatureConfig = {
|
||||
|
|
|
@ -9,12 +9,14 @@ import { i18n } from '@kbn/i18n';
|
|||
import { DEFAULT_APP_CATEGORIES } from '@kbn/core/server';
|
||||
import { logViewSavedObjectName } from '@kbn/logs-shared-plugin/server';
|
||||
import {
|
||||
DEPRECATED_ALERTING_CONSUMERS,
|
||||
ML_ANOMALY_DETECTION_RULE_TYPE_ID,
|
||||
OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import { ES_QUERY_ID } from '@kbn/rule-data-utils';
|
||||
import { metricsDataSourceSavedObjectName } from '@kbn/metrics-data-access-plugin/server';
|
||||
import { ALERTING_FEATURE_ID } from '@kbn/alerting-plugin/common';
|
||||
import type { KibanaFeatureConfig } from '@kbn/features-plugin/common';
|
||||
import { KibanaFeatureScope } from '@kbn/features-plugin/common';
|
||||
import { LOG_DOCUMENT_COUNT_RULE_TYPE_ID } from '../common/alerting/logs/log_threshold/types';
|
||||
import {
|
||||
|
@ -32,69 +34,76 @@ const metricRuleTypes = [
|
|||
ML_ANOMALY_DETECTION_RULE_TYPE_ID,
|
||||
];
|
||||
|
||||
const metricAlertingFeatures = metricRuleTypes.map((ruleTypeId) => ({
|
||||
ruleTypeId,
|
||||
consumers: [METRICS_FEATURE_ID, ALERTING_FEATURE_ID],
|
||||
}));
|
||||
export const getMetricsFeature = (): KibanaFeatureConfig => {
|
||||
const metricAlertingFeatures = metricRuleTypes.map((ruleTypeId) => {
|
||||
const consumers = [METRICS_FEATURE_ID, ALERTING_FEATURE_ID, ...DEPRECATED_ALERTING_CONSUMERS];
|
||||
|
||||
export const METRICS_FEATURE = {
|
||||
id: METRICS_FEATURE_ID,
|
||||
name: i18n.translate('xpack.infra.featureRegistry.linkInfrastructureTitle', {
|
||||
defaultMessage: 'Infrastructure',
|
||||
}),
|
||||
order: 800,
|
||||
category: DEFAULT_APP_CATEGORIES.observability,
|
||||
scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security],
|
||||
app: ['infra', 'metrics', 'kibana'],
|
||||
catalogue: ['infraops', 'metrics'],
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
alerting: metricAlertingFeatures,
|
||||
privileges: {
|
||||
all: {
|
||||
app: ['infra', 'metrics', 'kibana'],
|
||||
catalogue: ['infraops', 'metrics'],
|
||||
api: ['infra', 'rac'],
|
||||
savedObject: {
|
||||
all: ['infrastructure-ui-source', metricsDataSourceSavedObjectName],
|
||||
read: ['index-pattern'],
|
||||
},
|
||||
alerting: {
|
||||
rule: {
|
||||
all: metricAlertingFeatures,
|
||||
},
|
||||
alert: {
|
||||
all: metricAlertingFeatures,
|
||||
},
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
ui: ['show', 'configureSource', 'save'],
|
||||
return {
|
||||
ruleTypeId,
|
||||
consumers,
|
||||
};
|
||||
});
|
||||
|
||||
const METRICS_FEATURE = {
|
||||
id: METRICS_FEATURE_ID,
|
||||
name: i18n.translate('xpack.infra.featureRegistry.linkInfrastructureTitle', {
|
||||
defaultMessage: 'Infrastructure',
|
||||
}),
|
||||
order: 800,
|
||||
category: DEFAULT_APP_CATEGORIES.observability,
|
||||
scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security],
|
||||
app: ['infra', 'metrics', 'kibana'],
|
||||
catalogue: ['infraops', 'metrics'],
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
read: {
|
||||
app: ['infra', 'metrics', 'kibana'],
|
||||
catalogue: ['infraops', 'metrics'],
|
||||
api: ['infra', 'rac'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['infrastructure-ui-source', 'index-pattern', metricsDataSourceSavedObjectName],
|
||||
},
|
||||
alerting: {
|
||||
rule: {
|
||||
read: metricAlertingFeatures,
|
||||
alerting: metricAlertingFeatures,
|
||||
privileges: {
|
||||
all: {
|
||||
app: ['infra', 'metrics', 'kibana'],
|
||||
catalogue: ['infraops', 'metrics'],
|
||||
api: ['infra', 'rac'],
|
||||
savedObject: {
|
||||
all: ['infrastructure-ui-source', metricsDataSourceSavedObjectName],
|
||||
read: ['index-pattern'],
|
||||
},
|
||||
alert: {
|
||||
read: metricAlertingFeatures,
|
||||
alerting: {
|
||||
rule: {
|
||||
all: metricAlertingFeatures,
|
||||
},
|
||||
alert: {
|
||||
all: metricAlertingFeatures,
|
||||
},
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
ui: ['show', 'configureSource', 'save'],
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
read: {
|
||||
app: ['infra', 'metrics', 'kibana'],
|
||||
catalogue: ['infraops', 'metrics'],
|
||||
api: ['infra', 'rac'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: ['infrastructure-ui-source', 'index-pattern', metricsDataSourceSavedObjectName],
|
||||
},
|
||||
alerting: {
|
||||
rule: {
|
||||
read: metricAlertingFeatures,
|
||||
},
|
||||
alert: {
|
||||
read: metricAlertingFeatures,
|
||||
},
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
ui: ['show'],
|
||||
},
|
||||
ui: ['show'],
|
||||
},
|
||||
},
|
||||
};
|
||||
return METRICS_FEATURE;
|
||||
};
|
||||
|
||||
const logsRuleTypes = [
|
||||
|
@ -103,68 +112,74 @@ const logsRuleTypes = [
|
|||
OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
|
||||
ML_ANOMALY_DETECTION_RULE_TYPE_ID,
|
||||
];
|
||||
export const getLogsFeature = (): KibanaFeatureConfig => {
|
||||
const logsAlertingFeatures = logsRuleTypes.map((ruleTypeId) => {
|
||||
const consumers = [LOGS_FEATURE_ID, ALERTING_FEATURE_ID, ...DEPRECATED_ALERTING_CONSUMERS];
|
||||
|
||||
const logsAlertingFeatures = logsRuleTypes.map((ruleTypeId) => ({
|
||||
ruleTypeId,
|
||||
consumers: [LOGS_FEATURE_ID, ALERTING_FEATURE_ID],
|
||||
}));
|
||||
return {
|
||||
ruleTypeId,
|
||||
consumers,
|
||||
};
|
||||
});
|
||||
|
||||
export const LOGS_FEATURE = {
|
||||
id: LOGS_FEATURE_ID,
|
||||
name: i18n.translate('xpack.infra.featureRegistry.linkLogsTitle', {
|
||||
defaultMessage: 'Logs',
|
||||
}),
|
||||
order: 700,
|
||||
category: DEFAULT_APP_CATEGORIES.observability,
|
||||
scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security],
|
||||
app: ['infra', 'logs', 'kibana', 'observability-logs-explorer'],
|
||||
catalogue: ['infralogging', 'logs'],
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
alerting: logsAlertingFeatures,
|
||||
privileges: {
|
||||
all: {
|
||||
app: ['infra', 'logs', 'kibana', 'observability-logs-explorer'],
|
||||
catalogue: ['infralogging', 'logs'],
|
||||
api: ['infra', 'rac'],
|
||||
savedObject: {
|
||||
all: [infraSourceConfigurationSavedObjectName, logViewSavedObjectName],
|
||||
read: [],
|
||||
},
|
||||
alerting: {
|
||||
rule: {
|
||||
all: logsAlertingFeatures,
|
||||
},
|
||||
alert: {
|
||||
all: logsAlertingFeatures,
|
||||
},
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
ui: ['show', 'configureSource', 'save'],
|
||||
const LOGS_FEATURE = {
|
||||
id: LOGS_FEATURE_ID,
|
||||
name: i18n.translate('xpack.infra.featureRegistry.linkLogsTitle', {
|
||||
defaultMessage: 'Logs',
|
||||
}),
|
||||
order: 700,
|
||||
category: DEFAULT_APP_CATEGORIES.observability,
|
||||
scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security],
|
||||
app: ['infra', 'logs', 'kibana', 'observability-logs-explorer'],
|
||||
catalogue: ['infralogging', 'logs'],
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
read: {
|
||||
app: ['infra', 'logs', 'kibana', 'observability-logs-explorer'],
|
||||
catalogue: ['infralogging', 'logs'],
|
||||
api: ['infra', 'rac'],
|
||||
alerting: {
|
||||
rule: {
|
||||
read: logsAlertingFeatures,
|
||||
alerting: logsAlertingFeatures,
|
||||
privileges: {
|
||||
all: {
|
||||
app: ['infra', 'logs', 'kibana', 'observability-logs-explorer'],
|
||||
catalogue: ['infralogging', 'logs'],
|
||||
api: ['infra', 'rac'],
|
||||
savedObject: {
|
||||
all: [infraSourceConfigurationSavedObjectName, logViewSavedObjectName],
|
||||
read: [],
|
||||
},
|
||||
alert: {
|
||||
read: logsAlertingFeatures,
|
||||
alerting: {
|
||||
rule: {
|
||||
all: logsAlertingFeatures,
|
||||
},
|
||||
alert: {
|
||||
all: logsAlertingFeatures,
|
||||
},
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
ui: ['show', 'configureSource', 'save'],
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
read: {
|
||||
app: ['infra', 'logs', 'kibana', 'observability-logs-explorer'],
|
||||
catalogue: ['infralogging', 'logs'],
|
||||
api: ['infra', 'rac'],
|
||||
alerting: {
|
||||
rule: {
|
||||
read: logsAlertingFeatures,
|
||||
},
|
||||
alert: {
|
||||
read: logsAlertingFeatures,
|
||||
},
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [infraSourceConfigurationSavedObjectName, logViewSavedObjectName],
|
||||
},
|
||||
ui: ['show'],
|
||||
},
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [infraSourceConfigurationSavedObjectName, logViewSavedObjectName],
|
||||
},
|
||||
ui: ['show'],
|
||||
},
|
||||
},
|
||||
};
|
||||
return LOGS_FEATURE;
|
||||
};
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
import { type AlertsLocatorParams, alertsLocatorID } from '@kbn/observability-plugin/common';
|
||||
import { mapValues } from 'lodash';
|
||||
import { LOGS_FEATURE_ID, METRICS_FEATURE_ID } from '../common/constants';
|
||||
import { LOGS_FEATURE, METRICS_FEATURE } from './features';
|
||||
import { getLogsFeature, getMetricsFeature } from './features';
|
||||
import { registerRoutes } from './infra_server';
|
||||
import type {
|
||||
InfraServerPluginSetupDeps,
|
||||
|
@ -85,7 +85,6 @@ export class InfraServerPlugin
|
|||
constructor(context: PluginInitializerContext<InfraConfig>) {
|
||||
this.config = context.config.get();
|
||||
this.logger = context.logger.get();
|
||||
|
||||
this.logsRules = new RulesService(
|
||||
LOGS_FEATURE_ID,
|
||||
LOGS_RULES_ALERT_CONTEXT,
|
||||
|
@ -105,7 +104,6 @@ export class InfraServerPlugin
|
|||
|
||||
setup(core: InfraPluginCoreSetup, plugins: InfraServerPluginSetupDeps) {
|
||||
const framework = new KibanaFramework(core, this.config, plugins);
|
||||
|
||||
const metricsClient = plugins.metricsDataAccess.client;
|
||||
metricsClient.setDefaultMetricIndicesHandler(async (options: GetMetricIndicesOptions) => {
|
||||
const sourceConfiguration = await sources.getInfraSourceConfiguration(
|
||||
|
@ -181,8 +179,8 @@ export class InfraServerPlugin
|
|||
plugins: libsPlugins,
|
||||
};
|
||||
|
||||
plugins.features.registerKibanaFeature(METRICS_FEATURE);
|
||||
plugins.features.registerKibanaFeature(LOGS_FEATURE);
|
||||
plugins.features.registerKibanaFeature(getMetricsFeature());
|
||||
plugins.features.registerKibanaFeature(getLogsFeature());
|
||||
|
||||
// Register an handler to retrieve the fallback logView starting from a source configuration
|
||||
plugins.logsShared.logViews.registerLogViewFallbackHandler(async (sourceId, { soClient }) => {
|
||||
|
|
|
@ -30,7 +30,6 @@ export const observabilityAlertFeatureIds: ValidFeatureId[] = [
|
|||
export const observabilityRuleCreationValidConsumers: RuleCreationValidConsumer[] = [
|
||||
AlertConsumers.INFRASTRUCTURE,
|
||||
AlertConsumers.LOGS,
|
||||
AlertConsumers.OBSERVABILITY,
|
||||
];
|
||||
|
||||
export const EventsAsUnit = 'events';
|
||||
|
|
|
@ -25,11 +25,11 @@ export function RulePage() {
|
|||
application,
|
||||
notifications,
|
||||
charts,
|
||||
serverless,
|
||||
settings,
|
||||
data,
|
||||
dataViews,
|
||||
unifiedSearch,
|
||||
serverless,
|
||||
actionTypeRegistry,
|
||||
ruleTypeRegistry,
|
||||
chrome,
|
||||
|
@ -105,7 +105,6 @@ export function RulePage() {
|
|||
ruleTypeId={ruleTypeId}
|
||||
validConsumers={observabilityRuleCreationValidConsumers}
|
||||
multiConsumerSelection={AlertConsumers.LOGS}
|
||||
isServerless={!!serverless}
|
||||
onCancel={() => {
|
||||
if (returnApp && returnPath) {
|
||||
application.navigateToApp(returnApp, { path: returnPath });
|
||||
|
|
|
@ -13,18 +13,10 @@ import {
|
|||
getApiTags as getCasesApiTags,
|
||||
} from '@kbn/cases-plugin/common';
|
||||
import { CloudSetup } from '@kbn/cloud-plugin/server';
|
||||
import {
|
||||
CoreSetup,
|
||||
CoreStart,
|
||||
DEFAULT_APP_CATEGORIES,
|
||||
Logger,
|
||||
Plugin,
|
||||
PluginInitializerContext,
|
||||
} from '@kbn/core/server';
|
||||
import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from '@kbn/core/server';
|
||||
import { DISCOVER_APP_LOCATOR, type DiscoverAppLocatorParams } from '@kbn/discover-plugin/common';
|
||||
import { FeaturesPluginSetup } from '@kbn/features-plugin/server';
|
||||
import type { GuidedOnboardingPluginSetup } from '@kbn/guided-onboarding-plugin/server';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
RuleRegistryPluginSetupContract,
|
||||
RuleRegistryPluginStartContract,
|
||||
|
@ -33,8 +25,6 @@ import { SharePluginSetup } from '@kbn/share-plugin/server';
|
|||
import { SpacesPluginSetup, SpacesPluginStart } from '@kbn/spaces-plugin/server';
|
||||
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
|
||||
import { DataViewsServerPluginStart } from '@kbn/data-views-plugin/server';
|
||||
import { ALERTING_FEATURE_ID } from '@kbn/alerting-plugin/common';
|
||||
import { KibanaFeatureScope } from '@kbn/features-plugin/common';
|
||||
import { ObservabilityConfig } from '.';
|
||||
import { observabilityFeatureId } from '../common';
|
||||
import {
|
||||
|
@ -53,7 +43,6 @@ import { registerRoutes } from './routes/register_routes';
|
|||
import { threshold } from './saved_objects/threshold';
|
||||
import { AlertDetailsContextualInsightsService } from './services';
|
||||
import { uiSettings } from './ui_settings';
|
||||
import { OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES } from '../common/constants';
|
||||
import { getCasesFeature } from './features/cases_v1';
|
||||
import { getCasesFeatureV2 } from './features/cases_v2';
|
||||
import { getCasesFeatureV3 } from './features/cases_v3';
|
||||
|
@ -79,14 +68,6 @@ interface PluginStart {
|
|||
ruleRegistry: RuleRegistryPluginStartContract;
|
||||
dashboard: DashboardPluginStart;
|
||||
}
|
||||
|
||||
const alertingFeatures = OBSERVABILITY_RULE_TYPE_IDS_WITH_SUPPORTED_STACK_RULE_TYPES.map(
|
||||
(ruleTypeId) => ({
|
||||
ruleTypeId,
|
||||
consumers: [observabilityFeatureId, ALERTING_FEATURE_ID],
|
||||
})
|
||||
);
|
||||
|
||||
export class ObservabilityPlugin
|
||||
implements Plugin<ObservabilityPluginSetup, void, PluginSetup, PluginStart>
|
||||
{
|
||||
|
@ -130,59 +111,6 @@ export class ObservabilityPlugin
|
|||
});
|
||||
}
|
||||
|
||||
if (config.createO11yGenericFeatureId) {
|
||||
plugins.features.registerKibanaFeature({
|
||||
id: observabilityFeatureId,
|
||||
name: i18n.translate('xpack.observability.nameFeatureTitle', {
|
||||
defaultMessage: 'Observability',
|
||||
}),
|
||||
order: 1000,
|
||||
category: DEFAULT_APP_CATEGORIES.observability,
|
||||
scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security],
|
||||
app: [observabilityFeatureId],
|
||||
catalogue: [observabilityFeatureId],
|
||||
alerting: alertingFeatures,
|
||||
privileges: {
|
||||
all: {
|
||||
app: [observabilityFeatureId],
|
||||
catalogue: [observabilityFeatureId],
|
||||
api: ['rac'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
alerting: {
|
||||
rule: {
|
||||
all: alertingFeatures,
|
||||
},
|
||||
alert: {
|
||||
all: alertingFeatures,
|
||||
},
|
||||
},
|
||||
ui: ['read', 'write'],
|
||||
},
|
||||
read: {
|
||||
app: [observabilityFeatureId],
|
||||
catalogue: [observabilityFeatureId],
|
||||
api: ['rac'],
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
alerting: {
|
||||
rule: {
|
||||
read: alertingFeatures,
|
||||
},
|
||||
alert: {
|
||||
read: alertingFeatures,
|
||||
},
|
||||
},
|
||||
ui: ['read'],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const { ruleDataService } = plugins.ruleRegistry;
|
||||
|
||||
core.savedObjects.registerType(threshold);
|
||||
|
|
|
@ -17,6 +17,7 @@ describe('updateGlobalNavigation', () => {
|
|||
describe('when no observability apps are enabled', () => {
|
||||
it('hides the overview link', () => {
|
||||
const capabilities = {
|
||||
logs: { show: false },
|
||||
navLinks: { apm: false, logs: false, metrics: false, uptime: false },
|
||||
} as unknown as ApplicationStart['capabilities'];
|
||||
const deepLinks: AppDeepLink[] = [];
|
||||
|
@ -37,6 +38,7 @@ describe('updateGlobalNavigation', () => {
|
|||
describe('when one observability app is enabled', () => {
|
||||
it('shows the overview link', () => {
|
||||
const capabilities = {
|
||||
logs: { show: true },
|
||||
navLinks: { apm: true, logs: false, metrics: false, uptime: false },
|
||||
} as unknown as ApplicationStart['capabilities'];
|
||||
const deepLinks: AppDeepLink[] = [];
|
||||
|
@ -57,6 +59,7 @@ describe('updateGlobalNavigation', () => {
|
|||
it('shows the cases deep link', () => {
|
||||
const capabilities = {
|
||||
[casesFeatureId]: { read_cases: true },
|
||||
logs: { show: true },
|
||||
navLinks: { apm: true, logs: false, metrics: false, uptime: false },
|
||||
} as unknown as ApplicationStart['capabilities'];
|
||||
|
||||
|
@ -93,6 +96,7 @@ describe('updateGlobalNavigation', () => {
|
|||
it('hides the cases deep link', () => {
|
||||
const capabilities = {
|
||||
[casesFeatureId]: { read_cases: false },
|
||||
logs: { show: true },
|
||||
navLinks: { apm: true, logs: false, metrics: false, uptime: false },
|
||||
} as unknown as ApplicationStart['capabilities'];
|
||||
|
||||
|
@ -124,6 +128,7 @@ describe('updateGlobalNavigation', () => {
|
|||
it('shows the alerts deep link', () => {
|
||||
const capabilities = {
|
||||
[casesFeatureId]: { read_cases: true },
|
||||
logs: { show: true },
|
||||
navLinks: { apm: true, logs: false, metrics: false, uptime: false },
|
||||
} as unknown as ApplicationStart['capabilities'];
|
||||
|
||||
|
@ -156,6 +161,43 @@ describe('updateGlobalNavigation', () => {
|
|||
visibleIn: ['sideNav', 'globalSearch', 'home', 'kibanaOverview'],
|
||||
});
|
||||
});
|
||||
|
||||
it('shows the alerts deep link for logs', () => {
|
||||
const capabilities = {
|
||||
[casesFeatureId]: { read_cases: true },
|
||||
logs: { show: true },
|
||||
navLinks: { apm: false, logs: false, metrics: false, uptime: false },
|
||||
} as unknown as ApplicationStart['capabilities'];
|
||||
|
||||
const deepLinks = [
|
||||
{
|
||||
id: 'alerts',
|
||||
title: 'Alerts',
|
||||
order: 8001,
|
||||
path: '/alerts',
|
||||
visibleIn: [],
|
||||
},
|
||||
];
|
||||
const callback = jest.fn();
|
||||
const updater$ = {
|
||||
next: (cb: AppUpdater) => callback(cb(app)),
|
||||
} as unknown as Subject<AppUpdater>;
|
||||
|
||||
updateGlobalNavigation({ capabilities, deepLinks, updater$ });
|
||||
|
||||
expect(callback).toHaveBeenCalledWith({
|
||||
deepLinks: [
|
||||
{
|
||||
id: 'alerts',
|
||||
title: 'Alerts',
|
||||
order: 8001,
|
||||
path: '/alerts',
|
||||
visibleIn: ['sideNav', 'globalSearch'],
|
||||
},
|
||||
],
|
||||
visibleIn: ['sideNav', 'globalSearch', 'home', 'kibanaOverview'],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -19,12 +19,17 @@ export function updateGlobalNavigation({
|
|||
deepLinks: AppDeepLink[];
|
||||
updater$: Subject<AppUpdater>;
|
||||
}) {
|
||||
const { apm, logs, metrics, uptime, slo } = capabilities.navLinks;
|
||||
const { apm, metrics, uptime, synthetics, slo } = capabilities.navLinks;
|
||||
/* logs is a special case.
|
||||
* It is not a nav link but still exists as a
|
||||
* Kibana feature privilege with attached rule types */
|
||||
const logs = capabilities.logs?.show;
|
||||
const someVisible = Object.values({
|
||||
apm,
|
||||
logs,
|
||||
metrics,
|
||||
uptime,
|
||||
synthetics,
|
||||
slo,
|
||||
}).some((visible) => visible);
|
||||
|
||||
|
|
|
@ -315,7 +315,17 @@ export const createNavigationTree = ({
|
|||
defaultMessage: 'Access',
|
||||
}),
|
||||
breadcrumbStatus: 'hidden',
|
||||
children: [{ link: 'management:api_keys', breadcrumbStatus: 'hidden' }],
|
||||
children: [
|
||||
{ link: 'management:api_keys', breadcrumbStatus: 'hidden' },
|
||||
{ link: 'management:roles', breadcrumbStatus: 'hidden' },
|
||||
{
|
||||
cloudLink: 'userAndRoles',
|
||||
title: i18n.translate(
|
||||
'xpack.serverlessObservability.navLinks.projectSettings.mngt.usersAndRoles',
|
||||
{ defaultMessage: 'Manage organization members' }
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
title: i18n.translate(
|
||||
|
@ -374,10 +384,6 @@ export const createNavigationTree = ({
|
|||
{
|
||||
link: 'fleet',
|
||||
},
|
||||
{
|
||||
id: 'cloudLinkUserAndRoles',
|
||||
cloudLink: 'userAndRoles',
|
||||
},
|
||||
{
|
||||
id: 'cloudLinkBilling',
|
||||
cloudLink: 'billingAndSub',
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
import { KibanaFeatureScope } from '@kbn/features-plugin/common';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { AlertsLocatorDefinition, sloFeatureId } from '@kbn/observability-plugin/common';
|
||||
import { SLO_BURN_RATE_RULE_TYPE_ID } from '@kbn/rule-data-utils';
|
||||
import { SLO_BURN_RATE_RULE_TYPE_ID, DEPRECATED_ALERTING_CONSUMERS } from '@kbn/rule-data-utils';
|
||||
import { mapValues } from 'lodash';
|
||||
import { LockAcquisitionError, LockManagerService } from '@kbn/lock-manager';
|
||||
import { getSloClientWithRequest } from './client';
|
||||
|
@ -81,7 +81,7 @@ export class SLOPlugin
|
|||
|
||||
const alertingFeatures = sloRuleTypes.map((ruleTypeId) => ({
|
||||
ruleTypeId,
|
||||
consumers: [sloFeatureId, ALERTING_FEATURE_ID],
|
||||
consumers: [sloFeatureId, ALERTING_FEATURE_ID, ...DEPRECATED_ALERTING_CONSUMERS],
|
||||
}));
|
||||
|
||||
plugins.features.registerKibanaFeature({
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
SubFeaturePrivilegeGroupType,
|
||||
} from '@kbn/features-plugin/common';
|
||||
import { ALERTING_FEATURE_ID } from '@kbn/alerting-plugin/common';
|
||||
import { DEPRECATED_ALERTING_CONSUMERS } from '@kbn/rule-data-utils';
|
||||
import { UPTIME_RULE_TYPE_IDS, SYNTHETICS_RULE_TYPE_IDS } from '@kbn/rule-data-utils';
|
||||
import { KibanaFeatureScope } from '@kbn/features-plugin/common';
|
||||
import { syntheticsMonitorType, syntheticsParamType } from '../common/types/saved_objects';
|
||||
|
@ -32,7 +33,7 @@ const ruleTypes = [...UPTIME_RULE_TYPE_IDS, ...SYNTHETICS_RULE_TYPE_IDS];
|
|||
|
||||
const alertingFeatures = ruleTypes.map((ruleTypeId) => ({
|
||||
ruleTypeId,
|
||||
consumers: [PLUGIN.ID, ALERTING_FEATURE_ID],
|
||||
consumers: [PLUGIN.ID, ALERTING_FEATURE_ID, ...DEPRECATED_ALERTING_CONSUMERS],
|
||||
}));
|
||||
|
||||
const elasticManagedLocationsEnabledPrivilege: SubFeaturePrivilegeGroupConfig = {
|
||||
|
|
|
@ -45,6 +45,7 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon
|
|||
const samlAuth = getService('samlAuth');
|
||||
const alertingApi = getService('alertingApi');
|
||||
const observabilityAIAssistantAPIClient = getService('observabilityAIAssistantApi');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
|
||||
describe('alerts', function () {
|
||||
// LLM Proxy is not yet support in MKI: https://github.com/elastic/obs-ai-assistant-team/issues/199
|
||||
|
@ -62,6 +63,7 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon
|
|||
const end = 'now';
|
||||
|
||||
before(async () => {
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
internalReqHeader = samlAuth.getInternalRequestHeader();
|
||||
roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('editor');
|
||||
|
||||
|
@ -136,6 +138,7 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon
|
|||
await deleteRules({ getService, roleAuthc, internalReqHeader });
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc);
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
});
|
||||
|
||||
it('should execute the function without any errors', async () => {
|
||||
|
|
|
@ -4,11 +4,18 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { cleanup, Dataset, generate, PartialConfig } from '@kbn/data-forge';
|
||||
import { RoleCredentials, InternalRequestHeader } from '@kbn/ftr-common-functional-services';
|
||||
import expect from '@kbn/expect';
|
||||
import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context';
|
||||
import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
|
||||
|
||||
const RULE_TYPE_ID = 'slo.rules.burnRate';
|
||||
const DATA_VIEW = 'kbn-data-forge-fake_hosts.fake_hosts-*';
|
||||
const RULE_ALERT_INDEX = '.alerts-observability.slo.alerts-default';
|
||||
const RULE_ALERT_INDEX_PATTERN = '.alerts-observability.slo.alerts-*';
|
||||
const ALERT_ACTION_INDEX = 'alert-action-slo';
|
||||
const DATA_VIEW_ID = 'data-view-id';
|
||||
|
||||
export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
|
||||
const esClient = getService('es');
|
||||
|
@ -21,24 +28,33 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
|
|||
const sloApi = getService('sloApi');
|
||||
const config = getService('config');
|
||||
const isServerless = config.get('serverless');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const expectedConsumer = isServerless ? 'observability' : 'slo';
|
||||
|
||||
describe('Burn rate rule', () => {
|
||||
const RULE_TYPE_ID = 'slo.rules.burnRate';
|
||||
const DATA_VIEW = 'kbn-data-forge-fake_hosts.fake_hosts-*';
|
||||
const RULE_ALERT_INDEX = '.alerts-observability.slo.alerts-default';
|
||||
const ALERT_ACTION_INDEX = 'alert-action-slo';
|
||||
const DATA_VIEW_ID = 'data-view-id';
|
||||
let dataForgeConfig: PartialConfig;
|
||||
let dataForgeIndices: string[];
|
||||
let actionId: string;
|
||||
let ruleId: string;
|
||||
let dependencyRuleId: string;
|
||||
let editorRoleAuthc: RoleCredentials;
|
||||
let adminRoleAuthc: RoleCredentials;
|
||||
let currentRoleAuthc: RoleCredentials;
|
||||
let internalHeaders: InternalRequestHeader;
|
||||
let sloId: string;
|
||||
|
||||
before(async () => {
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
await esClient.deleteByQuery({
|
||||
index: RULE_ALERT_INDEX_PATTERN,
|
||||
query: {
|
||||
match_all: {},
|
||||
},
|
||||
conflicts: 'proceed',
|
||||
});
|
||||
editorRoleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('editor');
|
||||
adminRoleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin');
|
||||
currentRoleAuthc = isServerless ? editorRoleAuthc : adminRoleAuthc;
|
||||
internalHeaders = samlAuth.getInternalRequestHeader();
|
||||
dataForgeConfig = {
|
||||
schedule: [
|
||||
|
@ -66,7 +82,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
|
|||
docCountTarget: 360,
|
||||
});
|
||||
await dataViewApi.create({
|
||||
roleAuthc: adminRoleAuthc,
|
||||
roleAuthc: currentRoleAuthc,
|
||||
name: DATA_VIEW,
|
||||
id: DATA_VIEW_ID,
|
||||
title: DATA_VIEW,
|
||||
|
@ -76,53 +92,67 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
|
|||
after(async () => {
|
||||
await supertestWithoutAuth
|
||||
.delete(`/api/alerting/rule/${ruleId}`)
|
||||
.set(adminRoleAuthc.apiKeyHeader)
|
||||
.set(currentRoleAuthc.apiKeyHeader)
|
||||
.set(internalHeaders);
|
||||
await supertestWithoutAuth
|
||||
.delete(`/api/actions/connector/${actionId}`)
|
||||
.set(adminRoleAuthc.apiKeyHeader)
|
||||
.set(currentRoleAuthc.apiKeyHeader)
|
||||
.set(internalHeaders);
|
||||
await esClient.deleteByQuery({
|
||||
index: RULE_ALERT_INDEX,
|
||||
query: {
|
||||
bool: {
|
||||
should: [
|
||||
{ term: { 'kibana.alert.rule.uuid': ruleId } },
|
||||
{ term: { 'kibana.alert.rule.uuid': dependencyRuleId } },
|
||||
],
|
||||
if (ruleId) {
|
||||
await esClient.deleteByQuery({
|
||||
index: RULE_ALERT_INDEX,
|
||||
query: {
|
||||
bool: {
|
||||
should: [
|
||||
{ term: { 'kibana.alert.rule.uuid': ruleId } },
|
||||
...(dependencyRuleId
|
||||
? [{ term: { 'kibana.alert.rule.uuid': dependencyRuleId } }]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
conflicts: 'proceed',
|
||||
});
|
||||
await esClient.deleteByQuery({
|
||||
index: '.kibana-event-log-*',
|
||||
query: { term: { 'rule.id': ruleId } },
|
||||
conflicts: 'proceed',
|
||||
});
|
||||
conflicts: 'proceed',
|
||||
});
|
||||
|
||||
await esClient.deleteByQuery({
|
||||
index: '.kibana-event-log-*',
|
||||
query: { term: { 'rule.id': ruleId } },
|
||||
conflicts: 'proceed',
|
||||
});
|
||||
}
|
||||
await dataViewApi.delete({
|
||||
roleAuthc: adminRoleAuthc,
|
||||
roleAuthc: currentRoleAuthc,
|
||||
id: DATA_VIEW_ID,
|
||||
});
|
||||
await supertestWithoutAuth
|
||||
.delete('/api/observability/slos/my-custom-id')
|
||||
.set(adminRoleAuthc.apiKeyHeader)
|
||||
.delete(`/api/observability/slos/${sloId}`)
|
||||
.set(currentRoleAuthc.apiKeyHeader)
|
||||
.set(internalHeaders);
|
||||
await esDeleteAllIndices([ALERT_ACTION_INDEX, ...dataForgeIndices]);
|
||||
await cleanup({ client: esClient, config: dataForgeConfig, logger });
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(adminRoleAuthc);
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(currentRoleAuthc);
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
await esClient.deleteByQuery({
|
||||
index: RULE_ALERT_INDEX_PATTERN,
|
||||
query: {
|
||||
match_all: {},
|
||||
},
|
||||
conflicts: 'proceed',
|
||||
});
|
||||
});
|
||||
|
||||
describe('Rule creation', () => {
|
||||
describe('Rule creation', function () {
|
||||
it('creates rule successfully', async () => {
|
||||
sloId = uuidv4();
|
||||
actionId = await alertingApi.createIndexConnector({
|
||||
roleAuthc: adminRoleAuthc,
|
||||
roleAuthc: currentRoleAuthc,
|
||||
name: 'Index Connector: Slo Burn rate API test',
|
||||
indexName: ALERT_ACTION_INDEX,
|
||||
});
|
||||
|
||||
await sloApi.create(
|
||||
{
|
||||
id: 'my-custom-id',
|
||||
id: sloId,
|
||||
name: 'my custom name',
|
||||
description: 'my custom description',
|
||||
indicator: {
|
||||
|
@ -144,11 +174,11 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
|
|||
},
|
||||
groupBy: '*',
|
||||
},
|
||||
adminRoleAuthc
|
||||
currentRoleAuthc
|
||||
);
|
||||
|
||||
const dependencyRule = await alertingApi.createRule({
|
||||
roleAuthc: adminRoleAuthc,
|
||||
roleAuthc: currentRoleAuthc,
|
||||
tags: ['observability'],
|
||||
consumer: expectedConsumer,
|
||||
name: 'SLO Burn Rate rule - Dependency',
|
||||
|
@ -157,7 +187,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
|
|||
interval: '1m',
|
||||
},
|
||||
params: {
|
||||
sloId: 'my-custom-id',
|
||||
sloId,
|
||||
windows: [
|
||||
{
|
||||
id: '1',
|
||||
|
@ -222,7 +252,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
|
|||
|
||||
dependencyRuleId = dependencyRule.id;
|
||||
const createdRule = await alertingApi.createRule({
|
||||
roleAuthc: adminRoleAuthc,
|
||||
roleAuthc: currentRoleAuthc,
|
||||
tags: ['observability'],
|
||||
consumer: expectedConsumer,
|
||||
name: 'SLO Burn Rate rule',
|
||||
|
@ -231,7 +261,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
|
|||
interval: '1m',
|
||||
},
|
||||
params: {
|
||||
sloId: 'my-custom-id',
|
||||
sloId,
|
||||
dependencies: [
|
||||
{
|
||||
ruleId: dependencyRule.id,
|
||||
|
@ -305,7 +335,7 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
|
|||
|
||||
it('should be active', async () => {
|
||||
const executionStatus = await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: adminRoleAuthc,
|
||||
roleAuthc: currentRoleAuthc,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
});
|
||||
|
@ -325,10 +355,88 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
|
|||
});
|
||||
|
||||
it('should find the created rule with correct information about the consumer', async () => {
|
||||
const match = await alertingApi.findInRules(adminRoleAuthc, ruleId);
|
||||
const match = await alertingApi.findInRules(currentRoleAuthc, ruleId);
|
||||
expect(match).not.to.be(undefined);
|
||||
expect(match.consumer).to.be(expectedConsumer);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export const getSloBurnRateRuleConfiguration = ({
|
||||
sloId,
|
||||
consumer,
|
||||
}: {
|
||||
sloId: string;
|
||||
consumer: string;
|
||||
}) => ({
|
||||
tags: ['observability'],
|
||||
consumer,
|
||||
name: 'SLO Burn Rate rule',
|
||||
ruleTypeId: RULE_TYPE_ID,
|
||||
schedule: {
|
||||
interval: '1m',
|
||||
},
|
||||
params: {
|
||||
sloId,
|
||||
windows: [
|
||||
{
|
||||
id: '1',
|
||||
actionGroup: 'slo.burnRate.alert',
|
||||
burnRateThreshold: 3.36,
|
||||
maxBurnRateThreshold: 720,
|
||||
longWindow: {
|
||||
value: 1,
|
||||
unit: 'h',
|
||||
},
|
||||
shortWindow: {
|
||||
value: 5,
|
||||
unit: 'm',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
actionGroup: 'slo.burnRate.high',
|
||||
burnRateThreshold: 1.4,
|
||||
maxBurnRateThreshold: 120,
|
||||
longWindow: {
|
||||
value: 6,
|
||||
unit: 'h',
|
||||
},
|
||||
shortWindow: {
|
||||
value: 30,
|
||||
unit: 'm',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
actionGroup: 'slo.burnRate.medium',
|
||||
burnRateThreshold: 0.7,
|
||||
maxBurnRateThreshold: 30,
|
||||
longWindow: {
|
||||
value: 24,
|
||||
unit: 'h',
|
||||
},
|
||||
shortWindow: {
|
||||
value: 120,
|
||||
unit: 'm',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
actionGroup: 'slo.burnRate.low',
|
||||
burnRateThreshold: 0.234,
|
||||
maxBurnRateThreshold: 10,
|
||||
longWindow: {
|
||||
value: 72,
|
||||
unit: 'h',
|
||||
},
|
||||
shortWindow: {
|
||||
value: 360,
|
||||
unit: 'm',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
actions: [],
|
||||
});
|
|
@ -0,0 +1,691 @@
|
|||
/*
|
||||
* 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 { v4 as uuidv4 } from 'uuid';
|
||||
import { cleanup, Dataset, generate, PartialConfig } from '@kbn/data-forge';
|
||||
import { RoleCredentials, InternalRequestHeader } from '@kbn/ftr-common-functional-services';
|
||||
import expect from '@kbn/expect';
|
||||
import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
|
||||
|
||||
const RULE_TYPE_ID = 'slo.rules.burnRate';
|
||||
const DATA_VIEW = 'kbn-data-forge-fake_hosts.fake_hosts-*';
|
||||
const RULE_ALERT_INDEX = '.alerts-observability.slo.alerts-default';
|
||||
const RULE_ALERT_INDEX_PATTERN = '.alerts-observability.slo.alerts-*';
|
||||
const ALERT_ACTION_INDEX = 'alert-action-slo';
|
||||
const DATA_VIEW_ID = 'data-view-id';
|
||||
|
||||
export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
|
||||
const esClient = getService('es');
|
||||
const esDeleteAllIndices = getService('esDeleteAllIndices');
|
||||
const samlAuth = getService('samlAuth');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const logger = getService('log');
|
||||
const alertingApi = getService('alertingApi');
|
||||
const dataViewApi = getService('dataViewApi');
|
||||
const sloApi = getService('sloApi');
|
||||
const config = getService('config');
|
||||
const isServerless = config.get('serverless');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
|
||||
describe('Burn rate rule', () => {
|
||||
let dataForgeConfig: PartialConfig;
|
||||
let dataForgeIndices: string[];
|
||||
let actionId: string;
|
||||
let ruleId: string;
|
||||
let dependencyRuleId: string;
|
||||
let editorRoleAuthc: RoleCredentials;
|
||||
let adminRoleAuthc: RoleCredentials;
|
||||
let currentRoleAuthc: RoleCredentials;
|
||||
let internalHeaders: InternalRequestHeader;
|
||||
let sloId: string;
|
||||
|
||||
before(async () => {
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
await esClient.deleteByQuery({
|
||||
index: RULE_ALERT_INDEX_PATTERN,
|
||||
query: {
|
||||
match_all: {},
|
||||
},
|
||||
conflicts: 'proceed',
|
||||
});
|
||||
editorRoleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('editor');
|
||||
adminRoleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('admin');
|
||||
currentRoleAuthc = isServerless ? editorRoleAuthc : adminRoleAuthc;
|
||||
internalHeaders = samlAuth.getInternalRequestHeader();
|
||||
dataForgeConfig = {
|
||||
schedule: [
|
||||
{
|
||||
template: 'good',
|
||||
start: 'now-15m',
|
||||
end: 'now+5m',
|
||||
metrics: [
|
||||
{ name: 'system.cpu.user.pct', method: 'linear', start: 2.5, end: 2.5 },
|
||||
{ name: 'system.cpu.total.pct', method: 'linear', start: 0.5, end: 0.5 },
|
||||
{ name: 'system.cpu.total.norm.pct', method: 'linear', start: 0.8, end: 0.8 },
|
||||
],
|
||||
},
|
||||
],
|
||||
indexing: {
|
||||
dataset: 'fake_hosts' as Dataset,
|
||||
eventsPerCycle: 1,
|
||||
interval: 10000,
|
||||
alignEventsToInterval: true,
|
||||
},
|
||||
};
|
||||
dataForgeIndices = await generate({ client: esClient, config: dataForgeConfig, logger });
|
||||
await alertingApi.waitForDocumentInIndex({
|
||||
indexName: DATA_VIEW,
|
||||
docCountTarget: 360,
|
||||
});
|
||||
await dataViewApi.create({
|
||||
roleAuthc: currentRoleAuthc,
|
||||
name: DATA_VIEW,
|
||||
id: DATA_VIEW_ID,
|
||||
title: DATA_VIEW,
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await supertestWithoutAuth
|
||||
.delete(`/api/alerting/rule/${ruleId}`)
|
||||
.set(currentRoleAuthc.apiKeyHeader)
|
||||
.set(internalHeaders);
|
||||
await supertestWithoutAuth
|
||||
.delete(`/api/actions/connector/${actionId}`)
|
||||
.set(currentRoleAuthc.apiKeyHeader)
|
||||
.set(internalHeaders);
|
||||
if (ruleId) {
|
||||
await esClient.deleteByQuery({
|
||||
index: RULE_ALERT_INDEX,
|
||||
query: {
|
||||
bool: {
|
||||
should: [
|
||||
{ term: { 'kibana.alert.rule.uuid': ruleId } },
|
||||
...(dependencyRuleId
|
||||
? [{ term: { 'kibana.alert.rule.uuid': dependencyRuleId } }]
|
||||
: []),
|
||||
],
|
||||
},
|
||||
},
|
||||
conflicts: 'proceed',
|
||||
});
|
||||
|
||||
await esClient.deleteByQuery({
|
||||
index: '.kibana-event-log-*',
|
||||
query: { term: { 'rule.id': ruleId } },
|
||||
conflicts: 'proceed',
|
||||
});
|
||||
}
|
||||
await dataViewApi.delete({
|
||||
roleAuthc: currentRoleAuthc,
|
||||
id: DATA_VIEW_ID,
|
||||
});
|
||||
await supertestWithoutAuth
|
||||
.delete(`/api/observability/slos/${sloId}`)
|
||||
.set(currentRoleAuthc.apiKeyHeader)
|
||||
.set(internalHeaders);
|
||||
await esDeleteAllIndices([ALERT_ACTION_INDEX, ...dataForgeIndices]);
|
||||
await cleanup({ client: esClient, config: dataForgeConfig, logger });
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(currentRoleAuthc);
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
await esClient.deleteByQuery({
|
||||
index: RULE_ALERT_INDEX_PATTERN,
|
||||
query: {
|
||||
match_all: {},
|
||||
},
|
||||
conflicts: 'proceed',
|
||||
});
|
||||
});
|
||||
|
||||
describe('Burn rate rule - slo consumer', function () {
|
||||
this.tags(['skipMKI']);
|
||||
const consumer = 'slo';
|
||||
it('creates rule successfully', async () => {
|
||||
sloId = uuidv4();
|
||||
actionId = await alertingApi.createIndexConnector({
|
||||
roleAuthc: currentRoleAuthc,
|
||||
name: 'Index Connector: Slo Burn rate API test',
|
||||
indexName: ALERT_ACTION_INDEX,
|
||||
});
|
||||
|
||||
await sloApi.create(
|
||||
{
|
||||
id: sloId,
|
||||
name: 'my custom name',
|
||||
description: 'my custom description',
|
||||
indicator: {
|
||||
type: 'sli.kql.custom',
|
||||
params: {
|
||||
index: DATA_VIEW,
|
||||
good: 'system.cpu.total.norm.pct > 1',
|
||||
total: 'system.cpu.total.norm.pct: *',
|
||||
timestampField: '@timestamp',
|
||||
},
|
||||
},
|
||||
timeWindow: {
|
||||
duration: '7d',
|
||||
type: 'rolling',
|
||||
},
|
||||
budgetingMethod: 'occurrences',
|
||||
objective: {
|
||||
target: 0.999,
|
||||
},
|
||||
groupBy: '*',
|
||||
},
|
||||
currentRoleAuthc
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
const createdRule = await alertingApi.createRule({
|
||||
roleAuthc: currentRoleAuthc,
|
||||
...getSloBurnRateRuleConfiguration({
|
||||
sloId,
|
||||
consumer,
|
||||
}),
|
||||
});
|
||||
ruleId = createdRule.id;
|
||||
expect(ruleId).not.to.be(undefined);
|
||||
});
|
||||
|
||||
it('should find the created rule with correct information about the consumer', async () => {
|
||||
const match = await alertingApi.findInRules(currentRoleAuthc, ruleId);
|
||||
expect(match).not.to.be(undefined);
|
||||
expect(match.consumer).to.be(consumer);
|
||||
});
|
||||
|
||||
it('should be active and visible from editor role', async () => {
|
||||
const executionStatus = await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: currentRoleAuthc,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
});
|
||||
expect(executionStatus).to.be('active');
|
||||
});
|
||||
|
||||
it('should be active and visible from slo only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.slo_only);
|
||||
|
||||
const sloOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
const executionStatus = await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: sloOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
});
|
||||
expect(executionStatus).to.be('active');
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(sloOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
|
||||
it('should NOT be visible from synthetics only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.synthetics_only);
|
||||
const syntheticsOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
try {
|
||||
await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: syntheticsOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
timeout: 1000 * 3,
|
||||
});
|
||||
throw new Error('Expected rule to not be visible, but it was visible');
|
||||
} catch (error) {
|
||||
expect(error.message).to.contain('timeout');
|
||||
}
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(syntheticsOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Burn rate rule - consumer alerts', function () {
|
||||
this.tags(['skipMKI']);
|
||||
const consumer = 'alerts';
|
||||
it('creates rule successfully', async () => {
|
||||
sloId = uuidv4();
|
||||
actionId = await alertingApi.createIndexConnector({
|
||||
roleAuthc: currentRoleAuthc,
|
||||
name: 'Index Connector: Slo Burn rate API test',
|
||||
indexName: ALERT_ACTION_INDEX,
|
||||
});
|
||||
|
||||
await sloApi.create(
|
||||
{
|
||||
id: sloId,
|
||||
name: 'my custom name',
|
||||
description: 'my custom description',
|
||||
indicator: {
|
||||
type: 'sli.kql.custom',
|
||||
params: {
|
||||
index: DATA_VIEW,
|
||||
good: 'system.cpu.total.norm.pct > 1',
|
||||
total: 'system.cpu.total.norm.pct: *',
|
||||
timestampField: '@timestamp',
|
||||
},
|
||||
},
|
||||
timeWindow: {
|
||||
duration: '7d',
|
||||
type: 'rolling',
|
||||
},
|
||||
budgetingMethod: 'occurrences',
|
||||
objective: {
|
||||
target: 0.999,
|
||||
},
|
||||
groupBy: '*',
|
||||
},
|
||||
currentRoleAuthc
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
const createdRule = await alertingApi.createRule({
|
||||
roleAuthc: currentRoleAuthc,
|
||||
...getSloBurnRateRuleConfiguration({
|
||||
sloId,
|
||||
consumer,
|
||||
}),
|
||||
});
|
||||
ruleId = createdRule.id;
|
||||
expect(ruleId).not.to.be(undefined);
|
||||
});
|
||||
|
||||
it('should find the created rule with correct information about the consumer', async () => {
|
||||
const match = await alertingApi.findInRules(currentRoleAuthc, ruleId);
|
||||
expect(match).not.to.be(undefined);
|
||||
expect(match.consumer).to.be(consumer);
|
||||
});
|
||||
|
||||
it('should be active and visible from editor role', async () => {
|
||||
const executionStatus = await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: currentRoleAuthc,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
});
|
||||
expect(executionStatus).to.be('active');
|
||||
});
|
||||
|
||||
it('should be active and visible from slo only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.slo_only);
|
||||
|
||||
const sloOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
const executionStatus = await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: sloOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
});
|
||||
expect(executionStatus).to.be('active');
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(sloOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
|
||||
it('should NOT be visible from synthetics only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.synthetics_only);
|
||||
const syntheticsOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
try {
|
||||
await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: syntheticsOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
timeout: 1000 * 3,
|
||||
});
|
||||
throw new Error('Expected rule to not be visible, but it was visible');
|
||||
} catch (error) {
|
||||
expect(error.message).to.contain('timeout');
|
||||
}
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(syntheticsOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Burn rate rule - consumer observability', function () {
|
||||
this.tags(['skipMKI']);
|
||||
const consumer = 'observability';
|
||||
|
||||
it('creates rule successfully', async () => {
|
||||
sloId = uuidv4();
|
||||
actionId = await alertingApi.createIndexConnector({
|
||||
roleAuthc: currentRoleAuthc,
|
||||
name: 'Index Connector: Slo Burn rate API test',
|
||||
indexName: ALERT_ACTION_INDEX,
|
||||
});
|
||||
|
||||
await sloApi.create(
|
||||
{
|
||||
id: sloId,
|
||||
name: 'my custom name',
|
||||
description: 'my custom description',
|
||||
indicator: {
|
||||
type: 'sli.kql.custom',
|
||||
params: {
|
||||
index: DATA_VIEW,
|
||||
good: 'system.cpu.total.norm.pct > 1',
|
||||
total: 'system.cpu.total.norm.pct: *',
|
||||
timestampField: '@timestamp',
|
||||
},
|
||||
},
|
||||
timeWindow: {
|
||||
duration: '7d',
|
||||
type: 'rolling',
|
||||
},
|
||||
budgetingMethod: 'occurrences',
|
||||
objective: {
|
||||
target: 0.999,
|
||||
},
|
||||
groupBy: '*',
|
||||
},
|
||||
currentRoleAuthc
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
const createdRule = await alertingApi.createRule({
|
||||
roleAuthc: currentRoleAuthc,
|
||||
...getSloBurnRateRuleConfiguration({
|
||||
sloId,
|
||||
consumer,
|
||||
}),
|
||||
});
|
||||
ruleId = createdRule.id;
|
||||
expect(ruleId).not.to.be(undefined);
|
||||
});
|
||||
|
||||
it('should find the created rule with correct information about the consumer', async () => {
|
||||
const match = await alertingApi.findInRules(currentRoleAuthc, ruleId);
|
||||
expect(match).not.to.be(undefined);
|
||||
expect(match.consumer).to.be(consumer);
|
||||
});
|
||||
|
||||
it('should be active and visible from editor role', async () => {
|
||||
const executionStatus = await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: currentRoleAuthc,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
});
|
||||
expect(executionStatus).to.be('active');
|
||||
});
|
||||
|
||||
it('should be active and visible from slo only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.slo_only);
|
||||
|
||||
const sloOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
const executionStatus = await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: sloOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
});
|
||||
expect(executionStatus).to.be('active');
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(sloOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
|
||||
it('should NOT be visible from synthetics only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.synthetics_only);
|
||||
const syntheticsOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
try {
|
||||
await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: syntheticsOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
timeout: 1000 * 3,
|
||||
});
|
||||
throw new Error('Expected rule to not be visible, but it was visible');
|
||||
} catch (error) {
|
||||
expect(error.message).to.contain('timeout');
|
||||
}
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(syntheticsOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Burn rate rule - can create - slo only role', function () {
|
||||
this.tags(['skipMKI']);
|
||||
const consumer = 'slo';
|
||||
let sloOnlyRole: RoleCredentials;
|
||||
|
||||
it('creates rule successfully', async () => {
|
||||
sloId = uuidv4();
|
||||
await samlAuth.setCustomRole(ROLES.slo_only);
|
||||
sloOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
|
||||
await sloApi.create(
|
||||
{
|
||||
id: sloId,
|
||||
name: 'my custom name',
|
||||
description: 'my custom description',
|
||||
indicator: {
|
||||
type: 'sli.kql.custom',
|
||||
params: {
|
||||
index: DATA_VIEW,
|
||||
good: 'system.cpu.total.norm.pct > 1',
|
||||
total: 'system.cpu.total.norm.pct: *',
|
||||
timestampField: '@timestamp',
|
||||
},
|
||||
},
|
||||
timeWindow: {
|
||||
duration: '7d',
|
||||
type: 'rolling',
|
||||
},
|
||||
budgetingMethod: 'occurrences',
|
||||
objective: {
|
||||
target: 0.999,
|
||||
},
|
||||
groupBy: '*',
|
||||
},
|
||||
currentRoleAuthc
|
||||
);
|
||||
|
||||
// @ts-ignore
|
||||
const createdRule = await alertingApi.createRule({
|
||||
roleAuthc: currentRoleAuthc,
|
||||
...getSloBurnRateRuleConfiguration({
|
||||
sloId,
|
||||
consumer,
|
||||
}),
|
||||
});
|
||||
ruleId = createdRule.id;
|
||||
expect(ruleId).not.to.be(undefined);
|
||||
});
|
||||
|
||||
it('should find the created rule with correct information about the consumer', async () => {
|
||||
const match = await alertingApi.findInRules(currentRoleAuthc, ruleId);
|
||||
expect(match).not.to.be(undefined);
|
||||
expect(match.consumer).to.be(consumer);
|
||||
});
|
||||
|
||||
it('should be active and visible from slo role', async () => {
|
||||
const executionStatus = await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: sloOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
});
|
||||
expect(executionStatus).to.be('active');
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(sloOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Burn rate rule - can NOT create - synthetics only role', function () {
|
||||
this.tags(['skipMKI']);
|
||||
const consumer = 'slo';
|
||||
let syntheticsOnlyRole: RoleCredentials;
|
||||
|
||||
it('creates rule successfully', async () => {
|
||||
sloId = uuidv4();
|
||||
await samlAuth.setCustomRole(ROLES.synthetics_only);
|
||||
syntheticsOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
|
||||
// create SLO first with editor role
|
||||
await sloApi.create(
|
||||
{
|
||||
id: sloId,
|
||||
name: 'my custom name',
|
||||
description: 'my custom description',
|
||||
indicator: {
|
||||
type: 'sli.kql.custom',
|
||||
params: {
|
||||
index: DATA_VIEW,
|
||||
good: 'system.cpu.total.norm.pct > 1',
|
||||
total: 'system.cpu.total.norm.pct: *',
|
||||
timestampField: '@timestamp',
|
||||
},
|
||||
},
|
||||
timeWindow: {
|
||||
duration: '7d',
|
||||
type: 'rolling',
|
||||
},
|
||||
budgetingMethod: 'occurrences',
|
||||
objective: {
|
||||
target: 0.999,
|
||||
},
|
||||
groupBy: '*',
|
||||
},
|
||||
currentRoleAuthc
|
||||
);
|
||||
|
||||
// verify that synthetics only role cannot create the rule
|
||||
// @ts-ignore
|
||||
const respponse = await alertingApi.createRule({
|
||||
roleAuthc: syntheticsOnlyRole,
|
||||
...getSloBurnRateRuleConfiguration({
|
||||
sloId,
|
||||
consumer,
|
||||
}),
|
||||
});
|
||||
expect(respponse.statusCode).to.be(403);
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(syntheticsOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export const ROLES = {
|
||||
slo_only: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['read'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: [],
|
||||
spaces: ['*'],
|
||||
feature: {
|
||||
actions: ['all'],
|
||||
maintenanceWindow: ['all'],
|
||||
observabilityCasesV3: ['all'],
|
||||
slo: ['all'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
synthetics_only: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['read'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: [],
|
||||
spaces: ['*'],
|
||||
feature: {
|
||||
actions: ['all'],
|
||||
maintenanceWindow: ['all'],
|
||||
observabilityCasesV3: ['all'],
|
||||
uptime: ['all'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const getSloBurnRateRuleConfiguration = ({
|
||||
sloId,
|
||||
consumer,
|
||||
}: {
|
||||
sloId: string;
|
||||
consumer: string;
|
||||
}) => ({
|
||||
tags: ['observability'],
|
||||
consumer,
|
||||
name: 'SLO Burn Rate rule',
|
||||
ruleTypeId: RULE_TYPE_ID,
|
||||
schedule: {
|
||||
interval: '1m',
|
||||
},
|
||||
params: {
|
||||
sloId,
|
||||
windows: [
|
||||
{
|
||||
id: '1',
|
||||
actionGroup: 'slo.burnRate.alert',
|
||||
burnRateThreshold: 3.36,
|
||||
maxBurnRateThreshold: 720,
|
||||
longWindow: {
|
||||
value: 1,
|
||||
unit: 'h',
|
||||
},
|
||||
shortWindow: {
|
||||
value: 5,
|
||||
unit: 'm',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
actionGroup: 'slo.burnRate.high',
|
||||
burnRateThreshold: 1.4,
|
||||
maxBurnRateThreshold: 120,
|
||||
longWindow: {
|
||||
value: 6,
|
||||
unit: 'h',
|
||||
},
|
||||
shortWindow: {
|
||||
value: 30,
|
||||
unit: 'm',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
actionGroup: 'slo.burnRate.medium',
|
||||
burnRateThreshold: 0.7,
|
||||
maxBurnRateThreshold: 30,
|
||||
longWindow: {
|
||||
value: 24,
|
||||
unit: 'h',
|
||||
},
|
||||
shortWindow: {
|
||||
value: 120,
|
||||
unit: 'm',
|
||||
},
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
actionGroup: 'slo.burnRate.low',
|
||||
burnRateThreshold: 0.234,
|
||||
maxBurnRateThreshold: 10,
|
||||
longWindow: {
|
||||
value: 72,
|
||||
unit: 'h',
|
||||
},
|
||||
shortWindow: {
|
||||
value: 360,
|
||||
unit: 'm',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
actions: [],
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
|
||||
|
||||
export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) {
|
||||
describe('SLO Burn rate rule', () => {
|
||||
loadTestFile(require.resolve('./burn_rate_rule'));
|
||||
// movd to feature flag config until custom roles are supported in serverless
|
||||
// loadTestFile(require.resolve('./consumers_and_privileges'));
|
||||
});
|
||||
}
|
|
@ -0,0 +1,742 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { cleanup, generate, Dataset, PartialConfig } from '@kbn/data-forge';
|
||||
import { Aggregators } from '@kbn/observability-plugin/common/custom_threshold_rule/types';
|
||||
import { OBSERVABILITY_THRESHOLD_RULE_TYPE_ID } from '@kbn/rule-data-utils';
|
||||
import { COMPARATORS } from '@kbn/alerting-comparators';
|
||||
import type { InternalRequestHeader, RoleCredentials } from '@kbn/ftr-common-functional-services';
|
||||
import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
|
||||
const esClient = getService('es');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const esDeleteAllIndices = getService('esDeleteAllIndices');
|
||||
const logger = getService('log');
|
||||
const alertingApi = getService('alertingApi');
|
||||
const dataViewApi = getService('dataViewApi');
|
||||
const samlAuth = getService('samlAuth');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
let roleAuthc: RoleCredentials;
|
||||
let internalReqHeader: InternalRequestHeader;
|
||||
const config = getService('config');
|
||||
const isServerless = config.get('serverless');
|
||||
|
||||
describe('Custom Threshold Rule - consumers and priviledges', function () {
|
||||
// skip until custom roles are supported in serverless
|
||||
this.tags(['skipMKI']);
|
||||
const CUSTOM_THRESHOLD_RULE_ALERT_INDEX_PATTERN = '.alerts-observability.threshold.alerts-*';
|
||||
const ALERT_ACTION_INDEX = 'alert-action-threshold';
|
||||
const DATA_VIEW = 'kbn-data-forge-fake_hosts.fake_hosts-*';
|
||||
const DATA_VIEW_ID = 'data-view-id';
|
||||
const DATA_VIEW_NAME = 'data-view-name';
|
||||
let dataForgeConfig: PartialConfig;
|
||||
let dataForgeIndices: string[];
|
||||
let ruleId: string;
|
||||
|
||||
before(async () => {
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
await esClient.deleteByQuery({
|
||||
index: CUSTOM_THRESHOLD_RULE_ALERT_INDEX_PATTERN,
|
||||
query: { match_all: {} },
|
||||
conflicts: 'proceed',
|
||||
});
|
||||
roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('editor');
|
||||
internalReqHeader = samlAuth.getInternalRequestHeader();
|
||||
dataForgeConfig = {
|
||||
schedule: [
|
||||
{
|
||||
template: 'good',
|
||||
start: 'now-10m',
|
||||
end: 'now+5m',
|
||||
metrics: [
|
||||
{ name: 'system.cpu.user.pct', method: 'linear', start: 2.5, end: 2.5 },
|
||||
{ name: 'system.cpu.total.pct', method: 'linear', start: 0.5, end: 0.5 },
|
||||
{ name: 'system.cpu.total.norm.pct', method: 'linear', start: 0.8, end: 0.8 },
|
||||
],
|
||||
},
|
||||
],
|
||||
indexing: {
|
||||
dataset: 'fake_hosts' as Dataset,
|
||||
eventsPerCycle: 1,
|
||||
interval: 60000,
|
||||
alignEventsToInterval: true,
|
||||
},
|
||||
};
|
||||
dataForgeIndices = await generate({ client: esClient, config: dataForgeConfig, logger });
|
||||
await alertingApi.waitForDocumentInIndex({
|
||||
indexName: dataForgeIndices.join(','),
|
||||
docCountTarget: 45,
|
||||
});
|
||||
await dataViewApi.create({
|
||||
name: DATA_VIEW_NAME,
|
||||
id: DATA_VIEW_ID,
|
||||
title: DATA_VIEW,
|
||||
roleAuthc,
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await supertestWithoutAuth
|
||||
.delete(`/api/alerting/rule/${ruleId}`)
|
||||
.set(roleAuthc.apiKeyHeader)
|
||||
.set(internalReqHeader);
|
||||
if (ruleId) {
|
||||
await esClient.deleteByQuery({
|
||||
index: '.kibana-event-log-*',
|
||||
query: { term: { 'rule.id': ruleId } },
|
||||
conflicts: 'proceed',
|
||||
});
|
||||
}
|
||||
await dataViewApi.delete({
|
||||
id: DATA_VIEW_ID,
|
||||
roleAuthc,
|
||||
});
|
||||
await esDeleteAllIndices([ALERT_ACTION_INDEX, ...dataForgeIndices]);
|
||||
await cleanup({ client: esClient, config: dataForgeConfig, logger });
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(roleAuthc);
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
await esClient.deleteByQuery({
|
||||
index: CUSTOM_THRESHOLD_RULE_ALERT_INDEX_PATTERN,
|
||||
query: { match_all: {} },
|
||||
conflicts: 'proceed',
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom threshold - Rule visibility - consumer observability', () => {
|
||||
const consumer = 'observability';
|
||||
it('creates rule successfully', async () => {
|
||||
const createdRule = await alertingApi.createRule({
|
||||
roleAuthc,
|
||||
...getRuleConfiguration({ dataViewId: DATA_VIEW_ID, consumer }),
|
||||
});
|
||||
ruleId = createdRule.id;
|
||||
expect(ruleId).not.to.be(undefined);
|
||||
});
|
||||
|
||||
it('should find the created rule with correct information about the consumer', async () => {
|
||||
const match = await alertingApi.findInRules(roleAuthc, ruleId);
|
||||
expect(match).not.to.be(undefined);
|
||||
expect(match.consumer).to.be(consumer);
|
||||
});
|
||||
|
||||
it('should be active and visible from editor role', async () => {
|
||||
const executionStatus = await alertingApi.waitForRuleStatus({
|
||||
roleAuthc,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
});
|
||||
expect(executionStatus).to.be('active');
|
||||
});
|
||||
|
||||
it('should be active and visible from infra only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.infra_only);
|
||||
|
||||
const infraOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
const executionStatus = await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: infraOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
});
|
||||
expect(executionStatus).to.be('active');
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(infraOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
|
||||
it('should be active and visible from logs only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.logs_only);
|
||||
|
||||
const logsOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
const executionStatus = await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: logsOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
});
|
||||
expect(executionStatus).to.be('active');
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(logsOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
|
||||
it('should NOT visible from synthetics only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.synthetics_only);
|
||||
const syntheticsOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
try {
|
||||
await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: syntheticsOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
timeout: 1000 * 3,
|
||||
});
|
||||
throw new Error('Expected rule to not be visible, but it was visible');
|
||||
} catch (error) {
|
||||
expect(error.message).to.contain('timeout');
|
||||
}
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(syntheticsOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom threshold - Rule visibility - consumer alerts', () => {
|
||||
const consumer = 'alerts';
|
||||
it('creates rule successfully', async () => {
|
||||
const createdRule = await alertingApi.createRule({
|
||||
roleAuthc,
|
||||
...getRuleConfiguration({ dataViewId: DATA_VIEW_ID, consumer }),
|
||||
});
|
||||
ruleId = createdRule.id;
|
||||
expect(ruleId).not.to.be(undefined);
|
||||
});
|
||||
|
||||
it('should find the created rule with correct information about the consumer', async () => {
|
||||
const match = await alertingApi.findInRules(roleAuthc, ruleId);
|
||||
expect(match).not.to.be(undefined);
|
||||
expect(match.consumer).to.be(consumer);
|
||||
});
|
||||
|
||||
it('should be active and visible from editor role', async () => {
|
||||
const executionStatus = await alertingApi.waitForRuleStatus({
|
||||
roleAuthc,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
});
|
||||
expect(executionStatus).to.be('active');
|
||||
});
|
||||
|
||||
it('should be active and visible from infra only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.infra_only);
|
||||
|
||||
const infraOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
const executionStatus = await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: infraOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
});
|
||||
expect(executionStatus).to.be('active');
|
||||
});
|
||||
|
||||
it('should be active and visible from logs only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.logs_only);
|
||||
|
||||
const logsOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
const executionStatus = await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: logsOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
});
|
||||
expect(executionStatus).to.be('active');
|
||||
});
|
||||
|
||||
it('should NOT visible from synthetics only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.synthetics_only);
|
||||
const syntheticsOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
try {
|
||||
await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: syntheticsOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
timeout: 1000 * 3,
|
||||
});
|
||||
throw new Error('Expected rule to not be visible, but it was visible');
|
||||
} catch (error) {
|
||||
expect(error.message).to.contain('timeout');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom threshold - Rule visibility - consumer logs', () => {
|
||||
const consumer = 'logs';
|
||||
it('creates rule successfully', async () => {
|
||||
const createdRule = await alertingApi.createRule({
|
||||
roleAuthc,
|
||||
...getRuleConfiguration({ dataViewId: DATA_VIEW_ID, consumer }),
|
||||
});
|
||||
ruleId = createdRule.id;
|
||||
expect(ruleId).not.to.be(undefined);
|
||||
});
|
||||
|
||||
it('should find the created rule with correct information about the consumer', async () => {
|
||||
const match = await alertingApi.findInRules(roleAuthc, ruleId);
|
||||
expect(match).not.to.be(undefined);
|
||||
expect(match.consumer).to.be(consumer);
|
||||
});
|
||||
|
||||
it('should be active and visible from editor role', async () => {
|
||||
const executionStatus = await alertingApi.waitForRuleStatus({
|
||||
roleAuthc,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
});
|
||||
expect(executionStatus).to.be('active');
|
||||
});
|
||||
|
||||
it('should be active and visible from logs only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.logs_only);
|
||||
|
||||
const logsOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
const executionStatus = await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: logsOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
});
|
||||
expect(executionStatus).to.be('active');
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(logsOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
|
||||
it('should NOT be visible from infra only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.infra_only);
|
||||
const infraOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
try {
|
||||
await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: infraOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
timeout: 1000 * 3,
|
||||
});
|
||||
throw new Error('Expected rule to not be visible, but it was visible');
|
||||
} catch (error) {
|
||||
expect(error.message).to.contain('timeout');
|
||||
}
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(infraOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
|
||||
it('should NOT visible from synthetics only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.synthetics_only);
|
||||
const syntheticsOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
try {
|
||||
await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: syntheticsOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
timeout: 1000 * 3,
|
||||
});
|
||||
throw new Error('Expected rule to not be visible, but it was visible');
|
||||
} catch (error) {
|
||||
expect(error.message).to.contain('timeout');
|
||||
}
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(syntheticsOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom threshold - Rule visibility - consumer infrastructure', () => {
|
||||
const consumer = 'infrastructure';
|
||||
it('creates rule successfully', async () => {
|
||||
const createdRule = await alertingApi.createRule({
|
||||
roleAuthc,
|
||||
...getRuleConfiguration({ dataViewId: DATA_VIEW_ID, consumer }),
|
||||
});
|
||||
ruleId = createdRule.id;
|
||||
expect(ruleId).not.to.be(undefined);
|
||||
});
|
||||
|
||||
it('should find the created rule with correct information about the consumer', async () => {
|
||||
const match = await alertingApi.findInRules(roleAuthc, ruleId);
|
||||
expect(match).not.to.be(undefined);
|
||||
expect(match.consumer).to.be(consumer);
|
||||
});
|
||||
|
||||
it('should be active and visible from editor role', async () => {
|
||||
const executionStatus = await alertingApi.waitForRuleStatus({
|
||||
roleAuthc,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
});
|
||||
expect(executionStatus).to.be('active');
|
||||
});
|
||||
|
||||
it('should be active and visible from infra only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.infra_only);
|
||||
|
||||
const infraOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
const executionStatus = await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: infraOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
});
|
||||
expect(executionStatus).to.be('active');
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(infraOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
|
||||
it('should NOT be visible from logs only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.logs_only);
|
||||
const logsOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
try {
|
||||
await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: logsOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
timeout: 1000 * 3,
|
||||
});
|
||||
throw new Error('Expected rule to not be visible, but it was visible');
|
||||
} catch (error) {
|
||||
expect(error.message).to.contain('timeout');
|
||||
}
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(logsOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
|
||||
it('should NOT visible from synthetics only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.synthetics_only);
|
||||
const syntheticsOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
try {
|
||||
await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: syntheticsOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
timeout: 1000 * 3,
|
||||
});
|
||||
throw new Error('Expected rule to not be visible, but it was visible');
|
||||
} catch (error) {
|
||||
expect(error.message).to.contain('timeout');
|
||||
}
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(syntheticsOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom threshold - Rule execution - consumer stackAlerts', () => {
|
||||
const consumer = 'stackAlerts';
|
||||
|
||||
if (isServerless) {
|
||||
it('is forbidden', async () => {
|
||||
const createdRule = await alertingApi.createRule({
|
||||
roleAuthc,
|
||||
...getRuleConfiguration({ dataViewId: DATA_VIEW_ID, consumer }),
|
||||
});
|
||||
expect(createdRule.statusCode).to.be(403);
|
||||
});
|
||||
} else {
|
||||
it('creates rule successfully', async () => {
|
||||
const createdRule = await alertingApi.createRule({
|
||||
roleAuthc,
|
||||
...getRuleConfiguration({ dataViewId: DATA_VIEW_ID, consumer }),
|
||||
});
|
||||
ruleId = createdRule.id;
|
||||
expect(ruleId).not.to.be(undefined);
|
||||
});
|
||||
|
||||
it('should find the created rule with correct information about the consumer', async () => {
|
||||
const match = await alertingApi.findInRules(roleAuthc, ruleId);
|
||||
expect(match).not.to.be(undefined);
|
||||
expect(match.consumer).to.be(consumer);
|
||||
});
|
||||
|
||||
it('should be active and visible from editor role', async () => {
|
||||
const executionStatus = await alertingApi.waitForRuleStatus({
|
||||
roleAuthc,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
});
|
||||
expect(executionStatus).to.be('active');
|
||||
});
|
||||
|
||||
it('should NOT be visible from logs role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.logs_only);
|
||||
const logsOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
try {
|
||||
await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: logsOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
timeout: 1000 * 3,
|
||||
});
|
||||
throw new Error('Expected rule to not be visible, but it was visible');
|
||||
} catch (error) {
|
||||
expect(error.message).to.contain('timeout');
|
||||
}
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(logsOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
|
||||
it('should NOT be visible from infra role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.infra_only);
|
||||
const infraOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
try {
|
||||
await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: infraOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
timeout: 1000 * 3,
|
||||
});
|
||||
throw new Error('Expected rule to not be visible, but it was visible');
|
||||
} catch (error) {
|
||||
expect(error.message).to.contain('timeout');
|
||||
}
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(infraOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
}
|
||||
|
||||
it('should NOT visible from synthetics only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.synthetics_only);
|
||||
const syntheticsOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
try {
|
||||
await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: syntheticsOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
timeout: 1000 * 3,
|
||||
});
|
||||
throw new Error('Expected rule to not be visible, but it was visible');
|
||||
} catch (error) {
|
||||
expect(error.message).to.contain('timeout');
|
||||
}
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(syntheticsOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom threshold - Rule execution - consumer notavalidconsumer', () => {
|
||||
const consumer = 'notavalidconsumer';
|
||||
it('creates rule successfully', async () => {
|
||||
const createdRule = await alertingApi.createRule({
|
||||
roleAuthc,
|
||||
...getRuleConfiguration({ dataViewId: DATA_VIEW_ID, consumer }),
|
||||
});
|
||||
expect(createdRule.statusCode).to.be(403);
|
||||
expect(createdRule.message).to.be(
|
||||
'Unauthorized by "notavalidconsumer" to create "observability.rules.custom_threshold" rule'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom threshold - Rule execution - consumer undefined', function () {
|
||||
const consumer = undefined;
|
||||
it('creates rule successfully', async () => {
|
||||
const createdRule = await alertingApi.createRule({
|
||||
roleAuthc,
|
||||
// @ts-ignore
|
||||
...getRuleConfiguration({ dataViewId: DATA_VIEW_ID, consumer }),
|
||||
});
|
||||
expect(createdRule.statusCode).to.be(400);
|
||||
expect(createdRule.message).to.be(
|
||||
'[request body.consumer]: expected value of type [string] but got [undefined]'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom threshold - Rule creation - logs only role', function () {
|
||||
const consumer = 'logs';
|
||||
let logsOnlyRole: RoleCredentials;
|
||||
|
||||
it('creates rule successfully', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.logs_only);
|
||||
logsOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
const createdRule = await alertingApi.createRule({
|
||||
roleAuthc: logsOnlyRole,
|
||||
...getRuleConfiguration({ dataViewId: DATA_VIEW_ID, consumer }),
|
||||
});
|
||||
ruleId = createdRule.id;
|
||||
expect(ruleId).not.to.be(undefined);
|
||||
});
|
||||
|
||||
it('should find the created rule with correct information about the consumer', async () => {
|
||||
const match = await alertingApi.findInRules(logsOnlyRole, ruleId);
|
||||
expect(match).not.to.be(undefined);
|
||||
expect(match.consumer).to.be(consumer);
|
||||
});
|
||||
|
||||
it('should be active and visible from logs only role', async () => {
|
||||
const executionStatus = await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: logsOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
});
|
||||
expect(executionStatus).to.be('active');
|
||||
});
|
||||
|
||||
it('cannot create a rule with infrastructure consumer', async () => {
|
||||
const createdRule = await alertingApi.createRule({
|
||||
roleAuthc: logsOnlyRole,
|
||||
...getRuleConfiguration({ dataViewId: DATA_VIEW_ID, consumer: 'infrastructure' }),
|
||||
});
|
||||
expect(createdRule.statusCode).to.be(403);
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(logsOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom threshold - Rule creation - infra only role', function () {
|
||||
const consumer = 'infrastructure';
|
||||
let infraOnlyRole: RoleCredentials;
|
||||
|
||||
it('creates rule successfully', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.infra_only);
|
||||
infraOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
const createdRule = await alertingApi.createRule({
|
||||
roleAuthc: infraOnlyRole,
|
||||
...getRuleConfiguration({ dataViewId: DATA_VIEW_ID, consumer }),
|
||||
});
|
||||
ruleId = createdRule.id;
|
||||
expect(ruleId).not.to.be(undefined);
|
||||
});
|
||||
|
||||
it('should find the created rule with correct information about the consumer', async () => {
|
||||
const match = await alertingApi.findInRules(roleAuthc, ruleId);
|
||||
expect(match).not.to.be(undefined);
|
||||
expect(match.consumer).to.be(consumer);
|
||||
});
|
||||
|
||||
it('should be active and visible from logs only role', async () => {
|
||||
const executionStatus = await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: infraOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
});
|
||||
expect(executionStatus).to.be('active');
|
||||
});
|
||||
|
||||
it('cannot create a rule with logs consumer', async () => {
|
||||
const createdRule = await alertingApi.createRule({
|
||||
roleAuthc: infraOnlyRole,
|
||||
...getRuleConfiguration({ dataViewId: DATA_VIEW_ID, consumer: 'logs' }),
|
||||
});
|
||||
expect(createdRule.statusCode).to.be(403);
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(infraOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Custom threshold - rule cannot be created - synthetics only role', function () {
|
||||
const consumer = 'logs';
|
||||
it('creates rule successfully', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.synthetics_only);
|
||||
const syntheticsOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
const response = await alertingApi.createRule({
|
||||
roleAuthc: syntheticsOnlyRole,
|
||||
...getRuleConfiguration({ dataViewId: DATA_VIEW_ID, consumer }),
|
||||
});
|
||||
expect(response.statusCode).to.be(403);
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(syntheticsOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export const ROLES = {
|
||||
infra_only: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['read'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: [],
|
||||
spaces: ['*'],
|
||||
feature: {
|
||||
actions: ['all'],
|
||||
maintenanceWindow: ['all'],
|
||||
observabilityCasesV3: ['all'],
|
||||
indexPatterns: ['all'],
|
||||
infrastructure: ['all'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
logs_only: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['read'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: [],
|
||||
spaces: ['*'],
|
||||
feature: {
|
||||
actions: ['all'],
|
||||
maintenanceWindow: ['all'],
|
||||
observabilityCasesV3: ['all'],
|
||||
indexPatterns: ['all'],
|
||||
logs: ['all'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
synthetics_only: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['read'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: [],
|
||||
spaces: ['*'],
|
||||
feature: {
|
||||
actions: ['all'],
|
||||
maintenanceWindow: ['all'],
|
||||
observabilityCasesV3: ['all'],
|
||||
uptime: ['all'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const getRuleConfiguration = ({
|
||||
consumer,
|
||||
dataViewId,
|
||||
}: {
|
||||
consumer: string;
|
||||
dataViewId: string;
|
||||
}) => ({
|
||||
tags: ['observability'],
|
||||
consumer,
|
||||
name: 'Threshold rule',
|
||||
ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
|
||||
params: {
|
||||
criteria: [
|
||||
{
|
||||
comparator: COMPARATORS.NOT_BETWEEN,
|
||||
threshold: [1, 2],
|
||||
timeSize: 1,
|
||||
timeUnit: 'm' as const,
|
||||
metrics: [{ name: 'A', filter: 'container.id:*', aggType: Aggregators.COUNT }],
|
||||
},
|
||||
],
|
||||
alertOnNoData: true,
|
||||
alertOnGroupDisappear: true,
|
||||
searchConfiguration: {
|
||||
query: {
|
||||
query: 'host.name:*',
|
||||
language: 'kuery',
|
||||
},
|
||||
index: dataViewId,
|
||||
},
|
||||
},
|
||||
});
|
|
@ -18,5 +18,7 @@ export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext)
|
|||
loadTestFile(require.resolve('./group_by_fired'));
|
||||
loadTestFile(require.resolve('./p99_pct_fired'));
|
||||
loadTestFile(require.resolve('./rate_bytes_fired'));
|
||||
// moved to feature flag config until custom roles in serverless are supported
|
||||
// loadTestFile(require.resolve('./consumers_and_privileges'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,491 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { RoleCredentials, InternalRequestHeader } from '@kbn/ftr-common-functional-services';
|
||||
import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
|
||||
|
||||
const RULE_TYPE_ID = '.es-query';
|
||||
|
||||
export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
|
||||
const esClient = getService('es');
|
||||
const samlAuth = getService('samlAuth');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
const esDeleteAllIndices = getService('esDeleteAllIndices');
|
||||
const alertingApi = getService('alertingApi');
|
||||
const config = getService('config');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const isServerless = config.get('serverless');
|
||||
|
||||
let editorRoleAuthc: RoleCredentials;
|
||||
let internalReqHeader: InternalRequestHeader;
|
||||
|
||||
describe('Query DSL - Consumers and privileges', function () {
|
||||
// skip until custom roles are supported in serverless
|
||||
this.tags(['skipInMKI']);
|
||||
const ALERT_ACTION_INDEX = 'alert-action-es-query';
|
||||
const RULE_ALERT_INDEX = '.alerts-stack.alerts-default';
|
||||
let ruleId: string;
|
||||
|
||||
before(async () => {
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
await esClient.deleteByQuery({
|
||||
index: RULE_ALERT_INDEX,
|
||||
query: { match_all: {} },
|
||||
conflicts: 'proceed',
|
||||
});
|
||||
editorRoleAuthc = await samlAuth.createM2mApiKeyWithRoleScope('editor');
|
||||
internalReqHeader = samlAuth.getInternalRequestHeader();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await supertestWithoutAuth
|
||||
.delete(`/api/alerting/rule/${ruleId}`)
|
||||
.set(editorRoleAuthc.apiKeyHeader)
|
||||
.set(internalReqHeader);
|
||||
await esClient.deleteByQuery({
|
||||
index: RULE_ALERT_INDEX,
|
||||
query: { match_all: {} },
|
||||
conflicts: 'proceed',
|
||||
});
|
||||
await esClient.deleteByQuery({
|
||||
index: '.kibana-event-log-*',
|
||||
query: { term: { 'rule.id': ruleId } },
|
||||
conflicts: 'proceed',
|
||||
});
|
||||
await esDeleteAllIndices([ALERT_ACTION_INDEX]);
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(editorRoleAuthc);
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
});
|
||||
|
||||
describe('Rule creation - logs consumer', () => {
|
||||
const consumer = 'logs';
|
||||
it('creates rule successfully', async () => {
|
||||
const createdRule = await alertingApi.helpers.createEsQueryRule({
|
||||
roleAuthc: editorRoleAuthc,
|
||||
...getESQueryRuleConfiguration({ consumer }),
|
||||
});
|
||||
ruleId = createdRule.id;
|
||||
expect(ruleId).not.to.be(undefined);
|
||||
});
|
||||
|
||||
it('should be active', async () => {
|
||||
const executionStatus = await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: editorRoleAuthc,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
});
|
||||
expect(executionStatus).to.be('active');
|
||||
});
|
||||
|
||||
it('should find the created rule with correct information about the consumer', async () => {
|
||||
const match = await alertingApi.findInRules(editorRoleAuthc, ruleId);
|
||||
expect(match).not.to.be(undefined);
|
||||
expect(match.consumer).to.be(consumer);
|
||||
});
|
||||
|
||||
it('should be active and visible from logs only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.logs_only);
|
||||
|
||||
const logsOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
const executionStatus = await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: logsOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
});
|
||||
expect(executionStatus).to.be('active');
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(logsOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
|
||||
it('should NOT be visible from infra only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.infra_only);
|
||||
|
||||
const infraOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
try {
|
||||
await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: infraOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
timeout: 1000 * 3,
|
||||
});
|
||||
throw new Error('Expected rule to not be visible, but it was visible');
|
||||
} catch (error) {
|
||||
expect(error.message).to.contain('timeout');
|
||||
}
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(infraOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
|
||||
it('should NOT be visible from synthetics only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.synthetics_only);
|
||||
|
||||
const syntheticsOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
try {
|
||||
await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: syntheticsOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
timeout: 1000 * 3,
|
||||
});
|
||||
throw new Error('Expected rule to not be visible, but it was visible');
|
||||
} catch (error) {
|
||||
expect(error.message).to.contain('timeout');
|
||||
}
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(syntheticsOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Rule creation - infrastructure consumer', () => {
|
||||
const consumer = 'infrastructure';
|
||||
it('creates rule successfully', async () => {
|
||||
const createdRule = await alertingApi.helpers.createEsQueryRule({
|
||||
roleAuthc: editorRoleAuthc,
|
||||
...getESQueryRuleConfiguration({ consumer }),
|
||||
});
|
||||
ruleId = createdRule.id;
|
||||
expect(ruleId).not.to.be(undefined);
|
||||
});
|
||||
|
||||
it('should be active', async () => {
|
||||
const executionStatus = await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: editorRoleAuthc,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
});
|
||||
expect(executionStatus).to.be('active');
|
||||
});
|
||||
|
||||
it('should find the created rule with correct information about the consumer', async () => {
|
||||
const match = await alertingApi.findInRules(editorRoleAuthc, ruleId);
|
||||
expect(match).not.to.be(undefined);
|
||||
expect(match.consumer).to.be(consumer);
|
||||
});
|
||||
|
||||
it('should be active and visible from infrastructure only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.infra_only);
|
||||
|
||||
const infraOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
const executionStatus = await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: infraOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
});
|
||||
expect(executionStatus).to.be('active');
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(infraOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
|
||||
it('should NOT be visible from logs only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.logs_only);
|
||||
|
||||
const logsOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
try {
|
||||
await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: logsOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
timeout: 1000 * 3,
|
||||
});
|
||||
throw new Error('Expected rule to not be visible, but it was visible');
|
||||
} catch (error) {
|
||||
expect(error.message).to.contain('timeout');
|
||||
}
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(logsOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
|
||||
it('should NOT be visible from synthetics only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.synthetics_only);
|
||||
|
||||
const syntheticsOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
try {
|
||||
await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: syntheticsOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
timeout: 1000 * 3,
|
||||
});
|
||||
throw new Error('Expected rule to not be visible, but it was visible');
|
||||
} catch (error) {
|
||||
expect(error.message).to.contain('timeout');
|
||||
}
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(syntheticsOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Rule creation - observability consumer', () => {
|
||||
const consumer = 'observability';
|
||||
|
||||
it('creates rule successfully', async () => {
|
||||
const createdRule = await alertingApi.helpers.createEsQueryRule({
|
||||
roleAuthc: editorRoleAuthc,
|
||||
...getESQueryRuleConfiguration({ consumer }),
|
||||
});
|
||||
ruleId = createdRule.id;
|
||||
expect(ruleId).not.to.be(undefined);
|
||||
});
|
||||
|
||||
it('should be active and visible from editor role', async () => {
|
||||
const executionStatus = await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: editorRoleAuthc,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
});
|
||||
expect(executionStatus).to.be('active');
|
||||
});
|
||||
|
||||
it('should find the created rule with correct information about the consumer', async () => {
|
||||
const match = await alertingApi.findInRules(editorRoleAuthc, ruleId);
|
||||
expect(match).not.to.be(undefined);
|
||||
expect(match.consumer).to.be(consumer);
|
||||
});
|
||||
|
||||
it('should be active and visible from logs only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.logs_only);
|
||||
|
||||
const logsOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
const executionStatus = await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: logsOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
});
|
||||
expect(executionStatus).to.be('active');
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(logsOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
|
||||
it('should be active and visible from infra only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.infra_only);
|
||||
|
||||
const infraOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
const executionStatus = await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: infraOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
});
|
||||
expect(executionStatus).to.be('active');
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(infraOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
|
||||
it('should NOT be visible from synthetics only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.synthetics_only);
|
||||
|
||||
const syntheticsOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
try {
|
||||
await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: syntheticsOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
timeout: 1000 * 3,
|
||||
});
|
||||
throw new Error('Expected rule to not be visible, but it was visible');
|
||||
} catch (error) {
|
||||
expect(error.message).to.contain('timeout');
|
||||
}
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(syntheticsOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Rule creation - stackAlerts consumer', () => {
|
||||
const consumer = 'stackAlerts';
|
||||
|
||||
if (isServerless) {
|
||||
it('is forbidden', async () => {
|
||||
await alertingApi.helpers.createEsQueryRule({
|
||||
roleAuthc: editorRoleAuthc,
|
||||
...getESQueryRuleConfiguration({ consumer }),
|
||||
expectedStatusCode: 403,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
it('creates rule successfully', async () => {
|
||||
const createdRule = await alertingApi.helpers.createEsQueryRule({
|
||||
roleAuthc: editorRoleAuthc,
|
||||
...getESQueryRuleConfiguration({ consumer }),
|
||||
});
|
||||
ruleId = createdRule.id;
|
||||
expect(ruleId).not.to.be(undefined);
|
||||
});
|
||||
|
||||
it('should find the created rule with correct information about the consumer', async () => {
|
||||
const match = await alertingApi.findInRules(editorRoleAuthc, ruleId);
|
||||
expect(match).not.to.be(undefined);
|
||||
expect(match.consumer).to.be(consumer);
|
||||
});
|
||||
|
||||
it('should be active and visible from the editor role', async () => {
|
||||
const executionStatus = await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: editorRoleAuthc,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
});
|
||||
expect(executionStatus).to.be('active');
|
||||
});
|
||||
|
||||
it('should NOT be visible from logs only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.logs_only);
|
||||
|
||||
const logsOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
try {
|
||||
await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: logsOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
timeout: 1000 * 3,
|
||||
});
|
||||
throw new Error('Expected rule to not be visible, but it was visible');
|
||||
} catch (error) {
|
||||
expect(error.message).to.contain('timeout');
|
||||
}
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(logsOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
|
||||
it('should NOT be visible from infra only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.infra_only);
|
||||
|
||||
const infraOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
try {
|
||||
await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: infraOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
timeout: 1000 * 3,
|
||||
});
|
||||
throw new Error('Expected rule to not be visible, but it was visible');
|
||||
} catch (error) {
|
||||
expect(error.message).to.contain('timeout');
|
||||
}
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(infraOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
}
|
||||
|
||||
it('should NOT be visible from synthetics only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.synthetics_only);
|
||||
|
||||
const syntheticsOnlyRole = await samlAuth.createM2mApiKeyWithCustomRoleScope();
|
||||
try {
|
||||
await alertingApi.waitForRuleStatus({
|
||||
roleAuthc: syntheticsOnlyRole,
|
||||
ruleId,
|
||||
expectedStatus: 'active',
|
||||
timeout: 1000 * 3,
|
||||
});
|
||||
throw new Error('Expected rule to not be visible, but it was visible');
|
||||
} catch (error) {
|
||||
expect(error.message).to.contain('timeout');
|
||||
}
|
||||
|
||||
await samlAuth.invalidateM2mApiKeyWithRoleScope(syntheticsOnlyRole);
|
||||
await samlAuth.deleteCustomRole();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export const ROLES = {
|
||||
infra_only: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['read'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: [],
|
||||
spaces: ['*'],
|
||||
feature: {
|
||||
actions: ['all'],
|
||||
maintenanceWindow: ['all'],
|
||||
observabilityCasesV3: ['all'],
|
||||
infrastructure: ['all'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
logs_only: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['read'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: [],
|
||||
spaces: ['*'],
|
||||
feature: {
|
||||
actions: ['all'],
|
||||
maintenanceWindow: ['all'],
|
||||
observabilityCasesV3: ['all'],
|
||||
logs: ['all'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
synthetics_only: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['read'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: [],
|
||||
spaces: ['*'],
|
||||
feature: {
|
||||
actions: ['all'],
|
||||
maintenanceWindow: ['all'],
|
||||
observabilityCasesV3: ['all'],
|
||||
uptime: ['all'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const getESQueryRuleConfiguration = ({ consumer }: { consumer: string }) => ({
|
||||
consumer,
|
||||
name: 'always fire',
|
||||
ruleTypeId: RULE_TYPE_ID,
|
||||
params: {
|
||||
size: 100,
|
||||
thresholdComparator: '>',
|
||||
threshold: [-1],
|
||||
index: ['alert-test-data'],
|
||||
timeField: 'date',
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
timeWindowSize: 20,
|
||||
timeWindowUnit: 's',
|
||||
},
|
||||
actions: [],
|
||||
});
|
|
@ -11,5 +11,7 @@ export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext)
|
|||
describe('ElasticSearch query rule', () => {
|
||||
loadTestFile(require.resolve('./query_dsl'));
|
||||
loadTestFile(require.resolve('./query_dsl_with_group_by'));
|
||||
// movd to feature flag config until custom roles are supported in serverless
|
||||
// loadTestFile(require.resolve('./consumers_and_privileges'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_cont
|
|||
|
||||
export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) {
|
||||
describe('Observability Alerting', () => {
|
||||
loadTestFile(require.resolve('./burn_rate_rule'));
|
||||
loadTestFile(require.resolve('./burn_rate'));
|
||||
loadTestFile(require.resolve('./es_query'));
|
||||
loadTestFile(require.resolve('./custom_threshold'));
|
||||
});
|
||||
|
|
|
@ -108,12 +108,9 @@ export function createServerlessFeatureFlagTestConfig<T extends DeploymentAgnost
|
|||
...svlSharedConfig.get('esTestCluster'),
|
||||
serverArgs: [
|
||||
...svlSharedConfig.get('esTestCluster.serverArgs'),
|
||||
// custom native roles are enabled only for search and security projects
|
||||
...(options.serverlessProject !== 'oblt'
|
||||
? ['xpack.security.authc.native_roles.enabled=true']
|
||||
: []),
|
||||
...esServerArgsFromController[options.serverlessProject],
|
||||
...(options.esServerArgs || []),
|
||||
'xpack.security.authc.native_roles.enabled=true',
|
||||
],
|
||||
},
|
||||
kbnTestServer: {
|
||||
|
@ -122,6 +119,8 @@ export function createServerlessFeatureFlagTestConfig<T extends DeploymentAgnost
|
|||
...svlSharedConfig.get('kbnTestServer.serverArgs'),
|
||||
...kbnServerArgsFromController[options.serverlessProject],
|
||||
`--serverless=${options.serverlessProject}`,
|
||||
// Enable custom roles
|
||||
'--xpack.security.roleManagementEnabled=true',
|
||||
...(options.serverlessProject === 'oblt'
|
||||
? [
|
||||
// defined in MKI control plane. Necessary for Synthetics app testing
|
||||
|
|
|
@ -7,12 +7,21 @@
|
|||
import { DeploymentAgnosticFtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) {
|
||||
describe('Stateful Observability - Deployment-agnostic Synthetics Alerting API integration tests', function () {
|
||||
describe('Serverless Observability feature flag testing - Deployment-agnostic API integration tests', function () {
|
||||
loadTestFile(
|
||||
require.resolve('../../apis/observability/alerting/synthetics/synthetics_default_rule')
|
||||
);
|
||||
loadTestFile(
|
||||
require.resolve('../../apis/observability/alerting/synthetics/custom_status_rule')
|
||||
);
|
||||
loadTestFile(
|
||||
require.resolve('../../apis/observability/alerting/custom_threshold/consumers_and_privileges')
|
||||
);
|
||||
loadTestFile(
|
||||
require.resolve('../../apis/observability/alerting/es_query/consumers_and_privileges')
|
||||
);
|
||||
loadTestFile(
|
||||
require.resolve('../../apis/observability/alerting/burn_rate/consumers_and_privileges')
|
||||
);
|
||||
});
|
||||
}
|
|
@ -13,7 +13,7 @@ export default createServerlessFeatureFlagTestConfig({
|
|||
'--xpack.actions.preconfigured',
|
||||
'--xpack.alerting.rules.minimumScheduleInterval.value="1s"',
|
||||
],
|
||||
testFiles: [require.resolve('./oblt.synthetics.index.ts')],
|
||||
testFiles: [require.resolve('./oblt.index.ts')],
|
||||
junit: {
|
||||
reportName: 'Serverless Observability - Deployment-agnostic Feature Flag API Integration Tests',
|
||||
},
|
|
@ -7,12 +7,21 @@
|
|||
import { DeploymentAgnosticFtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) {
|
||||
describe('Serverless Observability - Deployment-agnostic Synthetics Alerting API integration tests', function () {
|
||||
describe('Stateful Observability feature flag testing - Deployment-agnostic API integration tests', function () {
|
||||
loadTestFile(
|
||||
require.resolve('../../apis/observability/alerting/synthetics/synthetics_default_rule')
|
||||
);
|
||||
loadTestFile(
|
||||
require.resolve('../../apis/observability/alerting/synthetics/custom_status_rule')
|
||||
);
|
||||
loadTestFile(
|
||||
require.resolve('../../apis/observability/alerting/custom_threshold/consumers_and_privileges')
|
||||
);
|
||||
loadTestFile(
|
||||
require.resolve('../../apis/observability/alerting/es_query/consumers_and_privileges')
|
||||
);
|
||||
loadTestFile(
|
||||
require.resolve('../../apis/observability/alerting/burn_rate/consumers_and_privileges')
|
||||
);
|
||||
});
|
||||
}
|
|
@ -8,7 +8,7 @@
|
|||
import { createStatefulFeatureFlagTestConfig } from '../../default_configs/feature_flag.stateful.config.base';
|
||||
|
||||
export default createStatefulFeatureFlagTestConfig({
|
||||
testFiles: [require.resolve('./oblt.synthetics.index.ts')],
|
||||
testFiles: [require.resolve('./oblt.index.ts')],
|
||||
kbnServerArgs: ['--xpack.actions.preconfigured'],
|
||||
junit: {
|
||||
reportName: 'Stateful Observability - Deployment-agnostic Feature Flag API Integration Tests',
|
|
@ -263,6 +263,7 @@ export function AlertingApiProvider({ getService }: DeploymentAgnosticFtrProvide
|
|||
consumer,
|
||||
notifyWhen,
|
||||
enabled = true,
|
||||
expectedStatusCode = 200,
|
||||
}: {
|
||||
roleAuthc: RoleCredentials;
|
||||
ruleTypeId: string;
|
||||
|
@ -274,6 +275,7 @@ export function AlertingApiProvider({ getService }: DeploymentAgnosticFtrProvide
|
|||
schedule?: { interval: string };
|
||||
notifyWhen?: string;
|
||||
enabled?: boolean;
|
||||
expectedStatusCode?: number;
|
||||
}) {
|
||||
const { body } = await supertestWithoutAuth
|
||||
.post(`/api/alerting/rule`)
|
||||
|
@ -292,7 +294,7 @@ export function AlertingApiProvider({ getService }: DeploymentAgnosticFtrProvide
|
|||
actions,
|
||||
...(notifyWhen ? { notify_when: notifyWhen, throttle: '5m' } : {}),
|
||||
})
|
||||
.expect(200);
|
||||
.expect(expectedStatusCode);
|
||||
return body;
|
||||
},
|
||||
|
||||
|
@ -926,13 +928,15 @@ export function AlertingApiProvider({ getService }: DeploymentAgnosticFtrProvide
|
|||
expectedStatus,
|
||||
roleAuthc,
|
||||
spaceId,
|
||||
timeout = retryTimeout,
|
||||
}: {
|
||||
ruleId: string;
|
||||
expectedStatus: string;
|
||||
roleAuthc: RoleCredentials;
|
||||
spaceId?: string;
|
||||
timeout?: number;
|
||||
}) {
|
||||
return await retry.tryForTime(retryTimeout, async () => {
|
||||
return await retry.tryForTime(timeout, async () => {
|
||||
const response = await supertestWithoutAuth
|
||||
.get(`${spaceId ? '/s/' + spaceId : ''}/api/alerting/rule/${ruleId}`)
|
||||
.set(roleAuthc.apiKeyHeader)
|
||||
|
|
|
@ -8,12 +8,17 @@
|
|||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
const RULE_ALERT_INDEX_PATTERN = '.alerts-stack.alerts-*';
|
||||
|
||||
export default ({ getService, getPageObjects }: FtrProviderContext) => {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const supertest = getService('supertest');
|
||||
const find = getService('find');
|
||||
const retry = getService('retry');
|
||||
const rulesService = getService('rules');
|
||||
const esClient = getService('es');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const RULE_ENDPOINT = '/api/alerting/rule';
|
||||
const INTERNAL_RULE_ENDPOINT = '/internal/alerting/rules';
|
||||
|
||||
|
@ -111,11 +116,23 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => {
|
|||
await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts');
|
||||
await esArchiver.load('x-pack/test/functional/es_archives/infra/metrics_and_logs');
|
||||
await observability.alerts.common.navigateWithoutFilter();
|
||||
await esClient.deleteByQuery({
|
||||
index: RULE_ALERT_INDEX_PATTERN,
|
||||
query: { match_all: {} },
|
||||
conflicts: 'proceed',
|
||||
});
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await esArchiver.unload('x-pack/test/functional/es_archives/observability/alerts');
|
||||
await esArchiver.unload('x-pack/test/functional/es_archives/infra/metrics_and_logs');
|
||||
await esClient.deleteByQuery({
|
||||
index: RULE_ALERT_INDEX_PATTERN,
|
||||
query: { match_all: {} },
|
||||
conflicts: 'proceed',
|
||||
});
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
});
|
||||
|
||||
describe('Feature flag', () => {
|
||||
|
@ -382,5 +399,30 @@ export default ({ getService, getPageObjects }: FtrProviderContext) => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Stack alerts consumer', () => {
|
||||
it('should create an ES Query rule and NOT display it when consumer is stackAlerts', async () => {
|
||||
const name = 'ES Query with stackAlerts consumer';
|
||||
await rulesService.api.createRule({
|
||||
name,
|
||||
consumer: 'stackAlerts',
|
||||
ruleTypeId: '.es-query',
|
||||
params: {
|
||||
size: 100,
|
||||
thresholdComparator: '>',
|
||||
threshold: [-1],
|
||||
index: ['alert-test-data'],
|
||||
timeField: 'date',
|
||||
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
|
||||
timeWindowSize: 20,
|
||||
timeWindowUnit: 's',
|
||||
},
|
||||
schedule: { interval: '1m' },
|
||||
});
|
||||
|
||||
await observability.alerts.common.navigateToRulesPage();
|
||||
await testSubjects.missingOrFail('rule-row');
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -28,5 +28,5 @@ export default createTestConfig({
|
|||
|
||||
// include settings from project controller
|
||||
// https://github.com/elastic/project-controller/blob/main/internal/project/observability/config/elasticsearch.yml
|
||||
esServerArgs: ['xpack.ml.dfa.enabled=false'],
|
||||
esServerArgs: ['xpack.ml.dfa.enabled=false', 'xpack.security.authc.native_roles.enabled=true'],
|
||||
});
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -11,6 +11,8 @@ export default function ({ loadTestFile }: FtrProviderContext) {
|
|||
describe('serverless observability UI - feature flags', function () {
|
||||
// add tests that require feature flags, defined in config.feature_flags.ts
|
||||
loadTestFile(require.resolve('./role_management'));
|
||||
loadTestFile(require.resolve('./rules/custom_threshold_consumer'));
|
||||
loadTestFile(require.resolve('./rules/es_query_consumer'));
|
||||
loadTestFile(require.resolve('./infra'));
|
||||
loadTestFile(require.resolve('./streams'));
|
||||
});
|
||||
|
|
|
@ -19,6 +19,9 @@ export default function ({ loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./discover/embeddables'));
|
||||
loadTestFile(require.resolve('./onboarding'));
|
||||
loadTestFile(require.resolve('./rules/rules_list'));
|
||||
// moved to feature flags config until custom roles in serverless are supported
|
||||
// loadTestFile(require.resolve('./rules/custom_threshold_consumer'));
|
||||
// loadTestFile(require.resolve('./rules/es_query_consumer'));
|
||||
loadTestFile(require.resolve('./cases'));
|
||||
loadTestFile(require.resolve('./advanced_settings'));
|
||||
loadTestFile(require.resolve('./ml'));
|
||||
|
|
|
@ -0,0 +1,229 @@
|
|||
/*
|
||||
* 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 { v4 as uuidv4 } from 'uuid';
|
||||
import { expect } from 'expect';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
import { RoleCredentials } from '../../../../shared/services';
|
||||
|
||||
export default ({ getPageObject, getService }: FtrProviderContext) => {
|
||||
const svlCommonPage = getPageObject('svlCommonPage');
|
||||
const svlCommonNavigation = getPageObject('svlCommonNavigation');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const supertest = getService('supertest');
|
||||
const retry = getService('retry');
|
||||
const svlUserManager = getService('svlUserManager');
|
||||
const alertingApi = getService('alertingApi');
|
||||
const dataViewApi = getService('dataViewApi');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const samlAuth = getService('samlAuth');
|
||||
let roleAuthc: RoleCredentials;
|
||||
|
||||
function createCustomThresholdRule({ ruleName }: { ruleName: string }) {
|
||||
it('navigates to the rules page', async () => {
|
||||
await svlCommonNavigation.sidenav.clickLink({ text: 'Alerts' });
|
||||
await testSubjects.click('manageRulesPageButton');
|
||||
});
|
||||
|
||||
it('should open the rule creation flyout', async () => {
|
||||
await retry.try(async () => {
|
||||
await testSubjects.click('createRuleButton');
|
||||
const isCreateRuleFlyoutVisible = await testSubjects.exists('ruleTypeModal');
|
||||
expect(isCreateRuleFlyoutVisible).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
it('should click the custom threshold rule type', async () => {
|
||||
await testSubjects.click('observability.rules.custom_threshold-SelectOption');
|
||||
const ruleType = await testSubjects.getVisibleText('ruleDefinitionHeaderRuleTypeName');
|
||||
expect(ruleType).toEqual('Custom threshold');
|
||||
await testSubjects.exists('selectDataViewExpression');
|
||||
});
|
||||
|
||||
it('should create a new custom threshold rule', async () => {
|
||||
const input = await testSubjects.find('ruleDetailsNameInput');
|
||||
await input.clearValueWithKeyboard();
|
||||
await testSubjects.setValue('ruleDetailsNameInput', ruleName);
|
||||
await retry.try(async () => {
|
||||
await testSubjects.click('rulePageFooterSaveButton');
|
||||
const doesConfirmModalExist = await testSubjects.exists('confirmModalConfirmButton');
|
||||
expect(doesConfirmModalExist).toBe(true);
|
||||
await testSubjects.click('confirmModalConfirmButton');
|
||||
const name = await testSubjects.getVisibleText('ruleName');
|
||||
expect(name).toEqual(ruleName);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe('Custom threshold rule - consumers', function () {
|
||||
// custom roles are not yet supported in MKI
|
||||
this.tags(['skipMKI']);
|
||||
const ruleIdList: string[] = [];
|
||||
|
||||
before(async () => {
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
// re-create the default data view in case it has been cleaned up by another test
|
||||
await dataViewApi.create({
|
||||
id: 'default_all_data_id',
|
||||
name: 'default:all-data',
|
||||
title: '*,-.*',
|
||||
});
|
||||
roleAuthc = await svlUserManager.createM2mApiKeyWithRoleScope('admin');
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
ruleIdList.map(async (ruleId) => {
|
||||
await supertest
|
||||
.delete(`/api/alerting/rule/${ruleId}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.set('x-elastic-internal-origin', 'foo');
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await svlUserManager.invalidateM2mApiKeyWithRoleScope(roleAuthc);
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
});
|
||||
|
||||
describe('both logs and infrastructure privileges', () => {
|
||||
const uuid = ` ${uuidv4()}`;
|
||||
const ruleName = `Custom threshold rule${uuid}`;
|
||||
it('logs in with privileged role', async () => {
|
||||
await svlCommonPage.loginWithPrivilegedRole();
|
||||
});
|
||||
|
||||
createCustomThresholdRule({ ruleName });
|
||||
|
||||
it('should have logs consumer by default', async () => {
|
||||
const searchResults = (await alertingApi.searchRules(
|
||||
roleAuthc,
|
||||
`alert.attributes.name:"${ruleName}"`
|
||||
)) as { body: { data: Array<{ consumer: string; id: string }> } };
|
||||
const rule = searchResults.body.data[0];
|
||||
expect(rule.consumer).toEqual('logs');
|
||||
ruleIdList.push(rule.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('only logs privileges', () => {
|
||||
const uuid = ` ${uuidv4()}`;
|
||||
const ruleName = `Custom threshold rule${uuid}`;
|
||||
it('logs in with logs only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.logs_only);
|
||||
|
||||
await svlCommonPage.loginWithCustomRole();
|
||||
});
|
||||
|
||||
createCustomThresholdRule({ ruleName });
|
||||
|
||||
it('should have logs consumer by default', async () => {
|
||||
const searchResults = await alertingApi.searchRules(
|
||||
roleAuthc,
|
||||
`alert.attributes.name:"${ruleName}"`
|
||||
);
|
||||
const rule = searchResults.body.data[0];
|
||||
expect(rule.consumer).toEqual('logs');
|
||||
ruleIdList.push(rule.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('only infrastructure privileges', () => {
|
||||
const uuid = ` ${uuidv4()}`;
|
||||
const ruleName = `Custom threshold rule${uuid}`;
|
||||
|
||||
it('logs in with infra only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.infra_only);
|
||||
|
||||
await svlCommonPage.loginWithCustomRole();
|
||||
});
|
||||
|
||||
createCustomThresholdRule({ ruleName });
|
||||
|
||||
it('should have infrastructure consumer by default', async () => {
|
||||
const searchResults = await alertingApi.searchRules(
|
||||
roleAuthc,
|
||||
`alert.attributes.name:"${ruleName}"`
|
||||
);
|
||||
const rule = searchResults.body.data[0];
|
||||
expect(rule.consumer).toEqual('infrastructure');
|
||||
ruleIdList.push(rule.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const ROLES = {
|
||||
infra_only: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['read'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: [],
|
||||
spaces: ['*'],
|
||||
feature: {
|
||||
actions: ['all'],
|
||||
maintenanceWindow: ['all'],
|
||||
observabilityCasesV3: ['all'],
|
||||
infrastructure: ['all'],
|
||||
indexPatterns: ['all'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
logs_only: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['read'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: [],
|
||||
spaces: ['*'],
|
||||
feature: {
|
||||
actions: ['all'],
|
||||
maintenanceWindow: ['all'],
|
||||
observabilityCasesV3: ['all'],
|
||||
logs: ['all'],
|
||||
indexPatterns: ['all'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
synthetics_only: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['read'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: [],
|
||||
spaces: ['*'],
|
||||
feature: {
|
||||
actions: ['all'],
|
||||
maintenanceWindow: ['all'],
|
||||
observabilityCasesV3: ['all'],
|
||||
uptime: ['all'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
|
@ -0,0 +1,227 @@
|
|||
/*
|
||||
* 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 { v4 as uuidv4 } from 'uuid';
|
||||
import { expect } from 'expect';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
import { RoleCredentials } from '../../../../shared/services';
|
||||
|
||||
export default ({ getPageObject, getService }: FtrProviderContext) => {
|
||||
const svlCommonPage = getPageObject('svlCommonPage');
|
||||
const svlCommonNavigation = getPageObject('svlCommonNavigation');
|
||||
const testSubjects = getService('testSubjects');
|
||||
const supertest = getService('supertest');
|
||||
const retry = getService('retry');
|
||||
const svlUserManager = getService('svlUserManager');
|
||||
const alertingApi = getService('alertingApi');
|
||||
const dataViewApi = getService('dataViewApi');
|
||||
const samlAuth = getService('samlAuth');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
let roleAuthc: RoleCredentials;
|
||||
|
||||
function createESQueryRule({ ruleName }: { ruleName: string }) {
|
||||
it('navigates to the rules page', async () => {
|
||||
await svlCommonNavigation.sidenav.clickLink({ text: 'Alerts' });
|
||||
await testSubjects.click('manageRulesPageButton');
|
||||
});
|
||||
|
||||
it('should open the rule creation flyout', async () => {
|
||||
await testSubjects.click('createRuleButton');
|
||||
const isCreateRuleFlyoutVisible = await testSubjects.exists('ruleTypeModal');
|
||||
expect(isCreateRuleFlyoutVisible).toBe(true);
|
||||
});
|
||||
|
||||
it('should click the es query rule type', async () => {
|
||||
await testSubjects.click('.es-query-SelectOption');
|
||||
const ruleType = await testSubjects.getVisibleText('ruleDefinitionHeaderRuleTypeName');
|
||||
expect(ruleType).toEqual('Elasticsearch query');
|
||||
});
|
||||
|
||||
it('should create a new es query rule', async () => {
|
||||
await testSubjects.click('queryFormType_searchSource');
|
||||
await testSubjects.exists('selectDataViewExpression');
|
||||
const input = await testSubjects.find('ruleDetailsNameInput');
|
||||
await input.clearValueWithKeyboard();
|
||||
await testSubjects.setValue('ruleDetailsNameInput', ruleName);
|
||||
await retry.try(async () => {
|
||||
await testSubjects.click('rulePageFooterSaveButton');
|
||||
const doesConfirmModalExist = await testSubjects.exists('confirmModalConfirmButton');
|
||||
expect(doesConfirmModalExist).toBe(true);
|
||||
});
|
||||
await testSubjects.click('confirmModalConfirmButton');
|
||||
const name = await testSubjects.getVisibleText('ruleName');
|
||||
expect(name).toEqual(ruleName);
|
||||
});
|
||||
}
|
||||
|
||||
describe('ES Query rule - consumers', function () {
|
||||
// custom roles are not yet supported in MKI
|
||||
this.tags(['skipMKI']);
|
||||
const ruleIdList: string[] = [];
|
||||
|
||||
before(async () => {
|
||||
roleAuthc = await svlUserManager.createM2mApiKeyWithRoleScope('admin');
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
// re-create the default data view in case it has been cleaned up by another test
|
||||
await dataViewApi.create({
|
||||
id: 'default_all_data_id',
|
||||
name: 'default:all-data',
|
||||
title: '*,-.*',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
ruleIdList.map(async (ruleId) => {
|
||||
await supertest
|
||||
.delete(`/api/alerting/rule/${ruleId}`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.set('x-elastic-internal-origin', 'foo');
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await svlUserManager.invalidateM2mApiKeyWithRoleScope(roleAuthc);
|
||||
});
|
||||
|
||||
describe('both logs and infrastructure privileges', () => {
|
||||
const uuid = ` ${uuidv4()}`;
|
||||
const ruleName = `ES Query rule${uuid}`;
|
||||
it('logs in with privileged role', async () => {
|
||||
await svlCommonPage.loginWithPrivilegedRole();
|
||||
});
|
||||
|
||||
createESQueryRule({ ruleName });
|
||||
|
||||
it('should have logs consumer by default', async () => {
|
||||
const searchResults = await alertingApi.searchRules(
|
||||
roleAuthc,
|
||||
`alert.attributes.name:"${ruleName}"`
|
||||
);
|
||||
const rule = searchResults.body.data[0];
|
||||
expect(rule.consumer).toEqual('logs');
|
||||
ruleIdList.push(rule.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('only logs privileges', () => {
|
||||
const uuid = ` ${uuidv4()}`;
|
||||
const ruleName = `ES Query rule${uuid}`;
|
||||
it('logs in with logs only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.logs_only);
|
||||
|
||||
await svlCommonPage.loginWithCustomRole();
|
||||
});
|
||||
|
||||
createESQueryRule({ ruleName });
|
||||
|
||||
it('should have logs consumer by default', async () => {
|
||||
const searchResults = await alertingApi.searchRules(
|
||||
roleAuthc,
|
||||
`alert.attributes.name:"${ruleName}"`
|
||||
);
|
||||
const rule = searchResults.body.data[0];
|
||||
expect(rule.consumer).toEqual('logs');
|
||||
ruleIdList.push(rule.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('only infrastructure privileges', () => {
|
||||
const uuid = ` ${uuidv4()}`;
|
||||
const ruleName = `ES Query rule${uuid}`;
|
||||
|
||||
it('logs in with infra only role', async () => {
|
||||
await samlAuth.setCustomRole(ROLES.infra_only);
|
||||
|
||||
await svlCommonPage.loginWithCustomRole();
|
||||
});
|
||||
|
||||
createESQueryRule({ ruleName });
|
||||
|
||||
it('should have infrastructure consumer by default', async () => {
|
||||
const searchResults = await alertingApi.searchRules(
|
||||
roleAuthc,
|
||||
`alert.attributes.name:"${ruleName}"`
|
||||
);
|
||||
const rule = searchResults.body.data[0];
|
||||
expect(rule.consumer).toEqual('infrastructure');
|
||||
ruleIdList.push(rule.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export const ROLES = {
|
||||
infra_only: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['read'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: [],
|
||||
spaces: ['*'],
|
||||
feature: {
|
||||
actions: ['all'],
|
||||
maintenanceWindow: ['all'],
|
||||
observabilityCasesV3: ['all'],
|
||||
indexPatterns: ['all'],
|
||||
infrastructure: ['all'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
logs_only: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['read'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: [],
|
||||
spaces: ['*'],
|
||||
feature: {
|
||||
actions: ['all'],
|
||||
maintenanceWindow: ['all'],
|
||||
observabilityCasesV3: ['all'],
|
||||
indexPatterns: ['all'],
|
||||
logs: ['all'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
synthetics_only: {
|
||||
elasticsearch: {
|
||||
indices: [
|
||||
{
|
||||
names: ['*'],
|
||||
privileges: ['read'],
|
||||
},
|
||||
],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
base: [],
|
||||
spaces: ['*'],
|
||||
feature: {
|
||||
actions: ['all'],
|
||||
maintenanceWindow: ['all'],
|
||||
observabilityCasesV3: ['all'],
|
||||
uptime: ['all'],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
|
@ -23,6 +23,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
|
|||
const log = getService('log');
|
||||
const svlUserManager = getService('svlUserManager');
|
||||
const alertingApi = getService('alertingApi');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
let roleAuthc: RoleCredentials;
|
||||
|
||||
async function refreshRulesList() {
|
||||
|
@ -74,6 +75,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
|
|||
};
|
||||
|
||||
before(async () => {
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
roleAuthc = await svlUserManager.createM2mApiKeyWithRoleScope('admin');
|
||||
await svlCommonPage.loginWithPrivilegedRole();
|
||||
await svlObltNavigation.navigateToLandingPage();
|
||||
|
@ -90,6 +92,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
|
|||
.set('x-elastic-internal-origin', 'foo');
|
||||
})
|
||||
);
|
||||
await kibanaServer.savedObjects.cleanStandardList();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
|
@ -99,7 +102,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
|
|||
it('should create an ES Query Rule and display it when consumer is observability', async () => {
|
||||
const esQuery = await alertingApi.helpers.createEsQueryRule({
|
||||
roleAuthc,
|
||||
name: 'ES Query',
|
||||
name: 'ES Query with observability consumer',
|
||||
consumer: 'observability',
|
||||
ruleTypeId: '.es-query',
|
||||
params: {
|
||||
|
@ -118,13 +121,15 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
|
|||
await refreshRulesList();
|
||||
const searchResults = await svlTriggersActionsUI.getRulesList();
|
||||
expect(searchResults.length).toEqual(1);
|
||||
expect(searchResults[0].name).toEqual('ES QueryElasticsearch query');
|
||||
expect(searchResults[0].name).toEqual(
|
||||
'ES Query with observability consumerElasticsearch query'
|
||||
);
|
||||
});
|
||||
|
||||
it('should create an ES Query rule but not display it when consumer is stackAlerts', async () => {
|
||||
it('should create an ES Query rule and NOT display it when consumer is stackAlerts', async () => {
|
||||
const esQuery = await alertingApi.helpers.createEsQueryRule({
|
||||
roleAuthc,
|
||||
name: 'ES Query',
|
||||
name: 'ES Query with stackAlerts consumer',
|
||||
consumer: 'stackAlerts',
|
||||
ruleTypeId: '.es-query',
|
||||
params: {
|
||||
|
@ -684,9 +689,7 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
|
|||
await testSubjects.click('ruleTypeFilterButton');
|
||||
}
|
||||
|
||||
expect(await (await testSubjects.find('ruleType0Group')).getVisibleText()).toEqual(
|
||||
'Observability'
|
||||
);
|
||||
expect(await (await testSubjects.find('ruleType0Group')).getVisibleText()).toEqual('Apm');
|
||||
});
|
||||
|
||||
await testSubjects.click('ruleTypeapm.anomalyFilterOption');
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue