Skip to content

Commit

Permalink
[8.x] feat: allow plugins to deprecate and replace features and featu…
Browse files Browse the repository at this point in the history
…re privileges (elastic#186800) (elastic#196204)

# Backport

This will backport the following commits from `main` to `8.x`:
- [feat: allow plugins to deprecate and replace features and feature
privileges (elastic#186800)](elastic#186800)

<!--- Backport version: 8.9.8 -->

### Questions ?
Please refer to the [Backport tool
documentation](https://github.com/sqren/backport)

<!--BACKPORT [{"author":{"name":"Aleh
Zasypkin","email":"[email protected]"},"sourceCommit":{"committedDate":"2024-10-14T19:40:59Z","message":"feat:
allow plugins to deprecate and replace features and feature privileges
(elastic#186800)\n\n## Summary\r\n\r\nThis change is the implementation of the
`Kibana Privilege Migrations`\r\nproposal/RFC and provides a framework
that allows developers to replace\r\nan existing feature with a new one
that has the desired configuration\r\nwhile teaching the platform how
the privileges of the deprecated feature\r\ncan be represented by
non-deprecated ones. This approach avoids\r\nintroducing breaking
changes for users who still rely on the deprecated\r\nprivileges in
their existing roles and any automation.\r\n\r\nAmong the use cases the
framework is supposed to handle, the most common\r\nare the
following:\r\n\r\n* Changing a feature ID from `Alpha` to `Beta`\r\n*
Splitting a feature `Alpha` into two features, `Beta` and `Gamma`\r\n*
Moving a capability between privileges within a feature (top-level
or\r\nsub-feature)\r\n* Consolidating capabilities across independent
features\r\n\r\n## Scope\r\n\r\nThis PR includes only the core
functionality proposed in the RFC and\r\nmost of the necessary
guardrails (tests, early validations, etc.) to\r\nhelp engineers start
planning and implementing their migrations as soon\r\nas possible. The
following functionality will be added in follow-ups or\r\nonce we
collect enough feedback:\r\n\r\n* Telemetry\r\n* Developer
documentation\r\n* UI enhancements (highlighting roles with deprecated
privileges and\r\nmanual migration actions)\r\n\r\n##
Framework\r\n\r\nThe steps below use a scenario where a feature `Alpha`
should be split\r\ninto two other features `Beta` and `Gamma` as an
example.\r\n\r\n### Step 1: Create new features with the desired
privileges\r\n\r\nFirst of all, define new feature or features with the
desired\r\nconfiguration as you'd do before. There are no constraints
here.\r\n\r\n<details>\r\n\r\n<summary>Click to see the
code</summary>\r\n\r\n```ts\r\ndeps.features.registerKibanaFeature({\r\n
id: 'feature_beta',\r\n name: 'Feature Beta',\r\n privileges: {\r\n all:
{\r\n savedObject: { all: ['saved_object_1'], read: [] },\r\n ui:
['ui_all'],\r\n api: ['api_all'],\r\n … omitted for brevity …\r\n },\r\n
read: {\r\n savedObject: { all: [], read: ['saved_object_1'] },\r\n ui:
['ui_read'],\r\n api: ['api_read'],\r\n … omitted for brevity …\r\n
},\r\n },\r\n … omitted for brevity
…\r\n});\r\n\r\ndeps.features.registerKibanaFeature({\r\n id:
'feature_gamma',\r\n name: 'Feature Gamma',\r\n privileges: {\r\n all:
{\r\n savedObject: { all: ['saved_object_2'], read: [] },\r\n ui:
['ui_all'],\r\n // Note that Feature Gamma, unlike Features Alpha and
Beta doesn't provide any API access tags\r\n … omitted for brevity …\r\n
},\r\n read: {\r\n savedObject: { all: [], read: ['saved_object_2']
},\r\n ui: ['ui_read'],\r\n // Note that Feature Gamma, unlike Features
Alpha and Beta doesn't provide any API access tags\r\n … omitted for
brevity …\r\n },\r\n },\r\n … omitted for brevity
…\r\n});\r\n```\r\n\r\n</details>\r\n\r\n### Step 2: Mark existing
feature as deprecated\r\n\r\nOnce a feature is marked as deprecated, it
should essentially be treated\r\nas frozen for backward compatibility
reasons. Deprecated features will\r\nno longer be available through the
Kibana role management UI and will be\r\nreplaced with non-deprecated
privileges.\r\n\r\nDeprecated privileges will still be accepted if the
role is created or\r\nupdated via the Kibana role management APIs to
avoid disrupting existing\r\nuser automation.\r\n\r\nTo avoid breaking
existing roles that reference privileges provided by\r\nthe deprecated
features, Kibana will continue registering these\r\nprivileges as
Elasticsearch application
privileges.\r\n\r\n<details>\r\n\r\n<summary>Click to see the
code</summary>\r\n\r\n```ts\r\ndeps.features.registerKibanaFeature({\r\n
// This is a new `KibanaFeature` property available during feature
registration.\r\n deprecated: {\r\n // User-facing justification for
privilege deprecation that we can display\r\n // to the user when we ask
them to perform role migration.\r\n notice:
i18n.translate('xpack.security...', {\r\n defaultMessage: \"Feature
Alpha is deprecated, refer to {link}...\",\r\n values: { link:
docLinks.links.security.deprecatedFeatureAlpha },\r\n })\r\n },\r\n //
Feature id should stay unchanged, and it's not possible to reuse it.\r\n
id: 'feature_alpha',\r\n name: 'Feature Alpha (DEPRECATED)',\r\n
privileges: {\r\n all: {\r\n savedObject: { all: ['saved_object_1',
'saved_object_2'], read: [] },\r\n ui: ['ui_all'],\r\n api:
['api_all'],\r\n … omitted for brevity …\r\n },\r\n read: {\r\n
savedObject: { all: [], read: ['saved_object_1', 'saved_object_2']
},\r\n ui: ['ui_read'],\r\n api: ['api_read'],\r\n … omitted for brevity
…\r\n },\r\n },\r\n … omitted for brevity
…\r\n});\r\n```\r\n</details>\r\n\r\n### Step 3: Map deprecated
feature’s privileges to the privileges of the\r\nnon-deprecated
features\r\n\r\nThe important requirement for a successful migration
from a deprecated\r\nfeature to a new feature or features is that it
should be possible to\r\nexpress **any combination** of the deprecated
feature and sub-feature\r\nprivileges with the feature or sub-feature
privileges of non-deprecated\r\nfeatures. This way, while editing a role
with deprecated feature\r\nprivileges in the UI, the admin will be
interacting with new privileges\r\nas if they were creating a new role
from scratch, maintaining\r\nconsistency.\r\n\r\nThe relationship
between the privileges of the deprecated feature and\r\nthe privileges
of the features that are supposed to replace them is\r\nexpressed with a
new `replacedBy` property available on the privileges\r\nof the
deprecated feature.\r\n\r\n<details>\r\n\r\n<summary>Click to see the
code</summary>\r\n\r\n```ts\r\ndeps.features.registerKibanaFeature({\r\n
// This is a new `KibanaFeature` property available during feature
registration.\r\n deprecated: {\r\n // User-facing justification for
privilege deprecation that we can display\r\n // to the user when we ask
them to perform role migration.\r\n notice:
i18n.translate('xpack.security...', {\r\n defaultMessage: \"Feature
Alpha is deprecated, refer to {link}...\",\r\n values: { link:
docLinks.links.security.deprecatedFeatureAlpha },\r\n })\r\n },\r\n //
Feature id should stay unchanged, and it's not possible to reuse it.\r\n
id: 'feature_alpha',\r\n name: 'Feature Alpha (DEPRECATED)',\r\n
privileges: {\r\n all: {\r\n savedObject: { all: ['saved_object_1',
'saved_object_2'], read: [] },\r\n ui: ['ui_all'],\r\n api:
['api_all'],\r\n replacedBy: [\r\n { feature: 'feature_beta',
privileges: ['all'] },\r\n { feature: 'feature_gamma', privileges:
['all'] },\r\n ],\r\n … omitted for brevity …\r\n },\r\n read: {\r\n
savedObject: { all: [], read: ['saved_object_1', 'saved_object_2']
},\r\n ui: ['ui_read'],\r\n api: ['api_read'],\r\n replacedBy: [\r\n {
feature: 'feature_beta', privileges: ['read'] },\r\n { feature:
'feature_gamma', privileges: ['read'] },\r\n\t],\r\n … omitted for
brevity …\r\n },\r\n },\r\n … omitted for brevity
…\r\n});\r\n```\r\n\r\n</details>\r\n\r\n### Step 4: Adjust the code to
rely only on new, non-deprecated features\r\n\r\nSpecial care should be
taken if the replacement privileges cannot reuse\r\nthe API access tags
from the deprecated privileges and introduce new\r\ntags that will be
applied to the same API endpoints. In this case,\r\ndevelopers should
replace the API access tags of the deprecated\r\nprivileges with the
corresponding tags provided by the replacement\r\nprivileges. This is
necessary because API endpoints can only be accessed\r\nif the user
privileges cover all the tags listed in the API endpoint\r\ndefinition,
and without these changes, existing roles referencing\r\ndeprecated
privileges won’t be able to access those endpoints.\r\n\r\nThe UI
capabilities are handled slightly differently because they are\r\nalways
prefixed with the feature ID. When migrating to new features with\r\nnew
IDs, the code that interacts with UI capabilities will be updated
to\r\nuse these new feature IDs.\r\n\r\n<details>\r\n\r\n<summary>Click
to see the code</summary>\r\n\r\n```ts\r\n// BEFORE
deprecation/migration\r\n// 1. Feature Alpha defition (not deprecated
yet)\r\ndeps.features.registerKibanaFeature({\r\n id:
'feature_alpha',\r\n privileges: {\r\n all: {\r\n api: ['api_all'],\r\n
… omitted for brevity …\r\n },\r\n },\r\n … omitted for brevity
…\r\n});\r\n\r\n// 2. Route protected by `all` privilege of the Feature
Alpha\r\nrouter.post(\r\n { path: '/api/domain/my_api', options: { tags:
['access:api_all'] } },\r\n async (_context, request, response) =>
{}\r\n);\r\n\r\n// AFTER deprecation/migration\r\n// 1. Feature Alpha
defition (deprecated, with updated API
tags)\r\ndeps.features.registerKibanaFeature({\r\n deprecated: …,\r\n
id: 'feature_alpha',\r\n privileges: {\r\n all: {\r\n api:
['api_all_v2'],\r\n replacedBy: [\r\n { feature: 'feature_beta',
privileges: ['all'] },\r\n ],\r\n … omitted for brevity …\r\n },\r\n
},\r\n … omitted for brevity …\r\n});\r\n\r\n// 2. Feature Beta defition
(new)\r\ndeps.features.registerKibanaFeature({\r\n id:
'feature_beta',\r\n privileges: {\r\n all: {\r\n api:
['api_all_v2'],\r\n … omitted for brevity …\r\n }\r\n },\r\n … omitted
for brevity …\r\n});\r\n\r\n// 3. Route protected by `all` privilege of
the Feature Alpha OR Feature Beta\r\nrouter.post(\r\n { path:
'/api/domain/my_api', options: { tags: ['access:api_all_v2'] } },\r\n
async (_context, request, response) => {}\r\n);\r\n\r\n----\r\n\r\n// ❌
Old client-side code (supports only deprecated privileges)\r\nif
(capabilities.feature_alpha.ui_all) {\r\n … omitted for brevity
…\r\n}\r\n\r\n// ✅ New client-side code (will work for **both** new and
deprecated privileges)\r\nif (capabilities.feature_beta.ui_all) {\r\n …
omitted for brevity …\r\n}\r\n```\r\n</details>\r\n\r\n## How to
test\r\n\r\nThe code introduces a set of API integration tests that are
designed to\r\nvalidate whether the privilege mapping between deprecated
and\r\nreplacement privileges maintains backward
compatibility.\r\n\r\nYou can run the test server with the following
config to register a\r\nnumber of [example
deprecated\r\nfeatures](https://github.com/elastic/kibana/pull/186800/files#diff-d887981d43bbe30cda039340b906b0fa7649ba80230be4de8eda326036f10f6fR20-R49)(`x-pack/test/security_api_integration/plugins/features_provider/server/index.ts`)\r\nand
the features that replace them, to see the framework in
action:\r\n\r\n```bash\r\nnode scripts/functional_tests_server.js
--config
x-pack/test/security_api_integration/features.config.ts\r\n```\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<[email protected]>","sha":"cb2112cae51d5f69b9e47ebfde66cfacb2a6719b","branchLabelMapping":{"^v9.0.0$":"main","^v8.16.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["Team:Security","release_note:skip","Feature:Security/Authorization","v9.0.0","backport:prev-minor","v8.16.0"],"number":186800,"url":"https://github.com/elastic/kibana/pull/186800","mergeCommit":{"message":"feat:
allow plugins to deprecate and replace features and feature privileges
(elastic#186800)\n\n## Summary\r\n\r\nThis change is the implementation of the
`Kibana Privilege Migrations`\r\nproposal/RFC and provides a framework
that allows developers to replace\r\nan existing feature with a new one
that has the desired configuration\r\nwhile teaching the platform how
the privileges of the deprecated feature\r\ncan be represented by
non-deprecated ones. This approach avoids\r\nintroducing breaking
changes for users who still rely on the deprecated\r\nprivileges in
their existing roles and any automation.\r\n\r\nAmong the use cases the
framework is supposed to handle, the most common\r\nare the
following:\r\n\r\n* Changing a feature ID from `Alpha` to `Beta`\r\n*
Splitting a feature `Alpha` into two features, `Beta` and `Gamma`\r\n*
Moving a capability between privileges within a feature (top-level
or\r\nsub-feature)\r\n* Consolidating capabilities across independent
features\r\n\r\n## Scope\r\n\r\nThis PR includes only the core
functionality proposed in the RFC and\r\nmost of the necessary
guardrails (tests, early validations, etc.) to\r\nhelp engineers start
planning and implementing their migrations as soon\r\nas possible. The
following functionality will be added in follow-ups or\r\nonce we
collect enough feedback:\r\n\r\n* Telemetry\r\n* Developer
documentation\r\n* UI enhancements (highlighting roles with deprecated
privileges and\r\nmanual migration actions)\r\n\r\n##
Framework\r\n\r\nThe steps below use a scenario where a feature `Alpha`
should be split\r\ninto two other features `Beta` and `Gamma` as an
example.\r\n\r\n### Step 1: Create new features with the desired
privileges\r\n\r\nFirst of all, define new feature or features with the
desired\r\nconfiguration as you'd do before. There are no constraints
here.\r\n\r\n<details>\r\n\r\n<summary>Click to see the
code</summary>\r\n\r\n```ts\r\ndeps.features.registerKibanaFeature({\r\n
id: 'feature_beta',\r\n name: 'Feature Beta',\r\n privileges: {\r\n all:
{\r\n savedObject: { all: ['saved_object_1'], read: [] },\r\n ui:
['ui_all'],\r\n api: ['api_all'],\r\n … omitted for brevity …\r\n },\r\n
read: {\r\n savedObject: { all: [], read: ['saved_object_1'] },\r\n ui:
['ui_read'],\r\n api: ['api_read'],\r\n … omitted for brevity …\r\n
},\r\n },\r\n … omitted for brevity
…\r\n});\r\n\r\ndeps.features.registerKibanaFeature({\r\n id:
'feature_gamma',\r\n name: 'Feature Gamma',\r\n privileges: {\r\n all:
{\r\n savedObject: { all: ['saved_object_2'], read: [] },\r\n ui:
['ui_all'],\r\n // Note that Feature Gamma, unlike Features Alpha and
Beta doesn't provide any API access tags\r\n … omitted for brevity …\r\n
},\r\n read: {\r\n savedObject: { all: [], read: ['saved_object_2']
},\r\n ui: ['ui_read'],\r\n // Note that Feature Gamma, unlike Features
Alpha and Beta doesn't provide any API access tags\r\n … omitted for
brevity …\r\n },\r\n },\r\n … omitted for brevity
…\r\n});\r\n```\r\n\r\n</details>\r\n\r\n### Step 2: Mark existing
feature as deprecated\r\n\r\nOnce a feature is marked as deprecated, it
should essentially be treated\r\nas frozen for backward compatibility
reasons. Deprecated features will\r\nno longer be available through the
Kibana role management UI and will be\r\nreplaced with non-deprecated
privileges.\r\n\r\nDeprecated privileges will still be accepted if the
role is created or\r\nupdated via the Kibana role management APIs to
avoid disrupting existing\r\nuser automation.\r\n\r\nTo avoid breaking
existing roles that reference privileges provided by\r\nthe deprecated
features, Kibana will continue registering these\r\nprivileges as
Elasticsearch application
privileges.\r\n\r\n<details>\r\n\r\n<summary>Click to see the
code</summary>\r\n\r\n```ts\r\ndeps.features.registerKibanaFeature({\r\n
// This is a new `KibanaFeature` property available during feature
registration.\r\n deprecated: {\r\n // User-facing justification for
privilege deprecation that we can display\r\n // to the user when we ask
them to perform role migration.\r\n notice:
i18n.translate('xpack.security...', {\r\n defaultMessage: \"Feature
Alpha is deprecated, refer to {link}...\",\r\n values: { link:
docLinks.links.security.deprecatedFeatureAlpha },\r\n })\r\n },\r\n //
Feature id should stay unchanged, and it's not possible to reuse it.\r\n
id: 'feature_alpha',\r\n name: 'Feature Alpha (DEPRECATED)',\r\n
privileges: {\r\n all: {\r\n savedObject: { all: ['saved_object_1',
'saved_object_2'], read: [] },\r\n ui: ['ui_all'],\r\n api:
['api_all'],\r\n … omitted for brevity …\r\n },\r\n read: {\r\n
savedObject: { all: [], read: ['saved_object_1', 'saved_object_2']
},\r\n ui: ['ui_read'],\r\n api: ['api_read'],\r\n … omitted for brevity
…\r\n },\r\n },\r\n … omitted for brevity
…\r\n});\r\n```\r\n</details>\r\n\r\n### Step 3: Map deprecated
feature’s privileges to the privileges of the\r\nnon-deprecated
features\r\n\r\nThe important requirement for a successful migration
from a deprecated\r\nfeature to a new feature or features is that it
should be possible to\r\nexpress **any combination** of the deprecated
feature and sub-feature\r\nprivileges with the feature or sub-feature
privileges of non-deprecated\r\nfeatures. This way, while editing a role
with deprecated feature\r\nprivileges in the UI, the admin will be
interacting with new privileges\r\nas if they were creating a new role
from scratch, maintaining\r\nconsistency.\r\n\r\nThe relationship
between the privileges of the deprecated feature and\r\nthe privileges
of the features that are supposed to replace them is\r\nexpressed with a
new `replacedBy` property available on the privileges\r\nof the
deprecated feature.\r\n\r\n<details>\r\n\r\n<summary>Click to see the
code</summary>\r\n\r\n```ts\r\ndeps.features.registerKibanaFeature({\r\n
// This is a new `KibanaFeature` property available during feature
registration.\r\n deprecated: {\r\n // User-facing justification for
privilege deprecation that we can display\r\n // to the user when we ask
them to perform role migration.\r\n notice:
i18n.translate('xpack.security...', {\r\n defaultMessage: \"Feature
Alpha is deprecated, refer to {link}...\",\r\n values: { link:
docLinks.links.security.deprecatedFeatureAlpha },\r\n })\r\n },\r\n //
Feature id should stay unchanged, and it's not possible to reuse it.\r\n
id: 'feature_alpha',\r\n name: 'Feature Alpha (DEPRECATED)',\r\n
privileges: {\r\n all: {\r\n savedObject: { all: ['saved_object_1',
'saved_object_2'], read: [] },\r\n ui: ['ui_all'],\r\n api:
['api_all'],\r\n replacedBy: [\r\n { feature: 'feature_beta',
privileges: ['all'] },\r\n { feature: 'feature_gamma', privileges:
['all'] },\r\n ],\r\n … omitted for brevity …\r\n },\r\n read: {\r\n
savedObject: { all: [], read: ['saved_object_1', 'saved_object_2']
},\r\n ui: ['ui_read'],\r\n api: ['api_read'],\r\n replacedBy: [\r\n {
feature: 'feature_beta', privileges: ['read'] },\r\n { feature:
'feature_gamma', privileges: ['read'] },\r\n\t],\r\n … omitted for
brevity …\r\n },\r\n },\r\n … omitted for brevity
…\r\n});\r\n```\r\n\r\n</details>\r\n\r\n### Step 4: Adjust the code to
rely only on new, non-deprecated features\r\n\r\nSpecial care should be
taken if the replacement privileges cannot reuse\r\nthe API access tags
from the deprecated privileges and introduce new\r\ntags that will be
applied to the same API endpoints. In this case,\r\ndevelopers should
replace the API access tags of the deprecated\r\nprivileges with the
corresponding tags provided by the replacement\r\nprivileges. This is
necessary because API endpoints can only be accessed\r\nif the user
privileges cover all the tags listed in the API endpoint\r\ndefinition,
and without these changes, existing roles referencing\r\ndeprecated
privileges won’t be able to access those endpoints.\r\n\r\nThe UI
capabilities are handled slightly differently because they are\r\nalways
prefixed with the feature ID. When migrating to new features with\r\nnew
IDs, the code that interacts with UI capabilities will be updated
to\r\nuse these new feature IDs.\r\n\r\n<details>\r\n\r\n<summary>Click
to see the code</summary>\r\n\r\n```ts\r\n// BEFORE
deprecation/migration\r\n// 1. Feature Alpha defition (not deprecated
yet)\r\ndeps.features.registerKibanaFeature({\r\n id:
'feature_alpha',\r\n privileges: {\r\n all: {\r\n api: ['api_all'],\r\n
… omitted for brevity …\r\n },\r\n },\r\n … omitted for brevity
…\r\n});\r\n\r\n// 2. Route protected by `all` privilege of the Feature
Alpha\r\nrouter.post(\r\n { path: '/api/domain/my_api', options: { tags:
['access:api_all'] } },\r\n async (_context, request, response) =>
{}\r\n);\r\n\r\n// AFTER deprecation/migration\r\n// 1. Feature Alpha
defition (deprecated, with updated API
tags)\r\ndeps.features.registerKibanaFeature({\r\n deprecated: …,\r\n
id: 'feature_alpha',\r\n privileges: {\r\n all: {\r\n api:
['api_all_v2'],\r\n replacedBy: [\r\n { feature: 'feature_beta',
privileges: ['all'] },\r\n ],\r\n … omitted for brevity …\r\n },\r\n
},\r\n … omitted for brevity …\r\n});\r\n\r\n// 2. Feature Beta defition
(new)\r\ndeps.features.registerKibanaFeature({\r\n id:
'feature_beta',\r\n privileges: {\r\n all: {\r\n api:
['api_all_v2'],\r\n … omitted for brevity …\r\n }\r\n },\r\n … omitted
for brevity …\r\n});\r\n\r\n// 3. Route protected by `all` privilege of
the Feature Alpha OR Feature Beta\r\nrouter.post(\r\n { path:
'/api/domain/my_api', options: { tags: ['access:api_all_v2'] } },\r\n
async (_context, request, response) => {}\r\n);\r\n\r\n----\r\n\r\n// ❌
Old client-side code (supports only deprecated privileges)\r\nif
(capabilities.feature_alpha.ui_all) {\r\n … omitted for brevity
…\r\n}\r\n\r\n// ✅ New client-side code (will work for **both** new and
deprecated privileges)\r\nif (capabilities.feature_beta.ui_all) {\r\n …
omitted for brevity …\r\n}\r\n```\r\n</details>\r\n\r\n## How to
test\r\n\r\nThe code introduces a set of API integration tests that are
designed to\r\nvalidate whether the privilege mapping between deprecated
and\r\nreplacement privileges maintains backward
compatibility.\r\n\r\nYou can run the test server with the following
config to register a\r\nnumber of [example
deprecated\r\nfeatures](https://github.com/elastic/kibana/pull/186800/files#diff-d887981d43bbe30cda039340b906b0fa7649ba80230be4de8eda326036f10f6fR20-R49)(`x-pack/test/security_api_integration/plugins/features_provider/server/index.ts`)\r\nand
the features that replace them, to see the framework in
action:\r\n\r\n```bash\r\nnode scripts/functional_tests_server.js
--config
x-pack/test/security_api_integration/features.config.ts\r\n```\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<[email protected]>","sha":"cb2112cae51d5f69b9e47ebfde66cfacb2a6719b"}},"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/186800","number":186800,"mergeCommit":{"message":"feat:
allow plugins to deprecate and replace features and feature privileges
(elastic#186800)\n\n## Summary\r\n\r\nThis change is the implementation of the
`Kibana Privilege Migrations`\r\nproposal/RFC and provides a framework
that allows developers to replace\r\nan existing feature with a new one
that has the desired configuration\r\nwhile teaching the platform how
the privileges of the deprecated feature\r\ncan be represented by
non-deprecated ones. This approach avoids\r\nintroducing breaking
changes for users who still rely on the deprecated\r\nprivileges in
their existing roles and any automation.\r\n\r\nAmong the use cases the
framework is supposed to handle, the most common\r\nare the
following:\r\n\r\n* Changing a feature ID from `Alpha` to `Beta`\r\n*
Splitting a feature `Alpha` into two features, `Beta` and `Gamma`\r\n*
Moving a capability between privileges within a feature (top-level
or\r\nsub-feature)\r\n* Consolidating capabilities across independent
features\r\n\r\n## Scope\r\n\r\nThis PR includes only the core
functionality proposed in the RFC and\r\nmost of the necessary
guardrails (tests, early validations, etc.) to\r\nhelp engineers start
planning and implementing their migrations as soon\r\nas possible. The
following functionality will be added in follow-ups or\r\nonce we
collect enough feedback:\r\n\r\n* Telemetry\r\n* Developer
documentation\r\n* UI enhancements (highlighting roles with deprecated
privileges and\r\nmanual migration actions)\r\n\r\n##
Framework\r\n\r\nThe steps below use a scenario where a feature `Alpha`
should be split\r\ninto two other features `Beta` and `Gamma` as an
example.\r\n\r\n### Step 1: Create new features with the desired
privileges\r\n\r\nFirst of all, define new feature or features with the
desired\r\nconfiguration as you'd do before. There are no constraints
here.\r\n\r\n<details>\r\n\r\n<summary>Click to see the
code</summary>\r\n\r\n```ts\r\ndeps.features.registerKibanaFeature({\r\n
id: 'feature_beta',\r\n name: 'Feature Beta',\r\n privileges: {\r\n all:
{\r\n savedObject: { all: ['saved_object_1'], read: [] },\r\n ui:
['ui_all'],\r\n api: ['api_all'],\r\n … omitted for brevity …\r\n },\r\n
read: {\r\n savedObject: { all: [], read: ['saved_object_1'] },\r\n ui:
['ui_read'],\r\n api: ['api_read'],\r\n … omitted for brevity …\r\n
},\r\n },\r\n … omitted for brevity
…\r\n});\r\n\r\ndeps.features.registerKibanaFeature({\r\n id:
'feature_gamma',\r\n name: 'Feature Gamma',\r\n privileges: {\r\n all:
{\r\n savedObject: { all: ['saved_object_2'], read: [] },\r\n ui:
['ui_all'],\r\n // Note that Feature Gamma, unlike Features Alpha and
Beta doesn't provide any API access tags\r\n … omitted for brevity …\r\n
},\r\n read: {\r\n savedObject: { all: [], read: ['saved_object_2']
},\r\n ui: ['ui_read'],\r\n // Note that Feature Gamma, unlike Features
Alpha and Beta doesn't provide any API access tags\r\n … omitted for
brevity …\r\n },\r\n },\r\n … omitted for brevity
…\r\n});\r\n```\r\n\r\n</details>\r\n\r\n### Step 2: Mark existing
feature as deprecated\r\n\r\nOnce a feature is marked as deprecated, it
should essentially be treated\r\nas frozen for backward compatibility
reasons. Deprecated features will\r\nno longer be available through the
Kibana role management UI and will be\r\nreplaced with non-deprecated
privileges.\r\n\r\nDeprecated privileges will still be accepted if the
role is created or\r\nupdated via the Kibana role management APIs to
avoid disrupting existing\r\nuser automation.\r\n\r\nTo avoid breaking
existing roles that reference privileges provided by\r\nthe deprecated
features, Kibana will continue registering these\r\nprivileges as
Elasticsearch application
privileges.\r\n\r\n<details>\r\n\r\n<summary>Click to see the
code</summary>\r\n\r\n```ts\r\ndeps.features.registerKibanaFeature({\r\n
// This is a new `KibanaFeature` property available during feature
registration.\r\n deprecated: {\r\n // User-facing justification for
privilege deprecation that we can display\r\n // to the user when we ask
them to perform role migration.\r\n notice:
i18n.translate('xpack.security...', {\r\n defaultMessage: \"Feature
Alpha is deprecated, refer to {link}...\",\r\n values: { link:
docLinks.links.security.deprecatedFeatureAlpha },\r\n })\r\n },\r\n //
Feature id should stay unchanged, and it's not possible to reuse it.\r\n
id: 'feature_alpha',\r\n name: 'Feature Alpha (DEPRECATED)',\r\n
privileges: {\r\n all: {\r\n savedObject: { all: ['saved_object_1',
'saved_object_2'], read: [] },\r\n ui: ['ui_all'],\r\n api:
['api_all'],\r\n … omitted for brevity …\r\n },\r\n read: {\r\n
savedObject: { all: [], read: ['saved_object_1', 'saved_object_2']
},\r\n ui: ['ui_read'],\r\n api: ['api_read'],\r\n … omitted for brevity
…\r\n },\r\n },\r\n … omitted for brevity
…\r\n});\r\n```\r\n</details>\r\n\r\n### Step 3: Map deprecated
feature’s privileges to the privileges of the\r\nnon-deprecated
features\r\n\r\nThe important requirement for a successful migration
from a deprecated\r\nfeature to a new feature or features is that it
should be possible to\r\nexpress **any combination** of the deprecated
feature and sub-feature\r\nprivileges with the feature or sub-feature
privileges of non-deprecated\r\nfeatures. This way, while editing a role
with deprecated feature\r\nprivileges in the UI, the admin will be
interacting with new privileges\r\nas if they were creating a new role
from scratch, maintaining\r\nconsistency.\r\n\r\nThe relationship
between the privileges of the deprecated feature and\r\nthe privileges
of the features that are supposed to replace them is\r\nexpressed with a
new `replacedBy` property available on the privileges\r\nof the
deprecated feature.\r\n\r\n<details>\r\n\r\n<summary>Click to see the
code</summary>\r\n\r\n```ts\r\ndeps.features.registerKibanaFeature({\r\n
// This is a new `KibanaFeature` property available during feature
registration.\r\n deprecated: {\r\n // User-facing justification for
privilege deprecation that we can display\r\n // to the user when we ask
them to perform role migration.\r\n notice:
i18n.translate('xpack.security...', {\r\n defaultMessage: \"Feature
Alpha is deprecated, refer to {link}...\",\r\n values: { link:
docLinks.links.security.deprecatedFeatureAlpha },\r\n })\r\n },\r\n //
Feature id should stay unchanged, and it's not possible to reuse it.\r\n
id: 'feature_alpha',\r\n name: 'Feature Alpha (DEPRECATED)',\r\n
privileges: {\r\n all: {\r\n savedObject: { all: ['saved_object_1',
'saved_object_2'], read: [] },\r\n ui: ['ui_all'],\r\n api:
['api_all'],\r\n replacedBy: [\r\n { feature: 'feature_beta',
privileges: ['all'] },\r\n { feature: 'feature_gamma', privileges:
['all'] },\r\n ],\r\n … omitted for brevity …\r\n },\r\n read: {\r\n
savedObject: { all: [], read: ['saved_object_1', 'saved_object_2']
},\r\n ui: ['ui_read'],\r\n api: ['api_read'],\r\n replacedBy: [\r\n {
feature: 'feature_beta', privileges: ['read'] },\r\n { feature:
'feature_gamma', privileges: ['read'] },\r\n\t],\r\n … omitted for
brevity …\r\n },\r\n },\r\n … omitted for brevity
…\r\n});\r\n```\r\n\r\n</details>\r\n\r\n### Step 4: Adjust the code to
rely only on new, non-deprecated features\r\n\r\nSpecial care should be
taken if the replacement privileges cannot reuse\r\nthe API access tags
from the deprecated privileges and introduce new\r\ntags that will be
applied to the same API endpoints. In this case,\r\ndevelopers should
replace the API access tags of the deprecated\r\nprivileges with the
corresponding tags provided by the replacement\r\nprivileges. This is
necessary because API endpoints can only be accessed\r\nif the user
privileges cover all the tags listed in the API endpoint\r\ndefinition,
and without these changes, existing roles referencing\r\ndeprecated
privileges won’t be able to access those endpoints.\r\n\r\nThe UI
capabilities are handled slightly differently because they are\r\nalways
prefixed with the feature ID. When migrating to new features with\r\nnew
IDs, the code that interacts with UI capabilities will be updated
to\r\nuse these new feature IDs.\r\n\r\n<details>\r\n\r\n<summary>Click
to see the code</summary>\r\n\r\n```ts\r\n// BEFORE
deprecation/migration\r\n// 1. Feature Alpha defition (not deprecated
yet)\r\ndeps.features.registerKibanaFeature({\r\n id:
'feature_alpha',\r\n privileges: {\r\n all: {\r\n api: ['api_all'],\r\n
… omitted for brevity …\r\n },\r\n },\r\n … omitted for brevity
…\r\n});\r\n\r\n// 2. Route protected by `all` privilege of the Feature
Alpha\r\nrouter.post(\r\n { path: '/api/domain/my_api', options: { tags:
['access:api_all'] } },\r\n async (_context, request, response) =>
{}\r\n);\r\n\r\n// AFTER deprecation/migration\r\n// 1. Feature Alpha
defition (deprecated, with updated API
tags)\r\ndeps.features.registerKibanaFeature({\r\n deprecated: …,\r\n
id: 'feature_alpha',\r\n privileges: {\r\n all: {\r\n api:
['api_all_v2'],\r\n replacedBy: [\r\n { feature: 'feature_beta',
privileges: ['all'] },\r\n ],\r\n … omitted for brevity …\r\n },\r\n
},\r\n … omitted for brevity …\r\n});\r\n\r\n// 2. Feature Beta defition
(new)\r\ndeps.features.registerKibanaFeature({\r\n id:
'feature_beta',\r\n privileges: {\r\n all: {\r\n api:
['api_all_v2'],\r\n … omitted for brevity …\r\n }\r\n },\r\n … omitted
for brevity …\r\n});\r\n\r\n// 3. Route protected by `all` privilege of
the Feature Alpha OR Feature Beta\r\nrouter.post(\r\n { path:
'/api/domain/my_api', options: { tags: ['access:api_all_v2'] } },\r\n
async (_context, request, response) => {}\r\n);\r\n\r\n----\r\n\r\n// ❌
Old client-side code (supports only deprecated privileges)\r\nif
(capabilities.feature_alpha.ui_all) {\r\n … omitted for brevity
…\r\n}\r\n\r\n// ✅ New client-side code (will work for **both** new and
deprecated privileges)\r\nif (capabilities.feature_beta.ui_all) {\r\n …
omitted for brevity …\r\n}\r\n```\r\n</details>\r\n\r\n## How to
test\r\n\r\nThe code introduces a set of API integration tests that are
designed to\r\nvalidate whether the privilege mapping between deprecated
and\r\nreplacement privileges maintains backward
compatibility.\r\n\r\nYou can run the test server with the following
config to register a\r\nnumber of [example
deprecated\r\nfeatures](https://github.com/elastic/kibana/pull/186800/files#diff-d887981d43bbe30cda039340b906b0fa7649ba80230be4de8eda326036f10f6fR20-R49)(`x-pack/test/security_api_integration/plugins/features_provider/server/index.ts`)\r\nand
the features that replace them, to see the framework in
action:\r\n\r\n```bash\r\nnode scripts/functional_tests_server.js
--config
x-pack/test/security_api_integration/features.config.ts\r\n```\r\n\r\n---------\r\n\r\nCo-authored-by:
kibanamachine
<[email protected]>","sha":"cb2112cae51d5f69b9e47ebfde66cfacb2a6719b"}},{"branch":"8.x","label":"v8.16.0","labelRegex":"^v8.16.0$","isSourceBranch":false,"state":"NOT_CREATED"}]}]
BACKPORT-->
  • Loading branch information
azasypkin authored Oct 15, 2024
1 parent abfed86 commit d63d726
Show file tree
Hide file tree
Showing 75 changed files with 3,895 additions and 183 deletions.
1 change: 1 addition & 0 deletions .buildkite/ftr_platform_stateful_configs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@ enabled:
- x-pack/test/security_api_integration/saml.http2.config.ts
- x-pack/test/security_api_integration/saml_cloud.config.ts
- x-pack/test/security_api_integration/chips.config.ts
- x-pack/test/security_api_integration/features.config.ts
- x-pack/test/security_api_integration/session_idle.config.ts
- x-pack/test/security_api_integration/session_invalidate.config.ts
- x-pack/test/security_api_integration/session_lifespan.config.ts
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,7 @@
"@kbn/feature-flags-example-plugin": "link:examples/feature_flags_example",
"@kbn/feature-usage-test-plugin": "link:x-pack/test/plugin_api_integration/plugins/feature_usage_test",
"@kbn/features-plugin": "link:x-pack/plugins/features",
"@kbn/features-provider-plugin": "link:x-pack/test/security_api_integration/plugins/features_provider",
"@kbn/fec-alerts-test-plugin": "link:x-pack/test/functional_execution_context/plugins/alerts",
"@kbn/field-formats-example-plugin": "link:examples/field_formats_example",
"@kbn/field-formats-plugin": "link:src/plugins/field_formats",
Expand Down Expand Up @@ -797,6 +798,7 @@
"@kbn/searchprofiler-plugin": "link:x-pack/plugins/searchprofiler",
"@kbn/security-api-key-management": "link:x-pack/packages/security/api_key_management",
"@kbn/security-authorization-core": "link:x-pack/packages/security/authorization_core",
"@kbn/security-authorization-core-common": "link:x-pack/packages/security/authorization_core_common",
"@kbn/security-form-components": "link:x-pack/packages/security/form_components",
"@kbn/security-hardening": "link:packages/kbn-security-hardening",
"@kbn/security-plugin": "link:x-pack/plugins/security",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,28 @@ import { KbnClient } from '@kbn/test';
export class Role {
constructor(private log: ToolingLog, private kibanaServer: KbnClient) {}

public async get(
name: string,
{ replaceDeprecatedPrivileges = true }: { replaceDeprecatedPrivileges?: boolean } = {}
) {
this.log.debug(`retrieving role ${name}`);
const { data, status, statusText } = await this.kibanaServer
.request({
path: `/api/security/role/${name}?replaceDeprecatedPrivileges=${replaceDeprecatedPrivileges}`,
method: 'GET',
})
.catch((e) => {
throw new Error(util.inspect(e.axiosError.response, true));
});
if (status !== 200) {
throw new Error(
`Expected status code of 200, received ${status} ${statusText}: ${util.inspect(data)}`
);
}

return data;
}

public async create(name: string, role: any) {
this.log.debug(`creating role ${name}`);
const { data, status, statusText } = await this.kibanaServer.request({
Expand Down
4 changes: 4 additions & 0 deletions tsconfig.base.json
Original file line number Diff line number Diff line change
Expand Up @@ -926,6 +926,8 @@
"@kbn/feature-usage-test-plugin/*": ["x-pack/test/plugin_api_integration/plugins/feature_usage_test/*"],
"@kbn/features-plugin": ["x-pack/plugins/features"],
"@kbn/features-plugin/*": ["x-pack/plugins/features/*"],
"@kbn/features-provider-plugin": ["x-pack/test/security_api_integration/plugins/features_provider"],
"@kbn/features-provider-plugin/*": ["x-pack/test/security_api_integration/plugins/features_provider/*"],
"@kbn/fec-alerts-test-plugin": ["x-pack/test/functional_execution_context/plugins/alerts"],
"@kbn/fec-alerts-test-plugin/*": ["x-pack/test/functional_execution_context/plugins/alerts/*"],
"@kbn/field-formats-example-plugin": ["examples/field_formats_example"],
Expand Down Expand Up @@ -1550,6 +1552,8 @@
"@kbn/security-api-key-management/*": ["x-pack/packages/security/api_key_management/*"],
"@kbn/security-authorization-core": ["x-pack/packages/security/authorization_core"],
"@kbn/security-authorization-core/*": ["x-pack/packages/security/authorization_core/*"],
"@kbn/security-authorization-core-common": ["x-pack/packages/security/authorization_core_common"],
"@kbn/security-authorization-core-common/*": ["x-pack/packages/security/authorization_core_common/*"],
"@kbn/security-form-components": ["x-pack/packages/security/form_components"],
"@kbn/security-form-components/*": ["x-pack/packages/security/form_components/*"],
"@kbn/security-hardening": ["packages/kbn-security-hardening"],
Expand Down
9 changes: 2 additions & 7 deletions x-pack/packages/security/authorization_core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,5 @@
*/

export { Actions } from './src/actions';
export { privilegesFactory } from './src/privileges';
export type {
CasesSupportedOperations,
PrivilegesService,
RawKibanaPrivileges,
RawKibanaFeaturePrivileges,
} from './src/privileges';
export { privilegesFactory, getReplacedByForPrivilege } from './src/privileges';
export type { CasesSupportedOperations, PrivilegesService } from './src/privileges';
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,21 @@ 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
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,12 @@ 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);
}
}
12 changes: 12 additions & 0 deletions x-pack/packages/security/authorization_core/src/actions/ui.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,15 @@ describe('#get', () => {
expect(uiActions.get('foo', 'fooCapability', 'subFoo')).toBe('ui:foo/fooCapability/subFoo');
});
});

test('#isValid', () => {
const uiActions = new UIActions();
expect(uiActions.isValid('ui:alpha')).toBe(true);
expect(uiActions.isValid('ui:beta')).toBe(true);

expect(uiActions.isValid('api:alpha')).toBe(false);
expect(uiActions.isValid('api:beta')).toBe(false);

expect(uiActions.isValid('ui_alpha')).toBe(false);
expect(uiActions.isValid('ui_beta')).toBe(false);
});
8 changes: 8 additions & 0 deletions x-pack/packages/security/authorization_core/src/actions/ui.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,12 @@ export class UIActions implements UIActionsType {

return `${this.prefix}${featureId}/${uiCapabilityParts.join('/')}`;
}

/**
* Checks if the action is a valid UI action.
* @param action The action string to check.
*/
public isValid(action: string) {
return action.startsWith(this.prefix);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,4 @@

export type { PrivilegesService } from './privileges';
export type { CasesSupportedOperations } from './feature_privilege_builder';
export { privilegesFactory } from './privileges';
export type { RawKibanaPrivileges, RawKibanaFeaturePrivileges } from './raw_kibana_privileges';
export { privilegesFactory, getReplacedByForPrivilege } from './privileges';
Loading

0 comments on commit d63d726

Please sign in to comment.