From 92aea028ec8f4b6455183655dd47e9c8b002e34f Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Fri, 26 Mar 2021 13:04:26 +1100 Subject: [PATCH 01/14] Service Account - fleet integration --- .../security/get-builtin-privileges.asciidoc | 2 + .../CreateServiceAccountTokenAction.java | 20 ++ .../CreateServiceAccountTokenRequest.java | 117 +++++++++ .../CreateServiceAccountTokenResponse.java | 98 +++++++ .../GetServiceAccountTokensAction.java | 20 ++ .../GetServiceAccountTokensRequest.java | 62 +++++ .../GetServiceAccountTokensResponse.java | 87 ++++++ .../security/action/service/TokenInfo.java | 64 +++++ .../privilege/ClusterPrivilegeResolver.java | 4 + ...CreateServiceAccountTokenRequestTests.java | 80 ++++++ ...reateServiceAccountTokenResponseTests.java | 47 ++++ .../xpack/security/operator/Constants.java | 2 + .../authc/service/ServiceAccountIT.java | 141 +++++++++- .../ServiceAccountSingleNodeTests.java | 20 ++ .../xpack/security/Security.java | 28 +- ...nsportCreateServiceAccountTokenAction.java | 70 +++++ ...ransportGetServiceAccountTokensAction.java | 63 +++++ .../xpack/security/authc/ApiKeyService.java | 11 + .../CachingServiceAccountsTokenStore.java | 6 + .../CompositeServiceAccountsTokenStore.java | 104 ++++++++ .../FileServiceAccountsTokenStore.java | 17 ++ .../authc/service/FileTokensTool.java | 1 + .../IndexServiceAccountsTokenStore.java | 196 ++++++++++++++ .../authc/service/ServiceAccountService.java | 36 ++- .../service/ServiceAccountsTokenStore.java | 43 +-- .../authz/store/CompositeRolesStore.java | 89 ++++--- .../RestCreateServiceAccountTokenAction.java | 62 +++++ .../RestGetServiceAccountTokensAction.java | 50 ++++ .../support/SecurityIndexManager.java | 24 +- ...tCreateServiceAccountTokenActionTests.java | 86 ++++++ ...CachingServiceAccountsTokenStoreTests.java | 13 + ...mpositeServiceAccountsTokenStoreTests.java | 4 +- .../IndexServiceAccountsTokenStoreTests.java | 248 ++++++++++++++++++ .../service/ServiceAccountServiceTests.java | 32 ++- .../test/privileges/11_builtin.yml | 2 +- 35 files changed, 1859 insertions(+), 90 deletions(-) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenAction.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenRequest.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenResponse.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensAction.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensRequest.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensResponse.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/TokenInfo.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenRequestTests.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenResponseTests.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportCreateServiceAccountTokenAction.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountTokensAction.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountsTokenStore.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStore.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/service/RestCreateServiceAccountTokenAction.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/service/RestGetServiceAccountTokensAction.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportCreateServiceAccountTokenActionTests.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStoreTests.java diff --git a/x-pack/docs/en/rest-api/security/get-builtin-privileges.asciidoc b/x-pack/docs/en/rest-api/security/get-builtin-privileges.asciidoc index cd7caffa85ffd..36043cffd5eeb 100644 --- a/x-pack/docs/en/rest-api/security/get-builtin-privileges.asciidoc +++ b/x-pack/docs/en/rest-api/security/get-builtin-privileges.asciidoc @@ -1,3 +1,4 @@ + [role="xpack"] [[security-api-get-builtin-privileges]] === Get builtin privileges API @@ -83,6 +84,7 @@ A successful call returns an object with "cluster" and "index" fields. "manage_rollup", "manage_saml", "manage_security", + "manage_service_account", "manage_slm", "manage_token", "manage_transform", diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenAction.java new file mode 100644 index 0000000000000..598606e988351 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenAction.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 CreateServiceAccountTokenAction extends ActionType { + + public static final String NAME = "cluster:admin/xpack/security/service_account_token/create"; + public static final CreateServiceAccountTokenAction INSTANCE = new CreateServiceAccountTokenAction(); + + private CreateServiceAccountTokenAction() { + super(NAME, CreateServiceAccountTokenResponse::new); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenRequest.java new file mode 100644 index 0000000000000..d841e0011e5ca --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenRequest.java @@ -0,0 +1,117 @@ +/* + * 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.action.support.WriteRequest; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; +import java.util.Objects; + +import static org.elasticsearch.action.ValidateActions.addValidationError; + +public class CreateServiceAccountTokenRequest extends ActionRequest { + + private final String namespace; + private final String serviceName; + private final String tokenName; + private WriteRequest.RefreshPolicy refreshPolicy = WriteRequest.RefreshPolicy.WAIT_UNTIL; + + public CreateServiceAccountTokenRequest(String namespace, String serviceName, String tokenName) { + this.namespace = namespace; + this.serviceName = serviceName; + this.tokenName = tokenName; + } + + public CreateServiceAccountTokenRequest(StreamInput in) throws IOException { + super(in); + this.namespace = in.readString(); + this.serviceName = in.readString(); + this.tokenName = in.readString(); + this.refreshPolicy = WriteRequest.RefreshPolicy.readFrom(in); + } + + public String getNamespace() { + return namespace; + } + + public String getServiceName() { + return serviceName; + } + + public String getTokenName() { + return tokenName; + } + + public WriteRequest.RefreshPolicy getRefreshPolicy() { + return refreshPolicy; + } + + public void setRefreshPolicy(WriteRequest.RefreshPolicy refreshPolicy) { + this.refreshPolicy = Objects.requireNonNull(refreshPolicy, "refresh policy may not be null"); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + CreateServiceAccountTokenRequest that = (CreateServiceAccountTokenRequest) o; + return Objects.equals(namespace, that.namespace) && Objects.equals(serviceName, that.serviceName) + && Objects.equals(tokenName, that.tokenName) && refreshPolicy == that.refreshPolicy; + } + + @Override + public int hashCode() { + return Objects.hash(namespace, serviceName, tokenName, refreshPolicy); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + out.writeString(namespace); + out.writeString(serviceName); + out.writeString(tokenName); + refreshPolicy.writeTo(out); + } + + @Override + public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = null; + if (Strings.isNullOrEmpty(namespace)) { + validationException = addValidationError("service account namespace is required", validationException); + } + + if (Strings.isNullOrEmpty(serviceName)) { + validationException = addValidationError("service account service-name is required", validationException); + } + + if (Strings.isNullOrEmpty(tokenName)) { + validationException = addValidationError("service account token name is required", validationException); + } else { + if (tokenName.length() > 256) { + validationException = addValidationError( + "service account token name may not be more than 256 characters long", validationException); + } + if (tokenName.equals(tokenName.trim()) == false) { + validationException = addValidationError( + "service account token name may not begin or end with whitespace", validationException); + } + if (tokenName.startsWith("_")) { + validationException = addValidationError( + "service account token name may not begin with an underscore", validationException); + } + } + return validationException; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenResponse.java new file mode 100644 index 0000000000000..b1ee09e0dccac --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenResponse.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.action.ActionResponse; +import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.xcontent.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Objects; + +public class CreateServiceAccountTokenResponse extends ActionResponse implements ToXContentObject { + + private final boolean created; + @Nullable + private final String name; + @Nullable + private final SecureString value; + + private CreateServiceAccountTokenResponse(boolean created, String name, SecureString value) { + this.created = created; + this.name = name; + this.value = value; + } + + public CreateServiceAccountTokenResponse(StreamInput in) throws IOException { + super(in); + this.created = in.readBoolean(); + this.name = in.readOptionalString(); + this.value = in.readOptionalSecureString(); + } + + public boolean isCreated() { + return created; + } + + public String getName() { + return name; + } + + public SecureString getValue() { + return value; + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field("created", created); + if (created) { + builder.field("token"); + builder.startObject(); + builder.field("name", name); + builder.field("value", value.toString()); + builder.endObject(); + } + builder.endObject(); + return builder; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeBoolean(created); + out.writeOptionalString(name); + out.writeOptionalSecureString(value); + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + CreateServiceAccountTokenResponse that = (CreateServiceAccountTokenResponse) o; + return created == that.created && Objects.equals(name, that.name) && Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(created, name, value); + } + + public static CreateServiceAccountTokenResponse created(String name, SecureString value) { + return new CreateServiceAccountTokenResponse(true, name, value); + } + + public static CreateServiceAccountTokenResponse notCreated() { + return new CreateServiceAccountTokenResponse(false, null, null); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensAction.java new file mode 100644 index 0000000000000..4bd3498fdf2de --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensAction.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 GetServiceAccountTokensAction extends ActionType { + + public static final String NAME = "cluster:admin/xpack/security/service_account_token/get"; + public static final GetServiceAccountTokensAction INSTANCE = new GetServiceAccountTokensAction(); + + public GetServiceAccountTokensAction() { + super(NAME, GetServiceAccountTokensResponse::new); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensRequest.java new file mode 100644 index 0000000000000..b7cf32aa68f17 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensRequest.java @@ -0,0 +1,62 @@ +/* + * 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.Strings; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; + +import static org.elasticsearch.action.ValidateActions.addValidationError; + +public class GetServiceAccountTokensRequest extends ActionRequest { + + private final String namespace; + private final String serviceName; + + public GetServiceAccountTokensRequest(String namespace, String serviceName) { + this.namespace = namespace; + this.serviceName = serviceName; + } + + public GetServiceAccountTokensRequest(StreamInput in) throws IOException { + super(in); + this.namespace = in.readString(); + this.serviceName = in.readString(); + } + + public String getNamespace() { + return namespace; + } + + public String getServiceName() { + return serviceName; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(namespace); + out.writeString(serviceName); + } + + @Override + public ActionRequestValidationException validate() { + ActionRequestValidationException validationException = null; + if (Strings.isNullOrEmpty(namespace)) { + validationException = addValidationError("service account namespace is required", validationException); + } + + if (Strings.isNullOrEmpty(serviceName)) { + validationException = addValidationError("service account service-name is required", validationException); + } + return validationException; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensResponse.java new file mode 100644 index 0000000000000..c0f91e952ab39 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensResponse.java @@ -0,0 +1,87 @@ +/* + * 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.Collection; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.toUnmodifiableList; + +public class GetServiceAccountTokensResponse extends ActionResponse implements ToXContentObject { + + private final String principal; + private final String nodeName; + private final Collection tokenInfos; + + public GetServiceAccountTokensResponse(String principal, String nodeName, Collection tokenInfos) { + this.principal = principal; + this.nodeName = nodeName; + this.tokenInfos = tokenInfos == null ? List.of() : tokenInfos; + } + + public GetServiceAccountTokensResponse(StreamInput in) throws IOException { + super(in); + this.principal = in.readString(); + this.nodeName = in.readString(); + this.tokenInfos = in.readList(TokenInfo::new); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(principal); + out.writeString(nodeName); + out.writeCollection(tokenInfos); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + final Map> tokenInfosBySource = + tokenInfos.stream().collect(groupingBy(TokenInfo::getSource, toUnmodifiableList())); + builder.startObject() + .field("service_account", principal) + .field("node_name", nodeName) + .field("count", tokenInfos.size()) + .field("tokens").startObject(); + for (TokenInfo info : tokenInfosBySource.getOrDefault(TokenInfo.TokenSource.INDEX, List.of())) { + info.toXContent(builder, params); + } + builder.endObject().field("file_tokens").startObject(); + for (TokenInfo info : tokenInfosBySource.getOrDefault(TokenInfo.TokenSource.FILE, List.of())) { + info.toXContent(builder, params); + } + builder.endObject().endObject(); + return builder; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + GetServiceAccountTokensResponse that = (GetServiceAccountTokensResponse) o; + return Objects.equals(principal, that.principal) && Objects.equals(nodeName, that.nodeName) && Objects.equals( + tokenInfos, + that.tokenInfos); + } + + @Override + public int hashCode() { + return Objects.hash(principal, nodeName, tokenInfos); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/TokenInfo.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/TokenInfo.java new file mode 100644 index 0000000000000..ca10ef34d564f --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/TokenInfo.java @@ -0,0 +1,64 @@ +/* + * 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.ToXContentObject; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.IOException; +import java.util.Map; + +public class TokenInfo implements Writeable, ToXContentObject { + + private final String name; + private final TokenSource source; + + private TokenInfo(String name, TokenSource source) { + this.name = name; + this.source = source; + } + + public TokenInfo(StreamInput in) throws IOException { + this.name = in.readString(); + this.source = in.readEnum(TokenSource.class); + } + + public String getName() { + return name; + } + + public TokenSource getSource() { + return source; + } + + public static TokenInfo indexToken(String name) { + return new TokenInfo(name, TokenSource.INDEX); + } + + public static TokenInfo fileToken(String name) { + return new TokenInfo(name, TokenSource.FILE); + } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + return builder.field(name, Map.of()); + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(name); + out.writeEnum(source); + } + + public enum TokenSource { + INDEX, FILE; + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java index 911104b94187d..f85c9a9548d2d 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java @@ -57,6 +57,7 @@ public class ClusterPrivilegeResolver { private static final Set MANAGE_OIDC_PATTERN = Set.of("cluster:admin/xpack/security/oidc/*"); private static final Set MANAGE_TOKEN_PATTERN = Set.of("cluster:admin/xpack/security/token/*"); private static final Set MANAGE_API_KEY_PATTERN = Set.of("cluster:admin/xpack/security/api_key/*"); + private static final Set MANAGE_SERVICE_ACCOUNT_PATTERN = Set.of("cluster:admin/xpack/security/service_account_token/*"); private static final Set GRANT_API_KEY_PATTERN = Set.of(GrantApiKeyAction.NAME + "*"); private static final Set MONITOR_PATTERN = Set.of("cluster:monitor/*"); private static final Set MONITOR_ML_PATTERN = Set.of("cluster:monitor/xpack/ml/*"); @@ -124,6 +125,8 @@ public class ClusterPrivilegeResolver { public static final NamedClusterPrivilege MANAGE_SAML = new ActionClusterPrivilege("manage_saml", MANAGE_SAML_PATTERN); public static final NamedClusterPrivilege MANAGE_OIDC = new ActionClusterPrivilege("manage_oidc", MANAGE_OIDC_PATTERN); public static final NamedClusterPrivilege MANAGE_API_KEY = new ActionClusterPrivilege("manage_api_key", MANAGE_API_KEY_PATTERN); + public static final NamedClusterPrivilege MANAGE_SERVICE_ACCOUNT = new ActionClusterPrivilege("manage_service_account", + MANAGE_SERVICE_ACCOUNT_PATTERN); public static final NamedClusterPrivilege GRANT_API_KEY = new ActionClusterPrivilege("grant_api_key", GRANT_API_KEY_PATTERN); public static final NamedClusterPrivilege MANAGE_PIPELINE = new ActionClusterPrivilege("manage_pipeline", Set.of("cluster:admin" + "/ingest/pipeline/*")); @@ -176,6 +179,7 @@ public class ClusterPrivilegeResolver { MANAGE_OIDC, MANAGE_API_KEY, GRANT_API_KEY, + MANAGE_SERVICE_ACCOUNT, MANAGE_PIPELINE, MANAGE_ROLLUP, MANAGE_AUTOSCALING, diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenRequestTests.java new file mode 100644 index 0000000000000..c589200b2fede --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenRequestTests.java @@ -0,0 +1,80 @@ +/* + * 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.ActionRequestValidationException; +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.InputStreamStreamInput; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.test.ESTestCase; + +import java.io.ByteArrayInputStream; +import java.io.IOException; + +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.nullValue; + +public class CreateServiceAccountTokenRequestTests extends ESTestCase { + + public void testReadWrite() throws IOException { + final CreateServiceAccountTokenRequest request = new CreateServiceAccountTokenRequest( + randomAlphaOfLengthBetween(3, 8), + randomAlphaOfLengthBetween(3, 8), + randomAlphaOfLengthBetween(3, 8)); + try (BytesStreamOutput out = new BytesStreamOutput()) { + request.writeTo(out); + try (StreamInput in = new InputStreamStreamInput(new ByteArrayInputStream(out.bytes().array()))) { + assertThat(new CreateServiceAccountTokenRequest(in), equalTo(request)); + } + } + } + + public void testValidation() { + final String namespace = randomAlphaOfLengthBetween(3, 8); + final String serviceName = randomAlphaOfLengthBetween(3, 8); + final String tokenName = randomAlphaOfLengthBetween(3, 8); + + final CreateServiceAccountTokenRequest request1 = + new CreateServiceAccountTokenRequest(randomFrom("", null), serviceName, tokenName); + final ActionRequestValidationException validation1 = request1.validate(); + assertThat(validation1.validationErrors(), contains(containsString("namespace is required"))); + + final CreateServiceAccountTokenRequest request2 = + new CreateServiceAccountTokenRequest(namespace, randomFrom("", null), tokenName); + final ActionRequestValidationException validation2 = request2.validate(); + assertThat(validation2.validationErrors(), contains(containsString("service-name is required"))); + + final CreateServiceAccountTokenRequest request3 = + new CreateServiceAccountTokenRequest(namespace, serviceName, randomFrom("", null)); + final ActionRequestValidationException validation3 = request3.validate(); + assertThat(validation3.validationErrors(), contains(containsString("token name is required"))); + + final CreateServiceAccountTokenRequest request4 = new CreateServiceAccountTokenRequest(namespace, serviceName, + randomFrom(" " + tokenName, tokenName + " ", " " + tokenName + " ")); + final ActionRequestValidationException validation4 = request4.validate(); + assertThat(validation4.validationErrors(), contains(containsString( + "service account token name may not begin or end with whitespace"))); + + final CreateServiceAccountTokenRequest request5 = new CreateServiceAccountTokenRequest(namespace, serviceName, "_" + tokenName); + final ActionRequestValidationException validation5 = request5.validate(); + assertThat(validation5.validationErrors(), contains(containsString( + "service account token name may not begin with an underscore"))); + + final CreateServiceAccountTokenRequest request6 = new CreateServiceAccountTokenRequest(namespace, serviceName, + randomAlphaOfLength(257)); + final ActionRequestValidationException validation6 = request6.validate(); + assertThat(validation6.validationErrors(), contains(containsString( + "service account token name may not be more than 256 characters long"))); + + final CreateServiceAccountTokenRequest request7 = new CreateServiceAccountTokenRequest(namespace, serviceName, tokenName); + final ActionRequestValidationException validation7 = request7.validate(); + assertThat(validation7, nullValue()); + } +} diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenResponseTests.java new file mode 100644 index 0000000000000..2bc35ed9f8a09 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenResponseTests.java @@ -0,0 +1,47 @@ +/* + * 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.common.settings.SecureString; +import org.elasticsearch.test.AbstractWireSerializingTestCase; + +import java.io.IOException; + +public class CreateServiceAccountTokenResponseTests extends AbstractWireSerializingTestCase { + + @Override + protected Writeable.Reader instanceReader() { + return CreateServiceAccountTokenResponse::new; + } + + @Override + protected CreateServiceAccountTokenResponse createTestInstance() { + if (randomBoolean()) { + return CreateServiceAccountTokenResponse.created( + randomAlphaOfLengthBetween(3, 8), new SecureString(randomAlphaOfLength(20).toCharArray())); + } else { + return CreateServiceAccountTokenResponse.notCreated(); + } + } + + @Override + protected CreateServiceAccountTokenResponse mutateInstance(CreateServiceAccountTokenResponse instance) throws IOException { + if (instance.isCreated()) { + if (randomBoolean()) { + return CreateServiceAccountTokenResponse.created(randomAlphaOfLengthBetween(3, 8), + new SecureString(randomAlphaOfLength(20).toCharArray())); + } else { + return CreateServiceAccountTokenResponse.notCreated(); + } + } else { + return CreateServiceAccountTokenResponse.created(randomAlphaOfLengthBetween(3, 8), + new SecureString(randomAlphaOfLength(20).toCharArray())); + } + } +} 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 c02d4aef1b465..2a2feda89830d 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,8 @@ 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_token/create", + "cluster:admin/xpack/security/service_account_token/get", "cluster:admin/xpack/security/token/create", "cluster:admin/xpack/security/token/invalidate", "cluster:admin/xpack/security/token/refresh", 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 767fa5b0a9fb6..7e4ea93d05adf 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 @@ -33,6 +33,7 @@ import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.is; public class ServiceAccountIT extends ESRestTestCase { @@ -93,11 +94,21 @@ public void testAuthenticate() throws IOException { } public void testAuthenticateShouldNotFallThroughInCaseOfFailure() throws IOException { + final boolean securityIndexExists = randomBoolean(); + if (securityIndexExists) { + final Request createRoleRequest = new Request("POST", "_security/role/dummy_role"); + createRoleRequest.setJsonEntity("{\"cluster\":[]}"); + assertOK(client().performRequest(createRoleRequest)); + } final Request request = new Request("GET", "_security/_authenticate"); request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "Bearer " + INVALID_SERVICE_TOKEN)); final ResponseException e = expectThrows(ResponseException.class, () -> client().performRequest(request)); assertThat(e.getResponse().getStatusLine().getStatusCode(), equalTo(401)); - assertThat(e.getMessage(), containsString("failed to authenticate service account [elastic/fleet] with token name [token1]")); + if (securityIndexExists) { + assertThat(e.getMessage(), containsString("failed to authenticate service account [elastic/fleet] with token name [token1]")); + } else { + assertThat(e.getMessage(), containsString("no such index [.security]")); + } } public void testAuthenticateShouldWorkWithOAuthBearerToken() throws IOException { @@ -138,4 +149,132 @@ public void testAuthenticateShouldDifferentiateBetweenNormalUserAndServiceAccoun Map authRealm = (Map) responseMap.get("authentication_realm"); assertThat(authRealm, hasEntry("type", "file")); } + + public void testCreateApiServiceAccountTokenAndAuthenticateWithIt() throws IOException { + final Request createTokenRequest = new Request("POST", "_security/service/elastic/fleet/credential/token/api-token-1"); + final Response createTokenResponse = client().performRequest(createTokenRequest); + assertOK(createTokenResponse); + final Map createTokenResponseMap = responseAsMap(createTokenResponse); + assertThat(createTokenResponseMap.get("created"), is(true)); + @SuppressWarnings("unchecked") + final Map tokenMap = (Map) createTokenResponseMap.get("token"); + assertThat(tokenMap.get("name"), equalTo("api-token-1")); + + final Request request = new Request("GET", "_security/_authenticate"); + request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "Bearer " + tokenMap.get("value"))); + final Response response = client().performRequest(request); + assertOK(response); + assertThat(responseAsMap(response), + equalTo(XContentHelper.convertToMap(new BytesArray(AUTHENTICATE_RESPONSE), false, XContentType.JSON).v2())); + } + + public void testFileTokenAndApiTokenCanShareTheSameNameAndBothWorks() throws IOException { + final Request createTokenRequest = new Request("POST", "_security/service/elastic/fleet/credential/token/token1"); + final Response createTokenResponse = client().performRequest(createTokenRequest); + assertOK(createTokenResponse); + final Map createTokenResponseMap = responseAsMap(createTokenResponse); + assertThat(createTokenResponseMap.get("created"), is(true)); + @SuppressWarnings("unchecked") + final Map tokenMap = (Map) createTokenResponseMap.get("token"); + assertThat(tokenMap.get("name"), equalTo("token1")); + + // The API token works + final Request request = new Request("GET", "_security/_authenticate"); + request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "Bearer " + tokenMap.get("value"))); + assertOK(client().performRequest(request)); + + // And the file token also works + request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "Bearer " + VALID_SERVICE_TOKEN)); + assertOK(client().performRequest(request)); + } + + public void testGetServiceAccountTokens() throws IOException { + final Request getTokensRequest = new Request("GET", "_security/service/elastic/fleet/credential"); + final Response getTokensResponse1 = client().performRequest(getTokensRequest); + assertOK(getTokensResponse1); + final Map getTokensResponseMap1 = responseAsMap(getTokensResponse1); + assertThat(getTokensResponseMap1.get("service_account"), equalTo("elastic/fleet")); + assertThat(getTokensResponseMap1.get("count"), equalTo(1)); + assertThat(getTokensResponseMap1.get("tokens"), equalTo(Map.of())); + assertThat(getTokensResponseMap1.get("file_tokens"), equalTo(Map.of("token1", Map.of()))); + + final Request createTokenRequest1 = new Request("POST", "_security/service/elastic/fleet/credential/token/api-token-1"); + final Response createTokenResponse1 = client().performRequest(createTokenRequest1); + assertOK(createTokenResponse1); + + final Request createTokenRequest2 = new Request("POST", "_security/service/elastic/fleet/credential/token/api-token-2"); + final Response createTokenResponse2 = client().performRequest(createTokenRequest2); + assertOK(createTokenResponse2); + + final Response getTokensResponse2 = client().performRequest(getTokensRequest); + assertOK(getTokensResponse2); + final Map getTokensResponseMap2 = responseAsMap(getTokensResponse2); + assertThat(getTokensResponseMap2.get("service_account"), equalTo("elastic/fleet")); + assertThat(getTokensResponseMap2.get("count"), equalTo(3)); + assertThat(getTokensResponseMap2.get("file_tokens"), equalTo(Map.of("token1", Map.of()))); + assertThat(getTokensResponseMap2.get("tokens"), equalTo(Map.of( + "api-token-1", Map.of(), + "api-token-2", Map.of() + ))); + } + + public void testManageOwnApiKey() throws IOException { + final String token; + if (randomBoolean()) { + token = VALID_SERVICE_TOKEN; + } else { + final Request createTokenRequest = new Request("POST", "_security/service/elastic/fleet/credential/token/api-token-42"); + final Response createTokenResponse = client().performRequest(createTokenRequest); + assertOK(createTokenResponse); + final Map createTokenResponseMap = responseAsMap(createTokenResponse); + assertThat(createTokenResponseMap.get("created"), is(true)); + @SuppressWarnings("unchecked") + final Map tokenMap = (Map) createTokenResponseMap.get("token"); + assertThat(tokenMap.get("name"), equalTo("api-token-42")); + token = tokenMap.get("value"); + } + final RequestOptions.Builder requestOptions = RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", "Bearer " + token); + + final Request createApiKeyRequest1 = new Request("PUT", "_security/api_key"); + if (randomBoolean()) { + createApiKeyRequest1.setJsonEntity("{\"name\":\"key-1\"}"); + } else { + createApiKeyRequest1.setJsonEntity("{\"name\":\"key-1\",\"role_descriptors\":{\"a\":{\"cluster\":[\"all\"]}}}"); + } + createApiKeyRequest1.setOptions(requestOptions); + final Response createApiKeyResponse1 = client().performRequest(createApiKeyRequest1); + assertOK(createApiKeyResponse1); + final String apiKeyId1 = (String) responseAsMap(createApiKeyResponse1).get("id"); + + assertApiKeys(apiKeyId1, "key-1", false, requestOptions); + + final Request invalidateApiKeysRequest = new Request("DELETE", "_security/api_key"); + invalidateApiKeysRequest.setJsonEntity("{\"ids\":[\"" + apiKeyId1 + "\"],\"owner\":true}"); + invalidateApiKeysRequest.setOptions(requestOptions); + final Response invalidateApiKeysResponse = client().performRequest(invalidateApiKeysRequest); + assertOK(invalidateApiKeysResponse); + final Map invalidateApiKeysResponseMap = responseAsMap(invalidateApiKeysResponse); + assertThat(invalidateApiKeysResponseMap.get("invalidated_api_keys"), equalTo(List.of(apiKeyId1))); + + assertApiKeys(apiKeyId1, "key-1", true, requestOptions); + } + + private void assertApiKeys(String apiKeyId, String name, boolean invalidated, + RequestOptions.Builder requestOptions) throws IOException { + final Request getApiKeysRequest = new Request("GET", "_security/api_key?owner=true"); + getApiKeysRequest.setOptions(requestOptions); + final Response getApiKeysResponse = client().performRequest(getApiKeysRequest); + assertOK(getApiKeysResponse); + final Map getApiKeysResponseMap = responseAsMap(getApiKeysResponse); + @SuppressWarnings("unchecked") + final List> apiKeys = (List>) getApiKeysResponseMap.get("api_keys"); + assertThat(apiKeys.size(), equalTo(1)); + + final Map apiKey = apiKeys.get(0); + assertThat(apiKey.get("id"), equalTo(apiKeyId)); + assertThat(apiKey.get("name"), equalTo(name)); + assertThat(apiKey.get("username"), equalTo("elastic/fleet")); + assertThat(apiKey.get("realm"), equalTo("service_account")); + assertThat(apiKey.get("invalidated"), is(invalidated)); + } } diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountSingleNodeTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountSingleNodeTests.java index 22dd5ce5d5fca..dd5464cd87eb2 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountSingleNodeTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountSingleNodeTests.java @@ -10,6 +10,7 @@ import org.elasticsearch.Version; import org.elasticsearch.client.Client; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.node.Node; import org.elasticsearch.test.SecuritySingleNodeTestCase; import org.elasticsearch.xpack.core.security.action.user.AuthenticateAction; @@ -20,12 +21,31 @@ import java.util.Map; +import static org.elasticsearch.test.SecuritySettingsSource.addSSLSettingsForNodePEMFiles; import static org.hamcrest.Matchers.equalTo; public class ServiceAccountSingleNodeTests extends SecuritySingleNodeTestCase { private static final String BEARER_TOKEN = "AAEAAWVsYXN0aWMvZmxlZXQvdG9rZW4xOnI1d2RiZGJvUVNlOXZHT0t3YUpHQXc"; + @Override + protected Settings nodeSettings() { + Settings.Builder builder = Settings.builder().put(super.nodeSettings()); + addSSLSettingsForNodePEMFiles(builder, "xpack.security.http.", true); + builder.put("xpack.security.http.ssl.enabled", true); + return builder.build(); + } + + @Override + protected boolean addMockHttpTransport() { + return false; // enable http + } + + @Override + protected boolean transportSSLEnabled() { + return true; + } + @Override protected String configServiceTokens() { return super.configServiceTokens() 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 0cd1aee8ded8a..700054b35803c 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 @@ -112,6 +112,8 @@ import org.elasticsearch.xpack.core.security.action.saml.SamlLogoutAction; 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.GetServiceAccountTokensAction; import org.elasticsearch.xpack.core.security.action.token.CreateTokenAction; import org.elasticsearch.xpack.core.security.action.token.InvalidateTokenAction; import org.elasticsearch.xpack.core.security.action.token.RefreshTokenAction; @@ -179,6 +181,8 @@ import org.elasticsearch.xpack.security.action.saml.TransportSamlLogoutAction; 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.TransportGetServiceAccountTokensAction; import org.elasticsearch.xpack.security.action.token.TransportCreateTokenAction; import org.elasticsearch.xpack.security.action.token.TransportInvalidateTokenAction; import org.elasticsearch.xpack.security.action.token.TransportRefreshTokenAction; @@ -201,8 +205,9 @@ import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; import org.elasticsearch.xpack.security.authc.service.FileServiceAccountsTokenStore; +import org.elasticsearch.xpack.security.authc.service.IndexServiceAccountsTokenStore; import org.elasticsearch.xpack.security.authc.service.ServiceAccountService; -import org.elasticsearch.xpack.security.authc.service.ServiceAccountsTokenStore.CompositeServiceAccountsTokenStore; +import org.elasticsearch.xpack.security.authc.service.CompositeServiceAccountsTokenStore; import org.elasticsearch.xpack.security.authc.support.SecondaryAuthenticator; import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore; import org.elasticsearch.xpack.security.authz.AuthorizationService; @@ -256,6 +261,8 @@ import org.elasticsearch.xpack.security.rest.action.saml.RestSamlLogoutAction; 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.RestGetServiceAccountTokensAction; import org.elasticsearch.xpack.security.rest.action.user.RestChangePasswordAction; import org.elasticsearch.xpack.security.rest.action.user.RestDeleteUserAction; import org.elasticsearch.xpack.security.rest.action.user.RestGetUserPrivilegesAction; @@ -492,10 +499,17 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste clusterService, cacheInvalidatorRegistry, threadPool); components.add(apiKeyService); - final ServiceAccountService serviceAccountService = new ServiceAccountService( - new CompositeServiceAccountsTokenStore( - List.of(new FileServiceAccountsTokenStore(environment, resourceWatcherService, threadPool)), + final IndexServiceAccountsTokenStore indexServiceAccountsTokenStore = new IndexServiceAccountsTokenStore( + settings, threadPool, getClock(), client, securityIndex.get(), clusterService, cacheInvalidatorRegistry); + components.add(indexServiceAccountsTokenStore); + + final FileServiceAccountsTokenStore fileServiceAccountsTokenStore = + new FileServiceAccountsTokenStore(environment, resourceWatcherService, threadPool); + + final ServiceAccountService serviceAccountService = new ServiceAccountService(settings, + new CompositeServiceAccountsTokenStore(List.of(fileServiceAccountsTokenStore, indexServiceAccountsTokenStore), threadPool.getThreadContext())); + components.add(serviceAccountService); final CompositeRolesStore allRolesStore = new CompositeRolesStore(settings, fileRolesStore, nativeRolesStore, reservedRolesStore, privilegeStore, rolesProviders, threadPool.getThreadContext(), getLicenseState(), fieldPermissionsCache, apiKeyService, @@ -848,6 +862,8 @@ public void onIndexModule(IndexModule module) { new ActionHandler<>(InvalidateApiKeyAction.INSTANCE, TransportInvalidateApiKeyAction.class), new ActionHandler<>(GetApiKeyAction.INSTANCE, TransportGetApiKeyAction.class), new ActionHandler<>(DelegatePkiAuthenticationAction.INSTANCE, TransportDelegatePkiAuthenticationAction.class), + new ActionHandler<>(CreateServiceAccountTokenAction.INSTANCE, TransportCreateServiceAccountTokenAction.class), + new ActionHandler<>(GetServiceAccountTokensAction.INSTANCE, TransportGetServiceAccountTokensAction.class), usageAction, infoAction); } @@ -907,7 +923,9 @@ public List getRestHandlers(Settings settings, RestController restC new RestGrantApiKeyAction(settings, getLicenseState()), new RestInvalidateApiKeyAction(settings, getLicenseState()), new RestGetApiKeyAction(settings, getLicenseState()), - new RestDelegatePkiAuthenticationAction(settings, getLicenseState()) + new RestDelegatePkiAuthenticationAction(settings, getLicenseState()), + new RestCreateServiceAccountTokenAction(settings, getLicenseState()), + new RestGetServiceAccountTokensAction(settings, getLicenseState()) ); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportCreateServiceAccountTokenAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportCreateServiceAccountTokenAction.java new file mode 100644 index 0000000000000..ad2f8a8e95752 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportCreateServiceAccountTokenAction.java @@ -0,0 +1,70 @@ +/* + * 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.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.ElasticsearchSecurityException; +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.common.settings.Settings; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.XPackSettings; +import org.elasticsearch.xpack.core.security.SecurityContext; +import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenAction; +import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenRequest; +import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenResponse; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.security.authc.service.IndexServiceAccountsTokenStore; + +public class TransportCreateServiceAccountTokenAction + extends HandledTransportAction { + + private static final Logger logger = LogManager.getLogger(TransportCreateServiceAccountTokenAction.class); + + private final IndexServiceAccountsTokenStore indexServiceAccountsTokenStore; + private final SecurityContext securityContext; + private final boolean httpTlsEnabled; + private final boolean transportTlsEnabled; + + @Inject + public TransportCreateServiceAccountTokenAction(TransportService transportService, ActionFilters actionFilters, + Settings settings, + IndexServiceAccountsTokenStore indexServiceAccountsTokenStore, + SecurityContext securityContext) { + super(CreateServiceAccountTokenAction.NAME, transportService, actionFilters, CreateServiceAccountTokenRequest::new); + this.indexServiceAccountsTokenStore = indexServiceAccountsTokenStore; + this.securityContext = securityContext; + this.httpTlsEnabled = XPackSettings.HTTP_SSL_ENABLED.get(settings); + this.transportTlsEnabled = XPackSettings.TRANSPORT_SSL_ENABLED.get(settings); + } + + @Override + protected void doExecute(Task task, CreateServiceAccountTokenRequest request, + ActionListener listener) { + if (false == httpTlsEnabled || false == transportTlsEnabled) { + final ParameterizedMessage message = new ParameterizedMessage( + "Service account APIs require TLS for both HTTP and Transport, " + + "but got HTTP TLS: [{}] and Transport TLS: [{}]", httpTlsEnabled, transportTlsEnabled); + logger.debug(message); + listener.onFailure(new ElasticsearchSecurityException(message.getFormattedMessage(), RestStatus.UNAUTHORIZED)); + return; + } + final Authentication authentication = securityContext.getAuthentication(); + if (authentication == null) { + listener.onFailure(new IllegalStateException("authentication is required")); + } else { + indexServiceAccountsTokenStore.createToken(authentication, request, listener); + } + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountTokensAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountTokensAction.java new file mode 100644 index 0000000000000..3e601fb2d4b3a --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountTokensAction.java @@ -0,0 +1,63 @@ +/* + * 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.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.ElasticsearchSecurityException; +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.common.settings.Settings; +import org.elasticsearch.node.Node; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.XPackSettings; +import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountTokensAction; +import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountTokensRequest; +import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountTokensResponse; +import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; +import org.elasticsearch.xpack.security.authc.service.ServiceAccountService; + +public class TransportGetServiceAccountTokensAction + extends HandledTransportAction { + + private static final Logger logger = LogManager.getLogger(TransportGetServiceAccountTokensAction.class); + + private final ServiceAccountService serviceAccountService; + private final String nodeName; + private final boolean httpTlsEnabled; + private final boolean transportTlsEnabled; + + @Inject + public TransportGetServiceAccountTokensAction( + TransportService transportService, ActionFilters actionFilters, Settings settings, ServiceAccountService serviceAccountService) { + super(GetServiceAccountTokensAction.NAME, transportService, actionFilters, GetServiceAccountTokensRequest::new); + this.nodeName = Node.NODE_NAME_SETTING.get(settings); + this.serviceAccountService = serviceAccountService; + this.httpTlsEnabled = XPackSettings.HTTP_SSL_ENABLED.get(settings); + this.transportTlsEnabled = XPackSettings.TRANSPORT_SSL_ENABLED.get(settings); + } + + @Override + protected void doExecute(Task task, GetServiceAccountTokensRequest request, ActionListener listener) { + if (false == httpTlsEnabled || false == transportTlsEnabled) { + final ParameterizedMessage message = new ParameterizedMessage( + "Service account APIs require TLS for both HTTP and Transport, " + + "but got HTTP TLS: [{}] and Transport TLS: [{}]", httpTlsEnabled, transportTlsEnabled); + logger.debug(message); + listener.onFailure(new ElasticsearchSecurityException(message.getFormattedMessage(), RestStatus.UNAUTHORIZED)); + return; + } + final ServiceAccountId accountId = new ServiceAccountId(request.getNamespace(), request.getServiceName()); + serviceAccountService.findTokensFor(accountId, nodeName, listener); + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java index 56fda5ae8a0c9..7f6ae6af2e9af 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/ApiKeyService.java @@ -710,6 +710,17 @@ static ApiKeyCredentials getCredentialsFromHeader(ThreadContext threadContext) { return null; } + public static boolean isApiKeyAuthentication(Authentication authentication) { + final Authentication.AuthenticationType authType = authentication.getAuthenticationType(); + if (Authentication.AuthenticationType.API_KEY == authType) { + assert API_KEY_REALM_TYPE.equals(authentication.getAuthenticatedBy().getType()) + : "API key authentication must have API key realm type"; + return true; + } else { + return false; + } + } + // Protected instance method so this can be mocked protected void verifyKeyAgainstHash(String apiKeyHash, ApiKeyCredentials credentials, ActionListener listener) { threadPool.executor(SECURITY_CRYPTO_THREAD_POOL_NAME).execute(ActionRunnable.supply(listener, () -> { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStore.java index 0d4cc958ec58a..cb6f97752f086 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStore.java @@ -37,11 +37,13 @@ public abstract class CachingServiceAccountsTokenStore implements ServiceAccount public static final Setting CACHE_MAX_TOKENS_SETTING = Setting.intSetting( "xpack.security.authc.service_token.cache.max_tokens", 100_000, Setting.Property.NodeScope); + private final Settings settings; private final ThreadPool threadPool; private final Cache> cache; private final Hasher hasher; CachingServiceAccountsTokenStore(Settings settings, ThreadPool threadPool) { + this.settings = settings; this.threadPool = threadPool; final TimeValue ttl = CACHE_TTL_SETTING.get(settings); if (ttl.getNanos() > 0) { @@ -122,6 +124,10 @@ public final void invalidateAll() { } } + protected Settings getSettings() { + return settings; + } + protected ThreadPool getThreadPool() { return threadPool; } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountsTokenStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountsTokenStore.java new file mode 100644 index 0000000000000..0ee91e5d8c46b --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountsTokenStore.java @@ -0,0 +1,104 @@ +/* + * 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.authc.service; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.xpack.core.common.IteratingActionListener; +import org.elasticsearch.xpack.core.security.action.service.TokenInfo; +import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; +import org.elasticsearch.xpack.security.authc.service.ServiceAccountToken; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Function; + +public final class CompositeServiceAccountsTokenStore implements ServiceAccountsTokenStore { + + private static final Logger logger = + LogManager.getLogger(org.elasticsearch.xpack.security.authc.service.CompositeServiceAccountsTokenStore.class); + + private final ThreadContext threadContext; + private final List stores; + + public CompositeServiceAccountsTokenStore( + List stores, ThreadContext threadContext) { + this.stores = stores; + this.threadContext = threadContext; + } + + @Override + public void authenticate(ServiceAccountToken token, ActionListener listener) { + // TODO: optimize store order based on auth result? + final IteratingActionListener authenticatingListener = new IteratingActionListener<>( + listener, + (store, successListener) -> store.authenticate(token, successListener), + stores, + threadContext, + Function.identity(), + success -> Boolean.FALSE == success); + try { + authenticatingListener.run(); + } catch (Exception e) { + logger.debug(new ParameterizedMessage("authentication of service token [{}] failed", token.getQualifiedName()), e); + listener.onFailure(e); + } + } + + @Override + public void findTokensFor(ServiceAccountId accountId, ActionListener> listener) { + final CollectingActionListener collector = new CollectingActionListener(accountId, listener); + try { + collector.run(); + } catch (Exception e) { + listener.onFailure(e); + } + } + + class CollectingActionListener implements ActionListener>, Runnable { + private final ActionListener> delegate; + private final ServiceAccountId accountId; + private final List result = new ArrayList<>(); + private int position = 0; + + CollectingActionListener(ServiceAccountId accountId, ActionListener> delegate) { + this.delegate = delegate; + this.accountId = accountId; + } + + @Override + public void run() { + if (stores.isEmpty()) { + delegate.onResponse(List.of()); + } else if (position < 0 || position >= stores.size()) { + onFailure(new IllegalArgumentException("invalid position [" + position + "]. List size [" + stores.size() + "]")); + } else { + stores.get(position++).findTokensFor(accountId, this); + } + } + + @Override + public void onResponse(Collection response) { + result.addAll(response); + if (position == stores.size()) { + delegate.onResponse(List.copyOf(result)); + } else { + stores.get(position++).findTokensFor(accountId, this); + } + } + + @Override + public void onFailure(Exception e) { + delegate.onFailure(e); + } + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStore.java index ffa56efb606ff..e17f52b757f71 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStore.java @@ -12,14 +12,18 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.Nullable; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.util.Maps; import org.elasticsearch.env.Environment; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.watcher.FileWatcher; import org.elasticsearch.watcher.ResourceWatcherService; import org.elasticsearch.xpack.core.XPackPlugin; +import org.elasticsearch.xpack.core.security.action.service.TokenInfo; import org.elasticsearch.xpack.core.security.authc.support.Hasher; import org.elasticsearch.xpack.core.security.support.NoOpLogger; +import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; +import org.elasticsearch.xpack.security.authc.service.ServiceAccountToken; import org.elasticsearch.xpack.security.support.FileLineParser; import org.elasticsearch.xpack.security.support.FileReloadListener; import org.elasticsearch.xpack.security.support.SecurityFiles; @@ -27,12 +31,14 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Collection; import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.concurrent.CopyOnWriteArrayList; +import java.util.stream.Collectors; public class FileServiceAccountsTokenStore extends CachingServiceAccountsTokenStore { @@ -69,6 +75,17 @@ public void doAuthenticate(ServiceAccountToken token, ActionListener li .orElse(false)); } + @Override + public void findTokensFor(ServiceAccountId accountId, ActionListener> listener) { + final String principal = accountId.asPrincipal(); + final List tokenInfos = tokenHashes.keySet() + .stream() + .filter(k -> k.startsWith(principal)) + .map(k -> TokenInfo.fileToken(Strings.substring(k, principal.length() + 1, k.length()))) + .collect(Collectors.toUnmodifiableList()); + listener.onResponse(tokenInfos); + } + public void addListener(Runnable listener) { refreshListeners.add(listener); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java index 07abb3d3f22a2..498789422957f 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java @@ -21,6 +21,7 @@ import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.security.authc.support.Hasher; import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; +import org.elasticsearch.xpack.security.authc.service.ServiceAccountToken; import org.elasticsearch.xpack.security.support.FileAttributesChecker; import java.nio.file.Path; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStore.java new file mode 100644 index 0000000000000..1f57b3e3ed22d --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStore.java @@ -0,0 +1,196 @@ +/* + * 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.authc.service; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.Version; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.DocWriteRequest.OpType; +import org.elasticsearch.action.DocWriteResponse; +import org.elasticsearch.action.bulk.BulkAction; +import org.elasticsearch.action.bulk.BulkRequest; +import org.elasticsearch.action.bulk.TransportSingleItemBulkWriteAction; +import org.elasticsearch.action.get.GetAction; +import org.elasticsearch.action.get.GetRequest; +import org.elasticsearch.action.get.GetResponse; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.index.IndexResponse; +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.action.support.ContextPreservingActionListener; +import org.elasticsearch.client.Client; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.CharArrays; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.index.query.BoolQueryBuilder; +import org.elasticsearch.index.query.QueryBuilders; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.xpack.core.XPackSettings; +import org.elasticsearch.xpack.core.security.ScrollHelper; +import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenRequest; +import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenResponse; +import org.elasticsearch.xpack.core.security.action.service.TokenInfo; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.support.Hasher; +import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; +import org.elasticsearch.xpack.security.authc.service.ServiceAccountToken; +import org.elasticsearch.xpack.security.support.CacheInvalidatorRegistry; +import org.elasticsearch.xpack.security.support.SecurityIndexManager; + +import java.io.IOException; +import java.time.Clock; +import java.util.Arrays; +import java.util.Collection; + +import static org.elasticsearch.action.bulk.TransportSingleItemBulkWriteAction.toSingleItemBulkRequest; +import static org.elasticsearch.search.SearchService.DEFAULT_KEEPALIVE_SETTING; +import static org.elasticsearch.xpack.core.ClientHelper.SECURITY_ORIGIN; +import static org.elasticsearch.xpack.core.ClientHelper.executeAsyncWithOrigin; +import static org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames.SECURITY_MAIN_ALIAS; + +public class IndexServiceAccountsTokenStore extends CachingServiceAccountsTokenStore { + + private static final Logger logger = LogManager.getLogger(IndexServiceAccountsTokenStore.class); + private static final String SERVICE_ACCOUNT_TOKEN_DOC_TYPE = "service_account_token"; + + private final Clock clock; + private final Client client; + private final SecurityIndexManager securityIndex; + private final ClusterService clusterService; + private final Hasher hasher; + + public IndexServiceAccountsTokenStore(Settings settings, ThreadPool threadPool, Clock clock, Client client, + SecurityIndexManager securityIndex, ClusterService clusterService, + CacheInvalidatorRegistry cacheInvalidatorRegistry) { + super(settings, threadPool); + this.clock = clock; + this.client = client; + this.securityIndex = securityIndex; + this.clusterService = clusterService; + cacheInvalidatorRegistry.registerCacheInvalidator("index_service_account_token", this); + this.hasher = Hasher.resolve(XPackSettings.SERVICE_TOKEN_HASHING_ALGORITHM.get(settings)); + } + + @Override + void doAuthenticate(ServiceAccountToken token, ActionListener listener) { + final GetRequest getRequest = client + .prepareGet(SECURITY_MAIN_ALIAS, docIdForToken(token)) + .setFetchSource(true) + .request(); + securityIndex.checkIndexVersionThenExecute(listener::onFailure, () -> + executeAsyncWithOrigin(client, SECURITY_ORIGIN, GetAction.INSTANCE, getRequest, ActionListener.wrap(response -> { + if (response.isExists()) { + final String tokenHash = (String) response.getSource().get("password"); + assert tokenHash != null : "service account token hash cannot be null"; + listener.onResponse(Hasher.verifyHash(token.getSecret(), tokenHash.toCharArray())); + } else { + logger.trace("service account token [{}] not found in index", token.getQualifiedName()); + listener.onResponse(false); + }}, listener::onFailure))); + } + + public void createToken(Authentication authentication, CreateServiceAccountTokenRequest request, + ActionListener listener) { + final ServiceAccountId accountId = new ServiceAccountId(request.getNamespace(), request.getServiceName()); + if (false == ServiceAccountService.isServiceAccountPrincipal(accountId.asPrincipal())) { + listener.onFailure(new IllegalArgumentException("service account [" + accountId + "] does not exist")); + return; + } + final ServiceAccountToken token = ServiceAccountToken.newToken(accountId, request.getTokenName()); + try (XContentBuilder builder = newDocument(authentication, token)) { + final IndexRequest indexRequest = + client.prepareIndex(SECURITY_MAIN_ALIAS) + .setId(docIdForToken(token)) + .setSource(builder) + .setOpType(OpType.CREATE) + .setRefreshPolicy(request.getRefreshPolicy()) + .request(); + final BulkRequest bulkRequest = toSingleItemBulkRequest(indexRequest); + + securityIndex.prepareIndexIfNeededThenExecute(listener::onFailure, () -> { + executeAsyncWithOrigin(client, SECURITY_ORIGIN, BulkAction.INSTANCE, bulkRequest, + TransportSingleItemBulkWriteAction.wrapBulkResponse(ActionListener.wrap(response -> { + if (DocWriteResponse.Result.CREATED == response.getResult()) { + listener.onResponse(CreateServiceAccountTokenResponse.created( + token.getTokenName(), token.asBearerString())); + } else { + listener.onResponse(CreateServiceAccountTokenResponse.notCreated()); + } + }, listener::onFailure))); + }); + } catch (IOException e) { + listener.onFailure(e); + } + } + + @Override + public void findTokensFor(ServiceAccountId accountId, ActionListener> listener) { + // TODO: wildcard support? + final BoolQueryBuilder query = QueryBuilders.boolQuery() + .filter(QueryBuilders.termQuery("doc_type", SERVICE_ACCOUNT_TOKEN_DOC_TYPE)) + .must(QueryBuilders.prefixQuery("name", accountId.asPrincipal())); + final SearchRequest request = client.prepareSearch(SECURITY_MAIN_ALIAS) + .setScroll(DEFAULT_KEEPALIVE_SETTING.get(getSettings())) + .setQuery(query) + .setSize(1000) + .setFetchSource(false) + .request(); + request.indicesOptions().ignoreUnavailable(); + + logger.trace("Searching tokens for service account [{}]", accountId); + ScrollHelper.fetchAllByEntity(client, request, + new ContextPreservingActionListener<>(client.threadPool().getThreadContext().newRestorableContext(false), listener), + hit -> extractTokenInfo(hit.getId(), accountId)); + } + + private String docIdForToken(ServiceAccountToken token) { + return SERVICE_ACCOUNT_TOKEN_DOC_TYPE + "-" + token.getQualifiedName(); + } + + private XContentBuilder newDocument(Authentication authentication, ServiceAccountToken serviceAccountToken) throws IOException { + final Version version = clusterService.state().nodes().getMinNodeVersion(); + + XContentBuilder builder = XContentFactory.jsonBuilder(); + builder.startObject() + .field("doc_type", SERVICE_ACCOUNT_TOKEN_DOC_TYPE) + .field("version", version.id) + .field("name", serviceAccountToken.getQualifiedName()) + .field("creation_time", clock.instant().toEpochMilli()) + .field("enabled", true) + .startObject("creator") + .field("principal", authentication.getUser().principal()) + .field("full_name", authentication.getUser().fullName()) + .field("email", authentication.getUser().email()) + .field("metadata", authentication.getUser().metadata()) + .field("realm", authentication.getSourceRealm().getName()) + .field("realm_type", authentication.getSourceRealm().getType()) + .endObject(); + + byte[] utf8Bytes = null; + final char[] tokenHash = hasher.hash(serviceAccountToken.getSecret()); + try { + utf8Bytes = CharArrays.toUtf8Bytes(tokenHash); + builder.field("password").utf8Value(utf8Bytes, 0, utf8Bytes.length); + } finally { + if (utf8Bytes != null) { + Arrays.fill(utf8Bytes, (byte) 0); + } + Arrays.fill(tokenHash, (char) 0); + } + builder.endObject(); + return builder; + } + + private TokenInfo extractTokenInfo(String docId, ServiceAccountId accountId) { + final String prefix = SERVICE_ACCOUNT_TOKEN_DOC_TYPE + "-" + accountId.asPrincipal() + "/"; + return TokenInfo.indexToken(Strings.substring(docId, prefix.length(), docId.length())); + } +} 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 c5373fde6626a..513df85b153db 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 @@ -9,14 +9,21 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.xpack.core.XPackSettings; +import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountTokensResponse; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.security.authc.service.ServiceAccount; +import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; +import org.elasticsearch.xpack.security.authc.service.ServiceAccountToken; import java.util.Collection; import java.util.Map; @@ -31,9 +38,13 @@ public class ServiceAccountService { private static final Logger logger = LogManager.getLogger(ServiceAccountService.class); private final ServiceAccountsTokenStore serviceAccountsTokenStore; + private final boolean httpTlsEnabled; + private final boolean transportTlsEnabled; - public ServiceAccountService(ServiceAccountsTokenStore serviceAccountsTokenStore) { + public ServiceAccountService(Settings settings, ServiceAccountsTokenStore serviceAccountsTokenStore) { this.serviceAccountsTokenStore = serviceAccountsTokenStore; + this.httpTlsEnabled = XPackSettings.HTTP_SSL_ENABLED.get(settings); + this.transportTlsEnabled = XPackSettings.TRANSPORT_SSL_ENABLED.get(settings); } public static boolean isServiceAccount(Authentication authentication) { @@ -74,8 +85,22 @@ public static ServiceAccountToken tryParseToken(SecureString bearerString) { } } + public void findTokensFor(ServiceAccountId accountId, String nodeName, ActionListener listener) { + serviceAccountsTokenStore.findTokensFor(accountId, ActionListener.wrap(tokenInfos -> { + listener.onResponse(new GetServiceAccountTokensResponse(accountId.asPrincipal(), nodeName, tokenInfos)); + }, listener::onFailure)); + } + public void authenticateToken(ServiceAccountToken serviceAccountToken, String nodeName, ActionListener listener) { logger.trace("attempt to authenticate service account token [{}]", serviceAccountToken.getQualifiedName()); + if (false == httpTlsEnabled || false == transportTlsEnabled) { + final ParameterizedMessage message = new ParameterizedMessage( + "Service account authentication requires TLS for both HTTP and Transport, " + + "but got HTTP TLS: [{}] and Transport TLS: [{}]", httpTlsEnabled, transportTlsEnabled); + logger.debug(message); + listener.onFailure(new ElasticsearchSecurityException(message.getFormattedMessage(), RestStatus.UNAUTHORIZED)); + return; + } if (ElasticServiceAccounts.NAMESPACE.equals(serviceAccountToken.getAccountId().namespace()) == false) { logger.debug("only [{}] service accounts are supported, but received [{}]", ElasticServiceAccounts.NAMESPACE, serviceAccountToken.getAccountId().asPrincipal()); @@ -103,7 +128,14 @@ public void authenticateToken(ServiceAccountToken serviceAccountToken, String no public void getRoleDescriptor(Authentication authentication, ActionListener listener) { assert isServiceAccount(authentication) : "authentication is not for service account: " + authentication; - + if (false == httpTlsEnabled || false == transportTlsEnabled) { + final ParameterizedMessage message = new ParameterizedMessage( + "Service account role descriptor resolving requires TLS for both HTTP and Transport, " + + "but got HTTP TLS: [{}] and Transport TLS: [{}]", httpTlsEnabled, transportTlsEnabled); + logger.debug(message); + listener.onFailure(new ElasticsearchSecurityException(message.getFormattedMessage(), RestStatus.UNAUTHORIZED)); + return; + } final String principal = authentication.getUser().principal(); final ServiceAccount account = ACCOUNTS.get(principal); if (account == null) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountsTokenStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountsTokenStore.java index 4252cfed8abec..c830a38769852 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountsTokenStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountsTokenStore.java @@ -7,15 +7,12 @@ package org.elasticsearch.xpack.security.authc.service; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.message.ParameterizedMessage; import org.elasticsearch.action.ActionListener; -import org.elasticsearch.common.util.concurrent.ThreadContext; -import org.elasticsearch.xpack.core.common.IteratingActionListener; +import org.elasticsearch.xpack.core.security.action.service.TokenInfo; +import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; +import org.elasticsearch.xpack.security.authc.service.ServiceAccountToken; -import java.util.List; -import java.util.function.Function; +import java.util.Collection; /** * The interface should be implemented by credential stores of different backends. @@ -27,36 +24,6 @@ public interface ServiceAccountsTokenStore { */ void authenticate(ServiceAccountToken token, ActionListener listener); - final class CompositeServiceAccountsTokenStore implements ServiceAccountsTokenStore { - - private static final Logger logger = LogManager.getLogger(CompositeServiceAccountsTokenStore.class); - - private final ThreadContext threadContext; - private final List stores; - - public CompositeServiceAccountsTokenStore( - List stores, ThreadContext threadContext) { - this.stores = stores; - this.threadContext = threadContext; - } - - @Override - public void authenticate(ServiceAccountToken token, ActionListener listener) { - final IteratingActionListener authenticatingListener = - new IteratingActionListener<>( - listener, - (store, successListener) -> store.authenticate(token, successListener), - stores, - threadContext, - Function.identity(), - success -> Boolean.FALSE == success); - try { - authenticatingListener.run(); - } catch (Exception e) { - logger.debug(new ParameterizedMessage("authentication of service token [{}] failed", token.getQualifiedName()), e); - listener.onFailure(e); - } - } - } + void findTokensFor(ServiceAccountId accountId, ActionListener> listener); } 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 08bbddcb4874d..c0a7dfcfea8c1 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 @@ -230,42 +230,10 @@ public void getRoles(User user, Authentication authentication, ActionListener { - if (role == Role.EMPTY) { - buildAndCacheRoleForApiKey(authentication, true, roleActionListener); - } else { - buildAndCacheRoleForApiKey(authentication, true, ActionListener.wrap( - limitedByRole -> roleActionListener.onResponse( - limitedByRole == Role.EMPTY ? role : LimitedRole.createLimitedRole(role, limitedByRole)), - roleActionListener::onFailure - )); - } - }, - roleActionListener::onFailure - )); - } else { - apiKeyService.getRoleForApiKey(authentication, ActionListener.wrap(apiKeyRoleDescriptors -> { - final List descriptors = apiKeyRoleDescriptors.getRoleDescriptors(); - if (descriptors == null) { - roleActionListener.onFailure(new IllegalStateException("missing role descriptors")); - } else if (apiKeyRoleDescriptors.getLimitedByRoleDescriptors() == null) { - buildAndCacheRoleFromDescriptors(descriptors, - apiKeyRoleDescriptors.getApiKeyId() + "_role_desc", roleActionListener); - } else { - buildAndCacheRoleFromDescriptors(descriptors, apiKeyRoleDescriptors.getApiKeyId() + "_role_desc", - ActionListener.wrap( - role -> buildAndCacheRoleFromDescriptors(apiKeyRoleDescriptors.getLimitedByRoleDescriptors(), - apiKeyRoleDescriptors.getApiKeyId() + "_limited_role_desc", ActionListener.wrap( - limitedBy -> roleActionListener.onResponse(LimitedRole.createLimitedRole(role, limitedBy)), - roleActionListener::onFailure)), roleActionListener::onFailure)); - } - }, roleActionListener::onFailure)); - } - + if (ServiceAccountService.isServiceAccount(authentication)) { + getRolesForServiceAccount(authentication, roleActionListener); + } else if (ApiKeyService.isApiKeyAuthentication(authentication)) { + getRolesForApiKey(authentication, roleActionListener); } else { Set roleNames = new HashSet<>(Arrays.asList(user.roles())); if (isAnonymousEnabled && anonymousUser.equals(user) == false) { @@ -285,6 +253,55 @@ public void getRoles(User user, Authentication authentication, ActionListener roleActionListener) { + serviceAccountService.getRoleDescriptor(authentication, ActionListener.wrap(roleDescriptor -> { + final RoleKey roleKey = new RoleKey(Set.of(roleDescriptor.getName()), "service_account"); + final Role existing = roleCache.get(roleKey); + if (existing == null) { + final long invalidationCounter = numInvalidation.get(); + buildThenMaybeCacheRole(roleKey, List.of(roleDescriptor), Set.of(), true, invalidationCounter, roleActionListener); + } else { + roleActionListener.onResponse(existing); + } + }, roleActionListener::onFailure)); + } + + private void getRolesForApiKey(Authentication authentication, ActionListener roleActionListener) { + if (authentication.getVersion().onOrAfter(VERSION_API_KEY_ROLES_AS_BYTES)) { + buildAndCacheRoleForApiKey(authentication, false, ActionListener.wrap( + role -> { + if (role == Role.EMPTY) { + buildAndCacheRoleForApiKey(authentication, true, roleActionListener); + } else { + buildAndCacheRoleForApiKey(authentication, true, ActionListener.wrap( + limitedByRole -> roleActionListener.onResponse( + limitedByRole == Role.EMPTY ? role : LimitedRole.createLimitedRole(role, limitedByRole)), + roleActionListener::onFailure + )); + } + }, + roleActionListener::onFailure + )); + } else { + apiKeyService.getRoleForApiKey(authentication, ActionListener.wrap(apiKeyRoleDescriptors -> { + final List descriptors = apiKeyRoleDescriptors.getRoleDescriptors(); + if (descriptors == null) { + roleActionListener.onFailure(new IllegalStateException("missing role descriptors")); + } else if (apiKeyRoleDescriptors.getLimitedByRoleDescriptors() == null) { + buildAndCacheRoleFromDescriptors(descriptors, + apiKeyRoleDescriptors.getApiKeyId() + "_role_desc", roleActionListener); + } else { + buildAndCacheRoleFromDescriptors(descriptors, apiKeyRoleDescriptors.getApiKeyId() + "_role_desc", + ActionListener.wrap( + role -> buildAndCacheRoleFromDescriptors(apiKeyRoleDescriptors.getLimitedByRoleDescriptors(), + apiKeyRoleDescriptors.getApiKeyId() + "_limited_role_desc", ActionListener.wrap( + limitedBy -> roleActionListener.onResponse(LimitedRole.createLimitedRole(role, limitedBy)), + roleActionListener::onFailure)), roleActionListener::onFailure)); + } + }, roleActionListener::onFailure)); + } + } + public void buildAndCacheRoleFromDescriptors(Collection roleDescriptors, String source, ActionListener listener) { if (ROLES_STORE_SOURCE.equals(source)) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/service/RestCreateServiceAccountTokenAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/service/RestCreateServiceAccountTokenAction.java new file mode 100644 index 0000000000000..73c93e9769fc6 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/service/RestCreateServiceAccountTokenAction.java @@ -0,0 +1,62 @@ +/* + * 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.action.support.WriteRequest; +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.Strings; +import org.elasticsearch.common.UUIDs; +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.CreateServiceAccountTokenAction; +import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenRequest; +import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler; + +import java.io.IOException; +import java.util.List; + +import static org.elasticsearch.rest.RestRequest.Method.POST; + +public class RestCreateServiceAccountTokenAction extends SecurityBaseRestHandler { + + public RestCreateServiceAccountTokenAction(Settings settings, XPackLicenseState licenseState) { + super(settings, licenseState); + } + + @Override + public List routes() { + return List.of( + new Route(POST, "/_security/service/{namespace}/{service}/credential/token/{name}"), + new Route(POST, "/_security/service/{namespace}/{service}/credential/token")); + } + + @Override + public String getName() { + return "xpack_security_create_service_account_token"; + } + + @Override + protected RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException { + String tokenName = request.param("name"); + if (Strings.isNullOrEmpty(tokenName)) { + tokenName = UUIDs.base64UUID(); + } + final CreateServiceAccountTokenRequest createServiceAccountTokenRequest = new CreateServiceAccountTokenRequest( + request.param("namespace"), request.param("service"), tokenName); + final String refreshPolicy = request.param("refresh"); + if (refreshPolicy != null) { + createServiceAccountTokenRequest.setRefreshPolicy(WriteRequest.RefreshPolicy.parse(refreshPolicy)); + } + + return channel -> client.execute(CreateServiceAccountTokenAction.INSTANCE, + createServiceAccountTokenRequest, + new RestToXContentListener<>(channel)); + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/service/RestGetServiceAccountTokensAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/service/RestGetServiceAccountTokensAction.java new file mode 100644 index 0000000000000..9a03df4640e2a --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/service/RestGetServiceAccountTokensAction.java @@ -0,0 +1,50 @@ +/* + * 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.GetServiceAccountTokensAction; +import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountTokensRequest; +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 RestGetServiceAccountTokensAction extends SecurityBaseRestHandler { + + public RestGetServiceAccountTokensAction(Settings settings, XPackLicenseState licenseState) { + super(settings, licenseState); + } + + @Override + public List routes() { + return List.of( + new Route(GET, "/_security/service/{namespace}/{service}/credential") + ); + } + + @Override + public String getName() { + return "xpack_security_get_service_account_tokens"; + } + + @Override + protected RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException { + final GetServiceAccountTokensRequest getServiceAccountTokensRequest = + new GetServiceAccountTokensRequest(request.param("namespace"), request.param("service")); + return channel -> client.execute(GetServiceAccountTokensAction.INSTANCE, + getServiceAccountTokensRequest, + new RestToXContentListener<>(channel)); + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecurityIndexManager.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecurityIndexManager.java index e74f62937a9a9..98cd4b926c8f2 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecurityIndexManager.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecurityIndexManager.java @@ -347,7 +347,11 @@ public void prepareIndexIfNeededThenExecute(final Consumer consumer, @Override public void onResponse(CreateIndexResponse createIndexResponse) { if (createIndexResponse.isAcknowledged()) { - andThen.run(); + try { + andThen.run(); + } catch (Exception e2) { + consumer.accept(e2); + } } else { consumer.accept(new ElasticsearchException("Failed to create security index")); } @@ -359,7 +363,11 @@ public void onFailure(Exception e) { if (cause instanceof ResourceAlreadyExistsException) { // the index already exists - it was probably just created so this // node hasn't yet received the cluster state update with the index - andThen.run(); + try { + andThen.run(); + } catch (Exception e2) { + consumer.accept(e2); + } } else { consumer.accept(e); } @@ -382,14 +390,22 @@ public void onFailure(Exception e) { executeAsyncWithOrigin(client.threadPool().getThreadContext(), systemIndexDescriptor.getOrigin(), request, ActionListener.wrap(putMappingResponse -> { if (putMappingResponse.isAcknowledged()) { - andThen.run(); + try { + andThen.run(); + } catch (Exception e2) { + consumer.accept(e2); + } } else { consumer.accept(new IllegalStateException("put mapping request was not acknowledged")); } }, consumer), client.admin().indices()::putMapping); } } else { - andThen.run(); + try { + andThen.run(); + } catch (Exception e2) { + consumer.accept(e2); + } } } catch (Exception e) { consumer.accept(e); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportCreateServiceAccountTokenActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportCreateServiceAccountTokenActionTests.java new file mode 100644 index 0000000000000..07ad368845452 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportCreateServiceAccountTokenActionTests.java @@ -0,0 +1,86 @@ +/* + * 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.ElasticsearchSecurityException; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.security.SecurityContext; +import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenRequest; +import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenResponse; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.security.authc.service.IndexServiceAccountsTokenStore; +import org.junit.Before; + +import java.util.Collections; +import java.util.concurrent.ExecutionException; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class TransportCreateServiceAccountTokenActionTests extends ESTestCase { + + private IndexServiceAccountsTokenStore indexServiceAccountsTokenStore; + private SecurityContext securityContext; + private TransportCreateServiceAccountTokenAction transportCreateServiceAccountTokenAction; + + @Before + public void init() { + indexServiceAccountsTokenStore = mock(IndexServiceAccountsTokenStore.class); + securityContext = mock(SecurityContext.class); + final Settings settings = Settings.builder() + .put("xpack.security.http.ssl.enabled", true) + .put("xpack.security.transport.ssl.enabled", true) + .build(); + transportCreateServiceAccountTokenAction = new TransportCreateServiceAccountTokenAction( + mock(TransportService.class), new ActionFilters(Collections.emptySet()), + settings, indexServiceAccountsTokenStore, securityContext); + } + + public void testAuthenticationIsRequired() { + when(securityContext.getAuthentication()).thenReturn(null); + final PlainActionFuture future = new PlainActionFuture<>(); + transportCreateServiceAccountTokenAction.doExecute(mock(Task.class), mock(CreateServiceAccountTokenRequest.class), future); + final ExecutionException e = expectThrows(ExecutionException.class, () -> future.get()); + assertThat(e.getCause().getClass(), is(IllegalStateException.class)); + assertThat(e.getCause().getMessage(), containsString("authentication is required")); + } + + public void testExecutionWillDelegate() { + final Authentication authentication = mock(Authentication.class); + when(securityContext.getAuthentication()).thenReturn(authentication); + final CreateServiceAccountTokenRequest request = mock(CreateServiceAccountTokenRequest.class); + final PlainActionFuture future = new PlainActionFuture<>(); + transportCreateServiceAccountTokenAction.doExecute(mock(Task.class), request, future); + verify(indexServiceAccountsTokenStore).createToken(authentication, request, future); + } + + public void testTlsRequired() { + final boolean httpTls = randomBoolean(); + final Settings settings = Settings.builder() + .put("xpack.security.http.ssl.enabled", httpTls) + .put("xpack.security.transport.ssl.enabled", randomFrom(false == httpTls, false)) + .build(); + TransportCreateServiceAccountTokenAction action = new TransportCreateServiceAccountTokenAction( + mock(TransportService.class), new ActionFilters(Collections.emptySet()), + settings, indexServiceAccountsTokenStore, securityContext); + + final PlainActionFuture future = new PlainActionFuture<>(); + action.doExecute(mock(Task.class), mock(CreateServiceAccountTokenRequest.class), future); + final ExecutionException e = expectThrows(ExecutionException.class, () -> future.get()); + assertThat(e.getCause().getClass(), is(ElasticsearchSecurityException.class)); + assertThat(e.getMessage(), containsString("Service account APIs require TLS for both HTTP and Transport")); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStoreTests.java index ad2adea1cfcf5..922b240848238 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStoreTests.java @@ -16,10 +16,13 @@ import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.xpack.core.security.action.service.TokenInfo; import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; +import org.elasticsearch.xpack.security.authc.service.ServiceAccountToken; import org.junit.After; import org.junit.Before; +import java.util.Collection; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicBoolean; @@ -63,6 +66,11 @@ void doAuthenticate(ServiceAccountToken token, ActionListener listener) doAuthenticateInvoked.set(true); listener.onResponse(validSecret.equals(token.getSecret())); } + + @Override + public void findTokensFor(ServiceAccountId accountId, ActionListener> listener) { + listener.onFailure(new UnsupportedOperationException()); + } }; final Cache> cache = store.getCache(); @@ -140,6 +148,11 @@ public void testCacheCanBeDisabled() throws ExecutionException, InterruptedExcep void doAuthenticate(ServiceAccountToken token, ActionListener listener) { listener.onResponse(success); } + + @Override + public void findTokensFor(ServiceAccountId accountId, ActionListener> listener) { + listener.onFailure(new UnsupportedOperationException()); + } }; assertThat(store.getCache(), nullValue()); // authenticate should still work diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountsTokenStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountsTokenStoreTests.java index 789fe4d04e6e0..b1a30a1bfcbda 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountsTokenStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountsTokenStoreTests.java @@ -66,8 +66,8 @@ public void testAuthenticate() throws ExecutionException, InterruptedException { return null; }).when(store3).authenticate(eq(token), any()); - final ServiceAccountsTokenStore.CompositeServiceAccountsTokenStore compositeStore = - new ServiceAccountsTokenStore.CompositeServiceAccountsTokenStore(List.of(store1, store2, store3), threadContext); + final CompositeServiceAccountsTokenStore compositeStore = + new CompositeServiceAccountsTokenStore(List.of(store1, store2, store3), threadContext); final PlainActionFuture future = new PlainActionFuture<>(); compositeStore.authenticate(token, future); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStoreTests.java new file mode 100644 index 0000000000000..241e2823ebb10 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStoreTests.java @@ -0,0 +1,248 @@ +/* + * 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.authc.service; + +import org.elasticsearch.Version; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.action.ActionType; +import org.elasticsearch.action.DocWriteRequest.OpType; +import org.elasticsearch.action.bulk.BulkItemResponse; +import org.elasticsearch.action.bulk.BulkRequest; +import org.elasticsearch.action.bulk.BulkResponse; +import org.elasticsearch.action.get.GetRequest; +import org.elasticsearch.action.get.GetResponse; +import org.elasticsearch.action.index.IndexRequest; +import org.elasticsearch.action.index.IndexResponse; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.client.Client; +import org.elasticsearch.client.FilterClient; +import org.elasticsearch.cluster.ClusterState; +import org.elasticsearch.cluster.node.DiscoveryNodes; +import org.elasticsearch.cluster.service.ClusterService; +import org.elasticsearch.common.CharArrays; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.index.get.GetResult; +import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.XContentTestUtils; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenRequest; +import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenResponse; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.authc.support.Hasher; +import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; +import org.elasticsearch.xpack.security.authc.service.ServiceAccountToken; +import org.elasticsearch.xpack.security.support.CacheInvalidatorRegistry; +import org.elasticsearch.xpack.security.support.SecurityIndexManager; +import org.junit.Before; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.time.Clock; +import java.util.Map; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Consumer; + +import static org.elasticsearch.index.seqno.SequenceNumbers.UNASSIGNED_PRIMARY_TERM; +import static org.elasticsearch.index.seqno.SequenceNumbers.UNASSIGNED_SEQ_NO; +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class IndexServiceAccountsTokenStoreTests extends ESTestCase { + + private Client client; + private ClusterService clusterService; + private CacheInvalidatorRegistry cacheInvalidatorRegistry; + private IndexServiceAccountsTokenStore store; + private final AtomicReference requestHolder = new AtomicReference<>(); + private final AtomicReference>> responseProviderHolder = new AtomicReference<>(); + + @Before + public void init() { + Client mockClient = mock(Client.class); + when(mockClient.settings()).thenReturn(Settings.EMPTY); + ThreadPool threadPool = mock(ThreadPool.class); + when(mockClient.threadPool()).thenReturn(threadPool); + when(threadPool.getThreadContext()).thenReturn(new ThreadContext(Settings.EMPTY)); + client = new FilterClient(mockClient) { + @Override + protected + void doExecute(ActionType action, Request request, ActionListener listener) { + requestHolder.set(request); + responseProviderHolder.get().accept((ActionListener) listener); + } + }; + clusterService = mock(ClusterService.class); + final ClusterState clusterState = mock(ClusterState.class); + final DiscoveryNodes discoveryNodes = mock(DiscoveryNodes.class); + when(discoveryNodes.getMinNodeVersion()).thenReturn(Version.CURRENT); + when(clusterState.nodes()).thenReturn(discoveryNodes); + when(clusterService.state()).thenReturn(clusterState); + cacheInvalidatorRegistry = mock(CacheInvalidatorRegistry.class); + + SecurityIndexManager securityIndex = mock(SecurityIndexManager.class); + when(securityIndex.isAvailable()).thenReturn(true); + when(securityIndex.indexExists()).thenReturn(true); + when(securityIndex.isIndexUpToDate()).thenReturn(true); + when(securityIndex.freeze()).thenReturn(securityIndex); + doAnswer((i) -> { + Runnable action = (Runnable) i.getArguments()[1]; + action.run(); + return null; + }).when(securityIndex).prepareIndexIfNeededThenExecute(any(Consumer.class), any(Runnable.class)); + doAnswer((i) -> { + Runnable action = (Runnable) i.getArguments()[1]; + action.run(); + return null; + }).when(securityIndex).checkIndexVersionThenExecute(any(Consumer.class), any(Runnable.class)); + store = new IndexServiceAccountsTokenStore(Settings.EMPTY, + threadPool, + Clock.systemUTC(), + client, + securityIndex, + clusterService, + cacheInvalidatorRegistry); + } + + public void testDoAuthenticate() throws IOException, ExecutionException, InterruptedException, IllegalAccessException { + final ServiceAccountId accountId = new ServiceAccountId(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8)); + final ServiceAccountToken serviceAccountToken = ServiceAccountToken.newToken(accountId, randomAlphaOfLengthBetween(3, 8)); + final GetResponse getResponse1 = createGetResponse(serviceAccountToken, true); + + // success + responseProviderHolder.set(l -> l.onResponse(getResponse1)); + final PlainActionFuture future1 = new PlainActionFuture<>(); + store.doAuthenticate(serviceAccountToken, future1); + final GetRequest getRequest = (GetRequest) requestHolder.get(); + assertThat(getRequest.id(), equalTo("service_account_token-" + serviceAccountToken.getQualifiedName())); + assertThat(future1.get(), is(true)); + + // token mismatch + final GetResponse getResponse2 = createGetResponse(ServiceAccountToken.newToken(accountId, randomAlphaOfLengthBetween(3, 8)), true); + responseProviderHolder.set(l -> l.onResponse(getResponse2)); + final PlainActionFuture future2 = new PlainActionFuture<>(); + store.doAuthenticate(serviceAccountToken, future2); + assertThat(future2.get(), is(false)); + + // token document not found + final GetResponse getResponse3 = createGetResponse(serviceAccountToken, false); + responseProviderHolder.set(l -> l.onResponse(getResponse3)); + final PlainActionFuture future3 = new PlainActionFuture<>(); + store.doAuthenticate(serviceAccountToken, future3); + assertThat(future3.get(), is(false)); + } + + public void testCreateToken() throws ExecutionException, InterruptedException { + final Authentication authentication = createAuthentication(); + final CreateServiceAccountTokenRequest request = + new CreateServiceAccountTokenRequest("elastic", "fleet", randomAlphaOfLengthBetween(3, 8)); + + // created + responseProviderHolder.set(l -> l.onResponse(createSingleBulkResponse(true))); + final PlainActionFuture future1 = new PlainActionFuture<>(); + store.createToken(authentication, request, future1); + final BulkRequest bulkRequest = (BulkRequest) requestHolder.get(); + assertThat(bulkRequest.requests().size(), equalTo(1)); + final IndexRequest indexRequest = (IndexRequest) bulkRequest.requests().get(0); + final Map sourceMap = indexRequest.sourceAsMap(); + assertThat(sourceMap.get("name"), equalTo("elastic/fleet/" + request.getTokenName())); + assertThat(sourceMap.get("doc_type"), equalTo("service_account_token")); + assertThat(sourceMap.get("version"), equalTo(Version.CURRENT.id)); + assertThat(sourceMap.get("password"), notNullValue()); + assertThat(Hasher.resolveFromHash(((String) sourceMap.get("password")).toCharArray()), equalTo(Hasher.PBKDF2_STRETCH)); + assertThat(sourceMap.get("creation_time"), notNullValue()); + @SuppressWarnings("unchecked") + final Map creatorMap = (Map) sourceMap.get("creator"); + assertThat(creatorMap, notNullValue()); + assertThat(creatorMap.get("principal"), equalTo(authentication.getUser().principal())); + assertThat(creatorMap.get("full_name"), equalTo(authentication.getUser().fullName())); + assertThat(creatorMap.get("email"), equalTo(authentication.getUser().email())); + assertThat(creatorMap.get("metadata"), equalTo(authentication.getUser().metadata())); + assertThat(creatorMap.get("realm"), equalTo(authentication.getSourceRealm().getName())); + assertThat(creatorMap.get("realm_type"), equalTo(authentication.getSourceRealm().getType())); + + final CreateServiceAccountTokenResponse createServiceAccountTokenResponse1 = future1.get(); + assertNotNull(createServiceAccountTokenResponse1); + assertThat(createServiceAccountTokenResponse1.isCreated(), is(true)); + assertThat(createServiceAccountTokenResponse1.getName(), equalTo(request.getTokenName())); + assertNotNull(createServiceAccountTokenResponse1.getValue()); + + // not created + responseProviderHolder.set(l -> l.onResponse(createSingleBulkResponse(false))); + final PlainActionFuture future2 = new PlainActionFuture<>(); + store.createToken(authentication, request, future2); + final CreateServiceAccountTokenResponse createServiceAccountTokenResponse2 = future2.get(); + assertNotNull(createServiceAccountTokenResponse2); + assertThat(createServiceAccountTokenResponse2.isCreated(), is(false)); + assertNull(createServiceAccountTokenResponse2.getName()); + assertNull(createServiceAccountTokenResponse2.getValue()); + + // failure + final Exception exception = mock(Exception.class); + responseProviderHolder.set(l -> l.onFailure(exception)); + final PlainActionFuture future3 = new PlainActionFuture<>(); + store.createToken(authentication, request, future3); + final ExecutionException e3 = expectThrows(ExecutionException.class, () -> future3.get()); + assertThat(e3.getCause(), is(exception)); + } + + public void testCreateTokenWillFailForInvalidServiceAccount() { + final Authentication authentication = createAuthentication(); + final CreateServiceAccountTokenRequest request = randomValueOtherThanMany( + r -> "elastic".equals(r.getNamespace()) && "fleet".equals(r.getServiceName()), + () -> new CreateServiceAccountTokenRequest(randomAlphaOfLengthBetween(3, 8), + randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8))); + final PlainActionFuture future = new PlainActionFuture<>(); + store.createToken(authentication, request, future); + final ExecutionException e = expectThrows(ExecutionException.class, () -> future.get()); + assertThat(e.getCause().getClass(), is(IllegalArgumentException.class)); + assertThat(e.getMessage(), + containsString("service account [" + request.getNamespace() + "/" + request.getServiceName() + "] does not exist")); + } + + private GetResponse createGetResponse(ServiceAccountToken serviceAccountToken, boolean exists) throws IOException { + final char[] hash = Hasher.PBKDF2_STRETCH.hash(serviceAccountToken.getSecret()); + final Map documentMap = Map.of("password", new String(CharArrays.toUtf8Bytes(hash), StandardCharsets.UTF_8)); + return new GetResponse(new GetResult( + randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8), + exists ? randomLongBetween(0, Long.MAX_VALUE) : UNASSIGNED_SEQ_NO, + exists ? randomLongBetween(1, Long.MAX_VALUE) : UNASSIGNED_PRIMARY_TERM, randomLong(), exists, + XContentTestUtils.convertToXContent(documentMap, XContentType.JSON), + Map.of(), Map.of())); + } + + private Authentication createAuthentication() { + return new Authentication(new User(randomAlphaOfLengthBetween(3, 8)), + new Authentication.RealmRef(randomAlphaOfLengthBetween(3, 8), + randomAlphaOfLengthBetween(3, 8), + randomAlphaOfLengthBetween(3, 8)), + randomFrom(new Authentication.RealmRef(randomAlphaOfLengthBetween(3, 8), + randomAlphaOfLengthBetween(3, 8), + randomAlphaOfLengthBetween(3, 8)), null)); + } + + private BulkResponse createSingleBulkResponse(boolean created) { + return new BulkResponse(new BulkItemResponse[] { + new BulkItemResponse(randomInt(), OpType.CREATE, new IndexResponse( + mock(ShardId.class), randomAlphaOfLengthBetween(3, 8), randomLong(), randomLong(), randomLong(), created + )) + }, randomLong()); + } +} 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 e0c5a32e7e264..f66a721ebdb7f 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 @@ -25,6 +25,7 @@ import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; +import org.elasticsearch.xpack.security.authc.service.ServiceAccountToken; import org.junit.Before; import java.io.ByteArrayOutputStream; @@ -55,7 +56,11 @@ public class ServiceAccountServiceTests extends ESTestCase { public void init() { threadContext = new ThreadContext(Settings.EMPTY); serviceAccountsTokenStore = mock(ServiceAccountsTokenStore.class); - serviceAccountService = new ServiceAccountService(serviceAccountsTokenStore); + final Settings settings = Settings.builder() + .put("xpack.security.http.ssl.enabled", true) + .put("xpack.security.transport.ssl.enabled", true) + .build(); + serviceAccountService = new ServiceAccountService(settings, serviceAccountsTokenStore); } public void testIsServiceAccount() { @@ -402,6 +407,31 @@ ServiceAccountService.REALM_NAME, ServiceAccountService.REALM_TYPE, randomAlphaO "cannot load role for service account [" + username + "] - no such service account")); } + public void testTlsIsRequired() { + final boolean httpTls = randomBoolean(); + final Settings settings = Settings.builder() + .put("xpack.security.http.ssl.enabled", httpTls) + .put("xpack.security.transport.ssl.enabled", randomFrom(false == httpTls, false)) + .build(); + final ServiceAccountService service = new ServiceAccountService(settings, serviceAccountsTokenStore); + + final PlainActionFuture future1 = new PlainActionFuture<>(); + service.authenticateToken(mock(ServiceAccountToken.class), randomAlphaOfLengthBetween(3, 8), future1); + final ExecutionException e1 = expectThrows(ExecutionException.class, () -> future1.get()); + assertThat(e1.getCause().getClass(), is(ElasticsearchSecurityException.class)); + assertThat(e1.getMessage(), containsString("Service account authentication requires TLS for both HTTP and Transport")); + + final PlainActionFuture future2 = new PlainActionFuture<>(); + final Authentication authentication = new Authentication(mock(User.class), + new Authentication.RealmRef(ServiceAccountService.REALM_NAME, ServiceAccountService.REALM_TYPE, + randomAlphaOfLengthBetween(3, 8)), + null); + service.getRoleDescriptor(authentication, future2); + final ExecutionException e2 = expectThrows(ExecutionException.class, () -> future2.get()); + assertThat(e2.getCause().getClass(), is(ElasticsearchSecurityException.class)); + assertThat(e2.getMessage(), containsString("Service account role descriptor resolving requires TLS for both HTTP and Transport")); + } + private SecureString createBearerString(List bytesList) throws IOException { try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { for (byte[] bytes : bytesList) { diff --git a/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/11_builtin.yml b/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/11_builtin.yml index 4274291bc9262..90842d3c04cd0 100644 --- a/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/11_builtin.yml +++ b/x-pack/plugin/src/test/resources/rest-api-spec/test/privileges/11_builtin.yml @@ -15,5 +15,5 @@ setup: # This is fragile - it needs to be updated every time we add a new cluster/index privilege # I would much prefer we could just check that specific entries are in the array, but we don't have # an assertion for that - - length: { "cluster" : 40 } + - length: { "cluster" : 41 } - length: { "index" : 19 } From d989ae8a6a16756e8b820e68392092649be9b313 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Fri, 26 Mar 2021 15:06:31 +1100 Subject: [PATCH 02/14] [Test] Service Account - fix test assumption --- .../security/authc/service/ServiceAccountServiceTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 f66a721ebdb7f..f51770e4db7f2 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 @@ -166,7 +166,7 @@ public void testTryParseToken() throws IOException, IllegalAccessException { final SecureString bearerString4 = createBearerString(List.of( magicBytes, (randomAlphaOfLengthBetween(3, 8) + "/" + randomAlphaOfLengthBetween(3, 8) - + "/" + ServiceAccountTokenTests.randomInvalidTokenName() + + "/" + randomValueOtherThanMany(n -> n.contains("/"), ServiceAccountTokenTests::randomInvalidTokenName) + ":" + randomAlphaOfLengthBetween(10, 20)).getBytes(StandardCharsets.UTF_8) )); assertNull(ServiceAccountService.tryParseToken(bearerString4)); From f47a90ac2d69bad541fde2ffb5a8ef6e9a9d6a1f Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Fri, 26 Mar 2021 15:09:40 +1100 Subject: [PATCH 03/14] Update x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenAction.java Co-authored-by: Tim Vernum --- .../action/service/CreateServiceAccountTokenAction.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenAction.java index 598606e988351..4771196c04fde 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenAction.java @@ -11,7 +11,7 @@ public class CreateServiceAccountTokenAction extends ActionType { - public static final String NAME = "cluster:admin/xpack/security/service_account_token/create"; + public static final String NAME = "cluster:admin/xpack/security/service_account/token/create"; public static final CreateServiceAccountTokenAction INSTANCE = new CreateServiceAccountTokenAction(); private CreateServiceAccountTokenAction() { From ebd871a25690ac99f378ce83e312058dd9f43872 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Fri, 26 Mar 2021 16:16:30 +1100 Subject: [PATCH 04/14] Fix tests and remove legacy code --- .../CreateServiceAccountTokenResponse.java | 4 ---- .../GetServiceAccountTokensAction.java | 2 +- .../privilege/ClusterPrivilegeResolver.java | 2 +- ...reateServiceAccountTokenResponseTests.java | 21 ++++--------------- .../xpack/security/operator/Constants.java | 4 ++-- .../authc/service/ServiceAccountIT.java | 12 +++++++++++ .../IndexServiceAccountsTokenStore.java | 15 +++++++------ 7 files changed, 27 insertions(+), 33 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenResponse.java index b1ee09e0dccac..67cf2cfb1399a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenResponse.java @@ -91,8 +91,4 @@ public int hashCode() { public static CreateServiceAccountTokenResponse created(String name, SecureString value) { return new CreateServiceAccountTokenResponse(true, name, value); } - - public static CreateServiceAccountTokenResponse notCreated() { - return new CreateServiceAccountTokenResponse(false, null, null); - } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensAction.java index 4bd3498fdf2de..b914272b9700b 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensAction.java @@ -11,7 +11,7 @@ public class GetServiceAccountTokensAction extends ActionType { - public static final String NAME = "cluster:admin/xpack/security/service_account_token/get"; + public static final String NAME = "cluster:admin/xpack/security/service_account/token/get"; public static final GetServiceAccountTokensAction INSTANCE = new GetServiceAccountTokensAction(); public GetServiceAccountTokensAction() { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java index f85c9a9548d2d..829ad1b291539 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/privilege/ClusterPrivilegeResolver.java @@ -57,7 +57,7 @@ public class ClusterPrivilegeResolver { private static final Set MANAGE_OIDC_PATTERN = Set.of("cluster:admin/xpack/security/oidc/*"); private static final Set MANAGE_TOKEN_PATTERN = Set.of("cluster:admin/xpack/security/token/*"); private static final Set MANAGE_API_KEY_PATTERN = Set.of("cluster:admin/xpack/security/api_key/*"); - private static final Set MANAGE_SERVICE_ACCOUNT_PATTERN = Set.of("cluster:admin/xpack/security/service_account_token/*"); + private static final Set MANAGE_SERVICE_ACCOUNT_PATTERN = Set.of("cluster:admin/xpack/security/service_account/*"); private static final Set GRANT_API_KEY_PATTERN = Set.of(GrantApiKeyAction.NAME + "*"); private static final Set MONITOR_PATTERN = Set.of("cluster:monitor/*"); private static final Set MONITOR_ML_PATTERN = Set.of("cluster:monitor/xpack/ml/*"); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenResponseTests.java index 2bc35ed9f8a09..c69ffce13eec0 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenResponseTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenResponseTests.java @@ -22,26 +22,13 @@ protected Writeable.Reader instanceReader() { @Override protected CreateServiceAccountTokenResponse createTestInstance() { - if (randomBoolean()) { - return CreateServiceAccountTokenResponse.created( - randomAlphaOfLengthBetween(3, 8), new SecureString(randomAlphaOfLength(20).toCharArray())); - } else { - return CreateServiceAccountTokenResponse.notCreated(); - } + return CreateServiceAccountTokenResponse.created( + randomAlphaOfLengthBetween(3, 8), new SecureString(randomAlphaOfLength(20).toCharArray())); } @Override protected CreateServiceAccountTokenResponse mutateInstance(CreateServiceAccountTokenResponse instance) throws IOException { - if (instance.isCreated()) { - if (randomBoolean()) { - return CreateServiceAccountTokenResponse.created(randomAlphaOfLengthBetween(3, 8), - new SecureString(randomAlphaOfLength(20).toCharArray())); - } else { - return CreateServiceAccountTokenResponse.notCreated(); - } - } else { - return CreateServiceAccountTokenResponse.created(randomAlphaOfLengthBetween(3, 8), - new SecureString(randomAlphaOfLength(20).toCharArray())); - } + return CreateServiceAccountTokenResponse.created(randomAlphaOfLengthBetween(3, 8), + new SecureString(randomAlphaOfLength(20).toCharArray())); } } 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 2a2feda89830d..f06d0613b3520 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,8 +191,8 @@ 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_token/create", - "cluster:admin/xpack/security/service_account_token/get", + "cluster:admin/xpack/security/service_account/token/create", + "cluster:admin/xpack/security/service_account/token/get", "cluster:admin/xpack/security/token/create", "cluster:admin/xpack/security/token/invalidate", "cluster:admin/xpack/security/token/refresh", 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 7e4ea93d05adf..ab0a79d6c8293 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 @@ -188,6 +188,18 @@ public void testFileTokenAndApiTokenCanShareTheSameNameAndBothWorks() throws IOE assertOK(client().performRequest(request)); } + public void testNoDuplicateApiServiceAccountToken() throws IOException { + final String tokeName = randomAlphaOfLengthBetween(3, 8); + final Request createTokenRequest = new Request("POST", "_security/service/elastic/fleet/credential/token/" + tokeName); + final Response createTokenResponse = client().performRequest(createTokenRequest); + assertOK(createTokenResponse); + + final ResponseException e = + expectThrows(ResponseException.class, () -> client().performRequest(createTokenRequest)); + assertThat(e.getResponse().getStatusLine().getStatusCode(), equalTo(409)); + assertThat(e.getMessage(), containsString("document already exists")); + } + public void testGetServiceAccountTokens() throws IOException { final Request getTokensRequest = new Request("GET", "_security/service/elastic/fleet/credential"); final Response getTokensResponse1 = client().performRequest(getTokensRequest); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStore.java index 1f57b3e3ed22d..484fe6cb0e5d8 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStore.java @@ -118,12 +118,10 @@ public void createToken(Authentication authentication, CreateServiceAccountToken securityIndex.prepareIndexIfNeededThenExecute(listener::onFailure, () -> { executeAsyncWithOrigin(client, SECURITY_ORIGIN, BulkAction.INSTANCE, bulkRequest, TransportSingleItemBulkWriteAction.wrapBulkResponse(ActionListener.wrap(response -> { - if (DocWriteResponse.Result.CREATED == response.getResult()) { - listener.onResponse(CreateServiceAccountTokenResponse.created( - token.getTokenName(), token.asBearerString())); - } else { - listener.onResponse(CreateServiceAccountTokenResponse.notCreated()); - } + assert DocWriteResponse.Result.CREATED == response.getResult() + : "an successful response of an OpType.CREATE request must have result of CREATED"; + listener.onResponse(CreateServiceAccountTokenResponse.created( + token.getTokenName(), token.asBearerString())); }, listener::onFailure))); }); } catch (IOException e) { @@ -136,7 +134,7 @@ public void findTokensFor(ServiceAccountId accountId, ActionListener Date: Fri, 26 Mar 2021 18:04:29 +1100 Subject: [PATCH 05/14] Add more tests --- .../GetServiceAccountTokensResponse.java | 21 ++- .../security/action/service/TokenInfo.java | 28 +++- .../GetServiceAccountTokensResponseTests.java | 127 ++++++++++++++++++ .../IndexServiceAccountsTokenStoreTests.java | 3 +- 4 files changed, 172 insertions(+), 7 deletions(-) create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensResponseTests.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensResponse.java index c0f91e952ab39..30bbef58d3106 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensResponse.java @@ -26,12 +26,12 @@ public class GetServiceAccountTokensResponse extends ActionResponse implements T private final String principal; private final String nodeName; - private final Collection tokenInfos; + private final List tokenInfos; public GetServiceAccountTokensResponse(String principal, String nodeName, Collection tokenInfos) { this.principal = principal; this.nodeName = nodeName; - this.tokenInfos = tokenInfos == null ? List.of() : tokenInfos; + this.tokenInfos = tokenInfos == null ? List.of() : tokenInfos.stream().sorted().collect(toUnmodifiableList()); } public GetServiceAccountTokensResponse(StreamInput in) throws IOException { @@ -41,11 +41,23 @@ public GetServiceAccountTokensResponse(StreamInput in) throws IOException { this.tokenInfos = in.readList(TokenInfo::new); } + public String getPrincipal() { + return principal; + } + + public String getNodeName() { + return nodeName; + } + + public Collection getTokenInfos() { + return tokenInfos; + } + @Override public void writeTo(StreamOutput out) throws IOException { out.writeString(principal); out.writeString(nodeName); - out.writeCollection(tokenInfos); + out.writeList(tokenInfos); } @Override @@ -76,8 +88,7 @@ public boolean equals(Object o) { return false; GetServiceAccountTokensResponse that = (GetServiceAccountTokensResponse) o; return Objects.equals(principal, that.principal) && Objects.equals(nodeName, that.nodeName) && Objects.equals( - tokenInfos, - that.tokenInfos); + tokenInfos, that.tokenInfos); } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/TokenInfo.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/TokenInfo.java index ca10ef34d564f..30e3e77b59d84 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/TokenInfo.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/TokenInfo.java @@ -15,8 +15,9 @@ import java.io.IOException; import java.util.Map; +import java.util.Objects; -public class TokenInfo implements Writeable, ToXContentObject { +public class TokenInfo implements Writeable, ToXContentObject, Comparable { private final String name; private final TokenSource source; @@ -39,6 +40,21 @@ public TokenSource getSource() { return source; } + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + TokenInfo tokenInfo = (TokenInfo) o; + return Objects.equals(name, tokenInfo.name) && source == tokenInfo.source; + } + + @Override + public int hashCode() { + return Objects.hash(name, source); + } + public static TokenInfo indexToken(String name) { return new TokenInfo(name, TokenSource.INDEX); } @@ -58,6 +74,16 @@ public void writeTo(StreamOutput out) throws IOException { out.writeEnum(source); } + @Override + public int compareTo(TokenInfo o) { + final int score = source.compareTo(o.source); + if (score == 0) { + return name.compareTo(o.name); + } else { + return score; + } + } + public enum TokenSource { INDEX, FILE; } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensResponseTests.java new file mode 100644 index 0000000000000..3e309e638eb6b --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensResponseTests.java @@ -0,0 +1,127 @@ +/* + * 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.test.AbstractWireSerializingTestCase; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import static org.hamcrest.Matchers.anEmptyMap; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +public class GetServiceAccountTokensResponseTests extends AbstractWireSerializingTestCase { + + @Override + protected Writeable.Reader instanceReader() { + return GetServiceAccountTokensResponse::new; + } + + @Override + protected GetServiceAccountTokensResponse createTestInstance() { + final String principal = randomAlphaOfLengthBetween(3, 8) + "/" + randomAlphaOfLengthBetween(3, 8); + final String nodeName = randomAlphaOfLengthBetween(3, 8); + final List tokenInfos = IntStream.range(0, randomIntBetween(0, 10)) + .mapToObj(i -> randomTokenInfo()) + .collect(Collectors.toUnmodifiableList()); + return new GetServiceAccountTokensResponse(principal, nodeName, tokenInfos); + } + + @Override + protected GetServiceAccountTokensResponse mutateInstance(GetServiceAccountTokensResponse instance) throws IOException { + + switch (randomIntBetween(0, 2)) { + case 0: + return new GetServiceAccountTokensResponse(randomValueOtherThan(instance.getPrincipal(), + () -> randomAlphaOfLengthBetween(3, 8) + "/" + randomAlphaOfLengthBetween(3, 8)), + instance.getNodeName(), instance.getTokenInfos()); + case 1: + return new GetServiceAccountTokensResponse(instance.getPrincipal(), + randomValueOtherThan(instance.getNodeName(), () -> randomAlphaOfLengthBetween(3, 8)), + instance.getTokenInfos()); + default: + final ArrayList tokenInfos = new ArrayList<>(instance.getTokenInfos()); + switch (randomIntBetween(0, 2)) { + case 0: + if (false == tokenInfos.isEmpty()) { + tokenInfos.remove(randomIntBetween(0, tokenInfos.size() - 1)); + } else { + tokenInfos.add(randomTokenInfo()); + } + break; + case 1: + tokenInfos.add(randomIntBetween(0, tokenInfos.size() - 1), randomTokenInfo()); + break; + default: + if (false == tokenInfos.isEmpty()) { + for (int i = 0; i < randomIntBetween(1, tokenInfos.size()); i++) { + final int j = randomIntBetween(0, tokenInfos.size() - 1); + tokenInfos.set(j, randomValueOtherThan(tokenInfos.get(j), this::randomTokenInfo)); + } + } else { + tokenInfos.add(randomTokenInfo()); + } + } + return new GetServiceAccountTokensResponse(instance.getPrincipal(), instance.getNodeName(), + tokenInfos.stream().collect(Collectors.toUnmodifiableList())); + } + } + + public void testEquals() { + final GetServiceAccountTokensResponse response = createTestInstance(); + final ArrayList tokenInfos = new ArrayList<>(response.getTokenInfos()); + Collections.shuffle(tokenInfos, random()); + assertThat(new GetServiceAccountTokensResponse( + response.getPrincipal(), response.getNodeName(), tokenInfos.stream().collect(Collectors.toUnmodifiableList())), + equalTo(response)); + } + + public void testToXContent() throws IOException { + final GetServiceAccountTokensResponse response = createTestInstance(); + final Map nameToTokenInfos = response.getTokenInfos().stream() + .collect(Collectors.toMap(TokenInfo::getName, Function.identity())); + XContentBuilder builder = XContentFactory.jsonBuilder(); + response.toXContent(builder, ToXContent.EMPTY_PARAMS); + final Map responseMap = XContentHelper.convertToMap(BytesReference.bytes(builder), + false, builder.contentType()).v2(); + + assertThat(responseMap.get("service_account"), equalTo(response.getPrincipal())); + assertThat(responseMap.get("node_name"), equalTo(response.getNodeName())); + assertThat(responseMap.get("count"), equalTo(response.getTokenInfos().size())); + @SuppressWarnings("unchecked") + final Map tokens = (Map) responseMap.get("tokens"); + assertNotNull(tokens); + tokens.keySet().forEach(k -> assertThat(nameToTokenInfos.remove(k).getSource(), equalTo(TokenInfo.TokenSource.INDEX))); + + @SuppressWarnings("unchecked") + final Map fileTokens = (Map) responseMap.get("file_tokens"); + assertNotNull(fileTokens); + fileTokens.keySet().forEach(k -> assertThat(nameToTokenInfos.remove(k).getSource(), equalTo(TokenInfo.TokenSource.FILE))); + + assertThat(nameToTokenInfos, is(anEmptyMap())); + } + + private TokenInfo randomTokenInfo() { + return randomBoolean() ? + TokenInfo.fileToken(randomAlphaOfLengthBetween(3, 8)) : + TokenInfo.indexToken(randomAlphaOfLengthBetween(3, 8)); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStoreTests.java index 241e2823ebb10..c6cd192b93288 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStoreTests.java @@ -162,7 +162,8 @@ public void testCreateToken() throws ExecutionException, InterruptedException { assertThat(bulkRequest.requests().size(), equalTo(1)); final IndexRequest indexRequest = (IndexRequest) bulkRequest.requests().get(0); final Map sourceMap = indexRequest.sourceAsMap(); - assertThat(sourceMap.get("name"), equalTo("elastic/fleet/" + request.getTokenName())); + assertThat(sourceMap.get("username"), equalTo("elastic/fleet")); + assertThat(sourceMap.get("name"), equalTo(request.getTokenName())); assertThat(sourceMap.get("doc_type"), equalTo("service_account_token")); assertThat(sourceMap.get("version"), equalTo(Version.CURRENT.id)); assertThat(sourceMap.get("password"), notNullValue()); From 96af0c602f6fbf49135eadc811b35f849d1beb85 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Fri, 26 Mar 2021 22:25:04 +1100 Subject: [PATCH 06/14] Complete tests --- .../GetServiceAccountTokensRequest.java | 17 +++ .../GetServiceAccountTokensRequestTests.java | 61 +++++++++++ .../CompositeServiceAccountsTokenStore.java | 1 - .../FileServiceAccountsTokenStore.java | 1 - .../authc/service/FileTokensTool.java | 1 - .../IndexServiceAccountsTokenStore.java | 3 +- .../authc/service/ServiceAccountService.java | 2 - .../service/ServiceAccountsTokenStore.java | 4 +- ...ortGetServiceAccountTokensActionTests.java | 79 ++++++++++++++ ...CachingServiceAccountsTokenStoreTests.java | 1 - ...mpositeServiceAccountsTokenStoreTests.java | 101 ++++++++++++++++-- .../FileServiceAccountsTokenStoreTests.java | 20 ++++ .../IndexServiceAccountsTokenStoreTests.java | 93 ++++++++++++---- .../service/ServiceAccountServiceTests.java | 1 - 14 files changed, 347 insertions(+), 38 deletions(-) create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensRequestTests.java create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountTokensActionTests.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensRequest.java index b7cf32aa68f17..5331c5a4f6e2a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensRequest.java @@ -14,6 +14,7 @@ import org.elasticsearch.common.io.stream.StreamOutput; import java.io.IOException; +import java.util.Objects; import static org.elasticsearch.action.ValidateActions.addValidationError; @@ -41,8 +42,24 @@ public String getServiceName() { return serviceName; } + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + GetServiceAccountTokensRequest that = (GetServiceAccountTokensRequest) 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.writeString(namespace); out.writeString(serviceName); } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensRequestTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensRequestTests.java new file mode 100644 index 0000000000000..0a9988ec3a8d0 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensRequestTests.java @@ -0,0 +1,61 @@ +/* + * 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.ActionRequestValidationException; +import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.test.AbstractWireSerializingTestCase; + +import java.io.IOException; + +import static org.hamcrest.Matchers.containsString; + +public class GetServiceAccountTokensRequestTests extends AbstractWireSerializingTestCase { + + @Override + protected Writeable.Reader instanceReader() { + return GetServiceAccountTokensRequest::new; + } + + @Override + protected GetServiceAccountTokensRequest createTestInstance() { + final String namespace = randomAlphaOfLengthBetween(3, 8); + final String serviceName = randomAlphaOfLengthBetween(3, 8); + return new GetServiceAccountTokensRequest(namespace, serviceName); + } + + @Override + protected GetServiceAccountTokensRequest mutateInstance(GetServiceAccountTokensRequest instance) throws IOException { + switch (randomIntBetween(0, 2)) { + case 0: + return new GetServiceAccountTokensRequest( + randomValueOtherThan(instance.getNamespace(), () -> randomAlphaOfLengthBetween(3, 8)), instance.getServiceName()); + case 1: + return new GetServiceAccountTokensRequest( + instance.getNamespace(), randomValueOtherThan(instance.getServiceName(), () -> randomAlphaOfLengthBetween(3, 8))); + default: + return new GetServiceAccountTokensRequest( + randomValueOtherThan(instance.getNamespace(), () -> randomAlphaOfLengthBetween(3, 8)), + randomValueOtherThan(instance.getServiceName(), () -> randomAlphaOfLengthBetween(3, 8))); + } + } + + public void testValidate() { + assertNull(createTestInstance().validate()); + + final GetServiceAccountTokensRequest request1 = + new GetServiceAccountTokensRequest(randomFrom("", null), randomAlphaOfLengthBetween(3, 8)); + final ActionRequestValidationException e1 = request1.validate(); + assertThat(e1.getMessage(), containsString("service account namespace is required")); + + final GetServiceAccountTokensRequest request2 = + new GetServiceAccountTokensRequest(randomAlphaOfLengthBetween(3, 8), randomFrom("", null)); + final ActionRequestValidationException e2 = request2.validate(); + assertThat(e2.getMessage(), containsString("service account service-name is required")); + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountsTokenStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountsTokenStore.java index 0ee91e5d8c46b..561de9083cdce 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountsTokenStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountsTokenStore.java @@ -15,7 +15,6 @@ import org.elasticsearch.xpack.core.common.IteratingActionListener; import org.elasticsearch.xpack.core.security.action.service.TokenInfo; import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; -import org.elasticsearch.xpack.security.authc.service.ServiceAccountToken; import java.util.ArrayList; import java.util.Collection; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStore.java index e17f52b757f71..488f161da6a64 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStore.java @@ -23,7 +23,6 @@ import org.elasticsearch.xpack.core.security.authc.support.Hasher; import org.elasticsearch.xpack.core.security.support.NoOpLogger; import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; -import org.elasticsearch.xpack.security.authc.service.ServiceAccountToken; import org.elasticsearch.xpack.security.support.FileLineParser; import org.elasticsearch.xpack.security.support.FileReloadListener; import org.elasticsearch.xpack.security.support.SecurityFiles; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java index 498789422957f..07abb3d3f22a2 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileTokensTool.java @@ -21,7 +21,6 @@ import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.security.authc.support.Hasher; import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; -import org.elasticsearch.xpack.security.authc.service.ServiceAccountToken; import org.elasticsearch.xpack.security.support.FileAttributesChecker; import java.nio.file.Path; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStore.java index 484fe6cb0e5d8..cc0545ab782d5 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStore.java @@ -41,7 +41,6 @@ import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.support.Hasher; import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; -import org.elasticsearch.xpack.security.authc.service.ServiceAccountToken; import org.elasticsearch.xpack.security.support.CacheInvalidatorRegistry; import org.elasticsearch.xpack.security.support.SecurityIndexManager; @@ -59,7 +58,7 @@ public class IndexServiceAccountsTokenStore extends CachingServiceAccountsTokenStore { private static final Logger logger = LogManager.getLogger(IndexServiceAccountsTokenStore.class); - private static final String SERVICE_ACCOUNT_TOKEN_DOC_TYPE = "service_account_token"; + static final String SERVICE_ACCOUNT_TOKEN_DOC_TYPE = "service_account_token"; private final Clock clock; private final Client client; 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 513df85b153db..b5c6cbfe80598 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 @@ -21,9 +21,7 @@ import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.user.User; -import org.elasticsearch.xpack.security.authc.service.ServiceAccount; import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; -import org.elasticsearch.xpack.security.authc.service.ServiceAccountToken; import java.util.Collection; import java.util.Map; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountsTokenStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountsTokenStore.java index c830a38769852..37590e545ed85 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountsTokenStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountsTokenStore.java @@ -10,7 +10,6 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.xpack.core.security.action.service.TokenInfo; import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; -import org.elasticsearch.xpack.security.authc.service.ServiceAccountToken; import java.util.Collection; @@ -24,6 +23,9 @@ public interface ServiceAccountsTokenStore { */ void authenticate(ServiceAccountToken token, ActionListener listener); + /** + * Get all tokens belong to the given service account id + */ void findTokensFor(ServiceAccountId accountId, ActionListener> listener); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountTokensActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountTokensActionTests.java new file mode 100644 index 0000000000000..8c6141bf245b9 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountTokensActionTests.java @@ -0,0 +1,79 @@ +/* + * 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.ElasticsearchSecurityException; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountTokensRequest; +import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountTokensResponse; +import org.elasticsearch.xpack.security.authc.service.ServiceAccount; +import org.elasticsearch.xpack.security.authc.service.ServiceAccountService; +import org.junit.Before; + +import java.util.Collections; +import java.util.concurrent.ExecutionException; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.is; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +public class TransportGetServiceAccountTokensActionTests extends ESTestCase { + + private TransportGetServiceAccountTokensAction transportGetServiceAccountTokensAction; + private ServiceAccountService serviceAccountService; + + @Before + public void init() { + final Settings settings = Settings.builder() + .put("node.name", "node_name") + .put("xpack.security.http.ssl.enabled", true) + .put("xpack.security.transport.ssl.enabled", true) + .build(); + serviceAccountService = mock(ServiceAccountService.class); + transportGetServiceAccountTokensAction = new TransportGetServiceAccountTokensAction( + mock(TransportService.class), new ActionFilters(Collections.emptySet()), + settings, serviceAccountService); + } + + public void testDoExecuteWillDelegate() { + final GetServiceAccountTokensRequest request = + new GetServiceAccountTokensRequest(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8)); + @SuppressWarnings("rawtypes") + final ActionListener listener = mock(ActionListener.class); + //noinspection unchecked + transportGetServiceAccountTokensAction.doExecute(mock(Task.class), request, listener); + verify(serviceAccountService).findTokensFor( + eq(new ServiceAccount.ServiceAccountId(request.getNamespace(), request.getServiceName())), + eq("node_name"), eq(listener)); + } + + public void testTlsRequired() { + final boolean httpTls = randomBoolean(); + final Settings settings = Settings.builder() + .put("xpack.security.http.ssl.enabled", httpTls) + .put("xpack.security.transport.ssl.enabled", randomFrom(false == httpTls, false)) + .build(); + final TransportGetServiceAccountTokensAction action = new TransportGetServiceAccountTokensAction( + mock(TransportService.class), new ActionFilters(Collections.emptySet()), + settings, mock(ServiceAccountService.class)); + + final PlainActionFuture future = new PlainActionFuture<>(); + action.doExecute(mock(Task.class), mock(GetServiceAccountTokensRequest.class), future); + final ExecutionException e = expectThrows(ExecutionException.class, () -> future.get()); + assertThat(e.getCause().getClass(), is(ElasticsearchSecurityException.class)); + assertThat(e.getMessage(), containsString("Service account APIs require TLS for both HTTP and Transport")); + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStoreTests.java index 922b240848238..ae225f2c92afa 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CachingServiceAccountsTokenStoreTests.java @@ -18,7 +18,6 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.security.action.service.TokenInfo; import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; -import org.elasticsearch.xpack.security.authc.service.ServiceAccountToken; import org.junit.After; import org.junit.Before; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountsTokenStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountsTokenStoreTests.java index b1a30a1bfcbda..0a769b92f286d 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountsTokenStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/CompositeServiceAccountsTokenStoreTests.java @@ -12,11 +12,20 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.action.service.TokenInfo; +import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; import org.junit.Before; +import org.mockito.Mockito; +import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.concurrent.ExecutionException; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; import static org.mockito.Matchers.any; import static org.mockito.Matchers.eq; @@ -28,19 +37,24 @@ public class CompositeServiceAccountsTokenStoreTests extends ESTestCase { private ThreadContext threadContext; + private ServiceAccountsTokenStore store1; + private ServiceAccountsTokenStore store2; + private ServiceAccountsTokenStore store3; + private CompositeServiceAccountsTokenStore compositeStore; @Before public void init() { threadContext = new ThreadContext(Settings.EMPTY); + store1 = mock(ServiceAccountsTokenStore.class); + store2 = mock(ServiceAccountsTokenStore.class); + store3 = mock(ServiceAccountsTokenStore.class); + compositeStore = new CompositeServiceAccountsTokenStore(List.of(store1, store2, store3), threadContext); } public void testAuthenticate() throws ExecutionException, InterruptedException { - final ServiceAccountToken token = mock(ServiceAccountToken.class); - - final ServiceAccountsTokenStore store1 = mock(ServiceAccountsTokenStore.class); - final ServiceAccountsTokenStore store2 = mock(ServiceAccountsTokenStore.class); - final ServiceAccountsTokenStore store3 = mock(ServiceAccountsTokenStore.class); + Mockito.reset(store1, store2, store3); + final ServiceAccountToken token = mock(ServiceAccountToken.class); final boolean store1Success = randomBoolean(); final boolean store2Success = randomBoolean(); final boolean store3Success = randomBoolean(); @@ -66,9 +80,6 @@ public void testAuthenticate() throws ExecutionException, InterruptedException { return null; }).when(store3).authenticate(eq(token), any()); - final CompositeServiceAccountsTokenStore compositeStore = - new CompositeServiceAccountsTokenStore(List.of(store1, store2, store3), threadContext); - final PlainActionFuture future = new PlainActionFuture<>(); compositeStore.authenticate(token, future); if (store1Success || store2Success || store3Success) { @@ -93,4 +104,78 @@ public void testAuthenticate() throws ExecutionException, InterruptedException { verify(store3).authenticate(eq(token), any()); } } + + public void testFindTokensFor() throws ExecutionException, InterruptedException { + Mockito.reset(store1, store2, store3); + + final ServiceAccountId accountId1 = new ServiceAccountId(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8)); + final ServiceAccountId accountId2 = new ServiceAccountId(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8)); + final boolean store1Error = randomBoolean(); + final RuntimeException e = new RuntimeException("fail"); + final Set allTokenInfos = new HashSet<>(); + + doAnswer(invocationOnMock -> { + final ServiceAccountId accountId = (ServiceAccountId) invocationOnMock.getArguments()[0]; + @SuppressWarnings("unchecked") + final ActionListener> listener = + (ActionListener>) invocationOnMock.getArguments()[1]; + if (accountId == accountId1) { + final Set tokenInfos = new HashSet<>(); + IntStream.range(0, randomIntBetween(0, 5)).forEach(i -> { + final TokenInfo tokenInfo = TokenInfo.fileToken(randomAlphaOfLengthBetween(3, 8)); + tokenInfos.add(tokenInfo); + }); + allTokenInfos.addAll(tokenInfos); + listener.onResponse(tokenInfos); + } else { + if (store1Error) { + listener.onFailure(e); + } else { + listener.onResponse(List.of()); + } + } + return null; + }).when(store1).findTokensFor(any(), any()); + + doAnswer(invocationOnMock -> { + final ServiceAccountId accountId = (ServiceAccountId) invocationOnMock.getArguments()[0]; + @SuppressWarnings("unchecked") + final ActionListener> listener = + (ActionListener>) invocationOnMock.getArguments()[1]; + if (accountId == accountId1) { + final Set tokenInfos = new HashSet<>(); + IntStream.range(0, randomIntBetween(0, 5)).forEach(i -> { + final TokenInfo tokenInfo = TokenInfo.indexToken(randomAlphaOfLengthBetween(3, 8)); + tokenInfos.add(tokenInfo); + }); + allTokenInfos.addAll(tokenInfos); + listener.onResponse(tokenInfos); + } else { + if (store1Error) { + listener.onResponse(List.of()); + } else { + listener.onFailure(e); + } + } + return null; + }).when(store2).findTokensFor(any(), any()); + + doAnswer(invocationOnMock -> { + @SuppressWarnings("unchecked") + final ActionListener> listener = + (ActionListener>) invocationOnMock.getArguments()[1]; + listener.onResponse(List.of()); + return null; + }).when(store3).findTokensFor(any(), any()); + + final PlainActionFuture> future1 = new PlainActionFuture<>(); + compositeStore.findTokensFor(accountId1, future1); + final Collection result = future1.get(); + assertThat(result.stream().collect(Collectors.toUnmodifiableSet()), equalTo(allTokenInfos)); + + final PlainActionFuture> future2 = new PlainActionFuture<>(); + compositeStore.findTokensFor(accountId2, future2); + final RuntimeException e2 = expectThrows(RuntimeException.class, future2::actionGet); + assertThat(e2, is(e)); + } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStoreTests.java index c7aed6940d745..7960f06a0928a 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStoreTests.java @@ -9,6 +9,7 @@ import org.apache.logging.log4j.Level; import org.apache.logging.log4j.Logger; +import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.env.Environment; @@ -17,8 +18,10 @@ import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.watcher.ResourceWatcherService; +import org.elasticsearch.xpack.core.security.action.service.TokenInfo; import org.elasticsearch.xpack.core.security.audit.logfile.CapturingLogger; import org.elasticsearch.xpack.core.security.authc.support.Hasher; +import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; import org.junit.After; import org.junit.Before; @@ -29,6 +32,7 @@ import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.nio.file.StandardOpenOption; +import java.util.Collection; import java.util.List; import java.util.Map; import java.util.concurrent.CountDownLatch; @@ -39,6 +43,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; +import static org.mockito.Mockito.mock; public class FileServiceAccountsTokenStoreTests extends ESTestCase { @@ -181,4 +186,19 @@ public void testAutoReload() throws Exception { assertThat(store.getTokenHashes().get(qualifiedTokenName), equalTo(newTokenHash)); } } + + public void testFindTokensFor() throws IOException { + Path serviceTokensSourceFile = getDataPath("service_tokens"); + Path configDir = env.configFile(); + Files.createDirectories(configDir); + Path targetFile = configDir.resolve("service_tokens"); + Files.copy(serviceTokensSourceFile, targetFile, StandardCopyOption.REPLACE_EXISTING); + FileServiceAccountsTokenStore store = new FileServiceAccountsTokenStore(env, mock(ResourceWatcherService.class), threadPool); + + final ServiceAccountId accountId = new ServiceAccountId("elastic", "fleet"); + final PlainActionFuture> future1 = new PlainActionFuture<>(); + store.findTokensFor(accountId, future1); + final Collection tokenInfos1 = future1.actionGet(); + assertThat(tokenInfos1.size(), equalTo(5)); + } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStoreTests.java index c6cd192b93288..1e28e96c805ef 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStoreTests.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.security.authc.service; +import org.apache.lucene.search.TotalHits; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.ActionRequest; @@ -20,6 +21,10 @@ import org.elasticsearch.action.get.GetResponse; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.action.index.IndexResponse; +import org.elasticsearch.action.search.ClearScrollRequest; +import org.elasticsearch.action.search.ClearScrollResponse; +import org.elasticsearch.action.search.SearchRequest; +import org.elasticsearch.action.search.SearchResponse; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.client.Client; import org.elasticsearch.client.FilterClient; @@ -32,16 +37,19 @@ import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.get.GetResult; import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.search.SearchHit; +import org.elasticsearch.search.SearchHits; +import org.elasticsearch.search.internal.InternalSearchResponse; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.XContentTestUtils; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenRequest; import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenResponse; +import org.elasticsearch.xpack.core.security.action.service.TokenInfo; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.support.Hasher; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; -import org.elasticsearch.xpack.security.authc.service.ServiceAccountToken; import org.elasticsearch.xpack.security.support.CacheInvalidatorRegistry; import org.elasticsearch.xpack.security.support.SecurityIndexManager; import org.junit.Before; @@ -49,13 +57,19 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; import java.time.Clock; +import java.util.Collection; import java.util.Map; +import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import static org.elasticsearch.index.seqno.SequenceNumbers.UNASSIGNED_PRIMARY_TERM; import static org.elasticsearch.index.seqno.SequenceNumbers.UNASSIGNED_SEQ_NO; +import static org.elasticsearch.xpack.security.authc.service.IndexServiceAccountsTokenStore.SERVICE_ACCOUNT_TOKEN_DOC_TYPE; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; @@ -72,7 +86,8 @@ public class IndexServiceAccountsTokenStoreTests extends ESTestCase { private CacheInvalidatorRegistry cacheInvalidatorRegistry; private IndexServiceAccountsTokenStore store; private final AtomicReference requestHolder = new AtomicReference<>(); - private final AtomicReference>> responseProviderHolder = new AtomicReference<>(); + private final AtomicReference>> responseProviderHolder = + new AtomicReference<>(); @Before public void init() { @@ -86,7 +101,7 @@ public void init() { protected void doExecute(ActionType action, Request request, ActionListener listener) { requestHolder.set(request); - responseProviderHolder.get().accept((ActionListener) listener); + responseProviderHolder.get().accept(request, (ActionListener) listener); } }; clusterService = mock(ClusterService.class); @@ -127,7 +142,7 @@ public void testDoAuthenticate() throws IOException, ExecutionException, Interru final GetResponse getResponse1 = createGetResponse(serviceAccountToken, true); // success - responseProviderHolder.set(l -> l.onResponse(getResponse1)); + responseProviderHolder.set((r, l) -> l.onResponse(getResponse1)); final PlainActionFuture future1 = new PlainActionFuture<>(); store.doAuthenticate(serviceAccountToken, future1); final GetRequest getRequest = (GetRequest) requestHolder.get(); @@ -136,14 +151,14 @@ public void testDoAuthenticate() throws IOException, ExecutionException, Interru // token mismatch final GetResponse getResponse2 = createGetResponse(ServiceAccountToken.newToken(accountId, randomAlphaOfLengthBetween(3, 8)), true); - responseProviderHolder.set(l -> l.onResponse(getResponse2)); + responseProviderHolder.set((r, l) -> l.onResponse(getResponse2)); final PlainActionFuture future2 = new PlainActionFuture<>(); store.doAuthenticate(serviceAccountToken, future2); assertThat(future2.get(), is(false)); // token document not found final GetResponse getResponse3 = createGetResponse(serviceAccountToken, false); - responseProviderHolder.set(l -> l.onResponse(getResponse3)); + responseProviderHolder.set((r, l) -> l.onResponse(getResponse3)); final PlainActionFuture future3 = new PlainActionFuture<>(); store.doAuthenticate(serviceAccountToken, future3); assertThat(future3.get(), is(false)); @@ -155,7 +170,7 @@ public void testCreateToken() throws ExecutionException, InterruptedException { new CreateServiceAccountTokenRequest("elastic", "fleet", randomAlphaOfLengthBetween(3, 8)); // created - responseProviderHolder.set(l -> l.onResponse(createSingleBulkResponse(true))); + responseProviderHolder.set((r, l) -> l.onResponse(createSingleBulkResponse())); final PlainActionFuture future1 = new PlainActionFuture<>(); store.createToken(authentication, request, future1); final BulkRequest bulkRequest = (BulkRequest) requestHolder.get(); @@ -185,19 +200,9 @@ public void testCreateToken() throws ExecutionException, InterruptedException { assertThat(createServiceAccountTokenResponse1.getName(), equalTo(request.getTokenName())); assertNotNull(createServiceAccountTokenResponse1.getValue()); - // not created - responseProviderHolder.set(l -> l.onResponse(createSingleBulkResponse(false))); - final PlainActionFuture future2 = new PlainActionFuture<>(); - store.createToken(authentication, request, future2); - final CreateServiceAccountTokenResponse createServiceAccountTokenResponse2 = future2.get(); - assertNotNull(createServiceAccountTokenResponse2); - assertThat(createServiceAccountTokenResponse2.isCreated(), is(false)); - assertNull(createServiceAccountTokenResponse2.getName()); - assertNull(createServiceAccountTokenResponse2.getValue()); - // failure final Exception exception = mock(Exception.class); - responseProviderHolder.set(l -> l.onFailure(exception)); + responseProviderHolder.set((r, l) -> l.onFailure(exception)); final PlainActionFuture future3 = new PlainActionFuture<>(); store.createToken(authentication, request, future3); final ExecutionException e3 = expectThrows(ExecutionException.class, () -> future3.get()); @@ -218,6 +223,54 @@ public void testCreateTokenWillFailForInvalidServiceAccount() { containsString("service account [" + request.getNamespace() + "/" + request.getServiceName() + "] does not exist")); } + public void testFindTokensFor() { + final ServiceAccountId accountId = new ServiceAccountId(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8)); + final int nhits = randomIntBetween(0, 10); + final String[] tokenNames = randomArray(nhits, nhits, String[]::new, ServiceAccountTokenTests::randomTokenName); + + responseProviderHolder.set((r, l) -> { + if (r instanceof SearchRequest) { + final SearchHit[] hits = IntStream.range(0, nhits) + .mapToObj(i -> + new SearchHit(randomIntBetween(0, Integer.MAX_VALUE), + SERVICE_ACCOUNT_TOKEN_DOC_TYPE + "-" + accountId.asPrincipal() + "/" + tokenNames[i], Map.of(), Map.of())) + .toArray(SearchHit[]::new); + final InternalSearchResponse internalSearchResponse; + internalSearchResponse = new InternalSearchResponse(new SearchHits(hits, + new TotalHits(nhits, TotalHits.Relation.EQUAL_TO), + randomFloat(), null, null, null), + null, null, null, false, null, 0); + + final SearchResponse searchResponse = + new SearchResponse(internalSearchResponse, randomAlphaOfLengthBetween(3, 8), + 1, 1, 0, 10, null, null); + l.onResponse(searchResponse); + } else if (r instanceof ClearScrollRequest) { + l.onResponse(new ClearScrollResponse(true, 1)); + } + }); + + final PlainActionFuture> future = new PlainActionFuture<>(); + store.findTokensFor(accountId, future); + final Collection tokenInfos = future.actionGet(); + assertThat(tokenInfos.stream().map(TokenInfo::getSource).allMatch(TokenInfo.TokenSource.INDEX::equals), is(true)); + assertThat(tokenInfos.stream().map(TokenInfo::getName).collect(Collectors.toUnmodifiableSet()), + equalTo(Set.of(tokenNames))); + } + + public void testFindTokensForException() { + final ServiceAccountId accountId = new ServiceAccountId(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8)); + final RuntimeException e = new RuntimeException("fail"); + responseProviderHolder.set((r, l) -> { + l.onFailure(e); + }); + + final PlainActionFuture> future = new PlainActionFuture<>(); + store.findTokensFor(accountId, future); + final RuntimeException e1 = expectThrows(RuntimeException.class, future::actionGet); + assertThat(e1, is(e)); + } + private GetResponse createGetResponse(ServiceAccountToken serviceAccountToken, boolean exists) throws IOException { final char[] hash = Hasher.PBKDF2_STRETCH.hash(serviceAccountToken.getSecret()); final Map documentMap = Map.of("password", new String(CharArrays.toUtf8Bytes(hash), StandardCharsets.UTF_8)); @@ -239,10 +292,10 @@ private Authentication createAuthentication() { randomAlphaOfLengthBetween(3, 8)), null)); } - private BulkResponse createSingleBulkResponse(boolean created) { + private BulkResponse createSingleBulkResponse() { return new BulkResponse(new BulkItemResponse[] { new BulkItemResponse(randomInt(), OpType.CREATE, new IndexResponse( - mock(ShardId.class), randomAlphaOfLengthBetween(3, 8), randomLong(), randomLong(), randomLong(), created + mock(ShardId.class), randomAlphaOfLengthBetween(3, 8), randomLong(), randomLong(), randomLong(), true )) }, randomLong()); } 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 f51770e4db7f2..834a6a8cbdc1c 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 @@ -25,7 +25,6 @@ import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; -import org.elasticsearch.xpack.security.authc.service.ServiceAccountToken; import org.junit.Before; import java.io.ByteArrayOutputStream; From a0c26adca0eac477470d0141d862f4fba0de056e Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Fri, 26 Mar 2021 23:00:26 +1100 Subject: [PATCH 07/14] revert unwanted changes --- .../support/SecurityIndexManager.java | 24 ++++--------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecurityIndexManager.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecurityIndexManager.java index 98cd4b926c8f2..e74f62937a9a9 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecurityIndexManager.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/support/SecurityIndexManager.java @@ -347,11 +347,7 @@ public void prepareIndexIfNeededThenExecute(final Consumer consumer, @Override public void onResponse(CreateIndexResponse createIndexResponse) { if (createIndexResponse.isAcknowledged()) { - try { - andThen.run(); - } catch (Exception e2) { - consumer.accept(e2); - } + andThen.run(); } else { consumer.accept(new ElasticsearchException("Failed to create security index")); } @@ -363,11 +359,7 @@ public void onFailure(Exception e) { if (cause instanceof ResourceAlreadyExistsException) { // the index already exists - it was probably just created so this // node hasn't yet received the cluster state update with the index - try { - andThen.run(); - } catch (Exception e2) { - consumer.accept(e2); - } + andThen.run(); } else { consumer.accept(e); } @@ -390,22 +382,14 @@ public void onFailure(Exception e) { executeAsyncWithOrigin(client.threadPool().getThreadContext(), systemIndexDescriptor.getOrigin(), request, ActionListener.wrap(putMappingResponse -> { if (putMappingResponse.isAcknowledged()) { - try { - andThen.run(); - } catch (Exception e2) { - consumer.accept(e2); - } + andThen.run(); } else { consumer.accept(new IllegalStateException("put mapping request was not acknowledged")); } }, consumer), client.admin().indices()::putMapping); } } else { - try { - andThen.run(); - } catch (Exception e2) { - consumer.accept(e2); - } + andThen.run(); } } catch (Exception e) { consumer.accept(e); From 9252cbf945f8c66972b3e0df040afe2c804d59b8 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Fri, 26 Mar 2021 23:09:01 +1100 Subject: [PATCH 08/14] fix test --- .../action/service/GetServiceAccountTokensResponseTests.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensResponseTests.java index 3e309e638eb6b..b75cdd99ce617 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensResponseTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/GetServiceAccountTokensResponseTests.java @@ -68,7 +68,7 @@ protected GetServiceAccountTokensResponse mutateInstance(GetServiceAccountTokens } break; case 1: - tokenInfos.add(randomIntBetween(0, tokenInfos.size() - 1), randomTokenInfo()); + tokenInfos.add(randomIntBetween(0, tokenInfos.isEmpty() ? 0 : tokenInfos.size() - 1), randomTokenInfo()); break; default: if (false == tokenInfos.isEmpty()) { From b396716635315e153300365875b5ab9ce0acf903 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Sun, 28 Mar 2021 13:18:11 +1100 Subject: [PATCH 09/14] extract tls runtime checker --- .../xpack/security/Security.java | 10 ++- ...nsportCreateServiceAccountTokenAction.java | 42 +++------ ...ransportGetServiceAccountTokensAction.java | 35 +++----- .../IndexServiceAccountsTokenStore.java | 5 +- .../authc/service/ServiceAccountService.java | 90 ++++++++----------- .../authc/support/TlsRuntimeCheck.java | 44 +++++++++ ...tCreateServiceAccountTokenActionTests.java | 12 +-- ...ortGetServiceAccountTokensActionTests.java | 14 ++- .../service/ServiceAccountServiceTests.java | 19 ++-- 9 files changed, 136 insertions(+), 135 deletions(-) create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/TlsRuntimeCheck.java 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 700054b35803c..58acb54c26a43 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 @@ -209,6 +209,7 @@ import org.elasticsearch.xpack.security.authc.service.ServiceAccountService; import org.elasticsearch.xpack.security.authc.service.CompositeServiceAccountsTokenStore; import org.elasticsearch.xpack.security.authc.support.SecondaryAuthenticator; +import org.elasticsearch.xpack.security.authc.support.TlsRuntimeCheck; import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore; import org.elasticsearch.xpack.security.authz.AuthorizationService; import org.elasticsearch.xpack.security.authz.SecuritySearchOperationListener; @@ -499,6 +500,9 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste clusterService, cacheInvalidatorRegistry, threadPool); components.add(apiKeyService); + final TlsRuntimeCheck tlsRuntimeCheck = new TlsRuntimeCheck(settings); + components.add(tlsRuntimeCheck); + final IndexServiceAccountsTokenStore indexServiceAccountsTokenStore = new IndexServiceAccountsTokenStore( settings, threadPool, getClock(), client, securityIndex.get(), clusterService, cacheInvalidatorRegistry); components.add(indexServiceAccountsTokenStore); @@ -506,9 +510,9 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste final FileServiceAccountsTokenStore fileServiceAccountsTokenStore = new FileServiceAccountsTokenStore(environment, resourceWatcherService, threadPool); - final ServiceAccountService serviceAccountService = new ServiceAccountService(settings, - new CompositeServiceAccountsTokenStore(List.of(fileServiceAccountsTokenStore, indexServiceAccountsTokenStore), - threadPool.getThreadContext())); + final ServiceAccountService serviceAccountService = new ServiceAccountService(new CompositeServiceAccountsTokenStore( + List.of(fileServiceAccountsTokenStore, indexServiceAccountsTokenStore), threadPool.getThreadContext()), + tlsRuntimeCheck); components.add(serviceAccountService); final CompositeRolesStore allRolesStore = new CompositeRolesStore(settings, fileRolesStore, nativeRolesStore, reservedRolesStore, diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportCreateServiceAccountTokenAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportCreateServiceAccountTokenAction.java index ad2f8a8e95752..9fe0d26a0a4d2 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportCreateServiceAccountTokenAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportCreateServiceAccountTokenAction.java @@ -7,64 +7,48 @@ package org.elasticsearch.xpack.security.action.service; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.message.ParameterizedMessage; -import org.elasticsearch.ElasticsearchSecurityException; 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.common.settings.Settings; -import org.elasticsearch.rest.RestStatus; import org.elasticsearch.tasks.Task; import org.elasticsearch.transport.TransportService; -import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.security.SecurityContext; import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenAction; import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenRequest; import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenResponse; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.security.authc.service.IndexServiceAccountsTokenStore; +import org.elasticsearch.xpack.security.authc.support.TlsRuntimeCheck; public class TransportCreateServiceAccountTokenAction extends HandledTransportAction { - private static final Logger logger = LogManager.getLogger(TransportCreateServiceAccountTokenAction.class); - private final IndexServiceAccountsTokenStore indexServiceAccountsTokenStore; private final SecurityContext securityContext; - private final boolean httpTlsEnabled; - private final boolean transportTlsEnabled; + private final TlsRuntimeCheck tlsRuntimeCheck; @Inject public TransportCreateServiceAccountTokenAction(TransportService transportService, ActionFilters actionFilters, - Settings settings, IndexServiceAccountsTokenStore indexServiceAccountsTokenStore, - SecurityContext securityContext) { + SecurityContext securityContext, + TlsRuntimeCheck tlsRuntimeCheck) { super(CreateServiceAccountTokenAction.NAME, transportService, actionFilters, CreateServiceAccountTokenRequest::new); this.indexServiceAccountsTokenStore = indexServiceAccountsTokenStore; this.securityContext = securityContext; - this.httpTlsEnabled = XPackSettings.HTTP_SSL_ENABLED.get(settings); - this.transportTlsEnabled = XPackSettings.TRANSPORT_SSL_ENABLED.get(settings); + this.tlsRuntimeCheck = tlsRuntimeCheck; } @Override protected void doExecute(Task task, CreateServiceAccountTokenRequest request, ActionListener listener) { - if (false == httpTlsEnabled || false == transportTlsEnabled) { - final ParameterizedMessage message = new ParameterizedMessage( - "Service account APIs require TLS for both HTTP and Transport, " + - "but got HTTP TLS: [{}] and Transport TLS: [{}]", httpTlsEnabled, transportTlsEnabled); - logger.debug(message); - listener.onFailure(new ElasticsearchSecurityException(message.getFormattedMessage(), RestStatus.UNAUTHORIZED)); - return; - } - final Authentication authentication = securityContext.getAuthentication(); - if (authentication == null) { - listener.onFailure(new IllegalStateException("authentication is required")); - } else { - indexServiceAccountsTokenStore.createToken(authentication, request, listener); - } + tlsRuntimeCheck.checkTlsThenExecute(listener::onFailure, "create service account token", () -> { + final Authentication authentication = securityContext.getAuthentication(); + if (authentication == null) { + listener.onFailure(new IllegalStateException("authentication is required")); + } else { + indexServiceAccountsTokenStore.createToken(authentication, request, listener); + } + }); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountTokensAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountTokensAction.java index 3e601fb2d4b3a..496a4c8c2ca16 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountTokensAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountTokensAction.java @@ -7,57 +7,44 @@ package org.elasticsearch.xpack.security.action.service; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.message.ParameterizedMessage; -import org.elasticsearch.ElasticsearchSecurityException; 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.common.settings.Settings; import org.elasticsearch.node.Node; -import org.elasticsearch.rest.RestStatus; import org.elasticsearch.tasks.Task; import org.elasticsearch.transport.TransportService; -import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountTokensAction; import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountTokensRequest; import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountTokensResponse; import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; import org.elasticsearch.xpack.security.authc.service.ServiceAccountService; +import org.elasticsearch.xpack.security.authc.support.TlsRuntimeCheck; public class TransportGetServiceAccountTokensAction extends HandledTransportAction { - private static final Logger logger = LogManager.getLogger(TransportGetServiceAccountTokensAction.class); - private final ServiceAccountService serviceAccountService; + private final TlsRuntimeCheck tlsRuntimeCheck; private final String nodeName; - private final boolean httpTlsEnabled; - private final boolean transportTlsEnabled; @Inject - public TransportGetServiceAccountTokensAction( - TransportService transportService, ActionFilters actionFilters, Settings settings, ServiceAccountService serviceAccountService) { + public TransportGetServiceAccountTokensAction(TransportService transportService, ActionFilters actionFilters, + Settings settings, + ServiceAccountService serviceAccountService, + TlsRuntimeCheck tlsRuntimeCheck) { super(GetServiceAccountTokensAction.NAME, transportService, actionFilters, GetServiceAccountTokensRequest::new); this.nodeName = Node.NODE_NAME_SETTING.get(settings); this.serviceAccountService = serviceAccountService; - this.httpTlsEnabled = XPackSettings.HTTP_SSL_ENABLED.get(settings); - this.transportTlsEnabled = XPackSettings.TRANSPORT_SSL_ENABLED.get(settings); + this.tlsRuntimeCheck = tlsRuntimeCheck; } @Override protected void doExecute(Task task, GetServiceAccountTokensRequest request, ActionListener listener) { - if (false == httpTlsEnabled || false == transportTlsEnabled) { - final ParameterizedMessage message = new ParameterizedMessage( - "Service account APIs require TLS for both HTTP and Transport, " + - "but got HTTP TLS: [{}] and Transport TLS: [{}]", httpTlsEnabled, transportTlsEnabled); - logger.debug(message); - listener.onFailure(new ElasticsearchSecurityException(message.getFormattedMessage(), RestStatus.UNAUTHORIZED)); - return; - } - final ServiceAccountId accountId = new ServiceAccountId(request.getNamespace(), request.getServiceName()); - serviceAccountService.findTokensFor(accountId, nodeName, listener); + tlsRuntimeCheck.checkTlsThenExecute(listener::onFailure, "get service account tokens", () -> { + final ServiceAccountId accountId = new ServiceAccountId(request.getNamespace(), request.getServiceName()); + serviceAccountService.findTokensFor(accountId, nodeName, listener); + }); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStore.java index cc0545ab782d5..86eafc534d059 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStore.java @@ -188,7 +188,8 @@ private XContentBuilder newDocument(Authentication authentication, ServiceAccoun } private TokenInfo extractTokenInfo(String docId, ServiceAccountId accountId) { - final String prefix = SERVICE_ACCOUNT_TOKEN_DOC_TYPE + "-" + accountId.asPrincipal() + "/"; - return TokenInfo.indexToken(Strings.substring(docId, prefix.length(), docId.length())); + // Prefix is SERVICE_ACCOUNT_TOKEN_DOC_TYPE + "-" + accountId.asPrincipal() + "/" + final int prefixLength = SERVICE_ACCOUNT_TOKEN_DOC_TYPE.length() + accountId.asPrincipal().length() + 2; + return TokenInfo.indexToken(Strings.substring(docId, prefixLength, docId.length())); } } 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 b5c6cbfe80598..194a26540c8ff 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 @@ -9,19 +9,17 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.message.ParameterizedMessage; import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.common.settings.SecureString; -import org.elasticsearch.common.settings.Settings; import org.elasticsearch.rest.RestStatus; -import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountTokensResponse; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; +import org.elasticsearch.xpack.security.authc.support.TlsRuntimeCheck; import java.util.Collection; import java.util.Map; @@ -36,13 +34,11 @@ public class ServiceAccountService { private static final Logger logger = LogManager.getLogger(ServiceAccountService.class); private final ServiceAccountsTokenStore serviceAccountsTokenStore; - private final boolean httpTlsEnabled; - private final boolean transportTlsEnabled; + private final TlsRuntimeCheck tlsRuntimeCheck; - public ServiceAccountService(Settings settings, ServiceAccountsTokenStore serviceAccountsTokenStore) { + public ServiceAccountService(ServiceAccountsTokenStore serviceAccountsTokenStore, TlsRuntimeCheck tlsRuntimeCheck) { this.serviceAccountsTokenStore = serviceAccountsTokenStore; - this.httpTlsEnabled = XPackSettings.HTTP_SSL_ENABLED.get(settings); - this.transportTlsEnabled = XPackSettings.TRANSPORT_SSL_ENABLED.get(settings); + this.tlsRuntimeCheck = tlsRuntimeCheck; } public static boolean isServiceAccount(Authentication authentication) { @@ -91,57 +87,45 @@ public void findTokensFor(ServiceAccountId accountId, String nodeName, ActionLis public void authenticateToken(ServiceAccountToken serviceAccountToken, String nodeName, ActionListener listener) { logger.trace("attempt to authenticate service account token [{}]", serviceAccountToken.getQualifiedName()); - if (false == httpTlsEnabled || false == transportTlsEnabled) { - final ParameterizedMessage message = new ParameterizedMessage( - "Service account authentication requires TLS for both HTTP and Transport, " + - "but got HTTP TLS: [{}] and Transport TLS: [{}]", httpTlsEnabled, transportTlsEnabled); - logger.debug(message); - listener.onFailure(new ElasticsearchSecurityException(message.getFormattedMessage(), RestStatus.UNAUTHORIZED)); - return; - } - if (ElasticServiceAccounts.NAMESPACE.equals(serviceAccountToken.getAccountId().namespace()) == false) { - logger.debug("only [{}] service accounts are supported, but received [{}]", - ElasticServiceAccounts.NAMESPACE, serviceAccountToken.getAccountId().asPrincipal()); - listener.onFailure(createAuthenticationException(serviceAccountToken)); - return; - } - - final ServiceAccount account = ACCOUNTS.get(serviceAccountToken.getAccountId().asPrincipal()); - if (account == null) { - logger.debug("the [{}] service account does not exist", serviceAccountToken.getAccountId().asPrincipal()); - listener.onFailure(createAuthenticationException(serviceAccountToken)); - return; - } + tlsRuntimeCheck.checkTlsThenExecute(listener::onFailure, "service account authentication", () -> { + if (ElasticServiceAccounts.NAMESPACE.equals(serviceAccountToken.getAccountId().namespace()) == false) { + logger.debug("only [{}] service accounts are supported, but received [{}]", + ElasticServiceAccounts.NAMESPACE, serviceAccountToken.getAccountId().asPrincipal()); + listener.onFailure(createAuthenticationException(serviceAccountToken)); + return; + } - serviceAccountsTokenStore.authenticate(serviceAccountToken, ActionListener.wrap(success -> { - if (success) { - listener.onResponse(createAuthentication(account, serviceAccountToken, nodeName)); - } else { - final ElasticsearchSecurityException e = createAuthenticationException(serviceAccountToken); - logger.debug(e.getMessage()); - listener.onFailure(e); + final ServiceAccount account = ACCOUNTS.get(serviceAccountToken.getAccountId().asPrincipal()); + if (account == null) { + logger.debug("the [{}] service account does not exist", serviceAccountToken.getAccountId().asPrincipal()); + listener.onFailure(createAuthenticationException(serviceAccountToken)); + return; } - }, listener::onFailure)); + + serviceAccountsTokenStore.authenticate(serviceAccountToken, ActionListener.wrap(success -> { + if (success) { + listener.onResponse(createAuthentication(account, serviceAccountToken, nodeName)); + } else { + final ElasticsearchSecurityException e = createAuthenticationException(serviceAccountToken); + logger.debug(e.getMessage()); + listener.onFailure(e); + } + }, listener::onFailure)); + }); } public void getRoleDescriptor(Authentication authentication, ActionListener listener) { assert isServiceAccount(authentication) : "authentication is not for service account: " + authentication; - if (false == httpTlsEnabled || false == transportTlsEnabled) { - final ParameterizedMessage message = new ParameterizedMessage( - "Service account role descriptor resolving requires TLS for both HTTP and Transport, " + - "but got HTTP TLS: [{}] and Transport TLS: [{}]", httpTlsEnabled, transportTlsEnabled); - logger.debug(message); - listener.onFailure(new ElasticsearchSecurityException(message.getFormattedMessage(), RestStatus.UNAUTHORIZED)); - return; - } - final String principal = authentication.getUser().principal(); - final ServiceAccount account = ACCOUNTS.get(principal); - if (account == null) { - listener.onFailure(new ElasticsearchSecurityException( - "cannot load role for service account [" + principal + "] - no such service account")); - return; - } - listener.onResponse(account.roleDescriptor()); + tlsRuntimeCheck.checkTlsThenExecute(listener::onFailure, "service account role descriptor resolving", () -> { + final String principal = authentication.getUser().principal(); + final ServiceAccount account = ACCOUNTS.get(principal); + if (account == null) { + listener.onFailure(new ElasticsearchSecurityException( + "cannot load role for service account [" + principal + "] - no such service account")); + return; + } + listener.onResponse(account.roleDescriptor()); + }); } private Authentication createAuthentication(ServiceAccount account, ServiceAccountToken token, String nodeName) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/TlsRuntimeCheck.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/TlsRuntimeCheck.java new file mode 100644 index 0000000000000..84ec6aecff90b --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/TlsRuntimeCheck.java @@ -0,0 +1,44 @@ +/* + * 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.authc.support; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.ElasticsearchException; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.xpack.core.XPackSettings; + +import java.util.function.Consumer; + +public class TlsRuntimeCheck { + + private static final Logger logger = LogManager.getLogger(TlsRuntimeCheck.class); + + private final Settings settings; + private final boolean httpTlsEnabled; + private final boolean transportTlsEnabled; + + public TlsRuntimeCheck(Settings settings) { + this.settings = settings; + this.httpTlsEnabled = XPackSettings.HTTP_SSL_ENABLED.get(settings); + this.transportTlsEnabled = XPackSettings.TRANSPORT_SSL_ENABLED.get(settings); + } + + public void checkTlsThenExecute(Consumer exceptionConsumer, String featureName, Runnable andThen) { + if (false == httpTlsEnabled || false == transportTlsEnabled) { + final ParameterizedMessage message = new ParameterizedMessage( + "[{}] requires TLS for both HTTP and Transport, " + + "but got HTTP TLS: [{}] and Transport TLS: [{}]", featureName, httpTlsEnabled, transportTlsEnabled); + logger.debug(message); + exceptionConsumer.accept(new ElasticsearchException(message.getFormattedMessage())); + } else { + andThen.run(); + } + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportCreateServiceAccountTokenActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportCreateServiceAccountTokenActionTests.java index 07ad368845452..bbae4b9b7d274 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportCreateServiceAccountTokenActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportCreateServiceAccountTokenActionTests.java @@ -7,7 +7,7 @@ package org.elasticsearch.xpack.security.action.service; -import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.common.settings.Settings; @@ -19,6 +19,7 @@ import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenResponse; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.security.authc.service.IndexServiceAccountsTokenStore; +import org.elasticsearch.xpack.security.authc.support.TlsRuntimeCheck; import org.junit.Before; import java.util.Collections; @@ -46,7 +47,7 @@ public void init() { .build(); transportCreateServiceAccountTokenAction = new TransportCreateServiceAccountTokenAction( mock(TransportService.class), new ActionFilters(Collections.emptySet()), - settings, indexServiceAccountsTokenStore, securityContext); + indexServiceAccountsTokenStore, securityContext, new TlsRuntimeCheck(settings)); } public void testAuthenticationIsRequired() { @@ -75,12 +76,11 @@ public void testTlsRequired() { .build(); TransportCreateServiceAccountTokenAction action = new TransportCreateServiceAccountTokenAction( mock(TransportService.class), new ActionFilters(Collections.emptySet()), - settings, indexServiceAccountsTokenStore, securityContext); + indexServiceAccountsTokenStore, securityContext, new TlsRuntimeCheck(settings)); final PlainActionFuture future = new PlainActionFuture<>(); action.doExecute(mock(Task.class), mock(CreateServiceAccountTokenRequest.class), future); - final ExecutionException e = expectThrows(ExecutionException.class, () -> future.get()); - assertThat(e.getCause().getClass(), is(ElasticsearchSecurityException.class)); - assertThat(e.getMessage(), containsString("Service account APIs require TLS for both HTTP and Transport")); + final ElasticsearchException e = expectThrows(ElasticsearchException.class, future::actionGet); + assertThat(e.getMessage(), containsString("[create service account token] requires TLS for both HTTP and Transport")); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountTokensActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountTokensActionTests.java index 8c6141bf245b9..697619c1b7cdb 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountTokensActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountTokensActionTests.java @@ -7,7 +7,7 @@ package org.elasticsearch.xpack.security.action.service; -import org.elasticsearch.ElasticsearchSecurityException; +import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.PlainActionFuture; @@ -19,13 +19,12 @@ import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountTokensResponse; import org.elasticsearch.xpack.security.authc.service.ServiceAccount; import org.elasticsearch.xpack.security.authc.service.ServiceAccountService; +import org.elasticsearch.xpack.security.authc.support.TlsRuntimeCheck; import org.junit.Before; import java.util.Collections; -import java.util.concurrent.ExecutionException; import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.is; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -45,7 +44,7 @@ public void init() { serviceAccountService = mock(ServiceAccountService.class); transportGetServiceAccountTokensAction = new TransportGetServiceAccountTokensAction( mock(TransportService.class), new ActionFilters(Collections.emptySet()), - settings, serviceAccountService); + settings, serviceAccountService, new TlsRuntimeCheck(settings)); } public void testDoExecuteWillDelegate() { @@ -68,12 +67,11 @@ public void testTlsRequired() { .build(); final TransportGetServiceAccountTokensAction action = new TransportGetServiceAccountTokensAction( mock(TransportService.class), new ActionFilters(Collections.emptySet()), - settings, mock(ServiceAccountService.class)); + settings, mock(ServiceAccountService.class), new TlsRuntimeCheck(settings)); final PlainActionFuture future = new PlainActionFuture<>(); action.doExecute(mock(Task.class), mock(GetServiceAccountTokensRequest.class), future); - final ExecutionException e = expectThrows(ExecutionException.class, () -> future.get()); - assertThat(e.getCause().getClass(), is(ElasticsearchSecurityException.class)); - assertThat(e.getMessage(), containsString("Service account APIs require TLS for both HTTP and Transport")); + final ElasticsearchException e = expectThrows(ElasticsearchException.class, future::actionGet); + assertThat(e.getMessage(), containsString("[get service account tokens] requires TLS for both HTTP and Transport")); } } 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 834a6a8cbdc1c..2df0fbaaeeef9 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 @@ -10,6 +10,7 @@ import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; @@ -25,6 +26,7 @@ import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; +import org.elasticsearch.xpack.security.authc.support.TlsRuntimeCheck; import org.junit.Before; import java.io.ByteArrayOutputStream; @@ -59,7 +61,7 @@ public void init() { .put("xpack.security.http.ssl.enabled", true) .put("xpack.security.transport.ssl.enabled", true) .build(); - serviceAccountService = new ServiceAccountService(settings, serviceAccountsTokenStore); + serviceAccountService = new ServiceAccountService(serviceAccountsTokenStore, new TlsRuntimeCheck(settings)); } public void testIsServiceAccount() { @@ -400,8 +402,7 @@ ServiceAccountService.REALM_NAME, ServiceAccountService.REALM_TYPE, randomAlphaO Map.of("_token_name", randomAlphaOfLengthBetween(3, 8))); final PlainActionFuture future2 = new PlainActionFuture<>(); serviceAccountService.getRoleDescriptor(auth2, future2); - final ExecutionException e = expectThrows(ExecutionException.class, () -> future2.get()); - assertThat(e.getCause().getClass(), is(ElasticsearchSecurityException.class)); + final ElasticsearchSecurityException e = expectThrows(ElasticsearchSecurityException.class, future2::actionGet); assertThat(e.getMessage(), containsString( "cannot load role for service account [" + username + "] - no such service account")); } @@ -412,13 +413,12 @@ public void testTlsIsRequired() { .put("xpack.security.http.ssl.enabled", httpTls) .put("xpack.security.transport.ssl.enabled", randomFrom(false == httpTls, false)) .build(); - final ServiceAccountService service = new ServiceAccountService(settings, serviceAccountsTokenStore); + final ServiceAccountService service = new ServiceAccountService(serviceAccountsTokenStore, new TlsRuntimeCheck(settings)); final PlainActionFuture future1 = new PlainActionFuture<>(); service.authenticateToken(mock(ServiceAccountToken.class), randomAlphaOfLengthBetween(3, 8), future1); - final ExecutionException e1 = expectThrows(ExecutionException.class, () -> future1.get()); - assertThat(e1.getCause().getClass(), is(ElasticsearchSecurityException.class)); - assertThat(e1.getMessage(), containsString("Service account authentication requires TLS for both HTTP and Transport")); + final ElasticsearchException e1 = expectThrows(ElasticsearchException.class, future1::actionGet); + assertThat(e1.getMessage(), containsString("[service account authentication] requires TLS for both HTTP and Transport")); final PlainActionFuture future2 = new PlainActionFuture<>(); final Authentication authentication = new Authentication(mock(User.class), @@ -426,9 +426,8 @@ public void testTlsIsRequired() { randomAlphaOfLengthBetween(3, 8)), null); service.getRoleDescriptor(authentication, future2); - final ExecutionException e2 = expectThrows(ExecutionException.class, () -> future2.get()); - assertThat(e2.getCause().getClass(), is(ElasticsearchSecurityException.class)); - assertThat(e2.getMessage(), containsString("Service account role descriptor resolving requires TLS for both HTTP and Transport")); + final ElasticsearchException e2 = expectThrows(ElasticsearchException.class, future2::actionGet); + assertThat(e2.getMessage(), containsString("[service account role descriptor resolving] requires TLS for both HTTP and Transport")); } private SecureString createBearerString(List bytesList) throws IOException { From f2ce8f7fa2a11d7bc263bc2abf5f78800ba2d2f8 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Mon, 29 Mar 2021 17:14:51 +1100 Subject: [PATCH 10/14] Apply suggestions from code review Co-authored-by: Tim Vernum --- .../service/CreateServiceAccountTokenResponseTests.java | 6 ++++-- .../authc/service/FileServiceAccountsTokenStore.java | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenResponseTests.java index c69ffce13eec0..a84749b2b0a9e 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenResponseTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenResponseTests.java @@ -28,7 +28,9 @@ protected CreateServiceAccountTokenResponse createTestInstance() { @Override protected CreateServiceAccountTokenResponse mutateInstance(CreateServiceAccountTokenResponse instance) throws IOException { - return CreateServiceAccountTokenResponse.created(randomAlphaOfLengthBetween(3, 8), - new SecureString(randomAlphaOfLength(20).toCharArray())); + return randomBoolean() + ? CreateServiceAccountTokenResponse.created(randomAlphaOfLength(10), instance.getValue()) + : CreateServiceAccountTokenResponse.created(instance.getName(), new SecureString(randomAlphaOfLength(22).toCharArray())) + ; } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStore.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStore.java index 488f161da6a64..9f8a023b95c55 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStore.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStore.java @@ -79,7 +79,7 @@ public void findTokensFor(ServiceAccountId accountId, ActionListener tokenInfos = tokenHashes.keySet() .stream() - .filter(k -> k.startsWith(principal)) + .filter(k -> k.startsWith(principal + "/")) .map(k -> TokenInfo.fileToken(Strings.substring(k, principal.length() + 1, k.length()))) .collect(Collectors.toUnmodifiableList()); listener.onResponse(tokenInfos); From 5b14f199670cf8c05d73a6cdce61a10ca1030247 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Mon, 29 Mar 2021 21:59:17 +1100 Subject: [PATCH 11/14] address feedback --- .../CreateServiceAccountTokenResponse.java | 30 ++++++----------- ...reateServiceAccountTokenResponseTests.java | 11 ++++--- .../xpack/security/Security.java | 13 +++++--- ...nsportCreateServiceAccountTokenAction.java | 10 +++--- ...ransportGetServiceAccountTokensAction.java | 10 +++--- .../authc/service/ServiceAccountService.java | 12 +++---- ...imeCheck.java => HttpTlsRuntimeCheck.java} | 32 +++++++++++++------ .../RestCreateServiceAccountTokenAction.java | 2 ++ ...tCreateServiceAccountTokenActionTests.java | 18 ++++------- ...ortGetServiceAccountTokensActionTests.java | 13 +++----- .../IndexServiceAccountsTokenStoreTests.java | 1 - .../service/ServiceAccountServiceTests.java | 6 ++-- 12 files changed, 81 insertions(+), 77 deletions(-) rename x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/{TlsRuntimeCheck.java => HttpTlsRuntimeCheck.java} (50%) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenResponse.java index 67cf2cfb1399a..605467a3f0290 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenResponse.java @@ -20,29 +20,22 @@ public class CreateServiceAccountTokenResponse extends ActionResponse implements ToXContentObject { - private final boolean created; @Nullable private final String name; @Nullable private final SecureString value; private CreateServiceAccountTokenResponse(boolean created, String name, SecureString value) { - this.created = created; this.name = name; this.value = value; } public CreateServiceAccountTokenResponse(StreamInput in) throws IOException { super(in); - this.created = in.readBoolean(); this.name = in.readOptionalString(); this.value = in.readOptionalSecureString(); } - public boolean isCreated() { - return created; - } - public String getName() { return name; } @@ -53,22 +46,19 @@ public SecureString getValue() { @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - builder.field("created", created); - if (created) { - builder.field("token"); - builder.startObject(); - builder.field("name", name); - builder.field("value", value.toString()); - builder.endObject(); - } - builder.endObject(); + builder.startObject() + .field("created", true) + .field("token") + .startObject() + .field("name", name) + .field("value", value.toString()) + .endObject() + .endObject(); return builder; } @Override public void writeTo(StreamOutput out) throws IOException { - out.writeBoolean(created); out.writeOptionalString(name); out.writeOptionalSecureString(value); } @@ -80,12 +70,12 @@ public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; CreateServiceAccountTokenResponse that = (CreateServiceAccountTokenResponse) o; - return created == that.created && Objects.equals(name, that.name) && Objects.equals(value, that.value); + return Objects.equals(name, that.name) && Objects.equals(value, that.value); } @Override public int hashCode() { - return Objects.hash(created, name, value); + return Objects.hash(name, value); } public static CreateServiceAccountTokenResponse created(String name, SecureString value) { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenResponseTests.java index a84749b2b0a9e..1ade9c0ab9c03 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenResponseTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/service/CreateServiceAccountTokenResponseTests.java @@ -28,9 +28,12 @@ protected CreateServiceAccountTokenResponse createTestInstance() { @Override protected CreateServiceAccountTokenResponse mutateInstance(CreateServiceAccountTokenResponse instance) throws IOException { - return randomBoolean() - ? CreateServiceAccountTokenResponse.created(randomAlphaOfLength(10), instance.getValue()) - : CreateServiceAccountTokenResponse.created(instance.getName(), new SecureString(randomAlphaOfLength(22).toCharArray())) - ; + if (randomBoolean()) { + return CreateServiceAccountTokenResponse.created( + randomValueOtherThan(instance.getName(), () -> randomAlphaOfLengthBetween(3, 8)), instance.getValue()); + } else { + return CreateServiceAccountTokenResponse.created(instance.getName(), + randomValueOtherThan(instance.getValue(), () -> new SecureString(randomAlphaOfLength(22).toCharArray()))); + } } } 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 58acb54c26a43..9e1eb084bd0ee 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 @@ -204,12 +204,13 @@ import org.elasticsearch.xpack.security.authc.TokenService; import org.elasticsearch.xpack.security.authc.esnative.NativeUsersStore; import org.elasticsearch.xpack.security.authc.esnative.ReservedRealm; +import org.elasticsearch.xpack.security.authc.service.CachingServiceAccountsTokenStore; import org.elasticsearch.xpack.security.authc.service.FileServiceAccountsTokenStore; import org.elasticsearch.xpack.security.authc.service.IndexServiceAccountsTokenStore; import org.elasticsearch.xpack.security.authc.service.ServiceAccountService; import org.elasticsearch.xpack.security.authc.service.CompositeServiceAccountsTokenStore; import org.elasticsearch.xpack.security.authc.support.SecondaryAuthenticator; -import org.elasticsearch.xpack.security.authc.support.TlsRuntimeCheck; +import org.elasticsearch.xpack.security.authc.support.HttpTlsRuntimeCheck; import org.elasticsearch.xpack.security.authc.support.mapper.NativeRoleMappingStore; import org.elasticsearch.xpack.security.authz.AuthorizationService; import org.elasticsearch.xpack.security.authz.SecuritySearchOperationListener; @@ -500,8 +501,8 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste clusterService, cacheInvalidatorRegistry, threadPool); components.add(apiKeyService); - final TlsRuntimeCheck tlsRuntimeCheck = new TlsRuntimeCheck(settings); - components.add(tlsRuntimeCheck); + final HttpTlsRuntimeCheck httpTlsRuntimeCheck = new HttpTlsRuntimeCheck(settings); + components.add(httpTlsRuntimeCheck); final IndexServiceAccountsTokenStore indexServiceAccountsTokenStore = new IndexServiceAccountsTokenStore( settings, threadPool, getClock(), client, securityIndex.get(), clusterService, cacheInvalidatorRegistry); @@ -511,8 +512,7 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste new FileServiceAccountsTokenStore(environment, resourceWatcherService, threadPool); final ServiceAccountService serviceAccountService = new ServiceAccountService(new CompositeServiceAccountsTokenStore( - List.of(fileServiceAccountsTokenStore, indexServiceAccountsTokenStore), threadPool.getThreadContext()), - tlsRuntimeCheck); + List.of(fileServiceAccountsTokenStore, indexServiceAccountsTokenStore), threadPool.getThreadContext()), httpTlsRuntimeCheck); components.add(serviceAccountService); final CompositeRolesStore allRolesStore = new CompositeRolesStore(settings, fileRolesStore, nativeRolesStore, reservedRolesStore, @@ -736,6 +736,9 @@ public static List> getSettings(List securityExten settingsList.add(NativePrivilegeStore.CACHE_MAX_APPLICATIONS_SETTING); settingsList.add(NativePrivilegeStore.CACHE_TTL_SETTING); settingsList.add(OPERATOR_PRIVILEGES_ENABLED); + settingsList.add(CachingServiceAccountsTokenStore.CACHE_TTL_SETTING); + settingsList.add(CachingServiceAccountsTokenStore.CACHE_HASH_ALGO_SETTING); + settingsList.add(CachingServiceAccountsTokenStore.CACHE_MAX_TOKENS_SETTING); // hide settings settingsList.add(Setting.listSetting(SecurityField.setting("hide_settings"), Collections.emptyList(), Function.identity(), diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportCreateServiceAccountTokenAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportCreateServiceAccountTokenAction.java index 9fe0d26a0a4d2..d7dfc9a215b46 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportCreateServiceAccountTokenAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportCreateServiceAccountTokenAction.java @@ -19,30 +19,30 @@ import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenResponse; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.security.authc.service.IndexServiceAccountsTokenStore; -import org.elasticsearch.xpack.security.authc.support.TlsRuntimeCheck; +import org.elasticsearch.xpack.security.authc.support.HttpTlsRuntimeCheck; public class TransportCreateServiceAccountTokenAction extends HandledTransportAction { private final IndexServiceAccountsTokenStore indexServiceAccountsTokenStore; private final SecurityContext securityContext; - private final TlsRuntimeCheck tlsRuntimeCheck; + private final HttpTlsRuntimeCheck httpTlsRuntimeCheck; @Inject public TransportCreateServiceAccountTokenAction(TransportService transportService, ActionFilters actionFilters, IndexServiceAccountsTokenStore indexServiceAccountsTokenStore, SecurityContext securityContext, - TlsRuntimeCheck tlsRuntimeCheck) { + HttpTlsRuntimeCheck httpTlsRuntimeCheck) { super(CreateServiceAccountTokenAction.NAME, transportService, actionFilters, CreateServiceAccountTokenRequest::new); this.indexServiceAccountsTokenStore = indexServiceAccountsTokenStore; this.securityContext = securityContext; - this.tlsRuntimeCheck = tlsRuntimeCheck; + this.httpTlsRuntimeCheck = httpTlsRuntimeCheck; } @Override protected void doExecute(Task task, CreateServiceAccountTokenRequest request, ActionListener listener) { - tlsRuntimeCheck.checkTlsThenExecute(listener::onFailure, "create service account token", () -> { + httpTlsRuntimeCheck.checkTlsThenExecute(listener::onFailure, "create service account token", () -> { final Authentication authentication = securityContext.getAuthentication(); if (authentication == null) { listener.onFailure(new IllegalStateException("authentication is required")); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountTokensAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountTokensAction.java index 496a4c8c2ca16..2b3b1ce3a5b40 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountTokensAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountTokensAction.java @@ -20,29 +20,29 @@ import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountTokensResponse; import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; import org.elasticsearch.xpack.security.authc.service.ServiceAccountService; -import org.elasticsearch.xpack.security.authc.support.TlsRuntimeCheck; +import org.elasticsearch.xpack.security.authc.support.HttpTlsRuntimeCheck; public class TransportGetServiceAccountTokensAction extends HandledTransportAction { private final ServiceAccountService serviceAccountService; - private final TlsRuntimeCheck tlsRuntimeCheck; + private final HttpTlsRuntimeCheck httpTlsRuntimeCheck; private final String nodeName; @Inject public TransportGetServiceAccountTokensAction(TransportService transportService, ActionFilters actionFilters, Settings settings, ServiceAccountService serviceAccountService, - TlsRuntimeCheck tlsRuntimeCheck) { + HttpTlsRuntimeCheck httpTlsRuntimeCheck) { super(GetServiceAccountTokensAction.NAME, transportService, actionFilters, GetServiceAccountTokensRequest::new); this.nodeName = Node.NODE_NAME_SETTING.get(settings); this.serviceAccountService = serviceAccountService; - this.tlsRuntimeCheck = tlsRuntimeCheck; + this.httpTlsRuntimeCheck = httpTlsRuntimeCheck; } @Override protected void doExecute(Task task, GetServiceAccountTokensRequest request, ActionListener listener) { - tlsRuntimeCheck.checkTlsThenExecute(listener::onFailure, "get service account tokens", () -> { + httpTlsRuntimeCheck.checkTlsThenExecute(listener::onFailure, "get service account tokens", () -> { final ServiceAccountId accountId = new ServiceAccountId(request.getNamespace(), request.getServiceName()); serviceAccountService.findTokensFor(accountId, nodeName, listener); }); 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 194a26540c8ff..2e3348b8d3e09 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 @@ -19,7 +19,7 @@ import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; -import org.elasticsearch.xpack.security.authc.support.TlsRuntimeCheck; +import org.elasticsearch.xpack.security.authc.support.HttpTlsRuntimeCheck; import java.util.Collection; import java.util.Map; @@ -34,11 +34,11 @@ public class ServiceAccountService { private static final Logger logger = LogManager.getLogger(ServiceAccountService.class); private final ServiceAccountsTokenStore serviceAccountsTokenStore; - private final TlsRuntimeCheck tlsRuntimeCheck; + private final HttpTlsRuntimeCheck httpTlsRuntimeCheck; - public ServiceAccountService(ServiceAccountsTokenStore serviceAccountsTokenStore, TlsRuntimeCheck tlsRuntimeCheck) { + public ServiceAccountService(ServiceAccountsTokenStore serviceAccountsTokenStore, HttpTlsRuntimeCheck httpTlsRuntimeCheck) { this.serviceAccountsTokenStore = serviceAccountsTokenStore; - this.tlsRuntimeCheck = tlsRuntimeCheck; + this.httpTlsRuntimeCheck = httpTlsRuntimeCheck; } public static boolean isServiceAccount(Authentication authentication) { @@ -87,7 +87,7 @@ public void findTokensFor(ServiceAccountId accountId, String nodeName, ActionLis public void authenticateToken(ServiceAccountToken serviceAccountToken, String nodeName, ActionListener listener) { logger.trace("attempt to authenticate service account token [{}]", serviceAccountToken.getQualifiedName()); - tlsRuntimeCheck.checkTlsThenExecute(listener::onFailure, "service account authentication", () -> { + httpTlsRuntimeCheck.checkTlsThenExecute(listener::onFailure, "service account authentication", () -> { if (ElasticServiceAccounts.NAMESPACE.equals(serviceAccountToken.getAccountId().namespace()) == false) { logger.debug("only [{}] service accounts are supported, but received [{}]", ElasticServiceAccounts.NAMESPACE, serviceAccountToken.getAccountId().asPrincipal()); @@ -116,7 +116,7 @@ public void authenticateToken(ServiceAccountToken serviceAccountToken, String no public void getRoleDescriptor(Authentication authentication, ActionListener listener) { assert isServiceAccount(authentication) : "authentication is not for service account: " + authentication; - tlsRuntimeCheck.checkTlsThenExecute(listener::onFailure, "service account role descriptor resolving", () -> { + httpTlsRuntimeCheck.checkTlsThenExecute(listener::onFailure, "service account role descriptor resolving", () -> { final String principal = authentication.getUser().principal(); final ServiceAccount account = ACCOUNTS.get(principal); if (account == null) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/TlsRuntimeCheck.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/HttpTlsRuntimeCheck.java similarity index 50% rename from x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/TlsRuntimeCheck.java rename to x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/HttpTlsRuntimeCheck.java index 84ec6aecff90b..8368f15e607e3 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/TlsRuntimeCheck.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/HttpTlsRuntimeCheck.java @@ -12,29 +12,43 @@ import org.apache.logging.log4j.message.ParameterizedMessage; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.discovery.DiscoveryModule; +import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.core.XPackSettings; +import java.util.Arrays; import java.util.function.Consumer; -public class TlsRuntimeCheck { +public class HttpTlsRuntimeCheck { - private static final Logger logger = LogManager.getLogger(TlsRuntimeCheck.class); + private static final Logger logger = LogManager.getLogger(HttpTlsRuntimeCheck.class); private final Settings settings; - private final boolean httpTlsEnabled; - private final boolean transportTlsEnabled; + private final Boolean httpTlsEnabled; + private final boolean enforce; - public TlsRuntimeCheck(Settings settings) { + public HttpTlsRuntimeCheck(Settings settings) { + this(settings, null); + } + + public HttpTlsRuntimeCheck(Settings settings, TransportService transportService) { this.settings = settings; this.httpTlsEnabled = XPackSettings.HTTP_SSL_ENABLED.get(settings); - this.transportTlsEnabled = XPackSettings.TRANSPORT_SSL_ENABLED.get(settings); + if (transportService != null) { + final boolean boundToLocal = Arrays.stream(transportService.boundAddress().boundAddresses()) + .allMatch(b -> b.address().getAddress().isLoopbackAddress()) + && transportService.boundAddress().publishAddress().address().getAddress().isLoopbackAddress(); + this.enforce = false == boundToLocal && false == "single-node".equals(DiscoveryModule.DISCOVERY_TYPE_SETTING.get(settings)); + } else { + enforce = true; + } + } public void checkTlsThenExecute(Consumer exceptionConsumer, String featureName, Runnable andThen) { - if (false == httpTlsEnabled || false == transportTlsEnabled) { + if (enforce && false == httpTlsEnabled) { final ParameterizedMessage message = new ParameterizedMessage( - "[{}] requires TLS for both HTTP and Transport, " + - "but got HTTP TLS: [{}] and Transport TLS: [{}]", featureName, httpTlsEnabled, transportTlsEnabled); + "[{}] requires TLS for the HTTP interface", featureName); logger.debug(message); exceptionConsumer.accept(new ElasticsearchException(message.getFormattedMessage())); } else { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/service/RestCreateServiceAccountTokenAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/service/RestCreateServiceAccountTokenAction.java index 73c93e9769fc6..e3461ba7ac781 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/service/RestCreateServiceAccountTokenAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/service/RestCreateServiceAccountTokenAction.java @@ -23,6 +23,7 @@ import java.util.List; import static org.elasticsearch.rest.RestRequest.Method.POST; +import static org.elasticsearch.rest.RestRequest.Method.PUT; public class RestCreateServiceAccountTokenAction extends SecurityBaseRestHandler { @@ -34,6 +35,7 @@ public RestCreateServiceAccountTokenAction(Settings settings, XPackLicenseState public List routes() { return List.of( new Route(POST, "/_security/service/{namespace}/{service}/credential/token/{name}"), + new Route(PUT, "/_security/service/{namespace}/{service}/credential/token/{name}"), new Route(POST, "/_security/service/{namespace}/{service}/credential/token")); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportCreateServiceAccountTokenActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportCreateServiceAccountTokenActionTests.java index bbae4b9b7d274..bc6936bc1bb33 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportCreateServiceAccountTokenActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportCreateServiceAccountTokenActionTests.java @@ -19,7 +19,7 @@ import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenResponse; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.security.authc.service.IndexServiceAccountsTokenStore; -import org.elasticsearch.xpack.security.authc.support.TlsRuntimeCheck; +import org.elasticsearch.xpack.security.authc.support.HttpTlsRuntimeCheck; import org.junit.Before; import java.util.Collections; @@ -43,20 +43,18 @@ public void init() { securityContext = mock(SecurityContext.class); final Settings settings = Settings.builder() .put("xpack.security.http.ssl.enabled", true) - .put("xpack.security.transport.ssl.enabled", true) .build(); transportCreateServiceAccountTokenAction = new TransportCreateServiceAccountTokenAction( mock(TransportService.class), new ActionFilters(Collections.emptySet()), - indexServiceAccountsTokenStore, securityContext, new TlsRuntimeCheck(settings)); + indexServiceAccountsTokenStore, securityContext, new HttpTlsRuntimeCheck(settings)); } public void testAuthenticationIsRequired() { when(securityContext.getAuthentication()).thenReturn(null); final PlainActionFuture future = new PlainActionFuture<>(); transportCreateServiceAccountTokenAction.doExecute(mock(Task.class), mock(CreateServiceAccountTokenRequest.class), future); - final ExecutionException e = expectThrows(ExecutionException.class, () -> future.get()); - assertThat(e.getCause().getClass(), is(IllegalStateException.class)); - assertThat(e.getCause().getMessage(), containsString("authentication is required")); + final IllegalStateException e = expectThrows(IllegalStateException.class, future::actionGet); + assertThat(e.getMessage(), containsString("authentication is required")); } public void testExecutionWillDelegate() { @@ -69,18 +67,16 @@ public void testExecutionWillDelegate() { } public void testTlsRequired() { - final boolean httpTls = randomBoolean(); final Settings settings = Settings.builder() - .put("xpack.security.http.ssl.enabled", httpTls) - .put("xpack.security.transport.ssl.enabled", randomFrom(false == httpTls, false)) + .put("xpack.security.http.ssl.enabled", false) .build(); TransportCreateServiceAccountTokenAction action = new TransportCreateServiceAccountTokenAction( mock(TransportService.class), new ActionFilters(Collections.emptySet()), - indexServiceAccountsTokenStore, securityContext, new TlsRuntimeCheck(settings)); + indexServiceAccountsTokenStore, securityContext, new HttpTlsRuntimeCheck(settings)); final PlainActionFuture future = new PlainActionFuture<>(); action.doExecute(mock(Task.class), mock(CreateServiceAccountTokenRequest.class), future); final ElasticsearchException e = expectThrows(ElasticsearchException.class, future::actionGet); - assertThat(e.getMessage(), containsString("[create service account token] requires TLS for both HTTP and Transport")); + assertThat(e.getMessage(), containsString("[create service account token] requires TLS for the HTTP interface")); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountTokensActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountTokensActionTests.java index 697619c1b7cdb..d3f54ca4c87b1 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountTokensActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountTokensActionTests.java @@ -19,7 +19,7 @@ import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountTokensResponse; import org.elasticsearch.xpack.security.authc.service.ServiceAccount; import org.elasticsearch.xpack.security.authc.service.ServiceAccountService; -import org.elasticsearch.xpack.security.authc.support.TlsRuntimeCheck; +import org.elasticsearch.xpack.security.authc.support.HttpTlsRuntimeCheck; import org.junit.Before; import java.util.Collections; @@ -39,12 +39,11 @@ public void init() { final Settings settings = Settings.builder() .put("node.name", "node_name") .put("xpack.security.http.ssl.enabled", true) - .put("xpack.security.transport.ssl.enabled", true) .build(); serviceAccountService = mock(ServiceAccountService.class); transportGetServiceAccountTokensAction = new TransportGetServiceAccountTokensAction( mock(TransportService.class), new ActionFilters(Collections.emptySet()), - settings, serviceAccountService, new TlsRuntimeCheck(settings)); + settings, serviceAccountService, new HttpTlsRuntimeCheck(settings)); } public void testDoExecuteWillDelegate() { @@ -60,18 +59,16 @@ public void testDoExecuteWillDelegate() { } public void testTlsRequired() { - final boolean httpTls = randomBoolean(); final Settings settings = Settings.builder() - .put("xpack.security.http.ssl.enabled", httpTls) - .put("xpack.security.transport.ssl.enabled", randomFrom(false == httpTls, false)) + .put("xpack.security.http.ssl.enabled", false) .build(); final TransportGetServiceAccountTokensAction action = new TransportGetServiceAccountTokensAction( mock(TransportService.class), new ActionFilters(Collections.emptySet()), - settings, mock(ServiceAccountService.class), new TlsRuntimeCheck(settings)); + settings, mock(ServiceAccountService.class), new HttpTlsRuntimeCheck(settings)); final PlainActionFuture future = new PlainActionFuture<>(); action.doExecute(mock(Task.class), mock(GetServiceAccountTokensRequest.class), future); final ElasticsearchException e = expectThrows(ElasticsearchException.class, future::actionGet); - assertThat(e.getMessage(), containsString("[get service account tokens] requires TLS for both HTTP and Transport")); + assertThat(e.getMessage(), containsString("[get service account tokens] requires TLS for the HTTP interface")); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStoreTests.java index 1e28e96c805ef..2d4722d838347 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStoreTests.java @@ -196,7 +196,6 @@ public void testCreateToken() throws ExecutionException, InterruptedException { final CreateServiceAccountTokenResponse createServiceAccountTokenResponse1 = future1.get(); assertNotNull(createServiceAccountTokenResponse1); - assertThat(createServiceAccountTokenResponse1.isCreated(), is(true)); assertThat(createServiceAccountTokenResponse1.getName(), equalTo(request.getTokenName())); assertNotNull(createServiceAccountTokenResponse1.getValue()); 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 2df0fbaaeeef9..a146848bacdbb 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 @@ -26,7 +26,7 @@ import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.service.ServiceAccount.ServiceAccountId; -import org.elasticsearch.xpack.security.authc.support.TlsRuntimeCheck; +import org.elasticsearch.xpack.security.authc.support.HttpTlsRuntimeCheck; import org.junit.Before; import java.io.ByteArrayOutputStream; @@ -61,7 +61,7 @@ public void init() { .put("xpack.security.http.ssl.enabled", true) .put("xpack.security.transport.ssl.enabled", true) .build(); - serviceAccountService = new ServiceAccountService(serviceAccountsTokenStore, new TlsRuntimeCheck(settings)); + serviceAccountService = new ServiceAccountService(serviceAccountsTokenStore, new HttpTlsRuntimeCheck(settings)); } public void testIsServiceAccount() { @@ -413,7 +413,7 @@ public void testTlsIsRequired() { .put("xpack.security.http.ssl.enabled", httpTls) .put("xpack.security.transport.ssl.enabled", randomFrom(false == httpTls, false)) .build(); - final ServiceAccountService service = new ServiceAccountService(serviceAccountsTokenStore, new TlsRuntimeCheck(settings)); + final ServiceAccountService service = new ServiceAccountService(serviceAccountsTokenStore, new HttpTlsRuntimeCheck(settings)); final PlainActionFuture future1 = new PlainActionFuture<>(); service.authenticateToken(mock(ServiceAccountToken.class), randomAlphaOfLengthBetween(3, 8), future1); From f4af54dd4ea7391a75fa0665114b52c73bbfe200 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Mon, 29 Mar 2021 22:49:14 +1100 Subject: [PATCH 12/14] improve http tls runtime check --- .../xpack/security/Security.java | 15 +++-- .../authc/support/HttpTlsRuntimeCheck.java | 59 +++++++++++-------- ...tCreateServiceAccountTokenActionTests.java | 40 ++++++++++--- ...ortGetServiceAccountTokensActionTests.java | 39 ++++++++++-- .../service/ServiceAccountServiceTests.java | 50 ++++++++++++---- 5 files changed, 147 insertions(+), 56 deletions(-) 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 9e1eb084bd0ee..87da11c48c337 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 @@ -349,6 +349,7 @@ public class Security extends Plugin implements SystemIndexPlugin, IngestPlugin, private final SetOnce dlsBitsetCache = new SetOnce<>(); private final SetOnce> bootstrapChecks = new SetOnce<>(); private final List securityExtensions = new ArrayList<>(); + private final SetOnce transportReference = new SetOnce<>(); public Security(Settings settings, final Path configPath) { this(settings, configPath, Collections.emptyList()); @@ -501,7 +502,7 @@ Collection createComponents(Client client, ThreadPool threadPool, Cluste clusterService, cacheInvalidatorRegistry, threadPool); components.add(apiKeyService); - final HttpTlsRuntimeCheck httpTlsRuntimeCheck = new HttpTlsRuntimeCheck(settings); + final HttpTlsRuntimeCheck httpTlsRuntimeCheck = new HttpTlsRuntimeCheck(settings, transportReference); components.add(httpTlsRuntimeCheck); final IndexServiceAccountsTokenStore indexServiceAccountsTokenStore = new IndexServiceAccountsTokenStore( @@ -1042,7 +1043,8 @@ public Map> getTransports(Settings settings, ThreadP return Map.of( // security based on Netty 4 SecurityField.NAME4, - () -> new SecurityNetty4ServerTransport( + () -> { + transportReference.set(new SecurityNetty4ServerTransport( settings, Version.CURRENT, threadPool, @@ -1052,10 +1054,13 @@ public Map> getTransports(Settings settings, ThreadP circuitBreakerService, ipFilter, getSslService(), - getNettySharedGroupFactory(settings)), + getNettySharedGroupFactory(settings))); + return transportReference.get(); + }, // security based on NIO SecurityField.NIO, - () -> new SecurityNioTransport(settings, + () -> { + transportReference.set(new SecurityNioTransport(settings, Version.CURRENT, threadPool, networkService, @@ -1065,6 +1070,8 @@ public Map> getTransports(Settings settings, ThreadP ipFilter, getSslService(), getNioGroupFactory(settings))); + return transportReference.get(); + }); } @Override diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/HttpTlsRuntimeCheck.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/HttpTlsRuntimeCheck.java index 8368f15e607e3..7b297229aa61f 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/HttpTlsRuntimeCheck.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/HttpTlsRuntimeCheck.java @@ -10,49 +10,58 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.message.ParameterizedMessage; +import org.apache.lucene.util.SetOnce; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.discovery.DiscoveryModule; -import org.elasticsearch.transport.TransportService; +import org.elasticsearch.transport.Transport; import org.elasticsearch.xpack.core.XPackSettings; import java.util.Arrays; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; public class HttpTlsRuntimeCheck { private static final Logger logger = LogManager.getLogger(HttpTlsRuntimeCheck.class); - private final Settings settings; + private final AtomicBoolean initialized = new AtomicBoolean(false); private final Boolean httpTlsEnabled; - private final boolean enforce; + private final SetOnce transportReference; + private final Boolean securityEnabled; + private final boolean singleNodeDiscovery; + private boolean enforce; - public HttpTlsRuntimeCheck(Settings settings) { - this(settings, null); - } - - public HttpTlsRuntimeCheck(Settings settings, TransportService transportService) { - this.settings = settings; + public HttpTlsRuntimeCheck(Settings settings, SetOnce transportReference) { + this.transportReference = transportReference; + this.securityEnabled = XPackSettings.SECURITY_ENABLED.get(settings); this.httpTlsEnabled = XPackSettings.HTTP_SSL_ENABLED.get(settings); - if (transportService != null) { - final boolean boundToLocal = Arrays.stream(transportService.boundAddress().boundAddresses()) - .allMatch(b -> b.address().getAddress().isLoopbackAddress()) - && transportService.boundAddress().publishAddress().address().getAddress().isLoopbackAddress(); - this.enforce = false == boundToLocal && false == "single-node".equals(DiscoveryModule.DISCOVERY_TYPE_SETTING.get(settings)); - } else { - enforce = true; - } - + this.singleNodeDiscovery = "single-node".equals(DiscoveryModule.DISCOVERY_TYPE_SETTING.get(settings)); } public void checkTlsThenExecute(Consumer exceptionConsumer, String featureName, Runnable andThen) { - if (enforce && false == httpTlsEnabled) { - final ParameterizedMessage message = new ParameterizedMessage( - "[{}] requires TLS for the HTTP interface", featureName); - logger.debug(message); - exceptionConsumer.accept(new ElasticsearchException(message.getFormattedMessage())); - } else { - andThen.run(); + // If security is enabled, but TLS is not enabled for the HTTP interface + if (securityEnabled && false == httpTlsEnabled) { + if (false == initialized.get()) { + final Transport transport = transportReference.get(); + if (transport == null) { + exceptionConsumer.accept(new ElasticsearchException("transport cannot be null")); + return; + } + final boolean boundToLocal = Arrays.stream(transport.boundAddress().boundAddresses()) + .allMatch(b -> b.address().getAddress().isLoopbackAddress()) + && transport.boundAddress().publishAddress().address().getAddress().isLoopbackAddress(); + this.enforce = false == boundToLocal && false == singleNodeDiscovery; + initialized.set(true); + } + if (enforce) { + final ParameterizedMessage message = new ParameterizedMessage( + "[{}] requires TLS for the HTTP interface", featureName); + logger.debug(message); + exceptionConsumer.accept(new ElasticsearchException(message.getFormattedMessage())); + return; + } } + andThen.run(); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportCreateServiceAccountTokenActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportCreateServiceAccountTokenActionTests.java index bc6936bc1bb33..d661e7e7c7bba 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportCreateServiceAccountTokenActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportCreateServiceAccountTokenActionTests.java @@ -7,12 +7,16 @@ package org.elasticsearch.xpack.security.action.service; +import org.apache.lucene.util.SetOnce; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.BoundTransportAddress; +import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.tasks.Task; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.transport.Transport; import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.core.security.SecurityContext; import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenRequest; @@ -21,12 +25,13 @@ import org.elasticsearch.xpack.security.authc.service.IndexServiceAccountsTokenStore; import org.elasticsearch.xpack.security.authc.support.HttpTlsRuntimeCheck; import org.junit.Before; +import org.mockito.Mockito; +import java.io.IOException; +import java.net.InetAddress; import java.util.Collections; -import java.util.concurrent.ExecutionException; import static org.hamcrest.Matchers.containsString; -import static org.hamcrest.Matchers.is; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -36,17 +41,31 @@ public class TransportCreateServiceAccountTokenActionTests extends ESTestCase { private IndexServiceAccountsTokenStore indexServiceAccountsTokenStore; private SecurityContext securityContext; private TransportCreateServiceAccountTokenAction transportCreateServiceAccountTokenAction; + private Transport transport; @Before - public void init() { + public void init() throws IOException { indexServiceAccountsTokenStore = mock(IndexServiceAccountsTokenStore.class); securityContext = mock(SecurityContext.class); - final Settings settings = Settings.builder() - .put("xpack.security.http.ssl.enabled", true) - .build(); + final Settings.Builder builder = Settings.builder() + .put("xpack.security.enabled", true); + transport = mock(Transport.class); + final TransportAddress transportAddress; + if (randomBoolean()) { + transportAddress = new TransportAddress(TransportAddress.META_ADDRESS, 9300); + } else { + transportAddress = new TransportAddress(InetAddress.getLocalHost(), 9300); + } + if (randomBoolean()) { + builder.put("xpack.security.http.ssl.enabled", true); + } else { + builder.put("discovery.type", "single-node"); + } + when(transport.boundAddress()).thenReturn( + new BoundTransportAddress(new TransportAddress[] { transportAddress }, transportAddress)); transportCreateServiceAccountTokenAction = new TransportCreateServiceAccountTokenAction( mock(TransportService.class), new ActionFilters(Collections.emptySet()), - indexServiceAccountsTokenStore, securityContext, new HttpTlsRuntimeCheck(settings)); + indexServiceAccountsTokenStore, securityContext, new HttpTlsRuntimeCheck(builder.build(), new SetOnce<>(transport))); } public void testAuthenticationIsRequired() { @@ -67,12 +86,17 @@ public void testExecutionWillDelegate() { } public void testTlsRequired() { + Mockito.reset(transport); final Settings settings = Settings.builder() .put("xpack.security.http.ssl.enabled", false) .build(); + final TransportAddress transportAddress = new TransportAddress(TransportAddress.META_ADDRESS, 9300); + when(transport.boundAddress()).thenReturn( + new BoundTransportAddress(new TransportAddress[] { transportAddress }, transportAddress)); + TransportCreateServiceAccountTokenAction action = new TransportCreateServiceAccountTokenAction( mock(TransportService.class), new ActionFilters(Collections.emptySet()), - indexServiceAccountsTokenStore, securityContext, new HttpTlsRuntimeCheck(settings)); + indexServiceAccountsTokenStore, securityContext, new HttpTlsRuntimeCheck(settings, new SetOnce<>(transport))); final PlainActionFuture future = new PlainActionFuture<>(); action.doExecute(mock(Task.class), mock(CreateServiceAccountTokenRequest.class), future); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountTokensActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountTokensActionTests.java index d3f54ca4c87b1..7cf21bca3e8e9 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountTokensActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountTokensActionTests.java @@ -7,13 +7,17 @@ package org.elasticsearch.xpack.security.action.service; +import org.apache.lucene.util.SetOnce; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.BoundTransportAddress; +import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.tasks.Task; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.transport.Transport; import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountTokensRequest; import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountTokensResponse; @@ -21,29 +25,48 @@ import org.elasticsearch.xpack.security.authc.service.ServiceAccountService; import org.elasticsearch.xpack.security.authc.support.HttpTlsRuntimeCheck; import org.junit.Before; +import org.mockito.Mockito; +import java.net.InetAddress; +import java.net.UnknownHostException; import java.util.Collections; import static org.hamcrest.Matchers.containsString; import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; public class TransportGetServiceAccountTokensActionTests extends ESTestCase { private TransportGetServiceAccountTokensAction transportGetServiceAccountTokensAction; private ServiceAccountService serviceAccountService; + private Transport transport; @Before - public void init() { - final Settings settings = Settings.builder() + public void init() throws UnknownHostException { + final Settings.Builder builder = Settings.builder() .put("node.name", "node_name") - .put("xpack.security.http.ssl.enabled", true) - .build(); + .put("xpack.security.enabled", true); + transport = mock(Transport.class); + final TransportAddress transportAddress; + if (randomBoolean()) { + transportAddress = new TransportAddress(TransportAddress.META_ADDRESS, 9300); + } else { + transportAddress = new TransportAddress(InetAddress.getLocalHost(), 9300); + } + if (randomBoolean()) { + builder.put("xpack.security.http.ssl.enabled", true); + } else { + builder.put("discovery.type", "single-node"); + } + when(transport.boundAddress()).thenReturn( + new BoundTransportAddress(new TransportAddress[] { transportAddress }, transportAddress)); + final Settings settings = builder.build(); serviceAccountService = mock(ServiceAccountService.class); transportGetServiceAccountTokensAction = new TransportGetServiceAccountTokensAction( mock(TransportService.class), new ActionFilters(Collections.emptySet()), - settings, serviceAccountService, new HttpTlsRuntimeCheck(settings)); + settings, serviceAccountService, new HttpTlsRuntimeCheck(settings, new SetOnce<>(transport))); } public void testDoExecuteWillDelegate() { @@ -59,12 +82,16 @@ public void testDoExecuteWillDelegate() { } public void testTlsRequired() { + Mockito.reset(transport); final Settings settings = Settings.builder() .put("xpack.security.http.ssl.enabled", false) .build(); + final TransportAddress transportAddress = new TransportAddress(TransportAddress.META_ADDRESS, 9300); + when(transport.boundAddress()).thenReturn( + new BoundTransportAddress(new TransportAddress[] { transportAddress }, transportAddress)); final TransportGetServiceAccountTokensAction action = new TransportGetServiceAccountTokensAction( mock(TransportService.class), new ActionFilters(Collections.emptySet()), - settings, mock(ServiceAccountService.class), new HttpTlsRuntimeCheck(settings)); + settings, mock(ServiceAccountService.class), new HttpTlsRuntimeCheck(settings, new SetOnce<>(transport))); final PlainActionFuture future = new PlainActionFuture<>(); action.doExecute(mock(Task.class), mock(GetServiceAccountTokensRequest.class), future); 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 a146848bacdbb..4af541fef098b 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 @@ -10,6 +10,7 @@ import org.apache.logging.log4j.Level; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.apache.lucene.util.SetOnce; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.Version; @@ -19,9 +20,12 @@ import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.transport.BoundTransportAddress; +import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.MockLogAppender; +import org.elasticsearch.transport.Transport; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.user.User; @@ -31,6 +35,8 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.net.InetAddress; +import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Base64; @@ -46,22 +52,37 @@ import static org.mockito.Matchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class ServiceAccountServiceTests extends ESTestCase { private ThreadContext threadContext; private ServiceAccountsTokenStore serviceAccountsTokenStore; private ServiceAccountService serviceAccountService; + private Transport transport; @Before - public void init() { + public void init() throws UnknownHostException { threadContext = new ThreadContext(Settings.EMPTY); serviceAccountsTokenStore = mock(ServiceAccountsTokenStore.class); - final Settings settings = Settings.builder() - .put("xpack.security.http.ssl.enabled", true) - .put("xpack.security.transport.ssl.enabled", true) - .build(); - serviceAccountService = new ServiceAccountService(serviceAccountsTokenStore, new HttpTlsRuntimeCheck(settings)); + final Settings.Builder builder = Settings.builder() + .put("xpack.security.enabled", true); + transport = mock(Transport.class); + final TransportAddress transportAddress; + if (randomBoolean()) { + transportAddress = new TransportAddress(TransportAddress.META_ADDRESS, 9300); + } else { + transportAddress = new TransportAddress(InetAddress.getLocalHost(), 9300); + } + if (randomBoolean()) { + builder.put("xpack.security.http.ssl.enabled", true); + } else { + builder.put("discovery.type", "single-node"); + } + when(transport.boundAddress()).thenReturn( + new BoundTransportAddress(new TransportAddress[] { transportAddress }, transportAddress)); + serviceAccountService = new ServiceAccountService(serviceAccountsTokenStore, + new HttpTlsRuntimeCheck(builder.build(), new SetOnce<>(transport))); } public void testIsServiceAccount() { @@ -407,18 +428,21 @@ ServiceAccountService.REALM_NAME, ServiceAccountService.REALM_TYPE, randomAlphaO "cannot load role for service account [" + username + "] - no such service account")); } - public void testTlsIsRequired() { - final boolean httpTls = randomBoolean(); + public void testTlsRequired() { final Settings settings = Settings.builder() - .put("xpack.security.http.ssl.enabled", httpTls) - .put("xpack.security.transport.ssl.enabled", randomFrom(false == httpTls, false)) + .put("xpack.security.http.ssl.enabled", false) .build(); - final ServiceAccountService service = new ServiceAccountService(serviceAccountsTokenStore, new HttpTlsRuntimeCheck(settings)); + final TransportAddress transportAddress = new TransportAddress(TransportAddress.META_ADDRESS, 9300); + when(transport.boundAddress()).thenReturn( + new BoundTransportAddress(new TransportAddress[] { transportAddress }, transportAddress)); + + final ServiceAccountService service = new ServiceAccountService(serviceAccountsTokenStore, + new HttpTlsRuntimeCheck(settings, new SetOnce<>(transport))); final PlainActionFuture future1 = new PlainActionFuture<>(); service.authenticateToken(mock(ServiceAccountToken.class), randomAlphaOfLengthBetween(3, 8), future1); final ElasticsearchException e1 = expectThrows(ElasticsearchException.class, future1::actionGet); - assertThat(e1.getMessage(), containsString("[service account authentication] requires TLS for both HTTP and Transport")); + assertThat(e1.getMessage(), containsString("[service account authentication] requires TLS for the HTTP interface")); final PlainActionFuture future2 = new PlainActionFuture<>(); final Authentication authentication = new Authentication(mock(User.class), @@ -427,7 +451,7 @@ public void testTlsIsRequired() { null); service.getRoleDescriptor(authentication, future2); final ElasticsearchException e2 = expectThrows(ElasticsearchException.class, future2::actionGet); - assertThat(e2.getMessage(), containsString("[service account role descriptor resolving] requires TLS for both HTTP and Transport")); + assertThat(e2.getMessage(), containsString("[service account role descriptor resolving] requires TLS for the HTTP interface")); } private SecureString createBearerString(List bytesList) throws IOException { From e1c758091108817ae7b04fd2ef79ec9d83193ae3 Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Mon, 29 Mar 2021 23:34:44 +1100 Subject: [PATCH 13/14] forbidden API --- .../service/TransportCreateServiceAccountTokenActionTests.java | 2 ++ .../service/TransportGetServiceAccountTokensActionTests.java | 2 ++ .../security/authc/service/ServiceAccountServiceTests.java | 2 ++ 3 files changed, 6 insertions(+) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportCreateServiceAccountTokenActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportCreateServiceAccountTokenActionTests.java index d661e7e7c7bba..4bc97ad1a1106 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportCreateServiceAccountTokenActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportCreateServiceAccountTokenActionTests.java @@ -11,6 +11,7 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.common.SuppressForbidden; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.BoundTransportAddress; import org.elasticsearch.common.transport.TransportAddress; @@ -44,6 +45,7 @@ public class TransportCreateServiceAccountTokenActionTests extends ESTestCase { private Transport transport; @Before + @SuppressForbidden(reason = "Allow accessing localhost") public void init() throws IOException { indexServiceAccountsTokenStore = mock(IndexServiceAccountsTokenStore.class); securityContext = mock(SecurityContext.class); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountTokensActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountTokensActionTests.java index 7cf21bca3e8e9..687c41420c30c 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountTokensActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountTokensActionTests.java @@ -12,6 +12,7 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.common.SuppressForbidden; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.transport.BoundTransportAddress; import org.elasticsearch.common.transport.TransportAddress; @@ -44,6 +45,7 @@ public class TransportGetServiceAccountTokensActionTests extends ESTestCase { private Transport transport; @Before + @SuppressForbidden(reason = "Allow accessing localhost") public void init() throws UnknownHostException { final Settings.Builder builder = Settings.builder() .put("node.name", "node_name") 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 4af541fef098b..f949bcf061bb7 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 @@ -17,6 +17,7 @@ import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.SuppressForbidden; import org.elasticsearch.common.logging.Loggers; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; @@ -62,6 +63,7 @@ public class ServiceAccountServiceTests extends ESTestCase { private Transport transport; @Before + @SuppressForbidden(reason = "Allow accessing localhost") public void init() throws UnknownHostException { threadContext = new ThreadContext(Settings.EMPTY); serviceAccountsTokenStore = mock(ServiceAccountsTokenStore.class); From a0fa91c48afd8345ddcc247ab171ca0d612982ac Mon Sep 17 00:00:00 2001 From: Yang Wang Date: Wed, 31 Mar 2021 11:43:46 +1100 Subject: [PATCH 14/14] Rename elastic/fleet to elastic/fleet-server --- .../security/qa/service-account/build.gradle | 2 +- .../authc/service/ServiceAccountIT.java | 35 ++++++++++--------- .../src/javaRestTest/resources/service_tokens | 2 +- .../ServiceAccountSingleNodeTests.java | 8 ++--- .../authc/service/ElasticServiceAccounts.java | 4 +-- .../authc/AuthenticationServiceTests.java | 6 ++-- .../service/ElasticServiceAccountsTests.java | 4 +-- .../FileServiceAccountsTokenStoreTests.java | 26 +++++++------- .../IndexServiceAccountsTokenStoreTests.java | 9 +++-- .../service/ServiceAccountServiceTests.java | 26 +++++++------- .../service/ServiceAccountTokenTests.java | 7 ++-- .../security/authc/service/service_tokens | 12 +++---- .../authc/service/FileTokensToolTests.java | 26 +++++++------- 13 files changed, 84 insertions(+), 83 deletions(-) diff --git a/x-pack/plugin/security/qa/service-account/build.gradle b/x-pack/plugin/security/qa/service-account/build.gradle index a2875d6f0bfc9..d77ecb7095e72 100644 --- a/x-pack/plugin/security/qa/service-account/build.gradle +++ b/x-pack/plugin/security/qa/service-account/build.gradle @@ -40,5 +40,5 @@ testClusters.javaRestTest { keystore 'xpack.security.http.ssl.secure_key_passphrase', 'node-password' user username: "test_admin", password: 'x-pack-test-password', role: "superuser" - user username: "elastic/fleet", password: 'x-pack-test-password', role: "superuser" + user username: "elastic/fleet-server", password: 'x-pack-test-password', role: "superuser" } 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 ab0a79d6c8293..bbf906fbc5057 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 @@ -37,15 +37,15 @@ public class ServiceAccountIT extends ESRestTestCase { - private static final String VALID_SERVICE_TOKEN = "AAEAAWVsYXN0aWMvZmxlZXQvdG9rZW4xOnI1d2RiZGJvUVNlOXZHT0t3YUpHQXc"; - private static final String INVALID_SERVICE_TOKEN = "AAEAAWVsYXN0aWMvZmxlZXQvdG9rZW4xOjNhSkNMYVFXUk4yc1hsT2R0eEEwU1E"; + private static final String VALID_SERVICE_TOKEN = "AAEAAWVsYXN0aWMvZmxlZXQtc2VydmVyL3Rva2VuMTpyNXdkYmRib1FTZTl2R09Ld2FKR0F3"; + private static final String INVALID_SERVICE_TOKEN = "AAEAAWVsYXN0aWMvZmxlZXQtc2VydmVyL3Rva2VuMTozYUpDTGFRV1JOMnNYbE9kdHhBMFNR"; private static Path caPath; private static final String AUTHENTICATE_RESPONSE = "" + "{\n" - + " \"username\": \"elastic/fleet\",\n" + + " \"username\": \"elastic/fleet-server\",\n" + " \"roles\": [],\n" - + " \"full_name\": \"Service account - elastic/fleet\",\n" + + " \"full_name\": \"Service account - elastic/fleet-server\",\n" + " \"email\": null,\n" + " \"metadata\": {\n" + " \"_elastic_service_account\": true\n" @@ -105,7 +105,8 @@ public void testAuthenticateShouldNotFallThroughInCaseOfFailure() throws IOExcep final ResponseException e = expectThrows(ResponseException.class, () -> client().performRequest(request)); assertThat(e.getResponse().getStatusLine().getStatusCode(), equalTo(401)); if (securityIndexExists) { - assertThat(e.getMessage(), containsString("failed to authenticate service account [elastic/fleet] with token name [token1]")); + assertThat(e.getMessage(), containsString( + "failed to authenticate service account [elastic/fleet-server] with token name [token1]")); } else { assertThat(e.getMessage(), containsString("no such index [.security]")); } @@ -137,13 +138,13 @@ public void testAuthenticateShouldWorkWithOAuthBearerToken() throws IOException public void testAuthenticateShouldDifferentiateBetweenNormalUserAndServiceAccount() throws IOException { final Request request = new Request("GET", "_security/_authenticate"); request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader( - "Authorization", basicAuthHeaderValue("elastic/fleet", new SecureString("x-pack-test-password".toCharArray())) + "Authorization", basicAuthHeaderValue("elastic/fleet-server", new SecureString("x-pack-test-password".toCharArray())) )); final Response response = client().performRequest(request); assertOK(response); final Map responseMap = responseAsMap(response); - assertThat(responseMap.get("username"), equalTo("elastic/fleet")); + assertThat(responseMap.get("username"), equalTo("elastic/fleet-server")); assertThat(responseMap.get("authentication_type"), equalTo("realm")); assertThat(responseMap.get("roles"), equalTo(List.of("superuser"))); Map authRealm = (Map) responseMap.get("authentication_realm"); @@ -151,7 +152,7 @@ public void testAuthenticateShouldDifferentiateBetweenNormalUserAndServiceAccoun } public void testCreateApiServiceAccountTokenAndAuthenticateWithIt() throws IOException { - final Request createTokenRequest = new Request("POST", "_security/service/elastic/fleet/credential/token/api-token-1"); + final Request createTokenRequest = new Request("POST", "_security/service/elastic/fleet-server/credential/token/api-token-1"); final Response createTokenResponse = client().performRequest(createTokenRequest); assertOK(createTokenResponse); final Map createTokenResponseMap = responseAsMap(createTokenResponse); @@ -169,7 +170,7 @@ public void testCreateApiServiceAccountTokenAndAuthenticateWithIt() throws IOExc } public void testFileTokenAndApiTokenCanShareTheSameNameAndBothWorks() throws IOException { - final Request createTokenRequest = new Request("POST", "_security/service/elastic/fleet/credential/token/token1"); + final Request createTokenRequest = new Request("POST", "_security/service/elastic/fleet-server/credential/token/token1"); final Response createTokenResponse = client().performRequest(createTokenRequest); assertOK(createTokenResponse); final Map createTokenResponseMap = responseAsMap(createTokenResponse); @@ -190,7 +191,7 @@ public void testFileTokenAndApiTokenCanShareTheSameNameAndBothWorks() throws IOE public void testNoDuplicateApiServiceAccountToken() throws IOException { final String tokeName = randomAlphaOfLengthBetween(3, 8); - final Request createTokenRequest = new Request("POST", "_security/service/elastic/fleet/credential/token/" + tokeName); + final Request createTokenRequest = new Request("POST", "_security/service/elastic/fleet-server/credential/token/" + tokeName); final Response createTokenResponse = client().performRequest(createTokenRequest); assertOK(createTokenResponse); @@ -201,27 +202,27 @@ public void testNoDuplicateApiServiceAccountToken() throws IOException { } public void testGetServiceAccountTokens() throws IOException { - final Request getTokensRequest = new Request("GET", "_security/service/elastic/fleet/credential"); + final Request getTokensRequest = new Request("GET", "_security/service/elastic/fleet-server/credential"); final Response getTokensResponse1 = client().performRequest(getTokensRequest); assertOK(getTokensResponse1); final Map getTokensResponseMap1 = responseAsMap(getTokensResponse1); - assertThat(getTokensResponseMap1.get("service_account"), equalTo("elastic/fleet")); + assertThat(getTokensResponseMap1.get("service_account"), equalTo("elastic/fleet-server")); assertThat(getTokensResponseMap1.get("count"), equalTo(1)); assertThat(getTokensResponseMap1.get("tokens"), equalTo(Map.of())); assertThat(getTokensResponseMap1.get("file_tokens"), equalTo(Map.of("token1", Map.of()))); - final Request createTokenRequest1 = new Request("POST", "_security/service/elastic/fleet/credential/token/api-token-1"); + final Request createTokenRequest1 = new Request("POST", "_security/service/elastic/fleet-server/credential/token/api-token-1"); final Response createTokenResponse1 = client().performRequest(createTokenRequest1); assertOK(createTokenResponse1); - final Request createTokenRequest2 = new Request("POST", "_security/service/elastic/fleet/credential/token/api-token-2"); + final Request createTokenRequest2 = new Request("POST", "_security/service/elastic/fleet-server/credential/token/api-token-2"); final Response createTokenResponse2 = client().performRequest(createTokenRequest2); assertOK(createTokenResponse2); final Response getTokensResponse2 = client().performRequest(getTokensRequest); assertOK(getTokensResponse2); final Map getTokensResponseMap2 = responseAsMap(getTokensResponse2); - assertThat(getTokensResponseMap2.get("service_account"), equalTo("elastic/fleet")); + assertThat(getTokensResponseMap2.get("service_account"), equalTo("elastic/fleet-server")); assertThat(getTokensResponseMap2.get("count"), equalTo(3)); assertThat(getTokensResponseMap2.get("file_tokens"), equalTo(Map.of("token1", Map.of()))); assertThat(getTokensResponseMap2.get("tokens"), equalTo(Map.of( @@ -235,7 +236,7 @@ public void testManageOwnApiKey() throws IOException { if (randomBoolean()) { token = VALID_SERVICE_TOKEN; } else { - final Request createTokenRequest = new Request("POST", "_security/service/elastic/fleet/credential/token/api-token-42"); + final Request createTokenRequest = new Request("POST", "_security/service/elastic/fleet-server/credential/token/api-token-42"); final Response createTokenResponse = client().performRequest(createTokenRequest); assertOK(createTokenResponse); final Map createTokenResponseMap = responseAsMap(createTokenResponse); @@ -285,7 +286,7 @@ private void assertApiKeys(String apiKeyId, String name, boolean invalidated, final Map apiKey = apiKeys.get(0); assertThat(apiKey.get("id"), equalTo(apiKeyId)); assertThat(apiKey.get("name"), equalTo(name)); - assertThat(apiKey.get("username"), equalTo("elastic/fleet")); + assertThat(apiKey.get("username"), equalTo("elastic/fleet-server")); assertThat(apiKey.get("realm"), equalTo("service_account")); assertThat(apiKey.get("invalidated"), is(invalidated)); } diff --git a/x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/service_tokens b/x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/service_tokens index e3bc027f4e6d3..4595836b0ad6b 100644 --- a/x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/service_tokens +++ b/x-pack/plugin/security/qa/service-account/src/javaRestTest/resources/service_tokens @@ -1 +1 @@ -elastic/fleet/token1:{PBKDF2_STRETCH}10000$8QN+eThJEaCd18sCP0nfzxJq2D9yhmSZgI20TDooYcE=$+0ELfqW4D2+/SlHvm/885dzv67qO2SMJg32Mv/9epXk= +elastic/fleet-server/token1:{PBKDF2_STRETCH}10000$8QN+eThJEaCd18sCP0nfzxJq2D9yhmSZgI20TDooYcE=$+0ELfqW4D2+/SlHvm/885dzv67qO2SMJg32Mv/9epXk= diff --git a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountSingleNodeTests.java b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountSingleNodeTests.java index dd5464cd87eb2..cf50914800148 100644 --- a/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountSingleNodeTests.java +++ b/x-pack/plugin/security/src/internalClusterTest/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountSingleNodeTests.java @@ -26,7 +26,7 @@ public class ServiceAccountSingleNodeTests extends SecuritySingleNodeTestCase { - private static final String BEARER_TOKEN = "AAEAAWVsYXN0aWMvZmxlZXQvdG9rZW4xOnI1d2RiZGJvUVNlOXZHT0t3YUpHQXc"; + private static final String BEARER_TOKEN = "AAEAAWVsYXN0aWMvZmxlZXQtc2VydmVyL3Rva2VuMTpyNXdkYmRib1FTZTl2R09Ld2FKR0F3"; @Override protected Settings nodeSettings() { @@ -49,18 +49,18 @@ protected boolean transportSSLEnabled() { @Override protected String configServiceTokens() { return super.configServiceTokens() - + "elastic/fleet/token1:" + + "elastic/fleet-server/token1:" + "{PBKDF2_STRETCH}10000$8QN+eThJEaCd18sCP0nfzxJq2D9yhmSZgI20TDooYcE=$+0ELfqW4D2+/SlHvm/885dzv67qO2SMJg32Mv/9epXk="; } public void testAuthenticateWithServiceFileToken() { - final AuthenticateRequest authenticateRequest = new AuthenticateRequest("elastic/fleet"); + final AuthenticateRequest authenticateRequest = new AuthenticateRequest("elastic/fleet-server"); final AuthenticateResponse authenticateResponse = createServiceAccountClient().execute(AuthenticateAction.INSTANCE, authenticateRequest).actionGet(); final String nodeName = node().settings().get(Node.NODE_NAME_SETTING.getKey()); assertThat(authenticateResponse.authentication(), equalTo( new Authentication( - new User("elastic/fleet", Strings.EMPTY_ARRAY, "Service account - elastic/fleet", null, + new User("elastic/fleet-server", Strings.EMPTY_ARRAY, "Service account - elastic/fleet-server", null, Map.of("_elastic_service_account", true), true), new Authentication.RealmRef("service_account", "service_account", nodeName), null, Version.CURRENT, Authentication.AuthenticationType.TOKEN, Map.of("_token_name", "token1") 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 b96265aac2824..587f63e03fffc 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 @@ -21,9 +21,9 @@ final class ElasticServiceAccounts { static final String NAMESPACE = "elastic"; - private static final ServiceAccount FLEET_ACCOUNT = new ElasticServiceAccount("fleet", + private static final ServiceAccount FLEET_ACCOUNT = new ElasticServiceAccount("fleet-server", new RoleDescriptor( - NAMESPACE + "/fleet", + NAMESPACE + "/fleet-server", new String[]{"monitor", "manage_own_api_key"}, new RoleDescriptor.IndicesPrivileges[]{ RoleDescriptor.IndicesPrivileges diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java index 25e0e9e053735..202b64f9f3b78 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java @@ -1909,10 +1909,10 @@ public void testExpiredApiKey() { public void testCanAuthenticateServiceAccount() throws ExecutionException, InterruptedException { Mockito.reset(serviceAccountService); final Authentication authentication = new Authentication( - new User("elastic/fleet"), + new User("elastic/fleet-server"), new RealmRef("service_account", "service_account", "foo"), null); try (ThreadContext.StoredContext ignored = threadContext.newStoredContext(false)) { - threadContext.putHeader("Authorization", "Bearer AAEAAWVsYXN0aWMvZmxlZXQvdG9rZW4xOnI1d2RiZGJvUVNlOXZHT0t3YUpHQXc"); + threadContext.putHeader("Authorization", "Bearer AAEAAWVsYXN0aWMvZmxlZXQtc2VydmVyL3Rva2VuMTpyNXdkYmRib1FTZTl2R09Ld2FKR0F3"); doAnswer(invocationOnMock -> { @SuppressWarnings("unchecked") final ActionListener listener = (ActionListener) invocationOnMock.getArguments()[2]; @@ -1929,7 +1929,7 @@ public void testServiceAccountFailureWillNotFallthrough() { Mockito.reset(serviceAccountService); final RuntimeException bailOut = new RuntimeException("bail out"); try (ThreadContext.StoredContext ignored = threadContext.newStoredContext(false)) { - threadContext.putHeader("Authorization", "Bearer AAEAAWVsYXN0aWMvZmxlZXQvdG9rZW4xOnI1d2RiZGJvUVNlOXZHT0t3YUpHQXc"); + threadContext.putHeader("Authorization", "Bearer AAEAAWVsYXN0aWMvZmxlZXQtc2VydmVyL3Rva2VuMTpyNXdkYmRib1FTZTl2R09Ld2FKR0F3"); doAnswer(invocationOnMock -> { @SuppressWarnings("unchecked") final ActionListener listener = (ActionListener) invocationOnMock.getArguments()[2]; 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 60ab161643354..e897773f91594 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 @@ -31,7 +31,7 @@ public class ElasticServiceAccountsTests extends ESTestCase { public void testElasticFleetPrivileges() { - final Role role = Role.builder(ElasticServiceAccounts.ACCOUNTS.get("elastic/fleet").roleDescriptor(), null).build(); + final Role role = Role.builder(ElasticServiceAccounts.ACCOUNTS.get("elastic/fleet-server").roleDescriptor(), null).build(); final Authentication authentication = mock(Authentication.class); assertThat(role.cluster().check(CreateApiKeyAction.NAME, new CreateApiKeyRequest(randomAlphaOfLengthBetween(3, 8), null, null), authentication), is(true)); @@ -42,7 +42,7 @@ public void testElasticFleetPrivileges() { assertThat(role.cluster().check(InvalidateApiKeyAction.NAME, InvalidateApiKeyRequest.usingUserName(randomAlphaOfLengthBetween(3, 16)), authentication), is(false)); - // TODO: more tests when role descriptor is finalised for elastic/fleet + // TODO: more tests when role descriptor is finalised for elastic/fleet-server } public void testElasticServiceAccount() { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStoreTests.java index 7960f06a0928a..f5a2c341cefa6 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/FileServiceAccountsTokenStoreTests.java @@ -83,19 +83,19 @@ public void testParseFile() throws Exception { assertThat(parsedTokenHashes, notNullValue()); assertThat(parsedTokenHashes.size(), is(5)); - assertThat(new String(parsedTokenHashes.get("elastic/fleet/bcrypt")), + assertThat(new String(parsedTokenHashes.get("elastic/fleet-server/bcrypt")), equalTo("$2a$10$uuCzGHRrEz/QMB/.bmL8qOKXHhPNt57dYBbWCH/Hbb3SjUyZ.Hf1i")); - assertThat(new String(parsedTokenHashes.get("elastic/fleet/bcrypt10")), + assertThat(new String(parsedTokenHashes.get("elastic/fleet-server/bcrypt10")), equalTo("$2a$10$ML0BUUxdzs8ApPNf1ayAwuh61ZhfqlzN/1DgZWZn6vNiUhpu1GKTe")); - assertThat(new String(parsedTokenHashes.get("elastic/fleet/pbkdf2")), + assertThat(new String(parsedTokenHashes.get("elastic/fleet-server/pbkdf2")), equalTo("{PBKDF2}10000$0N2h5/AsDS5uO0/A+B6y8AnTCJ3Tqo8nygbzu1gkgpo=$5aTcCtteHf2g2ye7Y3p6jSZBoGhNJ7l6F3tmUhPTwRo=")); - assertThat(new String(parsedTokenHashes.get("elastic/fleet/pbkdf2_50000")), + assertThat(new String(parsedTokenHashes.get("elastic/fleet-server/pbkdf2_50000")), equalTo("{PBKDF2}50000$IMzlphNClmrP/du40yxGM3fNjklg8CuACds12+Ry0jM=$KEC1S9a0NOs3OJKM4gEeBboU18EP4+3m/pyIA4MBDGk=")); - assertThat(new String(parsedTokenHashes.get("elastic/fleet/pbkdf2_stretch")), + assertThat(new String(parsedTokenHashes.get("elastic/fleet-server/pbkdf2_stretch")), equalTo("{PBKDF2_STRETCH}10000$Pa3oNkj8xTD8j2gTgjWnTvnE6jseKApWMFjcNCLxX1U=$84ECweHFZQ2DblHEjHTRWA+fG6h5bVMyTSJUmFvTo1o=")); - assertThat(parsedTokenHashes.get("elastic/fleet/plain"), nullValue()); + assertThat(parsedTokenHashes.get("elastic/fleet-server/plain"), nullValue()); } public void testParseFileNotExists() throws IllegalAccessException, IOException { @@ -122,7 +122,7 @@ public void testAutoReload() throws Exception { store.addListener(latch::countDown); //Token name shares the hashing algorithm name for convenience String tokenName = settings.get("xpack.security.authc.service_token_hashing.algorithm"); - final String qualifiedTokenName = "elastic/fleet/" + tokenName; + final String qualifiedTokenName = "elastic/fleet-server/" + tokenName; assertThat(store.getTokenHashes().containsKey(qualifiedTokenName), is(true)); // A blank line should not trigger update @@ -140,30 +140,30 @@ public void testAutoReload() throws Exception { hasher.hash(new SecureString("46ToAwIHZWxhc3RpYwVmbGVldAZ0b2tlbjEWWkYtQ3dlWlVTZldJX3p5Vk9ySnlSQQAAAAAAAAA".toCharArray())); try (BufferedWriter writer = Files.newBufferedWriter(targetFile, StandardCharsets.UTF_8, StandardOpenOption.APPEND)) { writer.newLine(); - writer.append("elastic/fleet/token1:").append(new String(newTokenHash)); + writer.append("elastic/fleet-server/token1:").append(new String(newTokenHash)); } assertBusy(() -> assertEquals("Waited too long for the updated file to be picked up", 4, latch.getCount()), 5, TimeUnit.SECONDS); - assertThat(store.getTokenHashes().containsKey("elastic/fleet/token1"), is(true)); + assertThat(store.getTokenHashes().containsKey("elastic/fleet-server/token1"), is(true)); // Remove the new entry Files.copy(serviceTokensSourceFile, targetFile, StandardCopyOption.REPLACE_EXISTING); assertBusy(() -> assertEquals("Waited too long for the updated file to be picked up", 3, latch.getCount()), 5, TimeUnit.SECONDS); - assertThat(store.getTokenHashes().containsKey("elastic/fleet/token1"), is(false)); + assertThat(store.getTokenHashes().containsKey("elastic/fleet-server/token1"), is(false)); assertThat(store.getTokenHashes().containsKey(qualifiedTokenName), is(true)); // Write a mal-formatted line if (randomBoolean()) { try (BufferedWriter writer = Files.newBufferedWriter(targetFile, StandardCharsets.UTF_8, StandardOpenOption.APPEND)) { writer.newLine(); - writer.append("elastic/fleet/tokenxfoobar"); + writer.append("elastic/fleet-server/tokenxfoobar"); } } else { // writing in utf_16 should cause a parsing error as we try to read the file in utf_8 try (BufferedWriter writer = Files.newBufferedWriter(targetFile, StandardCharsets.UTF_16, StandardOpenOption.APPEND)) { writer.newLine(); - writer.append("elastic/fleet/tokenx:").append(new String(newTokenHash)); + writer.append("elastic/fleet-server/tokenx:").append(new String(newTokenHash)); } } assertBusy(() -> assertEquals("Waited too long for the updated file to be picked up", 2, latch.getCount()), @@ -195,7 +195,7 @@ public void testFindTokensFor() throws IOException { Files.copy(serviceTokensSourceFile, targetFile, StandardCopyOption.REPLACE_EXISTING); FileServiceAccountsTokenStore store = new FileServiceAccountsTokenStore(env, mock(ResourceWatcherService.class), threadPool); - final ServiceAccountId accountId = new ServiceAccountId("elastic", "fleet"); + final ServiceAccountId accountId = new ServiceAccountId("elastic", "fleet-server"); final PlainActionFuture> future1 = new PlainActionFuture<>(); store.findTokensFor(accountId, future1); final Collection tokenInfos1 = future1.actionGet(); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStoreTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStoreTests.java index 2d4722d838347..fb69e5a699c1f 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStoreTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/IndexServiceAccountsTokenStoreTests.java @@ -167,7 +167,7 @@ public void testDoAuthenticate() throws IOException, ExecutionException, Interru public void testCreateToken() throws ExecutionException, InterruptedException { final Authentication authentication = createAuthentication(); final CreateServiceAccountTokenRequest request = - new CreateServiceAccountTokenRequest("elastic", "fleet", randomAlphaOfLengthBetween(3, 8)); + new CreateServiceAccountTokenRequest("elastic", "fleet-server", randomAlphaOfLengthBetween(3, 8)); // created responseProviderHolder.set((r, l) -> l.onResponse(createSingleBulkResponse())); @@ -177,7 +177,7 @@ public void testCreateToken() throws ExecutionException, InterruptedException { assertThat(bulkRequest.requests().size(), equalTo(1)); final IndexRequest indexRequest = (IndexRequest) bulkRequest.requests().get(0); final Map sourceMap = indexRequest.sourceAsMap(); - assertThat(sourceMap.get("username"), equalTo("elastic/fleet")); + assertThat(sourceMap.get("username"), equalTo("elastic/fleet-server")); assertThat(sourceMap.get("name"), equalTo(request.getTokenName())); assertThat(sourceMap.get("doc_type"), equalTo("service_account_token")); assertThat(sourceMap.get("version"), equalTo(Version.CURRENT.id)); @@ -211,13 +211,12 @@ public void testCreateToken() throws ExecutionException, InterruptedException { public void testCreateTokenWillFailForInvalidServiceAccount() { final Authentication authentication = createAuthentication(); final CreateServiceAccountTokenRequest request = randomValueOtherThanMany( - r -> "elastic".equals(r.getNamespace()) && "fleet".equals(r.getServiceName()), + r -> "elastic".equals(r.getNamespace()) && "fleet-server".equals(r.getServiceName()), () -> new CreateServiceAccountTokenRequest(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8))); final PlainActionFuture future = new PlainActionFuture<>(); store.createToken(authentication, request, future); - final ExecutionException e = expectThrows(ExecutionException.class, () -> future.get()); - assertThat(e.getCause().getClass(), is(IllegalArgumentException.class)); + final IllegalArgumentException e = expectThrows(IllegalArgumentException.class, future::actionGet); assertThat(e.getMessage(), containsString("service account [" + request.getNamespace() + "/" + request.getServiceName() + "] does not exist")); } 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 f949bcf061bb7..e8b5faaf67403 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 @@ -110,7 +110,7 @@ public void testIsServiceAccount() { } public void testGetServiceAccountPrincipals() { - assertThat(ServiceAccountService.getServiceAccountPrincipals(), equalTo(Set.of("elastic/fleet"))); + assertThat(ServiceAccountService.getServiceAccountPrincipals(), equalTo(Set.of("elastic/fleet-server"))); } public void testTryParseToken() throws IOException, IllegalAccessException { @@ -253,8 +253,8 @@ public void testTryParseToken() throws IOException, IllegalAccessException { // everything is fine assertThat(ServiceAccountService.tryParseToken( - new SecureString("AAEAAWVsYXN0aWMvZmxlZXQvdG9rZW4xOnN1cGVyc2VjcmV0".toCharArray())), - equalTo(new ServiceAccountToken(new ServiceAccountId("elastic", "fleet"), "token1", + new SecureString("AAEAAWVsYXN0aWMvZmxlZXQtc2VydmVyL3Rva2VuMTpzdXBlcnNlY3JldA".toCharArray())), + equalTo(new ServiceAccountToken(new ServiceAccountId("elastic", "fleet-server"), "token1", new SecureString("supersecret".toCharArray())))); } finally { appender.stop(); @@ -282,12 +282,12 @@ public void testTryAuthenticateBearerToken() throws ExecutionException, Interrup }).when(serviceAccountsTokenStore).authenticate(any(), any()); final String nodeName = randomAlphaOfLengthBetween(3, 8); serviceAccountService.authenticateToken( - new ServiceAccountToken(new ServiceAccountId("elastic", "fleet"), "token1", + new ServiceAccountToken(new ServiceAccountId("elastic", "fleet-server"), "token1", new SecureString("super-secret-value".toCharArray())), nodeName, future5); assertThat(future5.get(), equalTo( new Authentication( - new User("elastic/fleet", Strings.EMPTY_ARRAY, "Service account - elastic/fleet", null, + new User("elastic/fleet-server", Strings.EMPTY_ARRAY, "Service account - elastic/fleet-server", null, Map.of("_elastic_service_account", true), true), new Authentication.RealmRef(ServiceAccountService.REALM_NAME, ServiceAccountService.REALM_TYPE, nodeName), null, Version.CURRENT, Authentication.AuthenticationType.TOKEN, @@ -326,7 +326,7 @@ public void testAuthenticateWithToken() throws ExecutionException, InterruptedEx // Unknown elastic service name final ServiceAccountId accountId2 = new ServiceAccountId( ElasticServiceAccounts.NAMESPACE, - randomValueOtherThan("fleet", () -> randomAlphaOfLengthBetween(3, 8))); + randomValueOtherThan("fleet-server", () -> randomAlphaOfLengthBetween(3, 8))); appender.addExpectation(new MockLogAppender.SeenEventExpectation( "non-elastic service account", ServiceAccountService.class.getName(), Level.DEBUG, "the [" + accountId2.asPrincipal() + "] service account does not exist" @@ -341,7 +341,7 @@ public void testAuthenticateWithToken() throws ExecutionException, InterruptedEx appender.assertAllExpectationsMatched(); // Success based on credential store - final ServiceAccountId accountId3 = new ServiceAccountId(ElasticServiceAccounts.NAMESPACE, "fleet"); + final ServiceAccountId accountId3 = new ServiceAccountId(ElasticServiceAccounts.NAMESPACE, "fleet-server"); final ServiceAccountToken token3 = new ServiceAccountToken(accountId3, randomAlphaOfLengthBetween(3, 8), secret); final ServiceAccountToken token4 = new ServiceAccountToken(accountId3, randomAlphaOfLengthBetween(3, 8), new SecureString(randomAlphaOfLength(20).toCharArray())); @@ -364,8 +364,8 @@ public void testAuthenticateWithToken() throws ExecutionException, InterruptedEx serviceAccountService.authenticateToken(token3, nodeName, future3); final Authentication authentication = future3.get(); assertThat(authentication, equalTo(new Authentication( - new User("elastic/fleet", Strings.EMPTY_ARRAY, - "Service account - elastic/fleet", null, Map.of("_elastic_service_account", true), + new User("elastic/fleet-server", Strings.EMPTY_ARRAY, + "Service account - elastic/fleet-server", null, Map.of("_elastic_service_account", true), true), new Authentication.RealmRef(ServiceAccountService.REALM_NAME, ServiceAccountService.REALM_TYPE, nodeName), null, Version.CURRENT, Authentication.AuthenticationType.TOKEN, @@ -393,9 +393,9 @@ public void testAuthenticateWithToken() throws ExecutionException, InterruptedEx public void testGetRoleDescriptor() throws ExecutionException, InterruptedException { final Authentication auth1 = new Authentication( - new User("elastic/fleet", + new User("elastic/fleet-server", Strings.EMPTY_ARRAY, - "Service account - elastic/fleet", + "Service account - elastic/fleet-server", null, Map.of("_elastic_service_account", true), true), @@ -410,10 +410,10 @@ ServiceAccountService.REALM_NAME, ServiceAccountService.REALM_TYPE, randomAlphaO serviceAccountService.getRoleDescriptor(auth1, future1); final RoleDescriptor roleDescriptor1 = future1.get(); assertNotNull(roleDescriptor1); - assertThat(roleDescriptor1.getName(), equalTo("elastic/fleet")); + assertThat(roleDescriptor1.getName(), equalTo("elastic/fleet-server")); final String username = - randomValueOtherThan("elastic/fleet", () -> randomAlphaOfLengthBetween(3, 8) + "/" + randomAlphaOfLengthBetween(3, 8)); + randomValueOtherThan("elastic/fleet-server", () -> randomAlphaOfLengthBetween(3, 8) + "/" + randomAlphaOfLengthBetween(3, 8)); final Authentication auth2 = new Authentication( new User(username, Strings.EMPTY_ARRAY, "Service account - " + username, null, Map.of("_elastic_service_account", true), true), diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountTokenTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountTokenTests.java index 716c64c3405b6..f75d0663a7862 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountTokenTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountTokenTests.java @@ -82,11 +82,12 @@ public void testServiceAccountTokenNew() { public void testBearerString() throws IOException { final ServiceAccountToken serviceAccountToken = - new ServiceAccountToken(new ServiceAccountId("elastic", "fleet"), + new ServiceAccountToken(new ServiceAccountId("elastic", "fleet-server"), "token1", new SecureString("supersecret".toCharArray())); - assertThat(serviceAccountToken.asBearerString(), equalTo("AAEAAWVsYXN0aWMvZmxlZXQvdG9rZW4xOnN1cGVyc2VjcmV0")); + assertThat(serviceAccountToken.asBearerString(), equalTo("AAEAAWVsYXN0aWMvZmxlZXQtc2VydmVyL3Rva2VuMTpzdXBlcnNlY3JldA")); - assertThat(ServiceAccountToken.fromBearerString(new SecureString("AAEAAWVsYXN0aWMvZmxlZXQvdG9rZW4xOnN1cGVyc2VjcmV0".toCharArray())), + assertThat(ServiceAccountToken.fromBearerString( + new SecureString("AAEAAWVsYXN0aWMvZmxlZXQtc2VydmVyL3Rva2VuMTpzdXBlcnNlY3JldA".toCharArray())), equalTo(serviceAccountToken)); final ServiceAccountId accountId = new ServiceAccountId(randomAlphaOfLengthBetween(3, 8), randomAlphaOfLengthBetween(3, 8)); diff --git a/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/authc/service/service_tokens b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/authc/service/service_tokens index 56b028dd4a0aa..126c6f8621291 100644 --- a/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/authc/service/service_tokens +++ b/x-pack/plugin/security/src/test/resources/org/elasticsearch/xpack/security/authc/service/service_tokens @@ -1,6 +1,6 @@ -elastic/fleet/pbkdf2:{PBKDF2}10000$0N2h5/AsDS5uO0/A+B6y8AnTCJ3Tqo8nygbzu1gkgpo=$5aTcCtteHf2g2ye7Y3p6jSZBoGhNJ7l6F3tmUhPTwRo= -elastic/fleet/bcrypt10:$2a$10$ML0BUUxdzs8ApPNf1ayAwuh61ZhfqlzN/1DgZWZn6vNiUhpu1GKTe -elastic/fleet/pbkdf2_stretch:{PBKDF2_STRETCH}10000$Pa3oNkj8xTD8j2gTgjWnTvnE6jseKApWMFjcNCLxX1U=$84ECweHFZQ2DblHEjHTRWA+fG6h5bVMyTSJUmFvTo1o= -elastic/fleet/pbkdf2_50000:{PBKDF2}50000$IMzlphNClmrP/du40yxGM3fNjklg8CuACds12+Ry0jM=$KEC1S9a0NOs3OJKM4gEeBboU18EP4+3m/pyIA4MBDGk= -elastic/fleet/bcrypt:$2a$10$uuCzGHRrEz/QMB/.bmL8qOKXHhPNt57dYBbWCH/Hbb3SjUyZ.Hf1i -elastic/fleet/plain:{plain}_By842iQQVKSCLxVcJZWvw +elastic/fleet-server/pbkdf2:{PBKDF2}10000$0N2h5/AsDS5uO0/A+B6y8AnTCJ3Tqo8nygbzu1gkgpo=$5aTcCtteHf2g2ye7Y3p6jSZBoGhNJ7l6F3tmUhPTwRo= +elastic/fleet-server/bcrypt10:$2a$10$ML0BUUxdzs8ApPNf1ayAwuh61ZhfqlzN/1DgZWZn6vNiUhpu1GKTe +elastic/fleet-server/pbkdf2_stretch:{PBKDF2_STRETCH}10000$Pa3oNkj8xTD8j2gTgjWnTvnE6jseKApWMFjcNCLxX1U=$84ECweHFZQ2DblHEjHTRWA+fG6h5bVMyTSJUmFvTo1o= +elastic/fleet-server/pbkdf2_50000:{PBKDF2}50000$IMzlphNClmrP/du40yxGM3fNjklg8CuACds12+Ry0jM=$KEC1S9a0NOs3OJKM4gEeBboU18EP4+3m/pyIA4MBDGk= +elastic/fleet-server/bcrypt:$2a$10$uuCzGHRrEz/QMB/.bmL8qOKXHhPNt57dYBbWCH/Hbb3SjUyZ.Hf1i +elastic/fleet-server/plain:{plain}_By842iQQVKSCLxVcJZWvw diff --git a/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/authc/service/FileTokensToolTests.java b/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/authc/service/FileTokensToolTests.java index ae4ad44cf0784..9f390033901b6 100644 --- a/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/authc/service/FileTokensToolTests.java +++ b/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/authc/service/FileTokensToolTests.java @@ -70,9 +70,9 @@ public void setupHome() throws IOException { hasher = getFastStoredHashAlgoForTests(); Files.write(confDir.resolve("service_tokens"), List.of( - "elastic/fleet/server_1:" + new String(hasher.hash(token1)), - "elastic/fleet/server_2:" + new String(hasher.hash(token2)), - "elastic/fleet/server_3:" + new String(hasher.hash(token3)) + "elastic/fleet-server/server_1:" + new String(hasher.hash(token1)), + "elastic/fleet-server/server_2:" + new String(hasher.hash(token2)), + "elastic/fleet-server/server_3:" + new String(hasher.hash(token3)) )); settings = Settings.builder() .put("path.home", homeDir) @@ -107,8 +107,8 @@ protected Environment createEnv(Map settings) throws UserExcepti public void testParsePrincipalAndTokenName() throws UserException { final String tokenName1 = randomAlphaOfLengthBetween(3, 8); final Tuple tuple1 = - CreateFileTokenCommand.parsePrincipalAndTokenName(List.of("elastic/fleet", tokenName1), Settings.EMPTY); - assertEquals("elastic/fleet", tuple1.v1()); + CreateFileTokenCommand.parsePrincipalAndTokenName(List.of("elastic/fleet-server", tokenName1), Settings.EMPTY); + assertEquals("elastic/fleet-server", tuple1.v1()); assertEquals(tokenName1, tuple1.v2()); final UserException e2 = expectThrows(UserException.class, @@ -129,21 +129,21 @@ public void testParsePrincipalAndTokenName() throws UserException { public void testCreateToken() throws Exception { final String tokenName1 = ServiceAccountTokenTests.randomTokenName(); - execute("create", pathHomeParameter, "elastic/fleet", tokenName1); - assertServiceTokenExists("elastic/fleet/" + tokenName1); + execute("create", pathHomeParameter, "elastic/fleet-server", tokenName1); + assertServiceTokenExists("elastic/fleet-server/" + tokenName1); final String tokenName2 = ServiceAccountTokenTests.randomTokenName(); - execute("create", pathHomeParameter, "elastic/fleet", tokenName2); - assertServiceTokenExists("elastic/fleet/" + tokenName2); + execute("create", pathHomeParameter, "elastic/fleet-server", tokenName2); + assertServiceTokenExists("elastic/fleet-server/" + tokenName2); final String output = terminal.getOutput(); - assertThat(output, containsString("SERVICE_TOKEN elastic/fleet/" + tokenName1 + " = ")); - assertThat(output, containsString("SERVICE_TOKEN elastic/fleet/" + tokenName2 + " = ")); + assertThat(output, containsString("SERVICE_TOKEN elastic/fleet-server/" + tokenName1 + " = ")); + assertThat(output, containsString("SERVICE_TOKEN elastic/fleet-server/" + tokenName2 + " = ")); } public void testCreateTokenWithInvalidTokenName() throws Exception { final String tokenName = ServiceAccountTokenTests.randomInvalidTokenName(); final UserException e = expectThrows(UserException.class, - () -> execute("create", pathHomeParameter, "elastic/fleet", tokenName)); - assertServiceTokenNotExists("elastic/fleet/" + tokenName); + () -> execute("create", pathHomeParameter, "elastic/fleet-server", tokenName)); + assertServiceTokenNotExists("elastic/fleet-server/" + tokenName); assertThat(e.getMessage(), containsString(ServiceAccountToken.INVALID_TOKEN_NAME_MESSAGE)); }