From e41c0b1224001a4dc8014279056fa438f43ab1cc Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Wed, 15 Jan 2020 11:07:19 +1100 Subject: [PATCH] Deprecating kibana_user and kibana_dashboard_only_user roles (#50963) This change adds a new `kibana_admin` role, and deprecates the old `kibana_user` and`kibana_dashboard_only_user`roles. The deprecation is implemented via a new reserved metadata attribute, which can be consumed from the API and also triggers deprecation logging when used (by a user authenticating to Elasticsearch). Some docs have been updated to avoid references to these deprecated roles. Backport of: #46456 Co-authored-by: Larry Gregory --- .../SecurityDocumentationIT.java | 4 +- .../monitoring/configuring-filebeat.asciidoc | 2 +- .../authentication/oidc-guide.asciidoc | 10 +- .../authentication/saml-guide.asciidoc | 10 +- .../authorization/built-in-roles.asciidoc | 30 ++- .../cross-cluster-kibana.asciidoc | 5 +- .../en/security/get-started-security.asciidoc | 9 +- .../authz/store/ReservedRolesStore.java | 20 +- .../core/security/support/MetadataUtils.java | 12 ++ .../authz/store/ReservedRolesStoreTests.java | 52 ++++++ .../authz/store/CompositeRolesStore.java | 15 ++ .../authz/store/CompositeRolesStoreTests.java | 172 +++++++++++++----- .../authc/oidc/OpenIdConnectAuthIT.java | 4 +- 13 files changed, 268 insertions(+), 77 deletions(-) diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java index fa45739052c..0262f54c457 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java @@ -693,8 +693,8 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase { List roles = response.getRoles(); assertNotNull(response); - // 28 system roles plus the three we created - assertThat(roles.size(), equalTo(28 + 3)); + // 29 system roles plus the three we created + assertThat(roles.size(), equalTo(29 + 3)); } { diff --git a/docs/reference/monitoring/configuring-filebeat.asciidoc b/docs/reference/monitoring/configuring-filebeat.asciidoc index d51c65bbac8..6f4b5eee8ec 100644 --- a/docs/reference/monitoring/configuring-filebeat.asciidoc +++ b/docs/reference/monitoring/configuring-filebeat.asciidoc @@ -117,7 +117,7 @@ If {security-features} are enabled, you must provide a valid user ID and password so that {filebeat} can connect to {kib}: .. Create a user on the monitoring cluster that has the -<> or equivalent +<> or equivalent privileges. .. Add the `username` and `password` settings to the {es} output information in diff --git a/x-pack/docs/en/security/authentication/oidc-guide.asciidoc b/x-pack/docs/en/security/authentication/oidc-guide.asciidoc index fca31f4ee96..0cbb2c9a641 100644 --- a/x-pack/docs/en/security/authentication/oidc-guide.asciidoc +++ b/x-pack/docs/en/security/authentication/oidc-guide.asciidoc @@ -418,14 +418,14 @@ through either the NOTE: You cannot use <> to grant roles to users authenticating via OpenID Connect. -This is an example of a simple role mapping that grants the `kibana_user` role +This is an example of a simple role mapping that grants the `example_role` role to any user who authenticates against the `oidc1` OpenID Connect realm: [source,console] -------------------------------------------------- -PUT /_security/role_mapping/oidc-kibana +PUT /_security/role_mapping/oidc-example { - "roles": [ "kibana_user" ], + "roles": [ "example_role" ], <1> "enabled": true, "rules": { "field": { "realm.name": "oidc1" } @@ -433,6 +433,10 @@ PUT /_security/role_mapping/oidc-kibana } -------------------------------------------------- +<1> The `example_role` role is *not* a builtin Elasticsearch role. +This example assumes that you have created a custom role of your own, with +appropriate access to your <> and +{kibana-ref}/kibana-privileges.html#kibana-feature-privileges[Kibana features]. The user properties that are mapped via the realm configuration are used to process role mapping rules, and these rules determine which roles a user is granted. diff --git a/x-pack/docs/en/security/authentication/saml-guide.asciidoc b/x-pack/docs/en/security/authentication/saml-guide.asciidoc index 66ef0143bb2..d65d801e164 100644 --- a/x-pack/docs/en/security/authentication/saml-guide.asciidoc +++ b/x-pack/docs/en/security/authentication/saml-guide.asciidoc @@ -631,14 +631,14 @@ through either the NOTE: You cannot use <> to grant roles to users authenticating via SAML. -This is an example of a simple role mapping that grants the `kibana_user` role +This is an example of a simple role mapping that grants the `example_role` role to any user who authenticates against the `saml1` realm: [source,console] -------------------------------------------------- -PUT /_security/role_mapping/saml-kibana +PUT /_security/role_mapping/saml-example { - "roles": [ "kibana_user" ], + "roles": [ "example_role" ], <1> "enabled": true, "rules": { "field": { "realm.name": "saml1" } @@ -646,6 +646,10 @@ PUT /_security/role_mapping/saml-kibana } -------------------------------------------------- +<1> The `example_role` role is *not* a builtin Elasticsearch role. +This example assumes that you have created a custom role of your own, with +appropriate access to your <> and +{kibana-ref}/kibana-privileges.html#kibana-feature-privileges[Kibana features]. The attributes that are mapped via the realm configuration are used to process role mapping rules, and these rules determine which roles a user is granted. diff --git a/x-pack/docs/en/security/authorization/built-in-roles.asciidoc b/x-pack/docs/en/security/authorization/built-in-roles.asciidoc index 55d12709124..60043e62f94 100644 --- a/x-pack/docs/en/security/authorization/built-in-roles.asciidoc +++ b/x-pack/docs/en/security/authorization/built-in-roles.asciidoc @@ -72,10 +72,12 @@ NOTE: This role does *not* provide the ability to create indices; those privileg must be defined in a separate role. [[built-in-roles-kibana-dashboard]] `kibana_dashboard_only_user` :: -Grants access to the {kib} Dashboard and read-only permissions to Kibana. -This role does not have access to editing tools in {kib}. For more -information, see -{kibana-ref}/xpack-dashboard-only-mode.html[{kib} Dashboard Only Mode]. +(This role is deprecated, please use +{kibana-ref}/kibana-privileges.html#kibana-feature-privileges[{kib} feature privileges] +instead). +Grants read-only access to the {kib} Dashboard in every +{kibana-ref}/xpack-spaces.html[space in {kib}]. +This role does not have access to editing tools in {kib}. [[built-in-roles-kibana-system]] `kibana_system` :: Grants access necessary for the {kib} system user to read from and write to the @@ -87,9 +89,15 @@ see {kibana-ref}/using-kibana-with-security.html[Configuring Security in {kib}]. NOTE: This role should not be assigned to users as the granted permissions may change between releases. +[[built-in-roles-kibana-admin]] `kibana_admin`:: +Grants access to all features in {kib}. For more information on {kib} authorization, +see {kibana-ref}/xpack-security-authorization.html[Kibana authorization]. + [[built-in-roles-kibana-user]] `kibana_user`:: -Grants access to all features in {kib}. For more information on Kibana authorization, -see {kibana-ref}/xpack-security-authorization.html[Kibana Authorization]. +(This role is deprecated, please use the +<> role instead.) +Grants access to all features in {kib}. For more information on {kib} authorization, +see {kibana-ref}/xpack-security-authorization.html[Kibana authorization]. [[built-in-roles-logstash-admin]] `logstash_admin` :: Grants access to the `.logstash*` indices for managing configurations. @@ -127,7 +135,8 @@ Grants the minimum privileges required for any user of {monitoring} other than t required to use {kib}. This role grants access to the monitoring indices and grants privileges necessary for reading basic cluster information. This role also includes all {kibana-ref}/kibana-privileges.html[Kibana privileges] for the {stack-monitor-features}. -Monitoring users should also be assigned the `kibana_user` role. +Monitoring users should also be assigned the `kibana_admin` role, or another role +with {kibana-ref}/xpack-security-authorization.html[access to the {kib} instance]. [[built-in-roles-remote-monitoring-agent]] `remote_monitoring_agent`:: Grants the minimum privileges required to write data into the monitoring indices @@ -140,9 +149,10 @@ Grants the minimum privileges required to collect monitoring data for the {stack [[built-in-roles-reporting-user]] `reporting_user`:: Grants the specific privileges required for users of {reporting} other than those required to use {kib}. This role grants access to the reporting indices; each -user has access to only their own reports. Reporting users should also be -assigned the `kibana_user` role and a role that grants them access to the data -that will be used to generate reports. +user has access to only their own reports. +Reporting users should also be assigned additional roles that grant +{kibana-ref}/xpack-security-authorization.html[access to {kib}] as well as read +access to the <> that will be used to generate reports. [[built-in-roles-snapshot-user]] `snapshot_user`:: Grants the necessary privileges to create snapshots of **all** the indices and diff --git a/x-pack/docs/en/security/ccs-clients-integrations/cross-cluster-kibana.asciidoc b/x-pack/docs/en/security/ccs-clients-integrations/cross-cluster-kibana.asciidoc index 95e5d188f00..f9bcd86889c 100644 --- a/x-pack/docs/en/security/ccs-clients-integrations/cross-cluster-kibana.asciidoc +++ b/x-pack/docs/en/security/ccs-clients-integrations/cross-cluster-kibana.asciidoc @@ -31,8 +31,9 @@ NOTE: If you configure the local cluster as another remote in {es}, the `logstash_reader` role on your local cluster also needs to grant the `read_cross_cluster` privilege. -. Assign your {kib} users the `kibana_user` role and your `logstash_reader` -role. +. Assign your {kib} users a role that grants +{kibana-ref}/xpack-security-authorization.html[access to {kib}] +as well as your `logstash_reader` role. . On the remote cluster, create a `logstash_reader` role that grants the `read_cross_cluster` privilege and `read` and `view_index_metadata` privileges diff --git a/x-pack/docs/en/security/get-started-security.asciidoc b/x-pack/docs/en/security/get-started-security.asciidoc index 12c27e16634..fba70ccc022 100644 --- a/x-pack/docs/en/security/get-started-security.asciidoc +++ b/x-pack/docs/en/security/get-started-security.asciidoc @@ -167,15 +167,16 @@ Select a role to see more information about its privileges. For example, select the `kibana_system` role to see its list of cluster and index privileges. To learn more, see <>. -Let's assign the `kibana_user` role to your user. Go back to the -*Management / Security / Users* page and select your user. Add the `kibana_user` +Let's assign the `kibana_admin` role to your user. Go back to the +*Management / Security / Users* page and select your user. Add the `kibana_admin` role and save the change. For example: [role="screenshot"] image::security/images/assign-role.jpg["Assigning a role to a user in Kibana"] -This user now has access to all features in {kib}. For more information about granting -access to Kibana see {kibana-ref}/xpack-security-authorization.html[Kibana Authorization]. +This user now has administrative access to all features in {kib}. +For more information about granting access to Kibana see +{kibana-ref}/xpack-security-authorization.html[Kibana authorization]. If you completed all of the steps in {stack-gs}/get-started-elastic-stack.html[Getting started with the {stack}], you should diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java index a86f213e248..7899797b890 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java @@ -52,11 +52,9 @@ public class ReservedRolesStore implements BiConsumer, ActionListene .put("superuser", SUPERUSER_ROLE_DESCRIPTOR) .put("transport_client", new RoleDescriptor("transport_client", new String[] { "transport_client" }, null, null, MetadataUtils.DEFAULT_RESERVED_METADATA)) - .put("kibana_user", new RoleDescriptor("kibana_user", null, null, new RoleDescriptor.ApplicationResourcePrivileges[] { - RoleDescriptor.ApplicationResourcePrivileges.builder() - .application("kibana-.kibana").resources("*").privileges("all").build() }, - null, null, - MetadataUtils.DEFAULT_RESERVED_METADATA, null)) + .put("kibana_admin", kibanaAdminUser("kibana_admin", MetadataUtils.DEFAULT_RESERVED_METADATA)) + .put("kibana_user", kibanaAdminUser("kibana_user", + MetadataUtils.getDeprecatedReservedMetadata("Please use the [kibana_admin] role instead"))) .put("monitoring_user", new RoleDescriptor("monitoring_user", new String[] { "cluster:monitor/main", "cluster:monitor/xpack/info", RemoteInfoAction.NAME }, new RoleDescriptor.IndicesPrivileges[] { @@ -110,7 +108,7 @@ public class ReservedRolesStore implements BiConsumer, ActionListene RoleDescriptor.ApplicationResourcePrivileges.builder() .application("kibana-.kibana").resources("*").privileges("read").build() }, null, null, - MetadataUtils.DEFAULT_RESERVED_METADATA, + MetadataUtils.getDeprecatedReservedMetadata("Please use Kibana feature privileges instead"), null)) .put(KibanaUser.ROLE_NAME, new RoleDescriptor(KibanaUser.ROLE_NAME, new String[] { @@ -266,6 +264,16 @@ public class ReservedRolesStore implements BiConsumer, ActionListene .immutableMap(); } + private static RoleDescriptor kibanaAdminUser(String name, Map metadata) { + return new RoleDescriptor(name, null, null, + new RoleDescriptor.ApplicationResourcePrivileges[] { + RoleDescriptor.ApplicationResourcePrivileges.builder() + .application("kibana-.kibana") + .resources("*").privileges("all") + .build() }, + null, null, metadata, null); + } + public static boolean isReserved(String role) { return RESERVED_ROLES.containsKey(role) || UsernamesField.SYSTEM_ROLE.equals(role) || UsernamesField.XPACK_ROLE.equals(role); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/MetadataUtils.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/MetadataUtils.java index a48f562c7af..6398488b44b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/MetadataUtils.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/support/MetadataUtils.java @@ -5,6 +5,8 @@ */ package org.elasticsearch.xpack.core.security.support; +import org.elasticsearch.common.collect.MapBuilder; + import java.util.Collections; import java.util.Map; @@ -12,6 +14,8 @@ public class MetadataUtils { public static final String RESERVED_PREFIX = "_"; public static final String RESERVED_METADATA_KEY = RESERVED_PREFIX + "reserved"; + public static final String DEPRECATED_METADATA_KEY = RESERVED_PREFIX + "deprecated"; + public static final String DEPRECATED_REASON_METADATA_KEY = RESERVED_PREFIX + "deprecated_reason"; public static final Map DEFAULT_RESERVED_METADATA = Collections.singletonMap(RESERVED_METADATA_KEY, true); private MetadataUtils() { @@ -25,4 +29,12 @@ public class MetadataUtils { } return false; } + + public static Map getDeprecatedReservedMetadata(String reason) { + return MapBuilder.newMapBuilder() + .put(RESERVED_METADATA_KEY, true) + .put(DEPRECATED_METADATA_KEY, true) + .put(DEPRECATED_REASON_METADATA_KEY, reason) + .immutableMap(); + } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java index e7166f0c94f..11cc52a970f 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStoreTests.java @@ -169,6 +169,7 @@ import java.util.SortedMap; import static org.hamcrest.Matchers.hasEntry; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; import static org.mockito.Mockito.mock; /** @@ -184,6 +185,7 @@ public class ReservedRolesStoreTests extends ESTestCase { assertThat(ReservedRolesStore.isReserved("foobar"), is(false)); assertThat(ReservedRolesStore.isReserved(SystemUser.ROLE_NAME), is(true)); assertThat(ReservedRolesStore.isReserved("transport_client"), is(true)); + assertThat(ReservedRolesStore.isReserved("kibana_admin"), is(true)); assertThat(ReservedRolesStore.isReserved("kibana_user"), is(true)); assertThat(ReservedRolesStore.isReserved("ingest_admin"), is(true)); assertThat(ReservedRolesStore.isReserved("monitoring_user"), is(true)); @@ -409,6 +411,54 @@ public class ReservedRolesStoreTests extends ESTestCase { assertNoAccessAllowed(kibanaRole, RestrictedIndicesNames.ASYNC_SEARCH_PREFIX + randomAlphaOfLengthBetween(0, 2)); } + public void testKibanaAdminRole() { + final TransportRequest request = mock(TransportRequest.class); + final Authentication authentication = mock(Authentication.class); + + RoleDescriptor roleDescriptor = new ReservedRolesStore().roleDescriptor("kibana_admin"); + assertNotNull(roleDescriptor); + assertThat(roleDescriptor.getMetadata(), hasEntry("_reserved", true)); + assertThat(roleDescriptor.getMetadata(), not(hasEntry("_deprecated", true))); + + Role kibanaAdminRole = Role.builder(roleDescriptor, null).build(); + assertThat(kibanaAdminRole.cluster().check(ClusterHealthAction.NAME, request, authentication), is(false)); + assertThat(kibanaAdminRole.cluster().check(ClusterStateAction.NAME, request, authentication), is(false)); + assertThat(kibanaAdminRole.cluster().check(ClusterStatsAction.NAME, request, authentication), is(false)); + assertThat(kibanaAdminRole.cluster().check(PutIndexTemplateAction.NAME, request, authentication), is(false)); + assertThat(kibanaAdminRole.cluster().check(ClusterRerouteAction.NAME, request, authentication), is(false)); + assertThat(kibanaAdminRole.cluster().check(ClusterUpdateSettingsAction.NAME, request, authentication), + is(false)); + assertThat(kibanaAdminRole.cluster().check(MonitoringBulkAction.NAME, request, authentication), is(false)); + assertThat(kibanaAdminRole.cluster().check(DelegatePkiAuthenticationAction.NAME, request, authentication), + is(false)); + + assertThat(kibanaAdminRole.runAs().check(randomAlphaOfLengthBetween(1, 12)), is(false)); + + assertThat(kibanaAdminRole.indices().allowedIndicesMatcher(IndexAction.NAME).test("foo"), is(false)); + assertThat(kibanaAdminRole.indices().allowedIndicesMatcher(IndexAction.NAME).test(".reporting"), is(false)); + assertThat( + kibanaAdminRole.indices().allowedIndicesMatcher("indices:foo").test(randomAlphaOfLengthBetween(8, 24)), + is(false)); + + final String randomApplication = "kibana-" + randomAlphaOfLengthBetween(8, 24); + assertThat(kibanaAdminRole.application().grants(new ApplicationPrivilege(randomApplication, "app-random", "all"), + "*"), is(false)); + + final String application = "kibana-.kibana"; + assertThat(kibanaAdminRole.application().grants(new ApplicationPrivilege(application, "app-foo", "foo"), "*"), + is(false)); + assertThat(kibanaAdminRole.application().grants(new ApplicationPrivilege(application, "app-all", "all"), "*"), + is(true)); + + final String applicationWithRandomIndex = "kibana-.kibana_" + randomAlphaOfLengthBetween(8, 24); + assertThat( + kibanaAdminRole.application() + .grants(new ApplicationPrivilege(applicationWithRandomIndex, "app-random-index", "all"), "*"), + is(false)); + + assertNoAccessAllowed(kibanaAdminRole, RestrictedIndicesNames.RESTRICTED_NAMES); + } + public void testKibanaUserRole() { final TransportRequest request = mock(TransportRequest.class); final Authentication authentication = mock(Authentication.class); @@ -416,6 +466,7 @@ public class ReservedRolesStoreTests extends ESTestCase { RoleDescriptor roleDescriptor = new ReservedRolesStore().roleDescriptor("kibana_user"); assertNotNull(roleDescriptor); assertThat(roleDescriptor.getMetadata(), hasEntry("_reserved", true)); + assertThat(roleDescriptor.getMetadata(), hasEntry("_deprecated", true)); Role kibanaUserRole = Role.builder(roleDescriptor, null).build(); assertThat(kibanaUserRole.cluster().check(ClusterHealthAction.NAME, request, authentication), is(false)); @@ -745,6 +796,7 @@ public class ReservedRolesStoreTests extends ESTestCase { RoleDescriptor roleDescriptor = new ReservedRolesStore().roleDescriptor("kibana_dashboard_only_user"); assertNotNull(roleDescriptor); assertThat(roleDescriptor.getMetadata(), hasEntry("_reserved", true)); + assertThat(roleDescriptor.getMetadata(), hasEntry("_deprecated", true)); Role dashboardsOnlyUserRole = Role.builder(roleDescriptor, null).build(); assertThat(dashboardsOnlyUserRole.cluster().check(ClusterHealthAction.NAME, request, authentication), is(false)); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java index 76e18669129..54d86eda37f 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStore.java @@ -17,6 +17,7 @@ import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.cache.Cache; import org.elasticsearch.common.cache.CacheBuilder; import org.elasticsearch.common.collect.Tuple; +import org.elasticsearch.common.logging.DeprecationLogger; import org.elasticsearch.common.settings.Setting; import org.elasticsearch.common.settings.Setting.Property; import org.elasticsearch.common.settings.Settings; @@ -41,6 +42,7 @@ import org.elasticsearch.xpack.core.security.authz.privilege.Privilege; import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; import org.elasticsearch.xpack.core.security.authz.store.RoleRetrievalResult; import org.elasticsearch.xpack.core.security.support.CacheIteratorHelper; +import org.elasticsearch.xpack.core.security.support.MetadataUtils; import org.elasticsearch.xpack.core.security.user.AnonymousUser; import org.elasticsearch.xpack.core.security.user.SystemUser; import org.elasticsearch.xpack.core.security.user.User; @@ -83,6 +85,7 @@ public class CompositeRolesStore { Setting.intSetting("xpack.security.authz.store.roles.negative_lookup_cache.max_size", 10000, Property.NodeScope); private static final Logger logger = LogManager.getLogger(CompositeRolesStore.class); + private final DeprecationLogger deprecationLogger = new DeprecationLogger(logger); private final FileRolesStore fileRolesStore; private final NativeRolesStore nativeRolesStore; @@ -154,6 +157,7 @@ public class CompositeRolesStore { final long invalidationCounter = numInvalidation.get(); roleDescriptors(roleNames, ActionListener.wrap( rolesRetrievalResult -> { + logDeprecatedRoles(rolesRetrievalResult.roleDescriptors); final boolean missingRoles = rolesRetrievalResult.getMissingRoles().isEmpty() == false; if (missingRoles) { logger.debug(() -> new ParameterizedMessage("Could not find roles with names {}", @@ -179,6 +183,17 @@ public class CompositeRolesStore { } } + void logDeprecatedRoles(Set roleDescriptors) { + roleDescriptors.stream() + .filter(rd -> Boolean.TRUE.equals(rd.getMetadata().get(MetadataUtils.DEPRECATED_METADATA_KEY))) + .forEach(rd -> { + String reason = Objects.toString( + rd.getMetadata().get(MetadataUtils.DEPRECATED_REASON_METADATA_KEY), "Please check the documentation"); + deprecationLogger.deprecatedAndMaybeLog("deprecated_role-" + rd.getName(), "The role [" + rd.getName() + + "] is deprecated and will be removed in a future version of Elasticsearch. " + reason); + }); + } + public void getRoles(User user, Authentication authentication, ActionListener roleActionListener) { // we need to special case the internal users in this method, if we apply the anonymous roles to every user including these system // user accounts then we run into the chance of a deadlock because then we need to get a role that we may be trying to get as the diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java index 82af02dacdb..53d644eb9e4 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/store/CompositeRolesStoreTests.java @@ -15,6 +15,7 @@ import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.cluster.health.ClusterHealthStatus; import org.elasticsearch.cluster.metadata.IndexMetaData; import org.elasticsearch.cluster.metadata.MetaData; +import org.elasticsearch.common.Nullable; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.settings.Settings; @@ -53,6 +54,7 @@ import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege; import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; import org.elasticsearch.xpack.core.security.authz.store.RoleRetrievalResult; import org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames; +import org.elasticsearch.xpack.core.security.support.MetadataUtils; import org.elasticsearch.xpack.core.security.user.AnonymousUser; import org.elasticsearch.xpack.core.security.user.SystemUser; import org.elasticsearch.xpack.core.security.user.User; @@ -64,11 +66,14 @@ import org.elasticsearch.xpack.security.support.SecurityIndexManager; import java.io.IOException; import java.time.Instant; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; +import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ExecutionException; @@ -85,6 +90,7 @@ import static org.hamcrest.Matchers.anyOf; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasItem; +import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.nullValue; import static org.mockito.Matchers.any; @@ -144,21 +150,14 @@ public class CompositeRolesStoreTests extends ESTestCase { }, null); FileRolesStore fileRolesStore = mock(FileRolesStore.class); doCallRealMethod().when(fileRolesStore).accept(any(Set.class), any(ActionListener.class)); - ReservedRolesStore reservedRolesStore = mock(ReservedRolesStore.class); - doCallRealMethod().when(reservedRolesStore).accept(any(Set.class), any(ActionListener.class)); - NativeRolesStore nativeRolesStore = mock(NativeRolesStore.class); - doCallRealMethod().when(nativeRolesStore).accept(any(Set.class), any(ActionListener.class)); when(fileRolesStore.roleDescriptors(Collections.singleton("fls"))).thenReturn(Collections.singleton(flsRole)); when(fileRolesStore.roleDescriptors(Collections.singleton("dls"))).thenReturn(Collections.singleton(dlsRole)); when(fileRolesStore.roleDescriptors(Collections.singleton("fls_dls"))).thenReturn(Collections.singleton(flsDlsRole)); when(fileRolesStore.roleDescriptors(Collections.singleton("no_fls_dls"))).thenReturn(Collections.singleton(noFlsDlsRole)); final AtomicReference> effectiveRoleDescriptors = new AtomicReference>(); - final DocumentSubsetBitsetCache documentSubsetBitsetCache = buildBitsetCache(); - CompositeRolesStore compositeRolesStore = new CompositeRolesStore(Settings.EMPTY, fileRolesStore, nativeRolesStore, - reservedRolesStore, mock(NativePrivilegeStore.class), Collections.emptyList(), - new ThreadContext(Settings.EMPTY), licenseState, cache, mock(ApiKeyService.class), documentSubsetBitsetCache, - rds -> effectiveRoleDescriptors.set(rds)); + CompositeRolesStore compositeRolesStore = buildCompositeRolesStore(Settings.EMPTY, fileRolesStore, null, + null, null, licenseState, null, null, rds -> effectiveRoleDescriptors.set(rds)); PlainActionFuture roleFuture = new PlainActionFuture<>(); compositeRolesStore.roles(Collections.singleton("fls"), roleFuture); @@ -221,20 +220,13 @@ public class CompositeRolesStoreTests extends ESTestCase { }, null); FileRolesStore fileRolesStore = mock(FileRolesStore.class); doCallRealMethod().when(fileRolesStore).accept(any(Set.class), any(ActionListener.class)); - ReservedRolesStore reservedRolesStore = mock(ReservedRolesStore.class); - doCallRealMethod().when(reservedRolesStore).accept(any(Set.class), any(ActionListener.class)); - NativeRolesStore nativeRolesStore = mock(NativeRolesStore.class); - doCallRealMethod().when(nativeRolesStore).accept(any(Set.class), any(ActionListener.class)); when(fileRolesStore.roleDescriptors(Collections.singleton("fls"))).thenReturn(Collections.singleton(flsRole)); when(fileRolesStore.roleDescriptors(Collections.singleton("dls"))).thenReturn(Collections.singleton(dlsRole)); when(fileRolesStore.roleDescriptors(Collections.singleton("fls_dls"))).thenReturn(Collections.singleton(flsDlsRole)); when(fileRolesStore.roleDescriptors(Collections.singleton("no_fls_dls"))).thenReturn(Collections.singleton(noFlsDlsRole)); final AtomicReference> effectiveRoleDescriptors = new AtomicReference>(); - final DocumentSubsetBitsetCache documentSubsetBitsetCache = buildBitsetCache(); - CompositeRolesStore compositeRolesStore = new CompositeRolesStore(Settings.EMPTY, fileRolesStore, nativeRolesStore, - reservedRolesStore, mock(NativePrivilegeStore.class), Collections.emptyList(), - new ThreadContext(Settings.EMPTY), licenseState, cache, mock(ApiKeyService.class), documentSubsetBitsetCache, - rds -> effectiveRoleDescriptors.set(rds)); + CompositeRolesStore compositeRolesStore = buildCompositeRolesStore(Settings.EMPTY, fileRolesStore, null, + null, null, licenseState, null, null, rds -> effectiveRoleDescriptors.set(rds)); PlainActionFuture roleFuture = new PlainActionFuture<>(); compositeRolesStore.roles(Collections.singleton("fls"), roleFuture); @@ -267,6 +259,7 @@ public class CompositeRolesStoreTests extends ESTestCase { final NativeRolesStore nativeRolesStore = mock(NativeRolesStore.class); doCallRealMethod().when(nativeRolesStore).accept(any(Set.class), any(ActionListener.class)); when(fileRolesStore.roleDescriptors(anySetOf(String.class))).thenReturn(Collections.emptySet()); + doAnswer((invocationOnMock) -> { ActionListener callback = (ActionListener) invocationOnMock.getArguments()[1]; callback.onResponse(RoleRetrievalResult.success(Collections.emptySet())); @@ -282,12 +275,9 @@ public class CompositeRolesStoreTests extends ESTestCase { }).when(nativePrivilegeStore).getPrivileges(isA(Set.class), isA(Set.class), any(ActionListener.class)); final AtomicReference> effectiveRoleDescriptors = new AtomicReference>(); - final DocumentSubsetBitsetCache documentSubsetBitsetCache = buildBitsetCache(); - final CompositeRolesStore compositeRolesStore = - new CompositeRolesStore(SECURITY_ENABLED_SETTINGS, fileRolesStore, nativeRolesStore, reservedRolesStore, - nativePrivilegeStore, Collections.emptyList(), new ThreadContext(SECURITY_ENABLED_SETTINGS), - new XPackLicenseState(SECURITY_ENABLED_SETTINGS), cache, mock(ApiKeyService.class), - documentSubsetBitsetCache, rds -> effectiveRoleDescriptors.set(rds)); + final CompositeRolesStore compositeRolesStore = buildCompositeRolesStore(SECURITY_ENABLED_SETTINGS, + fileRolesStore, nativeRolesStore, reservedRolesStore, nativePrivilegeStore, null, null, null, + rds -> effectiveRoleDescriptors.set(rds)); verify(fileRolesStore).addListener(any(Consumer.class)); // adds a listener in ctor final String roleName = randomAlphaOfLengthBetween(1, 10); @@ -323,7 +313,7 @@ public class CompositeRolesStoreTests extends ESTestCase { if (getSuperuserRole && numberOfTimesToCall > 0) { // the superuser role was requested so we get the role descriptors again verify(reservedRolesStore, times(2)).accept(anySetOf(String.class), any(ActionListener.class)); - verify(nativePrivilegeStore).getPrivileges(isA(Set.class),isA(Set.class), any(ActionListener.class)); + verify(nativePrivilegeStore).getPrivileges(isA(Set.class), isA(Set.class), any(ActionListener.class)); } verifyNoMoreInteractions(fileRolesStore, reservedRolesStore, nativeRolesStore, nativePrivilegeStore); } @@ -423,9 +413,6 @@ public class CompositeRolesStoreTests extends ESTestCase { verifyNoMoreInteractions(fileRolesStore, reservedRolesStore, nativeRolesStore); } - private DocumentSubsetBitsetCache buildBitsetCache() { - return new DocumentSubsetBitsetCache(Settings.EMPTY, mock(ThreadPool.class)); - } public void testCustomRolesProviders() { final FileRolesStore fileRolesStore = mock(FileRolesStore.class); @@ -900,12 +887,9 @@ public class CompositeRolesStoreTests extends ESTestCase { }).when(nativeRolesStore).getRoleDescriptors(isA(Set.class), any(ActionListener.class)); final ReservedRolesStore reservedRolesStore = spy(new ReservedRolesStore()); - final DocumentSubsetBitsetCache documentSubsetBitsetCache = buildBitsetCache(); - final CompositeRolesStore compositeRolesStore = - new CompositeRolesStore(SECURITY_ENABLED_SETTINGS, fileRolesStore, nativeRolesStore, reservedRolesStore, - mock(NativePrivilegeStore.class), Collections.emptyList(), new ThreadContext(SECURITY_ENABLED_SETTINGS), - new XPackLicenseState(SECURITY_ENABLED_SETTINGS), cache, mock(ApiKeyService.class), documentSubsetBitsetCache, - rds -> {}); + final CompositeRolesStore compositeRolesStore = buildCompositeRolesStore(SECURITY_ENABLED_SETTINGS, fileRolesStore, + nativeRolesStore, reservedRolesStore, mock(NativePrivilegeStore.class), null, mock(ApiKeyService.class), + null, null); verify(fileRolesStore).addListener(any(Consumer.class)); // adds a listener in ctor PlainActionFuture rolesFuture = new PlainActionFuture<>(); @@ -941,11 +925,8 @@ public class CompositeRolesStoreTests extends ESTestCase { }).when(nativeRolesStore).getRoleDescriptors(isA(Set.class), any(ActionListener.class)); final ReservedRolesStore reservedRolesStore = spy(new ReservedRolesStore()); - final DocumentSubsetBitsetCache documentSubsetBitsetCache = buildBitsetCache(); - final CompositeRolesStore compositeRolesStore = - new CompositeRolesStore(settings, fileRolesStore, nativeRolesStore, reservedRolesStore, - mock(NativePrivilegeStore.class), Collections.emptyList(), new ThreadContext(settings), - new XPackLicenseState(settings), cache, mock(ApiKeyService.class), documentSubsetBitsetCache, rds -> {}); + final CompositeRolesStore compositeRolesStore = buildCompositeRolesStore(settings, fileRolesStore, nativeRolesStore, + reservedRolesStore, mock(NativePrivilegeStore.class), null, mock(ApiKeyService.class), null, null); verify(fileRolesStore).addListener(any(Consumer.class)); // adds a listener in ctor PlainActionFuture rolesFuture = new PlainActionFuture<>(); @@ -1128,11 +1109,9 @@ public class CompositeRolesStoreTests extends ESTestCase { final DocumentSubsetBitsetCache documentSubsetBitsetCache = buildBitsetCache(); - final CompositeRolesStore compositeRolesStore = - new CompositeRolesStore(SECURITY_ENABLED_SETTINGS, fileRolesStore, nativeRolesStore, reservedRolesStore, - mock(NativePrivilegeStore.class), Collections.emptyList(), new ThreadContext(SECURITY_ENABLED_SETTINGS), - new XPackLicenseState(SECURITY_ENABLED_SETTINGS), cache, mock(ApiKeyService.class), documentSubsetBitsetCache, rds -> { - }); + final CompositeRolesStore compositeRolesStore = buildCompositeRolesStore( + SECURITY_ENABLED_SETTINGS, fileRolesStore, nativeRolesStore, reservedRolesStore, null, null, mock(ApiKeyService.class), + documentSubsetBitsetCache, null); PlainActionFuture> usageStatsListener = new PlainActionFuture<>(); compositeRolesStore.usageStats(usageStatsListener); @@ -1142,6 +1121,111 @@ public class CompositeRolesStoreTests extends ESTestCase { assertThat(usageStats.get("dls"), is(Collections.singletonMap("bit_set_cache", documentSubsetBitsetCache.usageStats()))); } + public void testLoggingOfDeprecatedRoles() { + List descriptors = new ArrayList<>(); + Function, RoleDescriptor> newRole = metadata -> new RoleDescriptor( + randomAlphaOfLengthBetween(4, 9), generateRandomStringArray(5, 5, false, true), + null, null, null, null, metadata, null); + + RoleDescriptor deprecated1 = newRole.apply(MetadataUtils.getDeprecatedReservedMetadata("some reason")); + RoleDescriptor deprecated2 = newRole.apply(MetadataUtils.getDeprecatedReservedMetadata("a different reason")); + + // Can't use getDeprecatedReservedMetadata because `Map.of` doesn't accept null values, + // so we clone metadata with a real value and then remove that key + final Map nullReasonMetadata = new HashMap<>(deprecated2.getMetadata()); + nullReasonMetadata.remove(MetadataUtils.DEPRECATED_REASON_METADATA_KEY); + assertThat(nullReasonMetadata.keySet(), hasSize(deprecated2.getMetadata().size() -1)); + RoleDescriptor deprecated3 = newRole.apply(nullReasonMetadata); + + descriptors.add(deprecated1); + descriptors.add(deprecated2); + descriptors.add(deprecated3); + + for (int i = randomIntBetween(2, 10); i > 0; i--) { + // the non-deprecated metadata is randomly one of: + // {}, {_deprecated:null}, {_deprecated:false}, + // {_reserved:true}, {_reserved:true,_deprecated:null}, {_reserved:true,_deprecated:false} + Map metadata = randomBoolean() ? Collections.emptyMap() : MetadataUtils.DEFAULT_RESERVED_METADATA; + if (randomBoolean()) { + metadata = new HashMap<>(metadata); + metadata.put(MetadataUtils.DEPRECATED_METADATA_KEY, randomBoolean() ? null : false); + } + descriptors.add(newRole.apply(metadata)); + } + Collections.shuffle(descriptors, random()); + + final CompositeRolesStore compositeRolesStore = + buildCompositeRolesStore(SECURITY_ENABLED_SETTINGS, null, null, null, null, null, null, null, null); + + // Use a LHS so that the random-shufle-order of the list is preserved + compositeRolesStore.logDeprecatedRoles(new LinkedHashSet<>(descriptors)); + + assertWarnings( + "The role [" + deprecated1.getName() + "] is deprecated and will be removed in a future version of Elasticsearch." + + " some reason", + "The role [" + deprecated2.getName() + "] is deprecated and will be removed in a future version of Elasticsearch." + + " a different reason", + "The role [" + deprecated3.getName() + "] is deprecated and will be removed in a future version of Elasticsearch." + + " Please check the documentation" + ); + } + + private CompositeRolesStore buildCompositeRolesStore(Settings settings, + @Nullable FileRolesStore fileRolesStore, + @Nullable NativeRolesStore nativeRolesStore, + @Nullable ReservedRolesStore reservedRolesStore, + @Nullable NativePrivilegeStore privilegeStore, + @Nullable XPackLicenseState licenseState, + @Nullable ApiKeyService apiKeyService, + @Nullable DocumentSubsetBitsetCache documentSubsetBitsetCache, + @Nullable Consumer> roleConsumer) { + if (fileRolesStore == null) { + fileRolesStore = mock(FileRolesStore.class); + doCallRealMethod().when(fileRolesStore).accept(any(Set.class), any(ActionListener.class)); + when(fileRolesStore.roleDescriptors(anySetOf(String.class))).thenReturn(Collections.emptySet()); + } + if (nativeRolesStore == null) { + nativeRolesStore = mock(NativeRolesStore.class); + doCallRealMethod().when(nativeRolesStore).accept(any(Set.class), any(ActionListener.class)); + doAnswer((invocationOnMock) -> { + ActionListener callback = (ActionListener) invocationOnMock.getArguments()[1]; + callback.onResponse(RoleRetrievalResult.failure(new RuntimeException("intentionally failed!"))); + return null; + }).when(nativeRolesStore).getRoleDescriptors(isA(Set.class), any(ActionListener.class)); + } + if (reservedRolesStore == null) { + reservedRolesStore = mock(ReservedRolesStore.class); + doCallRealMethod().when(reservedRolesStore).accept(any(Set.class), any(ActionListener.class)); + } + if (privilegeStore == null) { + privilegeStore = mock(NativePrivilegeStore.class); + doAnswer((invocationOnMock) -> { + ActionListener> callback = null; + callback = (ActionListener>) invocationOnMock.getArguments()[2]; + callback.onResponse(Collections.emptyList()); + return null; + }).when(privilegeStore).getPrivileges(isA(Set.class), isA(Set.class), any(ActionListener.class)); + } + if (licenseState == null) { + licenseState = new XPackLicenseState(settings); + } + if (apiKeyService == null) { + apiKeyService = mock(ApiKeyService.class); + } + if (documentSubsetBitsetCache == null) { + documentSubsetBitsetCache = buildBitsetCache(); + } + if (roleConsumer == null) { + roleConsumer = rds -> { }; + } + return new CompositeRolesStore(settings, fileRolesStore, nativeRolesStore, reservedRolesStore, privilegeStore, + Collections.emptyList(), new ThreadContext(settings), licenseState, cache, apiKeyService, documentSubsetBitsetCache, + roleConsumer); + } + + private DocumentSubsetBitsetCache buildBitsetCache() { + return new DocumentSubsetBitsetCache(Settings.EMPTY, mock(ThreadPool.class)); + } private static class InMemoryRolesProvider implements BiConsumer, ActionListener> { private final Function, RoleRetrievalResult> roleDescriptorsFunc; diff --git a/x-pack/qa/oidc-op-tests/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthIT.java b/x-pack/qa/oidc-op-tests/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthIT.java index 67643af7238..4b0e3afc813 100644 --- a/x-pack/qa/oidc-op-tests/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthIT.java +++ b/x-pack/qa/oidc-op-tests/src/test/java/org/elasticsearch/xpack/security/authc/oidc/OpenIdConnectAuthIT.java @@ -276,7 +276,7 @@ public class OpenIdConnectAuthIT extends ESRestTestCase { final Map map = callAuthenticateApiUsingAccessToken(accessToken); logger.info("Authentication with token Response: " + map); assertThat(map.get("username"), equalTo("alice")); - assertThat((List) map.get("roles"), containsInAnyOrder("kibana_user", "auditor")); + assertThat((List) map.get("roles"), containsInAnyOrder("kibana_admin", "auditor")); assertThat(map.get("metadata"), instanceOf(Map.class)); final Map metadata = (Map) map.get("metadata"); @@ -374,7 +374,7 @@ public class OpenIdConnectAuthIT extends ESRestTestCase { private void setRoleMappings() throws IOException { Request createRoleMappingRequest = new Request("PUT", "/_security/role_mapping/oidc_kibana"); - createRoleMappingRequest.setJsonEntity("{ \"roles\" : [\"kibana_user\"]," + + createRoleMappingRequest.setJsonEntity("{ \"roles\" : [\"kibana_admin\"]," + "\"enabled\": true," + "\"rules\": {" + "\"field\": { \"realm.name\": \"" + REALM_NAME + "\"}" +