From 17e3971f57843cca86004326f333d911f01c0099 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Tue, 6 Apr 2021 20:09:48 +1000 Subject: [PATCH] Service Accounts - Get service account API (#71315) This PR adds a new API endpoint to retrieve service accounts. Depends on the request parameters, it returns either all accounts, accounts belong to a namespace, a specific account, or an empty map if nothing is found. --- .../service/GetServiceAccountAction.java | 20 ++++ .../service/GetServiceAccountRequest.java | 71 ++++++++++++++ .../service/GetServiceAccountResponse.java | 71 ++++++++++++++ .../action/service/ServiceAccountInfo.java | 77 +++++++++++++++ .../GetServiceAccountRequestTests.java | 40 ++++++++ .../GetServiceAccountResponseTests.java | 98 +++++++++++++++++++ .../xpack/security/operator/Constants.java | 1 + .../authc/service/ServiceAccountIT.java | 65 ++++++++++++ .../xpack/security/Security.java | 7 +- .../TransportGetServiceAccountAction.java | 56 +++++++++++ .../authc/service/ServiceAccountService.java | 4 + .../service/RestGetServiceAccountAction.java | 52 ++++++++++ ...TransportGetServiceAccountActionTests.java | 65 ++++++++++++ 13 files changed, 626 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountAction.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountRequest.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountResponse.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/ServiceAccountInfo.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountRequestTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountResponseTests.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountAction.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/service/RestGetServiceAccountAction.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountActionTests.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountAction.java new file mode 100644 index 0000000000000..c816d37411d1c --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountAction.java @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.core.security.action.service; + +import org.elasticsearch.action.ActionType; + +public class GetServiceAccountAction extends ActionType { + + public static final String NAME = "cluster:admin/xpack/security/service_account/get"; + public static final GetServiceAccountAction INSTANCE = new GetServiceAccountAction(); + + public GetServiceAccountAction() { + super(NAME, GetServiceAccountResponse::new); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountRequest.java new file mode 100644 index 0000000000000..782fa27780f17 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountRequest.java @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.core.security.action.service; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; +import java.util.Objects; + +public class GetServiceAccountRequest extends ActionRequest { + + @Nullable + private final String namespace; + @Nullable + private final String serviceName; + + public GetServiceAccountRequest(@Nullable String namespace, @Nullable String serviceName) { + this.namespace = namespace; + this.serviceName = serviceName; + } + + public GetServiceAccountRequest(StreamInput in) throws IOException { + super(in); + this.namespace = in.readOptionalString(); + this.serviceName = in.readOptionalString(); + } + + public String getNamespace() { + return namespace; + } + + public String getServiceName() { + return serviceName; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + GetServiceAccountRequest that = (GetServiceAccountRequest) o; + return Objects.equals(namespace, that.namespace) && Objects.equals(serviceName, that.serviceName); + } + + @Override + public int hashCode() { + return Objects.hash(namespace, serviceName); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeOptionalString(namespace); + out.writeOptionalString(serviceName); + } + + @Override + public ActionRequestValidationException validate() { + return null; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountResponse.java new file mode 100644 index 0000000000000..1a9b2c112183c --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountResponse.java @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.core.security.action.service; + +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Objects; + +public class GetServiceAccountResponse extends ActionResponse implements ToXContentObject { + + private final ServiceAccountInfo[] serviceAccountInfos; + + public GetServiceAccountResponse(ServiceAccountInfo[] serviceAccountInfos) { + this.serviceAccountInfos = Objects.requireNonNull(serviceAccountInfos); + } + + public GetServiceAccountResponse(StreamInput in) throws IOException { + super(in); + this.serviceAccountInfos = in.readArray(ServiceAccountInfo::new, ServiceAccountInfo[]::new); + } + + public ServiceAccountInfo[] getServiceAccountInfos() { + return serviceAccountInfos; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeArray(serviceAccountInfos); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + for (ServiceAccountInfo info : serviceAccountInfos) { + info.toXContent(builder, params); + } + builder.endObject(); + return builder; + } + + @Override + public String toString() { + return "GetServiceAccountResponse{" + "serviceAccountInfos=" + Arrays.toString(serviceAccountInfos) + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + GetServiceAccountResponse that = (GetServiceAccountResponse) o; + return Arrays.equals(serviceAccountInfos, that.serviceAccountInfos); + } + + @Override + public int hashCode() { + return Arrays.hashCode(serviceAccountInfos); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/ServiceAccountInfo.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/ServiceAccountInfo.java new file mode 100644 index 0000000000000..a886088978fc7 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/ServiceAccountInfo.java @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.core.security.action.service; + +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; + +import java.io.IOException; +import java.util.Objects; + +public class ServiceAccountInfo implements Writeable, ToXContent { + + private final String principal; + private final RoleDescriptor roleDescriptor; + + public ServiceAccountInfo(String principal, RoleDescriptor roleDescriptor) { + this.principal = Objects.requireNonNull(principal, "service account principal cannot be null"); + this.roleDescriptor = Objects.requireNonNull(roleDescriptor, "service account descriptor cannot be null"); + } + + public ServiceAccountInfo(StreamInput in) throws IOException { + this.principal = in.readString(); + this.roleDescriptor = new RoleDescriptor(in); + } + + public String getPrincipal() { + return principal; + } + + public RoleDescriptor getRoleDescriptor() { + return roleDescriptor; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(principal); + roleDescriptor.writeTo(out); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(principal); + builder.field("role_descriptor"); + roleDescriptor.toXContent(builder, params); + builder.endObject(); + return builder; + } + + @Override + public String toString() { + return "ServiceAccountInfo{" + "principal='" + principal + '\'' + ", roleDescriptor=" + roleDescriptor + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + ServiceAccountInfo that = (ServiceAccountInfo) o; + return principal.equals(that.principal) && roleDescriptor.equals(that.roleDescriptor); + } + + @Override + public int hashCode() { + return Objects.hash(principal, roleDescriptor); + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountRequestTests.java new file mode 100644 index 0000000000000..08f6cc4ee730b --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountRequestTests.java @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.core.security.action.service; + +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.test.AbstractWireSerializingTestCase; + +import java.io.IOException; + +public class GetServiceAccountRequestTests extends AbstractWireSerializingTestCase { + + @Override + protected Writeable.Reader instanceReader() { + return GetServiceAccountRequest::new; + } + + @Override + protected GetServiceAccountRequest createTestInstance() { + return new GetServiceAccountRequest(randomFrom(randomAlphaOfLengthBetween(3, 8), null), + randomFrom(randomAlphaOfLengthBetween(3, 8), null)); + } + + @Override + protected GetServiceAccountRequest mutateInstance(GetServiceAccountRequest instance) throws IOException { + if (randomBoolean()) { + return new GetServiceAccountRequest( + randomValueOtherThan(instance.getNamespace(), () -> randomFrom(randomAlphaOfLengthBetween(3, 8), null)), + instance.getServiceName()); + } else { + return new GetServiceAccountRequest( + instance.getNamespace(), + randomValueOtherThan(instance.getServiceName(), () -> randomFrom(randomAlphaOfLengthBetween(3, 8), null))); + } + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountResponseTests.java new file mode 100644 index 0000000000000..680606218821f --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountResponseTests.java @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.core.security.action.service; + +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.test.AbstractWireSerializingTestCase; +import org.elasticsearch.test.XContentTestUtils; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; + +import java.io.IOException; +import java.util.Map; + +import static org.hamcrest.Matchers.anEmptyMap; +import static org.hamcrest.Matchers.equalTo; + +public class GetServiceAccountResponseTests extends AbstractWireSerializingTestCase { + + @Override + protected Writeable.Reader instanceReader() { + return GetServiceAccountResponse::new; + } + + @Override + protected GetServiceAccountResponse createTestInstance() { + final String principal = randomPrincipal(); + return new GetServiceAccountResponse(randomBoolean() + ? new ServiceAccountInfo[]{new ServiceAccountInfo(principal, getRoleDescriptorFor(principal))} + : new ServiceAccountInfo[0]); + } + + @Override + protected GetServiceAccountResponse mutateInstance(GetServiceAccountResponse instance) throws IOException { + if (instance.getServiceAccountInfos().length == 0) { + final String principal = randomPrincipal(); + return new GetServiceAccountResponse(new ServiceAccountInfo[]{ + new ServiceAccountInfo(principal, getRoleDescriptorFor(principal))}); + } else { + return new GetServiceAccountResponse(new ServiceAccountInfo[0]); + } + } + + @SuppressWarnings("unchecked") + public void testToXContent() throws IOException { + final GetServiceAccountResponse response = createTestInstance(); + XContentBuilder builder = XContentFactory.jsonBuilder(); + response.toXContent(builder, ToXContent.EMPTY_PARAMS); + final Map responseMap = XContentHelper.convertToMap( + BytesReference.bytes(builder), + false, builder.contentType()).v2(); + final ServiceAccountInfo[] serviceAccountInfos = response.getServiceAccountInfos(); + if (serviceAccountInfos.length == 0) { + assertThat(responseMap, anEmptyMap()); + } else { + assertThat(responseMap.size(), equalTo(serviceAccountInfos.length)); + for (int i = 0; i < serviceAccountInfos.length - 1; i++) { + final String key = serviceAccountInfos[i].getPrincipal(); + assertRoleDescriptorEquals((Map) responseMap.get(key), serviceAccountInfos[i].getRoleDescriptor()); + } + } + } + + private String randomPrincipal() { + return randomAlphaOfLengthBetween(3, 8) + "/" + randomAlphaOfLengthBetween(3, 8); + } + + private RoleDescriptor getRoleDescriptorFor(String name) { + return new RoleDescriptor(name, + new String[] { "monitor", "manage_own_api_key" }, + new RoleDescriptor.IndicesPrivileges[] { + RoleDescriptor.IndicesPrivileges.builder() + .indices("logs-*", "metrics-*", "traces-*") + .privileges("write", "create_index", "auto_configure").build() }, + null, + null, + null, + null, + null); + } + + private void assertRoleDescriptorEquals(Map responseFragment, RoleDescriptor roleDescriptor) throws IOException { + @SuppressWarnings("unchecked") + final Map descriptorMap = (Map) responseFragment.get("role_descriptor"); + assertThat(RoleDescriptor.parse(roleDescriptor.getName(), + XContentTestUtils.convertToXContent(descriptorMap, XContentType.JSON), false, XContentType.JSON), + equalTo(roleDescriptor)); + } +} diff --git a/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java index fe1cef9251bfe..b676591c87a35 100644 --- a/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java +++ b/x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java @@ -191,6 +191,7 @@ public class Constants { "cluster:admin/xpack/security/saml/invalidate", "cluster:admin/xpack/security/saml/logout", "cluster:admin/xpack/security/saml/prepare", + "cluster:admin/xpack/security/service_account/get", "cluster:admin/xpack/security/service_account/token/create", "cluster:admin/xpack/security/service_account/token/get", "cluster:admin/xpack/security/token/create", 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 bbf906fbc5057..25f2888d47c64 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 @@ -30,6 +30,7 @@ import java.util.Map; import static org.elasticsearch.xpack.core.security.authc.support.UsernamePasswordToken.basicAuthHeaderValue; +import static org.hamcrest.Matchers.anEmptyMap; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasEntry; @@ -61,6 +62,36 @@ public class ServiceAccountIT extends ESRestTestCase { + " \"authentication_type\": \"token\"\n" + "}\n"; + private static final String ELASTIC_FLEET_ROLE_DESCRIPTOR = "" + + "{\n" + + " \"cluster\": [\n" + + " \"monitor\",\n" + + " \"manage_own_api_key\"\n" + + " ],\n" + + " \"indices\": [\n" + + " {\n" + + " \"names\": [\n" + + " \"logs-*\",\n" + + " \"metrics-*\",\n" + + " \"traces-*\"\n" + + " ],\n" + + " \"privileges\": [\n" + + " \"write\",\n" + + " \"create_index\",\n" + + " \"auto_configure\"\n" + + " ],\n" + + " \"allow_restricted_indices\": false\n" + + " }\n" + + " ],\n" + + " \"applications\": [],\n" + + " \"run_as\": [],\n" + + " \"metadata\": {},\n" + + " \"transient_metadata\": {\n" + + " \"enabled\": true\n" + + " }\n" + + " }\n" + + " }"; + @BeforeClass public static void init() throws URISyntaxException, FileNotFoundException { URL resource = ServiceAccountIT.class.getResource("/ssl/ca.crt"); @@ -84,6 +115,32 @@ protected Settings restClientSettings() { .build(); } + public void testGetServiceAccount() throws IOException { + final Request getServiceAccountRequest1 = new Request("GET", "_security/service"); + final Response getServiceAccountResponse1 = client().performRequest(getServiceAccountRequest1); + assertOK(getServiceAccountResponse1); + assertServiceAccountRoleDescriptor(getServiceAccountResponse1, + "elastic/fleet-server", ELASTIC_FLEET_ROLE_DESCRIPTOR); + + final Request getServiceAccountRequest2 = new Request("GET", "_security/service/elastic"); + final Response getServiceAccountResponse2 = client().performRequest(getServiceAccountRequest2); + assertOK(getServiceAccountResponse2); + assertServiceAccountRoleDescriptor(getServiceAccountResponse2, + "elastic/fleet-server", ELASTIC_FLEET_ROLE_DESCRIPTOR); + + final Request getServiceAccountRequest3 = new Request("GET", "_security/service/elastic/fleet-server"); + final Response getServiceAccountResponse3 = client().performRequest(getServiceAccountRequest3); + assertOK(getServiceAccountResponse3); + assertServiceAccountRoleDescriptor(getServiceAccountResponse3, + "elastic/fleet-server", ELASTIC_FLEET_ROLE_DESCRIPTOR); + + final String requestPath = "_security/service/" + randomFrom("foo", "elastic/foo", "foo/bar"); + final Request getServiceAccountRequest4 = new Request("GET", requestPath); + final Response getServiceAccountResponse4 = client().performRequest(getServiceAccountRequest4); + assertOK(getServiceAccountResponse4); + assertThat(responseAsMap(getServiceAccountResponse4), anEmptyMap()); + } + public void testAuthenticate() throws IOException { final Request request = new Request("GET", "_security/_authenticate"); request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "Bearer " + VALID_SERVICE_TOKEN)); @@ -290,4 +347,12 @@ private void assertApiKeys(String apiKeyId, String name, boolean invalidated, assertThat(apiKey.get("realm"), equalTo("service_account")); assertThat(apiKey.get("invalidated"), is(invalidated)); } + + private void assertServiceAccountRoleDescriptor(Response response, + String serviceAccountPrincipal, + String roleDescriptorString) throws IOException { + final Map responseMap = responseAsMap(response); + assertThat(responseMap, hasEntry(serviceAccountPrincipal, Map.of("role_descriptor", + XContentHelper.convertToMap(new BytesArray(roleDescriptorString), false, XContentType.JSON).v2()))); + } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 1f43f0e2fd272..4dad1e7a19514 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -113,6 +113,7 @@ import org.elasticsearch.xpack.core.security.action.saml.SamlPrepareAuthenticationAction; import org.elasticsearch.xpack.core.security.action.saml.SamlSpMetadataAction; import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenAction; +import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountAction; import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountTokensAction; import org.elasticsearch.xpack.core.security.action.token.CreateTokenAction; import org.elasticsearch.xpack.core.security.action.token.InvalidateTokenAction; @@ -182,6 +183,7 @@ import org.elasticsearch.xpack.security.action.saml.TransportSamlPrepareAuthenticationAction; import org.elasticsearch.xpack.security.action.saml.TransportSamlSpMetadataAction; import org.elasticsearch.xpack.security.action.service.TransportCreateServiceAccountTokenAction; +import org.elasticsearch.xpack.security.action.service.TransportGetServiceAccountAction; import org.elasticsearch.xpack.security.action.service.TransportGetServiceAccountTokensAction; import org.elasticsearch.xpack.security.action.token.TransportCreateTokenAction; import org.elasticsearch.xpack.security.action.token.TransportInvalidateTokenAction; @@ -264,6 +266,7 @@ import org.elasticsearch.xpack.security.rest.action.saml.RestSamlPrepareAuthenticationAction; import org.elasticsearch.xpack.security.rest.action.saml.RestSamlSpMetadataAction; import org.elasticsearch.xpack.security.rest.action.service.RestCreateServiceAccountTokenAction; +import org.elasticsearch.xpack.security.rest.action.service.RestGetServiceAccountAction; import org.elasticsearch.xpack.security.rest.action.service.RestGetServiceAccountTokensAction; import org.elasticsearch.xpack.security.rest.action.user.RestChangePasswordAction; import org.elasticsearch.xpack.security.rest.action.user.RestDeleteUserAction; @@ -872,6 +875,7 @@ public void onIndexModule(IndexModule module) { new ActionHandler<>(DelegatePkiAuthenticationAction.INSTANCE, TransportDelegatePkiAuthenticationAction.class), new ActionHandler<>(CreateServiceAccountTokenAction.INSTANCE, TransportCreateServiceAccountTokenAction.class), new ActionHandler<>(GetServiceAccountTokensAction.INSTANCE, TransportGetServiceAccountTokensAction.class), + new ActionHandler<>(GetServiceAccountAction.INSTANCE, TransportGetServiceAccountAction.class), usageAction, infoAction); } @@ -933,7 +937,8 @@ public List getRestHandlers(Settings settings, RestController restC new RestGetApiKeyAction(settings, getLicenseState()), new RestDelegatePkiAuthenticationAction(settings, getLicenseState()), new RestCreateServiceAccountTokenAction(settings, getLicenseState()), - new RestGetServiceAccountTokensAction(settings, getLicenseState()) + new RestGetServiceAccountTokensAction(settings, getLicenseState()), + new RestGetServiceAccountAction(settings, getLicenseState()) ); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountAction.java new file mode 100644 index 0000000000000..eae1464e2507a --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountAction.java @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.action.service; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountAction; +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.service.ServiceAccount; +import org.elasticsearch.xpack.security.authc.service.ServiceAccountService; +import org.elasticsearch.xpack.security.authc.support.HttpTlsRuntimeCheck; + +import java.util.function.Predicate; + +public class TransportGetServiceAccountAction extends HandledTransportAction { + + private final HttpTlsRuntimeCheck httpTlsRuntimeCheck; + + @Inject + public TransportGetServiceAccountAction(TransportService transportService, ActionFilters actionFilters, + HttpTlsRuntimeCheck httpTlsRuntimeCheck) { + super(GetServiceAccountAction.NAME, transportService, actionFilters, GetServiceAccountRequest::new); + this.httpTlsRuntimeCheck = httpTlsRuntimeCheck; + } + + @Override + protected void doExecute(Task task, GetServiceAccountRequest request, ActionListener listener) { + httpTlsRuntimeCheck.checkTlsThenExecute(listener::onFailure, "get service accounts", () -> { + Predicate filter = v -> true; + if (request.getNamespace() != null) { + filter = filter.and( v -> v.id().namespace().equals(request.getNamespace()) ); + } + if (request.getServiceName() != null) { + filter = filter.and( v -> v.id().serviceName().equals(request.getServiceName()) ); + } + final ServiceAccountInfo[] serviceAccountInfos = ServiceAccountService.getServiceAccounts() + .values() + .stream() + .filter(filter) + .map(v -> new ServiceAccountInfo(v.id().asPrincipal(), v.roleDescriptor())) + .toArray(ServiceAccountInfo[]::new); + listener.onResponse(new GetServiceAccountResponse(serviceAccountInfos)); + }); + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java index 2e3348b8d3e09..6e0ea99bebecf 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountService.java @@ -53,6 +53,10 @@ public static Collection getServiceAccountPrincipals() { return ACCOUNTS.keySet(); } + public static Map getServiceAccounts() { + return Map.copyOf(ACCOUNTS); + } + /** * Parses a token object from the content of a {@link ServiceAccountToken#asBearerString()} bearer string}. * This bearer string would typically be diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/service/RestGetServiceAccountAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/service/RestGetServiceAccountAction.java new file mode 100644 index 0000000000000..ff6b249d8822c --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/service/RestGetServiceAccountAction.java @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.rest.action.service; + +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.action.RestToXContentListener; +import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountAction; +import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountRequest; +import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler; + +import java.io.IOException; +import java.util.List; + +import static org.elasticsearch.rest.RestRequest.Method.GET; + +public class RestGetServiceAccountAction extends SecurityBaseRestHandler { + + public RestGetServiceAccountAction(Settings settings, XPackLicenseState licenseState) { + super(settings, licenseState); + } + + @Override + public List routes() { + return List.of( + new Route(GET, "/_security/service"), + new Route(GET, "/_security/service/{namespace}"), + new Route(GET, "/_security/service/{namespace}/{service}") + ); + } + + @Override + public String getName() { + return "xpack_security_get_service_account"; + } + + @Override + protected RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException { + final String namespace = request.param("namespace"); + final String serviceName = request.param("service"); + final GetServiceAccountRequest getServiceAccountRequest = new GetServiceAccountRequest(namespace, serviceName); + return channel -> client.execute(GetServiceAccountAction.INSTANCE, getServiceAccountRequest, + new RestToXContentListener<>(channel)); + } +} 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 new file mode 100644 index 0000000000000..77af6fe2ef889 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountActionTests.java @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.action.service; + +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.test.ESTestCase; +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.security.authc.support.HttpTlsRuntimeCheck; +import org.junit.Before; + +import java.util.Collections; + +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; + +public class TransportGetServiceAccountActionTests extends ESTestCase { + + private HttpTlsRuntimeCheck httpTlsRuntimeCheck; + private TransportGetServiceAccountAction transportGetServiceAccountAction; + + @Before + public void init() { + httpTlsRuntimeCheck = mock(HttpTlsRuntimeCheck.class); + transportGetServiceAccountAction = new TransportGetServiceAccountAction( + mock(TransportService.class), new ActionFilters(Collections.emptySet()), httpTlsRuntimeCheck); + + doAnswer(invocationOnMock -> { + final Object[] arguments = invocationOnMock.getArguments(); + ((Runnable) arguments[2]).run(); + return null; + }).when(httpTlsRuntimeCheck).checkTlsThenExecute(any(), any(), any()); + } + + public void testDoExecute() { + final GetServiceAccountRequest request1 = randomFrom( + new GetServiceAccountRequest(null, null), + new GetServiceAccountRequest("elastic", null), + new GetServiceAccountRequest("elastic", "fleet-server")); + 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")); + + final GetServiceAccountRequest request2 = randomFrom( + new GetServiceAccountRequest("foo", null), + new GetServiceAccountRequest("elastic", "foo"), + new GetServiceAccountRequest("foo", "bar")); + final PlainActionFuture future2 = new PlainActionFuture<>(); + transportGetServiceAccountAction.doExecute(mock(Task.class), request2, future2); + final GetServiceAccountResponse getServiceAccountResponse2 = future2.actionGet(); + assertThat(getServiceAccountResponse2.getServiceAccountInfos().length, equalTo(0)); + } +}