diff --git a/rest-api-spec/build.gradle b/rest-api-spec/build.gradle index fe1ca824479e2..50202cc02fb4c 100644 --- a/rest-api-spec/build.gradle +++ b/rest-api-spec/build.gradle @@ -86,7 +86,6 @@ def v7compatibilityNotSupportedTests = { 'search.aggregation/200_top_hits_metric/top_hits aggregation with sequence numbers', // #42809 the use nested path and filter sort throws an exception 'search/310_match_bool_prefix/multi_match multiple fields with cutoff_frequency throws exception', //#42654 cutoff_frequency, common terms are not supported. Throwing an exception - ] } tasks.named("yamlRestCompatTest").configure { diff --git a/x-pack/docs/en/security/authentication/service-accounts.asciidoc b/x-pack/docs/en/security/authentication/service-accounts.asciidoc index 4907ce37d20d6..6964d2e0785d6 100644 --- a/x-pack/docs/en/security/authentication/service-accounts.asciidoc +++ b/x-pack/docs/en/security/authentication/service-accounts.asciidoc @@ -18,12 +18,12 @@ prevents credential sharing between multiple instances of the same external service. Each instance can assume the same identity while using their own distinct service token for authentication. -Service accounts provide flexibility over <> +Service accounts provide flexibility over <> because they: * Do not rely on the <>, and aren't always required to rely on the `.security` index -* Use a role descriptor named after the service account principal instead of +* Use a role descriptor named after the service account principal instead of traditional roles * Support multiple credentials through service account tokens @@ -40,11 +40,15 @@ the format of `/`, where the `namespace` is a top-level grouping of service accounts, and `service` is the name of the service and must be unique within its namespace. -Service accounts are predefined in code. Currently, only one service account is available: +Service accounts are predefined in code. The following service accounts are +available: `elastic/fleet-server`:: The service account used by the {fleet} server to communicate with {es}. +`elastic/kibana`:: The service account used by {kib} to communicate with +{es}. + // tag::service-accounts-usage[] IMPORTANT: Do not attempt to use service accounts for authenticating individual users. Service accounts can only be authenticated with service tokens, which are @@ -83,11 +87,11 @@ the bearer token in the HTTP response Both of these methods create a service token with a guaranteed secret string length of `22`. The minimal, acceptable length of a secret string for a service -token is `10`. If the secret string doesn't meet this minimal length, +token is `10`. If the secret string doesn't meet this minimal length, authentication with {es} will fail without even checking the value of the service token. -Service tokens never expire. You must actively +Service tokens never expire. You must actively <> them if they are no longer needed. [discrete] diff --git a/x-pack/plugin/build.gradle b/x-pack/plugin/build.gradle index cdc08b15f1333..af0f1372d75a6 100644 --- a/x-pack/plugin/build.gradle +++ b/x-pack/plugin/build.gradle @@ -153,7 +153,8 @@ def v7compatibilityNotSupportedTests = { // a type field was added to cat.ml_trained_models #73660, this is a backwards compatible change. // still this is a cat api, and we don't support them with rest api compatibility. (the test would be very hard to transform too) - 'ml/trained_model_cat_apis/Test cat trained models' + 'ml/trained_model_cat_apis/Test cat trained models', + 'service_accounts/10_basic/Test get service accounts', //#76449, will remove upon backport ] } 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 bde1ef8978cf7..367a0afb599cb 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 @@ -21,7 +21,7 @@ import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivileges.ManageApplicationPrivileges; import org.elasticsearch.xpack.core.security.support.MetadataUtils; -import org.elasticsearch.xpack.core.security.user.KibanaUser; +import org.elasticsearch.xpack.core.security.user.KibanaSystemUser; import org.elasticsearch.xpack.core.security.user.UsernamesField; import org.elasticsearch.xpack.core.transform.transforms.persistence.TransformInternalIndexConstants; import org.elasticsearch.xpack.core.watcher.execution.TriggeredWatchStoreField; @@ -125,78 +125,7 @@ private static Map initializeReservedRoles() { null, null, MetadataUtils.getDeprecatedReservedMetadata("Please use Kibana feature privileges instead"), null)) - .put(KibanaUser.ROLE_NAME, new RoleDescriptor(KibanaUser.ROLE_NAME, - new String[] { - "monitor", "manage_index_templates", MonitoringBulkAction.NAME, "manage_saml", "manage_token", "manage_oidc", - InvalidateApiKeyAction.NAME, "grant_api_key", - GetBuiltinPrivilegesAction.NAME, "delegate_pki", GetLifecycleAction.NAME, PutLifecycleAction.NAME, - // To facilitate ML UI functionality being controlled using Kibana security privileges - "manage_ml", - // The symbolic constant for this one is in SecurityActionMapper, so not accessible from X-Pack core - "cluster:admin/analyze", - // To facilitate using the file uploader functionality - "monitor_text_structure", - // To cancel tasks and delete async searches - "cancel_task" - }, - new RoleDescriptor.IndicesPrivileges[] { - RoleDescriptor.IndicesPrivileges.builder() - .indices(".kibana*", ".reporting-*").privileges("all").build(), - RoleDescriptor.IndicesPrivileges.builder() - .indices(".monitoring-*").privileges("read", "read_cross_cluster").build(), - RoleDescriptor.IndicesPrivileges.builder() - .indices(".management-beats").privileges("create_index", "read", "write").build(), - // To facilitate ML UI functionality being controlled using Kibana security privileges - RoleDescriptor.IndicesPrivileges.builder() - .indices(".ml-anomalies*", ".ml-stats-*") - .privileges("read").build(), - RoleDescriptor.IndicesPrivileges.builder().indices(".ml-annotations*", ".ml-notifications*") - .privileges("read", "write").build(), - // APM agent configuration - RoleDescriptor.IndicesPrivileges.builder() - .indices(".apm-agent-configuration").privileges("all").build(), - // APM custom link index creation - RoleDescriptor.IndicesPrivileges.builder() - .indices(".apm-custom-link").privileges("all").build(), - // APM telemetry queries APM indices in kibana task runner - RoleDescriptor.IndicesPrivileges.builder() - .indices("apm-*") - .privileges("read", "read_cross_cluster").build(), - // Data telemetry reads mappings, metadata and stats of indices - RoleDescriptor.IndicesPrivileges.builder() - .indices("*") - .privileges("view_index_metadata", "monitor").build(), - // Endpoint diagnostic information. Kibana reads from these indices to send telemetry - RoleDescriptor.IndicesPrivileges.builder() - .indices(".logs-endpoint.diagnostic.collection-*") - .privileges("read").build(), - // Fleet Server indices. Kibana create this indice before Fleet Server use them. - // Fleet Server indices. Kibana read and write to this indice to manage Elastic Agents - RoleDescriptor.IndicesPrivileges.builder() - .indices(".fleet*") - .privileges("all").build(), - // Legacy "Alerts as data" index. Kibana user will create this index. - // Kibana user will read / write to these indices - RoleDescriptor.IndicesPrivileges.builder() - .indices(ReservedRolesStore.LEGACY_ALERTS_INDEX) - .privileges("all").build(), - // "Alerts as data" index. Kibana user will create this index. - // Kibana user will read / write to these indices - RoleDescriptor.IndicesPrivileges.builder() - .indices(ReservedRolesStore.ALERTS_INDEX) - .privileges("all").build(), - // Endpoint / Fleet policy responses. Kibana requires read access to send telemetry - RoleDescriptor.IndicesPrivileges.builder() - .indices("metrics-endpoint.policy-*") - .privileges("read").build(), - // Endpoint metrics. Kibana requires read access to send telemetry - RoleDescriptor.IndicesPrivileges.builder() - .indices("metrics-endpoint.metrics-*") - .privileges("read").build() - }, - null, - new ConfigurableClusterPrivilege[] { new ManageApplicationPrivileges(Collections.singleton("kibana-*")) }, - null, MetadataUtils.DEFAULT_RESERVED_METADATA, null)) + .put(KibanaSystemUser.ROLE_NAME, kibanaSystemRoleDescriptor(KibanaSystemUser.ROLE_NAME)) .put("logstash_system", new RoleDescriptor("logstash_system", new String[] { "monitor", MonitoringBulkAction.NAME}, null, null, MetadataUtils.DEFAULT_RESERVED_METADATA)) .put("beats_admin", new RoleDescriptor("beats_admin", @@ -434,6 +363,81 @@ private static RoleDescriptor kibanaAdminUser(String name, Map m null, null, metadata, null); } + public static RoleDescriptor kibanaSystemRoleDescriptor(String name) { + return new RoleDescriptor(name, + new String[] { + "monitor", "manage_index_templates", MonitoringBulkAction.NAME, "manage_saml", "manage_token", "manage_oidc", + InvalidateApiKeyAction.NAME, "grant_api_key", + GetBuiltinPrivilegesAction.NAME, "delegate_pki", GetLifecycleAction.NAME, PutLifecycleAction.NAME, + // To facilitate ML UI functionality being controlled using Kibana security privileges + "manage_ml", + // The symbolic constant for this one is in SecurityActionMapper, so not accessible from X-Pack core + "cluster:admin/analyze", + // To facilitate using the file uploader functionality + "monitor_text_structure", + // To cancel tasks and delete async searches + "cancel_task" + }, + new RoleDescriptor.IndicesPrivileges[] { + RoleDescriptor.IndicesPrivileges.builder() + .indices(".kibana*", ".reporting-*").privileges("all").build(), + RoleDescriptor.IndicesPrivileges.builder() + .indices(".monitoring-*").privileges("read", "read_cross_cluster").build(), + RoleDescriptor.IndicesPrivileges.builder() + .indices(".management-beats").privileges("create_index", "read", "write").build(), + // To facilitate ML UI functionality being controlled using Kibana security privileges + RoleDescriptor.IndicesPrivileges.builder() + .indices(".ml-anomalies*", ".ml-stats-*") + .privileges("read").build(), + RoleDescriptor.IndicesPrivileges.builder().indices(".ml-annotations*", ".ml-notifications*") + .privileges("read", "write").build(), + // APM agent configuration + RoleDescriptor.IndicesPrivileges.builder() + .indices(".apm-agent-configuration").privileges("all").build(), + // APM custom link index creation + RoleDescriptor.IndicesPrivileges.builder() + .indices(".apm-custom-link").privileges("all").build(), + // APM telemetry queries APM indices in kibana task runner + RoleDescriptor.IndicesPrivileges.builder() + .indices("apm-*") + .privileges("read", "read_cross_cluster").build(), + // Data telemetry reads mappings, metadata and stats of indices + RoleDescriptor.IndicesPrivileges.builder() + .indices("*") + .privileges("view_index_metadata", "monitor").build(), + // Endpoint diagnostic information. Kibana reads from these indices to send telemetry + RoleDescriptor.IndicesPrivileges.builder() + .indices(".logs-endpoint.diagnostic.collection-*") + .privileges("read").build(), + // Fleet Server indices. Kibana create this indice before Fleet Server use them. + // Fleet Server indices. Kibana read and write to this indice to manage Elastic Agents + RoleDescriptor.IndicesPrivileges.builder() + .indices(".fleet*") + .privileges("all").build(), + // Legacy "Alerts as data" index. Kibana user will create this index. + // Kibana user will read / write to these indices + RoleDescriptor.IndicesPrivileges.builder() + .indices(ReservedRolesStore.LEGACY_ALERTS_INDEX) + .privileges("all").build(), + // "Alerts as data" index. Kibana user will create this index. + // Kibana user will read / write to these indices + RoleDescriptor.IndicesPrivileges.builder() + .indices(ReservedRolesStore.ALERTS_INDEX) + .privileges("all").build(), + // Endpoint / Fleet policy responses. Kibana requires read access to send telemetry + RoleDescriptor.IndicesPrivileges.builder() + .indices("metrics-endpoint.policy-*") + .privileges("read").build(), + // Endpoint metrics. Kibana requires read access to send telemetry + RoleDescriptor.IndicesPrivileges.builder() + .indices("metrics-endpoint.metrics-*") + .privileges("read").build() + }, + null, + new ConfigurableClusterPrivilege[] { new ManageApplicationPrivileges(Collections.singleton("kibana-*")) }, + null, MetadataUtils.DEFAULT_RESERVED_METADATA, null); + } + public static boolean isReserved(String role) { return RESERVED_ROLES.containsKey(role) || UsernamesField.SYSTEM_ROLE.equals(role) || UsernamesField.XPACK_ROLE.equals(role) || UsernamesField.ASYNC_SEARCH_ROLE.equals(role); diff --git a/x-pack/plugin/security/qa/service-account/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountIT.java b/x-pack/plugin/security/qa/service-account/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountIT.java index d73b05ba1488c..e2645ec3226cb 100644 --- a/x-pack/plugin/security/qa/service-account/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountIT.java +++ b/x-pack/plugin/security/qa/service-account/src/javaRestTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountIT.java @@ -12,7 +12,11 @@ import org.elasticsearch.client.RequestOptions; import org.elasticsearch.client.Response; import org.elasticsearch.client.ResponseException; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.core.PathUtils; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; @@ -20,6 +24,8 @@ import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.test.rest.ESRestTestCase; +import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; +import org.elasticsearch.xpack.core.security.user.KibanaSystemUser; import org.junit.BeforeClass; import java.io.FileNotFoundException; @@ -165,6 +171,18 @@ public void testGetServiceAccount() throws IOException { assertServiceAccountRoleDescriptor(getServiceAccountResponse3, "elastic/fleet-server", ELASTIC_FLEET_SERVER_ROLE_DESCRIPTOR); + final Request getServiceAccountRequestKibana = new Request("GET", "_security/service/elastic/kibana"); + final Response getServiceAccountResponseKibana = client().performRequest(getServiceAccountRequestKibana); + assertOK(getServiceAccountResponseKibana); + assertServiceAccountRoleDescriptor( + getServiceAccountResponseKibana, + "elastic/kibana", + Strings.toString( + ReservedRolesStore.kibanaSystemRoleDescriptor(KibanaSystemUser.ROLE_NAME) + .toXContent(JsonXContent.contentBuilder(), ToXContent.EMPTY_PARAMS) + ) + ); + final String requestPath = "_security/service/" + randomFrom("foo", "elastic/foo", "foo/bar"); final Request getServiceAccountRequest4 = new Request("GET", requestPath); final Response getServiceAccountResponse4 = client().performRequest(getServiceAccountRequest4); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccounts.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccounts.java index 75d0ad260c37a..96ce558cae9ea 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccounts.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccounts.java @@ -9,6 +9,7 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; import org.elasticsearch.xpack.core.security.user.User; import java.util.List; @@ -43,8 +44,10 @@ final class ElasticServiceAccounts { null, null )); + private static final ServiceAccount KIBANA_SYSTEM_ACCOUNT = + new ElasticServiceAccount("kibana", ReservedRolesStore.kibanaSystemRoleDescriptor(NAMESPACE + "/kibana")); - static final Map ACCOUNTS = List.of(FLEET_ACCOUNT).stream() + static final Map ACCOUNTS = List.of(FLEET_ACCOUNT, KIBANA_SYSTEM_ACCOUNT).stream() .collect(Collectors.toMap(a -> a.id().asPrincipal(), Function.identity()));; private ElasticServiceAccounts() {} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountActionTests.java index 77af6fe2ef889..0204932509e0e 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountActionTests.java @@ -14,11 +14,15 @@ import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountRequest; import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountResponse; +import org.elasticsearch.xpack.core.security.action.service.ServiceAccountInfo; import org.elasticsearch.xpack.security.authc.support.HttpTlsRuntimeCheck; import org.junit.Before; +import java.util.Arrays; import java.util.Collections; +import java.util.stream.Collectors; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; import static org.mockito.Matchers.any; import static org.mockito.Mockito.doAnswer; @@ -43,23 +47,33 @@ public void init() { } public void testDoExecute() { - final GetServiceAccountRequest request1 = randomFrom( - new GetServiceAccountRequest(null, null), - new GetServiceAccountRequest("elastic", null), - new GetServiceAccountRequest("elastic", "fleet-server")); + final GetServiceAccountRequest request1 = randomFrom(new GetServiceAccountRequest(null, null), + new GetServiceAccountRequest("elastic", null)); final PlainActionFuture future1 = new PlainActionFuture<>(); transportGetServiceAccountAction.doExecute(mock(Task.class), request1, future1); final GetServiceAccountResponse getServiceAccountResponse1 = future1.actionGet(); - assertThat(getServiceAccountResponse1.getServiceAccountInfos().length, equalTo(1)); - assertThat(getServiceAccountResponse1.getServiceAccountInfos()[0].getPrincipal(), equalTo("elastic/fleet-server")); + assertThat(getServiceAccountResponse1.getServiceAccountInfos().length, equalTo(2)); + assertThat( + Arrays.stream(getServiceAccountResponse1.getServiceAccountInfos()) + .map(ServiceAccountInfo::getPrincipal) + .collect(Collectors.toList()), + containsInAnyOrder("elastic/fleet-server", "elastic/kibana") + ); - final GetServiceAccountRequest request2 = randomFrom( - new GetServiceAccountRequest("foo", null), - new GetServiceAccountRequest("elastic", "foo"), - new GetServiceAccountRequest("foo", "bar")); + final GetServiceAccountRequest request2 = new GetServiceAccountRequest("elastic", "fleet-server"); final PlainActionFuture future2 = new PlainActionFuture<>(); transportGetServiceAccountAction.doExecute(mock(Task.class), request2, future2); final GetServiceAccountResponse getServiceAccountResponse2 = future2.actionGet(); - assertThat(getServiceAccountResponse2.getServiceAccountInfos().length, equalTo(0)); + assertThat(getServiceAccountResponse2.getServiceAccountInfos().length, equalTo(1)); + assertThat(getServiceAccountResponse2.getServiceAccountInfos()[0].getPrincipal(), equalTo("elastic/fleet-server")); + + final GetServiceAccountRequest request3 = randomFrom( + new GetServiceAccountRequest("foo", null), + new GetServiceAccountRequest("elastic", "foo"), + new GetServiceAccountRequest("foo", "bar")); + final PlainActionFuture future3 = new PlainActionFuture<>(); + transportGetServiceAccountAction.doExecute(mock(Task.class), request3, future3); + final GetServiceAccountResponse getServiceAccountResponse3 = future3.actionGet(); + assertThat(getServiceAccountResponse3.getServiceAccountInfos().length, equalTo(0)); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccountsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccountsTests.java index bf40fd4e351ab..b81d4fc507f10 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccountsTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccountsTests.java @@ -20,9 +20,73 @@ import org.elasticsearch.action.index.IndexAction; import org.elasticsearch.action.search.MultiSearchAction; import org.elasticsearch.action.search.SearchAction; +import org.elasticsearch.action.update.UpdateAction; import org.elasticsearch.cluster.metadata.IndexAbstraction; import org.elasticsearch.common.Strings; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.xpack.core.ml.action.CloseJobAction; +import org.elasticsearch.xpack.core.ml.action.DeleteCalendarAction; +import org.elasticsearch.xpack.core.ml.action.DeleteCalendarEventAction; +import org.elasticsearch.xpack.core.ml.action.DeleteDatafeedAction; +import org.elasticsearch.xpack.core.ml.action.DeleteExpiredDataAction; +import org.elasticsearch.xpack.core.ml.action.DeleteFilterAction; +import org.elasticsearch.xpack.core.ml.action.DeleteForecastAction; +import org.elasticsearch.xpack.core.ml.action.DeleteJobAction; +import org.elasticsearch.xpack.core.ml.action.DeleteModelSnapshotAction; +import org.elasticsearch.xpack.core.ml.action.DeleteTrainedModelAction; +import org.elasticsearch.xpack.core.ml.action.EstimateModelMemoryAction; +import org.elasticsearch.xpack.core.ml.action.EvaluateDataFrameAction; +import org.elasticsearch.xpack.core.ml.action.ExplainDataFrameAnalyticsAction; +import org.elasticsearch.xpack.core.ml.action.FinalizeJobExecutionAction; +import org.elasticsearch.xpack.core.ml.action.FlushJobAction; +import org.elasticsearch.xpack.core.ml.action.ForecastJobAction; +import org.elasticsearch.xpack.core.ml.action.GetBucketsAction; +import org.elasticsearch.xpack.core.ml.action.GetCalendarEventsAction; +import org.elasticsearch.xpack.core.ml.action.GetCalendarsAction; +import org.elasticsearch.xpack.core.ml.action.GetCategoriesAction; +import org.elasticsearch.xpack.core.ml.action.GetDataFrameAnalyticsAction; +import org.elasticsearch.xpack.core.ml.action.GetDataFrameAnalyticsStatsAction; +import org.elasticsearch.xpack.core.ml.action.GetDatafeedsAction; +import org.elasticsearch.xpack.core.ml.action.GetDatafeedsStatsAction; +import org.elasticsearch.xpack.core.ml.action.GetFiltersAction; +import org.elasticsearch.xpack.core.ml.action.GetInfluencersAction; +import org.elasticsearch.xpack.core.ml.action.GetJobsAction; +import org.elasticsearch.xpack.core.ml.action.GetJobsStatsAction; +import org.elasticsearch.xpack.core.ml.action.GetModelSnapshotsAction; +import org.elasticsearch.xpack.core.ml.action.GetOverallBucketsAction; +import org.elasticsearch.xpack.core.ml.action.GetRecordsAction; +import org.elasticsearch.xpack.core.ml.action.GetTrainedModelsAction; +import org.elasticsearch.xpack.core.ml.action.GetTrainedModelsStatsAction; +import org.elasticsearch.xpack.core.ml.action.InternalInferModelAction; +import org.elasticsearch.xpack.core.ml.action.IsolateDatafeedAction; +import org.elasticsearch.xpack.core.ml.action.KillProcessAction; +import org.elasticsearch.xpack.core.ml.action.MlInfoAction; +import org.elasticsearch.xpack.core.ml.action.OpenJobAction; +import org.elasticsearch.xpack.core.ml.action.PersistJobAction; +import org.elasticsearch.xpack.core.ml.action.PostCalendarEventsAction; +import org.elasticsearch.xpack.core.ml.action.PostDataAction; +import org.elasticsearch.xpack.core.ml.action.PreviewDatafeedAction; +import org.elasticsearch.xpack.core.ml.action.PutCalendarAction; +import org.elasticsearch.xpack.core.ml.action.PutDataFrameAnalyticsAction; +import org.elasticsearch.xpack.core.ml.action.PutDatafeedAction; +import org.elasticsearch.xpack.core.ml.action.PutFilterAction; +import org.elasticsearch.xpack.core.ml.action.PutJobAction; +import org.elasticsearch.xpack.core.ml.action.PutTrainedModelAction; +import org.elasticsearch.xpack.core.ml.action.RevertModelSnapshotAction; +import org.elasticsearch.xpack.core.ml.action.SetUpgradeModeAction; +import org.elasticsearch.xpack.core.ml.action.StartDataFrameAnalyticsAction; +import org.elasticsearch.xpack.core.ml.action.StartDatafeedAction; +import org.elasticsearch.xpack.core.ml.action.StopDataFrameAnalyticsAction; +import org.elasticsearch.xpack.core.ml.action.StopDatafeedAction; +import org.elasticsearch.xpack.core.ml.action.UpdateCalendarJobAction; +import org.elasticsearch.xpack.core.ml.action.UpdateDatafeedAction; +import org.elasticsearch.xpack.core.ml.action.UpdateFilterAction; +import org.elasticsearch.xpack.core.ml.action.UpdateJobAction; +import org.elasticsearch.xpack.core.ml.action.UpdateModelSnapshotAction; +import org.elasticsearch.xpack.core.ml.action.UpdateProcessAction; +import org.elasticsearch.xpack.core.ml.action.ValidateDetectorAction; +import org.elasticsearch.xpack.core.ml.action.ValidateJobConfigAction; import org.elasticsearch.xpack.core.security.action.CreateApiKeyAction; import org.elasticsearch.xpack.core.security.action.CreateApiKeyRequest; import org.elasticsearch.xpack.core.security.action.GetApiKeyAction; @@ -32,9 +96,12 @@ import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.permission.Role; +import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; +import org.elasticsearch.xpack.core.security.user.KibanaSystemUser; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.service.ElasticServiceAccounts.ElasticServiceAccount; +import java.util.Collection; import java.util.List; import java.util.Map; @@ -46,6 +113,19 @@ public class ElasticServiceAccountsTests extends ESTestCase { + public void testKibanaSystemPrivileges() { + final RoleDescriptor serviceAccountRoleDescriptor = ElasticServiceAccounts.ACCOUNTS.get("elastic/kibana").roleDescriptor(); + final RoleDescriptor reservedRolesStoreRoleDescriptor = ReservedRolesStore.kibanaSystemRoleDescriptor(KibanaSystemUser.ROLE_NAME); + assertThat(serviceAccountRoleDescriptor.getClusterPrivileges(), equalTo(reservedRolesStoreRoleDescriptor.getClusterPrivileges())); + assertThat(serviceAccountRoleDescriptor.getApplicationPrivileges(), + equalTo(reservedRolesStoreRoleDescriptor.getApplicationPrivileges())); + assertThat(serviceAccountRoleDescriptor.getIndicesPrivileges(), equalTo(reservedRolesStoreRoleDescriptor.getIndicesPrivileges())); + assertThat(serviceAccountRoleDescriptor.getConditionalClusterPrivileges(), + equalTo(reservedRolesStoreRoleDescriptor.getConditionalClusterPrivileges())); + assertThat(serviceAccountRoleDescriptor.getRunAs(), equalTo(reservedRolesStoreRoleDescriptor.getRunAs())); + assertThat(serviceAccountRoleDescriptor.getMetadata(), equalTo(reservedRolesStoreRoleDescriptor.getMetadata())); + } + public void testElasticFleetPrivileges() { final Role role = Role.builder(ElasticServiceAccounts.ACCOUNTS.get("elastic/fleet-server").roleDescriptor(), null).build(); final Authentication authentication = mock(Authentication.class); @@ -130,4 +210,90 @@ private IndexAbstraction mockIndexAbstraction(String name) { IndexAbstraction.Type.ALIAS, IndexAbstraction.Type.DATA_STREAM)); return mock; } + + private void assertNoAccessAllowed(Role role, Collection indices) { + for (String index : indices) { + assertNoAccessAllowed(role, index); + } + } + + private void assertNoAccessAllowed(Role role, String index) { + assertThat(role.indices().allowedIndicesMatcher(DeleteIndexAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(CreateIndexAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(UpdateSettingsAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(SearchAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(GetAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(IndexAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(UpdateAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(DeleteAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(BulkAction.NAME).test(mockIndexAbstraction(index)), is(false)); + } + + private void assertRoleHasManageMl(Role role) { + final TransportRequest request = mock(TransportRequest.class); + final Authentication authentication = mock(Authentication.class); + + assertThat(role.cluster().check(CloseJobAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(DeleteCalendarAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(DeleteCalendarEventAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(DeleteDatafeedAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(DeleteExpiredDataAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(DeleteFilterAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(DeleteForecastAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(DeleteJobAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(DeleteModelSnapshotAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(DeleteTrainedModelAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(EstimateModelMemoryAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(EvaluateDataFrameAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(ExplainDataFrameAnalyticsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(FinalizeJobExecutionAction.NAME, request, authentication), is(false)); // internal use only + assertThat(role.cluster().check(FlushJobAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(ForecastJobAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetBucketsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetCalendarEventsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetCalendarsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetCategoriesAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetDatafeedsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetDatafeedsStatsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetDataFrameAnalyticsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetDataFrameAnalyticsStatsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetFiltersAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetInfluencersAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetJobsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetJobsStatsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetModelSnapshotsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetOverallBucketsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetRecordsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetTrainedModelsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetTrainedModelsStatsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(InternalInferModelAction.NAME, request, authentication), is(false)); // internal use only + assertThat(role.cluster().check(IsolateDatafeedAction.NAME, request, authentication), is(false)); // internal use only + assertThat(role.cluster().check(KillProcessAction.NAME, request, authentication), is(false)); // internal use only + assertThat(role.cluster().check(MlInfoAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(OpenJobAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(PersistJobAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(PostCalendarEventsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(PostDataAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(PreviewDatafeedAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(PutCalendarAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(PutDatafeedAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(PutDataFrameAnalyticsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(PutFilterAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(PutJobAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(PutTrainedModelAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(RevertModelSnapshotAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(SetUpgradeModeAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(StartDatafeedAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(StartDataFrameAnalyticsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(StopDatafeedAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(StopDataFrameAnalyticsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(UpdateCalendarJobAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(UpdateDatafeedAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(UpdateFilterAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(UpdateJobAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(UpdateModelSnapshotAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(UpdateProcessAction.NAME, request, authentication), is(false)); // internal use only + assertThat(role.cluster().check(ValidateDetectorAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(ValidateJobConfigAction.NAME, request, authentication), is(true)); + } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java index d2c5e25be76ee..b737726589444 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java @@ -59,12 +59,12 @@ import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import java.util.stream.IntStream; import java.util.stream.Stream; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; @@ -121,7 +121,8 @@ public void stopThreadPool() { } public void testGetServiceAccountPrincipals() { - assertThat(ServiceAccountService.getServiceAccountPrincipals(), equalTo(Set.of("elastic/fleet-server"))); + assertThat(ServiceAccountService.getServiceAccountPrincipals(), + containsInAnyOrder("elastic/fleet-server", "elastic/kibana")); } public void testTryParseToken() throws IOException, IllegalAccessException { diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/service_accounts/10_basic.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/service_accounts/10_basic.yml index 31dc539e11bfe..45e89a79a87e1 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/service_accounts/10_basic.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/service_accounts/10_basic.yml @@ -10,21 +10,30 @@ teardown: security.delete_service_token: namespace: elastic service: fleet-server - name: api-token-1 + name: api-token-fleet + ignore: 404 + + - do: + security.delete_service_token: + namespace: elastic + service: kibana + name: api-token-kibana ignore: 404 --- "Test get service accounts": - do: security.get_service_accounts: {} - - length: { '': 1 } + - length: { '': 2 } - is_true: "elastic/fleet-server" + - is_true: "elastic/kibana" - do: security.get_service_accounts: namespace: elastic - - length: { '': 1 } + - length: { '': 2 } - is_true: "elastic/fleet-server" + - is_true: "elastic/kibana" - do: security.get_service_accounts: @@ -41,26 +50,36 @@ teardown: security.create_service_token: namespace: elastic service: fleet-server - name: api-token-1 + name: api-token-fleet - is_true: created - - match: { "token.name": "api-token-1" } - - set: { "token.value": service_token } + - match: { "token.name": "api-token-fleet" } + - set: { "token.value": service_token_fleet } + + - do: + security.create_service_token: + namespace: elastic + service: kibana + name: api-token-kibana + + - is_true: created + - match: { "token.name": "api-token-kibana" } + - set: { "token.value": service_token_kibana } - do: headers: - Authorization: Bearer ${service_token} + Authorization: Bearer ${service_token_fleet} security.authenticate: {} - match: { username: "elastic/fleet-server" } - match: { roles: [] } - match: { full_name: "Service account - elastic/fleet-server" } - - match: { "token.name": "api-token-1" } + - match: { "token.name": "api-token-fleet" } - do: catch: forbidden headers: - Authorization: Bearer ${service_token} + Authorization: Bearer ${service_token_fleet} security.delete_user: username: foo @@ -68,6 +87,16 @@ teardown: - match: error.reason: "action [cluster:admin/xpack/security/user/delete] is unauthorized for user [elastic/fleet-server], this action is granted by the cluster privileges [manage_security,all]" + - do: + headers: + Authorization: Bearer ${service_token_kibana} + security.authenticate: {} + + - match: { username: "elastic/kibana" } + - match: { roles: [] } + - match: { full_name: "Service account - elastic/kibana" } + - match: { "token.name": "api-token-kibana" } + - do: security.get_service_credentials: namespace: elastic @@ -75,7 +104,7 @@ teardown: - match: { "service_account": "elastic/fleet-server" } - match: { "count": 2 } - - match: { "tokens": { "api-token-1": {} } } + - match: { "tokens": { "api-token-fleet": {} } } - match: { "nodes_credentials._nodes.failed": 0 } - is_true: nodes_credentials.file_tokens.token1 - is_true: nodes_credentials.file_tokens.token1.nodes @@ -85,7 +114,15 @@ teardown: security.clear_cached_service_tokens: namespace: elastic service: fleet-server - name: api-token-1 + name: api-token-fleet + + - match: { _nodes.failed: 0 } + + - do: + security.clear_cached_service_tokens: + namespace: elastic + service: kibana + name: api-token-kibana - match: { _nodes.failed: 0 } @@ -93,9 +130,16 @@ teardown: security.clear_cached_service_tokens: namespace: elastic service: fleet-server - name: [ "api-token-1", "does-not-exist" ] + name: [ "api-token-fleet", "does-not-exist" ] - match: { _nodes.failed: 0 } + - do: + security.clear_cached_service_tokens: + namespace: elastic + service: kibana + name: [ "api-token-kibana", "does-not-exist-either" ] + + - match: { _nodes.failed: 0 }