mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
# Backport This will backport the following commits from `main` to `8.x`: - [[ResponseOps][Alerting] Decouple feature IDs from consumers (#183756)](https://github.com/elastic/kibana/pull/183756) <!--- Backport version: 8.9.8 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sqren/backport) <!--BACKPORT [{"author":{"name":"Christos Nasikas","email":"christos.nasikas@elastic.co"},"sourceCommit":{"committedDate":"2024-12-03T10:21:53Z","message":"[ResponseOps][Alerting] Decouple feature IDs from consumers (#183756)\n\n## Summary\r\n\r\nThis PR aims to decouple the feature IDs from the `consumer` attribute\r\nof rules and alerts.\r\n\r\nTowards: https://github.com/elastic/kibana/issues/187202\r\nFixes: https://github.com/elastic/kibana/issues/181559\r\nFixes: https://github.com/elastic/kibana/issues/182435\r\n\r\n> [!NOTE] \r\n> Unfortunately, I could not break the PR into smaller pieces. The APIs\r\ncould not work anymore with feature IDs and had to convert them to use\r\nrule type IDs. Also, I took the chance and refactored crucial parts of\r\nthe authorization class that in turn affected a lot of files. Most of\r\nthe changes in the files are minimal and easy to review. The crucial\r\nchanges are in the authorization class and some alerting APIs.\r\n\r\n## Architecture\r\n\r\n### Alerting RBAC model\r\n\r\nThe Kibana security uses Elasticsearch's [application\r\nprivileges](https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-privileges.html#security-api-put-privileges).\r\nThis way Kibana can represent and store its privilege models within\r\nElasticsearch roles. To do that, Kibana security creates actions that\r\nare granted by a specific privilege. Alerting uses its own RBAC model\r\nand is built on top of the existing Kibana security model. The Alerting\r\nRBAC uses the `rule_type_id` and `consumer` attributes to define who\r\nowns the rule and the alerts procured by the rule. To connect the\r\n`rule_type_id` and `consumer` with the Kibana security actions the\r\nAlerting RBAC registers its custom actions. They are constructed as\r\n`alerting:<rule-type-id>/<feature-id>/<alerting-entity>/<operation>`.\r\nBecause to authorizate a resource an action has to be generated and\r\nbecause the action needs a valid feature ID the value of the `consumer`\r\nshould be a valid feature ID. For example, the\r\n`alerting:siem.esqlRule/siem/rule/get` action, means that a user with a\r\nrole that grants this action can get a rule of type `siem.esqlRule` with\r\nconsumer `siem`.\r\n\r\n### Problem statement\r\n\r\nAt the moment the `consumer` attribute should be a valid feature ID.\r\nThough this approach worked well so far it has its limitation.\r\nSpecifically:\r\n\r\n- Rule types cannot support more than one consumer.\r\n- To associate old rules with a new feature ID required a migration on\r\nthe rule's SOs and the alerts documents.\r\n- The API calls are feature ID-oriented and not rule-type-oriented.\r\n- The framework has to be aware of the values of the `consumer`\r\nattribute.\r\n- Feature IDs are tightly coupled with the alerting indices leading to\r\n[bugs](https://github.com/elastic/kibana/issues/179082).\r\n- Legacy consumers that are not a valid feature anymore can cause\r\n[bugs](https://github.com/elastic/kibana/issues/184595).\r\n- The framework has to be aware of legacy consumers to handle edge\r\ncases.\r\n- The framework has to be aware of specific consumers to handle edge\r\ncases.\r\n\r\n### Proposed solution\r\n\r\nThis PR aims to decouple the feature IDs from consumers. It achieves\r\nthat a) by changing the way solutions configure the alerting privileges\r\nwhen registering a feature and b) by changing the alerting actions. The\r\nschema changes as:\r\n\r\n```\r\n// Old formatting\r\nid: 'siem', <--- feature ID\r\nalerting:['siem.queryRule']\r\n\r\n// New formatting\r\nid: 'siem', <--- feature ID\r\nalerting: [{ ruleTypeId: 'siem.queryRule', consumers: ['siem'] }] <-- consumer same as the feature ID in the old formatting\r\n```\r\n\r\nThe new actions are constructed as\r\n`alerting:<rule-type-id>/<consumer>/<alerting-entity>/<operation>`. For\r\nexample `alerting:rule-type-id/my-consumer/rule/get`. The new action\r\nmeans that a user with a role that grants this action can get a rule of\r\ntype `rule-type` with consumer `my-consumer`. Changing the action\r\nstrings is not considered a breaking change as long as the user's\r\npermission works as before. In our case, this is true because the\r\nconsumer will be the same as before (feature ID), and the alerting\r\nsecurity actions will be the same. For example:\r\n\r\n**Old formatting**\r\n\r\nSchema:\r\n```\r\nid: 'logs', <--- feature ID\r\nalerting:['.es-query'] <-- rule type ID\r\n```\r\n\r\nGenerated action:\r\n\r\n```\r\nalerting:.es-query/logs/rule/get\r\n```\r\n\r\n**New formatting**\r\n\r\nSchema:\r\n```\r\nid: 'siem', <--- feature ID\r\nalerting: [{ ruleTypeId: '.es-query', consumers: ['logs'] }] <-- consumer same as the feature ID in the old formatting\r\n```\r\n\r\nGenerated action:\r\n\r\n```\r\nalerting:.es-query/logs/rule/get <--- consumer is set as logs and the action is the same as before\r\n```\r\n\r\nIn both formating the actions are the same thus breaking changes are\r\navoided.\r\n\r\n### Alerting authorization class\r\nThe alerting plugin uses and exports the alerting authorization class\r\n(`AlertingAuthorization`). The class is responsible for handling all\r\nauthorization actions related to rules and alerts. The class changed to\r\nhandle the new actions as described in the above sections. A lot of\r\nmethods were renamed, removed, and cleaned up, all method arguments\r\nconverted to be an object, and the response signature of some methods\r\nchanged. These changes affected various pieces of the code. The changes\r\nin this class are the most important in this PR especially the\r\n`_getAuthorizedRuleTypesWithAuthorizedConsumers` method which is the\r\ncornerstone of the alerting RBAC. Please review carefully.\r\n\r\n### Instantiation of the alerting authorization class\r\nThe `AlertingAuthorizationClientFactory` is used to create instances of\r\nthe `AlertingAuthorization` class. The `AlertingAuthorization` class\r\nneeds to perform async operations upon instantiation. Because JS, at the\r\nmoment, does not support async instantiation of classes the\r\n`AlertingAuthorization` class was assigning `Promise` objects to\r\nvariables that could be resolved later in other phases of the lifecycle\r\nof the class. To improve readability and make the lifecycle of the class\r\nclearer, I separated the construction of the class (initialization) from\r\nthe bootstrap process. As a result, getting the `AlertingAuthorization`\r\nclass or any client that depends on it (`getRulesClient` for example) is\r\nan async operation.\r\n\r\n### Filtering\r\nA lot of routes use the authorization class to get the authorization\r\nfilter (`getFindAuthorizationFilter`), a filter that, if applied,\r\nreturns only the rule types and consumers the user is authorized to. The\r\nmethod that returns the filter was built in a way to also support\r\nfiltering on top of the authorization filter thus coupling the\r\nauthorized filter with router filtering. I believe these two operations\r\nshould be decoupled and the filter method should return a filter that\r\ngives you all the authorized rule types. It is the responsibility of the\r\nconsumer, router in our case, to apply extra filters on top of the\r\nauthorization filter. For that reason, I made all the necessary changes\r\nto decouple them.\r\n\r\n### Legacy consumers & producer\r\nA lot of rules and alerts have been created and are still being created\r\nfrom observability with the `alerts` consumer. When the Alerting RBAC\r\nencounters a rule or alert with `alerts` as a consumer it falls back to\r\nthe `producer` of the rule type ID to construct the actions. For example\r\nif a rule with `ruleTypeId: .es-query` and `consumer: alerts` the\r\nalerting action will be constructed as\r\n`alerting:.es-query/stackAlerts/rule/get` where `stackRules` is the\r\nproducer of the `.es-query` rule type. The `producer` is used to be used\r\nin alerting authorization but due to its complexity, it was deprecated\r\nand only used as a fallback for the `alerts` consumer. To avoid breaking\r\nchanges all feature privileges that specify access to rule types add the\r\n`alerts` consumer when configuring their alerting privileges. By moving\r\nthe `alerts` consumer to the registration of the feature we can stop\r\nrelying on the `producer`. The `producer` is not used anymore in the\r\nauthorization class. In the next PRs the `producer` will removed\r\nentirely.\r\n\r\n### Routes\r\nThe following changes were introduced to the alerting routes:\r\n\r\n- All related routes changed to be rule-type oriented and not feature ID\r\noriented.\r\n- All related routes support the `ruleTypeIds` and the `consumers`\r\nparameters for filtering. In all routes, the filters are constructed as\r\n`ruleTypeIds: ['foo'] AND consumers: ['bar'] AND authorizationFilter`.\r\nFiltering by consumers is important. In o11y for example, we do not want\r\nto show ES rule types with the `stackAlerts` consumer even if the user\r\nhas access to them.\r\n- The `/internal/rac/alerts/_feature_ids` route got deleted as it was\r\nnot used anywhere in the codebase and it was internal.\r\n\r\nAll the changes in the routes are related to internal routes and no\r\nbreaking changes are introduced.\r\n\r\n### Constants\r\nI moved the o11y and stack rule type IDs to `kbn-rule-data-utils` and\r\nexported all security solution rule type IDs from\r\n`kbn-securitysolution-rules`. I am not a fan of having a centralized\r\nplace for the rule type IDs. Ideally, consumers of the framework should\r\nspecify keywords like `observablility` (category or subcategory) or even\r\n`apm.*` and the framework should know which rule type IDs to pick up. I\r\nthink it is out of the scope of the PR, and at the moment it seems the\r\nmost straightforward way to move forward. I will try to clean up as much\r\nas possible in further iterations. If you are interested in the upcoming\r\nwork follow this issue https://github.com/elastic/kibana/issues/187202.\r\n\r\n### Other notable code changes\r\n- Change all instances of feature IDs to rule type IDs.\r\n- `isSiemRuleType`: This is a temporary helper function that is needed\r\nin places where we handle edge cases related to security solution rule\r\ntypes. Ideally, the framework should be agnostic to the rule types or\r\nconsumers. The plan is to be removed entirely in further iterations.\r\n- Rename alerting `PluginSetupContract` and `PluginStartContract` to\r\n`AlertingServerSetup` and `AlertingServerStart`. This made me touch a\r\nlot of files but I could not resist.\r\n- `filter_consumers` was mistakenly exposed to a public API. It was\r\nundocumented.\r\n- Files or functions that were not used anywhere in the codebase got\r\ndeleted.\r\n- Change the returned type of the `list` method of the\r\n`RuleTypeRegistry` from `Set<RegistryRuleType>` to `Map<string,\r\nRegistryRuleType>`.\r\n- Assertion of `KueryNode` in tests changed to an assertion of KQL using\r\n`toKqlExpression`.\r\n- Removal of `useRuleAADFields` as it is not used anywhere.\r\n\r\n## Testing\r\n\r\n> [!CAUTION]\r\n> It is very important to test all the areas of the application where\r\nrules or alerts are being used directly or indirectly. Scenarios to\r\nconsider:\r\n> - The correct rules, alerts, and aggregations on top of them are being\r\nshown as expected as a superuser.\r\n> - The correct rules, alerts, and aggregations on top of them are being\r\nshown as expected by a user with limited access to certain features.\r\n> - The changes in this PR are backward compatible with the previous\r\nusers' permissions.\r\n\r\n### Solutions\r\nPlease test and verify that:\r\n- All the rule types you own with all possible combinations of\r\npermissions both in ESS and in Serverless.\r\n- The consumers and rule types make sense when registering the features.\r\n- The consumers and rule types that are passed to the components are the\r\nintended ones.\r\n\r\n### ResponseOps\r\nThe most important changes are in the alerting authorization class, the\r\nsearch strategy, and the routes. Please test:\r\n- The rules we own with all possible combinations of permissions.\r\n- The stack alerts page and its solution filtering.\r\n- The categories filtering in the maintenance window UI.\r\n\r\n## Risks\r\n> [!WARNING]\r\n> The risks involved in this PR are related to privileges. Specifically:\r\n> - Users with no privileges can access rules and alerts they do not\r\nhave access to.\r\n> - Users with privileges cannot access rules and alerts they have\r\naccess to.\r\n>\r\n> An excessive list of integration tests is in place to ensure that the\r\nabove scenarios will not occur. In the case of a bug, we could a)\r\nrelease an energy release for serverless and b) backport the fix in ESS.\r\nGiven that this PR is intended to be merged in 8.17 we have plenty of\r\ntime to test and to minimize the chances of risks.\r\n\r\n## FQA\r\n\r\n- I noticed that a lot of routes support the `filter` parameter where we\r\ncan pass an arbitrary KQL filter. Why we do not use this to filter by\r\nthe rule type IDs and the consumers and instead we introduce new\r\ndedicated parameters?\r\n\r\nThe `filter` parameter should not be exposed in the first place. It\r\nassumes that the consumer of the API knows the underlying structure and\r\nimplementation details of the persisted storage API (SavedObject client\r\nAPI). For example, a valid filter would be\r\n`alerting.attributes.rule_type_id`. In this filter the consumer should\r\nknow a) the name of the SO b) the keyword `attributes` (storage\r\nimplementation detail) and c) the name of the attribute as it is\r\npersisted in ES (snake case instead of camel case as it is returned by\r\nthe APIs). As there is no abstraction layer between the SO and the API,\r\nit makes it very difficult to make changes in the persistent schema or\r\nthe APIs. For all the above I decided to introduce new query parameters\r\nwhere the alerting framework has total control over it.\r\n\r\n- I noticed in the code a lot of instances where the consumer is used.\r\nShould not remove any logic around consumers?\r\n\r\nThis PR is a step forward making the framework as agnostic as possible.\r\nI had to keep the scope of the PR as contained as possible. We will get\r\nthere. It needs time :).\r\n\r\n- I noticed a lot of hacks like checking if the rule type is `siem`.\r\nShould not remove the hacks?\r\n\r\nThis PR is a step forward making the framework as agnostic as possible.\r\nI had to keep the scope of the PR as contained as possible. We will get\r\nthere. It needs time :).\r\n\r\n- I hate the \"Role visibility\" dropdown. Can we remove it?\r\n\r\nI also do not like it. The goal is to remove it. Follow\r\nhttps://github.com/elastic/kibana/issues/189997.\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by: Aleh Zasypkin <aleh.zasypkin@elastic.co>\r\nCo-authored-by: Paula Borgonovi <159723434+pborgonovi@users.noreply.github.com>","sha":"a3496c9ca6d99fa301fd8ca73ad44178cdeb2955","branchLabelMapping":{"^v9.0.0$":"main","^v8.18.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Feature:Alerting","release_note:skip","Team:ResponseOps","v9.0.0","backport:prev-minor","ci:cloud-deploy","ci:cloud-persist-deployment","ci:build-serverless-image","Team:Obs AI Assistant","ci:project-deploy-observability","Team:obs-ux-infra_services","Team:obs-ux-management","apm:review","v8.18.0"],"number":183756,"url":"https://github.com/elastic/kibana/pull/183756","mergeCommit":{"message":"[ResponseOps][Alerting] Decouple feature IDs from consumers (#183756)\n\n## Summary\r\n\r\nThis PR aims to decouple the feature IDs from the `consumer` attribute\r\nof rules and alerts.\r\n\r\nTowards: https://github.com/elastic/kibana/issues/187202\r\nFixes: https://github.com/elastic/kibana/issues/181559\r\nFixes: https://github.com/elastic/kibana/issues/182435\r\n\r\n> [!NOTE] \r\n> Unfortunately, I could not break the PR into smaller pieces. The APIs\r\ncould not work anymore with feature IDs and had to convert them to use\r\nrule type IDs. Also, I took the chance and refactored crucial parts of\r\nthe authorization class that in turn affected a lot of files. Most of\r\nthe changes in the files are minimal and easy to review. The crucial\r\nchanges are in the authorization class and some alerting APIs.\r\n\r\n## Architecture\r\n\r\n### Alerting RBAC model\r\n\r\nThe Kibana security uses Elasticsearch's [application\r\nprivileges](https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-privileges.html#security-api-put-privileges).\r\nThis way Kibana can represent and store its privilege models within\r\nElasticsearch roles. To do that, Kibana security creates actions that\r\nare granted by a specific privilege. Alerting uses its own RBAC model\r\nand is built on top of the existing Kibana security model. The Alerting\r\nRBAC uses the `rule_type_id` and `consumer` attributes to define who\r\nowns the rule and the alerts procured by the rule. To connect the\r\n`rule_type_id` and `consumer` with the Kibana security actions the\r\nAlerting RBAC registers its custom actions. They are constructed as\r\n`alerting:<rule-type-id>/<feature-id>/<alerting-entity>/<operation>`.\r\nBecause to authorizate a resource an action has to be generated and\r\nbecause the action needs a valid feature ID the value of the `consumer`\r\nshould be a valid feature ID. For example, the\r\n`alerting:siem.esqlRule/siem/rule/get` action, means that a user with a\r\nrole that grants this action can get a rule of type `siem.esqlRule` with\r\nconsumer `siem`.\r\n\r\n### Problem statement\r\n\r\nAt the moment the `consumer` attribute should be a valid feature ID.\r\nThough this approach worked well so far it has its limitation.\r\nSpecifically:\r\n\r\n- Rule types cannot support more than one consumer.\r\n- To associate old rules with a new feature ID required a migration on\r\nthe rule's SOs and the alerts documents.\r\n- The API calls are feature ID-oriented and not rule-type-oriented.\r\n- The framework has to be aware of the values of the `consumer`\r\nattribute.\r\n- Feature IDs are tightly coupled with the alerting indices leading to\r\n[bugs](https://github.com/elastic/kibana/issues/179082).\r\n- Legacy consumers that are not a valid feature anymore can cause\r\n[bugs](https://github.com/elastic/kibana/issues/184595).\r\n- The framework has to be aware of legacy consumers to handle edge\r\ncases.\r\n- The framework has to be aware of specific consumers to handle edge\r\ncases.\r\n\r\n### Proposed solution\r\n\r\nThis PR aims to decouple the feature IDs from consumers. It achieves\r\nthat a) by changing the way solutions configure the alerting privileges\r\nwhen registering a feature and b) by changing the alerting actions. The\r\nschema changes as:\r\n\r\n```\r\n// Old formatting\r\nid: 'siem', <--- feature ID\r\nalerting:['siem.queryRule']\r\n\r\n// New formatting\r\nid: 'siem', <--- feature ID\r\nalerting: [{ ruleTypeId: 'siem.queryRule', consumers: ['siem'] }] <-- consumer same as the feature ID in the old formatting\r\n```\r\n\r\nThe new actions are constructed as\r\n`alerting:<rule-type-id>/<consumer>/<alerting-entity>/<operation>`. For\r\nexample `alerting:rule-type-id/my-consumer/rule/get`. The new action\r\nmeans that a user with a role that grants this action can get a rule of\r\ntype `rule-type` with consumer `my-consumer`. Changing the action\r\nstrings is not considered a breaking change as long as the user's\r\npermission works as before. In our case, this is true because the\r\nconsumer will be the same as before (feature ID), and the alerting\r\nsecurity actions will be the same. For example:\r\n\r\n**Old formatting**\r\n\r\nSchema:\r\n```\r\nid: 'logs', <--- feature ID\r\nalerting:['.es-query'] <-- rule type ID\r\n```\r\n\r\nGenerated action:\r\n\r\n```\r\nalerting:.es-query/logs/rule/get\r\n```\r\n\r\n**New formatting**\r\n\r\nSchema:\r\n```\r\nid: 'siem', <--- feature ID\r\nalerting: [{ ruleTypeId: '.es-query', consumers: ['logs'] }] <-- consumer same as the feature ID in the old formatting\r\n```\r\n\r\nGenerated action:\r\n\r\n```\r\nalerting:.es-query/logs/rule/get <--- consumer is set as logs and the action is the same as before\r\n```\r\n\r\nIn both formating the actions are the same thus breaking changes are\r\navoided.\r\n\r\n### Alerting authorization class\r\nThe alerting plugin uses and exports the alerting authorization class\r\n(`AlertingAuthorization`). The class is responsible for handling all\r\nauthorization actions related to rules and alerts. The class changed to\r\nhandle the new actions as described in the above sections. A lot of\r\nmethods were renamed, removed, and cleaned up, all method arguments\r\nconverted to be an object, and the response signature of some methods\r\nchanged. These changes affected various pieces of the code. The changes\r\nin this class are the most important in this PR especially the\r\n`_getAuthorizedRuleTypesWithAuthorizedConsumers` method which is the\r\ncornerstone of the alerting RBAC. Please review carefully.\r\n\r\n### Instantiation of the alerting authorization class\r\nThe `AlertingAuthorizationClientFactory` is used to create instances of\r\nthe `AlertingAuthorization` class. The `AlertingAuthorization` class\r\nneeds to perform async operations upon instantiation. Because JS, at the\r\nmoment, does not support async instantiation of classes the\r\n`AlertingAuthorization` class was assigning `Promise` objects to\r\nvariables that could be resolved later in other phases of the lifecycle\r\nof the class. To improve readability and make the lifecycle of the class\r\nclearer, I separated the construction of the class (initialization) from\r\nthe bootstrap process. As a result, getting the `AlertingAuthorization`\r\nclass or any client that depends on it (`getRulesClient` for example) is\r\nan async operation.\r\n\r\n### Filtering\r\nA lot of routes use the authorization class to get the authorization\r\nfilter (`getFindAuthorizationFilter`), a filter that, if applied,\r\nreturns only the rule types and consumers the user is authorized to. The\r\nmethod that returns the filter was built in a way to also support\r\nfiltering on top of the authorization filter thus coupling the\r\nauthorized filter with router filtering. I believe these two operations\r\nshould be decoupled and the filter method should return a filter that\r\ngives you all the authorized rule types. It is the responsibility of the\r\nconsumer, router in our case, to apply extra filters on top of the\r\nauthorization filter. For that reason, I made all the necessary changes\r\nto decouple them.\r\n\r\n### Legacy consumers & producer\r\nA lot of rules and alerts have been created and are still being created\r\nfrom observability with the `alerts` consumer. When the Alerting RBAC\r\nencounters a rule or alert with `alerts` as a consumer it falls back to\r\nthe `producer` of the rule type ID to construct the actions. For example\r\nif a rule with `ruleTypeId: .es-query` and `consumer: alerts` the\r\nalerting action will be constructed as\r\n`alerting:.es-query/stackAlerts/rule/get` where `stackRules` is the\r\nproducer of the `.es-query` rule type. The `producer` is used to be used\r\nin alerting authorization but due to its complexity, it was deprecated\r\nand only used as a fallback for the `alerts` consumer. To avoid breaking\r\nchanges all feature privileges that specify access to rule types add the\r\n`alerts` consumer when configuring their alerting privileges. By moving\r\nthe `alerts` consumer to the registration of the feature we can stop\r\nrelying on the `producer`. The `producer` is not used anymore in the\r\nauthorization class. In the next PRs the `producer` will removed\r\nentirely.\r\n\r\n### Routes\r\nThe following changes were introduced to the alerting routes:\r\n\r\n- All related routes changed to be rule-type oriented and not feature ID\r\noriented.\r\n- All related routes support the `ruleTypeIds` and the `consumers`\r\nparameters for filtering. In all routes, the filters are constructed as\r\n`ruleTypeIds: ['foo'] AND consumers: ['bar'] AND authorizationFilter`.\r\nFiltering by consumers is important. In o11y for example, we do not want\r\nto show ES rule types with the `stackAlerts` consumer even if the user\r\nhas access to them.\r\n- The `/internal/rac/alerts/_feature_ids` route got deleted as it was\r\nnot used anywhere in the codebase and it was internal.\r\n\r\nAll the changes in the routes are related to internal routes and no\r\nbreaking changes are introduced.\r\n\r\n### Constants\r\nI moved the o11y and stack rule type IDs to `kbn-rule-data-utils` and\r\nexported all security solution rule type IDs from\r\n`kbn-securitysolution-rules`. I am not a fan of having a centralized\r\nplace for the rule type IDs. Ideally, consumers of the framework should\r\nspecify keywords like `observablility` (category or subcategory) or even\r\n`apm.*` and the framework should know which rule type IDs to pick up. I\r\nthink it is out of the scope of the PR, and at the moment it seems the\r\nmost straightforward way to move forward. I will try to clean up as much\r\nas possible in further iterations. If you are interested in the upcoming\r\nwork follow this issue https://github.com/elastic/kibana/issues/187202.\r\n\r\n### Other notable code changes\r\n- Change all instances of feature IDs to rule type IDs.\r\n- `isSiemRuleType`: This is a temporary helper function that is needed\r\nin places where we handle edge cases related to security solution rule\r\ntypes. Ideally, the framework should be agnostic to the rule types or\r\nconsumers. The plan is to be removed entirely in further iterations.\r\n- Rename alerting `PluginSetupContract` and `PluginStartContract` to\r\n`AlertingServerSetup` and `AlertingServerStart`. This made me touch a\r\nlot of files but I could not resist.\r\n- `filter_consumers` was mistakenly exposed to a public API. It was\r\nundocumented.\r\n- Files or functions that were not used anywhere in the codebase got\r\ndeleted.\r\n- Change the returned type of the `list` method of the\r\n`RuleTypeRegistry` from `Set<RegistryRuleType>` to `Map<string,\r\nRegistryRuleType>`.\r\n- Assertion of `KueryNode` in tests changed to an assertion of KQL using\r\n`toKqlExpression`.\r\n- Removal of `useRuleAADFields` as it is not used anywhere.\r\n\r\n## Testing\r\n\r\n> [!CAUTION]\r\n> It is very important to test all the areas of the application where\r\nrules or alerts are being used directly or indirectly. Scenarios to\r\nconsider:\r\n> - The correct rules, alerts, and aggregations on top of them are being\r\nshown as expected as a superuser.\r\n> - The correct rules, alerts, and aggregations on top of them are being\r\nshown as expected by a user with limited access to certain features.\r\n> - The changes in this PR are backward compatible with the previous\r\nusers' permissions.\r\n\r\n### Solutions\r\nPlease test and verify that:\r\n- All the rule types you own with all possible combinations of\r\npermissions both in ESS and in Serverless.\r\n- The consumers and rule types make sense when registering the features.\r\n- The consumers and rule types that are passed to the components are the\r\nintended ones.\r\n\r\n### ResponseOps\r\nThe most important changes are in the alerting authorization class, the\r\nsearch strategy, and the routes. Please test:\r\n- The rules we own with all possible combinations of permissions.\r\n- The stack alerts page and its solution filtering.\r\n- The categories filtering in the maintenance window UI.\r\n\r\n## Risks\r\n> [!WARNING]\r\n> The risks involved in this PR are related to privileges. Specifically:\r\n> - Users with no privileges can access rules and alerts they do not\r\nhave access to.\r\n> - Users with privileges cannot access rules and alerts they have\r\naccess to.\r\n>\r\n> An excessive list of integration tests is in place to ensure that the\r\nabove scenarios will not occur. In the case of a bug, we could a)\r\nrelease an energy release for serverless and b) backport the fix in ESS.\r\nGiven that this PR is intended to be merged in 8.17 we have plenty of\r\ntime to test and to minimize the chances of risks.\r\n\r\n## FQA\r\n\r\n- I noticed that a lot of routes support the `filter` parameter where we\r\ncan pass an arbitrary KQL filter. Why we do not use this to filter by\r\nthe rule type IDs and the consumers and instead we introduce new\r\ndedicated parameters?\r\n\r\nThe `filter` parameter should not be exposed in the first place. It\r\nassumes that the consumer of the API knows the underlying structure and\r\nimplementation details of the persisted storage API (SavedObject client\r\nAPI). For example, a valid filter would be\r\n`alerting.attributes.rule_type_id`. In this filter the consumer should\r\nknow a) the name of the SO b) the keyword `attributes` (storage\r\nimplementation detail) and c) the name of the attribute as it is\r\npersisted in ES (snake case instead of camel case as it is returned by\r\nthe APIs). As there is no abstraction layer between the SO and the API,\r\nit makes it very difficult to make changes in the persistent schema or\r\nthe APIs. For all the above I decided to introduce new query parameters\r\nwhere the alerting framework has total control over it.\r\n\r\n- I noticed in the code a lot of instances where the consumer is used.\r\nShould not remove any logic around consumers?\r\n\r\nThis PR is a step forward making the framework as agnostic as possible.\r\nI had to keep the scope of the PR as contained as possible. We will get\r\nthere. It needs time :).\r\n\r\n- I noticed a lot of hacks like checking if the rule type is `siem`.\r\nShould not remove the hacks?\r\n\r\nThis PR is a step forward making the framework as agnostic as possible.\r\nI had to keep the scope of the PR as contained as possible. We will get\r\nthere. It needs time :).\r\n\r\n- I hate the \"Role visibility\" dropdown. Can we remove it?\r\n\r\nI also do not like it. The goal is to remove it. Follow\r\nhttps://github.com/elastic/kibana/issues/189997.\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by: Aleh Zasypkin <aleh.zasypkin@elastic.co>\r\nCo-authored-by: Paula Borgonovi <159723434+pborgonovi@users.noreply.github.com>","sha":"a3496c9ca6d99fa301fd8ca73ad44178cdeb2955"}},"sourceBranch":"main","suggestedTargetBranches":["8.x"],"targetPullRequestStates":[{"branch":"main","label":"v9.0.0","labelRegex":"^v9.0.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/183756","number":183756,"mergeCommit":{"message":"[ResponseOps][Alerting] Decouple feature IDs from consumers (#183756)\n\n## Summary\r\n\r\nThis PR aims to decouple the feature IDs from the `consumer` attribute\r\nof rules and alerts.\r\n\r\nTowards: https://github.com/elastic/kibana/issues/187202\r\nFixes: https://github.com/elastic/kibana/issues/181559\r\nFixes: https://github.com/elastic/kibana/issues/182435\r\n\r\n> [!NOTE] \r\n> Unfortunately, I could not break the PR into smaller pieces. The APIs\r\ncould not work anymore with feature IDs and had to convert them to use\r\nrule type IDs. Also, I took the chance and refactored crucial parts of\r\nthe authorization class that in turn affected a lot of files. Most of\r\nthe changes in the files are minimal and easy to review. The crucial\r\nchanges are in the authorization class and some alerting APIs.\r\n\r\n## Architecture\r\n\r\n### Alerting RBAC model\r\n\r\nThe Kibana security uses Elasticsearch's [application\r\nprivileges](https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-privileges.html#security-api-put-privileges).\r\nThis way Kibana can represent and store its privilege models within\r\nElasticsearch roles. To do that, Kibana security creates actions that\r\nare granted by a specific privilege. Alerting uses its own RBAC model\r\nand is built on top of the existing Kibana security model. The Alerting\r\nRBAC uses the `rule_type_id` and `consumer` attributes to define who\r\nowns the rule and the alerts procured by the rule. To connect the\r\n`rule_type_id` and `consumer` with the Kibana security actions the\r\nAlerting RBAC registers its custom actions. They are constructed as\r\n`alerting:<rule-type-id>/<feature-id>/<alerting-entity>/<operation>`.\r\nBecause to authorizate a resource an action has to be generated and\r\nbecause the action needs a valid feature ID the value of the `consumer`\r\nshould be a valid feature ID. For example, the\r\n`alerting:siem.esqlRule/siem/rule/get` action, means that a user with a\r\nrole that grants this action can get a rule of type `siem.esqlRule` with\r\nconsumer `siem`.\r\n\r\n### Problem statement\r\n\r\nAt the moment the `consumer` attribute should be a valid feature ID.\r\nThough this approach worked well so far it has its limitation.\r\nSpecifically:\r\n\r\n- Rule types cannot support more than one consumer.\r\n- To associate old rules with a new feature ID required a migration on\r\nthe rule's SOs and the alerts documents.\r\n- The API calls are feature ID-oriented and not rule-type-oriented.\r\n- The framework has to be aware of the values of the `consumer`\r\nattribute.\r\n- Feature IDs are tightly coupled with the alerting indices leading to\r\n[bugs](https://github.com/elastic/kibana/issues/179082).\r\n- Legacy consumers that are not a valid feature anymore can cause\r\n[bugs](https://github.com/elastic/kibana/issues/184595).\r\n- The framework has to be aware of legacy consumers to handle edge\r\ncases.\r\n- The framework has to be aware of specific consumers to handle edge\r\ncases.\r\n\r\n### Proposed solution\r\n\r\nThis PR aims to decouple the feature IDs from consumers. It achieves\r\nthat a) by changing the way solutions configure the alerting privileges\r\nwhen registering a feature and b) by changing the alerting actions. The\r\nschema changes as:\r\n\r\n```\r\n// Old formatting\r\nid: 'siem', <--- feature ID\r\nalerting:['siem.queryRule']\r\n\r\n// New formatting\r\nid: 'siem', <--- feature ID\r\nalerting: [{ ruleTypeId: 'siem.queryRule', consumers: ['siem'] }] <-- consumer same as the feature ID in the old formatting\r\n```\r\n\r\nThe new actions are constructed as\r\n`alerting:<rule-type-id>/<consumer>/<alerting-entity>/<operation>`. For\r\nexample `alerting:rule-type-id/my-consumer/rule/get`. The new action\r\nmeans that a user with a role that grants this action can get a rule of\r\ntype `rule-type` with consumer `my-consumer`. Changing the action\r\nstrings is not considered a breaking change as long as the user's\r\npermission works as before. In our case, this is true because the\r\nconsumer will be the same as before (feature ID), and the alerting\r\nsecurity actions will be the same. For example:\r\n\r\n**Old formatting**\r\n\r\nSchema:\r\n```\r\nid: 'logs', <--- feature ID\r\nalerting:['.es-query'] <-- rule type ID\r\n```\r\n\r\nGenerated action:\r\n\r\n```\r\nalerting:.es-query/logs/rule/get\r\n```\r\n\r\n**New formatting**\r\n\r\nSchema:\r\n```\r\nid: 'siem', <--- feature ID\r\nalerting: [{ ruleTypeId: '.es-query', consumers: ['logs'] }] <-- consumer same as the feature ID in the old formatting\r\n```\r\n\r\nGenerated action:\r\n\r\n```\r\nalerting:.es-query/logs/rule/get <--- consumer is set as logs and the action is the same as before\r\n```\r\n\r\nIn both formating the actions are the same thus breaking changes are\r\navoided.\r\n\r\n### Alerting authorization class\r\nThe alerting plugin uses and exports the alerting authorization class\r\n(`AlertingAuthorization`). The class is responsible for handling all\r\nauthorization actions related to rules and alerts. The class changed to\r\nhandle the new actions as described in the above sections. A lot of\r\nmethods were renamed, removed, and cleaned up, all method arguments\r\nconverted to be an object, and the response signature of some methods\r\nchanged. These changes affected various pieces of the code. The changes\r\nin this class are the most important in this PR especially the\r\n`_getAuthorizedRuleTypesWithAuthorizedConsumers` method which is the\r\ncornerstone of the alerting RBAC. Please review carefully.\r\n\r\n### Instantiation of the alerting authorization class\r\nThe `AlertingAuthorizationClientFactory` is used to create instances of\r\nthe `AlertingAuthorization` class. The `AlertingAuthorization` class\r\nneeds to perform async operations upon instantiation. Because JS, at the\r\nmoment, does not support async instantiation of classes the\r\n`AlertingAuthorization` class was assigning `Promise` objects to\r\nvariables that could be resolved later in other phases of the lifecycle\r\nof the class. To improve readability and make the lifecycle of the class\r\nclearer, I separated the construction of the class (initialization) from\r\nthe bootstrap process. As a result, getting the `AlertingAuthorization`\r\nclass or any client that depends on it (`getRulesClient` for example) is\r\nan async operation.\r\n\r\n### Filtering\r\nA lot of routes use the authorization class to get the authorization\r\nfilter (`getFindAuthorizationFilter`), a filter that, if applied,\r\nreturns only the rule types and consumers the user is authorized to. The\r\nmethod that returns the filter was built in a way to also support\r\nfiltering on top of the authorization filter thus coupling the\r\nauthorized filter with router filtering. I believe these two operations\r\nshould be decoupled and the filter method should return a filter that\r\ngives you all the authorized rule types. It is the responsibility of the\r\nconsumer, router in our case, to apply extra filters on top of the\r\nauthorization filter. For that reason, I made all the necessary changes\r\nto decouple them.\r\n\r\n### Legacy consumers & producer\r\nA lot of rules and alerts have been created and are still being created\r\nfrom observability with the `alerts` consumer. When the Alerting RBAC\r\nencounters a rule or alert with `alerts` as a consumer it falls back to\r\nthe `producer` of the rule type ID to construct the actions. For example\r\nif a rule with `ruleTypeId: .es-query` and `consumer: alerts` the\r\nalerting action will be constructed as\r\n`alerting:.es-query/stackAlerts/rule/get` where `stackRules` is the\r\nproducer of the `.es-query` rule type. The `producer` is used to be used\r\nin alerting authorization but due to its complexity, it was deprecated\r\nand only used as a fallback for the `alerts` consumer. To avoid breaking\r\nchanges all feature privileges that specify access to rule types add the\r\n`alerts` consumer when configuring their alerting privileges. By moving\r\nthe `alerts` consumer to the registration of the feature we can stop\r\nrelying on the `producer`. The `producer` is not used anymore in the\r\nauthorization class. In the next PRs the `producer` will removed\r\nentirely.\r\n\r\n### Routes\r\nThe following changes were introduced to the alerting routes:\r\n\r\n- All related routes changed to be rule-type oriented and not feature ID\r\noriented.\r\n- All related routes support the `ruleTypeIds` and the `consumers`\r\nparameters for filtering. In all routes, the filters are constructed as\r\n`ruleTypeIds: ['foo'] AND consumers: ['bar'] AND authorizationFilter`.\r\nFiltering by consumers is important. In o11y for example, we do not want\r\nto show ES rule types with the `stackAlerts` consumer even if the user\r\nhas access to them.\r\n- The `/internal/rac/alerts/_feature_ids` route got deleted as it was\r\nnot used anywhere in the codebase and it was internal.\r\n\r\nAll the changes in the routes are related to internal routes and no\r\nbreaking changes are introduced.\r\n\r\n### Constants\r\nI moved the o11y and stack rule type IDs to `kbn-rule-data-utils` and\r\nexported all security solution rule type IDs from\r\n`kbn-securitysolution-rules`. I am not a fan of having a centralized\r\nplace for the rule type IDs. Ideally, consumers of the framework should\r\nspecify keywords like `observablility` (category or subcategory) or even\r\n`apm.*` and the framework should know which rule type IDs to pick up. I\r\nthink it is out of the scope of the PR, and at the moment it seems the\r\nmost straightforward way to move forward. I will try to clean up as much\r\nas possible in further iterations. If you are interested in the upcoming\r\nwork follow this issue https://github.com/elastic/kibana/issues/187202.\r\n\r\n### Other notable code changes\r\n- Change all instances of feature IDs to rule type IDs.\r\n- `isSiemRuleType`: This is a temporary helper function that is needed\r\nin places where we handle edge cases related to security solution rule\r\ntypes. Ideally, the framework should be agnostic to the rule types or\r\nconsumers. The plan is to be removed entirely in further iterations.\r\n- Rename alerting `PluginSetupContract` and `PluginStartContract` to\r\n`AlertingServerSetup` and `AlertingServerStart`. This made me touch a\r\nlot of files but I could not resist.\r\n- `filter_consumers` was mistakenly exposed to a public API. It was\r\nundocumented.\r\n- Files or functions that were not used anywhere in the codebase got\r\ndeleted.\r\n- Change the returned type of the `list` method of the\r\n`RuleTypeRegistry` from `Set<RegistryRuleType>` to `Map<string,\r\nRegistryRuleType>`.\r\n- Assertion of `KueryNode` in tests changed to an assertion of KQL using\r\n`toKqlExpression`.\r\n- Removal of `useRuleAADFields` as it is not used anywhere.\r\n\r\n## Testing\r\n\r\n> [!CAUTION]\r\n> It is very important to test all the areas of the application where\r\nrules or alerts are being used directly or indirectly. Scenarios to\r\nconsider:\r\n> - The correct rules, alerts, and aggregations on top of them are being\r\nshown as expected as a superuser.\r\n> - The correct rules, alerts, and aggregations on top of them are being\r\nshown as expected by a user with limited access to certain features.\r\n> - The changes in this PR are backward compatible with the previous\r\nusers' permissions.\r\n\r\n### Solutions\r\nPlease test and verify that:\r\n- All the rule types you own with all possible combinations of\r\npermissions both in ESS and in Serverless.\r\n- The consumers and rule types make sense when registering the features.\r\n- The consumers and rule types that are passed to the components are the\r\nintended ones.\r\n\r\n### ResponseOps\r\nThe most important changes are in the alerting authorization class, the\r\nsearch strategy, and the routes. Please test:\r\n- The rules we own with all possible combinations of permissions.\r\n- The stack alerts page and its solution filtering.\r\n- The categories filtering in the maintenance window UI.\r\n\r\n## Risks\r\n> [!WARNING]\r\n> The risks involved in this PR are related to privileges. Specifically:\r\n> - Users with no privileges can access rules and alerts they do not\r\nhave access to.\r\n> - Users with privileges cannot access rules and alerts they have\r\naccess to.\r\n>\r\n> An excessive list of integration tests is in place to ensure that the\r\nabove scenarios will not occur. In the case of a bug, we could a)\r\nrelease an energy release for serverless and b) backport the fix in ESS.\r\nGiven that this PR is intended to be merged in 8.17 we have plenty of\r\ntime to test and to minimize the chances of risks.\r\n\r\n## FQA\r\n\r\n- I noticed that a lot of routes support the `filter` parameter where we\r\ncan pass an arbitrary KQL filter. Why we do not use this to filter by\r\nthe rule type IDs and the consumers and instead we introduce new\r\ndedicated parameters?\r\n\r\nThe `filter` parameter should not be exposed in the first place. It\r\nassumes that the consumer of the API knows the underlying structure and\r\nimplementation details of the persisted storage API (SavedObject client\r\nAPI). For example, a valid filter would be\r\n`alerting.attributes.rule_type_id`. In this filter the consumer should\r\nknow a) the name of the SO b) the keyword `attributes` (storage\r\nimplementation detail) and c) the name of the attribute as it is\r\npersisted in ES (snake case instead of camel case as it is returned by\r\nthe APIs). As there is no abstraction layer between the SO and the API,\r\nit makes it very difficult to make changes in the persistent schema or\r\nthe APIs. For all the above I decided to introduce new query parameters\r\nwhere the alerting framework has total control over it.\r\n\r\n- I noticed in the code a lot of instances where the consumer is used.\r\nShould not remove any logic around consumers?\r\n\r\nThis PR is a step forward making the framework as agnostic as possible.\r\nI had to keep the scope of the PR as contained as possible. We will get\r\nthere. It needs time :).\r\n\r\n- I noticed a lot of hacks like checking if the rule type is `siem`.\r\nShould not remove the hacks?\r\n\r\nThis PR is a step forward making the framework as agnostic as possible.\r\nI had to keep the scope of the PR as contained as possible. We will get\r\nthere. It needs time :).\r\n\r\n- I hate the \"Role visibility\" dropdown. Can we remove it?\r\n\r\nI also do not like it. The goal is to remove it. Follow\r\nhttps://github.com/elastic/kibana/issues/189997.\r\n\r\n---------\r\n\r\nCo-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>\r\nCo-authored-by: Aleh Zasypkin <aleh.zasypkin@elastic.co>\r\nCo-authored-by: Paula Borgonovi <159723434+pborgonovi@users.noreply.github.com>","sha":"a3496c9ca6d99fa301fd8ca73ad44178cdeb2955"}},{"branch":"8.x","label":"v8.18.0","labelRegex":"^v8.18.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}] BACKPORT-->
This commit is contained in:
parent
b05111c955
commit
2970f7901a
530 changed files with 26655 additions and 11046 deletions
|
@ -8,7 +8,6 @@
|
|||
*/
|
||||
|
||||
import type { IEsSearchRequest, IEsSearchResponse } from '@kbn/search-types';
|
||||
import type { ValidFeatureId } from '@kbn/rule-data-utils';
|
||||
import type {
|
||||
MappingRuntimeFields,
|
||||
QueryDslFieldAndFormat,
|
||||
|
@ -18,7 +17,8 @@ import type {
|
|||
import type { Alert } from './alert_type';
|
||||
|
||||
export type RuleRegistrySearchRequest = IEsSearchRequest & {
|
||||
featureIds: ValidFeatureId[];
|
||||
ruleTypeIds: string[];
|
||||
consumers?: string[];
|
||||
fields?: QueryDslFieldAndFormat[];
|
||||
query?: Pick<QueryDslQueryContainer, 'bool' | 'ids'>;
|
||||
sort?: SortCombinations[];
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* 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".
|
||||
*/
|
||||
// ---------------------------------- WARNING ----------------------------------
|
||||
// this file was generated, and should not be edited by hand
|
||||
// ---------------------------------- WARNING ----------------------------------
|
||||
import * as rt from 'io-ts';
|
||||
import { Either } from 'fp-ts/lib/Either';
|
||||
import { AlertSchema } from './alert_schema';
|
||||
import { EcsSchema } from './ecs_schema';
|
||||
const ISO_DATE_PATTERN = /^d{4}-d{2}-d{2}Td{2}:d{2}:d{2}.d{3}Z$/;
|
||||
export const IsoDateString = new rt.Type<string, string, unknown>(
|
||||
'IsoDateString',
|
||||
rt.string.is,
|
||||
(input, context): Either<rt.Errors, string> => {
|
||||
if (typeof input === 'string' && ISO_DATE_PATTERN.test(input)) {
|
||||
return rt.success(input);
|
||||
} else {
|
||||
return rt.failure(input, context);
|
||||
}
|
||||
},
|
||||
rt.identity
|
||||
);
|
||||
export type IsoDateStringC = typeof IsoDateString;
|
||||
export const schemaUnknown = rt.unknown;
|
||||
export const schemaUnknownArray = rt.array(rt.unknown);
|
||||
export const schemaString = rt.string;
|
||||
export const schemaStringArray = rt.array(schemaString);
|
||||
export const schemaNumber = rt.number;
|
||||
export const schemaNumberArray = rt.array(schemaNumber);
|
||||
export const schemaDate = rt.union([IsoDateString, schemaNumber]);
|
||||
export const schemaDateArray = rt.array(schemaDate);
|
||||
export const schemaDateRange = rt.partial({
|
||||
gte: schemaDate,
|
||||
lte: schemaDate,
|
||||
});
|
||||
export const schemaDateRangeArray = rt.array(schemaDateRange);
|
||||
export const schemaStringOrNumber = rt.union([schemaString, schemaNumber]);
|
||||
export const schemaStringOrNumberArray = rt.array(schemaStringOrNumber);
|
||||
export const schemaBoolean = rt.boolean;
|
||||
export const schemaBooleanArray = rt.array(schemaBoolean);
|
||||
const schemaGeoPointCoords = rt.type({
|
||||
type: schemaString,
|
||||
coordinates: schemaNumberArray,
|
||||
});
|
||||
const schemaGeoPointString = schemaString;
|
||||
const schemaGeoPointLatLon = rt.type({
|
||||
lat: schemaNumber,
|
||||
lon: schemaNumber,
|
||||
});
|
||||
const schemaGeoPointLocation = rt.type({
|
||||
location: schemaNumberArray,
|
||||
});
|
||||
const schemaGeoPointLocationString = rt.type({
|
||||
location: schemaString,
|
||||
});
|
||||
export const schemaGeoPoint = rt.union([
|
||||
schemaGeoPointCoords,
|
||||
schemaGeoPointString,
|
||||
schemaGeoPointLatLon,
|
||||
schemaGeoPointLocation,
|
||||
schemaGeoPointLocationString,
|
||||
]);
|
||||
export const schemaGeoPointArray = rt.array(schemaGeoPoint);
|
||||
// prettier-ignore
|
||||
const ObservabilityThresholdAlertRequired = rt.type({
|
||||
});
|
||||
// prettier-ignore
|
||||
const ObservabilityThresholdAlertOptional = rt.partial({
|
||||
'kibana.alert.context': schemaUnknown,
|
||||
'kibana.alert.evaluation.threshold': schemaStringOrNumber,
|
||||
'kibana.alert.evaluation.value': schemaStringOrNumber,
|
||||
'kibana.alert.evaluation.values': schemaStringOrNumberArray,
|
||||
'kibana.alert.group': rt.array(
|
||||
rt.partial({
|
||||
field: schemaStringArray,
|
||||
value: schemaStringArray,
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
// prettier-ignore
|
||||
export const ObservabilityThresholdAlertSchema = rt.intersection([ObservabilityThresholdAlertRequired, ObservabilityThresholdAlertOptional, AlertSchema, EcsSchema]);
|
||||
// prettier-ignore
|
||||
export type ObservabilityThresholdAlert = rt.TypeOf<typeof ObservabilityThresholdAlertSchema>;
|
|
@ -23,7 +23,8 @@ import { groupingSearchResponse } from '../mocks/grouping_query.mock';
|
|||
import { useAlertsGroupingState } from '../contexts/alerts_grouping_context';
|
||||
import { I18nProvider } from '@kbn/i18n-react';
|
||||
import {
|
||||
mockFeatureIds,
|
||||
mockRuleTypeIds,
|
||||
mockConsumers,
|
||||
mockDate,
|
||||
mockGroupingProps,
|
||||
mockGroupingId,
|
||||
|
@ -146,7 +147,8 @@ describe('AlertsGrouping', () => {
|
|||
expect.objectContaining({
|
||||
params: {
|
||||
aggregations: {},
|
||||
featureIds: mockFeatureIds,
|
||||
ruleTypeIds: mockRuleTypeIds,
|
||||
consumers: mockConsumers,
|
||||
groupByField: 'kibana.alert.rule.name',
|
||||
filters: [
|
||||
{
|
||||
|
|
|
@ -66,7 +66,7 @@ const AlertsGroupingInternal = <T extends BaseAlertsGroupAggregations>(
|
|||
const {
|
||||
groupingId,
|
||||
services,
|
||||
featureIds,
|
||||
ruleTypeIds,
|
||||
defaultGroupingOptions,
|
||||
defaultFilters,
|
||||
globalFilters,
|
||||
|
@ -79,7 +79,7 @@ const AlertsGroupingInternal = <T extends BaseAlertsGroupAggregations>(
|
|||
const { grouping, updateGrouping } = useAlertsGroupingState(groupingId);
|
||||
|
||||
const { dataView } = useAlertsDataView({
|
||||
featureIds,
|
||||
ruleTypeIds,
|
||||
dataViewsService: dataViews,
|
||||
http,
|
||||
toasts: notifications.toasts,
|
||||
|
@ -252,7 +252,7 @@ const typedMemo: <T>(c: T) => T = memo;
|
|||
*
|
||||
* return (
|
||||
* <AlertsGrouping<YourAggregationsType>
|
||||
* featureIds={[...]}
|
||||
* ruleTypeIds={[...]}
|
||||
* globalQuery={{ query: ..., language: 'kql' }}
|
||||
* globalFilters={...}
|
||||
* from={...}
|
||||
|
|
|
@ -55,6 +55,10 @@ const mockGroupingLevelProps: Omit<AlertsGroupingLevelProps, 'children'> = {
|
|||
describe('AlertsGroupingLevel', () => {
|
||||
let buildEsQuerySpy: jest.SpyInstance;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
beforeAll(() => {
|
||||
buildEsQuerySpy = jest.spyOn(buildEsQueryModule, 'buildEsQuery');
|
||||
});
|
||||
|
@ -119,4 +123,58 @@ describe('AlertsGroupingLevel', () => {
|
|||
Object.keys(groupingSearchResponse.aggregations)
|
||||
);
|
||||
});
|
||||
|
||||
it('should calls useGetAlertsGroupAggregationsQuery with correct props', () => {
|
||||
render(
|
||||
<AlertsGroupingLevel {...mockGroupingLevelProps}>
|
||||
{() => <span data-test-subj="grouping-level" />}
|
||||
</AlertsGroupingLevel>
|
||||
);
|
||||
|
||||
expect(mockUseGetAlertsGroupAggregationsQuery.mock.calls).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
"enabled": true,
|
||||
"http": Object {
|
||||
"get": [MockFunction],
|
||||
},
|
||||
"params": Object {
|
||||
"aggregations": Object {},
|
||||
"consumers": Array [
|
||||
"stackAlerts",
|
||||
],
|
||||
"filters": Array [
|
||||
Object {
|
||||
"bool": Object {
|
||||
"filter": Array [],
|
||||
"must": Array [],
|
||||
"must_not": Array [],
|
||||
"should": Array [],
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"range": Object {
|
||||
"kibana.alert.time_range": Object {
|
||||
"gte": "2020-07-07T08:20:18.966Z",
|
||||
"lte": "2020-07-08T08:20:18.966Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
"groupByField": "selectedGroup",
|
||||
"pageIndex": 0,
|
||||
"pageSize": 10,
|
||||
"ruleTypeIds": Array [
|
||||
".es-query",
|
||||
],
|
||||
},
|
||||
"toasts": Object {
|
||||
"addDanger": [MockFunction],
|
||||
},
|
||||
},
|
||||
],
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -46,7 +46,8 @@ const DEFAULT_FILTERS: Filter[] = [];
|
|||
const typedMemo: <T>(c: T) => T = memo;
|
||||
export const AlertsGroupingLevel = typedMemo(
|
||||
<T extends BaseAlertsGroupAggregations>({
|
||||
featureIds,
|
||||
ruleTypeIds,
|
||||
consumers,
|
||||
defaultFilters = DEFAULT_FILTERS,
|
||||
from,
|
||||
getGrouping,
|
||||
|
@ -86,7 +87,8 @@ export const AlertsGroupingLevel = typedMemo(
|
|||
|
||||
const aggregationsQuery = useMemo<UseGetAlertsGroupAggregationsQueryProps['params']>(() => {
|
||||
return {
|
||||
featureIds,
|
||||
ruleTypeIds,
|
||||
consumers,
|
||||
groupByField: selectedGroup,
|
||||
aggregations: getAggregationsByGroupingField(selectedGroup)?.reduce(
|
||||
(acc, val) => Object.assign(acc, val),
|
||||
|
@ -107,12 +109,13 @@ export const AlertsGroupingLevel = typedMemo(
|
|||
pageSize,
|
||||
};
|
||||
}, [
|
||||
featureIds,
|
||||
consumers,
|
||||
filters,
|
||||
from,
|
||||
getAggregationsByGroupingField,
|
||||
pageIndex,
|
||||
pageSize,
|
||||
ruleTypeIds,
|
||||
selectedGroup,
|
||||
to,
|
||||
]);
|
||||
|
|
|
@ -8,12 +8,12 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import { AlertsGroupingProps } from '../types';
|
||||
|
||||
export const mockGroupingId = 'test';
|
||||
|
||||
export const mockFeatureIds = [AlertConsumers.STACK_ALERTS];
|
||||
export const mockRuleTypeIds = ['.es-query'];
|
||||
export const mockConsumers = ['stackAlerts'];
|
||||
|
||||
export const mockDate = {
|
||||
from: '2020-07-07T08:20:18.966Z',
|
||||
|
@ -30,7 +30,8 @@ export const mockOptions = [
|
|||
export const mockGroupingProps: Omit<AlertsGroupingProps, 'children'> = {
|
||||
...mockDate,
|
||||
groupingId: mockGroupingId,
|
||||
featureIds: mockFeatureIds,
|
||||
ruleTypeIds: mockRuleTypeIds,
|
||||
consumers: mockConsumers,
|
||||
defaultGroupingOptions: mockOptions,
|
||||
getAggregationsByGroupingField: () => [],
|
||||
getGroupStats: () => [{ title: 'Stat', component: <span /> }],
|
||||
|
|
|
@ -11,12 +11,12 @@ export const getQuery = ({
|
|||
selectedGroup,
|
||||
uniqueValue,
|
||||
timeRange,
|
||||
featureIds,
|
||||
ruleTypeIds,
|
||||
}: {
|
||||
selectedGroup: string;
|
||||
uniqueValue: string;
|
||||
timeRange: { from: string; to: string };
|
||||
featureIds: string[];
|
||||
ruleTypeIds: string[];
|
||||
}) => ({
|
||||
_source: false,
|
||||
aggs: {
|
||||
|
@ -52,7 +52,7 @@ export const getQuery = ({
|
|||
},
|
||||
},
|
||||
},
|
||||
feature_ids: featureIds,
|
||||
rule_type_ids: ruleTypeIds,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
*/
|
||||
|
||||
import type { Filter, Query } from '@kbn/es-query';
|
||||
import { ValidFeatureId } from '@kbn/rule-data-utils';
|
||||
import type { NotificationsStart } from '@kbn/core-notifications-browser';
|
||||
import type { DataViewsServicePublic } from '@kbn/data-views-plugin/public/types';
|
||||
import type { HttpSetup } from '@kbn/core-http-browser';
|
||||
|
@ -63,9 +62,13 @@ export interface AlertsGroupingProps<
|
|||
*/
|
||||
defaultGroupingOptions: GroupOption[];
|
||||
/**
|
||||
* The alerting feature ids this grouping covers
|
||||
* The alerting rule type ids this grouping covers
|
||||
*/
|
||||
featureIds: ValidFeatureId[];
|
||||
ruleTypeIds: string[];
|
||||
/**
|
||||
* The alerting consumers this grouping covers
|
||||
*/
|
||||
consumers?: string[];
|
||||
/**
|
||||
* Time filter start
|
||||
*/
|
||||
|
|
|
@ -289,5 +289,6 @@ function getAlertType(actionVariables: ActionVariables): RuleType {
|
|||
producer: ALERTING_FEATURE_ID,
|
||||
minimumLicenseRequired: 'basic',
|
||||
enabledInLicense: true,
|
||||
category: 'my-category',
|
||||
};
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@
|
|||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { AlertFilterControls, AlertFilterControlsProps } from './alert_filter_controls';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import { DEFAULT_CONTROLS } from './constants';
|
||||
import { useAlertsDataView } from '../common/hooks/use_alerts_data_view';
|
||||
import { FilterGroup } from './filter_group';
|
||||
|
@ -56,7 +55,7 @@ const ControlGroupRenderer = (() => (
|
|||
|
||||
describe('AlertFilterControls', () => {
|
||||
const props: AlertFilterControlsProps = {
|
||||
featureIds: [AlertConsumers.STACK_ALERTS],
|
||||
ruleTypeIds: ['.es-query'],
|
||||
defaultControls: DEFAULT_CONTROLS,
|
||||
dataViewSpec: {
|
||||
id: 'alerts-filters-dv',
|
||||
|
|
|
@ -12,7 +12,6 @@ import React, { useCallback, useEffect, useState } from 'react';
|
|||
import type { Filter } from '@kbn/es-query';
|
||||
import { EuiFlexItem } from '@elastic/eui';
|
||||
import type { DataViewSpec, DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import { HttpStart } from '@kbn/core-http-browser';
|
||||
import { NotificationsStart } from '@kbn/core-notifications-browser';
|
||||
import type { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
|
@ -24,12 +23,12 @@ import { FilterControlConfig } from './types';
|
|||
|
||||
export type AlertFilterControlsProps = Omit<
|
||||
ComponentProps<typeof FilterGroup>,
|
||||
'dataViewId' | 'defaultControls' | 'featureIds' | 'Storage'
|
||||
'dataViewId' | 'defaultControls' | 'ruleTypeIds' | 'Storage'
|
||||
> & {
|
||||
/**
|
||||
* The feature ids used to get the correct alert data view(s)
|
||||
* The rule type ids used to get the correct alert data view(s)
|
||||
*/
|
||||
featureIds?: AlertConsumers[];
|
||||
ruleTypeIds?: string[];
|
||||
/**
|
||||
* An array of default control configurations
|
||||
*/
|
||||
|
@ -57,7 +56,7 @@ export type AlertFilterControlsProps = Omit<
|
|||
*
|
||||
* <AlertFilterControls
|
||||
* // Data view configuration
|
||||
* featureIds={[AlertConsumers.STACK_ALERTS]}
|
||||
* ruleTypeIds={['.es-query']}
|
||||
* dataViewSpec={{
|
||||
* id: 'unified-alerts-dv',
|
||||
* title: '.alerts-*',
|
||||
|
@ -82,7 +81,7 @@ export type AlertFilterControlsProps = Omit<
|
|||
*/
|
||||
export const AlertFilterControls = (props: AlertFilterControlsProps) => {
|
||||
const {
|
||||
featureIds = [AlertConsumers.STACK_ALERTS],
|
||||
ruleTypeIds = [],
|
||||
defaultControls = DEFAULT_CONTROLS,
|
||||
dataViewSpec,
|
||||
onFiltersChange,
|
||||
|
@ -96,7 +95,7 @@ export const AlertFilterControls = (props: AlertFilterControlsProps) => {
|
|||
} = props;
|
||||
const [loadingPageFilters, setLoadingPageFilters] = useState(true);
|
||||
const { dataView, isLoading: isLoadingDataView } = useAlertsDataView({
|
||||
featureIds,
|
||||
ruleTypeIds,
|
||||
dataViewsService: dataViews,
|
||||
http,
|
||||
toasts,
|
||||
|
@ -156,7 +155,7 @@ export const AlertFilterControls = (props: AlertFilterControlsProps) => {
|
|||
<FilterGroup
|
||||
dataViewId={dataViewSpec?.id || null}
|
||||
onFiltersChange={handleFilterChanges}
|
||||
featureIds={featureIds}
|
||||
ruleTypeIds={ruleTypeIds}
|
||||
{...restFilterItemGroupProps}
|
||||
Storage={storage}
|
||||
defaultControls={defaultControls}
|
||||
|
|
|
@ -30,13 +30,12 @@ import {
|
|||
getMockedControlGroupRenderer,
|
||||
} from './mocks/control_group_renderer';
|
||||
import { URL_PARAM_ARRAY_EXCEPTION_MSG } from './translations';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import type { FilterGroupProps } from './types';
|
||||
|
||||
const featureIds = [AlertConsumers.STACK_ALERTS];
|
||||
const ruleTypeIds = ['.es-query'];
|
||||
const spaceId = 'test-space-id';
|
||||
const LOCAL_STORAGE_KEY = `${featureIds.join(',')}.${spaceId}.${URL_PARAM_KEY}`;
|
||||
const LOCAL_STORAGE_KEY = `${ruleTypeIds.join(',')}.${spaceId}.${URL_PARAM_KEY}`;
|
||||
|
||||
const controlGroupMock = getControlGroupMock();
|
||||
|
||||
|
@ -63,7 +62,7 @@ const TestComponent: FC<Partial<FilterGroupProps>> = (props) => {
|
|||
<FilterGroup
|
||||
spaceId={spaceId}
|
||||
dataViewId="alert-filters-test-dv"
|
||||
featureIds={featureIds}
|
||||
ruleTypeIds={ruleTypeIds}
|
||||
defaultControls={[
|
||||
...DEFAULT_CONTROLS,
|
||||
{
|
||||
|
|
|
@ -44,7 +44,6 @@ import { URL_PARAM_ARRAY_EXCEPTION_MSG } from './translations';
|
|||
|
||||
export const FilterGroup = (props: PropsWithChildren<FilterGroupProps>) => {
|
||||
const {
|
||||
featureIds,
|
||||
dataViewId,
|
||||
onFiltersChange,
|
||||
timeRange,
|
||||
|
@ -59,6 +58,7 @@ export const FilterGroup = (props: PropsWithChildren<FilterGroupProps>) => {
|
|||
maxControls = Infinity,
|
||||
ControlGroupRenderer,
|
||||
Storage,
|
||||
ruleTypeIds,
|
||||
storageKey,
|
||||
} = props;
|
||||
|
||||
|
@ -80,8 +80,8 @@ export const FilterGroup = (props: PropsWithChildren<FilterGroupProps>) => {
|
|||
const [controlGroup, setControlGroup] = useState<ControlGroupRendererApi>();
|
||||
|
||||
const localStoragePageFilterKey = useMemo(
|
||||
() => storageKey ?? `${featureIds.join(',')}.${spaceId}.${URL_PARAM_KEY}`,
|
||||
[featureIds, spaceId, storageKey]
|
||||
() => storageKey ?? `${ruleTypeIds.join(',')}.${spaceId}.${URL_PARAM_KEY}`,
|
||||
[ruleTypeIds, spaceId, storageKey]
|
||||
);
|
||||
|
||||
const currentFiltersRef = useRef<Filter[]>();
|
||||
|
|
|
@ -15,7 +15,6 @@ import type {
|
|||
ControlGroupRendererApi,
|
||||
} from '@kbn/controls-plugin/public';
|
||||
import type { Storage } from '@kbn/kibana-utils-plugin/public';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
|
||||
export type FilterUrlFormat = Record<
|
||||
string,
|
||||
|
@ -46,7 +45,7 @@ export interface FilterGroupProps extends Pick<ControlGroupRuntimeState, 'chaini
|
|||
|
||||
spaceId?: string;
|
||||
dataViewId: string | null;
|
||||
featureIds: AlertConsumers[];
|
||||
ruleTypeIds: string[];
|
||||
/**
|
||||
* Filters changed callback
|
||||
*/
|
||||
|
|
|
@ -12,7 +12,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|||
import { dataPluginMock } from '@kbn/data-plugin/public/mocks';
|
||||
import { Filter, FilterStateStore } from '@kbn/es-query';
|
||||
import { ToastsStart } from '@kbn/core-notifications-browser';
|
||||
import { useLoadRuleTypesQuery, useRuleAADFields, useAlertsDataView } from '../common/hooks';
|
||||
import { useAlertsDataView } from '../common/hooks';
|
||||
import { AlertsSearchBar } from '.';
|
||||
import { HttpStart } from '@kbn/core-http-browser';
|
||||
|
||||
|
@ -35,25 +35,6 @@ jest.mocked(useAlertsDataView).mockReturnValue({
|
|||
},
|
||||
});
|
||||
|
||||
jest.mocked(useLoadRuleTypesQuery).mockReturnValue({
|
||||
ruleTypesState: {
|
||||
isInitialLoad: false,
|
||||
data: new Map(),
|
||||
isLoading: false,
|
||||
error: null,
|
||||
},
|
||||
authorizedToReadAnyRules: false,
|
||||
hasAnyAuthorizedRuleType: false,
|
||||
authorizedRuleTypes: [],
|
||||
authorizedToCreateAnyRules: false,
|
||||
isSuccess: false,
|
||||
});
|
||||
|
||||
jest.mocked(useRuleAADFields).mockReturnValue({
|
||||
aadFields: [],
|
||||
loading: false,
|
||||
});
|
||||
|
||||
const unifiedSearchBarMock = jest.fn().mockImplementation((props) => (
|
||||
<button
|
||||
data-test-subj="querySubmitButton"
|
||||
|
@ -83,7 +64,6 @@ describe('AlertsSearchBar', () => {
|
|||
onQuerySubmit={jest.fn()}
|
||||
onFiltersUpdated={jest.fn()}
|
||||
appName={'test'}
|
||||
featureIds={['observability', 'stackAlerts']}
|
||||
unifiedSearchBar={unifiedSearchBarMock}
|
||||
toasts={toastsMock}
|
||||
http={httpMock}
|
||||
|
@ -108,7 +88,6 @@ describe('AlertsSearchBar', () => {
|
|||
http={httpMock}
|
||||
dataService={mockDataPlugin}
|
||||
appName={'test'}
|
||||
featureIds={['observability', 'stackAlerts']}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -157,7 +136,6 @@ describe('AlertsSearchBar', () => {
|
|||
http={httpMock}
|
||||
dataService={mockDataPlugin}
|
||||
appName={'test'}
|
||||
featureIds={['observability', 'stackAlerts']}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -168,4 +146,151 @@ describe('AlertsSearchBar', () => {
|
|||
expect(mockDataPlugin.query.filterManager.setFilters).toHaveBeenCalledWith(filters);
|
||||
});
|
||||
});
|
||||
|
||||
it('calls the unifiedSearchBar correctly for security rule types', async () => {
|
||||
render(
|
||||
<AlertsSearchBar
|
||||
rangeFrom="now/d"
|
||||
rangeTo="now/d"
|
||||
query=""
|
||||
onQuerySubmit={jest.fn()}
|
||||
toasts={toastsMock}
|
||||
http={httpMock}
|
||||
dataService={mockDataPlugin}
|
||||
appName={'test'}
|
||||
onFiltersUpdated={jest.fn()}
|
||||
unifiedSearchBar={unifiedSearchBarMock}
|
||||
ruleTypeIds={['siem.esqlRuleType', '.esQuery']}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(unifiedSearchBarMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
suggestionsAbstraction: undefined,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('calls the unifiedSearchBar correctly for NON security rule types', async () => {
|
||||
render(
|
||||
<AlertsSearchBar
|
||||
rangeFrom="now/d"
|
||||
rangeTo="now/d"
|
||||
query=""
|
||||
onQuerySubmit={jest.fn()}
|
||||
toasts={toastsMock}
|
||||
http={httpMock}
|
||||
dataService={mockDataPlugin}
|
||||
appName={'test'}
|
||||
onFiltersUpdated={jest.fn()}
|
||||
unifiedSearchBar={unifiedSearchBarMock}
|
||||
ruleTypeIds={['.esQuery']}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(unifiedSearchBarMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
suggestionsAbstraction: { type: 'alerts', fields: {} },
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('calls the unifiedSearchBar with correct index patters', async () => {
|
||||
render(
|
||||
<AlertsSearchBar
|
||||
rangeFrom="now/d"
|
||||
rangeTo="now/d"
|
||||
query=""
|
||||
onQuerySubmit={jest.fn()}
|
||||
toasts={toastsMock}
|
||||
http={httpMock}
|
||||
dataService={mockDataPlugin}
|
||||
appName={'test'}
|
||||
onFiltersUpdated={jest.fn()}
|
||||
unifiedSearchBar={unifiedSearchBarMock}
|
||||
ruleTypeIds={['.esQuery', 'apm.anomaly']}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(unifiedSearchBarMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
indexPatterns: [
|
||||
{
|
||||
fields: [
|
||||
{ aggregatable: true, name: 'event.action', searchable: true, type: 'string' },
|
||||
],
|
||||
title: '.esQuery,apm.anomaly',
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('calls the unifiedSearchBar with correct index patters without rule types', async () => {
|
||||
render(
|
||||
<AlertsSearchBar
|
||||
rangeFrom="now/d"
|
||||
rangeTo="now/d"
|
||||
query=""
|
||||
onQuerySubmit={jest.fn()}
|
||||
toasts={toastsMock}
|
||||
http={httpMock}
|
||||
dataService={mockDataPlugin}
|
||||
appName={'test'}
|
||||
onFiltersUpdated={jest.fn()}
|
||||
unifiedSearchBar={unifiedSearchBarMock}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(unifiedSearchBarMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
indexPatterns: [
|
||||
{
|
||||
fields: [
|
||||
{ aggregatable: true, name: 'event.action', searchable: true, type: 'string' },
|
||||
],
|
||||
title: '.alerts-*',
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('calls the unifiedSearchBar with correct index patters without data views', async () => {
|
||||
jest.mocked(useAlertsDataView).mockReturnValue({
|
||||
isLoading: false,
|
||||
dataView: undefined,
|
||||
});
|
||||
|
||||
render(
|
||||
<AlertsSearchBar
|
||||
rangeFrom="now/d"
|
||||
rangeTo="now/d"
|
||||
query=""
|
||||
onQuerySubmit={jest.fn()}
|
||||
toasts={toastsMock}
|
||||
http={httpMock}
|
||||
dataService={mockDataPlugin}
|
||||
appName={'test'}
|
||||
onFiltersUpdated={jest.fn()}
|
||||
unifiedSearchBar={unifiedSearchBarMock}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(unifiedSearchBarMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
indexPatterns: [],
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,22 +10,20 @@
|
|||
import { useCallback, useMemo, useState } from 'react';
|
||||
import type { Query, TimeRange } from '@kbn/es-query';
|
||||
import type { SuggestionsAbstraction } from '@kbn/unified-search-plugin/public/typeahead/suggestions_component';
|
||||
import { AlertConsumers, ValidFeatureId } from '@kbn/rule-data-utils';
|
||||
import { isSiemRuleType } from '@kbn/rule-data-utils';
|
||||
import { NO_INDEX_PATTERNS } from './constants';
|
||||
import { SEARCH_BAR_PLACEHOLDER } from './translations';
|
||||
import type { AlertsSearchBarProps, QueryLanguageType } from './types';
|
||||
import { useLoadRuleTypesQuery, useAlertsDataView, useRuleAADFields } from '../common/hooks';
|
||||
import { useAlertsDataView } from '../common/hooks';
|
||||
|
||||
export type { AlertsSearchBarProps } from './types';
|
||||
|
||||
const SA_ALERTS = { type: 'alerts', fields: {} } as SuggestionsAbstraction;
|
||||
const EMPTY_FEATURE_IDS: ValidFeatureId[] = [];
|
||||
|
||||
export const AlertsSearchBar = ({
|
||||
appName,
|
||||
disableQueryLanguageSwitcher = false,
|
||||
featureIds = EMPTY_FEATURE_IDS,
|
||||
ruleTypeId,
|
||||
ruleTypeIds = [],
|
||||
query,
|
||||
filters,
|
||||
onQueryChange,
|
||||
|
@ -45,39 +43,24 @@ export const AlertsSearchBar = ({
|
|||
}: AlertsSearchBarProps) => {
|
||||
const [queryLanguage, setQueryLanguage] = useState<QueryLanguageType>('kuery');
|
||||
const { dataView } = useAlertsDataView({
|
||||
featureIds,
|
||||
ruleTypeIds,
|
||||
http,
|
||||
toasts,
|
||||
dataViewsService: dataService.dataViews,
|
||||
});
|
||||
const { aadFields, loading: fieldsLoading } = useRuleAADFields({
|
||||
ruleTypeId,
|
||||
http,
|
||||
toasts,
|
||||
});
|
||||
|
||||
const indexPatterns = useMemo(() => {
|
||||
if (ruleTypeId && aadFields?.length) {
|
||||
return [{ title: ruleTypeId, fields: aadFields }];
|
||||
if (ruleTypeIds.length > 0 && dataView?.fields?.length) {
|
||||
return [{ title: ruleTypeIds.join(','), fields: dataView.fields }];
|
||||
}
|
||||
|
||||
if (dataView) {
|
||||
return [dataView];
|
||||
}
|
||||
return null;
|
||||
}, [aadFields, dataView, ruleTypeId]);
|
||||
}, [dataView, ruleTypeIds]);
|
||||
|
||||
const ruleType = useLoadRuleTypesQuery({
|
||||
filteredRuleTypes: ruleTypeId !== undefined ? [ruleTypeId] : [],
|
||||
enabled: ruleTypeId !== undefined,
|
||||
http,
|
||||
toasts,
|
||||
});
|
||||
|
||||
const isSecurity =
|
||||
(featureIds && featureIds.length === 1 && featureIds.includes(AlertConsumers.SIEM)) ||
|
||||
(ruleType &&
|
||||
ruleTypeId &&
|
||||
ruleType.ruleTypesState.data.get(ruleTypeId)?.producer === AlertConsumers.SIEM);
|
||||
const isSecurity = ruleTypeIds?.some(isSiemRuleType) ?? false;
|
||||
|
||||
const onSearchQuerySubmit = useCallback(
|
||||
({ dateRange, query: nextQuery }: { dateRange: TimeRange; query?: Query }) => {
|
||||
|
@ -110,7 +93,7 @@ export const AlertsSearchBar = ({
|
|||
appName,
|
||||
disableQueryLanguageSwitcher,
|
||||
// @ts-expect-error - DataView fields prop and SearchBar indexPatterns props are overly broad
|
||||
indexPatterns: !indexPatterns || fieldsLoading ? NO_INDEX_PATTERNS : indexPatterns,
|
||||
indexPatterns: !indexPatterns ? NO_INDEX_PATTERNS : indexPatterns,
|
||||
placeholder,
|
||||
query: { query: query ?? '', language: queryLanguage },
|
||||
filters,
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
*/
|
||||
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import type { ValidFeatureId } from '@kbn/rule-data-utils';
|
||||
import type { ToastsStart, HttpStart } from '@kbn/core/public';
|
||||
import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public';
|
||||
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
|
@ -18,7 +17,6 @@ export type QueryLanguageType = 'lucene' | 'kuery';
|
|||
export interface AlertsSearchBarProps {
|
||||
appName: string;
|
||||
disableQueryLanguageSwitcher?: boolean;
|
||||
featureIds: ValidFeatureId[];
|
||||
rangeFrom?: string;
|
||||
rangeTo?: string;
|
||||
query?: string;
|
||||
|
@ -28,7 +26,7 @@ export interface AlertsSearchBarProps {
|
|||
showSubmitButton?: boolean;
|
||||
placeholder?: string;
|
||||
submitOnBlur?: boolean;
|
||||
ruleTypeId?: string;
|
||||
ruleTypeIds?: string[];
|
||||
onQueryChange?: (query: {
|
||||
dateRange: { from: string; to: string; mode?: 'absolute' | 'relative' };
|
||||
query?: string;
|
||||
|
|
|
@ -9,12 +9,12 @@
|
|||
|
||||
import { httpServiceMock } from '@kbn/core/public/mocks';
|
||||
import { fetchAlertsFields } from '.';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
|
||||
describe('fetchAlertsFields', () => {
|
||||
const http = httpServiceMock.createStartContract();
|
||||
test('should call the browser_fields API with the correct parameters', async () => {
|
||||
const featureIds = [AlertConsumers.STACK_ALERTS];
|
||||
const ruleTypeIds = ['.es-query'];
|
||||
|
||||
http.get.mockResolvedValueOnce({
|
||||
browserFields: { fakeCategory: {} },
|
||||
fields: [
|
||||
|
@ -23,7 +23,7 @@ describe('fetchAlertsFields', () => {
|
|||
},
|
||||
],
|
||||
});
|
||||
const result = await fetchAlertsFields({ http, featureIds });
|
||||
const result = await fetchAlertsFields({ http, ruleTypeIds });
|
||||
expect(result).toEqual({
|
||||
browserFields: { fakeCategory: {} },
|
||||
fields: [
|
||||
|
@ -33,7 +33,7 @@ describe('fetchAlertsFields', () => {
|
|||
],
|
||||
});
|
||||
expect(http.get).toHaveBeenLastCalledWith('/internal/rac/alerts/browser_fields', {
|
||||
query: { featureIds },
|
||||
query: { ruleTypeIds },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,11 +12,11 @@ import type { BrowserFields } from '@kbn/alerting-types';
|
|||
import type { FetchAlertsFieldsParams } from './types';
|
||||
import { BASE_RAC_ALERTS_API_PATH } from '../../constants';
|
||||
|
||||
export const fetchAlertsFields = ({ http, featureIds }: FetchAlertsFieldsParams) => {
|
||||
export const fetchAlertsFields = ({ http, ruleTypeIds }: FetchAlertsFieldsParams) => {
|
||||
return http.get<{ browserFields: BrowserFields; fields: FieldDescriptor[] }>(
|
||||
`${BASE_RAC_ALERTS_API_PATH}/browser_fields`,
|
||||
{
|
||||
query: { featureIds },
|
||||
query: { ruleTypeIds },
|
||||
}
|
||||
);
|
||||
};
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
*/
|
||||
|
||||
import { HttpSetup } from '@kbn/core-http-browser';
|
||||
import { ValidFeatureId } from '@kbn/rule-data-utils';
|
||||
|
||||
export interface FetchAlertsFieldsParams {
|
||||
// Dependencies
|
||||
|
@ -16,7 +15,7 @@ export interface FetchAlertsFieldsParams {
|
|||
|
||||
// Params
|
||||
/**
|
||||
* Array of feature ids used for authorization and area-based filtering
|
||||
* Array of rule type ids used for authorization and area-based filtering
|
||||
*/
|
||||
featureIds: ValidFeatureId[];
|
||||
ruleTypeIds: string[];
|
||||
}
|
||||
|
|
|
@ -9,22 +9,21 @@
|
|||
|
||||
import { httpServiceMock } from '@kbn/core/public/mocks';
|
||||
import { fetchAlertsIndexNames } from '.';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import { BASE_RAC_ALERTS_API_PATH } from '../../constants';
|
||||
|
||||
describe('fetchAlertsIndexNames', () => {
|
||||
const http = httpServiceMock.createStartContract();
|
||||
|
||||
it('calls the alerts/index API with the correct parameters', async () => {
|
||||
const featureIds = [AlertConsumers.STACK_ALERTS, AlertConsumers.APM];
|
||||
const ruleTypeIds = ['.es-query'];
|
||||
const indexNames = ['test-index'];
|
||||
http.get.mockResolvedValueOnce({
|
||||
index_name: indexNames,
|
||||
});
|
||||
const result = await fetchAlertsIndexNames({ http, featureIds });
|
||||
const result = await fetchAlertsIndexNames({ http, ruleTypeIds });
|
||||
expect(result).toEqual(indexNames);
|
||||
expect(http.get).toHaveBeenLastCalledWith(`${BASE_RAC_ALERTS_API_PATH}/index`, {
|
||||
query: { features: featureIds.join(',') },
|
||||
query: { ruleTypeIds },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,11 +10,11 @@
|
|||
import { BASE_RAC_ALERTS_API_PATH } from '../../constants';
|
||||
import { FetchAlertsIndexNamesParams } from './types';
|
||||
|
||||
export const fetchAlertsIndexNames = async ({ http, featureIds }: FetchAlertsIndexNamesParams) => {
|
||||
export const fetchAlertsIndexNames = async ({ http, ruleTypeIds }: FetchAlertsIndexNamesParams) => {
|
||||
const { index_name: indexNames = [] } = await http.get<{ index_name: string[] }>(
|
||||
`${BASE_RAC_ALERTS_API_PATH}/index`,
|
||||
{
|
||||
query: { features: featureIds.join(',') },
|
||||
query: { ruleTypeIds },
|
||||
}
|
||||
);
|
||||
return indexNames;
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
*/
|
||||
|
||||
import { HttpSetup } from '@kbn/core-http-browser';
|
||||
import { ValidFeatureId } from '@kbn/rule-data-utils';
|
||||
|
||||
export interface FetchAlertsIndexNamesParams {
|
||||
// Dependencies
|
||||
|
@ -16,7 +15,7 @@ export interface FetchAlertsIndexNamesParams {
|
|||
|
||||
// Params
|
||||
/**
|
||||
* Array of feature ids used for authorization and area-based filtering
|
||||
* Array of rule type ids used for authorization and area-based filtering
|
||||
*/
|
||||
featureIds: ValidFeatureId[];
|
||||
ruleTypeIds: string[];
|
||||
}
|
||||
|
|
|
@ -204,7 +204,8 @@ describe('searchAlerts', () => {
|
|||
|
||||
const params: SearchAlertsParams = {
|
||||
data: mockDataPlugin as unknown as DataPublicPluginStart,
|
||||
featureIds: ['siem'],
|
||||
ruleTypeIds: ['siem.esqlRule'],
|
||||
consumers: ['siem'],
|
||||
fields: [
|
||||
{ field: 'kibana.rule.type.id', include_unmapped: true },
|
||||
{ field: '*', include_unmapped: true },
|
||||
|
@ -237,7 +238,8 @@ describe('searchAlerts', () => {
|
|||
expect(mockDataPlugin.search.search).toHaveBeenCalledTimes(1);
|
||||
expect(mockDataPlugin.search.search).toHaveBeenCalledWith(
|
||||
{
|
||||
featureIds: params.featureIds,
|
||||
ruleTypeIds: params.ruleTypeIds,
|
||||
consumers: params.consumers,
|
||||
fields: [...params.fields!],
|
||||
pagination: {
|
||||
pageIndex: params.pageIndex,
|
||||
|
|
|
@ -15,7 +15,6 @@ import type {
|
|||
} from '@kbn/alerting-types';
|
||||
import { set } from '@kbn/safer-lodash-set';
|
||||
import type { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import type { ValidFeatureId } from '@kbn/rule-data-utils';
|
||||
import type {
|
||||
MappingRuntimeFields,
|
||||
QueryDslFieldAndFormat,
|
||||
|
@ -37,9 +36,13 @@ export interface SearchAlertsParams {
|
|||
|
||||
// Parameters
|
||||
/**
|
||||
* Array of feature ids used for authorization and area-based filtering
|
||||
* Array of rule type ids used area-based filtering
|
||||
*/
|
||||
featureIds: ValidFeatureId[];
|
||||
ruleTypeIds: string[];
|
||||
/**
|
||||
* Array of consumers used area-based filtering
|
||||
*/
|
||||
consumers?: string[];
|
||||
/**
|
||||
* ES query to perform on the affected alert indices
|
||||
*/
|
||||
|
@ -80,7 +83,8 @@ export interface SearchAlertsResult {
|
|||
export const searchAlerts = ({
|
||||
data,
|
||||
signal,
|
||||
featureIds,
|
||||
ruleTypeIds,
|
||||
consumers,
|
||||
fields,
|
||||
query,
|
||||
sort,
|
||||
|
@ -92,7 +96,8 @@ export const searchAlerts = ({
|
|||
data.search
|
||||
.search<RuleRegistrySearchRequest, RuleRegistrySearchResponse>(
|
||||
{
|
||||
featureIds,
|
||||
ruleTypeIds,
|
||||
consumers,
|
||||
fields,
|
||||
query,
|
||||
pagination: { pageIndex, pageSize },
|
||||
|
|
|
@ -20,5 +20,4 @@ export * from './use_load_rule_types_query';
|
|||
export * from './use_load_ui_config';
|
||||
export * from './use_load_ui_health';
|
||||
export * from './use_resolve_rule';
|
||||
export * from './use_rule_aad_fields';
|
||||
export * from './use_update_rule';
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { DataView } from '@kbn/data-views-plugin/common';
|
||||
|
@ -42,6 +41,7 @@ const mockServices = {
|
|||
toasts: notificationServiceMock.createStartContract().toasts,
|
||||
dataViewsService: dataViewPluginMocks.createStartContract(),
|
||||
};
|
||||
|
||||
mockServices.dataViewsService.create.mockResolvedValue(mockDataView);
|
||||
|
||||
const queryClient = new QueryClient(testQueryClientConfig);
|
||||
|
@ -51,13 +51,6 @@ const wrapper: React.FC<React.PropsWithChildren<{}>> = ({ children }) => (
|
|||
);
|
||||
|
||||
describe('useAlertsDataView', () => {
|
||||
const observabilityFeatureIds = [
|
||||
AlertConsumers.APM,
|
||||
AlertConsumers.INFRASTRUCTURE,
|
||||
AlertConsumers.LOGS,
|
||||
AlertConsumers.UPTIME,
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
queryClient.clear();
|
||||
|
@ -73,7 +66,7 @@ describe('useAlertsDataView', () => {
|
|||
() =>
|
||||
useAlertsDataView({
|
||||
...mockServices,
|
||||
featureIds: observabilityFeatureIds,
|
||||
ruleTypeIds: ['apm'],
|
||||
}),
|
||||
{
|
||||
wrapper,
|
||||
|
@ -83,12 +76,12 @@ describe('useAlertsDataView', () => {
|
|||
await waitFor(() => expect(result.current).toEqual(mockedAsyncDataView));
|
||||
});
|
||||
|
||||
it('fetches indexes and fields for non-siem feature ids, returning a DataViewBase object', async () => {
|
||||
it('fetches indexes and fields for non-siem rule type ids, returning a DataViewBase object', async () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useAlertsDataView({
|
||||
...mockServices,
|
||||
featureIds: observabilityFeatureIds,
|
||||
ruleTypeIds: ['apm, .es-query'],
|
||||
}),
|
||||
{
|
||||
wrapper,
|
||||
|
@ -102,9 +95,9 @@ describe('useAlertsDataView', () => {
|
|||
expect(result.current.dataView).not.toBe(mockDataView);
|
||||
});
|
||||
|
||||
it('only fetches index names for the siem feature id, returning a DataView', async () => {
|
||||
it('only fetches index names for the siem rule type ids, returning a DataView', async () => {
|
||||
const { result } = renderHook(
|
||||
() => useAlertsDataView({ ...mockServices, featureIds: [AlertConsumers.SIEM] }),
|
||||
() => useAlertsDataView({ ...mockServices, ruleTypeIds: ['siem.esqlRule', 'siem.eqlRule'] }),
|
||||
{
|
||||
wrapper,
|
||||
}
|
||||
|
@ -116,12 +109,12 @@ describe('useAlertsDataView', () => {
|
|||
await waitFor(() => expect(result.current.dataView).toBe(mockDataView));
|
||||
});
|
||||
|
||||
it('does not fetch anything if siem and other feature ids are mixed together', async () => {
|
||||
it('does not fetch anything if siem and other rule type ids are mixed together', async () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useAlertsDataView({
|
||||
...mockServices,
|
||||
featureIds: [AlertConsumers.SIEM, AlertConsumers.LOGS],
|
||||
ruleTypeIds: ['siem.esqlRule', 'apm', 'logs'],
|
||||
}),
|
||||
{
|
||||
wrapper,
|
||||
|
@ -138,11 +131,34 @@ describe('useAlertsDataView', () => {
|
|||
expect(mockFetchAlertsFields).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it('does not fetch anything with empty array nor create a virtual data view', async () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useAlertsDataView({
|
||||
...mockServices,
|
||||
ruleTypeIds: [],
|
||||
}),
|
||||
{
|
||||
wrapper,
|
||||
}
|
||||
);
|
||||
|
||||
await waitFor(() =>
|
||||
expect(result.current).toEqual({
|
||||
isLoading: false,
|
||||
dataView: undefined,
|
||||
})
|
||||
);
|
||||
expect(mockFetchAlertsIndexNames).toHaveBeenCalledTimes(0);
|
||||
expect(mockFetchAlertsFields).toHaveBeenCalledTimes(0);
|
||||
expect(mockServices.dataViewsService.create).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns an undefined data view if any of the queries fails', async () => {
|
||||
mockFetchAlertsIndexNames.mockRejectedValue('error');
|
||||
|
||||
const { result } = renderHook(
|
||||
() => useAlertsDataView({ ...mockServices, featureIds: observabilityFeatureIds }),
|
||||
() => useAlertsDataView({ ...mockServices, ruleTypeIds: ['.es-query'] }),
|
||||
{
|
||||
wrapper,
|
||||
}
|
||||
|
@ -159,7 +175,7 @@ describe('useAlertsDataView', () => {
|
|||
it('shows an error toast if any of the queries fails', async () => {
|
||||
mockFetchAlertsIndexNames.mockRejectedValue('error');
|
||||
|
||||
renderHook(() => useAlertsDataView({ ...mockServices, featureIds: observabilityFeatureIds }), {
|
||||
renderHook(() => useAlertsDataView({ ...mockServices, ruleTypeIds: ['.es-query'] }), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
|
|
|
@ -10,10 +10,12 @@
|
|||
import { useEffect, useMemo } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { DataView, DataViewsContract, FieldSpec } from '@kbn/data-views-plugin/common';
|
||||
import { AlertConsumers, ValidFeatureId } from '@kbn/rule-data-utils';
|
||||
import { isSiemRuleType } from '@kbn/rule-data-utils';
|
||||
import type { ToastsStart, HttpStart } from '@kbn/core/public';
|
||||
|
||||
import { DataViewBase } from '@kbn/es-query';
|
||||
import type { FieldDescriptor } from '@kbn/data-views-plugin/server';
|
||||
import { BrowserFields } from '@kbn/alerting-types';
|
||||
import { useVirtualDataViewQuery } from './use_virtual_data_view_query';
|
||||
import { useFetchAlertsFieldsQuery } from './use_fetch_alerts_fields_query';
|
||||
import { useFetchAlertsIndexNamesQuery } from './use_fetch_alerts_index_names_query';
|
||||
|
@ -26,12 +28,12 @@ export interface UseAlertsDataViewParams {
|
|||
|
||||
// Params
|
||||
/**
|
||||
* Array of feature ids used for authorization and area-based filtering
|
||||
* Array of rule type ids used for authorization and area-based filtering
|
||||
*
|
||||
* Security data views must be requested in isolation (i.e. `['siem']`). If mixed with
|
||||
* other feature ids, the resulting data view will be empty.
|
||||
* other rule type ids, the resulting data view will be empty.
|
||||
*/
|
||||
featureIds: ValidFeatureId[];
|
||||
ruleTypeIds: string[];
|
||||
}
|
||||
|
||||
export interface UseAlertsDataViewResult {
|
||||
|
@ -53,7 +55,7 @@ const resolveDataView = ({
|
|||
isError: boolean;
|
||||
virtualDataView?: DataView;
|
||||
indexNames?: string[];
|
||||
fields?: { fields: FieldSpec[] };
|
||||
fields?: { browserFields: BrowserFields; fields: FieldDescriptor[] };
|
||||
}) => {
|
||||
if (isError) {
|
||||
return;
|
||||
|
@ -93,11 +95,11 @@ export const useAlertsDataView = ({
|
|||
http,
|
||||
dataViewsService,
|
||||
toasts,
|
||||
featureIds,
|
||||
ruleTypeIds,
|
||||
}: UseAlertsDataViewParams): UseAlertsDataViewResult => {
|
||||
const includesSecurity = featureIds.includes(AlertConsumers.SIEM);
|
||||
const isOnlySecurity = featureIds.length === 1 && includesSecurity;
|
||||
const hasMixedFeatureIds = featureIds.length > 1 && includesSecurity;
|
||||
const includesSecurity = ruleTypeIds.some(isSiemRuleType);
|
||||
const isOnlySecurity = ruleTypeIds.length > 0 && ruleTypeIds.every(isSiemRuleType);
|
||||
const hasMixedFeatureIds = !isOnlySecurity && includesSecurity;
|
||||
|
||||
const {
|
||||
data: indexNames,
|
||||
|
@ -105,10 +107,10 @@ export const useAlertsDataView = ({
|
|||
isLoading: isLoadingIndexNames,
|
||||
isInitialLoading: isInitialLoadingIndexNames,
|
||||
} = useFetchAlertsIndexNamesQuery(
|
||||
{ http, featureIds },
|
||||
{ http, ruleTypeIds },
|
||||
{
|
||||
// Don't fetch index names when featureIds includes both Security Solution and other features
|
||||
enabled: !!featureIds.length && (isOnlySecurity || !includesSecurity),
|
||||
enabled: !!ruleTypeIds.length && (isOnlySecurity || !includesSecurity),
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -118,10 +120,10 @@ export const useAlertsDataView = ({
|
|||
isLoading: isLoadingFields,
|
||||
isInitialLoading: isInitialLoadingFields,
|
||||
} = useFetchAlertsFieldsQuery(
|
||||
{ http, featureIds },
|
||||
{ http, ruleTypeIds },
|
||||
{
|
||||
// Don't fetch fields when featureIds includes Security Solution
|
||||
enabled: !!featureIds.length && !includesSecurity,
|
||||
// Don't fetch fields when ruleTypeIds includes Security Solution
|
||||
enabled: !!ruleTypeIds.length && !includesSecurity,
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -167,7 +169,7 @@ export const useAlertsDataView = ({
|
|||
|
||||
return useMemo(() => {
|
||||
let isLoading: boolean;
|
||||
if (!featureIds.length || hasMixedFeatureIds) {
|
||||
if (!ruleTypeIds.length || hasMixedFeatureIds) {
|
||||
isLoading = false;
|
||||
} else {
|
||||
if (isOnlySecurity) {
|
||||
|
@ -180,13 +182,14 @@ export const useAlertsDataView = ({
|
|||
isLoadingFields;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
dataView,
|
||||
isLoading,
|
||||
};
|
||||
}, [
|
||||
dataView,
|
||||
featureIds.length,
|
||||
ruleTypeIds.length,
|
||||
hasMixedFeatureIds,
|
||||
isInitialLoadingFields,
|
||||
isInitialLoadingIndexNames,
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
*/
|
||||
|
||||
import React, { FC } from 'react';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import * as ReactQuery from '@tanstack/react-query';
|
||||
import { waitFor, renderHook } from '@testing-library/react';
|
||||
import { testQueryClientConfig } from '../test_utils/test_query_client_config';
|
||||
|
@ -48,9 +47,9 @@ describe('useFetchAlertsFieldsQuery', () => {
|
|||
queryClient.clear();
|
||||
});
|
||||
|
||||
it('should not fetch for siem', () => {
|
||||
it('should not fetch for siem rule types', () => {
|
||||
const { result } = renderHook(
|
||||
() => useFetchAlertsFieldsQuery({ http: mockHttpClient, featureIds: ['siem'] }),
|
||||
() => useFetchAlertsFieldsQuery({ http: mockHttpClient, ruleTypeIds: ['siem.esqlRule'] }),
|
||||
{
|
||||
wrapper,
|
||||
}
|
||||
|
@ -63,14 +62,14 @@ describe('useFetchAlertsFieldsQuery', () => {
|
|||
it('should correctly override the `enabled` option', () => {
|
||||
const { rerender } = renderHook(
|
||||
({
|
||||
featureIds,
|
||||
ruleTypeIds,
|
||||
enabled,
|
||||
}: React.PropsWithChildren<{ featureIds: AlertConsumers[]; enabled?: boolean }>) =>
|
||||
useFetchAlertsFieldsQuery({ http: mockHttpClient, featureIds }, { enabled }),
|
||||
}: React.PropsWithChildren<{ ruleTypeIds: string[]; enabled?: boolean }>) =>
|
||||
useFetchAlertsFieldsQuery({ http: mockHttpClient, ruleTypeIds }, { enabled }),
|
||||
{
|
||||
wrapper,
|
||||
initialProps: {
|
||||
featureIds: ['apm'],
|
||||
ruleTypeIds: ['apm'],
|
||||
enabled: false,
|
||||
},
|
||||
}
|
||||
|
@ -78,18 +77,18 @@ describe('useFetchAlertsFieldsQuery', () => {
|
|||
|
||||
expect(useQuerySpy).toHaveBeenCalledWith(expect.objectContaining({ enabled: false }));
|
||||
|
||||
rerender({ featureIds: [], enabled: true });
|
||||
rerender({ ruleTypeIds: [], enabled: true });
|
||||
|
||||
expect(useQuerySpy).toHaveBeenCalledWith(expect.objectContaining({ enabled: false }));
|
||||
|
||||
rerender({ featureIds: ['apm'] });
|
||||
rerender({ ruleTypeIds: ['apm'] });
|
||||
|
||||
expect(useQuerySpy).toHaveBeenCalledWith(expect.objectContaining({ enabled: true }));
|
||||
});
|
||||
|
||||
it('should call the api only once', async () => {
|
||||
const { result, rerender } = renderHook(
|
||||
() => useFetchAlertsFieldsQuery({ http: mockHttpClient, featureIds: ['apm'] }),
|
||||
() => useFetchAlertsFieldsQuery({ http: mockHttpClient, ruleTypeIds: ['apm'] }),
|
||||
{
|
||||
wrapper,
|
||||
}
|
||||
|
@ -122,30 +121,12 @@ describe('useFetchAlertsFieldsQuery', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('should not fetch if the only featureId is not valid', async () => {
|
||||
it('should not fetch if all rule types are siem', async () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useFetchAlertsFieldsQuery({
|
||||
http: mockHttpClient,
|
||||
featureIds: ['alerts'] as unknown as AlertConsumers[],
|
||||
}),
|
||||
{
|
||||
wrapper,
|
||||
}
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockHttpGet).toHaveBeenCalledTimes(0);
|
||||
expect(result.current.data).toEqual(emptyData);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not fetch if all featureId are not valid', async () => {
|
||||
const { result } = renderHook(
|
||||
() =>
|
||||
useFetchAlertsFieldsQuery({
|
||||
http: mockHttpClient,
|
||||
featureIds: ['alerts', 'tomato'] as unknown as AlertConsumers[],
|
||||
ruleTypeIds: ['siem.esqlRule', 'siem.eqlRule'],
|
||||
}),
|
||||
{
|
||||
wrapper,
|
||||
|
@ -163,7 +144,7 @@ describe('useFetchAlertsFieldsQuery', () => {
|
|||
() =>
|
||||
useFetchAlertsFieldsQuery({
|
||||
http: mockHttpClient,
|
||||
featureIds: ['alerts', 'apm', 'logs'] as AlertConsumers[],
|
||||
ruleTypeIds: ['siem.esqlRule', 'apm', 'logs'],
|
||||
}),
|
||||
{
|
||||
wrapper,
|
||||
|
@ -173,7 +154,7 @@ describe('useFetchAlertsFieldsQuery', () => {
|
|||
await waitFor(() => {
|
||||
expect(mockHttpGet).toHaveBeenCalledTimes(1);
|
||||
expect(mockHttpGet).toHaveBeenCalledWith('/internal/rac/alerts/browser_fields', {
|
||||
query: { featureIds: ['apm', 'logs'] },
|
||||
query: { ruleTypeIds: ['apm', 'logs'] },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -7,15 +7,13 @@
|
|||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import { AlertConsumers, isValidFeatureId } from '@kbn/rule-data-utils';
|
||||
import { isSiemRuleType } from '@kbn/rule-data-utils';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import type { QueryOptionsOverrides } from '../types/tanstack_query_utility_types';
|
||||
import { fetchAlertsFields, FetchAlertsFieldsParams } from '../apis/fetch_alerts_fields';
|
||||
|
||||
export type UseFetchAlertsFieldsQueryParams = FetchAlertsFieldsParams;
|
||||
|
||||
const UNSUPPORTED_FEATURE_ID = AlertConsumers.SIEM;
|
||||
|
||||
export const queryKeyPrefix = ['alerts', fetchAlertsFields.name];
|
||||
|
||||
/**
|
||||
|
@ -31,19 +29,17 @@ export const useFetchAlertsFieldsQuery = (
|
|||
'placeholderData' | 'context' | 'onError' | 'refetchOnWindowFocus' | 'staleTime' | 'enabled'
|
||||
>
|
||||
) => {
|
||||
const { featureIds } = params;
|
||||
const { ruleTypeIds } = params;
|
||||
|
||||
const validFeatureIds = featureIds.filter(
|
||||
(fid) => isValidFeatureId(fid) && fid !== UNSUPPORTED_FEATURE_ID
|
||||
);
|
||||
const validRuleTypeIds = ruleTypeIds.filter((ruleTypeId) => !isSiemRuleType(ruleTypeId));
|
||||
|
||||
return useQuery({
|
||||
queryKey: queryKeyPrefix.concat(featureIds),
|
||||
queryFn: () => fetchAlertsFields({ http, featureIds: validFeatureIds }),
|
||||
queryKey: queryKeyPrefix.concat(ruleTypeIds),
|
||||
queryFn: () => fetchAlertsFields({ http, ruleTypeIds: validRuleTypeIds }),
|
||||
placeholderData: { browserFields: {}, fields: [] },
|
||||
staleTime: 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
...options,
|
||||
enabled: validFeatureIds.length > 0 && (options?.enabled == null || options.enabled),
|
||||
enabled: validRuleTypeIds.length > 0 && (options?.enabled == null || options.enabled),
|
||||
});
|
||||
};
|
||||
|
|
|
@ -36,8 +36,8 @@ describe('useFetchAlertsIndexNamesQuery', () => {
|
|||
queryClient.clear();
|
||||
});
|
||||
|
||||
it('does not fetch if featureIds is empty', () => {
|
||||
renderHook(() => useFetchAlertsIndexNamesQuery({ http: mockHttpClient, featureIds: [] }), {
|
||||
it('does not fetch if ruleTypeIds is empty', () => {
|
||||
renderHook(() => useFetchAlertsIndexNamesQuery({ http: mockHttpClient, ruleTypeIds: [] }), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
|
@ -45,19 +45,22 @@ describe('useFetchAlertsIndexNamesQuery', () => {
|
|||
});
|
||||
|
||||
it('calls fetchAlertsIndexNames with the correct parameters', () => {
|
||||
renderHook(() => useFetchAlertsIndexNamesQuery({ http: mockHttpClient, featureIds: ['apm'] }), {
|
||||
wrapper,
|
||||
});
|
||||
renderHook(
|
||||
() => useFetchAlertsIndexNamesQuery({ http: mockHttpClient, ruleTypeIds: ['apm'] }),
|
||||
{
|
||||
wrapper,
|
||||
}
|
||||
);
|
||||
|
||||
expect(mockFetchAlertsIndexNames).toHaveBeenCalledWith({
|
||||
http: mockHttpClient,
|
||||
featureIds: ['apm'],
|
||||
ruleTypeIds: ['apm'],
|
||||
});
|
||||
});
|
||||
|
||||
it('correctly caches the index names', async () => {
|
||||
const { result, rerender } = renderHook(
|
||||
() => useFetchAlertsIndexNamesQuery({ http: mockHttpClient, featureIds: ['apm'] }),
|
||||
() => useFetchAlertsIndexNamesQuery({ http: mockHttpClient, ruleTypeIds: ['apm'] }),
|
||||
{
|
||||
wrapper,
|
||||
}
|
||||
|
|
|
@ -25,16 +25,16 @@ export const queryKeyPrefix = ['alerts', fetchAlertsIndexNames.name];
|
|||
* @external https://tanstack.com/query/v4/docs/framework/react/guides/testing
|
||||
*/
|
||||
export const useFetchAlertsIndexNamesQuery = (
|
||||
{ http, featureIds }: UseFetchAlertsIndexNamesQueryParams,
|
||||
{ http, ruleTypeIds }: UseFetchAlertsIndexNamesQueryParams,
|
||||
options?: Pick<
|
||||
QueryOptionsOverrides<typeof fetchAlertsIndexNames>,
|
||||
'context' | 'onError' | 'refetchOnWindowFocus' | 'staleTime' | 'enabled'
|
||||
>
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: queryKeyPrefix.concat(featureIds),
|
||||
queryFn: () => fetchAlertsIndexNames({ http, featureIds }),
|
||||
enabled: featureIds.length > 0,
|
||||
queryKey: queryKeyPrefix.concat(ruleTypeIds),
|
||||
queryFn: () => fetchAlertsIndexNames({ http, ruleTypeIds }),
|
||||
enabled: ruleTypeIds.length > 0,
|
||||
staleTime: 60 * 1000,
|
||||
refetchOnWindowFocus: false,
|
||||
...options,
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* 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 React from 'react';
|
||||
import { renderHook } from '@testing-library/react-hooks';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { testQueryClientConfig } from '../test_utils/test_query_client_config';
|
||||
import { httpServiceMock } from '@kbn/core-http-browser-mocks';
|
||||
import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks';
|
||||
import { useFindAlertsQuery } from './use_find_alerts_query';
|
||||
|
||||
describe('useFindAlertsQuery', () => {
|
||||
const mockServices = {
|
||||
http: httpServiceMock.createStartContract(),
|
||||
toasts: notificationServiceMock.createStartContract().toasts,
|
||||
};
|
||||
|
||||
const queryClient = new QueryClient(testQueryClientConfig);
|
||||
|
||||
const wrapper: React.FC<React.PropsWithChildren<{}>> = ({ children }) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('calls the api correctly', async () => {
|
||||
const { result, waitForValueToChange } = renderHook(
|
||||
() =>
|
||||
useFindAlertsQuery({
|
||||
...mockServices,
|
||||
params: { ruleTypeIds: ['foo'], consumers: ['bar'] },
|
||||
}),
|
||||
{
|
||||
wrapper,
|
||||
}
|
||||
);
|
||||
|
||||
await waitForValueToChange(() => result.current.isLoading, { timeout: 5000 });
|
||||
|
||||
expect(mockServices.http.post).toHaveBeenCalledTimes(1);
|
||||
expect(mockServices.http.post).toBeCalledWith('/internal/rac/alerts/find', {
|
||||
body: '{"consumers":["bar"],"rule_type_ids":["foo"]}',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -19,7 +19,7 @@ export interface UseFindAlertsQueryProps {
|
|||
http: HttpStart;
|
||||
toasts: ToastsStart;
|
||||
enabled?: boolean;
|
||||
params: ISearchRequestParams & { feature_ids?: string[] };
|
||||
params: ISearchRequestParams & { ruleTypeIds?: string[]; consumers?: string[] };
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -34,6 +34,7 @@ export const useFindAlertsQuery = <T>({
|
|||
enabled = true,
|
||||
params,
|
||||
}: UseFindAlertsQueryProps) => {
|
||||
const { ruleTypeIds, ...rest } = params;
|
||||
const onErrorFn = (error: Error) => {
|
||||
if (error) {
|
||||
toasts.addDanger(
|
||||
|
@ -48,7 +49,7 @@ export const useFindAlertsQuery = <T>({
|
|||
queryKey: ['findAlerts', JSON.stringify(params)],
|
||||
queryFn: () =>
|
||||
http.post<SearchResponseBody<{}, T>>(`${BASE_RAC_ALERTS_API_PATH}/find`, {
|
||||
body: JSON.stringify(params),
|
||||
body: JSON.stringify({ ...rest, rule_type_ids: ruleTypeIds }),
|
||||
}),
|
||||
onError: onErrorFn,
|
||||
refetchOnWindowFocus: false,
|
||||
|
|
|
@ -11,7 +11,6 @@ import React from 'react';
|
|||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import type { HttpStart } from '@kbn/core-http-browser';
|
||||
import { ToastsStart } from '@kbn/core-notifications-browser';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
|
||||
import { useGetAlertsGroupAggregationsQuery } from './use_get_alerts_group_aggregations_query';
|
||||
import { waitFor, renderHook } from '@testing-library/react';
|
||||
|
@ -40,7 +39,8 @@ const mockToasts = {
|
|||
const toasts = mockToasts as unknown as ToastsStart;
|
||||
|
||||
const params = {
|
||||
featureIds: [AlertConsumers.STACK_ALERTS],
|
||||
ruleTypeIds: ['.es-query'],
|
||||
consumers: ['stackAlerts'],
|
||||
groupByField: 'kibana.alert.rule.name',
|
||||
};
|
||||
|
||||
|
|
|
@ -12,7 +12,6 @@ import { useQuery } from '@tanstack/react-query';
|
|||
import type { HttpStart } from '@kbn/core-http-browser';
|
||||
import type { ToastsStart } from '@kbn/core-notifications-browser';
|
||||
import { SearchResponseBody } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import type {
|
||||
AggregationsAggregationContainer,
|
||||
QueryDslQueryContainer,
|
||||
|
@ -25,7 +24,8 @@ export interface UseGetAlertsGroupAggregationsQueryProps {
|
|||
toasts: ToastsStart;
|
||||
enabled?: boolean;
|
||||
params: {
|
||||
featureIds: AlertConsumers[];
|
||||
ruleTypeIds: string[];
|
||||
consumers?: string[];
|
||||
groupByField: string;
|
||||
aggregations?: Record<string, AggregationsAggregationContainer>;
|
||||
filters?: QueryDslQueryContainer[];
|
||||
|
@ -49,7 +49,7 @@ export interface UseGetAlertsGroupAggregationsQueryProps {
|
|||
* `groupByField` buckets computed over a field with a null/absent value are marked with the
|
||||
* `isNullGroup` flag set to true and their key is set to the `--` string.
|
||||
*
|
||||
* Applies alerting RBAC through featureIds.
|
||||
* Applies alerting RBAC through ruleTypeIds.
|
||||
*/
|
||||
export const useGetAlertsGroupAggregationsQuery = <T>({
|
||||
http,
|
||||
|
@ -61,7 +61,7 @@ export const useGetAlertsGroupAggregationsQuery = <T>({
|
|||
if (error) {
|
||||
toasts.addDanger(
|
||||
i18n.translate(
|
||||
'alertsUIShared.hooks.useFindAlertsQuery.unableToFetchAlertsGroupingAggregations',
|
||||
'alertsUIShared.hooks.useGetAlertsGroupAggregationsQuery.unableToFetchAlertsGroupingAggregations',
|
||||
{
|
||||
defaultMessage: 'Unable to fetch alerts grouping aggregations',
|
||||
}
|
||||
|
|
|
@ -1,63 +0,0 @@
|
|||
/*
|
||||
* 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 { useMemo } from 'react';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import type { ToastsStart, HttpStart } from '@kbn/core/public';
|
||||
import type { DataViewField } from '@kbn/data-views-plugin/common';
|
||||
import { EMPTY_AAD_FIELDS } from '../constants';
|
||||
import { fetchRuleTypeAadTemplateFields } from '../apis';
|
||||
|
||||
export interface UseRuleAADFieldsProps {
|
||||
ruleTypeId?: string;
|
||||
http: HttpStart;
|
||||
toasts: ToastsStart;
|
||||
}
|
||||
|
||||
export interface UseRuleAADFieldsResult {
|
||||
aadFields: DataViewField[];
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export function useRuleAADFields(props: UseRuleAADFieldsProps): UseRuleAADFieldsResult {
|
||||
const { ruleTypeId, http, toasts } = props;
|
||||
|
||||
const queryAadFieldsFn = () => {
|
||||
return fetchRuleTypeAadTemplateFields({ http, ruleTypeId });
|
||||
};
|
||||
|
||||
const onErrorFn = () => {
|
||||
toasts.addDanger(
|
||||
i18n.translate('alertsUIShared.hooks.useRuleAADFields.errorMessage', {
|
||||
defaultMessage: 'Unable to load alert fields per rule type',
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const {
|
||||
data: aadFields = EMPTY_AAD_FIELDS,
|
||||
isInitialLoading,
|
||||
isLoading,
|
||||
} = useQuery({
|
||||
queryKey: ['loadAlertAadFieldsPerRuleType', ruleTypeId],
|
||||
queryFn: queryAadFieldsFn,
|
||||
onError: onErrorFn,
|
||||
refetchOnWindowFocus: false,
|
||||
enabled: ruleTypeId !== undefined,
|
||||
});
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
aadFields,
|
||||
loading: ruleTypeId === undefined ? false : isInitialLoading || isLoading,
|
||||
}),
|
||||
[aadFields, isInitialLoading, isLoading, ruleTypeId]
|
||||
);
|
||||
}
|
|
@ -98,7 +98,8 @@ describe('useSearchAlertsQuery', () => {
|
|||
|
||||
const params: UseSearchAlertsQueryParams = {
|
||||
data: mockDataPlugin as unknown as DataPublicPluginStart,
|
||||
featureIds: ['siem'],
|
||||
ruleTypeIds: ['siem.esqlRule'],
|
||||
consumers: ['siem'],
|
||||
fields: [
|
||||
{ field: 'kibana.rule.type.id', include_unmapped: true },
|
||||
{ field: '*', include_unmapped: true },
|
||||
|
@ -125,6 +126,33 @@ describe('useSearchAlertsQuery', () => {
|
|||
queryClient.removeQueries();
|
||||
});
|
||||
|
||||
it('calls searchAlerts with correct arguments', async () => {
|
||||
const { result } = renderHook(() => useSearchAlertsQuery(params), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
await waitFor(() => expect(result.current.data).toBeDefined());
|
||||
|
||||
expect(mockDataPlugin.search.search).toHaveBeenCalledWith(
|
||||
{
|
||||
consumers: ['siem'],
|
||||
fields: [
|
||||
{ field: 'kibana.rule.type.id', include_unmapped: true },
|
||||
{ field: '*', include_unmapped: true },
|
||||
],
|
||||
pagination: { pageIndex: 0, pageSize: 10 },
|
||||
query: { ids: { values: ['alert-id-1'] } },
|
||||
ruleTypeIds: ['siem.esqlRule'],
|
||||
runtimeMappings: undefined,
|
||||
sort: [],
|
||||
},
|
||||
{
|
||||
abortSignal: expect.any(AbortSignal),
|
||||
strategy: 'privateRuleRegistryAlertsSearchStrategy',
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the response correctly', async () => {
|
||||
const { result } = renderHook(() => useSearchAlertsQuery(params), {
|
||||
wrapper,
|
||||
|
@ -263,8 +291,8 @@ describe('useSearchAlertsQuery', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('does not fetch with no feature ids', () => {
|
||||
const { result } = renderHook(() => useSearchAlertsQuery({ ...params, featureIds: [] }), {
|
||||
it('does not fetch with no rule type ids', () => {
|
||||
const { result } = renderHook(() => useSearchAlertsQuery({ ...params, ruleTypeIds: [] }), {
|
||||
wrapper,
|
||||
});
|
||||
|
||||
|
|
|
@ -29,7 +29,8 @@ export const queryKeyPrefix = ['alerts', searchAlerts.name];
|
|||
*/
|
||||
export const useSearchAlertsQuery = ({ data, ...params }: UseSearchAlertsQueryParams) => {
|
||||
const {
|
||||
featureIds,
|
||||
ruleTypeIds,
|
||||
consumers,
|
||||
fields,
|
||||
query = {
|
||||
bool: {},
|
||||
|
@ -49,7 +50,8 @@ export const useSearchAlertsQuery = ({ data, ...params }: UseSearchAlertsQueryPa
|
|||
searchAlerts({
|
||||
data,
|
||||
signal,
|
||||
featureIds,
|
||||
ruleTypeIds,
|
||||
consumers,
|
||||
fields,
|
||||
query,
|
||||
sort,
|
||||
|
@ -59,7 +61,7 @@ export const useSearchAlertsQuery = ({ data, ...params }: UseSearchAlertsQueryPa
|
|||
}),
|
||||
refetchOnWindowFocus: false,
|
||||
context: AlertsQueryContext,
|
||||
enabled: featureIds.length > 0,
|
||||
enabled: ruleTypeIds.length > 0,
|
||||
// To avoid flash of empty state with pagination, see https://tanstack.com/query/latest/docs/framework/react/guides/paginated-queries#better-paginated-queries-with-placeholderdata
|
||||
keepPreviousData: true,
|
||||
placeholderData: {
|
||||
|
|
|
@ -93,6 +93,7 @@ export function MaintenanceWindowCallout({
|
|||
.flat()
|
||||
)
|
||||
);
|
||||
|
||||
const activeCategories = activeCategoryIds
|
||||
.map(
|
||||
(categoryId) =>
|
||||
|
|
|
@ -107,7 +107,7 @@ describe('ruleActionsAlertsFilter', () => {
|
|||
action={getAction('1')}
|
||||
onChange={mockOnChange}
|
||||
appName="stackAlerts"
|
||||
featureIds={['stackAlerts']}
|
||||
ruleTypeId=".es-query"
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -120,7 +120,7 @@ describe('ruleActionsAlertsFilter', () => {
|
|||
action={getAction('1')}
|
||||
onChange={mockOnChange}
|
||||
appName="stackAlerts"
|
||||
featureIds={['stackAlerts']}
|
||||
ruleTypeId=".es-query"
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -142,7 +142,7 @@ describe('ruleActionsAlertsFilter', () => {
|
|||
})}
|
||||
onChange={mockOnChange}
|
||||
appName="stackAlerts"
|
||||
featureIds={['stackAlerts']}
|
||||
ruleTypeId=".es-query"
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -164,7 +164,7 @@ describe('ruleActionsAlertsFilter', () => {
|
|||
})}
|
||||
onChange={mockOnChange}
|
||||
appName="stackAlerts"
|
||||
featureIds={['stackAlerts']}
|
||||
ruleTypeId=".es-query"
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -241,7 +241,6 @@ describe('ruleActionsAlertsFilter', () => {
|
|||
})}
|
||||
onChange={mockOnChange}
|
||||
appName="stackAlerts"
|
||||
featureIds={['stackAlerts']}
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
*/
|
||||
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { ValidFeatureId } from '@kbn/rule-data-utils';
|
||||
import { Filter } from '@kbn/es-query';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiSwitch, EuiSpacer } from '@elastic/eui';
|
||||
|
@ -25,7 +24,6 @@ export interface RuleActionsAlertsFilterProps {
|
|||
action: RuleAction;
|
||||
onChange: (update?: AlertsFilter['query']) => void;
|
||||
appName: string;
|
||||
featureIds: ValidFeatureId[];
|
||||
ruleTypeId?: string;
|
||||
plugins?: {
|
||||
http: RuleFormPlugins['http'];
|
||||
|
@ -39,7 +37,6 @@ export const RuleActionsAlertsFilter = ({
|
|||
action,
|
||||
onChange,
|
||||
appName,
|
||||
featureIds,
|
||||
ruleTypeId,
|
||||
plugins: propsPlugins,
|
||||
}: RuleActionsAlertsFilterProps) => {
|
||||
|
@ -101,6 +98,8 @@ export const RuleActionsAlertsFilter = ({
|
|||
[updateQuery]
|
||||
);
|
||||
|
||||
const ruleTypeIds = useMemo(() => (ruleTypeId != null ? [ruleTypeId] : []), [ruleTypeId]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiSwitch
|
||||
|
@ -123,8 +122,7 @@ export const RuleActionsAlertsFilter = ({
|
|||
unifiedSearchBar={unifiedSearch.ui.SearchBar}
|
||||
dataService={data}
|
||||
appName={appName}
|
||||
featureIds={featureIds}
|
||||
ruleTypeId={ruleTypeId}
|
||||
ruleTypeIds={ruleTypeIds}
|
||||
disableQueryLanguageSwitcher={true}
|
||||
query={query.kql}
|
||||
filters={query.filters ?? []}
|
||||
|
|
|
@ -510,7 +510,6 @@ export const RuleActionsItem = (props: RuleActionsItemProps) => {
|
|||
{tab === SETTINGS_TAB && (
|
||||
<RuleActionsSettings
|
||||
action={action}
|
||||
producerId={producerId}
|
||||
onUseDefaultMessageChange={() => setUseDefaultMessage(true)}
|
||||
onNotifyWhenChange={onNotifyWhenChange}
|
||||
onActionGroupChange={onActionGroupChange}
|
||||
|
|
|
@ -168,7 +168,6 @@ describe('ruleActionsSettings', () => {
|
|||
render(
|
||||
<RuleActionsSettings
|
||||
action={getAction('1')}
|
||||
producerId="stackAlerts"
|
||||
onUseDefaultMessageChange={mockOnUseDefaultMessageChange}
|
||||
onNotifyWhenChange={mockOnNotifyWhenChange}
|
||||
onActionGroupChange={mockOnActionGroupChange}
|
||||
|
@ -185,7 +184,6 @@ describe('ruleActionsSettings', () => {
|
|||
render(
|
||||
<RuleActionsSettings
|
||||
action={getAction('1')}
|
||||
producerId="stackAlerts"
|
||||
onUseDefaultMessageChange={mockOnUseDefaultMessageChange}
|
||||
onNotifyWhenChange={mockOnNotifyWhenChange}
|
||||
onActionGroupChange={mockOnActionGroupChange}
|
||||
|
@ -224,7 +222,6 @@ describe('ruleActionsSettings', () => {
|
|||
notifyWhen: 'onActionGroupChange',
|
||||
},
|
||||
})}
|
||||
producerId="stackAlerts"
|
||||
onUseDefaultMessageChange={mockOnUseDefaultMessageChange}
|
||||
onNotifyWhenChange={mockOnNotifyWhenChange}
|
||||
onActionGroupChange={mockOnActionGroupChange}
|
||||
|
@ -261,7 +258,6 @@ describe('ruleActionsSettings', () => {
|
|||
notifyWhen: 'onActionGroupChange',
|
||||
},
|
||||
})}
|
||||
producerId="stackAlerts"
|
||||
onUseDefaultMessageChange={mockOnUseDefaultMessageChange}
|
||||
onNotifyWhenChange={mockOnNotifyWhenChange}
|
||||
onActionGroupChange={mockOnActionGroupChange}
|
||||
|
@ -278,7 +274,6 @@ describe('ruleActionsSettings', () => {
|
|||
render(
|
||||
<RuleActionsSettings
|
||||
action={getAction('1')}
|
||||
producerId="stackAlerts"
|
||||
onUseDefaultMessageChange={mockOnUseDefaultMessageChange}
|
||||
onNotifyWhenChange={mockOnNotifyWhenChange}
|
||||
onActionGroupChange={mockOnActionGroupChange}
|
||||
|
@ -304,7 +299,6 @@ describe('ruleActionsSettings', () => {
|
|||
render(
|
||||
<RuleActionsSettings
|
||||
action={getAction('1')}
|
||||
producerId="stackAlerts"
|
||||
onUseDefaultMessageChange={mockOnUseDefaultMessageChange}
|
||||
onNotifyWhenChange={mockOnNotifyWhenChange}
|
||||
onActionGroupChange={mockOnActionGroupChange}
|
||||
|
@ -341,7 +335,6 @@ describe('ruleActionsSettings', () => {
|
|||
render(
|
||||
<RuleActionsSettings
|
||||
action={getAction('1')}
|
||||
producerId="stackAlerts"
|
||||
onUseDefaultMessageChange={mockOnUseDefaultMessageChange}
|
||||
onNotifyWhenChange={mockOnNotifyWhenChange}
|
||||
onActionGroupChange={mockOnActionGroupChange}
|
||||
|
@ -375,7 +368,6 @@ describe('ruleActionsSettings', () => {
|
|||
render(
|
||||
<RuleActionsSettings
|
||||
action={getAction('1')}
|
||||
producerId="stackAlerts"
|
||||
onUseDefaultMessageChange={mockOnUseDefaultMessageChange}
|
||||
onNotifyWhenChange={mockOnNotifyWhenChange}
|
||||
onActionGroupChange={mockOnActionGroupChange}
|
||||
|
@ -418,7 +410,6 @@ describe('ruleActionsSettings', () => {
|
|||
render(
|
||||
<RuleActionsSettings
|
||||
action={getAction('1')}
|
||||
producerId="stackAlerts"
|
||||
onUseDefaultMessageChange={mockOnUseDefaultMessageChange}
|
||||
onNotifyWhenChange={mockOnNotifyWhenChange}
|
||||
onActionGroupChange={mockOnActionGroupChange}
|
||||
|
@ -429,4 +420,41 @@ describe('ruleActionsSettings', () => {
|
|||
|
||||
expect(screen.queryByText('filter query error')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should show the rule actions filter for siem rule types', () => {
|
||||
useRuleFormState.mockReturnValue({
|
||||
plugins: {
|
||||
settings: {},
|
||||
},
|
||||
formData: {
|
||||
consumer: 'siem',
|
||||
schedule: { interval: '5h' },
|
||||
},
|
||||
selectedRuleType: {
|
||||
/**
|
||||
* With the current configuration
|
||||
* hasFieldsForAad will return false
|
||||
* and we are testing the isSiemRuleType(ruleTypeId)
|
||||
* branch of the code
|
||||
*/
|
||||
...ruleType,
|
||||
id: 'siem.esqlRuleType',
|
||||
hasFieldsForAAD: false,
|
||||
},
|
||||
selectedRuleTypeModel: ruleModel,
|
||||
});
|
||||
|
||||
render(
|
||||
<RuleActionsSettings
|
||||
action={getAction('1')}
|
||||
onUseDefaultMessageChange={mockOnUseDefaultMessageChange}
|
||||
onNotifyWhenChange={mockOnNotifyWhenChange}
|
||||
onActionGroupChange={mockOnActionGroupChange}
|
||||
onAlertsFilterChange={mockOnAlertsFilterChange}
|
||||
onTimeframeChange={mockOnTimeframeChange}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText('RuleActionsAlertsFilter')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
RecoveredActionGroup,
|
||||
RuleActionFrequency,
|
||||
} from '@kbn/alerting-types';
|
||||
import { AlertConsumers, ValidFeatureId } from '@kbn/rule-data-utils';
|
||||
import { isSiemRuleType } from '@kbn/rule-data-utils';
|
||||
import { useRuleFormState } from '../hooks';
|
||||
import { RuleAction, RuleTypeWithDescription } from '../../common';
|
||||
import {
|
||||
|
@ -120,7 +120,6 @@ const actionGroupDisplay = ({
|
|||
|
||||
export interface RuleActionsSettingsProps {
|
||||
action: RuleAction;
|
||||
producerId: string;
|
||||
onUseDefaultMessageChange: () => void;
|
||||
onNotifyWhenChange: (frequency: RuleActionFrequency) => void;
|
||||
onActionGroupChange: (group: string) => void;
|
||||
|
@ -131,7 +130,6 @@ export interface RuleActionsSettingsProps {
|
|||
export const RuleActionsSettings = (props: RuleActionsSettingsProps) => {
|
||||
const {
|
||||
action,
|
||||
producerId,
|
||||
onUseDefaultMessageChange,
|
||||
onNotifyWhenChange,
|
||||
onActionGroupChange,
|
||||
|
@ -190,12 +188,14 @@ export const RuleActionsSettings = (props: RuleActionsSettingsProps) => {
|
|||
minimumActionThrottleUnit,
|
||||
});
|
||||
|
||||
const ruleTypeId = selectedRuleType.id;
|
||||
|
||||
const showActionAlertsFilter =
|
||||
hasFieldsForAad({
|
||||
ruleType: selectedRuleType,
|
||||
consumer,
|
||||
validConsumers,
|
||||
}) || producerId === AlertConsumers.SIEM;
|
||||
}) || isSiemRuleType(ruleTypeId);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" data-test-subj="ruleActionsSettings">
|
||||
|
@ -258,9 +258,8 @@ export const RuleActionsSettings = (props: RuleActionsSettingsProps) => {
|
|||
<RuleActionsAlertsFilter
|
||||
action={action}
|
||||
onChange={onAlertsFilterChange}
|
||||
featureIds={[producerId as ValidFeatureId]}
|
||||
appName="stackAlerts"
|
||||
ruleTypeId={selectedRuleType.id}
|
||||
ruleTypeId={ruleTypeId}
|
||||
/>
|
||||
</EuiFormRow>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -26,6 +26,7 @@ const mockRuleType: (
|
|||
recoveryActionGroup: { id: 'recovered', name: 'recovered' },
|
||||
actionGroups: [],
|
||||
defaultActionGroupId: 'default',
|
||||
category: 'my-category',
|
||||
});
|
||||
|
||||
const pickRuleTypeName = (ruleType: RuleTypeWithDescription) => ({ name: ruleType.name });
|
||||
|
|
|
@ -31,6 +31,7 @@ const ruleTypes: RuleTypeWithDescription[] = [
|
|||
name: 'default',
|
||||
},
|
||||
defaultActionGroupId: '1',
|
||||
category: 'my-category-1',
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
|
@ -49,6 +50,7 @@ const ruleTypes: RuleTypeWithDescription[] = [
|
|||
name: 'default',
|
||||
},
|
||||
defaultActionGroupId: '2',
|
||||
category: 'my-category-2',
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
|
@ -67,6 +69,7 @@ const ruleTypes: RuleTypeWithDescription[] = [
|
|||
name: 'default',
|
||||
},
|
||||
defaultActionGroupId: '3',
|
||||
category: 'my-category-3',
|
||||
},
|
||||
];
|
||||
|
||||
|
|
14
packages/kbn-rule-data-utils/jest.config.js
Normal file
14
packages/kbn-rule-data-utils/jest.config.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* 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".
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
preset: '@kbn/test',
|
||||
rootDir: '../..',
|
||||
roots: ['<rootDir>/packages/kbn-rule-data-utils'],
|
||||
};
|
22
packages/kbn-rule-data-utils/src/alerts_as_data_rbac.test.ts
Normal file
22
packages/kbn-rule-data-utils/src/alerts_as_data_rbac.test.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 { isSiemRuleType } from './alerts_as_data_rbac';
|
||||
|
||||
describe('alertsAsDataRbac', () => {
|
||||
describe('isSiemRuleType', () => {
|
||||
test('returns true for siem rule types', () => {
|
||||
expect(isSiemRuleType('siem.esqlRuleType')).toBe(true);
|
||||
});
|
||||
|
||||
test('returns false for NON siem rule types', () => {
|
||||
expect(isSiemRuleType('apm.anomaly')).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -28,6 +28,8 @@ export const AlertConsumers = {
|
|||
STACK_ALERTS: 'stackAlerts',
|
||||
EXAMPLE: 'AlertingExample',
|
||||
MONITORING: 'monitoring',
|
||||
ALERTS: 'alerts',
|
||||
DISCOVER: 'discover',
|
||||
} as const;
|
||||
export type AlertConsumers = (typeof AlertConsumers)[keyof typeof AlertConsumers];
|
||||
export type STATUS_VALUES = 'open' | 'acknowledged' | 'closed' | 'in-progress'; // TODO: remove 'in-progress' after migration to 'acknowledged'
|
||||
|
@ -91,3 +93,9 @@ export const getEsQueryConfig = (params?: GetEsQueryConfigParamType): EsQueryCon
|
|||
}, {} as EsQueryConfig);
|
||||
return paramKeysWithValues;
|
||||
};
|
||||
|
||||
/**
|
||||
* TODO: Remove when checks for specific rule type ids is not needed
|
||||
*in the codebase.
|
||||
*/
|
||||
export const isSiemRuleType = (ruleTypeId: string) => ruleTypeId.startsWith('siem.');
|
||||
|
|
|
@ -8,11 +8,10 @@
|
|||
*/
|
||||
|
||||
export const OBSERVABILITY_THRESHOLD_RULE_TYPE_ID = 'observability.rules.custom_threshold';
|
||||
export const SLO_BURN_RATE_RULE_TYPE_ID = 'slo.rules.burnRate';
|
||||
|
||||
export const METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.inventory.threshold';
|
||||
export const METRIC_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.threshold';
|
||||
export const LOG_THRESHOLD_ALERT_TYPE_ID = 'logs.alert.document.count';
|
||||
/**
|
||||
* APM rule types
|
||||
*/
|
||||
|
||||
export enum ApmRuleType {
|
||||
ErrorCount = 'apm.error_rate', // ErrorRate was renamed to ErrorCount but the key is kept as `error_rate` for backwards-compat.
|
||||
|
@ -21,5 +20,68 @@ export enum ApmRuleType {
|
|||
Anomaly = 'apm.anomaly',
|
||||
}
|
||||
|
||||
export const APM_RULE_TYPE_IDS = Object.values(ApmRuleType);
|
||||
|
||||
/**
|
||||
* Synthetics ryle types
|
||||
*/
|
||||
|
||||
export const SYNTHETICS_STATUS_RULE = 'xpack.synthetics.alerts.monitorStatus';
|
||||
export const SYNTHETICS_TLS_RULE = 'xpack.synthetics.alerts.tls';
|
||||
|
||||
export const SYNTHETICS_ALERT_RULE_TYPES = {
|
||||
MONITOR_STATUS: SYNTHETICS_STATUS_RULE,
|
||||
TLS: SYNTHETICS_TLS_RULE,
|
||||
};
|
||||
|
||||
export const SYNTHETICS_RULE_TYPE_IDS = [SYNTHETICS_STATUS_RULE, SYNTHETICS_TLS_RULE];
|
||||
|
||||
/**
|
||||
* SLO rule types
|
||||
*/
|
||||
export const SLO_BURN_RATE_RULE_TYPE_ID = 'slo.rules.burnRate';
|
||||
export const SLO_RULE_TYPE_IDS = [SLO_BURN_RATE_RULE_TYPE_ID];
|
||||
|
||||
/**
|
||||
* Metrics rule types
|
||||
*/
|
||||
export const METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.inventory.threshold';
|
||||
export const METRIC_THRESHOLD_ALERT_TYPE_ID = 'metrics.alert.threshold';
|
||||
|
||||
/**
|
||||
* Logs rule types
|
||||
*/
|
||||
export const LOG_THRESHOLD_ALERT_TYPE_ID = 'logs.alert.document.count';
|
||||
export const LOG_RULE_TYPE_IDS = [LOG_THRESHOLD_ALERT_TYPE_ID];
|
||||
|
||||
/**
|
||||
* Uptime rule types
|
||||
*/
|
||||
|
||||
export const UPTIME_RULE_TYPE_IDS = [
|
||||
'xpack.uptime.alerts.tls',
|
||||
'xpack.uptime.alerts.tlsCertificate',
|
||||
'xpack.uptime.alerts.monitorStatus',
|
||||
'xpack.uptime.alerts.durationAnomaly',
|
||||
];
|
||||
|
||||
/**
|
||||
* Infra rule types
|
||||
*/
|
||||
|
||||
export enum InfraRuleType {
|
||||
MetricThreshold = METRIC_THRESHOLD_ALERT_TYPE_ID,
|
||||
InventoryThreshold = METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID,
|
||||
}
|
||||
|
||||
export const INFRA_RULE_TYPE_IDS = Object.values(InfraRuleType);
|
||||
|
||||
export const OBSERVABILITY_RULE_TYPE_IDS = [
|
||||
...APM_RULE_TYPE_IDS,
|
||||
...SYNTHETICS_RULE_TYPE_IDS,
|
||||
...INFRA_RULE_TYPE_IDS,
|
||||
...UPTIME_RULE_TYPE_IDS,
|
||||
...LOG_RULE_TYPE_IDS,
|
||||
...SLO_RULE_TYPE_IDS,
|
||||
OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
|
||||
];
|
||||
|
|
|
@ -10,3 +10,13 @@
|
|||
export const STACK_ALERTS_FEATURE_ID = 'stackAlerts';
|
||||
export const ES_QUERY_ID = '.es-query';
|
||||
export const ML_ANOMALY_DETECTION_RULE_TYPE_ID = 'xpack.ml.anomaly_detection_alert';
|
||||
|
||||
/**
|
||||
* These rule types are not the only stack rules. There are more.
|
||||
* The variable holds all stack rule types that support multiple
|
||||
* consumers aka the "Role visibility" UX dropdown.
|
||||
*/
|
||||
export const STACK_RULE_TYPE_IDS_SUPPORTED_BY_OBSERVABILITY = [
|
||||
ES_QUERY_ID,
|
||||
ML_ANOMALY_DETECTION_RULE_TYPE_ID,
|
||||
];
|
||||
|
|
|
@ -24,3 +24,14 @@ export const QUERY_RULE_TYPE_ID = `${RULE_TYPE_PREFIX}.queryRule` as const;
|
|||
export const SAVED_QUERY_RULE_TYPE_ID = `${RULE_TYPE_PREFIX}.savedQueryRule` as const;
|
||||
export const THRESHOLD_RULE_TYPE_ID = `${RULE_TYPE_PREFIX}.thresholdRule` as const;
|
||||
export const NEW_TERMS_RULE_TYPE_ID = `${RULE_TYPE_PREFIX}.newTermsRule` as const;
|
||||
|
||||
export const SECURITY_SOLUTION_RULE_TYPE_IDS = [
|
||||
EQL_RULE_TYPE_ID,
|
||||
ESQL_RULE_TYPE_ID,
|
||||
INDICATOR_RULE_TYPE_ID,
|
||||
ML_RULE_TYPE_ID,
|
||||
QUERY_RULE_TYPE_ID,
|
||||
SAVED_QUERY_RULE_TYPE_ID,
|
||||
THRESHOLD_RULE_TYPE_ID,
|
||||
NEW_TERMS_RULE_TYPE_ID,
|
||||
];
|
||||
|
|
|
@ -25,6 +25,7 @@ export interface RuleType<
|
|||
| 'ruleTaskTimeout'
|
||||
| 'defaultScheduleInterval'
|
||||
| 'doesSetRecoveryContext'
|
||||
| 'category'
|
||||
> {
|
||||
actionVariables: ActionVariables;
|
||||
authorizedConsumers: Record<string, { read: boolean; all: boolean }>;
|
||||
|
|
|
@ -43,6 +43,7 @@ const createStartContract = (): Start => {
|
|||
create: jest.fn().mockReturnValue(Promise.resolve({})),
|
||||
toDataView: jest.fn().mockReturnValue(Promise.resolve({})),
|
||||
toDataViewLazy: jest.fn().mockReturnValue(Promise.resolve({})),
|
||||
clearInstanceCache: jest.fn(),
|
||||
} as unknown as jest.Mocked<DataViewsContract>;
|
||||
};
|
||||
|
||||
|
|
|
@ -9,9 +9,10 @@ import { Plugin, CoreSetup } from '@kbn/core/server';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
// import directly to support examples functional tests (@kbn-test/src/functional_tests/lib/babel_register_for_test_plugins.js)
|
||||
import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common';
|
||||
import { PluginSetupContract as AlertingSetup } from '@kbn/alerting-plugin/server';
|
||||
import { AlertingServerSetup } from '@kbn/alerting-plugin/server';
|
||||
import { FeaturesPluginSetup } from '@kbn/features-plugin/server';
|
||||
|
||||
import { ALERTING_FEATURE_ID } from '@kbn/alerting-plugin/common';
|
||||
import { KibanaFeatureScope } from '@kbn/features-plugin/common';
|
||||
import { ruleType as alwaysFiringRule } from './rule_types/always_firing';
|
||||
import { ruleType as peopleInSpaceRule } from './rule_types/astros';
|
||||
|
@ -22,7 +23,7 @@ import { ALERTING_EXAMPLE_APP_ID } from '../common/constants';
|
|||
|
||||
// this plugin's dependencies
|
||||
export interface AlertingExampleDeps {
|
||||
alerting: AlertingSetup;
|
||||
alerting: AlertingServerSetup;
|
||||
features: FeaturesPluginSetup;
|
||||
}
|
||||
|
||||
|
@ -43,15 +44,54 @@ export class AlertingExamplePlugin implements Plugin<void, void, AlertingExample
|
|||
},
|
||||
category: DEFAULT_APP_CATEGORIES.management,
|
||||
scope: [KibanaFeatureScope.Spaces, KibanaFeatureScope.Security],
|
||||
alerting: [alwaysFiringRule.id, peopleInSpaceRule.id, INDEX_THRESHOLD_ID],
|
||||
alerting: [
|
||||
{
|
||||
ruleTypeId: alwaysFiringRule.id,
|
||||
consumers: [ALERTING_EXAMPLE_APP_ID, ALERTING_FEATURE_ID],
|
||||
},
|
||||
{
|
||||
ruleTypeId: peopleInSpaceRule.id,
|
||||
consumers: [ALERTING_EXAMPLE_APP_ID, ALERTING_FEATURE_ID],
|
||||
},
|
||||
{
|
||||
ruleTypeId: INDEX_THRESHOLD_ID,
|
||||
consumers: [ALERTING_EXAMPLE_APP_ID, ALERTING_FEATURE_ID],
|
||||
},
|
||||
],
|
||||
privileges: {
|
||||
all: {
|
||||
alerting: {
|
||||
rule: {
|
||||
all: [alwaysFiringRule.id, peopleInSpaceRule.id, INDEX_THRESHOLD_ID],
|
||||
all: [
|
||||
{
|
||||
ruleTypeId: alwaysFiringRule.id,
|
||||
consumers: [ALERTING_EXAMPLE_APP_ID, ALERTING_FEATURE_ID],
|
||||
},
|
||||
{
|
||||
ruleTypeId: peopleInSpaceRule.id,
|
||||
consumers: [ALERTING_EXAMPLE_APP_ID, ALERTING_FEATURE_ID],
|
||||
},
|
||||
{
|
||||
ruleTypeId: INDEX_THRESHOLD_ID,
|
||||
consumers: [ALERTING_EXAMPLE_APP_ID, ALERTING_FEATURE_ID],
|
||||
},
|
||||
],
|
||||
},
|
||||
alert: {
|
||||
all: [alwaysFiringRule.id, peopleInSpaceRule.id, INDEX_THRESHOLD_ID],
|
||||
all: [
|
||||
{
|
||||
ruleTypeId: alwaysFiringRule.id,
|
||||
consumers: [ALERTING_EXAMPLE_APP_ID, ALERTING_FEATURE_ID],
|
||||
},
|
||||
{
|
||||
ruleTypeId: peopleInSpaceRule.id,
|
||||
consumers: [ALERTING_EXAMPLE_APP_ID, ALERTING_FEATURE_ID],
|
||||
},
|
||||
{
|
||||
ruleTypeId: INDEX_THRESHOLD_ID,
|
||||
consumers: [ALERTING_EXAMPLE_APP_ID, ALERTING_FEATURE_ID],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
savedObject: {
|
||||
|
@ -66,10 +106,36 @@ export class AlertingExamplePlugin implements Plugin<void, void, AlertingExample
|
|||
read: {
|
||||
alerting: {
|
||||
rule: {
|
||||
read: [alwaysFiringRule.id, peopleInSpaceRule.id, INDEX_THRESHOLD_ID],
|
||||
read: [
|
||||
{
|
||||
ruleTypeId: alwaysFiringRule.id,
|
||||
consumers: [ALERTING_EXAMPLE_APP_ID, ALERTING_FEATURE_ID],
|
||||
},
|
||||
{
|
||||
ruleTypeId: peopleInSpaceRule.id,
|
||||
consumers: [ALERTING_EXAMPLE_APP_ID, ALERTING_FEATURE_ID],
|
||||
},
|
||||
{
|
||||
ruleTypeId: INDEX_THRESHOLD_ID,
|
||||
consumers: [ALERTING_EXAMPLE_APP_ID, ALERTING_FEATURE_ID],
|
||||
},
|
||||
],
|
||||
},
|
||||
alert: {
|
||||
read: [alwaysFiringRule.id, peopleInSpaceRule.id, INDEX_THRESHOLD_ID],
|
||||
read: [
|
||||
{
|
||||
ruleTypeId: alwaysFiringRule.id,
|
||||
consumers: [ALERTING_EXAMPLE_APP_ID, ALERTING_FEATURE_ID],
|
||||
},
|
||||
{
|
||||
ruleTypeId: peopleInSpaceRule.id,
|
||||
consumers: [ALERTING_EXAMPLE_APP_ID, ALERTING_FEATURE_ID],
|
||||
},
|
||||
{
|
||||
ruleTypeId: INDEX_THRESHOLD_ID,
|
||||
consumers: [ALERTING_EXAMPLE_APP_ID, ALERTING_FEATURE_ID],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
savedObject: {
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
import React from 'react';
|
||||
import { TriggersAndActionsUIPublicPluginStart } from '@kbn/triggers-actions-ui-plugin/public';
|
||||
import { AlertsTableStateProps } from '@kbn/triggers-actions-ui-plugin/public/application/sections/alerts_table/alerts_table_state';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
|
||||
interface SandboxProps {
|
||||
triggersActionsUi: TriggersAndActionsUIPublicPluginStart;
|
||||
|
@ -19,12 +18,7 @@ export const AlertsTableSandbox = ({ triggersActionsUi }: SandboxProps) => {
|
|||
id: 'observabilityCases',
|
||||
configurationId: 'observabilityCases',
|
||||
alertsTableConfigurationRegistry,
|
||||
featureIds: [
|
||||
AlertConsumers.INFRASTRUCTURE,
|
||||
AlertConsumers.APM,
|
||||
AlertConsumers.OBSERVABILITY,
|
||||
AlertConsumers.LOGS,
|
||||
],
|
||||
ruleTypeIds: ['.es-query'],
|
||||
query: {
|
||||
bool: {
|
||||
filter: [],
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import { Plugin, CoreSetup } from '@kbn/core/server';
|
||||
|
||||
import { PluginSetupContract as ActionsSetup } from '@kbn/actions-plugin/server';
|
||||
import { PluginSetupContract as AlertingSetup } from '@kbn/alerting-plugin/server';
|
||||
import { AlertingServerSetup } from '@kbn/alerting-plugin/server';
|
||||
|
||||
import {
|
||||
getConnectorType as getSystemLogExampleConnectorType,
|
||||
|
@ -17,7 +17,7 @@ import {
|
|||
|
||||
// this plugin's dependencies
|
||||
export interface TriggersActionsUiExampleDeps {
|
||||
alerting: AlertingSetup;
|
||||
alerting: AlertingServerSetup;
|
||||
actions: ActionsSetup;
|
||||
}
|
||||
export class TriggersActionsUiExamplePlugin
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
"@kbn/alerting-plugin",
|
||||
"@kbn/triggers-actions-ui-plugin",
|
||||
"@kbn/developer-examples-plugin",
|
||||
"@kbn/rule-data-utils",
|
||||
"@kbn/data-plugin",
|
||||
"@kbn/i18n-react",
|
||||
"@kbn/shared-ux-router",
|
||||
|
|
|
@ -5,10 +5,9 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { HttpSetup } from '@kbn/core-http-browser';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { httpServiceMock } from '@kbn/core-http-browser-mocks';
|
||||
import {
|
||||
type UseAlertsHistory,
|
||||
useAlertsHistory,
|
||||
|
@ -25,6 +24,11 @@ const queryClient = new QueryClient({
|
|||
queries: { retry: false, cacheTime: 0 },
|
||||
},
|
||||
});
|
||||
|
||||
const mockServices = {
|
||||
http: httpServiceMock.createStartContract(),
|
||||
};
|
||||
|
||||
const wrapper = ({ children }: any) => (
|
||||
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
||||
);
|
||||
|
@ -34,7 +38,7 @@ describe('useAlertsHistory', () => {
|
|||
const end = '2023-05-10T00:00:00.000Z';
|
||||
const ruleId = 'cfd36e60-ef22-11ed-91eb-b7893acacfe2';
|
||||
|
||||
afterEach(() => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
|
@ -44,7 +48,7 @@ describe('useAlertsHistory', () => {
|
|||
() =>
|
||||
useAlertsHistory({
|
||||
http,
|
||||
featureIds: [AlertConsumers.APM],
|
||||
ruleTypeIds: ['apm'],
|
||||
ruleId,
|
||||
dateRange: { from: start, to: end },
|
||||
}),
|
||||
|
@ -61,16 +65,13 @@ describe('useAlertsHistory', () => {
|
|||
});
|
||||
|
||||
it('returns no data when API error', async () => {
|
||||
const http = {
|
||||
post: jest.fn().mockImplementation(() => {
|
||||
throw new Error('ES error');
|
||||
}),
|
||||
} as unknown as HttpSetup;
|
||||
mockServices.http.post.mockRejectedValueOnce(new Error('ES error'));
|
||||
|
||||
const { result, waitFor } = renderHook<useAlertsHistoryProps, UseAlertsHistory>(
|
||||
() =>
|
||||
useAlertsHistory({
|
||||
http,
|
||||
featureIds: [AlertConsumers.APM],
|
||||
...mockServices,
|
||||
ruleTypeIds: ['apm'],
|
||||
ruleId,
|
||||
dateRange: { from: start, to: end },
|
||||
}),
|
||||
|
@ -87,54 +88,53 @@ describe('useAlertsHistory', () => {
|
|||
});
|
||||
|
||||
it('returns the alert history chart data', async () => {
|
||||
const http = {
|
||||
post: jest.fn().mockResolvedValue({
|
||||
hits: { total: { value: 32, relation: 'eq' }, max_score: null, hits: [] },
|
||||
aggregations: {
|
||||
avgTimeToRecoverUS: { doc_count: 28, recoveryTime: { value: 134959464.2857143 } },
|
||||
histogramTriggeredAlerts: {
|
||||
buckets: [
|
||||
{ key_as_string: '2023-04-10T00:00:00.000Z', key: 1681084800000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-11T00:00:00.000Z', key: 1681171200000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-12T00:00:00.000Z', key: 1681257600000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-13T00:00:00.000Z', key: 1681344000000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-14T00:00:00.000Z', key: 1681430400000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-15T00:00:00.000Z', key: 1681516800000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-16T00:00:00.000Z', key: 1681603200000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-17T00:00:00.000Z', key: 1681689600000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-18T00:00:00.000Z', key: 1681776000000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-19T00:00:00.000Z', key: 1681862400000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-20T00:00:00.000Z', key: 1681948800000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-21T00:00:00.000Z', key: 1682035200000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-22T00:00:00.000Z', key: 1682121600000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-23T00:00:00.000Z', key: 1682208000000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-24T00:00:00.000Z', key: 1682294400000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-25T00:00:00.000Z', key: 1682380800000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-26T00:00:00.000Z', key: 1682467200000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-27T00:00:00.000Z', key: 1682553600000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-28T00:00:00.000Z', key: 1682640000000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-29T00:00:00.000Z', key: 1682726400000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-30T00:00:00.000Z', key: 1682812800000, doc_count: 0 },
|
||||
{ key_as_string: '2023-05-01T00:00:00.000Z', key: 1682899200000, doc_count: 0 },
|
||||
{ key_as_string: '2023-05-02T00:00:00.000Z', key: 1682985600000, doc_count: 0 },
|
||||
{ key_as_string: '2023-05-03T00:00:00.000Z', key: 1683072000000, doc_count: 0 },
|
||||
{ key_as_string: '2023-05-04T00:00:00.000Z', key: 1683158400000, doc_count: 0 },
|
||||
{ key_as_string: '2023-05-05T00:00:00.000Z', key: 1683244800000, doc_count: 0 },
|
||||
{ key_as_string: '2023-05-06T00:00:00.000Z', key: 1683331200000, doc_count: 0 },
|
||||
{ key_as_string: '2023-05-07T00:00:00.000Z', key: 1683417600000, doc_count: 0 },
|
||||
{ key_as_string: '2023-05-08T00:00:00.000Z', key: 1683504000000, doc_count: 0 },
|
||||
{ key_as_string: '2023-05-09T00:00:00.000Z', key: 1683590400000, doc_count: 0 },
|
||||
{ key_as_string: '2023-05-10T00:00:00.000Z', key: 1683676800000, doc_count: 32 },
|
||||
],
|
||||
},
|
||||
mockServices.http.post.mockResolvedValueOnce({
|
||||
hits: { total: { value: 32, relation: 'eq' }, max_score: null, hits: [] },
|
||||
aggregations: {
|
||||
avgTimeToRecoverUS: { doc_count: 28, recoveryTime: { value: 134959464.2857143 } },
|
||||
histogramTriggeredAlerts: {
|
||||
buckets: [
|
||||
{ key_as_string: '2023-04-10T00:00:00.000Z', key: 1681084800000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-11T00:00:00.000Z', key: 1681171200000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-12T00:00:00.000Z', key: 1681257600000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-13T00:00:00.000Z', key: 1681344000000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-14T00:00:00.000Z', key: 1681430400000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-15T00:00:00.000Z', key: 1681516800000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-16T00:00:00.000Z', key: 1681603200000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-17T00:00:00.000Z', key: 1681689600000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-18T00:00:00.000Z', key: 1681776000000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-19T00:00:00.000Z', key: 1681862400000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-20T00:00:00.000Z', key: 1681948800000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-21T00:00:00.000Z', key: 1682035200000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-22T00:00:00.000Z', key: 1682121600000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-23T00:00:00.000Z', key: 1682208000000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-24T00:00:00.000Z', key: 1682294400000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-25T00:00:00.000Z', key: 1682380800000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-26T00:00:00.000Z', key: 1682467200000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-27T00:00:00.000Z', key: 1682553600000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-28T00:00:00.000Z', key: 1682640000000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-29T00:00:00.000Z', key: 1682726400000, doc_count: 0 },
|
||||
{ key_as_string: '2023-04-30T00:00:00.000Z', key: 1682812800000, doc_count: 0 },
|
||||
{ key_as_string: '2023-05-01T00:00:00.000Z', key: 1682899200000, doc_count: 0 },
|
||||
{ key_as_string: '2023-05-02T00:00:00.000Z', key: 1682985600000, doc_count: 0 },
|
||||
{ key_as_string: '2023-05-03T00:00:00.000Z', key: 1683072000000, doc_count: 0 },
|
||||
{ key_as_string: '2023-05-04T00:00:00.000Z', key: 1683158400000, doc_count: 0 },
|
||||
{ key_as_string: '2023-05-05T00:00:00.000Z', key: 1683244800000, doc_count: 0 },
|
||||
{ key_as_string: '2023-05-06T00:00:00.000Z', key: 1683331200000, doc_count: 0 },
|
||||
{ key_as_string: '2023-05-07T00:00:00.000Z', key: 1683417600000, doc_count: 0 },
|
||||
{ key_as_string: '2023-05-08T00:00:00.000Z', key: 1683504000000, doc_count: 0 },
|
||||
{ key_as_string: '2023-05-09T00:00:00.000Z', key: 1683590400000, doc_count: 0 },
|
||||
{ key_as_string: '2023-05-10T00:00:00.000Z', key: 1683676800000, doc_count: 32 },
|
||||
],
|
||||
},
|
||||
}),
|
||||
} as unknown as HttpSetup;
|
||||
},
|
||||
});
|
||||
|
||||
const { result, waitFor } = renderHook<useAlertsHistoryProps, UseAlertsHistory>(
|
||||
() =>
|
||||
useAlertsHistory({
|
||||
http,
|
||||
featureIds: [AlertConsumers.APM],
|
||||
...mockServices,
|
||||
ruleTypeIds: ['apm'],
|
||||
ruleId,
|
||||
dateRange: { from: start, to: end },
|
||||
}),
|
||||
|
@ -155,26 +155,24 @@ describe('useAlertsHistory', () => {
|
|||
it('calls http post including instanceId query', async () => {
|
||||
const controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
const mockedHttpPost = jest.fn();
|
||||
const http = {
|
||||
post: mockedHttpPost.mockResolvedValue({
|
||||
hits: { total: { value: 32, relation: 'eq' }, max_score: null, hits: [] },
|
||||
aggregations: {
|
||||
avgTimeToRecoverUS: { doc_count: 28, recoveryTime: { value: 134959464.2857143 } },
|
||||
histogramTriggeredAlerts: {
|
||||
buckets: [
|
||||
{ key_as_string: '2023-04-10T00:00:00.000Z', key: 1681084800000, doc_count: 0 },
|
||||
],
|
||||
},
|
||||
mockServices.http.post.mockResolvedValueOnce({
|
||||
hits: { total: { value: 32, relation: 'eq' }, max_score: null, hits: [] },
|
||||
aggregations: {
|
||||
avgTimeToRecoverUS: { doc_count: 28, recoveryTime: { value: 134959464.2857143 } },
|
||||
histogramTriggeredAlerts: {
|
||||
buckets: [
|
||||
{ key_as_string: '2023-04-10T00:00:00.000Z', key: 1681084800000, doc_count: 0 },
|
||||
],
|
||||
},
|
||||
}),
|
||||
} as unknown as HttpSetup;
|
||||
},
|
||||
});
|
||||
|
||||
const { result, waitFor } = renderHook<useAlertsHistoryProps, UseAlertsHistory>(
|
||||
() =>
|
||||
useAlertsHistory({
|
||||
http,
|
||||
featureIds: [AlertConsumers.APM],
|
||||
...mockServices,
|
||||
ruleTypeIds: ['apm'],
|
||||
consumers: ['foo'],
|
||||
ruleId,
|
||||
dateRange: { from: start, to: end },
|
||||
instanceId: 'instance-1',
|
||||
|
@ -187,9 +185,10 @@ describe('useAlertsHistory', () => {
|
|||
await act(async () => {
|
||||
await waitFor(() => result.current.isSuccess);
|
||||
});
|
||||
expect(mockedHttpPost).toBeCalledWith('/internal/rac/alerts/find', {
|
||||
|
||||
expect(mockServices.http.post).toBeCalledWith('/internal/rac/alerts/find', {
|
||||
body:
|
||||
'{"size":0,"feature_ids":["apm"],"query":{"bool":{"must":[' +
|
||||
'{"size":0,"rule_type_ids":["apm"],"consumers":["foo"],"query":{"bool":{"must":[' +
|
||||
'{"term":{"kibana.alert.rule.uuid":"cfd36e60-ef22-11ed-91eb-b7893acacfe2"}},' +
|
||||
'{"term":{"kibana.alert.instance.id":"instance-1"}},' +
|
||||
'{"range":{"kibana.alert.time_range":{"from":"2023-04-10T00:00:00.000Z","to":"2023-05-10T00:00:00.000Z"}}}]}},' +
|
||||
|
@ -204,26 +203,23 @@ describe('useAlertsHistory', () => {
|
|||
it('calls http post without * instanceId query', async () => {
|
||||
const controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
const mockedHttpPost = jest.fn();
|
||||
const http = {
|
||||
post: mockedHttpPost.mockResolvedValue({
|
||||
hits: { total: { value: 32, relation: 'eq' }, max_score: null, hits: [] },
|
||||
aggregations: {
|
||||
avgTimeToRecoverUS: { doc_count: 28, recoveryTime: { value: 134959464.2857143 } },
|
||||
histogramTriggeredAlerts: {
|
||||
buckets: [
|
||||
{ key_as_string: '2023-04-10T00:00:00.000Z', key: 1681084800000, doc_count: 0 },
|
||||
],
|
||||
},
|
||||
mockServices.http.post.mockResolvedValueOnce({
|
||||
hits: { total: { value: 32, relation: 'eq' }, max_score: null, hits: [] },
|
||||
aggregations: {
|
||||
avgTimeToRecoverUS: { doc_count: 28, recoveryTime: { value: 134959464.2857143 } },
|
||||
histogramTriggeredAlerts: {
|
||||
buckets: [
|
||||
{ key_as_string: '2023-04-10T00:00:00.000Z', key: 1681084800000, doc_count: 0 },
|
||||
],
|
||||
},
|
||||
}),
|
||||
} as unknown as HttpSetup;
|
||||
},
|
||||
});
|
||||
|
||||
const { result, waitFor } = renderHook<useAlertsHistoryProps, UseAlertsHistory>(
|
||||
() =>
|
||||
useAlertsHistory({
|
||||
http,
|
||||
featureIds: [AlertConsumers.APM],
|
||||
...mockServices,
|
||||
ruleTypeIds: ['apm'],
|
||||
ruleId,
|
||||
dateRange: { from: start, to: end },
|
||||
instanceId: '*',
|
||||
|
@ -236,9 +232,10 @@ describe('useAlertsHistory', () => {
|
|||
await act(async () => {
|
||||
await waitFor(() => result.current.isSuccess);
|
||||
});
|
||||
expect(mockedHttpPost).toBeCalledWith('/internal/rac/alerts/find', {
|
||||
|
||||
expect(mockServices.http.post).toBeCalledWith('/internal/rac/alerts/find', {
|
||||
body:
|
||||
'{"size":0,"feature_ids":["apm"],"query":{"bool":{"must":[' +
|
||||
'{"size":0,"rule_type_ids":["apm"],"query":{"bool":{"must":[' +
|
||||
'{"term":{"kibana.alert.rule.uuid":"cfd36e60-ef22-11ed-91eb-b7893acacfe2"}},' +
|
||||
'{"range":{"kibana.alert.time_range":{"from":"2023-04-10T00:00:00.000Z","to":"2023-05-10T00:00:00.000Z"}}}]}},' +
|
||||
'"aggs":{"histogramTriggeredAlerts":{"date_histogram":{"field":"kibana.alert.start","fixed_interval":"1d",' +
|
||||
|
|
|
@ -14,14 +14,14 @@ import {
|
|||
ALERT_START,
|
||||
ALERT_STATUS,
|
||||
ALERT_TIME_RANGE,
|
||||
ValidFeatureId,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import { BASE_RAC_ALERTS_API_PATH } from '@kbn/rule-registry-plugin/common';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
export interface Props {
|
||||
http: HttpSetup | undefined;
|
||||
featureIds: ValidFeatureId[];
|
||||
ruleTypeIds: string[];
|
||||
consumers?: string[];
|
||||
ruleId: string;
|
||||
dateRange: {
|
||||
from: string;
|
||||
|
@ -49,13 +49,15 @@ export const EMPTY_ALERTS_HISTORY = {
|
|||
};
|
||||
|
||||
export function useAlertsHistory({
|
||||
featureIds,
|
||||
ruleTypeIds,
|
||||
consumers,
|
||||
ruleId,
|
||||
dateRange,
|
||||
http,
|
||||
instanceId,
|
||||
}: Props): UseAlertsHistory {
|
||||
const enabled = !!featureIds.length;
|
||||
const enabled = !!ruleTypeIds.length;
|
||||
|
||||
const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data } = useQuery({
|
||||
queryKey: ['useAlertsHistory'],
|
||||
queryFn: async ({ signal }) => {
|
||||
|
@ -63,7 +65,8 @@ export function useAlertsHistory({
|
|||
throw new Error('Http client is missing');
|
||||
}
|
||||
return fetchTriggeredAlertsHistory({
|
||||
featureIds,
|
||||
ruleTypeIds,
|
||||
consumers,
|
||||
http,
|
||||
ruleId,
|
||||
dateRange,
|
||||
|
@ -74,6 +77,7 @@ export function useAlertsHistory({
|
|||
refetchOnWindowFocus: false,
|
||||
enabled,
|
||||
});
|
||||
|
||||
return {
|
||||
data: isInitialLoading ? EMPTY_ALERTS_HISTORY : data ?? EMPTY_ALERTS_HISTORY,
|
||||
isLoading: enabled && (isInitialLoading || isLoading || isRefetching),
|
||||
|
@ -101,14 +105,16 @@ interface AggsESResponse {
|
|||
}
|
||||
|
||||
export async function fetchTriggeredAlertsHistory({
|
||||
featureIds,
|
||||
ruleTypeIds,
|
||||
consumers,
|
||||
http,
|
||||
ruleId,
|
||||
dateRange,
|
||||
signal,
|
||||
instanceId,
|
||||
}: {
|
||||
featureIds: ValidFeatureId[];
|
||||
ruleTypeIds: string[];
|
||||
consumers?: string[];
|
||||
http: HttpSetup;
|
||||
ruleId: string;
|
||||
dateRange: {
|
||||
|
@ -123,7 +129,8 @@ export async function fetchTriggeredAlertsHistory({
|
|||
signal,
|
||||
body: JSON.stringify({
|
||||
size: 0,
|
||||
feature_ids: featureIds,
|
||||
rule_type_ids: ruleTypeIds,
|
||||
consumers,
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
|
|
|
@ -17,9 +17,9 @@
|
|||
],
|
||||
"kbn_references": [
|
||||
"@kbn/i18n",
|
||||
"@kbn/core-http-browser",
|
||||
"@kbn/rule-data-utils",
|
||||
"@kbn/core",
|
||||
"@kbn/rule-registry-plugin"
|
||||
"@kbn/rule-registry-plugin",
|
||||
"@kbn/core-http-browser-mocks"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -41,6 +41,11 @@ const SECURITY_RULE_TYPES = [
|
|||
NEW_TERMS_RULE_TYPE_ID,
|
||||
];
|
||||
|
||||
const alertingFeatures = SECURITY_RULE_TYPES.map((ruleTypeId) => ({
|
||||
ruleTypeId,
|
||||
consumers: [SERVER_APP_ID],
|
||||
}));
|
||||
|
||||
export const getSecurityBaseKibanaFeature = ({
|
||||
savedObjects,
|
||||
}: SecurityFeatureParams): BaseKibanaFeatureConfig => ({
|
||||
|
@ -59,7 +64,7 @@ export const getSecurityBaseKibanaFeature = ({
|
|||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
},
|
||||
alerting: SECURITY_RULE_TYPES,
|
||||
alerting: alertingFeatures,
|
||||
description: i18n.translate(
|
||||
'securitySolutionPackages.features.featureRegistry.securityGroupDescription',
|
||||
{
|
||||
|
@ -87,12 +92,8 @@ export const getSecurityBaseKibanaFeature = ({
|
|||
read: [],
|
||||
},
|
||||
alerting: {
|
||||
rule: {
|
||||
all: SECURITY_RULE_TYPES,
|
||||
},
|
||||
alert: {
|
||||
all: SECURITY_RULE_TYPES,
|
||||
},
|
||||
rule: { all: alertingFeatures },
|
||||
alert: { all: alertingFeatures },
|
||||
},
|
||||
management: {
|
||||
insightsAndAlerting: ['triggersActions'],
|
||||
|
@ -109,10 +110,10 @@ export const getSecurityBaseKibanaFeature = ({
|
|||
},
|
||||
alerting: {
|
||||
rule: {
|
||||
read: SECURITY_RULE_TYPES,
|
||||
read: alertingFeatures,
|
||||
},
|
||||
alert: {
|
||||
all: SECURITY_RULE_TYPES,
|
||||
all: alertingFeatures,
|
||||
},
|
||||
},
|
||||
management: {
|
||||
|
|
|
@ -51,21 +51,3 @@ describe('#get', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
test('#isValid', () => {
|
||||
const alertingActions = new AlertingActions();
|
||||
expect(alertingActions.isValid('alerting:foo-ruleType/consumer/alertingType/bar-operation')).toBe(
|
||||
true
|
||||
);
|
||||
|
||||
expect(
|
||||
alertingActions.isValid('api:alerting:foo-ruleType/consumer/alertingType/bar-operation')
|
||||
).toBe(false);
|
||||
expect(alertingActions.isValid('api:foo-ruleType/consumer/alertingType/bar-operation')).toBe(
|
||||
false
|
||||
);
|
||||
|
||||
expect(alertingActions.isValid('alerting_foo-ruleType/consumer/alertingType/bar-operation')).toBe(
|
||||
false
|
||||
);
|
||||
});
|
||||
|
|
|
@ -40,12 +40,4 @@ export class AlertingActions implements AlertingActionsType {
|
|||
|
||||
return `${this.prefix}${ruleTypeId}/${consumer}/${alertingEntity}/${operation}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the action is a valid alerting action.
|
||||
* @param action The action string to check.
|
||||
*/
|
||||
public isValid(action: string) {
|
||||
return action.startsWith(this.prefix);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,7 +28,6 @@ describe(`feature_privilege_builder`, () => {
|
|||
read: [],
|
||||
},
|
||||
},
|
||||
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
|
@ -59,10 +58,9 @@ describe(`feature_privilege_builder`, () => {
|
|||
alerting: {
|
||||
rule: {
|
||||
all: [],
|
||||
read: ['alert-type'],
|
||||
read: [{ ruleTypeId: 'alert-type', consumers: ['my-consumer'] }],
|
||||
},
|
||||
},
|
||||
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
|
@ -83,15 +81,15 @@ describe(`feature_privilege_builder`, () => {
|
|||
|
||||
expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"alerting:alert-type/my-feature/rule/get",
|
||||
"alerting:alert-type/my-feature/rule/getRuleState",
|
||||
"alerting:alert-type/my-feature/rule/getAlertSummary",
|
||||
"alerting:alert-type/my-feature/rule/getExecutionLog",
|
||||
"alerting:alert-type/my-feature/rule/getActionErrorLog",
|
||||
"alerting:alert-type/my-feature/rule/find",
|
||||
"alerting:alert-type/my-feature/rule/getRuleExecutionKPI",
|
||||
"alerting:alert-type/my-feature/rule/getBackfill",
|
||||
"alerting:alert-type/my-feature/rule/findBackfill",
|
||||
"alerting:alert-type/my-consumer/rule/get",
|
||||
"alerting:alert-type/my-consumer/rule/getRuleState",
|
||||
"alerting:alert-type/my-consumer/rule/getAlertSummary",
|
||||
"alerting:alert-type/my-consumer/rule/getExecutionLog",
|
||||
"alerting:alert-type/my-consumer/rule/getActionErrorLog",
|
||||
"alerting:alert-type/my-consumer/rule/find",
|
||||
"alerting:alert-type/my-consumer/rule/getRuleExecutionKPI",
|
||||
"alerting:alert-type/my-consumer/rule/getBackfill",
|
||||
"alerting:alert-type/my-consumer/rule/findBackfill",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
@ -104,10 +102,9 @@ describe(`feature_privilege_builder`, () => {
|
|||
alerting: {
|
||||
alert: {
|
||||
all: [],
|
||||
read: ['alert-type'],
|
||||
read: [{ ruleTypeId: 'alert-type', consumers: ['my-consumer'] }],
|
||||
},
|
||||
},
|
||||
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
|
@ -128,10 +125,10 @@ describe(`feature_privilege_builder`, () => {
|
|||
|
||||
expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"alerting:alert-type/my-feature/alert/get",
|
||||
"alerting:alert-type/my-feature/alert/find",
|
||||
"alerting:alert-type/my-feature/alert/getAuthorizedAlertsIndices",
|
||||
"alerting:alert-type/my-feature/alert/getAlertSummary",
|
||||
"alerting:alert-type/my-consumer/alert/get",
|
||||
"alerting:alert-type/my-consumer/alert/find",
|
||||
"alerting:alert-type/my-consumer/alert/getAuthorizedAlertsIndices",
|
||||
"alerting:alert-type/my-consumer/alert/getAlertSummary",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
@ -144,14 +141,13 @@ describe(`feature_privilege_builder`, () => {
|
|||
alerting: {
|
||||
rule: {
|
||||
all: [],
|
||||
read: ['alert-type'],
|
||||
read: [{ ruleTypeId: 'alert-type', consumers: ['my-consumer'] }],
|
||||
},
|
||||
alert: {
|
||||
all: [],
|
||||
read: ['alert-type'],
|
||||
read: [{ ruleTypeId: 'alert-type', consumers: ['my-consumer'] }],
|
||||
},
|
||||
},
|
||||
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
|
@ -172,19 +168,19 @@ describe(`feature_privilege_builder`, () => {
|
|||
|
||||
expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"alerting:alert-type/my-feature/rule/get",
|
||||
"alerting:alert-type/my-feature/rule/getRuleState",
|
||||
"alerting:alert-type/my-feature/rule/getAlertSummary",
|
||||
"alerting:alert-type/my-feature/rule/getExecutionLog",
|
||||
"alerting:alert-type/my-feature/rule/getActionErrorLog",
|
||||
"alerting:alert-type/my-feature/rule/find",
|
||||
"alerting:alert-type/my-feature/rule/getRuleExecutionKPI",
|
||||
"alerting:alert-type/my-feature/rule/getBackfill",
|
||||
"alerting:alert-type/my-feature/rule/findBackfill",
|
||||
"alerting:alert-type/my-feature/alert/get",
|
||||
"alerting:alert-type/my-feature/alert/find",
|
||||
"alerting:alert-type/my-feature/alert/getAuthorizedAlertsIndices",
|
||||
"alerting:alert-type/my-feature/alert/getAlertSummary",
|
||||
"alerting:alert-type/my-consumer/rule/get",
|
||||
"alerting:alert-type/my-consumer/rule/getRuleState",
|
||||
"alerting:alert-type/my-consumer/rule/getAlertSummary",
|
||||
"alerting:alert-type/my-consumer/rule/getExecutionLog",
|
||||
"alerting:alert-type/my-consumer/rule/getActionErrorLog",
|
||||
"alerting:alert-type/my-consumer/rule/find",
|
||||
"alerting:alert-type/my-consumer/rule/getRuleExecutionKPI",
|
||||
"alerting:alert-type/my-consumer/rule/getBackfill",
|
||||
"alerting:alert-type/my-consumer/rule/findBackfill",
|
||||
"alerting:alert-type/my-consumer/alert/get",
|
||||
"alerting:alert-type/my-consumer/alert/find",
|
||||
"alerting:alert-type/my-consumer/alert/getAuthorizedAlertsIndices",
|
||||
"alerting:alert-type/my-consumer/alert/getAlertSummary",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
@ -196,7 +192,7 @@ describe(`feature_privilege_builder`, () => {
|
|||
const privilege: FeatureKibanaPrivileges = {
|
||||
alerting: {
|
||||
rule: {
|
||||
all: ['alert-type'],
|
||||
all: [{ ruleTypeId: 'alert-type', consumers: ['my-consumer'] }],
|
||||
read: [],
|
||||
},
|
||||
},
|
||||
|
@ -221,34 +217,34 @@ describe(`feature_privilege_builder`, () => {
|
|||
|
||||
expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"alerting:alert-type/my-feature/rule/get",
|
||||
"alerting:alert-type/my-feature/rule/getRuleState",
|
||||
"alerting:alert-type/my-feature/rule/getAlertSummary",
|
||||
"alerting:alert-type/my-feature/rule/getExecutionLog",
|
||||
"alerting:alert-type/my-feature/rule/getActionErrorLog",
|
||||
"alerting:alert-type/my-feature/rule/find",
|
||||
"alerting:alert-type/my-feature/rule/getRuleExecutionKPI",
|
||||
"alerting:alert-type/my-feature/rule/getBackfill",
|
||||
"alerting:alert-type/my-feature/rule/findBackfill",
|
||||
"alerting:alert-type/my-feature/rule/create",
|
||||
"alerting:alert-type/my-feature/rule/delete",
|
||||
"alerting:alert-type/my-feature/rule/update",
|
||||
"alerting:alert-type/my-feature/rule/updateApiKey",
|
||||
"alerting:alert-type/my-feature/rule/enable",
|
||||
"alerting:alert-type/my-feature/rule/disable",
|
||||
"alerting:alert-type/my-feature/rule/muteAll",
|
||||
"alerting:alert-type/my-feature/rule/unmuteAll",
|
||||
"alerting:alert-type/my-feature/rule/muteAlert",
|
||||
"alerting:alert-type/my-feature/rule/unmuteAlert",
|
||||
"alerting:alert-type/my-feature/rule/snooze",
|
||||
"alerting:alert-type/my-feature/rule/bulkEdit",
|
||||
"alerting:alert-type/my-feature/rule/bulkDelete",
|
||||
"alerting:alert-type/my-feature/rule/bulkEnable",
|
||||
"alerting:alert-type/my-feature/rule/bulkDisable",
|
||||
"alerting:alert-type/my-feature/rule/unsnooze",
|
||||
"alerting:alert-type/my-feature/rule/runSoon",
|
||||
"alerting:alert-type/my-feature/rule/scheduleBackfill",
|
||||
"alerting:alert-type/my-feature/rule/deleteBackfill",
|
||||
"alerting:alert-type/my-consumer/rule/get",
|
||||
"alerting:alert-type/my-consumer/rule/getRuleState",
|
||||
"alerting:alert-type/my-consumer/rule/getAlertSummary",
|
||||
"alerting:alert-type/my-consumer/rule/getExecutionLog",
|
||||
"alerting:alert-type/my-consumer/rule/getActionErrorLog",
|
||||
"alerting:alert-type/my-consumer/rule/find",
|
||||
"alerting:alert-type/my-consumer/rule/getRuleExecutionKPI",
|
||||
"alerting:alert-type/my-consumer/rule/getBackfill",
|
||||
"alerting:alert-type/my-consumer/rule/findBackfill",
|
||||
"alerting:alert-type/my-consumer/rule/create",
|
||||
"alerting:alert-type/my-consumer/rule/delete",
|
||||
"alerting:alert-type/my-consumer/rule/update",
|
||||
"alerting:alert-type/my-consumer/rule/updateApiKey",
|
||||
"alerting:alert-type/my-consumer/rule/enable",
|
||||
"alerting:alert-type/my-consumer/rule/disable",
|
||||
"alerting:alert-type/my-consumer/rule/muteAll",
|
||||
"alerting:alert-type/my-consumer/rule/unmuteAll",
|
||||
"alerting:alert-type/my-consumer/rule/muteAlert",
|
||||
"alerting:alert-type/my-consumer/rule/unmuteAlert",
|
||||
"alerting:alert-type/my-consumer/rule/snooze",
|
||||
"alerting:alert-type/my-consumer/rule/bulkEdit",
|
||||
"alerting:alert-type/my-consumer/rule/bulkDelete",
|
||||
"alerting:alert-type/my-consumer/rule/bulkEnable",
|
||||
"alerting:alert-type/my-consumer/rule/bulkDisable",
|
||||
"alerting:alert-type/my-consumer/rule/unsnooze",
|
||||
"alerting:alert-type/my-consumer/rule/runSoon",
|
||||
"alerting:alert-type/my-consumer/rule/scheduleBackfill",
|
||||
"alerting:alert-type/my-consumer/rule/deleteBackfill",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
@ -260,11 +256,10 @@ describe(`feature_privilege_builder`, () => {
|
|||
const privilege: FeatureKibanaPrivileges = {
|
||||
alerting: {
|
||||
alert: {
|
||||
all: ['alert-type'],
|
||||
all: [{ ruleTypeId: 'alert-type', consumers: ['my-consumer'] }],
|
||||
read: [],
|
||||
},
|
||||
},
|
||||
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
|
@ -285,11 +280,11 @@ describe(`feature_privilege_builder`, () => {
|
|||
|
||||
expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"alerting:alert-type/my-feature/alert/get",
|
||||
"alerting:alert-type/my-feature/alert/find",
|
||||
"alerting:alert-type/my-feature/alert/getAuthorizedAlertsIndices",
|
||||
"alerting:alert-type/my-feature/alert/getAlertSummary",
|
||||
"alerting:alert-type/my-feature/alert/update",
|
||||
"alerting:alert-type/my-consumer/alert/get",
|
||||
"alerting:alert-type/my-consumer/alert/find",
|
||||
"alerting:alert-type/my-consumer/alert/getAuthorizedAlertsIndices",
|
||||
"alerting:alert-type/my-consumer/alert/getAlertSummary",
|
||||
"alerting:alert-type/my-consumer/alert/update",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
@ -301,15 +296,14 @@ describe(`feature_privilege_builder`, () => {
|
|||
const privilege: FeatureKibanaPrivileges = {
|
||||
alerting: {
|
||||
rule: {
|
||||
all: ['alert-type'],
|
||||
all: [{ ruleTypeId: 'alert-type', consumers: ['my-consumer'] }],
|
||||
read: [],
|
||||
},
|
||||
alert: {
|
||||
all: ['alert-type'],
|
||||
all: [{ ruleTypeId: 'alert-type', consumers: ['my-consumer'] }],
|
||||
read: [],
|
||||
},
|
||||
},
|
||||
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
|
@ -330,39 +324,39 @@ describe(`feature_privilege_builder`, () => {
|
|||
|
||||
expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"alerting:alert-type/my-feature/rule/get",
|
||||
"alerting:alert-type/my-feature/rule/getRuleState",
|
||||
"alerting:alert-type/my-feature/rule/getAlertSummary",
|
||||
"alerting:alert-type/my-feature/rule/getExecutionLog",
|
||||
"alerting:alert-type/my-feature/rule/getActionErrorLog",
|
||||
"alerting:alert-type/my-feature/rule/find",
|
||||
"alerting:alert-type/my-feature/rule/getRuleExecutionKPI",
|
||||
"alerting:alert-type/my-feature/rule/getBackfill",
|
||||
"alerting:alert-type/my-feature/rule/findBackfill",
|
||||
"alerting:alert-type/my-feature/rule/create",
|
||||
"alerting:alert-type/my-feature/rule/delete",
|
||||
"alerting:alert-type/my-feature/rule/update",
|
||||
"alerting:alert-type/my-feature/rule/updateApiKey",
|
||||
"alerting:alert-type/my-feature/rule/enable",
|
||||
"alerting:alert-type/my-feature/rule/disable",
|
||||
"alerting:alert-type/my-feature/rule/muteAll",
|
||||
"alerting:alert-type/my-feature/rule/unmuteAll",
|
||||
"alerting:alert-type/my-feature/rule/muteAlert",
|
||||
"alerting:alert-type/my-feature/rule/unmuteAlert",
|
||||
"alerting:alert-type/my-feature/rule/snooze",
|
||||
"alerting:alert-type/my-feature/rule/bulkEdit",
|
||||
"alerting:alert-type/my-feature/rule/bulkDelete",
|
||||
"alerting:alert-type/my-feature/rule/bulkEnable",
|
||||
"alerting:alert-type/my-feature/rule/bulkDisable",
|
||||
"alerting:alert-type/my-feature/rule/unsnooze",
|
||||
"alerting:alert-type/my-feature/rule/runSoon",
|
||||
"alerting:alert-type/my-feature/rule/scheduleBackfill",
|
||||
"alerting:alert-type/my-feature/rule/deleteBackfill",
|
||||
"alerting:alert-type/my-feature/alert/get",
|
||||
"alerting:alert-type/my-feature/alert/find",
|
||||
"alerting:alert-type/my-feature/alert/getAuthorizedAlertsIndices",
|
||||
"alerting:alert-type/my-feature/alert/getAlertSummary",
|
||||
"alerting:alert-type/my-feature/alert/update",
|
||||
"alerting:alert-type/my-consumer/rule/get",
|
||||
"alerting:alert-type/my-consumer/rule/getRuleState",
|
||||
"alerting:alert-type/my-consumer/rule/getAlertSummary",
|
||||
"alerting:alert-type/my-consumer/rule/getExecutionLog",
|
||||
"alerting:alert-type/my-consumer/rule/getActionErrorLog",
|
||||
"alerting:alert-type/my-consumer/rule/find",
|
||||
"alerting:alert-type/my-consumer/rule/getRuleExecutionKPI",
|
||||
"alerting:alert-type/my-consumer/rule/getBackfill",
|
||||
"alerting:alert-type/my-consumer/rule/findBackfill",
|
||||
"alerting:alert-type/my-consumer/rule/create",
|
||||
"alerting:alert-type/my-consumer/rule/delete",
|
||||
"alerting:alert-type/my-consumer/rule/update",
|
||||
"alerting:alert-type/my-consumer/rule/updateApiKey",
|
||||
"alerting:alert-type/my-consumer/rule/enable",
|
||||
"alerting:alert-type/my-consumer/rule/disable",
|
||||
"alerting:alert-type/my-consumer/rule/muteAll",
|
||||
"alerting:alert-type/my-consumer/rule/unmuteAll",
|
||||
"alerting:alert-type/my-consumer/rule/muteAlert",
|
||||
"alerting:alert-type/my-consumer/rule/unmuteAlert",
|
||||
"alerting:alert-type/my-consumer/rule/snooze",
|
||||
"alerting:alert-type/my-consumer/rule/bulkEdit",
|
||||
"alerting:alert-type/my-consumer/rule/bulkDelete",
|
||||
"alerting:alert-type/my-consumer/rule/bulkEnable",
|
||||
"alerting:alert-type/my-consumer/rule/bulkDisable",
|
||||
"alerting:alert-type/my-consumer/rule/unsnooze",
|
||||
"alerting:alert-type/my-consumer/rule/runSoon",
|
||||
"alerting:alert-type/my-consumer/rule/scheduleBackfill",
|
||||
"alerting:alert-type/my-consumer/rule/deleteBackfill",
|
||||
"alerting:alert-type/my-consumer/alert/get",
|
||||
"alerting:alert-type/my-consumer/alert/find",
|
||||
"alerting:alert-type/my-consumer/alert/getAuthorizedAlertsIndices",
|
||||
"alerting:alert-type/my-consumer/alert/getAlertSummary",
|
||||
"alerting:alert-type/my-consumer/alert/update",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
@ -374,11 +368,10 @@ describe(`feature_privilege_builder`, () => {
|
|||
const privilege: FeatureKibanaPrivileges = {
|
||||
alerting: {
|
||||
rule: {
|
||||
all: ['alert-type'],
|
||||
read: ['readonly-alert-type'],
|
||||
all: [{ ruleTypeId: 'alert-type', consumers: ['my-consumer'] }],
|
||||
read: [{ ruleTypeId: 'readonly-alert-type', consumers: ['my-consumer'] }],
|
||||
},
|
||||
},
|
||||
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
|
@ -399,43 +392,43 @@ describe(`feature_privilege_builder`, () => {
|
|||
|
||||
expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"alerting:alert-type/my-feature/rule/get",
|
||||
"alerting:alert-type/my-feature/rule/getRuleState",
|
||||
"alerting:alert-type/my-feature/rule/getAlertSummary",
|
||||
"alerting:alert-type/my-feature/rule/getExecutionLog",
|
||||
"alerting:alert-type/my-feature/rule/getActionErrorLog",
|
||||
"alerting:alert-type/my-feature/rule/find",
|
||||
"alerting:alert-type/my-feature/rule/getRuleExecutionKPI",
|
||||
"alerting:alert-type/my-feature/rule/getBackfill",
|
||||
"alerting:alert-type/my-feature/rule/findBackfill",
|
||||
"alerting:alert-type/my-feature/rule/create",
|
||||
"alerting:alert-type/my-feature/rule/delete",
|
||||
"alerting:alert-type/my-feature/rule/update",
|
||||
"alerting:alert-type/my-feature/rule/updateApiKey",
|
||||
"alerting:alert-type/my-feature/rule/enable",
|
||||
"alerting:alert-type/my-feature/rule/disable",
|
||||
"alerting:alert-type/my-feature/rule/muteAll",
|
||||
"alerting:alert-type/my-feature/rule/unmuteAll",
|
||||
"alerting:alert-type/my-feature/rule/muteAlert",
|
||||
"alerting:alert-type/my-feature/rule/unmuteAlert",
|
||||
"alerting:alert-type/my-feature/rule/snooze",
|
||||
"alerting:alert-type/my-feature/rule/bulkEdit",
|
||||
"alerting:alert-type/my-feature/rule/bulkDelete",
|
||||
"alerting:alert-type/my-feature/rule/bulkEnable",
|
||||
"alerting:alert-type/my-feature/rule/bulkDisable",
|
||||
"alerting:alert-type/my-feature/rule/unsnooze",
|
||||
"alerting:alert-type/my-feature/rule/runSoon",
|
||||
"alerting:alert-type/my-feature/rule/scheduleBackfill",
|
||||
"alerting:alert-type/my-feature/rule/deleteBackfill",
|
||||
"alerting:readonly-alert-type/my-feature/rule/get",
|
||||
"alerting:readonly-alert-type/my-feature/rule/getRuleState",
|
||||
"alerting:readonly-alert-type/my-feature/rule/getAlertSummary",
|
||||
"alerting:readonly-alert-type/my-feature/rule/getExecutionLog",
|
||||
"alerting:readonly-alert-type/my-feature/rule/getActionErrorLog",
|
||||
"alerting:readonly-alert-type/my-feature/rule/find",
|
||||
"alerting:readonly-alert-type/my-feature/rule/getRuleExecutionKPI",
|
||||
"alerting:readonly-alert-type/my-feature/rule/getBackfill",
|
||||
"alerting:readonly-alert-type/my-feature/rule/findBackfill",
|
||||
"alerting:alert-type/my-consumer/rule/get",
|
||||
"alerting:alert-type/my-consumer/rule/getRuleState",
|
||||
"alerting:alert-type/my-consumer/rule/getAlertSummary",
|
||||
"alerting:alert-type/my-consumer/rule/getExecutionLog",
|
||||
"alerting:alert-type/my-consumer/rule/getActionErrorLog",
|
||||
"alerting:alert-type/my-consumer/rule/find",
|
||||
"alerting:alert-type/my-consumer/rule/getRuleExecutionKPI",
|
||||
"alerting:alert-type/my-consumer/rule/getBackfill",
|
||||
"alerting:alert-type/my-consumer/rule/findBackfill",
|
||||
"alerting:alert-type/my-consumer/rule/create",
|
||||
"alerting:alert-type/my-consumer/rule/delete",
|
||||
"alerting:alert-type/my-consumer/rule/update",
|
||||
"alerting:alert-type/my-consumer/rule/updateApiKey",
|
||||
"alerting:alert-type/my-consumer/rule/enable",
|
||||
"alerting:alert-type/my-consumer/rule/disable",
|
||||
"alerting:alert-type/my-consumer/rule/muteAll",
|
||||
"alerting:alert-type/my-consumer/rule/unmuteAll",
|
||||
"alerting:alert-type/my-consumer/rule/muteAlert",
|
||||
"alerting:alert-type/my-consumer/rule/unmuteAlert",
|
||||
"alerting:alert-type/my-consumer/rule/snooze",
|
||||
"alerting:alert-type/my-consumer/rule/bulkEdit",
|
||||
"alerting:alert-type/my-consumer/rule/bulkDelete",
|
||||
"alerting:alert-type/my-consumer/rule/bulkEnable",
|
||||
"alerting:alert-type/my-consumer/rule/bulkDisable",
|
||||
"alerting:alert-type/my-consumer/rule/unsnooze",
|
||||
"alerting:alert-type/my-consumer/rule/runSoon",
|
||||
"alerting:alert-type/my-consumer/rule/scheduleBackfill",
|
||||
"alerting:alert-type/my-consumer/rule/deleteBackfill",
|
||||
"alerting:readonly-alert-type/my-consumer/rule/get",
|
||||
"alerting:readonly-alert-type/my-consumer/rule/getRuleState",
|
||||
"alerting:readonly-alert-type/my-consumer/rule/getAlertSummary",
|
||||
"alerting:readonly-alert-type/my-consumer/rule/getExecutionLog",
|
||||
"alerting:readonly-alert-type/my-consumer/rule/getActionErrorLog",
|
||||
"alerting:readonly-alert-type/my-consumer/rule/find",
|
||||
"alerting:readonly-alert-type/my-consumer/rule/getRuleExecutionKPI",
|
||||
"alerting:readonly-alert-type/my-consumer/rule/getBackfill",
|
||||
"alerting:readonly-alert-type/my-consumer/rule/findBackfill",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
@ -447,8 +440,8 @@ describe(`feature_privilege_builder`, () => {
|
|||
const privilege: FeatureKibanaPrivileges = {
|
||||
alerting: {
|
||||
alert: {
|
||||
all: ['alert-type'],
|
||||
read: ['readonly-alert-type'],
|
||||
all: [{ ruleTypeId: 'alert-type', consumers: ['my-consumer'] }],
|
||||
read: [{ ruleTypeId: 'readonly-alert-type', consumers: ['my-consumer'] }],
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -472,15 +465,15 @@ describe(`feature_privilege_builder`, () => {
|
|||
|
||||
expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"alerting:alert-type/my-feature/alert/get",
|
||||
"alerting:alert-type/my-feature/alert/find",
|
||||
"alerting:alert-type/my-feature/alert/getAuthorizedAlertsIndices",
|
||||
"alerting:alert-type/my-feature/alert/getAlertSummary",
|
||||
"alerting:alert-type/my-feature/alert/update",
|
||||
"alerting:readonly-alert-type/my-feature/alert/get",
|
||||
"alerting:readonly-alert-type/my-feature/alert/find",
|
||||
"alerting:readonly-alert-type/my-feature/alert/getAuthorizedAlertsIndices",
|
||||
"alerting:readonly-alert-type/my-feature/alert/getAlertSummary",
|
||||
"alerting:alert-type/my-consumer/alert/get",
|
||||
"alerting:alert-type/my-consumer/alert/find",
|
||||
"alerting:alert-type/my-consumer/alert/getAuthorizedAlertsIndices",
|
||||
"alerting:alert-type/my-consumer/alert/getAlertSummary",
|
||||
"alerting:alert-type/my-consumer/alert/update",
|
||||
"alerting:readonly-alert-type/my-consumer/alert/get",
|
||||
"alerting:readonly-alert-type/my-consumer/alert/find",
|
||||
"alerting:readonly-alert-type/my-consumer/alert/getAuthorizedAlertsIndices",
|
||||
"alerting:readonly-alert-type/my-consumer/alert/getAlertSummary",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
@ -492,12 +485,12 @@ describe(`feature_privilege_builder`, () => {
|
|||
const privilege: FeatureKibanaPrivileges = {
|
||||
alerting: {
|
||||
rule: {
|
||||
all: ['alert-type'],
|
||||
read: ['readonly-alert-type'],
|
||||
all: [{ ruleTypeId: 'alert-type', consumers: ['my-consumer'] }],
|
||||
read: [{ ruleTypeId: 'readonly-alert-type', consumers: ['my-consumer'] }],
|
||||
},
|
||||
alert: {
|
||||
all: ['another-alert-type'],
|
||||
read: ['readonly-alert-type'],
|
||||
all: [{ ruleTypeId: 'another-alert-type', consumers: ['my-consumer'] }],
|
||||
read: [{ ruleTypeId: 'readonly-alert-type', consumers: ['my-consumer'] }],
|
||||
},
|
||||
},
|
||||
|
||||
|
@ -521,52 +514,278 @@ describe(`feature_privilege_builder`, () => {
|
|||
|
||||
expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"alerting:alert-type/my-feature/rule/get",
|
||||
"alerting:alert-type/my-feature/rule/getRuleState",
|
||||
"alerting:alert-type/my-feature/rule/getAlertSummary",
|
||||
"alerting:alert-type/my-feature/rule/getExecutionLog",
|
||||
"alerting:alert-type/my-feature/rule/getActionErrorLog",
|
||||
"alerting:alert-type/my-feature/rule/find",
|
||||
"alerting:alert-type/my-feature/rule/getRuleExecutionKPI",
|
||||
"alerting:alert-type/my-feature/rule/getBackfill",
|
||||
"alerting:alert-type/my-feature/rule/findBackfill",
|
||||
"alerting:alert-type/my-feature/rule/create",
|
||||
"alerting:alert-type/my-feature/rule/delete",
|
||||
"alerting:alert-type/my-feature/rule/update",
|
||||
"alerting:alert-type/my-feature/rule/updateApiKey",
|
||||
"alerting:alert-type/my-feature/rule/enable",
|
||||
"alerting:alert-type/my-feature/rule/disable",
|
||||
"alerting:alert-type/my-feature/rule/muteAll",
|
||||
"alerting:alert-type/my-feature/rule/unmuteAll",
|
||||
"alerting:alert-type/my-feature/rule/muteAlert",
|
||||
"alerting:alert-type/my-feature/rule/unmuteAlert",
|
||||
"alerting:alert-type/my-feature/rule/snooze",
|
||||
"alerting:alert-type/my-feature/rule/bulkEdit",
|
||||
"alerting:alert-type/my-feature/rule/bulkDelete",
|
||||
"alerting:alert-type/my-feature/rule/bulkEnable",
|
||||
"alerting:alert-type/my-feature/rule/bulkDisable",
|
||||
"alerting:alert-type/my-feature/rule/unsnooze",
|
||||
"alerting:alert-type/my-feature/rule/runSoon",
|
||||
"alerting:alert-type/my-feature/rule/scheduleBackfill",
|
||||
"alerting:alert-type/my-feature/rule/deleteBackfill",
|
||||
"alerting:readonly-alert-type/my-feature/rule/get",
|
||||
"alerting:readonly-alert-type/my-feature/rule/getRuleState",
|
||||
"alerting:readonly-alert-type/my-feature/rule/getAlertSummary",
|
||||
"alerting:readonly-alert-type/my-feature/rule/getExecutionLog",
|
||||
"alerting:readonly-alert-type/my-feature/rule/getActionErrorLog",
|
||||
"alerting:readonly-alert-type/my-feature/rule/find",
|
||||
"alerting:readonly-alert-type/my-feature/rule/getRuleExecutionKPI",
|
||||
"alerting:readonly-alert-type/my-feature/rule/getBackfill",
|
||||
"alerting:readonly-alert-type/my-feature/rule/findBackfill",
|
||||
"alerting:another-alert-type/my-feature/alert/get",
|
||||
"alerting:another-alert-type/my-feature/alert/find",
|
||||
"alerting:another-alert-type/my-feature/alert/getAuthorizedAlertsIndices",
|
||||
"alerting:another-alert-type/my-feature/alert/getAlertSummary",
|
||||
"alerting:another-alert-type/my-feature/alert/update",
|
||||
"alerting:readonly-alert-type/my-feature/alert/get",
|
||||
"alerting:readonly-alert-type/my-feature/alert/find",
|
||||
"alerting:readonly-alert-type/my-feature/alert/getAuthorizedAlertsIndices",
|
||||
"alerting:readonly-alert-type/my-feature/alert/getAlertSummary",
|
||||
"alerting:alert-type/my-consumer/rule/get",
|
||||
"alerting:alert-type/my-consumer/rule/getRuleState",
|
||||
"alerting:alert-type/my-consumer/rule/getAlertSummary",
|
||||
"alerting:alert-type/my-consumer/rule/getExecutionLog",
|
||||
"alerting:alert-type/my-consumer/rule/getActionErrorLog",
|
||||
"alerting:alert-type/my-consumer/rule/find",
|
||||
"alerting:alert-type/my-consumer/rule/getRuleExecutionKPI",
|
||||
"alerting:alert-type/my-consumer/rule/getBackfill",
|
||||
"alerting:alert-type/my-consumer/rule/findBackfill",
|
||||
"alerting:alert-type/my-consumer/rule/create",
|
||||
"alerting:alert-type/my-consumer/rule/delete",
|
||||
"alerting:alert-type/my-consumer/rule/update",
|
||||
"alerting:alert-type/my-consumer/rule/updateApiKey",
|
||||
"alerting:alert-type/my-consumer/rule/enable",
|
||||
"alerting:alert-type/my-consumer/rule/disable",
|
||||
"alerting:alert-type/my-consumer/rule/muteAll",
|
||||
"alerting:alert-type/my-consumer/rule/unmuteAll",
|
||||
"alerting:alert-type/my-consumer/rule/muteAlert",
|
||||
"alerting:alert-type/my-consumer/rule/unmuteAlert",
|
||||
"alerting:alert-type/my-consumer/rule/snooze",
|
||||
"alerting:alert-type/my-consumer/rule/bulkEdit",
|
||||
"alerting:alert-type/my-consumer/rule/bulkDelete",
|
||||
"alerting:alert-type/my-consumer/rule/bulkEnable",
|
||||
"alerting:alert-type/my-consumer/rule/bulkDisable",
|
||||
"alerting:alert-type/my-consumer/rule/unsnooze",
|
||||
"alerting:alert-type/my-consumer/rule/runSoon",
|
||||
"alerting:alert-type/my-consumer/rule/scheduleBackfill",
|
||||
"alerting:alert-type/my-consumer/rule/deleteBackfill",
|
||||
"alerting:readonly-alert-type/my-consumer/rule/get",
|
||||
"alerting:readonly-alert-type/my-consumer/rule/getRuleState",
|
||||
"alerting:readonly-alert-type/my-consumer/rule/getAlertSummary",
|
||||
"alerting:readonly-alert-type/my-consumer/rule/getExecutionLog",
|
||||
"alerting:readonly-alert-type/my-consumer/rule/getActionErrorLog",
|
||||
"alerting:readonly-alert-type/my-consumer/rule/find",
|
||||
"alerting:readonly-alert-type/my-consumer/rule/getRuleExecutionKPI",
|
||||
"alerting:readonly-alert-type/my-consumer/rule/getBackfill",
|
||||
"alerting:readonly-alert-type/my-consumer/rule/findBackfill",
|
||||
"alerting:another-alert-type/my-consumer/alert/get",
|
||||
"alerting:another-alert-type/my-consumer/alert/find",
|
||||
"alerting:another-alert-type/my-consumer/alert/getAuthorizedAlertsIndices",
|
||||
"alerting:another-alert-type/my-consumer/alert/getAlertSummary",
|
||||
"alerting:another-alert-type/my-consumer/alert/update",
|
||||
"alerting:readonly-alert-type/my-consumer/alert/get",
|
||||
"alerting:readonly-alert-type/my-consumer/alert/find",
|
||||
"alerting:readonly-alert-type/my-consumer/alert/getAuthorizedAlertsIndices",
|
||||
"alerting:readonly-alert-type/my-consumer/alert/getAlertSummary",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test('handles multiple rule types and consumers correctly', () => {
|
||||
const actions = new Actions();
|
||||
const alertingFeaturePrivileges = new FeaturePrivilegeAlertingBuilder(actions);
|
||||
|
||||
const privilege: FeatureKibanaPrivileges = {
|
||||
alerting: {
|
||||
rule: {
|
||||
all: [
|
||||
{ ruleTypeId: 'alert-type-1', consumers: ['my-consumer-1', 'my-consumer-2'] },
|
||||
{ ruleTypeId: 'alert-type-2', consumers: ['my-consumer-3'] },
|
||||
],
|
||||
read: [
|
||||
{
|
||||
ruleTypeId: 'readonly-alert-type-1',
|
||||
consumers: ['my-read-consumer-1', 'my-read-consumer-2'],
|
||||
},
|
||||
{
|
||||
ruleTypeId: 'readonly-alert-type-2',
|
||||
consumers: ['my-read-consumer-3', 'my-read-consumer-4'],
|
||||
},
|
||||
],
|
||||
},
|
||||
alert: {
|
||||
all: [
|
||||
{
|
||||
ruleTypeId: 'another-alert-type-1',
|
||||
consumers: ['my-consumer-another-1', 'my-consumer-another-2'],
|
||||
},
|
||||
{
|
||||
ruleTypeId: 'another-alert-type-2',
|
||||
consumers: ['my-consumer-another-3', 'my-consumer-another-1'],
|
||||
},
|
||||
],
|
||||
read: [
|
||||
{
|
||||
ruleTypeId: 'readonly-another-alert-type-1',
|
||||
consumers: ['my-read-other-consumer-1', 'my-read-other-consumer-2'],
|
||||
},
|
||||
{
|
||||
ruleTypeId: 'readonly-another-alert-type-2',
|
||||
consumers: ['my-read-other-consumer-3', 'my-read-other-consumer-4'],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
|
||||
savedObject: {
|
||||
all: [],
|
||||
read: [],
|
||||
},
|
||||
ui: [],
|
||||
};
|
||||
|
||||
const feature = new KibanaFeature({
|
||||
id: 'my-feature',
|
||||
name: 'my-feature',
|
||||
app: [],
|
||||
category: { id: 'foo', label: 'foo' },
|
||||
privileges: {
|
||||
all: privilege,
|
||||
read: privilege,
|
||||
},
|
||||
});
|
||||
|
||||
expect(alertingFeaturePrivileges.getActions(privilege, feature)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"alerting:alert-type-1/my-consumer-1/rule/get",
|
||||
"alerting:alert-type-1/my-consumer-1/rule/getRuleState",
|
||||
"alerting:alert-type-1/my-consumer-1/rule/getAlertSummary",
|
||||
"alerting:alert-type-1/my-consumer-1/rule/getExecutionLog",
|
||||
"alerting:alert-type-1/my-consumer-1/rule/getActionErrorLog",
|
||||
"alerting:alert-type-1/my-consumer-1/rule/find",
|
||||
"alerting:alert-type-1/my-consumer-1/rule/getRuleExecutionKPI",
|
||||
"alerting:alert-type-1/my-consumer-1/rule/getBackfill",
|
||||
"alerting:alert-type-1/my-consumer-1/rule/findBackfill",
|
||||
"alerting:alert-type-1/my-consumer-1/rule/create",
|
||||
"alerting:alert-type-1/my-consumer-1/rule/delete",
|
||||
"alerting:alert-type-1/my-consumer-1/rule/update",
|
||||
"alerting:alert-type-1/my-consumer-1/rule/updateApiKey",
|
||||
"alerting:alert-type-1/my-consumer-1/rule/enable",
|
||||
"alerting:alert-type-1/my-consumer-1/rule/disable",
|
||||
"alerting:alert-type-1/my-consumer-1/rule/muteAll",
|
||||
"alerting:alert-type-1/my-consumer-1/rule/unmuteAll",
|
||||
"alerting:alert-type-1/my-consumer-1/rule/muteAlert",
|
||||
"alerting:alert-type-1/my-consumer-1/rule/unmuteAlert",
|
||||
"alerting:alert-type-1/my-consumer-1/rule/snooze",
|
||||
"alerting:alert-type-1/my-consumer-1/rule/bulkEdit",
|
||||
"alerting:alert-type-1/my-consumer-1/rule/bulkDelete",
|
||||
"alerting:alert-type-1/my-consumer-1/rule/bulkEnable",
|
||||
"alerting:alert-type-1/my-consumer-1/rule/bulkDisable",
|
||||
"alerting:alert-type-1/my-consumer-1/rule/unsnooze",
|
||||
"alerting:alert-type-1/my-consumer-1/rule/runSoon",
|
||||
"alerting:alert-type-1/my-consumer-1/rule/scheduleBackfill",
|
||||
"alerting:alert-type-1/my-consumer-1/rule/deleteBackfill",
|
||||
"alerting:alert-type-1/my-consumer-2/rule/get",
|
||||
"alerting:alert-type-1/my-consumer-2/rule/getRuleState",
|
||||
"alerting:alert-type-1/my-consumer-2/rule/getAlertSummary",
|
||||
"alerting:alert-type-1/my-consumer-2/rule/getExecutionLog",
|
||||
"alerting:alert-type-1/my-consumer-2/rule/getActionErrorLog",
|
||||
"alerting:alert-type-1/my-consumer-2/rule/find",
|
||||
"alerting:alert-type-1/my-consumer-2/rule/getRuleExecutionKPI",
|
||||
"alerting:alert-type-1/my-consumer-2/rule/getBackfill",
|
||||
"alerting:alert-type-1/my-consumer-2/rule/findBackfill",
|
||||
"alerting:alert-type-1/my-consumer-2/rule/create",
|
||||
"alerting:alert-type-1/my-consumer-2/rule/delete",
|
||||
"alerting:alert-type-1/my-consumer-2/rule/update",
|
||||
"alerting:alert-type-1/my-consumer-2/rule/updateApiKey",
|
||||
"alerting:alert-type-1/my-consumer-2/rule/enable",
|
||||
"alerting:alert-type-1/my-consumer-2/rule/disable",
|
||||
"alerting:alert-type-1/my-consumer-2/rule/muteAll",
|
||||
"alerting:alert-type-1/my-consumer-2/rule/unmuteAll",
|
||||
"alerting:alert-type-1/my-consumer-2/rule/muteAlert",
|
||||
"alerting:alert-type-1/my-consumer-2/rule/unmuteAlert",
|
||||
"alerting:alert-type-1/my-consumer-2/rule/snooze",
|
||||
"alerting:alert-type-1/my-consumer-2/rule/bulkEdit",
|
||||
"alerting:alert-type-1/my-consumer-2/rule/bulkDelete",
|
||||
"alerting:alert-type-1/my-consumer-2/rule/bulkEnable",
|
||||
"alerting:alert-type-1/my-consumer-2/rule/bulkDisable",
|
||||
"alerting:alert-type-1/my-consumer-2/rule/unsnooze",
|
||||
"alerting:alert-type-1/my-consumer-2/rule/runSoon",
|
||||
"alerting:alert-type-1/my-consumer-2/rule/scheduleBackfill",
|
||||
"alerting:alert-type-1/my-consumer-2/rule/deleteBackfill",
|
||||
"alerting:alert-type-2/my-consumer-3/rule/get",
|
||||
"alerting:alert-type-2/my-consumer-3/rule/getRuleState",
|
||||
"alerting:alert-type-2/my-consumer-3/rule/getAlertSummary",
|
||||
"alerting:alert-type-2/my-consumer-3/rule/getExecutionLog",
|
||||
"alerting:alert-type-2/my-consumer-3/rule/getActionErrorLog",
|
||||
"alerting:alert-type-2/my-consumer-3/rule/find",
|
||||
"alerting:alert-type-2/my-consumer-3/rule/getRuleExecutionKPI",
|
||||
"alerting:alert-type-2/my-consumer-3/rule/getBackfill",
|
||||
"alerting:alert-type-2/my-consumer-3/rule/findBackfill",
|
||||
"alerting:alert-type-2/my-consumer-3/rule/create",
|
||||
"alerting:alert-type-2/my-consumer-3/rule/delete",
|
||||
"alerting:alert-type-2/my-consumer-3/rule/update",
|
||||
"alerting:alert-type-2/my-consumer-3/rule/updateApiKey",
|
||||
"alerting:alert-type-2/my-consumer-3/rule/enable",
|
||||
"alerting:alert-type-2/my-consumer-3/rule/disable",
|
||||
"alerting:alert-type-2/my-consumer-3/rule/muteAll",
|
||||
"alerting:alert-type-2/my-consumer-3/rule/unmuteAll",
|
||||
"alerting:alert-type-2/my-consumer-3/rule/muteAlert",
|
||||
"alerting:alert-type-2/my-consumer-3/rule/unmuteAlert",
|
||||
"alerting:alert-type-2/my-consumer-3/rule/snooze",
|
||||
"alerting:alert-type-2/my-consumer-3/rule/bulkEdit",
|
||||
"alerting:alert-type-2/my-consumer-3/rule/bulkDelete",
|
||||
"alerting:alert-type-2/my-consumer-3/rule/bulkEnable",
|
||||
"alerting:alert-type-2/my-consumer-3/rule/bulkDisable",
|
||||
"alerting:alert-type-2/my-consumer-3/rule/unsnooze",
|
||||
"alerting:alert-type-2/my-consumer-3/rule/runSoon",
|
||||
"alerting:alert-type-2/my-consumer-3/rule/scheduleBackfill",
|
||||
"alerting:alert-type-2/my-consumer-3/rule/deleteBackfill",
|
||||
"alerting:readonly-alert-type-1/my-read-consumer-1/rule/get",
|
||||
"alerting:readonly-alert-type-1/my-read-consumer-1/rule/getRuleState",
|
||||
"alerting:readonly-alert-type-1/my-read-consumer-1/rule/getAlertSummary",
|
||||
"alerting:readonly-alert-type-1/my-read-consumer-1/rule/getExecutionLog",
|
||||
"alerting:readonly-alert-type-1/my-read-consumer-1/rule/getActionErrorLog",
|
||||
"alerting:readonly-alert-type-1/my-read-consumer-1/rule/find",
|
||||
"alerting:readonly-alert-type-1/my-read-consumer-1/rule/getRuleExecutionKPI",
|
||||
"alerting:readonly-alert-type-1/my-read-consumer-1/rule/getBackfill",
|
||||
"alerting:readonly-alert-type-1/my-read-consumer-1/rule/findBackfill",
|
||||
"alerting:readonly-alert-type-1/my-read-consumer-2/rule/get",
|
||||
"alerting:readonly-alert-type-1/my-read-consumer-2/rule/getRuleState",
|
||||
"alerting:readonly-alert-type-1/my-read-consumer-2/rule/getAlertSummary",
|
||||
"alerting:readonly-alert-type-1/my-read-consumer-2/rule/getExecutionLog",
|
||||
"alerting:readonly-alert-type-1/my-read-consumer-2/rule/getActionErrorLog",
|
||||
"alerting:readonly-alert-type-1/my-read-consumer-2/rule/find",
|
||||
"alerting:readonly-alert-type-1/my-read-consumer-2/rule/getRuleExecutionKPI",
|
||||
"alerting:readonly-alert-type-1/my-read-consumer-2/rule/getBackfill",
|
||||
"alerting:readonly-alert-type-1/my-read-consumer-2/rule/findBackfill",
|
||||
"alerting:readonly-alert-type-2/my-read-consumer-3/rule/get",
|
||||
"alerting:readonly-alert-type-2/my-read-consumer-3/rule/getRuleState",
|
||||
"alerting:readonly-alert-type-2/my-read-consumer-3/rule/getAlertSummary",
|
||||
"alerting:readonly-alert-type-2/my-read-consumer-3/rule/getExecutionLog",
|
||||
"alerting:readonly-alert-type-2/my-read-consumer-3/rule/getActionErrorLog",
|
||||
"alerting:readonly-alert-type-2/my-read-consumer-3/rule/find",
|
||||
"alerting:readonly-alert-type-2/my-read-consumer-3/rule/getRuleExecutionKPI",
|
||||
"alerting:readonly-alert-type-2/my-read-consumer-3/rule/getBackfill",
|
||||
"alerting:readonly-alert-type-2/my-read-consumer-3/rule/findBackfill",
|
||||
"alerting:readonly-alert-type-2/my-read-consumer-4/rule/get",
|
||||
"alerting:readonly-alert-type-2/my-read-consumer-4/rule/getRuleState",
|
||||
"alerting:readonly-alert-type-2/my-read-consumer-4/rule/getAlertSummary",
|
||||
"alerting:readonly-alert-type-2/my-read-consumer-4/rule/getExecutionLog",
|
||||
"alerting:readonly-alert-type-2/my-read-consumer-4/rule/getActionErrorLog",
|
||||
"alerting:readonly-alert-type-2/my-read-consumer-4/rule/find",
|
||||
"alerting:readonly-alert-type-2/my-read-consumer-4/rule/getRuleExecutionKPI",
|
||||
"alerting:readonly-alert-type-2/my-read-consumer-4/rule/getBackfill",
|
||||
"alerting:readonly-alert-type-2/my-read-consumer-4/rule/findBackfill",
|
||||
"alerting:another-alert-type-1/my-consumer-another-1/alert/get",
|
||||
"alerting:another-alert-type-1/my-consumer-another-1/alert/find",
|
||||
"alerting:another-alert-type-1/my-consumer-another-1/alert/getAuthorizedAlertsIndices",
|
||||
"alerting:another-alert-type-1/my-consumer-another-1/alert/getAlertSummary",
|
||||
"alerting:another-alert-type-1/my-consumer-another-1/alert/update",
|
||||
"alerting:another-alert-type-1/my-consumer-another-2/alert/get",
|
||||
"alerting:another-alert-type-1/my-consumer-another-2/alert/find",
|
||||
"alerting:another-alert-type-1/my-consumer-another-2/alert/getAuthorizedAlertsIndices",
|
||||
"alerting:another-alert-type-1/my-consumer-another-2/alert/getAlertSummary",
|
||||
"alerting:another-alert-type-1/my-consumer-another-2/alert/update",
|
||||
"alerting:another-alert-type-2/my-consumer-another-3/alert/get",
|
||||
"alerting:another-alert-type-2/my-consumer-another-3/alert/find",
|
||||
"alerting:another-alert-type-2/my-consumer-another-3/alert/getAuthorizedAlertsIndices",
|
||||
"alerting:another-alert-type-2/my-consumer-another-3/alert/getAlertSummary",
|
||||
"alerting:another-alert-type-2/my-consumer-another-3/alert/update",
|
||||
"alerting:another-alert-type-2/my-consumer-another-1/alert/get",
|
||||
"alerting:another-alert-type-2/my-consumer-another-1/alert/find",
|
||||
"alerting:another-alert-type-2/my-consumer-another-1/alert/getAuthorizedAlertsIndices",
|
||||
"alerting:another-alert-type-2/my-consumer-another-1/alert/getAlertSummary",
|
||||
"alerting:another-alert-type-2/my-consumer-another-1/alert/update",
|
||||
"alerting:readonly-another-alert-type-1/my-read-other-consumer-1/alert/get",
|
||||
"alerting:readonly-another-alert-type-1/my-read-other-consumer-1/alert/find",
|
||||
"alerting:readonly-another-alert-type-1/my-read-other-consumer-1/alert/getAuthorizedAlertsIndices",
|
||||
"alerting:readonly-another-alert-type-1/my-read-other-consumer-1/alert/getAlertSummary",
|
||||
"alerting:readonly-another-alert-type-1/my-read-other-consumer-2/alert/get",
|
||||
"alerting:readonly-another-alert-type-1/my-read-other-consumer-2/alert/find",
|
||||
"alerting:readonly-another-alert-type-1/my-read-other-consumer-2/alert/getAuthorizedAlertsIndices",
|
||||
"alerting:readonly-another-alert-type-1/my-read-other-consumer-2/alert/getAlertSummary",
|
||||
"alerting:readonly-another-alert-type-2/my-read-other-consumer-3/alert/get",
|
||||
"alerting:readonly-another-alert-type-2/my-read-other-consumer-3/alert/find",
|
||||
"alerting:readonly-another-alert-type-2/my-read-other-consumer-3/alert/getAuthorizedAlertsIndices",
|
||||
"alerting:readonly-another-alert-type-2/my-read-other-consumer-3/alert/getAlertSummary",
|
||||
"alerting:readonly-another-alert-type-2/my-read-other-consumer-4/alert/get",
|
||||
"alerting:readonly-another-alert-type-2/my-read-other-consumer-4/alert/find",
|
||||
"alerting:readonly-another-alert-type-2/my-read-other-consumer-4/alert/getAuthorizedAlertsIndices",
|
||||
"alerting:readonly-another-alert-type-2/my-read-other-consumer-4/alert/getAlertSummary",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { get, uniq } from 'lodash';
|
||||
|
||||
import type { AlertingKibanaPrivilege } from '@kbn/features-plugin/common/alerting_kibana_privilege';
|
||||
import type { FeatureKibanaPrivileges, KibanaFeature } from '@kbn/features-plugin/server';
|
||||
|
||||
import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder';
|
||||
|
@ -67,13 +68,14 @@ export class FeaturePrivilegeAlertingBuilder extends BaseFeaturePrivilegeBuilder
|
|||
): string[] {
|
||||
const getAlertingPrivilege = (
|
||||
operations: string[],
|
||||
privilegedTypes: readonly string[],
|
||||
alertingEntity: string,
|
||||
consumer: string
|
||||
privileges: AlertingKibanaPrivilege,
|
||||
alertingEntity: string
|
||||
) =>
|
||||
privilegedTypes.flatMap((type) =>
|
||||
operations.map((operation) =>
|
||||
this.actions.alerting.get(type, consumer, alertingEntity, operation)
|
||||
privileges.flatMap(({ ruleTypeId, consumers }) =>
|
||||
consumers.flatMap((consumer) =>
|
||||
operations.map((operation) =>
|
||||
this.actions.alerting.get(ruleTypeId, consumer, alertingEntity, operation)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
|
@ -82,8 +84,8 @@ export class FeaturePrivilegeAlertingBuilder extends BaseFeaturePrivilegeBuilder
|
|||
const read = get(privilegeDefinition.alerting, `${entity}.read`) ?? [];
|
||||
|
||||
return uniq([
|
||||
...getAlertingPrivilege(allOperations[entity], all, entity, feature.id),
|
||||
...getAlertingPrivilege(readOperations[entity], read, entity, feature.id),
|
||||
...getAlertingPrivilege(allOperations[entity], all, entity),
|
||||
...getAlertingPrivilege(readOperations[entity], read, entity),
|
||||
]);
|
||||
};
|
||||
|
||||
|
|
|
@ -481,7 +481,7 @@ describe('features', () => {
|
|||
name: 'Feature Alpha',
|
||||
app: [],
|
||||
category: { id: 'alpha', label: 'alpha' },
|
||||
alerting: ['rule-type-1'],
|
||||
alerting: [{ ruleTypeId: 'rule-type-1', consumers: ['alpha'] }],
|
||||
privileges: {
|
||||
all: {
|
||||
savedObject: {
|
||||
|
@ -491,7 +491,7 @@ describe('features', () => {
|
|||
ui: ['all-alpha-ui'],
|
||||
app: ['all-alpha-app'],
|
||||
api: ['all-alpha-api'],
|
||||
alerting: { rule: { all: ['rule-type-1'] } },
|
||||
alerting: { rule: { all: [{ ruleTypeId: 'rule-type-1', consumers: ['alpha'] }] } },
|
||||
replacedBy: [{ feature: 'beta', privileges: ['all'] }],
|
||||
},
|
||||
read: {
|
||||
|
@ -514,7 +514,7 @@ describe('features', () => {
|
|||
name: 'Feature Beta',
|
||||
app: [],
|
||||
category: { id: 'beta', label: 'beta' },
|
||||
alerting: ['rule-type-1'],
|
||||
alerting: [{ ruleTypeId: 'rule-type-1', consumers: ['beta'] }],
|
||||
privileges: {
|
||||
all: {
|
||||
savedObject: {
|
||||
|
@ -524,7 +524,7 @@ describe('features', () => {
|
|||
ui: ['all-beta-ui'],
|
||||
app: ['all-beta-app'],
|
||||
api: ['all-beta-api'],
|
||||
alerting: { rule: { all: ['rule-type-1'] } },
|
||||
alerting: { rule: { all: [{ ruleTypeId: 'rule-type-1', consumers: ['beta'] }] } },
|
||||
},
|
||||
read: {
|
||||
savedObject: {
|
||||
|
@ -604,13 +604,8 @@ describe('features', () => {
|
|||
...alertingOperations.map((operation) =>
|
||||
actions.alerting.get('rule-type-1', 'alpha', 'rule', operation)
|
||||
),
|
||||
// To maintain compatibility with the new UI capabilities and new alerting entities that are
|
||||
// feature specific: all.replacedBy: [{ feature: 'beta', privileges: ['all'] }]
|
||||
actions.ui.get('navLinks', 'all-beta-app'),
|
||||
actions.ui.get('beta', 'all-beta-ui'),
|
||||
...alertingOperations.map((operation) =>
|
||||
actions.alerting.get('rule-type-1', 'beta', 'rule', operation)
|
||||
),
|
||||
];
|
||||
|
||||
const expectedReadPrivileges = [
|
||||
|
|
|
@ -94,17 +94,15 @@ export function privilegesFactory(
|
|||
// If a privilege is configured with `replacedBy`, it's part of the deprecated feature and
|
||||
// should be complemented with the subset of actions from the referenced privileges to
|
||||
// maintain backward compatibility. Namely, deprecated privileges should grant the same UI
|
||||
// capabilities and alerting actions as the privileges that replace them, so that the
|
||||
// client-side code can safely use only non-deprecated UI capabilities and users can still
|
||||
// access previously created alerting rules and alerts.
|
||||
// capabilities as the privileges that replace them, so that the client-side code can safely
|
||||
// use only non-deprecated UI capabilities.
|
||||
const replacedBy = getReplacedByForPrivilege(privilegeId, privilege);
|
||||
if (replacedBy) {
|
||||
composablePrivileges.push({
|
||||
featureId: feature.id,
|
||||
privilegeId,
|
||||
references: replacedBy,
|
||||
actionsFilter: (action) =>
|
||||
actions.ui.isValid(action) || actions.alerting.isValid(action),
|
||||
actionsFilter: (action) => actions.ui.isValid(action),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -24,7 +24,8 @@ export const aggregateRulesRequestBodySchema = schema.object({
|
|||
)
|
||||
),
|
||||
filter: schema.maybe(schema.string()),
|
||||
filter_consumers: schema.maybe(schema.arrayOf(schema.string())),
|
||||
rule_type_ids: schema.maybe(schema.arrayOf(schema.string())),
|
||||
consumers: schema.maybe(schema.arrayOf(schema.string())),
|
||||
});
|
||||
|
||||
export const aggregateRulesResponseBodySchema = schema.object({
|
||||
|
|
|
@ -9,5 +9,5 @@ import { schema } from '@kbn/config-schema';
|
|||
|
||||
export const bulkUntrackByQueryBodySchema = schema.object({
|
||||
query: schema.arrayOf(schema.any()),
|
||||
feature_ids: schema.arrayOf(schema.string()),
|
||||
rule_type_ids: schema.arrayOf(schema.string()),
|
||||
});
|
||||
|
|
|
@ -8,8 +8,13 @@
|
|||
export { findRulesRequestQuerySchema } from './schemas/latest';
|
||||
export type { FindRulesRequestQuery, FindRulesResponse } from './types/latest';
|
||||
|
||||
export { findRulesRequestQuerySchema as findRulesRequestQuerySchemaV1 } from './schemas/v1';
|
||||
export {
|
||||
findRulesRequestQuerySchema as findRulesRequestQuerySchemaV1,
|
||||
findRulesInternalRequestBodySchema as findRulesInternalRequestBodySchemaV1,
|
||||
} from './schemas/v1';
|
||||
|
||||
export type {
|
||||
FindRulesRequestQuery as FindRulesRequestQueryV1,
|
||||
FindRulesInternalRequestBody as FindRulesInternalRequestBodyV1,
|
||||
FindRulesResponse as FindRulesResponseV1,
|
||||
} from './types/latest';
|
||||
|
|
|
@ -103,3 +103,35 @@ export const findRulesRequestQuerySchema = schema.object({
|
|||
)
|
||||
),
|
||||
});
|
||||
|
||||
export const findRulesInternalRequestBodySchema = schema.object({
|
||||
per_page: schema.number({
|
||||
defaultValue: 10,
|
||||
min: 0,
|
||||
}),
|
||||
page: schema.number({
|
||||
defaultValue: 1,
|
||||
min: 1,
|
||||
}),
|
||||
search: schema.maybe(schema.string()),
|
||||
default_search_operator: schema.oneOf([schema.literal('OR'), schema.literal('AND')], {
|
||||
defaultValue: 'OR',
|
||||
}),
|
||||
search_fields: schema.maybe(schema.oneOf([schema.arrayOf(schema.string()), schema.string()])),
|
||||
sort_field: schema.maybe(schema.string()),
|
||||
sort_order: schema.maybe(schema.oneOf([schema.literal('asc'), schema.literal('desc')])),
|
||||
has_reference: schema.maybe(
|
||||
// use nullable as maybe is currently broken
|
||||
// in config-schema
|
||||
schema.nullable(
|
||||
schema.object({
|
||||
type: schema.string(),
|
||||
id: schema.string(),
|
||||
})
|
||||
)
|
||||
),
|
||||
fields: schema.maybe(schema.arrayOf(schema.string())),
|
||||
filter: schema.maybe(schema.string()),
|
||||
rule_type_ids: schema.maybe(schema.arrayOf(schema.string())),
|
||||
consumers: schema.maybe(schema.arrayOf(schema.string())),
|
||||
});
|
||||
|
|
|
@ -7,9 +7,10 @@
|
|||
|
||||
import type { TypeOf } from '@kbn/config-schema';
|
||||
import { RuleParamsV1, RuleResponseV1 } from '../../../response';
|
||||
import { findRulesRequestQuerySchemaV1 } from '..';
|
||||
import { findRulesRequestQuerySchemaV1, findRulesInternalRequestBodySchemaV1 } from '..';
|
||||
|
||||
export type FindRulesRequestQuery = TypeOf<typeof findRulesRequestQuerySchemaV1>;
|
||||
export type FindRulesInternalRequestBody = TypeOf<typeof findRulesInternalRequestBodySchemaV1>;
|
||||
|
||||
export interface FindRulesResponse<Params extends RuleParamsV1 = never> {
|
||||
body: {
|
||||
|
|
|
@ -31,7 +31,6 @@ import {
|
|||
} from '@elastic/eui';
|
||||
import { TIMEZONE_OPTIONS as UI_TIMEZONE_OPTIONS } from '@kbn/core-ui-settings-common';
|
||||
import { DEFAULT_APP_CATEGORIES } from '@kbn/core-application-common';
|
||||
import type { ValidFeatureId } from '@kbn/rule-data-utils';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import type { IHttpFetchError } from '@kbn/core-http-browser';
|
||||
import type { KibanaServerError } from '@kbn/kibana-utils-plugin/public';
|
||||
|
@ -73,7 +72,10 @@ const useDefaultTimezone = () => {
|
|||
}
|
||||
return { defaultTimezone: kibanaTz, isBrowser: false };
|
||||
};
|
||||
const TIMEZONE_OPTIONS = UI_TIMEZONE_OPTIONS.map((n) => ({ label: n })) ?? [{ label: 'UTC' }];
|
||||
|
||||
const TIMEZONE_OPTIONS = UI_TIMEZONE_OPTIONS.map((timezoneOption) => ({
|
||||
label: timezoneOption,
|
||||
})) ?? [{ label: 'UTC' }];
|
||||
|
||||
export const CreateMaintenanceWindowForm = React.memo<CreateMaintenanceWindowFormProps>((props) => {
|
||||
const { onCancel, onSuccess, initialValue, maintenanceWindowId } = props;
|
||||
|
@ -203,6 +205,7 @@ export const CreateMaintenanceWindowForm = React.memo<CreateMaintenanceWindowFor
|
|||
form,
|
||||
watch: ['recurring', 'timezone', 'categoryIds', 'scopedQuery'],
|
||||
});
|
||||
|
||||
const isRecurring = recurring || false;
|
||||
const showTimezone = isBrowser || initialValue?.timezone !== undefined;
|
||||
|
||||
|
@ -222,20 +225,20 @@ export const CreateMaintenanceWindowForm = React.memo<CreateMaintenanceWindowFor
|
|||
return [...new Set(validRuleTypes.map((ruleType) => ruleType.category))];
|
||||
}, [validRuleTypes]);
|
||||
|
||||
const featureIds = useMemo(() => {
|
||||
const ruleTypeIds = useMemo(() => {
|
||||
if (!Array.isArray(validRuleTypes) || !Array.isArray(categoryIds) || !mounted) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const featureIdsSet = new Set<ValidFeatureId>();
|
||||
const uniqueRuleTypeIds = new Set<string>();
|
||||
|
||||
validRuleTypes.forEach((ruleType) => {
|
||||
if (categoryIds.includes(ruleType.category)) {
|
||||
featureIdsSet.add(ruleType.producer as ValidFeatureId);
|
||||
uniqueRuleTypeIds.add(ruleType.id);
|
||||
}
|
||||
});
|
||||
|
||||
return [...featureIdsSet];
|
||||
return [...uniqueRuleTypeIds];
|
||||
}, [validRuleTypes, categoryIds, mounted]);
|
||||
|
||||
const onCategoryIdsChange = useCallback(
|
||||
|
@ -472,7 +475,7 @@ export const CreateMaintenanceWindowForm = React.memo<CreateMaintenanceWindowFor
|
|||
<UseField path="scopedQuery">
|
||||
{() => (
|
||||
<MaintenanceWindowScopedQuery
|
||||
featureIds={featureIds}
|
||||
ruleTypeIds={ruleTypeIds}
|
||||
query={query}
|
||||
filters={filters}
|
||||
isLoading={isLoadingRuleTypes}
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
import React from 'react';
|
||||
import { screen } from '@testing-library/react';
|
||||
import type { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import { AppMockRenderer, createAppMockRenderer } from '../../../lib/test_utils';
|
||||
import { MaintenanceWindowScopedQuery } from './maintenance_window_scoped_query';
|
||||
|
||||
|
@ -47,7 +46,7 @@ describe('MaintenanceWindowScopedQuery', () => {
|
|||
it('renders correctly', () => {
|
||||
appMockRenderer.render(
|
||||
<MaintenanceWindowScopedQuery
|
||||
featureIds={['observability', 'management', 'securitySolution'] as AlertConsumers[]}
|
||||
ruleTypeIds={['apm', '.es-query', 'siem.esqlRule']}
|
||||
query={''}
|
||||
filters={[]}
|
||||
onQueryChange={jest.fn()}
|
||||
|
@ -60,7 +59,7 @@ describe('MaintenanceWindowScopedQuery', () => {
|
|||
it('should hide the search bar if isEnabled is false', () => {
|
||||
appMockRenderer.render(
|
||||
<MaintenanceWindowScopedQuery
|
||||
featureIds={['observability', 'management', 'securitySolution'] as AlertConsumers[]}
|
||||
ruleTypeIds={['apm', '.es-query', 'siem.esqlRule']}
|
||||
isEnabled={false}
|
||||
query={''}
|
||||
filters={[]}
|
||||
|
@ -74,7 +73,7 @@ describe('MaintenanceWindowScopedQuery', () => {
|
|||
it('should render loading if isLoading is true', () => {
|
||||
appMockRenderer.render(
|
||||
<MaintenanceWindowScopedQuery
|
||||
featureIds={['observability', 'management', 'securitySolution'] as AlertConsumers[]}
|
||||
ruleTypeIds={['apm', '.es-query', 'siem.esqlRule']}
|
||||
isLoading={true}
|
||||
query={''}
|
||||
filters={[]}
|
||||
|
|
|
@ -7,14 +7,13 @@
|
|||
import React, { useCallback } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiLoadingSpinner } from '@elastic/eui';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import type { Filter } from '@kbn/es-query';
|
||||
import { AlertsSearchBar } from '@kbn/alerts-ui-shared';
|
||||
import { PLUGIN } from '../../../../common/constants/plugin';
|
||||
import { useKibana } from '../../../utils/kibana_react';
|
||||
|
||||
export interface MaintenanceWindowScopedQueryProps {
|
||||
featureIds: AlertConsumers[];
|
||||
ruleTypeIds: string[];
|
||||
query: string;
|
||||
filters: Filter[];
|
||||
errors?: string[];
|
||||
|
@ -27,7 +26,7 @@ export interface MaintenanceWindowScopedQueryProps {
|
|||
export const MaintenanceWindowScopedQuery = React.memo(
|
||||
(props: MaintenanceWindowScopedQueryProps) => {
|
||||
const {
|
||||
featureIds,
|
||||
ruleTypeIds,
|
||||
query,
|
||||
filters,
|
||||
errors = [],
|
||||
|
@ -76,7 +75,7 @@ export const MaintenanceWindowScopedQuery = React.memo(
|
|||
<EuiFormRow fullWidth isInvalid={errors.length !== 0} error={errors[0]}>
|
||||
<AlertsSearchBar
|
||||
appName={PLUGIN.getI18nName(i18n)}
|
||||
featureIds={featureIds}
|
||||
ruleTypeIds={ruleTypeIds}
|
||||
disableQueryLanguageSwitcher={true}
|
||||
query={query}
|
||||
filters={filters}
|
||||
|
|
|
@ -15,58 +15,78 @@ import { featuresPluginMock } from '@kbn/features-plugin/server/mocks';
|
|||
|
||||
jest.mock('./authorization/alerting_authorization');
|
||||
|
||||
const features = featuresPluginMock.createStart();
|
||||
describe('AlertingAuthorizationClientFactory', () => {
|
||||
const features = featuresPluginMock.createStart();
|
||||
const securityPluginStart = securityMock.createStart();
|
||||
const alertingAuthorizationClientFactoryParams: jest.Mocked<AlertingAuthorizationClientFactoryOpts> =
|
||||
{
|
||||
ruleTypeRegistry: ruleTypeRegistryMock.create(),
|
||||
getSpace: jest.fn(),
|
||||
getSpaceId: jest.fn(),
|
||||
features,
|
||||
};
|
||||
|
||||
const securityPluginSetup = securityMock.createSetup();
|
||||
const securityPluginStart = securityMock.createStart();
|
||||
|
||||
const alertingAuthorizationClientFactoryParams: jest.Mocked<AlertingAuthorizationClientFactoryOpts> =
|
||||
{
|
||||
ruleTypeRegistry: ruleTypeRegistryMock.create(),
|
||||
getSpace: jest.fn(),
|
||||
getSpaceId: jest.fn(),
|
||||
features,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
test('creates an alerting authorization client with proper constructor arguments when security is enabled', async () => {
|
||||
const factory = new AlertingAuthorizationClientFactory();
|
||||
factory.initialize({
|
||||
securityPluginSetup,
|
||||
securityPluginStart,
|
||||
...alertingAuthorizationClientFactoryParams,
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
const request = mockRouter.createKibanaRequest();
|
||||
|
||||
factory.create(request);
|
||||
it('creates an alerting authorization client with proper constructor arguments when security is enabled', async () => {
|
||||
const factory = new AlertingAuthorizationClientFactory();
|
||||
|
||||
const { AlertingAuthorization } = jest.requireMock('./authorization/alerting_authorization');
|
||||
expect(AlertingAuthorization).toHaveBeenCalledWith({
|
||||
request,
|
||||
authorization: securityPluginStart.authz,
|
||||
ruleTypeRegistry: alertingAuthorizationClientFactoryParams.ruleTypeRegistry,
|
||||
features: alertingAuthorizationClientFactoryParams.features,
|
||||
getSpace: expect.any(Function),
|
||||
getSpaceId: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
test('creates an alerting authorization client with proper constructor arguments', async () => {
|
||||
const factory = new AlertingAuthorizationClientFactory();
|
||||
factory.initialize(alertingAuthorizationClientFactoryParams);
|
||||
const request = mockRouter.createKibanaRequest();
|
||||
|
||||
factory.create(request);
|
||||
|
||||
const { AlertingAuthorization } = jest.requireMock('./authorization/alerting_authorization');
|
||||
expect(AlertingAuthorization).toHaveBeenCalledWith({
|
||||
request,
|
||||
ruleTypeRegistry: alertingAuthorizationClientFactoryParams.ruleTypeRegistry,
|
||||
features: alertingAuthorizationClientFactoryParams.features,
|
||||
getSpace: expect.any(Function),
|
||||
getSpaceId: expect.any(Function),
|
||||
factory.initialize({
|
||||
securityPluginStart,
|
||||
...alertingAuthorizationClientFactoryParams,
|
||||
});
|
||||
|
||||
const request = mockRouter.createKibanaRequest();
|
||||
|
||||
await factory.create(request);
|
||||
|
||||
const { AlertingAuthorization } = jest.requireMock('./authorization/alerting_authorization');
|
||||
expect(AlertingAuthorization.create).toHaveBeenCalledWith({
|
||||
request,
|
||||
authorization: securityPluginStart.authz,
|
||||
ruleTypeRegistry: alertingAuthorizationClientFactoryParams.ruleTypeRegistry,
|
||||
features: alertingAuthorizationClientFactoryParams.features,
|
||||
getSpace: expect.any(Function),
|
||||
getSpaceId: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('creates an alerting authorization client with proper constructor arguments', async () => {
|
||||
const factory = new AlertingAuthorizationClientFactory();
|
||||
factory.initialize(alertingAuthorizationClientFactoryParams);
|
||||
const request = mockRouter.createKibanaRequest();
|
||||
|
||||
await factory.create(request);
|
||||
|
||||
const { AlertingAuthorization } = jest.requireMock('./authorization/alerting_authorization');
|
||||
expect(AlertingAuthorization.create).toHaveBeenCalledWith({
|
||||
request,
|
||||
ruleTypeRegistry: alertingAuthorizationClientFactoryParams.ruleTypeRegistry,
|
||||
features: alertingAuthorizationClientFactoryParams.features,
|
||||
getSpace: expect.any(Function),
|
||||
getSpaceId: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('throws when trying to initialize again and it is already initialized', async () => {
|
||||
const factory = new AlertingAuthorizationClientFactory();
|
||||
factory.initialize(alertingAuthorizationClientFactoryParams);
|
||||
|
||||
expect(() =>
|
||||
factory.initialize(alertingAuthorizationClientFactoryParams)
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"AlertingAuthorizationClientFactory already initialized"`
|
||||
);
|
||||
});
|
||||
|
||||
it('throws when trying to create an instance and the factory is not initialized', async () => {
|
||||
const request = mockRouter.createKibanaRequest();
|
||||
const factory = new AlertingAuthorizationClientFactory();
|
||||
|
||||
await expect(() => factory.create(request)).rejects.toThrowErrorMatchingInlineSnapshot(
|
||||
`"AlertingAuthorizationClientFactory must be initialized before calling create"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
|
||||
import { KibanaRequest } from '@kbn/core/server';
|
||||
import { SecurityPluginSetup, SecurityPluginStart } from '@kbn/security-plugin/server';
|
||||
import { SecurityPluginStart } from '@kbn/security-plugin/server';
|
||||
import { FeaturesPluginStart } from '@kbn/features-plugin/server';
|
||||
import { Space } from '@kbn/spaces-plugin/server';
|
||||
import { AlertingAuthorization } from './authorization/alerting_authorization';
|
||||
|
@ -14,7 +14,6 @@ import { RuleTypeRegistry } from './types';
|
|||
|
||||
export interface AlertingAuthorizationClientFactoryOpts {
|
||||
ruleTypeRegistry: RuleTypeRegistry;
|
||||
securityPluginSetup?: SecurityPluginSetup;
|
||||
securityPluginStart?: SecurityPluginStart;
|
||||
getSpace: (request: KibanaRequest) => Promise<Space | undefined>;
|
||||
getSpaceId: (request: KibanaRequest) => string;
|
||||
|
@ -23,33 +22,39 @@ export interface AlertingAuthorizationClientFactoryOpts {
|
|||
|
||||
export class AlertingAuthorizationClientFactory {
|
||||
private isInitialized = false;
|
||||
private ruleTypeRegistry!: RuleTypeRegistry;
|
||||
private securityPluginStart?: SecurityPluginStart;
|
||||
private features!: FeaturesPluginStart;
|
||||
private getSpace!: (request: KibanaRequest) => Promise<Space | undefined>;
|
||||
private getSpaceId!: (request: KibanaRequest) => string;
|
||||
// The reason this is protected is because we'll get type collisions otherwise because we're using a type guard assert
|
||||
// to ensure the options member is instantiated before using it in various places
|
||||
// See for more info: https://stackoverflow.com/questions/66206180/typescript-typeguard-attribut-with-method
|
||||
protected options?: AlertingAuthorizationClientFactoryOpts;
|
||||
|
||||
public initialize(options: AlertingAuthorizationClientFactoryOpts) {
|
||||
if (this.isInitialized) {
|
||||
throw new Error('AlertingAuthorizationClientFactory already initialized');
|
||||
}
|
||||
this.isInitialized = true;
|
||||
this.getSpace = options.getSpace;
|
||||
this.ruleTypeRegistry = options.ruleTypeRegistry;
|
||||
this.securityPluginStart = options.securityPluginStart;
|
||||
this.features = options.features;
|
||||
this.getSpaceId = options.getSpaceId;
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
public create(request: KibanaRequest): AlertingAuthorization {
|
||||
const { securityPluginStart, features } = this;
|
||||
return new AlertingAuthorization({
|
||||
authorization: securityPluginStart?.authz,
|
||||
public async create(request: KibanaRequest): Promise<AlertingAuthorization> {
|
||||
this.validateInitialization();
|
||||
|
||||
return AlertingAuthorization.create({
|
||||
authorization: this.options.securityPluginStart?.authz,
|
||||
request,
|
||||
getSpace: this.getSpace,
|
||||
getSpaceId: this.getSpaceId,
|
||||
ruleTypeRegistry: this.ruleTypeRegistry,
|
||||
features: features!,
|
||||
getSpace: this.options.getSpace,
|
||||
getSpaceId: this.options.getSpaceId,
|
||||
ruleTypeRegistry: this.options.ruleTypeRegistry,
|
||||
features: this.options.features,
|
||||
});
|
||||
}
|
||||
|
||||
private validateInitialization(): asserts this is this & {
|
||||
options: AlertingAuthorizationClientFactoryOpts;
|
||||
} {
|
||||
if (!this.isInitialized || this.options == null) {
|
||||
throw new Error(
|
||||
'AlertingAuthorizationClientFactory must be initialized before calling create'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,10 +15,8 @@ import { setAlertsToUntracked } from './set_alerts_to_untracked';
|
|||
let clusterClient: ElasticsearchClientMock;
|
||||
let logger: ReturnType<(typeof loggingSystemMock)['createLogger']>;
|
||||
|
||||
const getAuthorizedRuleTypesMock = jest.fn();
|
||||
|
||||
const getAllAuthorizedRuleTypesFindOperationMock = jest.fn();
|
||||
const getAlertIndicesAliasMock = jest.fn();
|
||||
|
||||
const ensureAuthorizedMock = jest.fn();
|
||||
|
||||
describe('setAlertsToUntracked()', () => {
|
||||
|
@ -362,11 +360,16 @@ describe('setAlertsToUntracked()', () => {
|
|||
});
|
||||
|
||||
test('should untrack by query', async () => {
|
||||
getAuthorizedRuleTypesMock.mockResolvedValue([
|
||||
{
|
||||
id: 'test-rule-type',
|
||||
},
|
||||
]);
|
||||
getAllAuthorizedRuleTypesFindOperationMock.mockResolvedValue(
|
||||
new Map([
|
||||
[
|
||||
'test-rule-type',
|
||||
{
|
||||
id: 'test-rule-type',
|
||||
},
|
||||
],
|
||||
])
|
||||
);
|
||||
getAlertIndicesAliasMock.mockResolvedValue(['test-alert-index']);
|
||||
|
||||
clusterClient.search.mockResponseOnce({
|
||||
|
@ -441,9 +444,9 @@ describe('setAlertsToUntracked()', () => {
|
|||
},
|
||||
},
|
||||
],
|
||||
featureIds: ['o11y'],
|
||||
ruleTypeIds: ['my-rule-type-id'],
|
||||
spaceId: 'default',
|
||||
getAuthorizedRuleTypes: getAuthorizedRuleTypesMock,
|
||||
getAllAuthorizedRuleTypesFindOperation: getAllAuthorizedRuleTypesFindOperationMock,
|
||||
getAlertIndicesAlias: getAlertIndicesAliasMock,
|
||||
ensureAuthorized: ensureAuthorizedMock,
|
||||
logger,
|
||||
|
@ -527,11 +530,16 @@ describe('setAlertsToUntracked()', () => {
|
|||
});
|
||||
|
||||
test('should return an empty array if the search returns zero results', async () => {
|
||||
getAuthorizedRuleTypesMock.mockResolvedValue([
|
||||
{
|
||||
id: 'test-rule-type',
|
||||
},
|
||||
]);
|
||||
getAllAuthorizedRuleTypesFindOperationMock.mockResolvedValue(
|
||||
new Map([
|
||||
[
|
||||
'test-rule-type',
|
||||
{
|
||||
id: 'test-rule-type',
|
||||
},
|
||||
],
|
||||
])
|
||||
);
|
||||
getAlertIndicesAliasMock.mockResolvedValue(['test-alert-index']);
|
||||
|
||||
clusterClient.search.mockResponseOnce({
|
||||
|
@ -575,9 +583,9 @@ describe('setAlertsToUntracked()', () => {
|
|||
},
|
||||
},
|
||||
],
|
||||
featureIds: ['o11y'],
|
||||
ruleTypeIds: ['my-rule-type-id'],
|
||||
spaceId: 'default',
|
||||
getAuthorizedRuleTypes: getAuthorizedRuleTypesMock,
|
||||
getAllAuthorizedRuleTypesFindOperation: getAllAuthorizedRuleTypesFindOperationMock,
|
||||
getAlertIndicesAlias: getAlertIndicesAliasMock,
|
||||
ensureAuthorized: ensureAuthorizedMock,
|
||||
logger,
|
||||
|
|
|
@ -21,8 +21,8 @@ import {
|
|||
AlertStatus,
|
||||
} from '@kbn/rule-data-utils';
|
||||
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { AlertingAuthorizationEntity } from '../../authorization/alerting_authorization';
|
||||
import type { RulesClientContext } from '../../rules_client';
|
||||
import { AlertingAuthorizationEntity } from '../../authorization/types';
|
||||
|
||||
type EnsureAuthorized = (opts: { ruleTypeId: string; consumer: string }) => Promise<unknown>;
|
||||
|
||||
|
@ -32,9 +32,9 @@ export interface SetAlertsToUntrackedParams {
|
|||
alertUuids?: string[];
|
||||
query?: QueryDslQueryContainer[];
|
||||
spaceId?: RulesClientContext['spaceId'];
|
||||
featureIds?: string[];
|
||||
ruleTypeIds?: string[];
|
||||
isUsingQuery?: boolean;
|
||||
getAuthorizedRuleTypes?: RulesClientContext['authorization']['getAuthorizedRuleTypes'];
|
||||
getAllAuthorizedRuleTypesFindOperation?: RulesClientContext['authorization']['getAllAuthorizedRuleTypesFindOperation'];
|
||||
getAlertIndicesAlias?: RulesClientContext['getAlertIndicesAlias'];
|
||||
ensureAuthorized?: EnsureAuthorized;
|
||||
}
|
||||
|
@ -155,21 +155,26 @@ const ensureAuthorizedToUntrack = async (params: SetAlertsToUntrackedParamsWithD
|
|||
};
|
||||
|
||||
const getAuthorizedAlertsIndices = async ({
|
||||
featureIds,
|
||||
getAuthorizedRuleTypes,
|
||||
ruleTypeIds,
|
||||
getAllAuthorizedRuleTypesFindOperation,
|
||||
getAlertIndicesAlias,
|
||||
spaceId,
|
||||
logger,
|
||||
}: SetAlertsToUntrackedParamsWithDep) => {
|
||||
try {
|
||||
const authorizedRuleTypes =
|
||||
(await getAuthorizedRuleTypes?.(AlertingAuthorizationEntity.Alert, new Set(featureIds))) ||
|
||||
[];
|
||||
const indices = getAlertIndicesAlias?.(
|
||||
authorizedRuleTypes.map((art: { id: string }) => art.id),
|
||||
spaceId
|
||||
);
|
||||
return indices;
|
||||
const authorizedRuleTypes = await getAllAuthorizedRuleTypesFindOperation?.({
|
||||
authorizationEntity: AlertingAuthorizationEntity.Alert,
|
||||
ruleTypeIds,
|
||||
});
|
||||
|
||||
if (authorizedRuleTypes) {
|
||||
return getAlertIndicesAlias?.(
|
||||
Array.from(authorizedRuleTypes.keys()).map((id) => id),
|
||||
spaceId
|
||||
);
|
||||
}
|
||||
|
||||
return [];
|
||||
} catch (error) {
|
||||
const errMessage = `Failed to get authorized rule types to untrack alerts by query: ${error}`;
|
||||
logger.error(errMessage);
|
||||
|
|
|
@ -241,12 +241,15 @@ describe('findBackfill()', () => {
|
|||
test('should successfully find backfill with no filter', async () => {
|
||||
const result = await rulesClient.findBackfill({ page: 1, perPage: 10 });
|
||||
|
||||
expect(authorization.getFindAuthorizationFilter).toHaveBeenCalledWith('rule', {
|
||||
fieldNames: {
|
||||
consumer: 'ad_hoc_run_params.attributes.rule.consumer',
|
||||
ruleTypeId: 'ad_hoc_run_params.attributes.rule.alertTypeId',
|
||||
expect(authorization.getFindAuthorizationFilter).toHaveBeenCalledWith({
|
||||
authorizationEntity: 'rule',
|
||||
filterOpts: {
|
||||
fieldNames: {
|
||||
consumer: 'ad_hoc_run_params.attributes.rule.consumer',
|
||||
ruleTypeId: 'ad_hoc_run_params.attributes.rule.alertTypeId',
|
||||
},
|
||||
type: 'kql',
|
||||
},
|
||||
type: 'kql',
|
||||
});
|
||||
|
||||
expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledWith({
|
||||
|
@ -286,12 +289,15 @@ describe('findBackfill()', () => {
|
|||
test('should successfully find backfill with rule id', async () => {
|
||||
const result = await rulesClient.findBackfill({ page: 1, perPage: 10, ruleIds: 'abc' });
|
||||
|
||||
expect(authorization.getFindAuthorizationFilter).toHaveBeenCalledWith('rule', {
|
||||
fieldNames: {
|
||||
consumer: 'ad_hoc_run_params.attributes.rule.consumer',
|
||||
ruleTypeId: 'ad_hoc_run_params.attributes.rule.alertTypeId',
|
||||
expect(authorization.getFindAuthorizationFilter).toHaveBeenCalledWith({
|
||||
authorizationEntity: 'rule',
|
||||
filterOpts: {
|
||||
fieldNames: {
|
||||
consumer: 'ad_hoc_run_params.attributes.rule.consumer',
|
||||
ruleTypeId: 'ad_hoc_run_params.attributes.rule.alertTypeId',
|
||||
},
|
||||
type: 'kql',
|
||||
},
|
||||
type: 'kql',
|
||||
});
|
||||
|
||||
expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledWith({
|
||||
|
@ -336,12 +342,15 @@ describe('findBackfill()', () => {
|
|||
start: '2024-03-29T02:07:55Z',
|
||||
});
|
||||
|
||||
expect(authorization.getFindAuthorizationFilter).toHaveBeenCalledWith('rule', {
|
||||
fieldNames: {
|
||||
consumer: 'ad_hoc_run_params.attributes.rule.consumer',
|
||||
ruleTypeId: 'ad_hoc_run_params.attributes.rule.alertTypeId',
|
||||
expect(authorization.getFindAuthorizationFilter).toHaveBeenCalledWith({
|
||||
authorizationEntity: 'rule',
|
||||
filterOpts: {
|
||||
fieldNames: {
|
||||
consumer: 'ad_hoc_run_params.attributes.rule.consumer',
|
||||
ruleTypeId: 'ad_hoc_run_params.attributes.rule.alertTypeId',
|
||||
},
|
||||
type: 'kql',
|
||||
},
|
||||
type: 'kql',
|
||||
});
|
||||
|
||||
expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledWith({
|
||||
|
@ -400,12 +409,15 @@ describe('findBackfill()', () => {
|
|||
end: '2024-03-29T02:07:55Z',
|
||||
});
|
||||
|
||||
expect(authorization.getFindAuthorizationFilter).toHaveBeenCalledWith('rule', {
|
||||
fieldNames: {
|
||||
consumer: 'ad_hoc_run_params.attributes.rule.consumer',
|
||||
ruleTypeId: 'ad_hoc_run_params.attributes.rule.alertTypeId',
|
||||
expect(authorization.getFindAuthorizationFilter).toHaveBeenCalledWith({
|
||||
authorizationEntity: 'rule',
|
||||
filterOpts: {
|
||||
fieldNames: {
|
||||
consumer: 'ad_hoc_run_params.attributes.rule.consumer',
|
||||
ruleTypeId: 'ad_hoc_run_params.attributes.rule.alertTypeId',
|
||||
},
|
||||
type: 'kql',
|
||||
},
|
||||
type: 'kql',
|
||||
});
|
||||
|
||||
expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledWith({
|
||||
|
@ -465,12 +477,15 @@ describe('findBackfill()', () => {
|
|||
end: '2024-03-29T02:07:55Z',
|
||||
});
|
||||
|
||||
expect(authorization.getFindAuthorizationFilter).toHaveBeenCalledWith('rule', {
|
||||
fieldNames: {
|
||||
consumer: 'ad_hoc_run_params.attributes.rule.consumer',
|
||||
ruleTypeId: 'ad_hoc_run_params.attributes.rule.alertTypeId',
|
||||
expect(authorization.getFindAuthorizationFilter).toHaveBeenCalledWith({
|
||||
authorizationEntity: 'rule',
|
||||
filterOpts: {
|
||||
fieldNames: {
|
||||
consumer: 'ad_hoc_run_params.attributes.rule.consumer',
|
||||
ruleTypeId: 'ad_hoc_run_params.attributes.rule.alertTypeId',
|
||||
},
|
||||
type: 'kql',
|
||||
},
|
||||
type: 'kql',
|
||||
});
|
||||
|
||||
expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledWith({
|
||||
|
@ -546,12 +561,15 @@ describe('findBackfill()', () => {
|
|||
ruleIds: 'abc',
|
||||
});
|
||||
|
||||
expect(authorization.getFindAuthorizationFilter).toHaveBeenCalledWith('rule', {
|
||||
fieldNames: {
|
||||
consumer: 'ad_hoc_run_params.attributes.rule.consumer',
|
||||
ruleTypeId: 'ad_hoc_run_params.attributes.rule.alertTypeId',
|
||||
expect(authorization.getFindAuthorizationFilter).toHaveBeenCalledWith({
|
||||
authorizationEntity: 'rule',
|
||||
filterOpts: {
|
||||
fieldNames: {
|
||||
consumer: 'ad_hoc_run_params.attributes.rule.consumer',
|
||||
ruleTypeId: 'ad_hoc_run_params.attributes.rule.alertTypeId',
|
||||
},
|
||||
type: 'kql',
|
||||
},
|
||||
type: 'kql',
|
||||
});
|
||||
|
||||
expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledWith({
|
||||
|
@ -627,12 +645,15 @@ describe('findBackfill()', () => {
|
|||
sortOrder: 'asc',
|
||||
});
|
||||
|
||||
expect(authorization.getFindAuthorizationFilter).toHaveBeenCalledWith('rule', {
|
||||
fieldNames: {
|
||||
consumer: 'ad_hoc_run_params.attributes.rule.consumer',
|
||||
ruleTypeId: 'ad_hoc_run_params.attributes.rule.alertTypeId',
|
||||
expect(authorization.getFindAuthorizationFilter).toHaveBeenCalledWith({
|
||||
authorizationEntity: 'rule',
|
||||
filterOpts: {
|
||||
fieldNames: {
|
||||
consumer: 'ad_hoc_run_params.attributes.rule.consumer',
|
||||
ruleTypeId: 'ad_hoc_run_params.attributes.rule.alertTypeId',
|
||||
},
|
||||
type: 'kql',
|
||||
},
|
||||
type: 'kql',
|
||||
});
|
||||
|
||||
expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledWith({
|
||||
|
|
|
@ -40,16 +40,16 @@ export async function findBackfill(
|
|||
|
||||
let authorizationTuple;
|
||||
try {
|
||||
authorizationTuple = await context.authorization.getFindAuthorizationFilter(
|
||||
AlertingAuthorizationEntity.Rule,
|
||||
{
|
||||
authorizationTuple = await context.authorization.getFindAuthorizationFilter({
|
||||
authorizationEntity: AlertingAuthorizationEntity.Rule,
|
||||
filterOpts: {
|
||||
type: AlertingAuthorizationFilterType.KQL,
|
||||
fieldNames: {
|
||||
ruleTypeId: 'ad_hoc_run_params.attributes.rule.alertTypeId',
|
||||
consumer: 'ad_hoc_run_params.attributes.rule.consumer',
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
context.auditLogger?.log(
|
||||
adHocRunAuditEvent({
|
||||
|
|
|
@ -251,12 +251,15 @@ describe('scheduleBackfill()', () => {
|
|||
const mockData = [getMockData(), getMockData({ ruleId: '2', end: '2023-11-17T08:00:00.000Z' })];
|
||||
const result = await rulesClient.scheduleBackfill(mockData);
|
||||
|
||||
expect(authorization.getFindAuthorizationFilter).toHaveBeenCalledWith('rule', {
|
||||
fieldNames: {
|
||||
consumer: 'alert.attributes.consumer',
|
||||
ruleTypeId: 'alert.attributes.alertTypeId',
|
||||
expect(authorization.getFindAuthorizationFilter).toHaveBeenCalledWith({
|
||||
authorizationEntity: 'rule',
|
||||
filterOpts: {
|
||||
fieldNames: {
|
||||
consumer: 'alert.attributes.consumer',
|
||||
ruleTypeId: 'alert.attributes.alertTypeId',
|
||||
},
|
||||
type: 'kql',
|
||||
},
|
||||
type: 'kql',
|
||||
});
|
||||
|
||||
expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledWith({
|
||||
|
|
|
@ -46,10 +46,10 @@ export async function scheduleBackfill(
|
|||
|
||||
let authorizationTuple;
|
||||
try {
|
||||
authorizationTuple = await context.authorization.getFindAuthorizationFilter(
|
||||
AlertingAuthorizationEntity.Rule,
|
||||
alertingAuthorizationFilterOpts
|
||||
);
|
||||
authorizationTuple = await context.authorization.getFindAuthorizationFilter({
|
||||
authorizationEntity: AlertingAuthorizationEntity.Rule,
|
||||
filterOpts: alertingAuthorizationFilterOpts,
|
||||
});
|
||||
} catch (error) {
|
||||
context.auditLogger?.log(
|
||||
ruleAuditEvent({
|
||||
|
|
|
@ -23,7 +23,7 @@ import { auditLoggerMock } from '@kbn/security-plugin/server/audit/mocks';
|
|||
import { getBeforeSetup, setGlobalDate } from '../../../../rules_client/tests/lib';
|
||||
|
||||
import { RegistryRuleType } from '../../../../rule_type_registry';
|
||||
import { fromKueryExpression, nodeTypes } from '@kbn/es-query';
|
||||
import { fromKueryExpression, nodeTypes, toKqlExpression } from '@kbn/es-query';
|
||||
import { RecoveredActionGroup } from '../../../../../common';
|
||||
import { DefaultRuleAggregationResult } from '../../../../routes/rule/apis/aggregate/types';
|
||||
import { defaultRuleAggregationFactory } from '.';
|
||||
|
@ -78,24 +78,28 @@ beforeEach(() => {
|
|||
setGlobalDate();
|
||||
|
||||
describe('aggregate()', () => {
|
||||
const listedTypes = new Set<RegistryRuleType>([
|
||||
{
|
||||
actionGroups: [],
|
||||
actionVariables: undefined,
|
||||
defaultActionGroupId: 'default',
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: true,
|
||||
recoveryActionGroup: RecoveredActionGroup,
|
||||
id: 'myType',
|
||||
name: 'myType',
|
||||
category: 'test',
|
||||
producer: 'myApp',
|
||||
enabledInLicense: true,
|
||||
hasAlertsMappings: false,
|
||||
hasFieldsForAAD: false,
|
||||
validLegacyConsumers: [],
|
||||
},
|
||||
const listedTypes = new Map<string, RegistryRuleType>([
|
||||
[
|
||||
'myType',
|
||||
{
|
||||
actionGroups: [],
|
||||
actionVariables: undefined,
|
||||
defaultActionGroupId: 'default',
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: true,
|
||||
recoveryActionGroup: RecoveredActionGroup,
|
||||
id: 'myType',
|
||||
name: 'myType',
|
||||
category: 'test',
|
||||
producer: 'myApp',
|
||||
enabledInLicense: true,
|
||||
hasAlertsMappings: false,
|
||||
hasFieldsForAAD: false,
|
||||
validLegacyConsumers: [],
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
beforeEach(() => {
|
||||
authorization.getFindAuthorizationFilter.mockResolvedValue({
|
||||
ensureRuleTypeIsAuthorized() {},
|
||||
|
@ -161,26 +165,16 @@ describe('aggregate()', () => {
|
|||
});
|
||||
|
||||
ruleTypeRegistry.list.mockReturnValue(listedTypes);
|
||||
authorization.filterByRuleTypeAuthorization.mockResolvedValue(
|
||||
new Set([
|
||||
{
|
||||
id: 'myType',
|
||||
name: 'Test',
|
||||
actionGroups: [{ id: 'default', name: 'Default' }],
|
||||
defaultActionGroupId: 'default',
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: true,
|
||||
recoveryActionGroup: RecoveredActionGroup,
|
||||
category: 'test',
|
||||
producer: 'alerts',
|
||||
authorizedConsumers: {
|
||||
myApp: { read: true, all: true },
|
||||
authorization.getAuthorizedRuleTypes.mockResolvedValue(
|
||||
new Map([
|
||||
[
|
||||
'myType',
|
||||
{
|
||||
authorizedConsumers: {
|
||||
myApp: { read: true, all: true },
|
||||
},
|
||||
},
|
||||
enabledInLicense: true,
|
||||
hasAlertsMappings: false,
|
||||
hasFieldsForAAD: false,
|
||||
validLegacyConsumers: [],
|
||||
},
|
||||
],
|
||||
])
|
||||
);
|
||||
});
|
||||
|
@ -394,6 +388,34 @@ describe('aggregate()', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
test('combines the filters with the auth filter correctly', async () => {
|
||||
const authFilter = fromKueryExpression(
|
||||
'alert.attributes.alertTypeId:myType and alert.attributes.consumer:myApp'
|
||||
);
|
||||
|
||||
authorization.getFindAuthorizationFilter.mockResolvedValue({
|
||||
filter: authFilter,
|
||||
ensureRuleTypeIsAuthorized() {},
|
||||
});
|
||||
|
||||
const rulesClient = new RulesClient(rulesClientParams);
|
||||
await rulesClient.aggregate({
|
||||
options: {
|
||||
ruleTypeIds: ['my-rule-type-id'],
|
||||
consumers: ['bar'],
|
||||
filter: `alert.attributes.tags: ['bar']`,
|
||||
},
|
||||
aggs: defaultRuleAggregationFactory(),
|
||||
});
|
||||
|
||||
const finalFilter = unsecuredSavedObjectsClient.find.mock.calls[0][0].filter;
|
||||
|
||||
expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledTimes(1);
|
||||
expect(toKqlExpression(finalFilter)).toMatchInlineSnapshot(
|
||||
`"((alert.attributes.tags: ['bar'] AND alert.attributes.alertTypeId: my-rule-type-id AND alert.attributes.consumer: bar) AND (alert.attributes.alertTypeId: myType AND alert.attributes.consumer: myApp))"`
|
||||
);
|
||||
});
|
||||
|
||||
test('logs audit event when not authorized to aggregate rules', async () => {
|
||||
const rulesClient = new RulesClient({ ...rulesClientParams, auditLogger });
|
||||
authorization.getFindAuthorizationFilter.mockRejectedValue(new Error('Unauthorized'));
|
||||
|
|
|
@ -5,8 +5,13 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { KueryNode, nodeBuilder } from '@kbn/es-query';
|
||||
import { isEmpty } from 'lodash';
|
||||
import type { KueryNode } from '@kbn/es-query';
|
||||
import {
|
||||
buildConsumersFilter,
|
||||
buildRuleTypeIdsFilter,
|
||||
combineFilterWithAuthorizationFilter,
|
||||
combineFilters,
|
||||
} from '../../../../rules_client/common/filters';
|
||||
import { findRulesSo } from '../../../../data/rule';
|
||||
import { AlertingAuthorizationEntity } from '../../../../authorization';
|
||||
import { ruleAuditEvent, RuleAuditAction } from '../../../../rules_client/common/audit_events';
|
||||
|
@ -22,15 +27,15 @@ export async function aggregateRules<T = Record<string, unknown>>(
|
|||
params: AggregateParams<T>
|
||||
): Promise<T> {
|
||||
const { options = {}, aggs } = params;
|
||||
const { filter, page = 1, perPage = 0, filterConsumers, ...restOptions } = options;
|
||||
const { filter, page = 1, perPage = 0, ruleTypeIds, consumers, ...restOptions } = options;
|
||||
|
||||
let authorizationTuple;
|
||||
try {
|
||||
authorizationTuple = await context.authorization.getFindAuthorizationFilter(
|
||||
AlertingAuthorizationEntity.Rule,
|
||||
alertingAuthorizationFilterOpts,
|
||||
isEmpty(filterConsumers) ? undefined : new Set(filterConsumers)
|
||||
);
|
||||
authorizationTuple = await context.authorization.getFindAuthorizationFilter({
|
||||
authorizationEntity: AlertingAuthorizationEntity.Rule,
|
||||
filterOpts: alertingAuthorizationFilterOpts,
|
||||
});
|
||||
|
||||
validateRuleAggregationFields(aggs);
|
||||
aggregateOptionsSchema.validate(options);
|
||||
} catch (error) {
|
||||
|
@ -45,15 +50,21 @@ export async function aggregateRules<T = Record<string, unknown>>(
|
|||
|
||||
const { filter: authorizationFilter } = authorizationTuple;
|
||||
const filterKueryNode = buildKueryNodeFilter(filter);
|
||||
const ruleTypeIdsFilter = buildRuleTypeIdsFilter(ruleTypeIds);
|
||||
const consumersFilter = buildConsumersFilter(consumers);
|
||||
const combinedFilters = combineFilters(
|
||||
[filterKueryNode, ruleTypeIdsFilter, consumersFilter],
|
||||
'and'
|
||||
);
|
||||
|
||||
const { aggregations } = await findRulesSo<T>({
|
||||
savedObjectsClient: context.unsecuredSavedObjectsClient,
|
||||
savedObjectsFindOptions: {
|
||||
...restOptions,
|
||||
filter:
|
||||
authorizationFilter && filterKueryNode
|
||||
? nodeBuilder.and([filterKueryNode, authorizationFilter as KueryNode])
|
||||
: authorizationFilter,
|
||||
filter: combineFilterWithAuthorizationFilter(
|
||||
combinedFilters,
|
||||
authorizationFilter as KueryNode
|
||||
),
|
||||
page,
|
||||
perPage,
|
||||
aggs,
|
||||
|
|
|
@ -16,7 +16,8 @@ export const aggregateOptionsSchema = schema.object({
|
|||
id: schema.string(),
|
||||
})
|
||||
),
|
||||
filterConsumers: schema.maybe(schema.arrayOf(schema.string())),
|
||||
ruleTypeIds: schema.maybe(schema.arrayOf(schema.string())),
|
||||
consumers: schema.maybe(schema.arrayOf(schema.string())),
|
||||
// filter type is `string | KueryNode`, but `KueryNode` has no schema to import yet
|
||||
filter: schema.maybe(
|
||||
schema.oneOf([schema.string(), schema.recordOf(schema.string(), schema.any())])
|
||||
|
|
|
@ -9,17 +9,9 @@ import { TypeOf } from '@kbn/config-schema';
|
|||
import { KueryNode } from '@kbn/es-query';
|
||||
import { aggregateOptionsSchema } from '../schemas';
|
||||
|
||||
type AggregateOptionsSchemaTypes = TypeOf<typeof aggregateOptionsSchema>;
|
||||
export type AggregateOptions = TypeOf<typeof aggregateOptionsSchema> & {
|
||||
search?: AggregateOptionsSchemaTypes['search'];
|
||||
defaultSearchOperator?: AggregateOptionsSchemaTypes['defaultSearchOperator'];
|
||||
searchFields?: AggregateOptionsSchemaTypes['searchFields'];
|
||||
hasReference?: AggregateOptionsSchemaTypes['hasReference'];
|
||||
// Adding filter as in schema it's defined as any instead of KueryNode
|
||||
filter?: string | KueryNode;
|
||||
page?: AggregateOptionsSchemaTypes['page'];
|
||||
perPage?: AggregateOptionsSchemaTypes['perPage'];
|
||||
filterConsumers?: string[];
|
||||
};
|
||||
|
||||
export interface AggregateParams<AggregationResult> {
|
||||
|
|
|
@ -132,10 +132,10 @@ export async function bulkEditRules<Params extends RuleParams>(
|
|||
const qNodeFilter = ids ? convertRuleIdsToKueryNode(ids) : qNodeQueryFilter;
|
||||
let authorizationTuple;
|
||||
try {
|
||||
authorizationTuple = await context.authorization.getFindAuthorizationFilter(
|
||||
AlertingAuthorizationEntity.Rule,
|
||||
alertingAuthorizationFilterOpts
|
||||
);
|
||||
authorizationTuple = await context.authorization.getFindAuthorizationFilter({
|
||||
authorizationEntity: AlertingAuthorizationEntity.Rule,
|
||||
filterOpts: alertingAuthorizationFilterOpts,
|
||||
});
|
||||
} catch (error) {
|
||||
context.auditLogger?.log(
|
||||
ruleAuditEvent({
|
||||
|
|
|
@ -41,12 +41,11 @@ async function bulkUntrackAlertsWithOCC(context: RulesClientContext, params: Bul
|
|||
if (!context.alertsService) throw new Error('unable to access alertsService');
|
||||
const result = await context.alertsService.setAlertsToUntracked({
|
||||
...params,
|
||||
featureIds: params.featureIds || [],
|
||||
ruleTypeIds: params.ruleTypeIds || [],
|
||||
spaceId: context.spaceId,
|
||||
getAlertIndicesAlias: context.getAlertIndicesAlias,
|
||||
getAuthorizedRuleTypes: context.authorization.getAuthorizedRuleTypes.bind(
|
||||
context.authorization
|
||||
),
|
||||
getAllAuthorizedRuleTypesFindOperation:
|
||||
context.authorization.getAllAuthorizedRuleTypesFindOperation.bind(context.authorization),
|
||||
ensureAuthorized: async ({
|
||||
ruleTypeId,
|
||||
consumer,
|
||||
|
|
|
@ -11,5 +11,5 @@ export const bulkUntrackBodySchema = schema.object({
|
|||
indices: schema.maybe(schema.arrayOf(schema.string())),
|
||||
alertUuids: schema.maybe(schema.arrayOf(schema.string())),
|
||||
query: schema.maybe(schema.arrayOf(schema.any())),
|
||||
featureIds: schema.maybe(schema.arrayOf(schema.string())),
|
||||
ruleTypeIds: schema.maybe(schema.arrayOf(schema.string())),
|
||||
});
|
||||
|
|
|
@ -84,6 +84,12 @@ export async function createRule<Params extends RuleParams = never>(
|
|||
throw Boom.badRequest(`Error validating create data - ${error.message}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* ruleTypeRegistry.get will throw a 400 (Bad request)
|
||||
* error if the rule type is not registered.
|
||||
*/
|
||||
context.ruleTypeRegistry.get(data.alertTypeId);
|
||||
|
||||
let validationPayload: ValidateScheduleLimitResult = null;
|
||||
if (data.enabled) {
|
||||
validationPayload = await validateScheduleLimit({
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
import { taskManagerMock } from '@kbn/task-manager-plugin/server/mocks';
|
||||
import { ruleTypeRegistryMock } from '../../../../rule_type_registry.mock';
|
||||
import { alertingAuthorizationMock } from '../../../../authorization/alerting_authorization.mock';
|
||||
import { nodeTypes, fromKueryExpression } from '@kbn/es-query';
|
||||
import { nodeTypes, fromKueryExpression, toKqlExpression } from '@kbn/es-query';
|
||||
import { encryptedSavedObjectsMock } from '@kbn/encrypted-saved-objects-plugin/server/mocks';
|
||||
import { actionsAuthorizationMock } from '@kbn/actions-plugin/server/mocks';
|
||||
import { AlertingAuthorization } from '../../../../authorization/alerting_authorization';
|
||||
|
@ -92,23 +92,26 @@ jest.mock('../../../../rules_client/common/map_sort_field', () => ({
|
|||
}));
|
||||
|
||||
describe('find()', () => {
|
||||
const listedTypes = new Set<RegistryRuleType>([
|
||||
{
|
||||
actionGroups: [],
|
||||
recoveryActionGroup: RecoveredActionGroup,
|
||||
actionVariables: undefined,
|
||||
defaultActionGroupId: 'default',
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: true,
|
||||
id: 'myType',
|
||||
name: 'myType',
|
||||
category: 'test',
|
||||
producer: 'myApp',
|
||||
enabledInLicense: true,
|
||||
hasAlertsMappings: false,
|
||||
hasFieldsForAAD: false,
|
||||
validLegacyConsumers: [],
|
||||
},
|
||||
const listedTypes = new Map<string, RegistryRuleType>([
|
||||
[
|
||||
'myType',
|
||||
{
|
||||
actionGroups: [],
|
||||
recoveryActionGroup: RecoveredActionGroup,
|
||||
actionVariables: undefined,
|
||||
defaultActionGroupId: 'default',
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: true,
|
||||
id: 'myType',
|
||||
name: 'myType',
|
||||
category: 'test',
|
||||
producer: 'myApp',
|
||||
enabledInLicense: true,
|
||||
hasAlertsMappings: false,
|
||||
hasFieldsForAAD: false,
|
||||
validLegacyConsumers: [],
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -163,26 +166,16 @@ describe('find()', () => {
|
|||
});
|
||||
|
||||
ruleTypeRegistry.list.mockReturnValue(listedTypes);
|
||||
authorization.filterByRuleTypeAuthorization.mockResolvedValue(
|
||||
new Set([
|
||||
{
|
||||
id: 'myType',
|
||||
name: 'Test',
|
||||
actionGroups: [{ id: 'default', name: 'Default' }],
|
||||
recoveryActionGroup: RecoveredActionGroup,
|
||||
defaultActionGroupId: 'default',
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: true,
|
||||
category: 'test',
|
||||
producer: 'alerts',
|
||||
authorizedConsumers: {
|
||||
myApp: { read: true, all: true },
|
||||
authorization.getAuthorizedRuleTypes.mockResolvedValue(
|
||||
new Map([
|
||||
[
|
||||
'myType',
|
||||
{
|
||||
authorizedConsumers: {
|
||||
myApp: { read: true, all: true },
|
||||
},
|
||||
},
|
||||
enabledInLicense: true,
|
||||
hasAlertsMappings: false,
|
||||
hasFieldsForAAD: false,
|
||||
validLegacyConsumers: [],
|
||||
},
|
||||
],
|
||||
])
|
||||
);
|
||||
});
|
||||
|
@ -235,7 +228,7 @@ describe('find()', () => {
|
|||
Array [
|
||||
Object {
|
||||
"fields": undefined,
|
||||
"filter": null,
|
||||
"filter": undefined,
|
||||
"sortField": undefined,
|
||||
"type": "alert",
|
||||
},
|
||||
|
@ -349,7 +342,7 @@ describe('find()', () => {
|
|||
Array [
|
||||
Object {
|
||||
"fields": undefined,
|
||||
"filter": null,
|
||||
"filter": undefined,
|
||||
"sortField": undefined,
|
||||
"type": "alert",
|
||||
},
|
||||
|
@ -461,7 +454,7 @@ describe('find()', () => {
|
|||
Array [
|
||||
Object {
|
||||
"fields": undefined,
|
||||
"filter": null,
|
||||
"filter": undefined,
|
||||
"sortField": undefined,
|
||||
"type": "alert",
|
||||
},
|
||||
|
@ -513,13 +506,34 @@ describe('find()', () => {
|
|||
authorization.getFindAuthorizationFilter.mockResolvedValue({
|
||||
ensureRuleTypeIsAuthorized() {},
|
||||
});
|
||||
|
||||
const injectReferencesFn = jest.fn().mockReturnValue({
|
||||
bar: true,
|
||||
parameterThatIsSavedObjectId: '9',
|
||||
});
|
||||
ruleTypeRegistry.list.mockReturnValue(
|
||||
new Set([
|
||||
...listedTypes,
|
||||
|
||||
const ruleTypes = new Map<string, RegistryRuleType>([
|
||||
[
|
||||
'myType',
|
||||
{
|
||||
actionGroups: [],
|
||||
recoveryActionGroup: RecoveredActionGroup,
|
||||
actionVariables: undefined,
|
||||
defaultActionGroupId: 'default',
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: true,
|
||||
id: 'myType',
|
||||
name: 'myType',
|
||||
category: 'test',
|
||||
producer: 'myApp',
|
||||
enabledInLicense: true,
|
||||
hasAlertsMappings: false,
|
||||
hasFieldsForAAD: false,
|
||||
validLegacyConsumers: [],
|
||||
},
|
||||
],
|
||||
[
|
||||
'123',
|
||||
{
|
||||
actionGroups: [],
|
||||
recoveryActionGroup: RecoveredActionGroup,
|
||||
|
@ -536,8 +550,10 @@ describe('find()', () => {
|
|||
hasFieldsForAAD: false,
|
||||
validLegacyConsumers: [],
|
||||
},
|
||||
])
|
||||
);
|
||||
],
|
||||
]);
|
||||
|
||||
ruleTypeRegistry.list.mockReturnValue(ruleTypes);
|
||||
ruleTypeRegistry.get.mockImplementationOnce(() => ({
|
||||
id: 'myType',
|
||||
name: 'myType',
|
||||
|
@ -750,12 +766,33 @@ describe('find()', () => {
|
|||
authorization.getFindAuthorizationFilter.mockResolvedValue({
|
||||
ensureRuleTypeIsAuthorized() {},
|
||||
});
|
||||
|
||||
const injectReferencesFn = jest.fn().mockImplementation(() => {
|
||||
throw new Error('something went wrong!');
|
||||
});
|
||||
ruleTypeRegistry.list.mockReturnValue(
|
||||
new Set([
|
||||
...listedTypes,
|
||||
|
||||
const ruleTypes = new Map<string, RegistryRuleType>([
|
||||
[
|
||||
'myType',
|
||||
{
|
||||
actionGroups: [],
|
||||
recoveryActionGroup: RecoveredActionGroup,
|
||||
actionVariables: undefined,
|
||||
defaultActionGroupId: 'default',
|
||||
minimumLicenseRequired: 'basic',
|
||||
isExportable: true,
|
||||
id: 'myType',
|
||||
name: 'myType',
|
||||
category: 'test',
|
||||
producer: 'myApp',
|
||||
enabledInLicense: true,
|
||||
hasAlertsMappings: false,
|
||||
hasFieldsForAAD: false,
|
||||
validLegacyConsumers: [],
|
||||
},
|
||||
],
|
||||
[
|
||||
'123',
|
||||
{
|
||||
actionGroups: [],
|
||||
recoveryActionGroup: RecoveredActionGroup,
|
||||
|
@ -772,8 +809,10 @@ describe('find()', () => {
|
|||
hasFieldsForAAD: false,
|
||||
validLegacyConsumers: [],
|
||||
},
|
||||
])
|
||||
);
|
||||
],
|
||||
]);
|
||||
|
||||
ruleTypeRegistry.list.mockReturnValue(ruleTypes);
|
||||
ruleTypeRegistry.get.mockImplementationOnce(() => ({
|
||||
id: 'myType',
|
||||
name: 'myType',
|
||||
|
@ -792,6 +831,7 @@ describe('find()', () => {
|
|||
},
|
||||
validLegacyConsumers: [],
|
||||
}));
|
||||
|
||||
ruleTypeRegistry.get.mockImplementationOnce(() => ({
|
||||
id: '123',
|
||||
name: 'Test',
|
||||
|
@ -987,12 +1027,58 @@ describe('find()', () => {
|
|||
|
||||
expect(unsecuredSavedObjectsClient.find).toHaveBeenCalledWith({
|
||||
fields: ['tags', 'alertTypeId', 'consumer'],
|
||||
filter: null,
|
||||
filter: undefined,
|
||||
sortField: undefined,
|
||||
type: RULE_SAVED_OBJECT_TYPE,
|
||||
});
|
||||
expect(ensureRuleTypeIsAuthorized).toHaveBeenCalledWith('myType', 'myApp', 'rule');
|
||||
});
|
||||
|
||||
test('calls getFindAuthorizationFilter correctly', async () => {
|
||||
authorization.getFindAuthorizationFilter.mockResolvedValue({
|
||||
ensureRuleTypeIsAuthorized() {},
|
||||
});
|
||||
|
||||
const rulesClient = new RulesClient(rulesClientParams);
|
||||
await rulesClient.find({ options: { ruleTypeIds: ['foo'], consumers: ['bar'] } });
|
||||
|
||||
expect(authorization.getFindAuthorizationFilter).toHaveBeenCalledWith({
|
||||
authorizationEntity: 'rule',
|
||||
filterOpts: {
|
||||
fieldNames: {
|
||||
consumer: 'alert.attributes.consumer',
|
||||
ruleTypeId: 'alert.attributes.alertTypeId',
|
||||
},
|
||||
type: 'kql',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('combines the filters with the auth filter correctly', async () => {
|
||||
const filter = fromKueryExpression(
|
||||
'alert.attributes.alertTypeId:myType and alert.attributes.consumer:myApp'
|
||||
);
|
||||
|
||||
authorization.getFindAuthorizationFilter.mockResolvedValue({
|
||||
filter,
|
||||
ensureRuleTypeIsAuthorized() {},
|
||||
});
|
||||
|
||||
const rulesClient = new RulesClient(rulesClientParams);
|
||||
await rulesClient.find({
|
||||
options: {
|
||||
ruleTypeIds: ['foo'],
|
||||
consumers: ['bar'],
|
||||
filter: `alert.attributes.tags: ['bar']`,
|
||||
},
|
||||
});
|
||||
|
||||
const finalFilter = unsecuredSavedObjectsClient.find.mock.calls[0][0].filter;
|
||||
|
||||
expect(toKqlExpression(finalFilter)).toMatchInlineSnapshot(
|
||||
`"((alert.attributes.tags: ['bar'] AND alert.attributes.alertTypeId: foo AND alert.attributes.consumer: bar) AND (alert.attributes.alertTypeId: myType AND alert.attributes.consumer: myApp))"`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('auditLogger', () => {
|
||||
|
|
|
@ -6,11 +6,17 @@
|
|||
*/
|
||||
|
||||
import Boom from '@hapi/boom';
|
||||
import { isEmpty, pick } from 'lodash';
|
||||
import { KueryNode, nodeBuilder } from '@kbn/es-query';
|
||||
import { pick } from 'lodash';
|
||||
import { KueryNode } from '@kbn/es-query';
|
||||
import { AlertConsumers } from '@kbn/rule-data-utils';
|
||||
import {
|
||||
buildConsumersFilter,
|
||||
buildRuleTypeIdsFilter,
|
||||
combineFilterWithAuthorizationFilter,
|
||||
combineFilters,
|
||||
} from '../../../../rules_client/common/filters';
|
||||
import { AlertingAuthorizationEntity } from '../../../../authorization/types';
|
||||
import { SanitizedRule, Rule as DeprecatedRule, RawRule } from '../../../../types';
|
||||
import { AlertingAuthorizationEntity } from '../../../../authorization';
|
||||
import { ruleAuditEvent, RuleAuditAction } from '../../../../rules_client/common/audit_events';
|
||||
import {
|
||||
mapSortField,
|
||||
|
@ -46,7 +52,7 @@ export async function findRules<Params extends RuleParams = never>(
|
|||
): Promise<FindResult<Params>> {
|
||||
const { options, excludeFromPublicApi = false, includeSnoozeData = false } = params || {};
|
||||
|
||||
const { fields, filterConsumers, ...restOptions } = options || {};
|
||||
const { fields, ruleTypeIds, consumers, ...restOptions } = options || {};
|
||||
|
||||
try {
|
||||
if (params) {
|
||||
|
@ -58,11 +64,10 @@ export async function findRules<Params extends RuleParams = never>(
|
|||
|
||||
let authorizationTuple;
|
||||
try {
|
||||
authorizationTuple = await context.authorization.getFindAuthorizationFilter(
|
||||
AlertingAuthorizationEntity.Rule,
|
||||
alertingAuthorizationFilterOpts,
|
||||
isEmpty(filterConsumers) ? undefined : new Set(filterConsumers)
|
||||
);
|
||||
authorizationTuple = await context.authorization.getFindAuthorizationFilter({
|
||||
authorizationEntity: AlertingAuthorizationEntity.Rule,
|
||||
filterOpts: alertingAuthorizationFilterOpts,
|
||||
});
|
||||
} catch (error) {
|
||||
context.auditLogger?.log(
|
||||
ruleAuditEvent({
|
||||
|
@ -76,6 +81,7 @@ export async function findRules<Params extends RuleParams = never>(
|
|||
const { filter: authorizationFilter, ensureRuleTypeIsAuthorized } = authorizationTuple;
|
||||
const filterKueryNode = buildKueryNodeFilter(restOptions.filter as string | KueryNode);
|
||||
let sortField = mapSortField(restOptions.sortField);
|
||||
|
||||
if (excludeFromPublicApi) {
|
||||
try {
|
||||
validateOperationOnAttributes(
|
||||
|
@ -111,6 +117,18 @@ export async function findRules<Params extends RuleParams = never>(
|
|||
modifyFilterKueryNode({ astFilter: filterKueryNode });
|
||||
}
|
||||
|
||||
const ruleTypeIdsFilter = buildRuleTypeIdsFilter(ruleTypeIds);
|
||||
const consumersFilter = buildConsumersFilter(consumers);
|
||||
const combinedFilters = combineFilters(
|
||||
[filterKueryNode, ruleTypeIdsFilter, consumersFilter],
|
||||
'and'
|
||||
);
|
||||
|
||||
const finalFilter = combineFilterWithAuthorizationFilter(
|
||||
combinedFilters,
|
||||
authorizationFilter as KueryNode
|
||||
);
|
||||
|
||||
const {
|
||||
page,
|
||||
per_page: perPage,
|
||||
|
@ -121,10 +139,7 @@ export async function findRules<Params extends RuleParams = never>(
|
|||
savedObjectsFindOptions: {
|
||||
...modifiedOptions,
|
||||
sortField,
|
||||
filter:
|
||||
(authorizationFilter && filterKueryNode
|
||||
? nodeBuilder.and([filterKueryNode, authorizationFilter as KueryNode])
|
||||
: authorizationFilter) ?? filterKueryNode,
|
||||
filter: finalFilter,
|
||||
fields: fields ? includeFieldsRequiredForAuthentication(fields) : fields,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -28,7 +28,8 @@ export const findRulesOptionsSchema = schema.object(
|
|||
filter: schema.maybe(
|
||||
schema.oneOf([schema.string(), schema.recordOf(schema.string(), schema.any())])
|
||||
),
|
||||
filterConsumers: schema.maybe(schema.arrayOf(schema.string())),
|
||||
ruleTypeIds: schema.maybe(schema.arrayOf(schema.string())),
|
||||
consumers: schema.maybe(schema.arrayOf(schema.string())),
|
||||
},
|
||||
{ unknowns: 'allow' }
|
||||
);
|
||||
|
@ -37,5 +38,4 @@ export const findRulesParamsSchema = schema.object({
|
|||
options: schema.maybe(findRulesOptionsSchema),
|
||||
excludeFromPublicApi: schema.maybe(schema.boolean()),
|
||||
includeSnoozeData: schema.maybe(schema.boolean()),
|
||||
featureIds: schema.maybe(schema.arrayOf(schema.string())),
|
||||
});
|
||||
|
|
|
@ -9,5 +9,4 @@ import { TypeOf } from '@kbn/config-schema';
|
|||
import { findRulesOptionsSchema, findRulesParamsSchema } from '../schemas';
|
||||
|
||||
export type FindRulesOptions = TypeOf<typeof findRulesOptionsSchema>;
|
||||
|
||||
export type FindRulesParams = TypeOf<typeof findRulesParamsSchema>;
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* 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 { rulesClientContextMock } from '../../../../rules_client/rules_client.mock';
|
||||
import { RulesClient } from '../../../../rules_client';
|
||||
|
||||
describe('listRuleTypes', () => {
|
||||
const rulesClientContext = rulesClientContextMock.create();
|
||||
let rulesClient: RulesClient;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
rulesClient = new RulesClient(rulesClientContext);
|
||||
|
||||
rulesClientContext.ruleTypeRegistry.list = jest.fn().mockReturnValue(
|
||||
new Map([
|
||||
['apm.anomaly', { name: 'Anomaly' }],
|
||||
['.es-query', { name: 'ES rule type' }],
|
||||
])
|
||||
);
|
||||
rulesClientContext.ruleTypeRegistry.has = jest
|
||||
.fn()
|
||||
.mockImplementation((ruleTypeId: string) => ruleTypeId === '.es-query');
|
||||
|
||||
rulesClientContext.authorization.getAuthorizedRuleTypes = jest.fn().mockResolvedValue(
|
||||
new Map([
|
||||
['.es-query', { authorizedConsumers: { all: true, read: true } }],
|
||||
['.not-exist', { authorizedConsumers: { all: true, read: true } }],
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
it('authorizes correctly', async () => {
|
||||
await rulesClient.listRuleTypes();
|
||||
|
||||
expect(rulesClientContext.authorization.getAuthorizedRuleTypes).toHaveBeenCalledWith({
|
||||
authorizationEntity: 'rule',
|
||||
operations: ['get', 'create'],
|
||||
ruleTypeIds: ['apm.anomaly', '.es-query'],
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the authorized rule types correctly and does not return non authorized or non existing rule types', async () => {
|
||||
const res = await rulesClient.listRuleTypes();
|
||||
|
||||
expect(res).toEqual([{ name: 'ES rule type', authorizedConsumers: { all: true, read: true } }]);
|
||||
});
|
||||
});
|
|
@ -6,16 +6,28 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
WriteOperations,
|
||||
ReadOperations,
|
||||
AlertingAuthorizationEntity,
|
||||
ReadOperations,
|
||||
RegistryAlertTypeWithAuth,
|
||||
WriteOperations,
|
||||
} from '../../../../authorization';
|
||||
import { RulesClientContext } from '../../../../rules_client/types';
|
||||
|
||||
export async function listRuleTypes(context: RulesClientContext) {
|
||||
return await context.authorization.filterByRuleTypeAuthorization(
|
||||
context.ruleTypeRegistry.list(),
|
||||
[ReadOperations.Get, WriteOperations.Create],
|
||||
AlertingAuthorizationEntity.Rule
|
||||
);
|
||||
export async function listRuleTypes(
|
||||
context: RulesClientContext
|
||||
): Promise<RegistryAlertTypeWithAuth[]> {
|
||||
const registeredRuleTypes = context.ruleTypeRegistry.list();
|
||||
|
||||
const authorizedRuleTypes = await context.authorization.getAuthorizedRuleTypes({
|
||||
authorizationEntity: AlertingAuthorizationEntity.Rule,
|
||||
operations: [ReadOperations.Get, WriteOperations.Create],
|
||||
ruleTypeIds: Array.from(registeredRuleTypes.keys()).map((id) => id),
|
||||
});
|
||||
|
||||
return Array.from(authorizedRuleTypes.entries())
|
||||
.filter(([id, _]) => context.ruleTypeRegistry.has(id))
|
||||
.map(([id, { authorizedConsumers }]) => ({
|
||||
...registeredRuleTypes.get(id)!,
|
||||
authorizedConsumers,
|
||||
}));
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue