From 2bb52b590e5810019d75f3ecf76eebba0bffa234 Mon Sep 17 00:00:00 2001 From: Lyudmila Fokina <35386883+BigPandaToo@users.noreply.github.com> Date: Tue, 13 Apr 2021 14:08:45 +0200 Subject: [PATCH 01/14] Create enrollment token API As part of the security on be default project, we will be offering an enrollment process that allows new nodes to join a cluster, or clients to bootstrap their configuration to communicate with a cluster that is already running with security enabled. This enrollment process is based on the use of enrollment tokens. --- .../client/security/user/privileges/Role.java | 4 +- .../security/get-builtin-privileges.asciidoc | 2 + .../authorization/privileges.asciidoc | 8 ++ .../CreateEnrollmentTokenAction.java | 23 ++++++ .../CreateEnrollmentTokenRequest.java | 44 +++++++++++ .../CreateEnrollmentTokenResponse.java | 53 ++++++++++++++ .../privilege/ClusterPrivilegeResolver.java | 11 ++- .../TransportCreateEnrollmentTokenAction.java | 71 ++++++++++++++++++ .../RestCreateEnrollmentTokenAction.java | 73 +++++++++++++++++++ .../test/privileges/11_builtin.yml | 2 +- 10 files changed, 288 insertions(+), 3 deletions(-) create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenAction.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenRequest.java create mode 100644 x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenResponse.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/enrollment/RestCreateEnrollmentTokenAction.java diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/Role.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/Role.java index 111f66bad5f91..9f649570dda50 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/Role.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/user/privileges/Role.java @@ -326,11 +326,13 @@ public static class ClusterPrivilegeName { public static final String MANAGE_ILM = "manage_ilm"; public static final String READ_ILM = "read_ilm"; public static final String MANAGE_ENRICH = "manage_enrich"; + public static final String MANAGE_ENROLLMENT = "manage_enrollment"; + public static final String ENROLL = "enroll"; public static final String[] ALL_ARRAY = new String[] { NONE, ALL, MONITOR, MONITOR_TRANSFORM_DEPRECATED, MONITOR_TRANSFORM, MONITOR_ML, MONITOR_TEXT_STRUCTURE, MONITOR_WATCHER, MONITOR_ROLLUP, MANAGE, MANAGE_TRANSFORM_DEPRECATED, MANAGE_TRANSFORM, MANAGE_ML, MANAGE_WATCHER, MANAGE_ROLLUP, MANAGE_INDEX_TEMPLATES, MANAGE_INGEST_PIPELINES, READ_PIPELINE, TRANSPORT_CLIENT, MANAGE_SECURITY, MANAGE_SAML, MANAGE_OIDC, MANAGE_TOKEN, MANAGE_PIPELINE, MANAGE_AUTOSCALING, MANAGE_CCR, - READ_CCR, MANAGE_ILM, READ_ILM, MANAGE_ENRICH }; + READ_CCR, MANAGE_ILM, READ_ILM, MANAGE_ENRICH, MANAGE_ENROLLMENT, ENROLL }; } /** 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 36043cffd5eeb..4f1f863536341 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 @@ -66,6 +66,7 @@ A successful call returns an object with "cluster" and "index" fields. "cancel_task", "create_snapshot", "delegate_pki", + "enroll", "grant_api_key", "manage", "manage_api_key", @@ -73,6 +74,7 @@ A successful call returns an object with "cluster" and "index" fields. "manage_ccr", "manage_data_frame_transforms", "manage_enrich", + "manage_enrollment" "manage_ilm", "manage_index_templates", "manage_ingest_pipelines", diff --git a/x-pack/docs/en/security/authorization/privileges.asciidoc b/x-pack/docs/en/security/authorization/privileges.asciidoc index 0c532de72e7e3..35e43b0413c8e 100644 --- a/x-pack/docs/en/security/authorization/privileges.asciidoc +++ b/x-pack/docs/en/security/authorization/privileges.asciidoc @@ -20,6 +20,10 @@ See <> API for more informations. Privileges to create snapshots for existing repositories. Can also list and view details on existing repositories and snapshots. +`enroll`:: +The privilege to call the APIs to enroll a node in a cluster and to allow Kibana to +configure itself to communicate with an Elasticsearch cluster. + `grant_api_key`:: Privileges to create {es} API keys on behalf of other users. @@ -54,6 +58,10 @@ patterns. It also includes the authority to grant the privileges necessary to manage follower indices and auto-follow patterns. This privilege is necessary only on clusters that contain follower indices. +`manage_enrollment`:: +The privilege to call the create enrollment token API and to call +the Enroll Node and Enroll Token API. `enroll` is also included in `manage_enrollment`. + `manage_ilm`:: All {Ilm} operations related to managing policies. diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenAction.java new file mode 100644 index 0000000000000..75c3cc26e0474 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenAction.java @@ -0,0 +1,23 @@ +/* + * 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.enrollment; + +import org.elasticsearch.action.ActionType; + +/** + * ActionType for creating an enrollment new token + */ +public class CreateEnrollmentTokenAction extends ActionType { + public static final String NAME = "cluster:admin/xpack/security/enrollment/create"; + public static final org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenAction INSTANCE = + new CreateEnrollmentTokenAction(); + + private CreateEnrollmentTokenAction() { + super(NAME, CreateEnrollmentTokenResponse::new); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenRequest.java new file mode 100644 index 0000000000000..0ca504c60432f --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenRequest.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.core.security.action.enrollment; + +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; + +/** + * Represents a request to create an enrollment token based on the provided information. + */ + +public class CreateEnrollmentTokenRequest extends ActionRequest { + + public CreateEnrollmentTokenRequest(StreamInput in) throws IOException { + super(in); + } + + public CreateEnrollmentTokenRequest() { + } + + @Override + public ActionRequestValidationException validate() { + return null; + } + + @Override + public String toString() { + return "get_enrollment_token"; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + super.writeTo(out); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenResponse.java new file mode 100644 index 0000000000000..2fac28d3dcdf6 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenResponse.java @@ -0,0 +1,53 @@ +/* + * 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.enrollment; + +import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.io.stream.StreamOutput; + +import java.io.IOException; +import java.util.Objects; + +/** + * Response containing the enrollment token string that was generated from an enrollment token creation request. + */ +public class CreateEnrollmentTokenResponse extends ActionResponse { + private String enrollmentTokenString; + + public CreateEnrollmentTokenResponse(StreamInput in) throws IOException { + super(in); + enrollmentTokenString = in.readString(); + } + + public CreateEnrollmentTokenResponse(String enrollmentTokenString) { + this.enrollmentTokenString = Objects.requireNonNull(enrollmentTokenString); + } + + public String getEnrollmentToken() { + return enrollmentTokenString; + } + + @Override + public void writeTo(StreamOutput out) throws IOException { + out.writeString(enrollmentTokenString); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CreateEnrollmentTokenResponse that = (CreateEnrollmentTokenResponse) o; + return Objects.equals(enrollmentTokenString, that.enrollmentTokenString); + } + + @Override + public int hashCode() { + return Objects.hash(enrollmentTokenString); + } +} 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 829ad1b291539..8969cea6b97f2 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 @@ -134,6 +134,8 @@ public class ClusterPrivilegeResolver { "manage_autoscaling", Set.of("cluster:admin/autoscaling/*") ); + private static final Set MANAGE_ENROLLMENT_PATTERN = Set.of("cluster:admin/xpack/security/enrollment/*"); + public static final NamedClusterPrivilege MANAGE_CCR = new ActionClusterPrivilege("manage_ccr", MANAGE_CCR_PATTERN); public static final NamedClusterPrivilege READ_CCR = new ActionClusterPrivilege("read_ccr", READ_CCR_PATTERN); public static final NamedClusterPrivilege CREATE_SNAPSHOT = new ActionClusterPrivilege("create_snapshot", CREATE_SNAPSHOT_PATTERN); @@ -154,6 +156,11 @@ public class ClusterPrivilegeResolver { public static final NamedClusterPrivilege CANCEL_TASK = new ActionClusterPrivilege("cancel_task", Set.of("cluster:admin/tasks/cancel")); + public static final NamedClusterPrivilege MANAGE_ENROLLMENT = new ActionClusterPrivilege("manage_enrollment", + MANAGE_ENROLLMENT_PATTERN); + public static final NamedClusterPrivilege ENROLL = new ActionClusterPrivilege("enroll", + Set.of("cluster:admin/xpack/security/enrollment/enroll")); + private static final Map VALUES = sortByAccessLevel(List.of( NONE, ALL, @@ -195,7 +202,9 @@ public class ClusterPrivilegeResolver { MANAGE_OWN_API_KEY, MANAGE_ENRICH, MANAGE_LOGSTASH_PIPELINES, - CANCEL_TASK)); + CANCEL_TASK, + MANAGE_ENROLLMENT, + ENROLL)); /** * Resolves a {@link NamedClusterPrivilege} from a given name if it exists. diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java new file mode 100644 index 0000000000000..32746ea4fe5cf --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +package org.elasticsearch.xpack.security.action.enrollment; + +import org.apache.logging.log4j.message.ParameterizedMessage; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenAction; +import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenRequest; +import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenResponse; +import org.elasticsearch.xpack.core.security.action.saml.SamlSpMetadataAction; +import org.elasticsearch.xpack.core.security.action.saml.SamlSpMetadataRequest; +import org.elasticsearch.xpack.core.security.action.saml.SamlSpMetadataResponse; +import org.elasticsearch.xpack.security.authc.Realms; +import org.elasticsearch.xpack.security.authc.saml.SamlRealm; +import org.elasticsearch.xpack.security.authc.saml.SamlSpMetadataBuilder; +import org.elasticsearch.xpack.security.authc.saml.SamlUtils; +import org.elasticsearch.xpack.security.authc.saml.SpConfiguration; +import org.opensaml.saml.saml2.core.AuthnRequest; +import org.opensaml.saml.saml2.metadata.EntityDescriptor; +import org.opensaml.saml.saml2.metadata.impl.EntityDescriptorMarshaller; +import org.w3c.dom.Element; + +import javax.xml.transform.Transformer; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; + +import java.io.StringWriter; +import java.util.List; +import java.util.Locale; + +import static org.elasticsearch.xpack.security.authc.saml.SamlRealm.findSamlRealms; + +/** + * Transport action responsible for creating an enrollment token based on a request. + */ + +public class TransportCreateEnrollmentTokenAction + extends HandledTransportAction { + + @Inject + public TransportCreateEnrollmentTokenAction(TransportService transportService, ActionFilters actionFilters) { + super(CreateEnrollmentTokenAction.NAME, transportService, actionFilters, CreateEnrollmentTokenRequest::new + ); + } + + @Override + protected void doExecute(Task task, CreateEnrollmentTokenRequest request, + ActionListener listener) { + createEnrolmentToken(listener); + } + + private void createEnrolmentToken(ActionListener listener) { + try { + String enrollmentTokenString = new String(); + listener.onResponse(new CreateEnrollmentTokenResponse(enrollmentTokenString)); + } catch (Exception e) { + logger.error(new ParameterizedMessage("Error generating enrollment token", e)); + listener.onFailure(e); + } + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/enrollment/RestCreateEnrollmentTokenAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/enrollment/RestCreateEnrollmentTokenAction.java new file mode 100644 index 0000000000000..eab3c96ff9fea --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/enrollment/RestCreateEnrollmentTokenAction.java @@ -0,0 +1,73 @@ +/* + * 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.enrollment; + +import org.elasticsearch.action.ActionType; +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.rest.BytesRestResponse; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.RestResponse; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.rest.action.RestBuilderListener; +import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenAction; +import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenRequest; +import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenResponse; +import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler; + +import java.io.IOException; +import java.util.List; + +import static org.elasticsearch.rest.RestRequest.Method.POST; +import static org.elasticsearch.rest.RestRequest.Method.PUT; + +/** + * Rest endpoint to create an enrollment token + */ +public class RestCreateEnrollmentTokenAction extends SecurityBaseRestHandler { + + /** + * @param settings the node's settings + * @param licenseState the license state that will be used to determine if + * security is licensed + */ + public RestCreateEnrollmentTokenAction(Settings settings, XPackLicenseState licenseState) { + super(settings, licenseState); + } + + @Override + public List routes() { + return List.of( + new Route(POST, "/_cluster/enrollment_token"), + new Route(PUT, "/_cluster/enrollment_token")); + } + + @Override + public String getName() { + return "cluster_enrolment_token"; + } + + @Override + protected RestChannelConsumer innerPrepareRequest(final RestRequest request, final NodeClient client) throws IOException { + final CreateEnrollmentTokenRequest enrollmentTokenRequest = new CreateEnrollmentTokenRequest(); + final ActionType action = CreateEnrollmentTokenAction.INSTANCE; + return channel -> client.execute(action, enrollmentTokenRequest, + new RestBuilderListener<>(channel) { + @Override + public RestResponse buildResponse(CreateEnrollmentTokenResponse response, XContentBuilder builder) throws Exception { + builder.startObject(); + builder.field("enrollment_token", response.getEnrollmentToken()); + builder.endObject(); + return new BytesRestResponse(RestStatus.OK, builder); + } + } + ); + } +} diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/privileges/11_builtin.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/privileges/11_builtin.yml index 90842d3c04cd0..be7d28af3ae42 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/privileges/11_builtin.yml +++ b/x-pack/plugin/src/yamlRestTest/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" : 41 } + - length: { "cluster" : 43 } - length: { "index" : 19 } From f78af775cfddd15efcdb1903bd1bcc1b96fa6fbd Mon Sep 17 00:00:00 2001 From: Lyudmila Fokina <35386883+BigPandaToo@users.noreply.github.com> Date: Thu, 15 Apr 2021 09:50:17 +0200 Subject: [PATCH 02/14] Create enrolment token --- .../CreateEnrollmentTokenRequest.java | 9 ++++ .../TransportCreateEnrollmentTokenAction.java | 53 +++++++++---------- .../xpack/security/authc/ApiKeyService.java | 53 ++++++++++++++++++- .../authc/support/ApiKeyGenerator.java | 26 +++++++++ 4 files changed, 113 insertions(+), 28 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenRequest.java index 0ca504c60432f..54e77845292b5 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenRequest.java @@ -9,6 +9,7 @@ import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionRequestValidationException; +import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; @@ -19,12 +20,20 @@ */ public class CreateEnrollmentTokenRequest extends ActionRequest { + private final String id; public CreateEnrollmentTokenRequest(StreamInput in) throws IOException { super(in); + this.id = UUIDs.base64UUID(); } public CreateEnrollmentTokenRequest() { + super(); + this.id = UUIDs.base64UUID(); + } + + public String getId() { + return id; } @Override diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java index 32746ea4fe5cf..9f83c3dda6002 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java @@ -12,33 +12,17 @@ import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.HandledTransportAction; import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.tasks.Task; import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.security.SecurityContext; import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenAction; import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenRequest; import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenResponse; -import org.elasticsearch.xpack.core.security.action.saml.SamlSpMetadataAction; -import org.elasticsearch.xpack.core.security.action.saml.SamlSpMetadataRequest; -import org.elasticsearch.xpack.core.security.action.saml.SamlSpMetadataResponse; -import org.elasticsearch.xpack.security.authc.Realms; -import org.elasticsearch.xpack.security.authc.saml.SamlRealm; -import org.elasticsearch.xpack.security.authc.saml.SamlSpMetadataBuilder; -import org.elasticsearch.xpack.security.authc.saml.SamlUtils; -import org.elasticsearch.xpack.security.authc.saml.SpConfiguration; -import org.opensaml.saml.saml2.core.AuthnRequest; -import org.opensaml.saml.saml2.metadata.EntityDescriptor; -import org.opensaml.saml.saml2.metadata.impl.EntityDescriptorMarshaller; -import org.w3c.dom.Element; - -import javax.xml.transform.Transformer; -import javax.xml.transform.dom.DOMSource; -import javax.xml.transform.stream.StreamResult; - -import java.io.StringWriter; -import java.util.List; -import java.util.Locale; - -import static org.elasticsearch.xpack.security.authc.saml.SamlRealm.findSamlRealms; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.security.authc.ApiKeyService; +import org.elasticsearch.xpack.security.authc.support.ApiKeyGenerator; +import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; /** * Transport action responsible for creating an enrollment token based on a request. @@ -47,19 +31,25 @@ public class TransportCreateEnrollmentTokenAction extends HandledTransportAction { + private final ApiKeyGenerator generator; + private final SecurityContext securityContext; + @Inject - public TransportCreateEnrollmentTokenAction(TransportService transportService, ActionFilters actionFilters) { - super(CreateEnrollmentTokenAction.NAME, transportService, actionFilters, CreateEnrollmentTokenRequest::new - ); + public TransportCreateEnrollmentTokenAction(TransportService transportService, ActionFilters actionFilters, ApiKeyService apiKeyService, + SecurityContext context, CompositeRolesStore rolesStore, + NamedXContentRegistry xContentRegistry) { + super(CreateEnrollmentTokenAction.NAME, transportService, actionFilters, CreateEnrollmentTokenRequest::new); + this.generator = new ApiKeyGenerator(apiKeyService, rolesStore, xContentRegistry); + this.securityContext = context; } @Override protected void doExecute(Task task, CreateEnrollmentTokenRequest request, ActionListener listener) { - createEnrolmentToken(listener); + createEnrolmentToken(request, listener); } - private void createEnrolmentToken(ActionListener listener) { + private void createEnrolmentToken(CreateEnrollmentTokenRequest request, ActionListener listener) { try { String enrollmentTokenString = new String(); listener.onResponse(new CreateEnrollmentTokenResponse(enrollmentTokenString)); @@ -68,4 +58,13 @@ private void createEnrolmentToken(ActionListener listener.onFailure(e); } } + + private void createApiKey(CreateEnrollmentTokenRequest request, ActionListener listener) { + final Authentication authentication = securityContext.getAuthentication(); + if (authentication == null) { + listener.onFailure(new IllegalStateException("authentication is required")); + } else { + generator.generateApiKeyForEnrollment(authentication, request, 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 1e5f552b91bf0..add17d05e5022 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 @@ -15,6 +15,8 @@ import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.ActionRequest; +import org.elasticsearch.action.ActionResponse; import org.elasticsearch.action.ActionRunnable; import org.elasticsearch.action.DocWriteResponse; import org.elasticsearch.action.bulk.BulkAction; @@ -29,6 +31,7 @@ import org.elasticsearch.action.index.IndexResponse; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.support.ContextPreservingActionListener; +import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.action.support.WriteRequest.RefreshPolicy; import org.elasticsearch.action.update.UpdateRequest; import org.elasticsearch.action.update.UpdateResponse; @@ -83,6 +86,8 @@ import org.elasticsearch.xpack.core.security.action.CreateApiKeyResponse; import org.elasticsearch.xpack.core.security.action.GetApiKeyResponse; import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyResponse; +import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenRequest; +import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenResponse; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; @@ -146,6 +151,7 @@ public class ApiKeyService { public static final String API_KEY_REALM_TYPE = "_es_api_key"; public static final String API_KEY_CREATOR_REALM_NAME = "_security_api_key_creator_realm_name"; public static final String API_KEY_CREATOR_REALM_TYPE = "_security_api_key_creator_realm_type"; + public static final long ENROLL_API_KEY_EXPIRATION_SEC = 30*60; public static final Setting PASSWORD_HASHING_ALGORITHM = new Setting<>( "xpack.security.authc.api_key.hashing.algorithm", "pbkdf2", Function.identity(), v -> { @@ -255,6 +261,16 @@ public void createApiKey(Authentication authentication, CreateApiKeyRequest requ } } + public void createApiKeyForEnrollment(Authentication authentication, CreateEnrollmentTokenRequest request, + Set userRoles, ActionListener listener) { + ensureEnabled(); + if (authentication == null) { + listener.onFailure(new IllegalArgumentException("authentication must be provided")); + } else { + createApiKeyAndIndexItForEnrollmentToken(authentication, request, userRoles, listener); + } + } + private void createApiKeyAndIndexIt(Authentication authentication, CreateApiKeyRequest request, Set roleDescriptorSet, ActionListener listener) { final Instant created = clock.instant(); @@ -279,7 +295,42 @@ private void createApiKeyAndIndexIt(Authentication authentication, CreateApiKeyR indexResponse -> { assert request.getId().equals(indexResponse.getId()); listener.onResponse( - new CreateApiKeyResponse(request.getName(), request.getId(), apiKey, expiration)); + new CreateApiKeyResponse(request.getName(), request.getId(), apiKey, expiration)); + }, + listener::onFailure)))); + } catch (IOException e) { + listener.onFailure(e); + } + } + + private void createApiKeyAndIndexItForEnrollmentToken(Authentication authentication, CreateEnrollmentTokenRequest request, + Set roleDescriptorSet, ActionListener listener) { + final Instant created = clock.instant(); + final Instant expiration = created.plusSeconds(ENROLL_API_KEY_EXPIRATION_SEC); + final SecureString apiKey = UUIDs.randomBase64UUIDSecureString(); + final Version version = clusterService.state().nodes().getMinNodeVersion(); + final String name = "enrollment_token_API_key_" + created.toString(); + final String requestId = request.getId(); + final WriteRequest.RefreshPolicy policy = WriteRequest.RefreshPolicy.IMMEDIATE; + + try (XContentBuilder builder = newDocument(apiKey, name, authentication, roleDescriptorSet, created, expiration, + null, version, null)) { + + final IndexRequest indexRequest = + client.prepareIndex(SECURITY_MAIN_ALIAS) + .setSource(builder) + .setId(requestId) + .setRefreshPolicy(policy) + .request(); + final BulkRequest bulkRequest = toSingleItemBulkRequest(indexRequest); + + securityIndex.prepareIndexIfNeededThenExecute(listener::onFailure, () -> + executeAsyncWithOrigin(client, SECURITY_ORIGIN, BulkAction.INSTANCE, bulkRequest, + TransportSingleItemBulkWriteAction.wrapBulkResponse(ActionListener.wrap( + indexResponse -> { + assert requestId.equals(indexResponse.getId()); + listener.onResponse( + new CreateApiKeyResponse(name, requestId, apiKey, expiration)); }, listener::onFailure)))); } catch (IOException e) { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/ApiKeyGenerator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/ApiKeyGenerator.java index 71afd11c21696..e3424164d0676 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/ApiKeyGenerator.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/ApiKeyGenerator.java @@ -10,9 +10,12 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.xpack.core.security.action.CreateApiKeyRequest; import org.elasticsearch.xpack.core.security.action.CreateApiKeyResponse; +import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenRequest; +import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenResponse; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.support.DLSRoleQueryValidator; @@ -20,6 +23,7 @@ import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; import java.util.Arrays; +import java.util.Collections; import java.util.HashSet; public class ApiKeyGenerator { @@ -56,4 +60,26 @@ public void generateApiKey(Authentication authentication, CreateApiKeyRequest re } + public SecureString generateApiKeyForEnrollment(Authentication authentication, CreateEnrollmentTokenRequest request, + ActionListener listener) { + if (authentication == null) { + listener.onFailure(new ElasticsearchSecurityException("no authentication available to generate API key for enrollment token")); + return null; + } + apiKeyService.ensureEnabled(); + rolesStore.getRoleDescriptors(new HashSet<>(Collections.singleton("enroll")), + ActionListener.wrap(roleDescriptors -> { + for (RoleDescriptor rd : roleDescriptors) { + try { + DLSRoleQueryValidator.validateQueryField(rd.getIndicesPrivileges(), xContentRegistry); + } catch (ElasticsearchException | IllegalArgumentException e) { + listener.onFailure(e); + return; + } + } + apiKeyService.createApiKeyForEnrollment(authentication, request, roleDescriptors, listener); + }, + listener::onFailure)); + + } } From 6441ac6b8b2c09b2005efa18494420848773d907 Mon Sep 17 00:00:00 2001 From: Lyudmila Fokina <35386883+BigPandaToo@users.noreply.github.com> Date: Sat, 24 Apr 2021 01:23:09 +0200 Subject: [PATCH 03/14] Create enrollment token API API can be called by the startup process or a user with appropriate privileges while elasticsearch is in the enrollment mode to obtain an enrollment token used to enroll a new node to the cluster or configure a new client to communicate with the cluster. Resolve: #71438 Related: #72129 --- docs/reference/cluster.asciidoc | 2 + .../cluster/create_enrollment_token.asciidoc | 57 +++++++++++++++ .../api/cluster.create_enrollment_token.json | 24 +++++++ .../common/settings/ClusterSettings.java | 4 +- .../enrollment/EnrollmentSettings.java | 22 ++++++ .../license/XPackLicenseState.java | 14 +++- .../CreateEnrollmentTokenResponse.java | 3 + .../privilege/ClusterPrivilegeResolver.java | 2 +- .../action/CreateEnrollmentTokenTests.java | 39 +++++++++++ .../xpack/core/action/httpCA.pem | 25 +++++++ .../xpack/security/operator/Constants.java | 1 + .../xpack/security/Security.java | 7 +- .../TransportCreateEnrollmentTokenAction.java | 69 +++++++++++++++---- .../xpack/security/authc/ApiKeyService.java | 15 ++-- .../authc/support/ApiKeyGenerator.java | 13 ++-- .../RestCreateEnrollmentTokenAction.java | 4 +- 16 files changed, 266 insertions(+), 35 deletions(-) create mode 100644 docs/reference/cluster/create_enrollment_token.asciidoc create mode 100644 rest-api-spec/src/main/resources/rest-api-spec/api/cluster.create_enrollment_token.json create mode 100644 server/src/main/java/org/elasticsearch/enrollment/EnrollmentSettings.java create mode 100644 x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateEnrollmentTokenTests.java create mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/core/action/httpCA.pem diff --git a/docs/reference/cluster.asciidoc b/docs/reference/cluster.asciidoc index 66fd5525e7b8e..754022d5cfc41 100644 --- a/docs/reference/cluster.asciidoc +++ b/docs/reference/cluster.asciidoc @@ -78,6 +78,8 @@ GET /_nodes/ra*:2* include::cluster/allocation-explain.asciidoc[] +include::cluster/create_enrollment_token.asciidoc[] + include::cluster/get-settings.asciidoc[] include::cluster/health.asciidoc[] diff --git a/docs/reference/cluster/create_enrollment_token.asciidoc b/docs/reference/cluster/create_enrollment_token.asciidoc new file mode 100644 index 0000000000000..19e850065eceb --- /dev/null +++ b/docs/reference/cluster/create_enrollment_token.asciidoc @@ -0,0 +1,57 @@ +[[create-enrollment-token]] +=== Create Enrollment Token API +++++ +Create Enrollment Token +++++ + +Create an enrollment token to enroll a new node to join an existing cluster with security enabled. + +[[create-enrollment-token-api-request]] +==== {api-request-title} + +`POST /_cluster/enrollment_token` + +[[cluster-create-enrollment-token-api-prereqs]] +==== {api-prereq-title} + +* You must have the `manage_enrollment` <> to use this API. +[[cluster-create-enrollment-token-api-desc]] +==== {api-description-title} + +The purpose of the create enrollment token API is to generate a token with which user can enroll a new node +to join an existing cluster where security is enabled. + +[[cluster-create-enrollment-token-api-examples]] +==== {api-examples-title} + +[source,console] +-------------------------------------------------- +POST /_cluster/enrollment_token +-------------------------------------------------- + +The API returns a response such as + +[source,console-result] +-------------------------------------------------- +{ + "enrollment_token" : "eyJhZHIiOiIxOTQ2ZHY0JDZGJrU....UTm1zeWFrdzl0dk5udyJ9Cg==" <1> +} +-------------------------------------------------- + +<1> The enrollment token containing information about: + - IP Address and port number for the interface where the Elasticsearch node is listening on; + - The fingerprint of the CA certificate that is used to sign the certificate that the Elasticsearch node presents; + - Credentials by which the holder of the token can authenticate itself to the elasticsearch node. An API key is used for this. + +as Base64 encoded string. + +The enrollment key may look like: +[source,console-result] +-------------------------------------------------- +{ +"adr":"192.168.1.43:9201", +"fgr":"48:CC:6C:F8:76:43:3C:97:85:....0A:C8:8D:AE:5C:4D", +"key":"VuaCfGcBCdbkQm-e5aOx:ui2lp2axTNmsyakw9tvNnw" +} +-------------------------------------------------- + diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/cluster.create_enrollment_token.json b/rest-api-spec/src/main/resources/rest-api-spec/api/cluster.create_enrollment_token.json new file mode 100644 index 0000000000000..db6c610e534de --- /dev/null +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/cluster.create_enrollment_token.json @@ -0,0 +1,24 @@ +{ + "cluster.create_enrollment_token":{ + "documentation":{ + "url":"https://www.elastic.co/guide/en/elasticsearch/reference/master/cluster-create-enrollment-token.html", + "description":"Create an enrollment token to enroll a new node to join an existing cluster with security enabled." + }, + "stability":"stable", + "visibility":"public", + "headers":{ + "accept": [ "application/json"], + "content_type": ["application/json"] + }, + "url":{ + "paths":[ + { + "path":"/_cluster/enrollment_token", + "methods":[ + "POST" + ] + } + ] + } + } +} diff --git a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java index 01013cfbe093c..79f5d3a7cf3ed 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java @@ -59,6 +59,7 @@ import org.elasticsearch.discovery.PeerFinder; import org.elasticsearch.discovery.SeedHostsResolver; import org.elasticsearch.discovery.SettingsBasedSeedHostsProvider; +import org.elasticsearch.enrollment.EnrollmentSettings; import org.elasticsearch.env.Environment; import org.elasticsearch.env.NodeEnvironment; import org.elasticsearch.gateway.GatewayService; @@ -479,7 +480,8 @@ public void apply(Settings value, Settings current, Settings previous) { FsHealthService.REFRESH_INTERVAL_SETTING, FsHealthService.SLOW_PATH_LOGGING_THRESHOLD_SETTING, IndexingPressure.MAX_INDEXING_BYTES, - ShardLimitValidator.SETTING_CLUSTER_MAX_SHARDS_PER_NODE_FROZEN); + ShardLimitValidator.SETTING_CLUSTER_MAX_SHARDS_PER_NODE_FROZEN, + EnrollmentSettings.ENROLLMENT_ENABLED); static List> BUILT_IN_SETTING_UPGRADERS = Collections.emptyList(); diff --git a/server/src/main/java/org/elasticsearch/enrollment/EnrollmentSettings.java b/server/src/main/java/org/elasticsearch/enrollment/EnrollmentSettings.java new file mode 100644 index 0000000000000..fe2788e279b05 --- /dev/null +++ b/server/src/main/java/org/elasticsearch/enrollment/EnrollmentSettings.java @@ -0,0 +1,22 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.enrollment; + +import org.elasticsearch.common.settings.Setting; + +public class EnrollmentSettings { + + private EnrollmentSettings() { + } + + /** Setting for enabling or disabling enrollment mode. Defaults to false. */ + public static final Setting ENROLLMENT_ENABLED = Setting.boolSetting("cluster.enrollment.enabled", false, + Setting.Property.NodeScope); + +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java index c3907b136986e..40abe98d05a89 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java @@ -13,6 +13,7 @@ import org.elasticsearch.common.logging.HeaderWarning; import org.elasticsearch.common.logging.LoggerMessageFormat; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.enrollment.EnrollmentSettings; import org.elasticsearch.license.License.OperationMode; import org.elasticsearch.xpack.core.XPackField; import org.elasticsearch.xpack.core.XPackSettings; @@ -405,6 +406,7 @@ private static class Status { private final boolean isSecurityExplicitlyEnabled; private final Map lastUsed; private final LongSupplier epochMillisProvider; + private final boolean isInEnrollmentMode; // Since Status is the only field that can be updated, we do not need to synchronize access to // XPackLicenseState. However, if status is read multiple times in a method, it can change in between @@ -416,6 +418,7 @@ public XPackLicenseState(Settings settings, LongSupplier epochMillisProvider) { this.listeners = new CopyOnWriteArrayList<>(); this.isSecurityEnabled = XPackSettings.SECURITY_ENABLED.get(settings); this.isSecurityExplicitlyEnabled = isSecurityEnabled && isSecurityExplicitlyEnabled(settings); + this.isInEnrollmentMode = EnrollmentSettings.ENROLLMENT_ENABLED.get(settings); // prepopulate feature last used map with entries for non basic features, which are the ones we // care to actually keep track of @@ -430,13 +433,15 @@ public XPackLicenseState(Settings settings, LongSupplier epochMillisProvider) { } private XPackLicenseState(List listeners, boolean isSecurityEnabled, boolean isSecurityExplicitlyEnabled, - Status status, Map lastUsed, LongSupplier epochMillisProvider) { + Status status, Map lastUsed, LongSupplier epochMillisProvider, + boolean isInEnrollmentMode) { this.listeners = listeners; this.isSecurityEnabled = isSecurityEnabled; this.isSecurityExplicitlyEnabled = isSecurityExplicitlyEnabled; this.status = status; this.lastUsed = lastUsed; this.epochMillisProvider = epochMillisProvider; + this.isInEnrollmentMode = isInEnrollmentMode; } private static boolean isSecurityExplicitlyEnabled(Settings settings) { @@ -559,6 +564,10 @@ public boolean isSecurityEnabled() { return isSecurityEnabled(status.mode, isSecurityExplicitlyEnabled, isSecurityEnabled); } + public boolean isInEnrollmentMode() { + return this.isInEnrollmentMode; + } + public static boolean isTransportTlsRequired(License license, Settings settings) { if (license == null) { return false; @@ -611,7 +620,8 @@ public static boolean isAllowedByOperationMode( */ public XPackLicenseState copyCurrentLicenseState() { return executeAgainstStatus(status -> - new XPackLicenseState(listeners, isSecurityEnabled, isSecurityExplicitlyEnabled, status, lastUsed, epochMillisProvider)); + new XPackLicenseState(listeners, isSecurityEnabled, isSecurityExplicitlyEnabled, status, lastUsed, epochMillisProvider, + isInEnrollmentMode)); } /** diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenResponse.java index 2fac28d3dcdf6..5f238e13f90a6 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenResponse.java @@ -8,6 +8,7 @@ package org.elasticsearch.xpack.core.security.action.enrollment; import org.elasticsearch.action.ActionResponse; +import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; @@ -18,6 +19,8 @@ * Response containing the enrollment token string that was generated from an enrollment token creation request. */ public class CreateEnrollmentTokenResponse extends ActionResponse { + private static final ParseField ENROLLMENT_TOKEN = new ParseField("enrollment_token"); + private String enrollmentTokenString; public CreateEnrollmentTokenResponse(StreamInput in) throws IOException { 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 8969cea6b97f2..d2a32fa0f7cc3 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 @@ -134,7 +134,7 @@ public class ClusterPrivilegeResolver { "manage_autoscaling", Set.of("cluster:admin/autoscaling/*") ); - private static final Set MANAGE_ENROLLMENT_PATTERN = Set.of("cluster:admin/xpack/security/enrollment/*"); + private static final Set MANAGE_ENROLLMENT_PATTERN = Set.of("cluster:admin/enrollment/*"); public static final NamedClusterPrivilege MANAGE_CCR = new ActionClusterPrivilege("manage_ccr", MANAGE_CCR_PATTERN); public static final NamedClusterPrivilege READ_CCR = new ActionClusterPrivilege("read_ccr", READ_CCR_PATTERN); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateEnrollmentTokenTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateEnrollmentTokenTests.java new file mode 100644 index 0000000000000..528ee825b1774 --- /dev/null +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateEnrollmentTokenTests.java @@ -0,0 +1,39 @@ +/* + * 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; + +import org.elasticsearch.common.io.stream.BytesStreamOutput; +import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenResponse; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +public class CreateEnrollmentTokenTests extends ESTestCase { + + public void testSerialization() throws Exception { + CreateEnrollmentTokenResponse response = createTestInstance(); + try (BytesStreamOutput out = new BytesStreamOutput()) { + response.writeTo(out); + try (StreamInput in = out.bytes().streamInput()) { + CreateEnrollmentTokenResponse serialized = new CreateEnrollmentTokenResponse(in); + assertEquals(response.getEnrollmentToken(), serialized.getEnrollmentToken()); + } + } + } + + protected CreateEnrollmentTokenResponse createTestInstance() { + final String jsonString = "{\"adr\":\"192.168.1.43:9201\",\"fgr\":\"" + + "48:CC:6C:F8:76:43:3C:97:85:B6:24:45:5B:FF:BD:40:4B:D6:35:81:51:E7:A9:99:60:E4:0A:C8:8D:AE:5C:4D\",\"key\":\"" + + "VuaCfGcBCdbkQm-e5aOx:ui2lp2axTNmsyakw9tvNnw\" }"; + + final String token = Base64.getEncoder().encodeToString(jsonString.getBytes(StandardCharsets.UTF_8)); + return new CreateEnrollmentTokenResponse(token); + } +} diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/core/action/httpCA.pem b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/core/action/httpCA.pem new file mode 100644 index 0000000000000..c8a0918e5160d --- /dev/null +++ b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/core/action/httpCA.pem @@ -0,0 +1,25 @@ +Bag Attributes + friendlyName: ca + localKeyID: 54 69 6D 65 20 31 36 31 39 32 30 30 32 36 39 38 30 35 +subject=/CN=Elastic Certificate Tool Autogenerated CA +issuer=/CN=Elastic Certificate Tool Autogenerated CA +-----BEGIN CERTIFICATE----- +MIIDSjCCAjKgAwIBAgIVAJYNz6ukOFW1+vygPZ99NGPSZRNOMA0GCSqGSIb3DQEB +CwUAMDQxMjAwBgNVBAMTKUVsYXN0aWMgQ2VydGlmaWNhdGUgVG9vbCBBdXRvZ2Vu +ZXJhdGVkIENBMB4XDTIxMDQyMzE3NTA1NFoXDTI0MDQyMjE3NTA1NFowNDEyMDAG +A1UEAxMpRWxhc3RpYyBDZXJ0aWZpY2F0ZSBUb29sIEF1dG9nZW5lcmF0ZWQgQ0Ew +ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCuRxEbHE8sH7nPuvlCIcAw +nV61P7VNLzOIdjWPC9L8WtEkOy8ahEjonxZ74QyEL38fc4ZmtcZorxrve3rqKiVF +xiQkOlNjz5D9ziKP62jCl7iRiZeSMe3DXqU2TA+WdEtbVHO/UuuA+7M8uPgc9r8X +Usg6tjGUMB5Jta3Jh80WC8esXD/KB/1Cb4laW3GDOKYfVJ6xBQ0QqYQNRbQisY7a +nDlWVBou/al4XM8ovcZm2SrdrtVrNVO1bMpyO7wIMJk0/ckq5o8xs9QUd3UfAnZ1 +5QlqsLCr09EJvJnI+P5L+b5HP/kiSNp+NXyiq+4rfdLwDIaTZDKnVM7CG2rk4ZdB +AgMBAAGjUzBRMB0GA1UdDgQWBBRMK0ffcvF8fwK+9vmzLefmTJq2yDAfBgNVHSME +GDAWgBRMK0ffcvF8fwK+9vmzLefmTJq2yDAPBgNVHRMBAf8EBTADAQH/MA0GCSqG +SIb3DQEBCwUAA4IBAQCAiEV9wGWt6BpDYOudBfi674tX+yiF2rXAurnbFQtiTa8/ +o7y99wx7Jt9GWLY+uASapKQ5ybu68wRbW3MVJpIu7T5Abo30+VsoUMdJDCKoh2Uv +jR8VxtCmUTSi8/htXB4WI9S7AkpEBEWAqT0XY6C35+W+3J6w95GuCTq5ZijURE+J +7j5+aCydq4KmLGKrJlGQGOWz+bNJvwmxxZTvYEhAwaAuj47Ba91rE4D6Lob40LSV +b1gRpIkwf5EyNWALnqSqWpnF4qP9mSZHCi1aRlAb9G5GtrDushXHVQVFN0hTqkzh +YdOtGvJvCNAn0xtZwn9WjSoEzpV+6ByaEgqP50KV +-----END CERTIFICATE----- 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 86cfc55bd4558..9947bf05437d6 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 @@ -172,6 +172,7 @@ public class Constants { "cluster:admin/xpack/security/api_key/invalidate", "cluster:admin/xpack/security/cache/clear", "cluster:admin/xpack/security/delegate_pki", + "cluster:admin/xpack/security/enrollment/create", "cluster:admin/xpack/security/oidc/authenticate", "cluster:admin/xpack/security/oidc/logout", "cluster:admin/xpack/security/oidc/prepare", 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 18f88df5ea501..519e96468ce18 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 @@ -90,6 +90,7 @@ import org.elasticsearch.xpack.core.security.action.GetApiKeyAction; import org.elasticsearch.xpack.core.security.action.GrantApiKeyAction; import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyAction; +import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenAction; import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectAuthenticateAction; import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectLogoutAction; import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectPrepareAuthenticationAction; @@ -160,6 +161,7 @@ import org.elasticsearch.xpack.security.action.TransportGetApiKeyAction; import org.elasticsearch.xpack.security.action.TransportGrantApiKeyAction; import org.elasticsearch.xpack.security.action.TransportInvalidateApiKeyAction; +import org.elasticsearch.xpack.security.action.enrollment.TransportCreateEnrollmentTokenAction; import org.elasticsearch.xpack.security.action.filter.SecurityActionFilter; import org.elasticsearch.xpack.security.action.oidc.TransportOpenIdConnectAuthenticateAction; import org.elasticsearch.xpack.security.action.oidc.TransportOpenIdConnectLogoutAction; @@ -243,6 +245,7 @@ import org.elasticsearch.xpack.security.rest.action.apikey.RestGetApiKeyAction; import org.elasticsearch.xpack.security.rest.action.apikey.RestGrantApiKeyAction; import org.elasticsearch.xpack.security.rest.action.apikey.RestInvalidateApiKeyAction; +import org.elasticsearch.xpack.security.rest.action.enrollment.RestCreateEnrollmentTokenAction; import org.elasticsearch.xpack.security.rest.action.oauth2.RestGetTokenAction; import org.elasticsearch.xpack.security.rest.action.oauth2.RestInvalidateTokenAction; import org.elasticsearch.xpack.security.rest.action.oidc.RestOpenIdConnectAuthenticateAction; @@ -884,6 +887,7 @@ public void onIndexModule(IndexModule module) { new ActionHandler<>(DeleteServiceAccountTokenAction.INSTANCE, TransportDeleteServiceAccountTokenAction.class), new ActionHandler<>(GetServiceAccountCredentialsAction.INSTANCE, TransportGetServiceAccountCredentialsAction.class), new ActionHandler<>(GetServiceAccountAction.INSTANCE, TransportGetServiceAccountAction.class), + new ActionHandler<>(CreateEnrollmentTokenAction.INSTANCE, TransportCreateEnrollmentTokenAction.class), usageAction, infoAction); } @@ -948,7 +952,8 @@ public List getRestHandlers(Settings settings, RestController restC new RestCreateServiceAccountTokenAction(settings, getLicenseState()), new RestDeleteServiceAccountTokenAction(settings, getLicenseState()), new RestGetServiceAccountCredentialsAction(settings, getLicenseState()), - new RestGetServiceAccountAction(settings, getLicenseState()) + new RestGetServiceAccountAction(settings, getLicenseState()), + new RestCreateEnrollmentTokenAction(settings, getLicenseState()) ); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java index 9f83c3dda6002..f620eefc22067 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java @@ -9,21 +9,36 @@ import org.apache.logging.log4j.message.ParameterizedMessage; import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.HandledTransportAction; +import org.elasticsearch.common.Strings; import org.elasticsearch.common.inject.Inject; +import org.elasticsearch.common.ssl.SslUtil; import org.elasticsearch.common.xcontent.NamedXContentRegistry; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.env.Environment; +import org.elasticsearch.http.HttpInfo; +import org.elasticsearch.node.NodeService; import org.elasticsearch.tasks.Task; import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.core.security.SecurityContext; import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenAction; import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenRequest; import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenResponse; -import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.ssl.CertParsingUtils; import org.elasticsearch.xpack.security.authc.ApiKeyService; import org.elasticsearch.xpack.security.authc.support.ApiKeyGenerator; import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.List; + /** * Transport action responsible for creating an enrollment token based on a request. */ @@ -33,14 +48,18 @@ public class TransportCreateEnrollmentTokenAction private final ApiKeyGenerator generator; private final SecurityContext securityContext; + private final Environment environment; + private final NodeService nodeService; @Inject public TransportCreateEnrollmentTokenAction(TransportService transportService, ActionFilters actionFilters, ApiKeyService apiKeyService, SecurityContext context, CompositeRolesStore rolesStore, - NamedXContentRegistry xContentRegistry) { + NamedXContentRegistry xContentRegistry, Environment environment, NodeService nodeService) { super(CreateEnrollmentTokenAction.NAME, transportService, actionFilters, CreateEnrollmentTokenRequest::new); this.generator = new ApiKeyGenerator(apiKeyService, rolesStore, xContentRegistry); this.securityContext = context; + this.environment = environment; + this.nodeService = nodeService; } @Override @@ -51,20 +70,42 @@ protected void doExecute(Task task, CreateEnrollmentTokenRequest request, private void createEnrolmentToken(CreateEnrollmentTokenRequest request, ActionListener listener) { try { - String enrollmentTokenString = new String(); - listener.onResponse(new CreateEnrollmentTokenResponse(enrollmentTokenString)); + generator.generateApiKeyForEnrollment(securityContext.getAuthentication(), request, + ActionListener.wrap( + CreateApiKeyResponse -> {; + final String httpCaCert = "httpCa.pem"; + final Path httpCaCertPath = environment.configFile().resolve(httpCaCert); + if (Files.exists(httpCaCertPath) == false) { + listener.onFailure(new IllegalStateException("HTTP layer CA certificate " + httpCaCert + " does not exist")); + } + + NodeInfo nodeInfo = nodeService.info(false, false, false, false, false, false, + true, false, false, false, false); + HttpInfo httpInfo = nodeInfo.getInfo(HttpInfo.class); + String address = httpInfo.getAddress().publishAddress().toString(); + + final X509Certificate[] certificates = CertParsingUtils.readX509Certificates(List.of(httpCaCertPath)); + final X509Certificate cert = certificates[0]; + final String fingerprint = SslUtil.calculateFingerprint(cert); + + XContentBuilder builder = JsonXContent.contentBuilder(); + builder.startObject(); + builder.field("adr", address); + builder.field("fgr", fingerprint); + builder.field("key", CreateApiKeyResponse.getKey().toString()); + builder.endObject(); + String jsonString = Strings.toString(builder); + String token = Base64.getEncoder().encodeToString(jsonString.getBytes(StandardCharsets.UTF_8)); + + final CreateEnrollmentTokenResponse response = new CreateEnrollmentTokenResponse(token); + listener.onResponse(response); + }, + listener::onFailure + ) + ); } catch (Exception e) { - logger.error(new ParameterizedMessage("Error generating enrollment token", e)); + logger.error(() -> new ParameterizedMessage("Error generating enrollment token"), e); listener.onFailure(e); } } - - private void createApiKey(CreateEnrollmentTokenRequest request, ActionListener listener) { - final Authentication authentication = securityContext.getAuthentication(); - if (authentication == null) { - listener.onFailure(new IllegalStateException("authentication is required")); - } else { - generator.generateApiKeyForEnrollment(authentication, request, 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 add17d05e5022..12acabae71a63 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 @@ -15,8 +15,6 @@ import org.elasticsearch.ExceptionsHelper; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.ActionRequest; -import org.elasticsearch.action.ActionResponse; import org.elasticsearch.action.ActionRunnable; import org.elasticsearch.action.DocWriteResponse; import org.elasticsearch.action.bulk.BulkAction; @@ -87,7 +85,6 @@ import org.elasticsearch.xpack.core.security.action.GetApiKeyResponse; import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyResponse; import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenRequest; -import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenResponse; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; @@ -261,9 +258,15 @@ public void createApiKey(Authentication authentication, CreateApiKeyRequest requ } } + public boolean isInEnrollmentMode () { + return licenseState.isInEnrollmentMode(); + } + public void createApiKeyForEnrollment(Authentication authentication, CreateEnrollmentTokenRequest request, - Set userRoles, ActionListener listener) { - ensureEnabled(); + Set userRoles, ActionListener listener) { + if (isInEnrollmentMode () == false) { + listener.onFailure(new IllegalArgumentException("Enrollment mode is not enabled")); + } if (authentication == null) { listener.onFailure(new IllegalArgumentException("authentication must be provided")); } else { @@ -304,7 +307,7 @@ private void createApiKeyAndIndexIt(Authentication authentication, CreateApiKeyR } private void createApiKeyAndIndexItForEnrollmentToken(Authentication authentication, CreateEnrollmentTokenRequest request, - Set roleDescriptorSet, ActionListener listener) { + Set roleDescriptorSet, ActionListener listener) { final Instant created = clock.instant(); final Instant expiration = created.plusSeconds(ENROLL_API_KEY_EXPIRATION_SEC); final SecureString apiKey = UUIDs.randomBase64UUIDSecureString(); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/ApiKeyGenerator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/ApiKeyGenerator.java index e3424164d0676..458ad9362cef5 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/ApiKeyGenerator.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/ApiKeyGenerator.java @@ -10,12 +10,10 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.action.ActionListener; -import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.xpack.core.security.action.CreateApiKeyRequest; import org.elasticsearch.xpack.core.security.action.CreateApiKeyResponse; import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenRequest; -import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenResponse; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.support.DLSRoleQueryValidator; @@ -60,13 +58,14 @@ public void generateApiKey(Authentication authentication, CreateApiKeyRequest re } - public SecureString generateApiKeyForEnrollment(Authentication authentication, CreateEnrollmentTokenRequest request, - ActionListener listener) { + public void generateApiKeyForEnrollment(Authentication authentication, CreateEnrollmentTokenRequest request, + ActionListener listener) { if (authentication == null) { listener.onFailure(new ElasticsearchSecurityException("no authentication available to generate API key for enrollment token")); - return null; } - apiKeyService.ensureEnabled(); + if (apiKeyService.isInEnrollmentMode () == false) { + listener.onFailure(new IllegalArgumentException("Enrollment mode is not enabled")); + } rolesStore.getRoleDescriptors(new HashSet<>(Collections.singleton("enroll")), ActionListener.wrap(roleDescriptors -> { for (RoleDescriptor rd : roleDescriptors) { @@ -74,12 +73,10 @@ public SecureString generateApiKeyForEnrollment(Authentication authentication, C DLSRoleQueryValidator.validateQueryField(rd.getIndicesPrivileges(), xContentRegistry); } catch (ElasticsearchException | IllegalArgumentException e) { listener.onFailure(e); - return; } } apiKeyService.createApiKeyForEnrollment(authentication, request, roleDescriptors, listener); }, listener::onFailure)); - } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/enrollment/RestCreateEnrollmentTokenAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/enrollment/RestCreateEnrollmentTokenAction.java index eab3c96ff9fea..579d5748c366b 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/enrollment/RestCreateEnrollmentTokenAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/enrollment/RestCreateEnrollmentTokenAction.java @@ -51,7 +51,7 @@ public List routes() { @Override public String getName() { - return "cluster_enrolment_token"; + return "cluster_enrolment_token_action"; } @Override @@ -63,7 +63,7 @@ protected RestChannelConsumer innerPrepareRequest(final RestRequest request, fin @Override public RestResponse buildResponse(CreateEnrollmentTokenResponse response, XContentBuilder builder) throws Exception { builder.startObject(); - builder.field("enrollment_token", response.getEnrollmentToken()); + builder.field("enrollment_token", response.getEnrollmentToken().toString()); builder.endObject(); return new BytesRestResponse(RestStatus.OK, builder); } From 60c0574000d83642e886bbe9c3fd3a5a2cc60b54 Mon Sep 17 00:00:00 2001 From: Lyudmila Fokina <35386883+BigPandaToo@users.noreply.github.com> Date: Mon, 26 Apr 2021 19:01:00 +0200 Subject: [PATCH 04/14] Addressing PR feedback --- docs/reference/cluster.asciidoc | 2 - ...iidoc => create-enrollment-token.asciidoc} | 18 ++++--- .../api/cluster.create_enrollment_token.json | 2 +- .../enrollment/EnrollmentSettings.java | 2 - .../security/get-builtin-privileges.asciidoc | 2 +- .../authorization/privileges.asciidoc | 7 ++- .../license/XPackLicenseState.java | 14 +---- .../CreateEnrollmentTokenAction.java | 7 +-- .../CreateEnrollmentTokenRequest.java | 9 +--- .../CreateEnrollmentTokenResponse.java | 13 ++++- .../privilege/ClusterPrivilegeResolver.java | 5 +- ...> CreateEnrollmentTokenResponseTests.java} | 34 ++++++++++-- .../xpack/security/Security.java | 2 +- .../TransportCreateEnrollmentTokenAction.java | 40 +++++++++----- .../xpack/security/authc/ApiKeyService.java | 53 ------------------- .../authc/support/ApiKeyGenerator.java | 24 --------- .../RestCreateEnrollmentTokenAction.java | 12 +++-- 17 files changed, 103 insertions(+), 143 deletions(-) rename docs/reference/cluster/{create_enrollment_token.asciidoc => create-enrollment-token.asciidoc} (65%) rename x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/{security/action => }/enrollment/CreateEnrollmentTokenAction.java (68%) rename x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/{security/action => }/enrollment/CreateEnrollmentTokenRequest.java (84%) rename x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/{security/action => }/enrollment/CreateEnrollmentTokenResponse.java (79%) rename x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/{CreateEnrollmentTokenTests.java => CreateEnrollmentTokenResponseTests.java} (54%) diff --git a/docs/reference/cluster.asciidoc b/docs/reference/cluster.asciidoc index 754022d5cfc41..66fd5525e7b8e 100644 --- a/docs/reference/cluster.asciidoc +++ b/docs/reference/cluster.asciidoc @@ -78,8 +78,6 @@ GET /_nodes/ra*:2* include::cluster/allocation-explain.asciidoc[] -include::cluster/create_enrollment_token.asciidoc[] - include::cluster/get-settings.asciidoc[] include::cluster/health.asciidoc[] diff --git a/docs/reference/cluster/create_enrollment_token.asciidoc b/docs/reference/cluster/create-enrollment-token.asciidoc similarity index 65% rename from docs/reference/cluster/create_enrollment_token.asciidoc rename to docs/reference/cluster/create-enrollment-token.asciidoc index 19e850065eceb..0d549328adbb5 100644 --- a/docs/reference/cluster/create_enrollment_token.asciidoc +++ b/docs/reference/cluster/create-enrollment-token.asciidoc @@ -4,9 +4,10 @@ Create Enrollment Token ++++ -Create an enrollment token to enroll a new node to join an existing cluster with security enabled. +Create an enrollment token to allow a new node enroll in an existing secured elasticsearch cluster, or a client to configure itself to +communicate with a secured elasticsearch cluster. -[[create-enrollment-token-api-request]] +[[cluster-create-enrollment-token-api-request]] ==== {api-request-title} `POST /_cluster/enrollment_token` @@ -19,7 +20,8 @@ Create an enrollment token to enroll a new node to join an existing cluster with ==== {api-description-title} The purpose of the create enrollment token API is to generate a token with which user can enroll a new node -to join an existing cluster where security is enabled. +in an existing secured elasticsearch cluster, or a client can configure itself to +communicate with a secured elasticsearch cluster. [[cluster-create-enrollment-token-api-examples]] ==== {api-examples-title} @@ -38,14 +40,14 @@ The API returns a response such as } -------------------------------------------------- -<1> The enrollment token containing information about: - - IP Address and port number for the interface where the Elasticsearch node is listening on; - - The fingerprint of the CA certificate that is used to sign the certificate that the Elasticsearch node presents; - - Credentials by which the holder of the token can authenticate itself to the elasticsearch node. An API key is used for this. +<1> The enrollment token contains the following information: + - IP Address and port number for the interface where the Elasticsearch node is listening for HTTP connections; + - The fingerprint of the CA certificate that is used to sign the certificate that the Elasticsearch node presents for TLS on the HTTP layer; + - An API key which allows a holder of the token to authenticate themself to the elasticsearch node. as Base64 encoded string. -The enrollment key may look like: +A decoded enrollment token is shown below (spaces and new lines added for clarity) : [source,console-result] -------------------------------------------------- { diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/cluster.create_enrollment_token.json b/rest-api-spec/src/main/resources/rest-api-spec/api/cluster.create_enrollment_token.json index db6c610e534de..f086e0a8609d4 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/cluster.create_enrollment_token.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/cluster.create_enrollment_token.json @@ -2,7 +2,7 @@ "cluster.create_enrollment_token":{ "documentation":{ "url":"https://www.elastic.co/guide/en/elasticsearch/reference/master/cluster-create-enrollment-token.html", - "description":"Create an enrollment token to enroll a new node to join an existing cluster with security enabled." + "description":"Create an enrollment token to allow a new node enroll in an existing secured elasticsearch cluster, or a client to configure itself to\ncommunicate with a secured elasticsearch cluster." }, "stability":"stable", "visibility":"public", diff --git a/server/src/main/java/org/elasticsearch/enrollment/EnrollmentSettings.java b/server/src/main/java/org/elasticsearch/enrollment/EnrollmentSettings.java index fe2788e279b05..caf9a5cebb44a 100644 --- a/server/src/main/java/org/elasticsearch/enrollment/EnrollmentSettings.java +++ b/server/src/main/java/org/elasticsearch/enrollment/EnrollmentSettings.java @@ -11,12 +11,10 @@ import org.elasticsearch.common.settings.Setting; public class EnrollmentSettings { - private EnrollmentSettings() { } /** Setting for enabling or disabling enrollment mode. Defaults to false. */ public static final Setting ENROLLMENT_ENABLED = Setting.boolSetting("cluster.enrollment.enabled", false, Setting.Property.NodeScope); - } 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 4f1f863536341..3a93218708144 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 @@ -74,7 +74,7 @@ A successful call returns an object with "cluster" and "index" fields. "manage_ccr", "manage_data_frame_transforms", "manage_enrich", - "manage_enrollment" + "manage_enrollment", "manage_ilm", "manage_index_templates", "manage_ingest_pipelines", diff --git a/x-pack/docs/en/security/authorization/privileges.asciidoc b/x-pack/docs/en/security/authorization/privileges.asciidoc index 35e43b0413c8e..8c224f7e2e510 100644 --- a/x-pack/docs/en/security/authorization/privileges.asciidoc +++ b/x-pack/docs/en/security/authorization/privileges.asciidoc @@ -21,8 +21,8 @@ Privileges to create snapshots for existing repositories. Can also list and view details on existing repositories and snapshots. `enroll`:: -The privilege to call the APIs to enroll a node in a cluster and to allow Kibana to -configure itself to communicate with an Elasticsearch cluster. +The privilege to call the enroll API to enroll a node in a secured Elasticsearch cluster and to allow a client to +configure itself to communicate with a secured Elasticsearch cluster. `grant_api_key`:: Privileges to create {es} API keys on behalf of other users. @@ -59,8 +59,7 @@ manage follower indices and auto-follow patterns. This privilege is necessary only on clusters that contain follower indices. `manage_enrollment`:: -The privilege to call the create enrollment token API and to call -the Enroll Node and Enroll Token API. `enroll` is also included in `manage_enrollment`. +The privilege to call the create enrollment token and enroll APIs. `enroll` is implicitly granted by `manage_enrollment`. `manage_ilm`:: All {Ilm} operations related to managing policies. diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java index 40abe98d05a89..c3907b136986e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java @@ -13,7 +13,6 @@ import org.elasticsearch.common.logging.HeaderWarning; import org.elasticsearch.common.logging.LoggerMessageFormat; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.enrollment.EnrollmentSettings; import org.elasticsearch.license.License.OperationMode; import org.elasticsearch.xpack.core.XPackField; import org.elasticsearch.xpack.core.XPackSettings; @@ -406,7 +405,6 @@ private static class Status { private final boolean isSecurityExplicitlyEnabled; private final Map lastUsed; private final LongSupplier epochMillisProvider; - private final boolean isInEnrollmentMode; // Since Status is the only field that can be updated, we do not need to synchronize access to // XPackLicenseState. However, if status is read multiple times in a method, it can change in between @@ -418,7 +416,6 @@ public XPackLicenseState(Settings settings, LongSupplier epochMillisProvider) { this.listeners = new CopyOnWriteArrayList<>(); this.isSecurityEnabled = XPackSettings.SECURITY_ENABLED.get(settings); this.isSecurityExplicitlyEnabled = isSecurityEnabled && isSecurityExplicitlyEnabled(settings); - this.isInEnrollmentMode = EnrollmentSettings.ENROLLMENT_ENABLED.get(settings); // prepopulate feature last used map with entries for non basic features, which are the ones we // care to actually keep track of @@ -433,15 +430,13 @@ public XPackLicenseState(Settings settings, LongSupplier epochMillisProvider) { } private XPackLicenseState(List listeners, boolean isSecurityEnabled, boolean isSecurityExplicitlyEnabled, - Status status, Map lastUsed, LongSupplier epochMillisProvider, - boolean isInEnrollmentMode) { + Status status, Map lastUsed, LongSupplier epochMillisProvider) { this.listeners = listeners; this.isSecurityEnabled = isSecurityEnabled; this.isSecurityExplicitlyEnabled = isSecurityExplicitlyEnabled; this.status = status; this.lastUsed = lastUsed; this.epochMillisProvider = epochMillisProvider; - this.isInEnrollmentMode = isInEnrollmentMode; } private static boolean isSecurityExplicitlyEnabled(Settings settings) { @@ -564,10 +559,6 @@ public boolean isSecurityEnabled() { return isSecurityEnabled(status.mode, isSecurityExplicitlyEnabled, isSecurityEnabled); } - public boolean isInEnrollmentMode() { - return this.isInEnrollmentMode; - } - public static boolean isTransportTlsRequired(License license, Settings settings) { if (license == null) { return false; @@ -620,8 +611,7 @@ public static boolean isAllowedByOperationMode( */ public XPackLicenseState copyCurrentLicenseState() { return executeAgainstStatus(status -> - new XPackLicenseState(listeners, isSecurityEnabled, isSecurityExplicitlyEnabled, status, lastUsed, epochMillisProvider, - isInEnrollmentMode)); + new XPackLicenseState(listeners, isSecurityEnabled, isSecurityExplicitlyEnabled, status, lastUsed, epochMillisProvider)); } /** diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrollment/CreateEnrollmentTokenAction.java similarity index 68% rename from x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenAction.java rename to x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrollment/CreateEnrollmentTokenAction.java index 75c3cc26e0474..91d88af73f01d 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrollment/CreateEnrollmentTokenAction.java @@ -5,16 +5,17 @@ * 2.0. */ -package org.elasticsearch.xpack.core.security.action.enrollment; +package org.elasticsearch.xpack.core.enrollment; import org.elasticsearch.action.ActionType; +import org.elasticsearch.common.settings.Setting; /** * ActionType for creating an enrollment new token */ public class CreateEnrollmentTokenAction extends ActionType { - public static final String NAME = "cluster:admin/xpack/security/enrollment/create"; - public static final org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenAction INSTANCE = + public static final String NAME = "cluster:admin/xpack/enrollment/create"; + public static final CreateEnrollmentTokenAction INSTANCE = new CreateEnrollmentTokenAction(); private CreateEnrollmentTokenAction() { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrollment/CreateEnrollmentTokenRequest.java similarity index 84% rename from x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenRequest.java rename to x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrollment/CreateEnrollmentTokenRequest.java index 54e77845292b5..5663d192aad2a 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrollment/CreateEnrollmentTokenRequest.java @@ -5,7 +5,7 @@ * 2.0. */ -package org.elasticsearch.xpack.core.security.action.enrollment; +package org.elasticsearch.xpack.core.enrollment; import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionRequestValidationException; @@ -20,20 +20,13 @@ */ public class CreateEnrollmentTokenRequest extends ActionRequest { - private final String id; public CreateEnrollmentTokenRequest(StreamInput in) throws IOException { super(in); - this.id = UUIDs.base64UUID(); } public CreateEnrollmentTokenRequest() { super(); - this.id = UUIDs.base64UUID(); - } - - public String getId() { - return id; } @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrollment/CreateEnrollmentTokenResponse.java similarity index 79% rename from x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenResponse.java rename to x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrollment/CreateEnrollmentTokenResponse.java index 5f238e13f90a6..5cf611eee130c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrollment/CreateEnrollmentTokenResponse.java @@ -5,12 +5,14 @@ * 2.0. */ -package org.elasticsearch.xpack.core.security.action.enrollment; +package org.elasticsearch.xpack.core.enrollment; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.common.ParseField; 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.Objects; @@ -18,7 +20,7 @@ /** * Response containing the enrollment token string that was generated from an enrollment token creation request. */ -public class CreateEnrollmentTokenResponse extends ActionResponse { +public class CreateEnrollmentTokenResponse extends ActionResponse implements ToXContentObject { private static final ParseField ENROLLMENT_TOKEN = new ParseField("enrollment_token"); private String enrollmentTokenString; @@ -53,4 +55,11 @@ public boolean equals(Object o) { public int hashCode() { return Objects.hash(enrollmentTokenString); } + + @Override + public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject(); + builder.field(ENROLLMENT_TOKEN.getPreferredName(), enrollmentTokenString); + return builder.endObject(); + } } 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 d2a32fa0f7cc3..cf6839917be7c 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 @@ -134,7 +134,6 @@ public class ClusterPrivilegeResolver { "manage_autoscaling", Set.of("cluster:admin/autoscaling/*") ); - private static final Set MANAGE_ENROLLMENT_PATTERN = Set.of("cluster:admin/enrollment/*"); public static final NamedClusterPrivilege MANAGE_CCR = new ActionClusterPrivilege("manage_ccr", MANAGE_CCR_PATTERN); public static final NamedClusterPrivilege READ_CCR = new ActionClusterPrivilege("read_ccr", READ_CCR_PATTERN); @@ -157,9 +156,9 @@ public class ClusterPrivilegeResolver { Set.of("cluster:admin/tasks/cancel")); public static final NamedClusterPrivilege MANAGE_ENROLLMENT = new ActionClusterPrivilege("manage_enrollment", - MANAGE_ENROLLMENT_PATTERN); + Set.of("cluster:admin/xpack/enrollment/*")); public static final NamedClusterPrivilege ENROLL = new ActionClusterPrivilege("enroll", - Set.of("cluster:admin/xpack/security/enrollment/enroll")); + Set.of("cluster:admin/xpack/enrollment/enroll")); private static final Map VALUES = sortByAccessLevel(List.of( NONE, diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateEnrollmentTokenTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateEnrollmentTokenResponseTests.java similarity index 54% rename from x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateEnrollmentTokenTests.java rename to x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateEnrollmentTokenResponseTests.java index 528ee825b1774..ea0b97164f6c2 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateEnrollmentTokenTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateEnrollmentTokenResponseTests.java @@ -7,15 +7,20 @@ package org.elasticsearch.xpack.core.security.action; +import org.elasticsearch.common.ParseField; import org.elasticsearch.common.io.stream.BytesStreamOutput; import org.elasticsearch.common.io.stream.StreamInput; -import org.elasticsearch.test.ESTestCase; -import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenResponse; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.test.AbstractXContentTestCase; +import org.elasticsearch.xpack.core.enrollment.CreateEnrollmentTokenResponse; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Base64; -public class CreateEnrollmentTokenTests extends ESTestCase { +public class CreateEnrollmentTokenResponseTests extends AbstractXContentTestCase { + private static final ParseField ENROLLMENT_TOKEN = new ParseField("enrollment_token"); public void testSerialization() throws Exception { CreateEnrollmentTokenResponse response = createTestInstance(); @@ -28,6 +33,7 @@ public void testSerialization() throws Exception { } } + @Override protected CreateEnrollmentTokenResponse createTestInstance() { final String jsonString = "{\"adr\":\"192.168.1.43:9201\",\"fgr\":\"" + "48:CC:6C:F8:76:43:3C:97:85:B6:24:45:5B:FF:BD:40:4B:D6:35:81:51:E7:A9:99:60:E4:0A:C8:8D:AE:5C:4D\",\"key\":\"" + @@ -36,4 +42,26 @@ protected CreateEnrollmentTokenResponse createTestInstance() { final String token = Base64.getEncoder().encodeToString(jsonString.getBytes(StandardCharsets.UTF_8)); return new CreateEnrollmentTokenResponse(token); } + + @Override + protected CreateEnrollmentTokenResponse doParseInstance(XContentParser parser) throws IOException { + return PARSER.apply(parser, null); + } + + @Override + protected boolean supportsUnknownFields() { + return true; + } + + @SuppressWarnings("unchecked") + public static final ConstructingObjectParser + PARSER = + new ConstructingObjectParser<>("create_enrollment_token_response", true, a -> { + final String enrollmentToken = (String) a[0]; + return new CreateEnrollmentTokenResponse(enrollmentToken); + }); + + static { + PARSER.declareString(ConstructingObjectParser.constructorArg(), ENROLLMENT_TOKEN); + } } 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 519e96468ce18..6134de5492885 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 @@ -90,7 +90,7 @@ import org.elasticsearch.xpack.core.security.action.GetApiKeyAction; import org.elasticsearch.xpack.core.security.action.GrantApiKeyAction; import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyAction; -import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenAction; +import org.elasticsearch.xpack.core.enrollment.CreateEnrollmentTokenAction; import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectAuthenticateAction; import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectLogoutAction; import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectPrepareAuthenticationAction; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java index f620eefc22067..2271b6ded5d69 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java @@ -13,8 +13,10 @@ import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.HandledTransportAction; import org.elasticsearch.common.Strings; +import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.ssl.SslUtil; +import org.elasticsearch.common.unit.TimeValue; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.json.JsonXContent; @@ -24,9 +26,11 @@ import org.elasticsearch.tasks.Task; import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.core.security.SecurityContext; -import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenAction; -import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenRequest; -import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenResponse; +import org.elasticsearch.xpack.core.security.action.CreateApiKeyRequest; +import org.elasticsearch.xpack.core.enrollment.CreateEnrollmentTokenAction; +import org.elasticsearch.xpack.core.enrollment.CreateEnrollmentTokenRequest; +import org.elasticsearch.xpack.core.enrollment.CreateEnrollmentTokenResponse; +import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.ssl.CertParsingUtils; import org.elasticsearch.xpack.security.authc.ApiKeyService; import org.elasticsearch.xpack.security.authc.support.ApiKeyGenerator; @@ -36,6 +40,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.Base64; import java.util.List; @@ -46,6 +51,8 @@ public class TransportCreateEnrollmentTokenAction extends HandledTransportAction { + public static final long ENROLL_API_KEY_EXPIRATION_SEC = 30*60; + private final ApiKeyGenerator generator; private final SecurityContext securityContext; private final Environment environment; @@ -65,37 +72,44 @@ public TransportCreateEnrollmentTokenAction(TransportService transportService, A @Override protected void doExecute(Task task, CreateEnrollmentTokenRequest request, ActionListener listener) { - createEnrolmentToken(request, listener); + createEnrolmentToken(listener); } - private void createEnrolmentToken(CreateEnrollmentTokenRequest request, ActionListener listener) { + private void createEnrolmentToken(ActionListener listener) { try { - generator.generateApiKeyForEnrollment(securityContext.getAuthentication(), request, + final TimeValue expiration = TimeValue.timeValueSeconds(ENROLL_API_KEY_EXPIRATION_SEC); + final List roleDescriptors = new ArrayList<>(1); + final String[] clusterPrivileges = { "enroll" }; + final RoleDescriptor roleDescriptor = new RoleDescriptor("create_enrollment_token", clusterPrivileges, null, null); + roleDescriptors.add(roleDescriptor); + CreateApiKeyRequest apiRequest = new CreateApiKeyRequest("enrollment_token_API_key_" + UUIDs.base64UUID(), + roleDescriptors, expiration); + generator.generateApiKey(securityContext.getAuthentication(), apiRequest, ActionListener.wrap( CreateApiKeyResponse -> {; final String httpCaCert = "httpCa.pem"; final Path httpCaCertPath = environment.configFile().resolve(httpCaCert); if (Files.exists(httpCaCertPath) == false) { listener.onFailure(new IllegalStateException("HTTP layer CA certificate " + httpCaCert + " does not exist")); + return; } - - NodeInfo nodeInfo = nodeService.info(false, false, false, false, false, false, + final NodeInfo nodeInfo = nodeService.info(false, false, false, false, false, false, true, false, false, false, false); - HttpInfo httpInfo = nodeInfo.getInfo(HttpInfo.class); - String address = httpInfo.getAddress().publishAddress().toString(); + final HttpInfo httpInfo = nodeInfo.getInfo(HttpInfo.class); + final String address = httpInfo.getAddress().publishAddress().toString(); final X509Certificate[] certificates = CertParsingUtils.readX509Certificates(List.of(httpCaCertPath)); final X509Certificate cert = certificates[0]; final String fingerprint = SslUtil.calculateFingerprint(cert); - XContentBuilder builder = JsonXContent.contentBuilder(); + final XContentBuilder builder = JsonXContent.contentBuilder(); builder.startObject(); builder.field("adr", address); builder.field("fgr", fingerprint); builder.field("key", CreateApiKeyResponse.getKey().toString()); builder.endObject(); - String jsonString = Strings.toString(builder); - String token = Base64.getEncoder().encodeToString(jsonString.getBytes(StandardCharsets.UTF_8)); + final String jsonString = Strings.toString(builder); + final String token = Base64.getEncoder().encodeToString(jsonString.getBytes(StandardCharsets.UTF_8)); final CreateEnrollmentTokenResponse response = new CreateEnrollmentTokenResponse(token); listener.onResponse(response); 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 12acabae71a63..d4d34f75a3a75 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 @@ -84,7 +84,6 @@ import org.elasticsearch.xpack.core.security.action.CreateApiKeyResponse; import org.elasticsearch.xpack.core.security.action.GetApiKeyResponse; import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyResponse; -import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenRequest; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; @@ -148,7 +147,6 @@ public class ApiKeyService { public static final String API_KEY_REALM_TYPE = "_es_api_key"; public static final String API_KEY_CREATOR_REALM_NAME = "_security_api_key_creator_realm_name"; public static final String API_KEY_CREATOR_REALM_TYPE = "_security_api_key_creator_realm_type"; - public static final long ENROLL_API_KEY_EXPIRATION_SEC = 30*60; public static final Setting PASSWORD_HASHING_ALGORITHM = new Setting<>( "xpack.security.authc.api_key.hashing.algorithm", "pbkdf2", Function.identity(), v -> { @@ -258,22 +256,6 @@ public void createApiKey(Authentication authentication, CreateApiKeyRequest requ } } - public boolean isInEnrollmentMode () { - return licenseState.isInEnrollmentMode(); - } - - public void createApiKeyForEnrollment(Authentication authentication, CreateEnrollmentTokenRequest request, - Set userRoles, ActionListener listener) { - if (isInEnrollmentMode () == false) { - listener.onFailure(new IllegalArgumentException("Enrollment mode is not enabled")); - } - if (authentication == null) { - listener.onFailure(new IllegalArgumentException("authentication must be provided")); - } else { - createApiKeyAndIndexItForEnrollmentToken(authentication, request, userRoles, listener); - } - } - private void createApiKeyAndIndexIt(Authentication authentication, CreateApiKeyRequest request, Set roleDescriptorSet, ActionListener listener) { final Instant created = clock.instant(); @@ -306,41 +288,6 @@ private void createApiKeyAndIndexIt(Authentication authentication, CreateApiKeyR } } - private void createApiKeyAndIndexItForEnrollmentToken(Authentication authentication, CreateEnrollmentTokenRequest request, - Set roleDescriptorSet, ActionListener listener) { - final Instant created = clock.instant(); - final Instant expiration = created.plusSeconds(ENROLL_API_KEY_EXPIRATION_SEC); - final SecureString apiKey = UUIDs.randomBase64UUIDSecureString(); - final Version version = clusterService.state().nodes().getMinNodeVersion(); - final String name = "enrollment_token_API_key_" + created.toString(); - final String requestId = request.getId(); - final WriteRequest.RefreshPolicy policy = WriteRequest.RefreshPolicy.IMMEDIATE; - - try (XContentBuilder builder = newDocument(apiKey, name, authentication, roleDescriptorSet, created, expiration, - null, version, null)) { - - final IndexRequest indexRequest = - client.prepareIndex(SECURITY_MAIN_ALIAS) - .setSource(builder) - .setId(requestId) - .setRefreshPolicy(policy) - .request(); - final BulkRequest bulkRequest = toSingleItemBulkRequest(indexRequest); - - securityIndex.prepareIndexIfNeededThenExecute(listener::onFailure, () -> - executeAsyncWithOrigin(client, SECURITY_ORIGIN, BulkAction.INSTANCE, bulkRequest, - TransportSingleItemBulkWriteAction.wrapBulkResponse(ActionListener.wrap( - indexResponse -> { - assert requestId.equals(indexResponse.getId()); - listener.onResponse( - new CreateApiKeyResponse(name, requestId, apiKey, expiration)); - }, - listener::onFailure)))); - } catch (IOException e) { - listener.onFailure(e); - } - } - /** * package-private for testing */ diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/ApiKeyGenerator.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/ApiKeyGenerator.java index 458ad9362cef5..953655fe35eed 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/ApiKeyGenerator.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/ApiKeyGenerator.java @@ -13,7 +13,6 @@ import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.xpack.core.security.action.CreateApiKeyRequest; import org.elasticsearch.xpack.core.security.action.CreateApiKeyResponse; -import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenRequest; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.support.DLSRoleQueryValidator; @@ -21,7 +20,6 @@ import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; import java.util.Arrays; -import java.util.Collections; import java.util.HashSet; public class ApiKeyGenerator { @@ -57,26 +55,4 @@ public void generateApiKey(Authentication authentication, CreateApiKeyRequest re listener::onFailure)); } - - public void generateApiKeyForEnrollment(Authentication authentication, CreateEnrollmentTokenRequest request, - ActionListener listener) { - if (authentication == null) { - listener.onFailure(new ElasticsearchSecurityException("no authentication available to generate API key for enrollment token")); - } - if (apiKeyService.isInEnrollmentMode () == false) { - listener.onFailure(new IllegalArgumentException("Enrollment mode is not enabled")); - } - rolesStore.getRoleDescriptors(new HashSet<>(Collections.singleton("enroll")), - ActionListener.wrap(roleDescriptors -> { - for (RoleDescriptor rd : roleDescriptors) { - try { - DLSRoleQueryValidator.validateQueryField(rd.getIndicesPrivileges(), xContentRegistry); - } catch (ElasticsearchException | IllegalArgumentException e) { - listener.onFailure(e); - } - } - apiKeyService.createApiKeyForEnrollment(authentication, request, roleDescriptors, listener); - }, - listener::onFailure)); - } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/enrollment/RestCreateEnrollmentTokenAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/enrollment/RestCreateEnrollmentTokenAction.java index 579d5748c366b..b2a3f863becdf 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/enrollment/RestCreateEnrollmentTokenAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/enrollment/RestCreateEnrollmentTokenAction.java @@ -11,15 +11,16 @@ import org.elasticsearch.client.node.NodeClient; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.enrollment.EnrollmentSettings; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.rest.BytesRestResponse; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.RestResponse; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.rest.action.RestBuilderListener; -import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenAction; -import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenRequest; -import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenResponse; +import org.elasticsearch.xpack.core.enrollment.CreateEnrollmentTokenAction; +import org.elasticsearch.xpack.core.enrollment.CreateEnrollmentTokenRequest; +import org.elasticsearch.xpack.core.enrollment.CreateEnrollmentTokenResponse; import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler; import java.io.IOException; @@ -32,6 +33,7 @@ * Rest endpoint to create an enrollment token */ public class RestCreateEnrollmentTokenAction extends SecurityBaseRestHandler { + private final Settings settings; /** * @param settings the node's settings @@ -40,6 +42,7 @@ public class RestCreateEnrollmentTokenAction extends SecurityBaseRestHandler { */ public RestCreateEnrollmentTokenAction(Settings settings, XPackLicenseState licenseState) { super(settings, licenseState); + this.settings = settings; } @Override @@ -56,6 +59,9 @@ public String getName() { @Override protected RestChannelConsumer innerPrepareRequest(final RestRequest request, final NodeClient client) throws IOException { + if (EnrollmentSettings.ENROLLMENT_ENABLED.get(settings) != true) { + throw new IllegalStateException("Enrollment mode is not enabled."); + } final CreateEnrollmentTokenRequest enrollmentTokenRequest = new CreateEnrollmentTokenRequest(); final ActionType action = CreateEnrollmentTokenAction.INSTANCE; return channel -> client.execute(action, enrollmentTokenRequest, From 97b2d32f1ccfa6c92524da364231654a67f3c67f Mon Sep 17 00:00:00 2001 From: Lyudmila Fokina <35386883+BigPandaToo@users.noreply.github.com> Date: Mon, 26 Apr 2021 21:41:34 +0200 Subject: [PATCH 05/14] Addressing PR feedback --- .../xpack/core/enrollment/CreateEnrollmentTokenAction.java | 1 - .../xpack/core/enrollment/CreateEnrollmentTokenRequest.java | 1 - 2 files changed, 2 deletions(-) diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrollment/CreateEnrollmentTokenAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrollment/CreateEnrollmentTokenAction.java index 91d88af73f01d..5c3f6f581ba33 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrollment/CreateEnrollmentTokenAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrollment/CreateEnrollmentTokenAction.java @@ -8,7 +8,6 @@ package org.elasticsearch.xpack.core.enrollment; import org.elasticsearch.action.ActionType; -import org.elasticsearch.common.settings.Setting; /** * ActionType for creating an enrollment new token diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrollment/CreateEnrollmentTokenRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrollment/CreateEnrollmentTokenRequest.java index 5663d192aad2a..10eaf7c0fbbb5 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrollment/CreateEnrollmentTokenRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrollment/CreateEnrollmentTokenRequest.java @@ -9,7 +9,6 @@ import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionRequestValidationException; -import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; From 23d290791c72ebb5b2cb1fd848b0d353dd2db237 Mon Sep 17 00:00:00 2001 From: Lyudmila Fokina <35386883+BigPandaToo@users.noreply.github.com> Date: Tue, 4 May 2021 21:23:15 +0200 Subject: [PATCH 06/14] Addressing PR comments --- .../cluster/create-enrollment-token.asciidoc | 4 +- .../api/cluster.create_enrollment_token.json | 2 +- .../enrollment/EnrollmentSettings.java | 2 +- .../CreateEnrollmentTokenAction.java | 4 +- .../CreateEnrollmentTokenRequest.java | 2 +- .../CreateEnrollmentTokenResponse.java | 2 +- .../privilege/ClusterPrivilegeResolver.java | 4 +- .../xpack/core/ssl/KeyConfig.java | 2 +- .../xpack/core/ssl/SSLConfiguration.java | 4 +- .../xpack/core/ssl/StoreKeyConfig.java | 16 ++++- .../CreateEnrollmentTokenResponseTests.java | 2 +- .../xpack/security/Security.java | 2 +- .../TransportCreateEnrollmentTokenAction.java | 70 ++++++++++++------- .../RestCreateEnrollmentTokenAction.java | 10 +-- 14 files changed, 78 insertions(+), 48 deletions(-) rename x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/{ => security/action}/enrollment/CreateEnrollmentTokenAction.java (81%) rename x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/{ => security/action}/enrollment/CreateEnrollmentTokenRequest.java (94%) rename x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/{ => security/action}/enrollment/CreateEnrollmentTokenResponse.java (97%) diff --git a/docs/reference/cluster/create-enrollment-token.asciidoc b/docs/reference/cluster/create-enrollment-token.asciidoc index 0d549328adbb5..05f0ff09e5554 100644 --- a/docs/reference/cluster/create-enrollment-token.asciidoc +++ b/docs/reference/cluster/create-enrollment-token.asciidoc @@ -10,7 +10,7 @@ communicate with a secured elasticsearch cluster. [[cluster-create-enrollment-token-api-request]] ==== {api-request-title} -`POST /_cluster/enrollment_token` +`POST /_security/enrollment_token` [[cluster-create-enrollment-token-api-prereqs]] ==== {api-prereq-title} @@ -28,7 +28,7 @@ communicate with a secured elasticsearch cluster. [source,console] -------------------------------------------------- -POST /_cluster/enrollment_token +POST /_security/enrollment_token -------------------------------------------------- The API returns a response such as diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/cluster.create_enrollment_token.json b/rest-api-spec/src/main/resources/rest-api-spec/api/cluster.create_enrollment_token.json index f086e0a8609d4..377647b71526e 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/cluster.create_enrollment_token.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/cluster.create_enrollment_token.json @@ -13,7 +13,7 @@ "url":{ "paths":[ { - "path":"/_cluster/enrollment_token", + "path":"/_security/enrollment_token", "methods":[ "POST" ] diff --git a/server/src/main/java/org/elasticsearch/enrollment/EnrollmentSettings.java b/server/src/main/java/org/elasticsearch/enrollment/EnrollmentSettings.java index caf9a5cebb44a..353382137ef5c 100644 --- a/server/src/main/java/org/elasticsearch/enrollment/EnrollmentSettings.java +++ b/server/src/main/java/org/elasticsearch/enrollment/EnrollmentSettings.java @@ -15,6 +15,6 @@ private EnrollmentSettings() { } /** Setting for enabling or disabling enrollment mode. Defaults to false. */ - public static final Setting ENROLLMENT_ENABLED = Setting.boolSetting("cluster.enrollment.enabled", false, + public static final Setting ENROLLMENT_ENABLED = Setting.boolSetting("xpack.security.enrollment.enabled", false, Setting.Property.NodeScope); } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrollment/CreateEnrollmentTokenAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenAction.java similarity index 81% rename from x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrollment/CreateEnrollmentTokenAction.java rename to x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenAction.java index 5c3f6f581ba33..2ef6fcb0964dd 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrollment/CreateEnrollmentTokenAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenAction.java @@ -5,7 +5,7 @@ * 2.0. */ -package org.elasticsearch.xpack.core.enrollment; +package org.elasticsearch.xpack.core.security.action.enrollment; import org.elasticsearch.action.ActionType; @@ -13,7 +13,7 @@ * ActionType for creating an enrollment new token */ public class CreateEnrollmentTokenAction extends ActionType { - public static final String NAME = "cluster:admin/xpack/enrollment/create"; + public static final String NAME = "cluster:admin/xpack/security/enrollment/create"; public static final CreateEnrollmentTokenAction INSTANCE = new CreateEnrollmentTokenAction(); diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrollment/CreateEnrollmentTokenRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenRequest.java similarity index 94% rename from x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrollment/CreateEnrollmentTokenRequest.java rename to x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenRequest.java index 10eaf7c0fbbb5..38d8e7d2f39d9 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrollment/CreateEnrollmentTokenRequest.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenRequest.java @@ -5,7 +5,7 @@ * 2.0. */ -package org.elasticsearch.xpack.core.enrollment; +package org.elasticsearch.xpack.core.security.action.enrollment; import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.ActionRequestValidationException; diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrollment/CreateEnrollmentTokenResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenResponse.java similarity index 97% rename from x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrollment/CreateEnrollmentTokenResponse.java rename to x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenResponse.java index 5cf611eee130c..32b4e79170547 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/enrollment/CreateEnrollmentTokenResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/CreateEnrollmentTokenResponse.java @@ -5,7 +5,7 @@ * 2.0. */ -package org.elasticsearch.xpack.core.enrollment; +package org.elasticsearch.xpack.core.security.action.enrollment; import org.elasticsearch.action.ActionResponse; import org.elasticsearch.common.ParseField; 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 cf6839917be7c..a7b37c0a28717 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 @@ -156,9 +156,9 @@ public class ClusterPrivilegeResolver { Set.of("cluster:admin/tasks/cancel")); public static final NamedClusterPrivilege MANAGE_ENROLLMENT = new ActionClusterPrivilege("manage_enrollment", - Set.of("cluster:admin/xpack/enrollment/*")); + Set.of("cluster:admin/xpack/security/enrollment/*")); public static final NamedClusterPrivilege ENROLL = new ActionClusterPrivilege("enroll", - Set.of("cluster:admin/xpack/enrollment/enroll")); + Set.of("cluster:admin/xpack/security/enrollment/enroll*")); private static final Map VALUES = sortByAccessLevel(List.of( NONE, diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/KeyConfig.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/KeyConfig.java index 892cf1378756e..67212ab2105d0 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/KeyConfig.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/KeyConfig.java @@ -29,7 +29,7 @@ import java.util.Collections; import java.util.List; -abstract class KeyConfig extends TrustConfig { +public abstract class KeyConfig extends TrustConfig { static final KeyConfig NONE = new KeyConfig() { @Override diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLConfiguration.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLConfiguration.java index e13ce2f1b2ad1..abdd415a32a0c 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLConfiguration.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/SSLConfiguration.java @@ -48,7 +48,7 @@ public final class SSLConfiguration { * * @param settings the SSL specific settings; only the settings under a *.ssl. prefix */ - SSLConfiguration(Settings settings) { + public SSLConfiguration(Settings settings) { this.keyConfig = createKeyConfig(settings); this.trustConfig = createTrustConfig(settings, keyConfig); this.ciphers = getListOrDefault(SETTINGS_PARSER.ciphers, settings, XPackSettings.DEFAULT_CIPHERS); @@ -61,7 +61,7 @@ public final class SSLConfiguration { /** * The configuration for the key, if any, that will be used as part of this ssl configuration */ - KeyConfig keyConfig() { + public KeyConfig keyConfig() { return keyConfig; } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/StoreKeyConfig.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/StoreKeyConfig.java index 7812eb04cdcc1..7646da62e097e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/StoreKeyConfig.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/ssl/StoreKeyConfig.java @@ -38,7 +38,7 @@ /** * A key configuration that is backed by a {@link KeyStore} */ -class StoreKeyConfig extends KeyConfig { +public class StoreKeyConfig extends KeyConfig { private static final String KEYSTORE_FILE = "keystore"; @@ -126,6 +126,20 @@ Collection certificates(Environment environment) throws General return certificates; } + public Collection x509Certificates(Environment environment) throws GeneralSecurityException, IOException { + final KeyStore trustStore = getStore(CertParsingUtils.resolvePath(keyStorePath, environment), keyStoreType, keyStorePassword); + final List certificates = new ArrayList<>(); + final Enumeration aliases = trustStore.aliases(); + while (aliases.hasMoreElements()) { + String alias = aliases.nextElement(); + final Certificate certificate = trustStore.getCertificate(alias); + if (certificate instanceof X509Certificate) { + certificates.add((X509Certificate) certificate); + } + } + return certificates; + } + @Override List filesToMonitor(@Nullable Environment environment) { if (keyStorePath == null) { diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateEnrollmentTokenResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateEnrollmentTokenResponseTests.java index ea0b97164f6c2..090a0226cedc8 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateEnrollmentTokenResponseTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/CreateEnrollmentTokenResponseTests.java @@ -13,7 +13,7 @@ import org.elasticsearch.common.xcontent.ConstructingObjectParser; import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.test.AbstractXContentTestCase; -import org.elasticsearch.xpack.core.enrollment.CreateEnrollmentTokenResponse; +import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenResponse; import java.io.IOException; import java.nio.charset.StandardCharsets; 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 6134de5492885..519e96468ce18 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 @@ -90,7 +90,7 @@ import org.elasticsearch.xpack.core.security.action.GetApiKeyAction; import org.elasticsearch.xpack.core.security.action.GrantApiKeyAction; import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyAction; -import org.elasticsearch.xpack.core.enrollment.CreateEnrollmentTokenAction; +import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenAction; import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectAuthenticateAction; import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectLogoutAction; import org.elasticsearch.xpack.core.security.action.oidc.OpenIdConnectPrepareAuthenticationAction; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java index 2271b6ded5d69..e68bdc0c3bb39 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java @@ -27,22 +27,23 @@ import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.core.security.SecurityContext; import org.elasticsearch.xpack.core.security.action.CreateApiKeyRequest; -import org.elasticsearch.xpack.core.enrollment.CreateEnrollmentTokenAction; -import org.elasticsearch.xpack.core.enrollment.CreateEnrollmentTokenRequest; -import org.elasticsearch.xpack.core.enrollment.CreateEnrollmentTokenResponse; +import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenAction; +import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenRequest; +import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenResponse; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; -import org.elasticsearch.xpack.core.ssl.CertParsingUtils; +import org.elasticsearch.xpack.core.ssl.KeyConfig; +import org.elasticsearch.xpack.core.ssl.SSLService; +import org.elasticsearch.xpack.core.ssl.StoreKeyConfig; import org.elasticsearch.xpack.security.authc.ApiKeyService; import org.elasticsearch.xpack.security.authc.support.ApiKeyGenerator; import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Base64; import java.util.List; +import java.util.stream.Collectors; /** * Transport action responsible for creating an enrollment token based on a request. @@ -57,51 +58,66 @@ public class TransportCreateEnrollmentTokenAction private final SecurityContext securityContext; private final Environment environment; private final NodeService nodeService; + private final SSLService sslService; @Inject public TransportCreateEnrollmentTokenAction(TransportService transportService, ActionFilters actionFilters, ApiKeyService apiKeyService, SecurityContext context, CompositeRolesStore rolesStore, - NamedXContentRegistry xContentRegistry, Environment environment, NodeService nodeService) { + NamedXContentRegistry xContentRegistry, Environment environment, NodeService nodeService, + SSLService sslService) { super(CreateEnrollmentTokenAction.NAME, transportService, actionFilters, CreateEnrollmentTokenRequest::new); this.generator = new ApiKeyGenerator(apiKeyService, rolesStore, xContentRegistry); this.securityContext = context; this.environment = environment; this.nodeService = nodeService; + this.sslService = sslService; } @Override protected void doExecute(Task task, CreateEnrollmentTokenRequest request, ActionListener listener) { - createEnrolmentToken(listener); + try { + final KeyConfig keyConfig = sslService.getHttpTransportSSLConfiguration().keyConfig(); + if (keyConfig instanceof StoreKeyConfig == false) { + listener.onFailure(new IllegalStateException( + "Unable to create an enrollment token. Elasticsearch node HTTP layer SSL configuration is not configured with a " + + "keystore")); + return; + } + final List caCertificates = ((StoreKeyConfig) keyConfig).x509Certificates(environment).stream() + .filter(x509Certificate -> x509Certificate.getBasicConstraints() != -1).collect(Collectors.toList()); + if (caCertificates.size() != 1) { + listener.onFailure(new IllegalStateException( + "Unable to create an enrollment token. Elasticsearch node HTTP layer SSL configuration Keystore doesn't contain a " + + "single PrivateKey entry where the associated certificate is a CA certificate")); + + } else { + final TimeValue expiration = TimeValue.timeValueSeconds(ENROLL_API_KEY_EXPIRATION_SEC); + final List roleDescriptors = new ArrayList<>(1); + final String[] clusterPrivileges = { "enroll" }; + final RoleDescriptor roleDescriptor = new RoleDescriptor("create_enrollment_token", clusterPrivileges, null, null); + roleDescriptors.add(roleDescriptor); + CreateApiKeyRequest apiRequest = new CreateApiKeyRequest("enrollment_token_API_key_" + UUIDs.base64UUID(), + roleDescriptors, expiration); + final String fingerprint = SslUtil.calculateFingerprint(caCertificates.get(0)); + createEnrolmentToken(fingerprint, apiRequest, listener); + } + } catch (Exception e) { + listener.onFailure(e); + } } - private void createEnrolmentToken(ActionListener listener) { + private void createEnrolmentToken(String fingerprint, CreateApiKeyRequest apiRequest, + ActionListener listener) { try { - final TimeValue expiration = TimeValue.timeValueSeconds(ENROLL_API_KEY_EXPIRATION_SEC); - final List roleDescriptors = new ArrayList<>(1); - final String[] clusterPrivileges = { "enroll" }; - final RoleDescriptor roleDescriptor = new RoleDescriptor("create_enrollment_token", clusterPrivileges, null, null); - roleDescriptors.add(roleDescriptor); - CreateApiKeyRequest apiRequest = new CreateApiKeyRequest("enrollment_token_API_key_" + UUIDs.base64UUID(), - roleDescriptors, expiration); generator.generateApiKey(securityContext.getAuthentication(), apiRequest, ActionListener.wrap( - CreateApiKeyResponse -> {; - final String httpCaCert = "httpCa.pem"; - final Path httpCaCertPath = environment.configFile().resolve(httpCaCert); - if (Files.exists(httpCaCertPath) == false) { - listener.onFailure(new IllegalStateException("HTTP layer CA certificate " + httpCaCert + " does not exist")); - return; - } + CreateApiKeyResponse -> { final NodeInfo nodeInfo = nodeService.info(false, false, false, false, false, false, true, false, false, false, false); final HttpInfo httpInfo = nodeInfo.getInfo(HttpInfo.class); final String address = httpInfo.getAddress().publishAddress().toString(); - final X509Certificate[] certificates = CertParsingUtils.readX509Certificates(List.of(httpCaCertPath)); - final X509Certificate cert = certificates[0]; - final String fingerprint = SslUtil.calculateFingerprint(cert); - final XContentBuilder builder = JsonXContent.contentBuilder(); builder.startObject(); builder.field("adr", address); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/enrollment/RestCreateEnrollmentTokenAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/enrollment/RestCreateEnrollmentTokenAction.java index b2a3f863becdf..9ec26412540b8 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/enrollment/RestCreateEnrollmentTokenAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/enrollment/RestCreateEnrollmentTokenAction.java @@ -18,9 +18,9 @@ import org.elasticsearch.rest.RestResponse; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.rest.action.RestBuilderListener; -import org.elasticsearch.xpack.core.enrollment.CreateEnrollmentTokenAction; -import org.elasticsearch.xpack.core.enrollment.CreateEnrollmentTokenRequest; -import org.elasticsearch.xpack.core.enrollment.CreateEnrollmentTokenResponse; +import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenAction; +import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenRequest; +import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenResponse; import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler; import java.io.IOException; @@ -48,8 +48,8 @@ public RestCreateEnrollmentTokenAction(Settings settings, XPackLicenseState lice @Override public List routes() { return List.of( - new Route(POST, "/_cluster/enrollment_token"), - new Route(PUT, "/_cluster/enrollment_token")); + new Route(POST, "/_security/enrollment_token"), + new Route(PUT, "/_security/enrollment_token")); } @Override From 4151e40eb9516e69f8f21622bebca0f95572c7f1 Mon Sep 17 00:00:00 2001 From: Lyudmila Fokina <35386883+BigPandaToo@users.noreply.github.com> Date: Wed, 5 May 2021 21:34:36 +0200 Subject: [PATCH 07/14] Addressing PR comments --- .../elasticsearch/client/SecurityClient.java | 27 ++++++++++ .../CreateEnrollmentTokenRequest.java | 26 +++++++++ .../CreateEnrollmentTokenResponse.java | 54 +++++++++++++++++++ .../org/elasticsearch/client/SecurityIT.java | 18 +++++-- .../CreateEnrollmentTokenResponseTests.java | 39 ++++++++++++++ .../security/create-enrollment-token.asciidoc | 40 ++++++++++++++ ... => security.create_enrollment_token.json} | 6 +-- x-pack/docs/en/rest-api/security.asciidoc | 10 ++++ .../create-enrollment-token.asciidoc | 8 +-- .../TransportCreateEnrollmentTokenAction.java | 37 +++++++------ .../xpack/security/authc/ApiKeyService.java | 1 - .../RestCreateEnrollmentTokenAction.java | 2 +- 12 files changed, 239 insertions(+), 29 deletions(-) create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateEnrollmentTokenRequest.java create mode 100644 client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateEnrollmentTokenResponse.java create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/security/CreateEnrollmentTokenResponseTests.java create mode 100644 docs/java-rest/high-level/security/create-enrollment-token.asciidoc rename rest-api-spec/src/main/resources/rest-api-spec/api/{cluster.create_enrollment_token.json => security.create_enrollment_token.json} (67%) rename {docs/reference/cluster => x-pack/docs/en/rest-api/security}/create-enrollment-token.asciidoc (87%) diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java index 6d0c49c92dcf6..80a6083c36bf6 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java @@ -22,6 +22,8 @@ import org.elasticsearch.client.security.ClearSecurityCacheResponse; import org.elasticsearch.client.security.CreateApiKeyRequest; import org.elasticsearch.client.security.CreateApiKeyResponse; +import org.elasticsearch.client.security.CreateEnrollmentTokenRequest; +import org.elasticsearch.client.security.CreateEnrollmentTokenResponse; import org.elasticsearch.client.security.CreateTokenRequest; import org.elasticsearch.client.security.CreateTokenResponse; import org.elasticsearch.client.security.DelegatePkiAuthenticationRequest; @@ -1130,4 +1132,29 @@ public Cancellable delegatePkiAuthenticationAsync(DelegatePkiAuthenticationReque return restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::delegatePkiAuthentication, options, DelegatePkiAuthenticationResponse::fromXContent, listener, emptySet()); } + + /** + * Create an Enrollment Token used to enroll a client to a secured cluster using the Enroll Client API + * + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @return the response + * @throws IOException in case there is a problem sending the request or parsing back the response + */ + public CreateEnrollmentTokenResponse createEnrollmentToken(RequestOptions options) throws IOException { + return restHighLevelClient.performRequestAndParseEntity(CreateEnrollmentTokenRequest.INSTANCE, + CreateEnrollmentTokenRequest::getRequest, options, + CreateEnrollmentTokenResponse::fromXContent, emptySet()); + } + + /** + * Asynchronously create an Enrollment Token used to enroll a client to a secured cluster using the Enroll Client API + * + * @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized + * @param listener the listener to be notified upon request completion + */ + public Cancellable createEnrollmentTokenAsync(RequestOptions options, ActionListener listener) { + return restHighLevelClient.performRequestAsyncAndParseEntity(CreateEnrollmentTokenRequest.INSTANCE, + CreateEnrollmentTokenRequest::getRequest, options, + CreateEnrollmentTokenResponse::fromXContent, listener, emptySet()); + } } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateEnrollmentTokenRequest.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateEnrollmentTokenRequest.java new file mode 100644 index 0000000000000..0537c3ef52693 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateEnrollmentTokenRequest.java @@ -0,0 +1,26 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.client.security; + +import org.apache.http.client.methods.HttpPut; +import org.elasticsearch.client.Request; +import org.elasticsearch.client.Validatable; + +public class CreateEnrollmentTokenRequest implements Validatable { + + public CreateEnrollmentTokenRequest() { + } + + public static final CreateEnrollmentTokenRequest INSTANCE = new CreateEnrollmentTokenRequest(); + + + public Request getRequest() { + return new Request(HttpPut.METHOD_NAME, "/_security/enrollment_token"); + } +} diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateEnrollmentTokenResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateEnrollmentTokenResponse.java new file mode 100644 index 0000000000000..2af865a3699b0 --- /dev/null +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/CreateEnrollmentTokenResponse.java @@ -0,0 +1,54 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.client.security; + +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.XContentParser; + +import java.io.IOException; +import java.util.Objects; + +public class CreateEnrollmentTokenResponse { + private String enrollmentToken; + + public CreateEnrollmentTokenResponse(String enrollmentToken) { + this.enrollmentToken = enrollmentToken; + } + + public String getEnrollmentToken() { + return enrollmentToken; + } + + private static final ParseField ENROLLMENT_TOKEN = new ParseField("enrollment_token"); + + @SuppressWarnings("unchecked") + private static final ConstructingObjectParser PARSER = + new ConstructingObjectParser<>(CreateEnrollmentTokenResponse.class.getName(), true, + a -> new CreateEnrollmentTokenResponse((String) a[0])); + + static { + PARSER.declareString(ConstructingObjectParser.constructorArg(), ENROLLMENT_TOKEN); + } + + public static CreateEnrollmentTokenResponse fromXContent(XContentParser parser) throws IOException { + return PARSER.apply(parser, null); + } + + @Override public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CreateEnrollmentTokenResponse that = (CreateEnrollmentTokenResponse) o; + return enrollmentToken.equals(that.enrollmentToken); + } + + @Override public int hashCode() { + return Objects.hash(enrollmentToken); + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityIT.java index 6aae086c841b6..6d05e0a43344c 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityIT.java @@ -11,6 +11,7 @@ import org.apache.http.client.methods.HttpDelete; import org.elasticsearch.ElasticsearchStatusException; import org.elasticsearch.client.security.AuthenticateResponse; +import org.elasticsearch.client.security.CreateEnrollmentTokenResponse; import org.elasticsearch.client.security.DeleteRoleRequest; import org.elasticsearch.client.security.DeleteRoleResponse; import org.elasticsearch.client.security.DeleteUserRequest; @@ -42,9 +43,11 @@ import java.util.Locale; import java.util.Map; +import static org.elasticsearch.client.RequestOptions.DEFAULT; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.notNullValue; public class SecurityIT extends ESRestHighLevelClientTestCase { @@ -138,7 +141,7 @@ public void testPutRole() throws Exception { assertThat(createRoleResponse.isCreated(), is(true)); final GetRolesRequest getRoleRequest = new GetRolesRequest(role.getName()); - final GetRolesResponse getRoleResponse = securityClient.getRoles(getRoleRequest, RequestOptions.DEFAULT); + final GetRolesResponse getRoleResponse = securityClient.getRoles(getRoleRequest, DEFAULT); // assert role is equal assertThat(getRoleResponse.getRoles(), contains(role)); @@ -147,11 +150,20 @@ public void testPutRole() throws Exception { assertThat(updateRoleResponse.isCreated(), is(false)); final DeleteRoleRequest deleteRoleRequest = new DeleteRoleRequest(role.getName()); - final DeleteRoleResponse deleteRoleResponse = securityClient.deleteRole(deleteRoleRequest, RequestOptions.DEFAULT); + final DeleteRoleResponse deleteRoleResponse = securityClient.deleteRole(deleteRoleRequest, DEFAULT); // assert role deleted assertThat(deleteRoleResponse.isFound(), is(true)); } + @AwaitsFix(bugUrl = "Requires SSL") + public void testCreateEnrollmentTokenClient() throws Exception { + final SecurityClient securityClient = highLevelClient().security(); + CreateEnrollmentTokenResponse clientResponse = + execute(securityClient::createEnrollmentToken, securityClient::createEnrollmentTokenAsync, DEFAULT); + + assertThat(clientResponse, notNullValue()); + } + private void deleteUser(User user) throws IOException { final Request deleteUserRequest = new Request(HttpDelete.METHOD_NAME, "/_security/user/" + user.getUsername()); highLevelClient().getLowLevelClient().performRequest(deleteUserRequest); @@ -222,7 +234,7 @@ private static String basicAuthHeader(String username, char[] password) { } private static RequestOptions authorizationRequestOptions(String authorizationHeader) { - final RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder(); + final RequestOptions.Builder builder = DEFAULT.toBuilder(); builder.addHeader("Authorization", authorizationHeader); return builder.build(); } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/CreateEnrollmentTokenResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/CreateEnrollmentTokenResponseTests.java new file mode 100644 index 0000000000000..f1a4b162b66fc --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/CreateEnrollmentTokenResponseTests.java @@ -0,0 +1,39 @@ +/* + * 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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +package org.elasticsearch.client.security; + +import org.elasticsearch.common.UUIDs; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentType; +import org.elasticsearch.test.ESTestCase; + +import java.io.IOException; + +import static org.hamcrest.Matchers.equalTo; + +public class CreateEnrollmentTokenResponseTests extends ESTestCase { + + public void testFromXContent() throws IOException { + final SecureString enrollment_token = UUIDs.randomBase64UUIDSecureString(); + + final XContentType xContentType = randomFrom(XContentType.values()); + final XContentBuilder builder = XContentFactory.contentBuilder(xContentType); + builder.startObject() + .field("enrollment_token", enrollment_token.toString()); + builder.endObject(); + BytesReference xContent = BytesReference.bytes(builder); + + final CreateEnrollmentTokenResponse response = CreateEnrollmentTokenResponse.fromXContent(createParser(xContentType.xContent(), + xContent)); + assertThat(response.getEnrollmentToken(), equalTo(enrollment_token.toString())); + } +} diff --git a/docs/java-rest/high-level/security/create-enrollment-token.asciidoc b/docs/java-rest/high-level/security/create-enrollment-token.asciidoc new file mode 100644 index 0000000000000..4d4ed10ca2835 --- /dev/null +++ b/docs/java-rest/high-level/security/create-enrollment-token.asciidoc @@ -0,0 +1,40 @@ +-- +:api: create-enrollment-token +:request: CreateEnrollmentTokenRequest +:response: CreateEnrollmentTokenResponse +-- +[role="xpack"] +[id="{upid}-{api}"] +=== Create Enrollment Token API + +Enrollment Token can be created using this API. + +[id="{upid}-{api}-request"] +==== Create Enrollment Token Request + +A +{request}+ contains no parameters. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-request] +-------------------------------------------------- + +include::../execution.asciidoc[] + +[id="{upid}-{api}-response"] +==== Create Enrollment Token Response + +The returned +{response}+ contains a string that contains the enrollment token with which user can enroll a new node +in an existing secured elasticsearch cluster, or a client can configure itself to +communicate with a secured elasticsearch cluster. + +["source","java",subs="attributes,callouts,macros"] +-------------------------------------------------- +include-tagged::{doc-tests-file}[{api}-response] +-------------------------------------------------- +<1> The enrollment token contains the following information: +- IP Address and port number for the interface where the Elasticsearch node is listening for HTTP connections; +- The fingerprint of the CA certificate that is used to sign the certificate that the Elasticsearch node presents for TLS on the HTTP layer; +- An API key which allows a holder of the token to authenticate themself to the elasticsearch node; + +as Base64 encoded string. diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/cluster.create_enrollment_token.json b/rest-api-spec/src/main/resources/rest-api-spec/api/security.create_enrollment_token.json similarity index 67% rename from rest-api-spec/src/main/resources/rest-api-spec/api/cluster.create_enrollment_token.json rename to rest-api-spec/src/main/resources/rest-api-spec/api/security.create_enrollment_token.json index 377647b71526e..9dc53261216cc 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/cluster.create_enrollment_token.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/security.create_enrollment_token.json @@ -1,8 +1,8 @@ { - "cluster.create_enrollment_token":{ + "security.create_enrollment_token":{ "documentation":{ - "url":"https://www.elastic.co/guide/en/elasticsearch/reference/master/cluster-create-enrollment-token.html", - "description":"Create an enrollment token to allow a new node enroll in an existing secured elasticsearch cluster, or a client to configure itself to\ncommunicate with a secured elasticsearch cluster." + "url":"https://www.elastic.co/guide/en/elasticsearch/reference/master/security-create-enrollment-token.html", + "description":"Create an enrollment token to allow a new node to enroll in an existing secured elasticsearch cluster, or a client to configure itself to\ncommunicate with a secured elasticsearch cluster." }, "stability":"stable", "visibility":"public", diff --git a/x-pack/docs/en/rest-api/security.asciidoc b/x-pack/docs/en/rest-api/security.asciidoc index e653f52bf3212..08d95398e0e90 100644 --- a/x-pack/docs/en/rest-api/security.asciidoc +++ b/x-pack/docs/en/rest-api/security.asciidoc @@ -105,6 +105,15 @@ realm when using a custom web application other than Kibana * <> * <> +[discrete] +[[security-create-enrollment-token-apis]] +=== Enrollment + +You can use the following API to generate a token with which a user can enroll a new node +in an existing secured elasticsearch cluster, or a client can configure itself to +communicate with a secured elasticsearch cluster. + +* <> include::security/authenticate.asciidoc[] include::security/change-password.asciidoc[] @@ -124,6 +133,7 @@ include::security/delete-roles.asciidoc[] include::security/delete-users.asciidoc[] include::security/disable-users.asciidoc[] include::security/enable-users.asciidoc[] +include::security/create-enrollment-token.asciidoc[] include::security/get-api-keys.asciidoc[] include::security/get-app-privileges.asciidoc[] include::security/get-builtin-privileges.asciidoc[] diff --git a/docs/reference/cluster/create-enrollment-token.asciidoc b/x-pack/docs/en/rest-api/security/create-enrollment-token.asciidoc similarity index 87% rename from docs/reference/cluster/create-enrollment-token.asciidoc rename to x-pack/docs/en/rest-api/security/create-enrollment-token.asciidoc index 05f0ff09e5554..6f8c12815cbf9 100644 --- a/docs/reference/cluster/create-enrollment-token.asciidoc +++ b/x-pack/docs/en/rest-api/security/create-enrollment-token.asciidoc @@ -4,7 +4,7 @@ Create Enrollment Token ++++ -Create an enrollment token to allow a new node enroll in an existing secured elasticsearch cluster, or a client to configure itself to +Create an enrollment token to allow a new node to enroll in an existing secured elasticsearch cluster, or a client to configure itself to communicate with a secured elasticsearch cluster. [[cluster-create-enrollment-token-api-request]] @@ -51,9 +51,9 @@ A decoded enrollment token is shown below (spaces and new lines added for clarit [source,console-result] -------------------------------------------------- { -"adr":"192.168.1.43:9201", -"fgr":"48:CC:6C:F8:76:43:3C:97:85:....0A:C8:8D:AE:5C:4D", -"key":"VuaCfGcBCdbkQm-e5aOx:ui2lp2axTNmsyakw9tvNnw" +"adr": "192.168.1.43:9201", +"fgr": "48:CC:6C:F8:76:43:3C:97:85:....0A:C8:8D:AE:5C:4D", +"key": "VuaCfGcBCdbkQm-e5aOx:ui2lp2axTNmsyakw9tvNnw" } -------------------------------------------------- diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java index e68bdc0c3bb39..c93359c36456b 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java @@ -90,15 +90,13 @@ protected void doExecute(Task task, CreateEnrollmentTokenRequest request, listener.onFailure(new IllegalStateException( "Unable to create an enrollment token. Elasticsearch node HTTP layer SSL configuration Keystore doesn't contain a " + "single PrivateKey entry where the associated certificate is a CA certificate")); - } else { final TimeValue expiration = TimeValue.timeValueSeconds(ENROLL_API_KEY_EXPIRATION_SEC); final List roleDescriptors = new ArrayList<>(1); final String[] clusterPrivileges = { "enroll" }; final RoleDescriptor roleDescriptor = new RoleDescriptor("create_enrollment_token", clusterPrivileges, null, null); roleDescriptors.add(roleDescriptor); - CreateApiKeyRequest apiRequest = new CreateApiKeyRequest("enrollment_token_API_key_" + UUIDs.base64UUID(), - roleDescriptors, expiration); + CreateApiKeyRequest apiRequest = new CreateApiKeyRequest(UUIDs.base64UUID(), roleDescriptors, expiration); final String fingerprint = SslUtil.calculateFingerprint(caCertificates.get(0)); createEnrolmentToken(fingerprint, apiRequest, listener); } @@ -113,22 +111,27 @@ private void createEnrolmentToken(String fingerprint, CreateApiKeyRequest apiReq generator.generateApiKey(securityContext.getAuthentication(), apiRequest, ActionListener.wrap( CreateApiKeyResponse -> { - final NodeInfo nodeInfo = nodeService.info(false, false, false, false, false, false, - true, false, false, false, false); - final HttpInfo httpInfo = nodeInfo.getInfo(HttpInfo.class); - final String address = httpInfo.getAddress().publishAddress().toString(); + try { + final NodeInfo nodeInfo = nodeService.info(false, false, false, false, false, false, + true, false, false, false, false); + final HttpInfo httpInfo = nodeInfo.getInfo(HttpInfo.class); + final String address = httpInfo.getAddress().publishAddress().toString(); - final XContentBuilder builder = JsonXContent.contentBuilder(); - builder.startObject(); - builder.field("adr", address); - builder.field("fgr", fingerprint); - builder.field("key", CreateApiKeyResponse.getKey().toString()); - builder.endObject(); - final String jsonString = Strings.toString(builder); - final String token = Base64.getEncoder().encodeToString(jsonString.getBytes(StandardCharsets.UTF_8)); + final XContentBuilder builder = JsonXContent.contentBuilder(); + builder.startObject(); + builder.field("adr", address); + builder.field("fgr", fingerprint); + builder.field("key", CreateApiKeyResponse.getKey().toString()); + builder.endObject(); + final String jsonString = Strings.toString(builder); + final String token = Base64.getEncoder().encodeToString(jsonString.getBytes(StandardCharsets.UTF_8)); - final CreateEnrollmentTokenResponse response = new CreateEnrollmentTokenResponse(token); - listener.onResponse(response); + final CreateEnrollmentTokenResponse response = new CreateEnrollmentTokenResponse(token); + listener.onResponse(response); + } catch (Exception e) { + logger.error(() -> new ParameterizedMessage("Error generating enrollment token"), e); + listener.onFailure(e); + } }, listener::onFailure ) 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 36ef63117e1e9..dddaf233a1872 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 @@ -29,7 +29,6 @@ import org.elasticsearch.action.index.IndexResponse; import org.elasticsearch.action.search.SearchRequest; import org.elasticsearch.action.support.ContextPreservingActionListener; -import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.action.support.WriteRequest.RefreshPolicy; import org.elasticsearch.action.update.UpdateRequest; import org.elasticsearch.action.update.UpdateResponse; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/enrollment/RestCreateEnrollmentTokenAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/enrollment/RestCreateEnrollmentTokenAction.java index 9ec26412540b8..49c848a5f92ee 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/enrollment/RestCreateEnrollmentTokenAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/enrollment/RestCreateEnrollmentTokenAction.java @@ -69,7 +69,7 @@ protected RestChannelConsumer innerPrepareRequest(final RestRequest request, fin @Override public RestResponse buildResponse(CreateEnrollmentTokenResponse response, XContentBuilder builder) throws Exception { builder.startObject(); - builder.field("enrollment_token", response.getEnrollmentToken().toString()); + builder.field("enrollment_token", response.getEnrollmentToken()); builder.endObject(); return new BytesRestResponse(RestStatus.OK, builder); } From db565459be6d850000f28122ff74b73e2ff98222 Mon Sep 17 00:00:00 2001 From: Lyudmila Fokina <35386883+BigPandaToo@users.noreply.github.com> Date: Wed, 12 May 2021 21:55:00 +0200 Subject: [PATCH 08/14] Addressing PR comments adding tests --- .../elasticsearch/client/SecurityClient.java | 1 + .../org/elasticsearch/client/SecurityIT.java | 18 +- .../security/create-enrollment-token.asciidoc | 4 +- .../api/security.create_enrollment_token.json | 3 +- .../common/settings/ClusterSettings.java | 3 +- .../enrollment/EnrollmentSettings.java | 20 -- .../java/org/elasticsearch/node/Node.java | 4 + .../security/create-enrollment-token.asciidoc | 6 +- .../authorization/privileges.asciidoc | 5 +- .../privilege/ClusterPrivilegeResolver.java | 2 +- .../xpack/core/action/httpCA.pem | 25 --- .../xpack/security/Security.java | 2 +- .../TransportCreateEnrollmentTokenAction.java | 108 +++++------ .../RestCreateEnrollmentTokenAction.java | 4 +- ...sportCreateEnrollmentTokenActionTests.java | 171 ++++++++++++++++++ 15 files changed, 262 insertions(+), 114 deletions(-) delete mode 100644 server/src/main/java/org/elasticsearch/enrollment/EnrollmentSettings.java delete mode 100644 x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/core/action/httpCA.pem create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenActionTests.java diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java index 61ca62252d595..14d9d87bad8dd 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java @@ -1155,6 +1155,7 @@ public Cancellable createEnrollmentTokenAsync(RequestOptions options, ActionList return restHighLevelClient.performRequestAsyncAndParseEntity(CreateEnrollmentTokenRequest.INSTANCE, CreateEnrollmentTokenRequest::getRequest, options, CreateEnrollmentTokenResponse::fromXContent, listener, emptySet()); + } /** diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityIT.java index bd9e60f382022..eca91f9c87d54 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityIT.java @@ -34,8 +34,11 @@ import org.elasticsearch.client.security.user.privileges.IndicesPrivilegesTests; import org.elasticsearch.client.security.user.privileges.Role; import org.elasticsearch.common.CharArrays; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.json.JsonXContent; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; @@ -43,6 +46,7 @@ import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.stream.Collectors; import static org.elasticsearch.client.RequestOptions.DEFAULT; import static org.hamcrest.Matchers.endsWith; @@ -158,13 +162,23 @@ public void testPutRole() throws Exception { assertThat(deleteRoleResponse.isFound(), is(true)); } - @AwaitsFix(bugUrl = "Requires SSL") + @AwaitsFix(bugUrl = "Determine behavior for keystores with multiple keys") public void testCreateEnrollmentTokenClient() throws Exception { final SecurityClient securityClient = highLevelClient().security(); + final Map info; CreateEnrollmentTokenResponse clientResponse = execute(securityClient::createEnrollmentToken, securityClient::createEnrollmentTokenAsync, DEFAULT); - assertThat(clientResponse, notNullValue()); + String jsonString = new String(Base64.getDecoder().decode(clientResponse.getEnrollmentToken()), StandardCharsets.UTF_8); + try (XContentParser parser = createParser(JsonXContent.jsonXContent, jsonString)) { + info = parser.map(); + assertNotEquals(info, null); + info.entrySet().stream() + .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue().toString())); + } + assertThat(info.get("adr"), notNullValue()); + assertEquals("598a35cd831ee6bb90e79aa80d6b073cda88b41d", info.get("fgr")); + assertThat(info.get("key"), notNullValue()); } @AwaitsFix(bugUrl = "Determine behavior for keystore with multiple keys") diff --git a/docs/java-rest/high-level/security/create-enrollment-token.asciidoc b/docs/java-rest/high-level/security/create-enrollment-token.asciidoc index 4d4ed10ca2835..968154878504a 100644 --- a/docs/java-rest/high-level/security/create-enrollment-token.asciidoc +++ b/docs/java-rest/high-level/security/create-enrollment-token.asciidoc @@ -7,7 +7,7 @@ [id="{upid}-{api}"] === Create Enrollment Token API -Enrollment Token can be created using this API. +Enrollment Tokens can be created using this API. [id="{upid}-{api}-request"] ==== Create Enrollment Token Request @@ -24,7 +24,7 @@ include::../execution.asciidoc[] [id="{upid}-{api}-response"] ==== Create Enrollment Token Response -The returned +{response}+ contains a string that contains the enrollment token with which user can enroll a new node +The returned +{response}+ contains a string with the enrollment token with which user can enroll a new node in an existing secured elasticsearch cluster, or a client can configure itself to communicate with a secured elasticsearch cluster. diff --git a/rest-api-spec/src/main/resources/rest-api-spec/api/security.create_enrollment_token.json b/rest-api-spec/src/main/resources/rest-api-spec/api/security.create_enrollment_token.json index 9dc53261216cc..449427f30ecaa 100644 --- a/rest-api-spec/src/main/resources/rest-api-spec/api/security.create_enrollment_token.json +++ b/rest-api-spec/src/main/resources/rest-api-spec/api/security.create_enrollment_token.json @@ -2,7 +2,7 @@ "security.create_enrollment_token":{ "documentation":{ "url":"https://www.elastic.co/guide/en/elasticsearch/reference/master/security-create-enrollment-token.html", - "description":"Create an enrollment token to allow a new node to enroll in an existing secured elasticsearch cluster, or a client to configure itself to\ncommunicate with a secured elasticsearch cluster." + "description":"Create an enrollment token to allow a new node to enroll in an existing secured elasticsearch cluster, or a client to configure itself to communicate with a secured elasticsearch cluster." }, "stability":"stable", "visibility":"public", @@ -15,6 +15,7 @@ { "path":"/_security/enrollment_token", "methods":[ + "PUT", "POST" ] } diff --git a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java index 73cd663e3c35e..e933838d00dd9 100644 --- a/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java +++ b/server/src/main/java/org/elasticsearch/common/settings/ClusterSettings.java @@ -59,7 +59,6 @@ import org.elasticsearch.discovery.PeerFinder; import org.elasticsearch.discovery.SeedHostsResolver; import org.elasticsearch.discovery.SettingsBasedSeedHostsProvider; -import org.elasticsearch.enrollment.EnrollmentSettings; import org.elasticsearch.env.Environment; import org.elasticsearch.env.NodeEnvironment; import org.elasticsearch.gateway.GatewayService; @@ -480,7 +479,7 @@ public void apply(Settings value, Settings current, Settings previous) { FsHealthService.SLOW_PATH_LOGGING_THRESHOLD_SETTING, IndexingPressure.MAX_INDEXING_BYTES, ShardLimitValidator.SETTING_CLUSTER_MAX_SHARDS_PER_NODE_FROZEN, - EnrollmentSettings.ENROLLMENT_ENABLED); + Node.ENROLLMENT_ENABLED); static List> BUILT_IN_SETTING_UPGRADERS = Collections.emptyList(); diff --git a/server/src/main/java/org/elasticsearch/enrollment/EnrollmentSettings.java b/server/src/main/java/org/elasticsearch/enrollment/EnrollmentSettings.java deleted file mode 100644 index 353382137ef5c..0000000000000 --- a/server/src/main/java/org/elasticsearch/enrollment/EnrollmentSettings.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * 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 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -package org.elasticsearch.enrollment; - -import org.elasticsearch.common.settings.Setting; - -public class EnrollmentSettings { - private EnrollmentSettings() { - } - - /** Setting for enabling or disabling enrollment mode. Defaults to false. */ - public static final Setting ENROLLMENT_ENABLED = Setting.boolSetting("xpack.security.enrollment.enabled", false, - Setting.Property.NodeScope); -} diff --git a/server/src/main/java/org/elasticsearch/node/Node.java b/server/src/main/java/org/elasticsearch/node/Node.java index 29f1026715c28..2ca6985f7429a 100644 --- a/server/src/main/java/org/elasticsearch/node/Node.java +++ b/server/src/main/java/org/elasticsearch/node/Node.java @@ -230,6 +230,10 @@ public class Node implements Closeable { public static final Setting INITIAL_STATE_TIMEOUT_SETTING = Setting.positiveTimeSetting("discovery.initial_state_timeout", TimeValue.timeValueSeconds(30), Property.NodeScope); + /** Setting for enabling or disabling enrollment mode. Defaults to false. */ + public static final Setting ENROLLMENT_ENABLED = Setting.boolSetting("node.enrollment.enabled", false, + Setting.Property.NodeScope); + private static final String CLIENT_TYPE = "node"; private final Lifecycle lifecycle = new Lifecycle(); diff --git a/x-pack/docs/en/rest-api/security/create-enrollment-token.asciidoc b/x-pack/docs/en/rest-api/security/create-enrollment-token.asciidoc index 6f8c12815cbf9..2cb950070f032 100644 --- a/x-pack/docs/en/rest-api/security/create-enrollment-token.asciidoc +++ b/x-pack/docs/en/rest-api/security/create-enrollment-token.asciidoc @@ -12,6 +12,8 @@ communicate with a secured elasticsearch cluster. `POST /_security/enrollment_token` +`PUT /_security/enrollment_token` + [[cluster-create-enrollment-token-api-prereqs]] ==== {api-prereq-title} @@ -19,7 +21,7 @@ communicate with a secured elasticsearch cluster. [[cluster-create-enrollment-token-api-desc]] ==== {api-description-title} -The purpose of the create enrollment token API is to generate a token with which user can enroll a new node +The purpose of the create enrollment token API is to generate an enrollment with which user can enroll a new node in an existing secured elasticsearch cluster, or a client can configure itself to communicate with a secured elasticsearch cluster. @@ -45,7 +47,7 @@ The API returns a response such as - The fingerprint of the CA certificate that is used to sign the certificate that the Elasticsearch node presents for TLS on the HTTP layer; - An API key which allows a holder of the token to authenticate themself to the elasticsearch node. -as Base64 encoded string. +as a Base64 encoded string. A decoded enrollment token is shown below (spaces and new lines added for clarity) : [source,console-result] diff --git a/x-pack/docs/en/security/authorization/privileges.asciidoc b/x-pack/docs/en/security/authorization/privileges.asciidoc index 8c224f7e2e510..a2192b0deb980 100644 --- a/x-pack/docs/en/security/authorization/privileges.asciidoc +++ b/x-pack/docs/en/security/authorization/privileges.asciidoc @@ -21,8 +21,7 @@ Privileges to create snapshots for existing repositories. Can also list and view details on existing repositories and snapshots. `enroll`:: -The privilege to call the enroll API to enroll a node in a secured Elasticsearch cluster and to allow a client to -configure itself to communicate with a secured Elasticsearch cluster. +Privilege to join new nodes to a cluster with security features enabled and to configure clients to communicate with a cluster with security features enabled. `grant_api_key`:: Privileges to create {es} API keys on behalf of other users. @@ -59,7 +58,7 @@ manage follower indices and auto-follow patterns. This privilege is necessary only on clusters that contain follower indices. `manage_enrollment`:: -The privilege to call the create enrollment token and enroll APIs. `enroll` is implicitly granted by `manage_enrollment`. +Privilege to create enrollment tokens that can be used to join new nodes to a cluster with security features enabled and to configure clients to communicate with a cluster with security features enabled. `enroll` is implicitly granted by `manage_enrollment`. `manage_ilm`:: All {Ilm} operations related to managing policies. 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 762c7ec1cca81..e25a40529621b 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 @@ -122,7 +122,7 @@ public class ClusterPrivilegeResolver { public static final NamedClusterPrivilege TRANSPORT_CLIENT = new ActionClusterPrivilege("transport_client", TRANSPORT_CLIENT_PATTERN); public static final NamedClusterPrivilege MANAGE_SECURITY = new ActionClusterPrivilege("manage_security", ALL_SECURITY_PATTERN, - Set.of(DelegatePkiAuthenticationAction.NAME)); + Set.of(DelegatePkiAuthenticationAction.NAME, "cluster:admin/xpack/security/enrollment*")); 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); diff --git a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/core/action/httpCA.pem b/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/core/action/httpCA.pem deleted file mode 100644 index c8a0918e5160d..0000000000000 --- a/x-pack/plugin/core/src/test/resources/org/elasticsearch/xpack/core/action/httpCA.pem +++ /dev/null @@ -1,25 +0,0 @@ -Bag Attributes - friendlyName: ca - localKeyID: 54 69 6D 65 20 31 36 31 39 32 30 30 32 36 39 38 30 35 -subject=/CN=Elastic Certificate Tool Autogenerated CA -issuer=/CN=Elastic Certificate Tool Autogenerated CA ------BEGIN CERTIFICATE----- -MIIDSjCCAjKgAwIBAgIVAJYNz6ukOFW1+vygPZ99NGPSZRNOMA0GCSqGSIb3DQEB -CwUAMDQxMjAwBgNVBAMTKUVsYXN0aWMgQ2VydGlmaWNhdGUgVG9vbCBBdXRvZ2Vu -ZXJhdGVkIENBMB4XDTIxMDQyMzE3NTA1NFoXDTI0MDQyMjE3NTA1NFowNDEyMDAG -A1UEAxMpRWxhc3RpYyBDZXJ0aWZpY2F0ZSBUb29sIEF1dG9nZW5lcmF0ZWQgQ0Ew -ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCuRxEbHE8sH7nPuvlCIcAw -nV61P7VNLzOIdjWPC9L8WtEkOy8ahEjonxZ74QyEL38fc4ZmtcZorxrve3rqKiVF -xiQkOlNjz5D9ziKP62jCl7iRiZeSMe3DXqU2TA+WdEtbVHO/UuuA+7M8uPgc9r8X -Usg6tjGUMB5Jta3Jh80WC8esXD/KB/1Cb4laW3GDOKYfVJ6xBQ0QqYQNRbQisY7a -nDlWVBou/al4XM8ovcZm2SrdrtVrNVO1bMpyO7wIMJk0/ckq5o8xs9QUd3UfAnZ1 -5QlqsLCr09EJvJnI+P5L+b5HP/kiSNp+NXyiq+4rfdLwDIaTZDKnVM7CG2rk4ZdB -AgMBAAGjUzBRMB0GA1UdDgQWBBRMK0ffcvF8fwK+9vmzLefmTJq2yDAfBgNVHSME -GDAWgBRMK0ffcvF8fwK+9vmzLefmTJq2yDAPBgNVHRMBAf8EBTADAQH/MA0GCSqG -SIb3DQEBCwUAA4IBAQCAiEV9wGWt6BpDYOudBfi674tX+yiF2rXAurnbFQtiTa8/ -o7y99wx7Jt9GWLY+uASapKQ5ybu68wRbW3MVJpIu7T5Abo30+VsoUMdJDCKoh2Uv -jR8VxtCmUTSi8/htXB4WI9S7AkpEBEWAqT0XY6C35+W+3J6w95GuCTq5ZijURE+J -7j5+aCydq4KmLGKrJlGQGOWz+bNJvwmxxZTvYEhAwaAuj47Ba91rE4D6Lob40LSV -b1gRpIkwf5EyNWALnqSqWpnF4qP9mSZHCi1aRlAb9G5GtrDushXHVQVFN0hTqkzh -YdOtGvJvCNAn0xtZwn9WjSoEzpV+6ByaEgqP50KV ------END CERTIFICATE----- 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 e8d3d20df478c..3af391b9b98a6 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 @@ -957,7 +957,7 @@ public List getRestHandlers(Settings settings, RestController restC new RestDeleteServiceAccountTokenAction(settings, getLicenseState()), new RestGetServiceAccountCredentialsAction(settings, getLicenseState()), new RestGetServiceAccountAction(settings, getLicenseState()), - new RestCreateEnrollmentTokenAction(settings, getLicenseState()) + new RestCreateEnrollmentTokenAction(settings, getLicenseState()), new RestNodeEnrollmentAction(settings, getLicenseState()) ); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java index c93359c36456b..3dcb8d760987e 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java @@ -7,13 +7,14 @@ package org.elasticsearch.xpack.security.action.enrollment; -import org.apache.logging.log4j.message.ParameterizedMessage; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.HandledTransportAction; import org.elasticsearch.common.Strings; import org.elasticsearch.common.UUIDs; +import org.elasticsearch.common.collect.Tuple; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.ssl.SslUtil; import org.elasticsearch.common.unit.TimeValue; @@ -39,8 +40,8 @@ import org.elasticsearch.xpack.security.authz.store.CompositeRolesStore; import java.nio.charset.StandardCharsets; +import java.security.PrivateKey; import java.security.cert.X509Certificate; -import java.util.ArrayList; import java.util.Base64; import java.util.List; import java.util.stream.Collectors; @@ -52,8 +53,9 @@ public class TransportCreateEnrollmentTokenAction extends HandledTransportAction { - public static final long ENROLL_API_KEY_EXPIRATION_SEC = 30*60; + protected static final long ENROLL_API_KEY_EXPIRATION_SEC = 30*60; + private static final Logger logger = LogManager.getLogger(TransportCreateEnrollmentTokenAction.class); private final ApiKeyGenerator generator; private final SecurityContext securityContext; private final Environment environment; @@ -65,8 +67,16 @@ public TransportCreateEnrollmentTokenAction(TransportService transportService, A SecurityContext context, CompositeRolesStore rolesStore, NamedXContentRegistry xContentRegistry, Environment environment, NodeService nodeService, SSLService sslService) { + this(transportService, actionFilters, context, + environment, nodeService, sslService, new ApiKeyGenerator(apiKeyService, rolesStore, xContentRegistry)); + } + + // Constructor for testing + TransportCreateEnrollmentTokenAction(TransportService transportService, ActionFilters actionFilters, SecurityContext context, + Environment environment, NodeService nodeService, SSLService sslService, + ApiKeyGenerator generator) { super(CreateEnrollmentTokenAction.NAME, transportService, actionFilters, CreateEnrollmentTokenRequest::new); - this.generator = new ApiKeyGenerator(apiKeyService, rolesStore, xContentRegistry); + this.generator = generator; this.securityContext = context; this.environment = environment; this.nodeService = nodeService; @@ -84,61 +94,53 @@ protected void doExecute(Task task, CreateEnrollmentTokenRequest request, "keystore")); return; } - final List caCertificates = ((StoreKeyConfig) keyConfig).x509Certificates(environment).stream() - .filter(x509Certificate -> x509Certificate.getBasicConstraints() != -1).collect(Collectors.toList()); - if (caCertificates.size() != 1) { + final List> httpCaKeysAndCertificates = + ((StoreKeyConfig) keyConfig).getPrivateKeyEntries(environment).stream() + .filter(t -> t.v2().getBasicConstraints() != -1).collect(Collectors.toList()); + if (httpCaKeysAndCertificates.isEmpty()) { + listener.onFailure(new IllegalStateException( + "Unable to create an enrollment token. Elasticsearch node HTTP layer SSL configuration Keystore doesn't contain any " + + "PrivateKey entries where the associated certificate is a CA certificate")); + return; + } else if (httpCaKeysAndCertificates.size() > 1) { listener.onFailure(new IllegalStateException( - "Unable to create an enrollment token. Elasticsearch node HTTP layer SSL configuration Keystore doesn't contain a " + - "single PrivateKey entry where the associated certificate is a CA certificate")); + "Unable to create an enrollment token. Elasticsearch node HTTP layer SSL configuration Keystore contain multiple " + + "PrivateKey entries where the associated certificate is a CA certificate")); + return; } else { final TimeValue expiration = TimeValue.timeValueSeconds(ENROLL_API_KEY_EXPIRATION_SEC); - final List roleDescriptors = new ArrayList<>(1); final String[] clusterPrivileges = { "enroll" }; - final RoleDescriptor roleDescriptor = new RoleDescriptor("create_enrollment_token", clusterPrivileges, null, null); - roleDescriptors.add(roleDescriptor); - CreateApiKeyRequest apiRequest = new CreateApiKeyRequest(UUIDs.base64UUID(), roleDescriptors, expiration); - final String fingerprint = SslUtil.calculateFingerprint(caCertificates.get(0)); - createEnrolmentToken(fingerprint, apiRequest, listener); + final List roleDescriptors = List.of(new RoleDescriptor("create_enrollment_token", clusterPrivileges, null, null)); + CreateApiKeyRequest apiRequest = new CreateApiKeyRequest("enrollment_token_API_key_" + UUIDs.base64UUID(), + roleDescriptors, expiration); + final String fingerprint = SslUtil.calculateFingerprint(httpCaKeysAndCertificates.get(0).v2()); + generator.generateApiKey(securityContext.getAuthentication(), apiRequest, + ActionListener.wrap( + CreateApiKeyResponse -> { + try { + final String address = nodeService.info(false, false, false, false, false, false, + true, false, false, false, false).getInfo(HttpInfo.class).getAddress().publishAddress().toString(); + final XContentBuilder builder = JsonXContent.contentBuilder(); + builder.startObject(); + builder.field("adr", address); + builder.field("fgr", fingerprint); + builder.field("key", CreateApiKeyResponse.getKey().toString()); + builder.endObject(); + final String jsonString = Strings.toString(builder); + final String token = Base64.getEncoder().encodeToString(jsonString.getBytes(StandardCharsets.UTF_8)); + final CreateEnrollmentTokenResponse response = new CreateEnrollmentTokenResponse(token); + listener.onResponse(response); + } catch (Exception e) { + logger.error(("Error generating enrollment token"), e); + listener.onFailure(e); + } + }, + listener::onFailure + ) + ); } } catch (Exception e) { listener.onFailure(e); } } - - private void createEnrolmentToken(String fingerprint, CreateApiKeyRequest apiRequest, - ActionListener listener) { - try { - generator.generateApiKey(securityContext.getAuthentication(), apiRequest, - ActionListener.wrap( - CreateApiKeyResponse -> { - try { - final NodeInfo nodeInfo = nodeService.info(false, false, false, false, false, false, - true, false, false, false, false); - final HttpInfo httpInfo = nodeInfo.getInfo(HttpInfo.class); - final String address = httpInfo.getAddress().publishAddress().toString(); - - final XContentBuilder builder = JsonXContent.contentBuilder(); - builder.startObject(); - builder.field("adr", address); - builder.field("fgr", fingerprint); - builder.field("key", CreateApiKeyResponse.getKey().toString()); - builder.endObject(); - final String jsonString = Strings.toString(builder); - final String token = Base64.getEncoder().encodeToString(jsonString.getBytes(StandardCharsets.UTF_8)); - - final CreateEnrollmentTokenResponse response = new CreateEnrollmentTokenResponse(token); - listener.onResponse(response); - } catch (Exception e) { - logger.error(() -> new ParameterizedMessage("Error generating enrollment token"), e); - listener.onFailure(e); - } - }, - listener::onFailure - ) - ); - } catch (Exception e) { - logger.error(() -> new ParameterizedMessage("Error generating enrollment token"), e); - listener.onFailure(e); - } - } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/enrollment/RestCreateEnrollmentTokenAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/enrollment/RestCreateEnrollmentTokenAction.java index 49c848a5f92ee..a6d6cd272dde3 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/enrollment/RestCreateEnrollmentTokenAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/enrollment/RestCreateEnrollmentTokenAction.java @@ -11,8 +11,8 @@ import org.elasticsearch.client.node.NodeClient; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.enrollment.EnrollmentSettings; import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.node.Node; import org.elasticsearch.rest.BytesRestResponse; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.rest.RestResponse; @@ -59,7 +59,7 @@ public String getName() { @Override protected RestChannelConsumer innerPrepareRequest(final RestRequest request, final NodeClient client) throws IOException { - if (EnrollmentSettings.ENROLLMENT_ENABLED.get(settings) != true) { + if (Node.ENROLLMENT_ENABLED.get(settings) != true) { throw new IllegalStateException("Enrollment mode is not enabled."); } final CreateEnrollmentTokenRequest enrollmentTokenRequest = new CreateEnrollmentTokenRequest(); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenActionTests.java new file mode 100644 index 0000000000000..94bcb174b42a1 --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenActionTests.java @@ -0,0 +1,171 @@ +/* + * 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.enrollment; + +import org.elasticsearch.Build; +import org.elasticsearch.Version; +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.admin.cluster.node.info.NodeInfo; +import org.elasticsearch.action.support.ActionFilters; +import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.cluster.node.DiscoveryNode; +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.xcontent.XContentParser; +import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.env.Environment; +import org.elasticsearch.http.HttpInfo; +import org.elasticsearch.node.NodeService; +import org.elasticsearch.tasks.Task; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.VersionUtils; +import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.transport.Transport; +import org.elasticsearch.transport.TransportService; +import org.elasticsearch.xpack.core.security.SecurityContext; +import org.elasticsearch.xpack.core.security.action.CreateApiKeyRequest; +import org.elasticsearch.xpack.core.security.action.CreateApiKeyResponse; +import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenRequest; +import org.elasticsearch.xpack.core.security.action.enrollment.CreateEnrollmentTokenResponse; +import org.elasticsearch.xpack.core.security.authc.Authentication; +import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.core.ssl.SSLConfiguration; +import org.elasticsearch.xpack.core.ssl.SSLService; +import org.elasticsearch.xpack.security.authc.support.ApiKeyGenerator; +import org.junit.Before; +import org.mockito.Mockito; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Clock; +import java.time.Instant; +import java.util.Base64; +import java.util.Collections; +import java.util.Map; +import java.util.stream.Collectors; + +import static java.util.Collections.emptyMap; +import static java.util.Collections.emptySet; +import static org.elasticsearch.xpack.security.action.enrollment.TransportCreateEnrollmentTokenAction.ENROLL_API_KEY_EXPIRATION_SEC; +import static org.hamcrest.Matchers.arrayWithSize; +import static org.hamcrest.Matchers.equalTo; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +public class TransportCreateEnrollmentTokenActionTests extends ESTestCase { + private TransportCreateEnrollmentTokenAction action; + private ApiKeyGenerator apiKeyGenerator; + private SecureString key; + private BoundTransportAddress dummyBoundTransportAddress; + private Instant now; + + @Before + public void setupMocks() throws Exception { + final Clock clock = Clock.systemUTC(); + now = clock.instant(); + final Environment env = mock(Environment.class); + final Path tempDir = createTempDir(); + final Path httpCaPath = tempDir.resolve("httpCa.p12"); + Files.copy(getDataPath("/org/elasticsearch/xpack/security/action/enrollment/httpCa.p12"), httpCaPath); + when(env.configFile()).thenReturn(tempDir); + final Settings settings = Settings.builder() + .put("xpack.security.enabled", true) + .put( "xpack.security.authc.api_key.enabled", true) + .put("keystore.path", "httpCa.p12") + .put("keystore.password", "password") + .build(); + final SSLService sslService = mock(SSLService.class); + final SSLConfiguration sslConfiguration = new SSLConfiguration(settings); + when(sslService.getHttpTransportSSLConfiguration()).thenReturn(sslConfiguration); + + Authentication authentication = + new Authentication(new User("joe", "manage_enrollment"), + new Authentication.RealmRef("test", "test", "node"), null); + final SecurityContext securityContext = mock(SecurityContext.class); + when(securityContext.getAuthentication()).thenReturn(authentication); + + final NodeService nodeService = mock(NodeService.class); + dummyBoundTransportAddress = new BoundTransportAddress( + new TransportAddress[]{buildNewFakeTransportAddress()}, buildNewFakeTransportAddress()); + NodeInfo nodeInfo = new NodeInfo( + Version.CURRENT, + Build.CURRENT, + new DiscoveryNode("test_node", buildNewFakeTransportAddress(), emptyMap(), emptySet(), VersionUtils.randomVersion(random())), + null, + null, + null, + null, + null, + null, + new HttpInfo(dummyBoundTransportAddress, randomNonNegativeLong()), + null, + null, + null, + null); + doReturn(nodeInfo).when(nodeService).info(false, false, false, false, false, false, + true, false, false, false, false); + + final TransportService transportService = new TransportService(settings, + mock(Transport.class), + mock(ThreadPool.class), + TransportService.NOOP_TRANSPORT_INTERCEPTOR, + x -> null, + null, + Collections.emptySet()); + + apiKeyGenerator = mock(ApiKeyGenerator.class); + key = new SecureString(randomAlphaOfLength(18).toCharArray()); + final CreateApiKeyResponse createApiKeyResponse = new CreateApiKeyResponse(randomAlphaOfLengthBetween(6, 32), + randomAlphaOfLength(12), key, now.plusMillis(ENROLL_API_KEY_EXPIRATION_SEC*1000)); + + Mockito.doAnswer(inv -> { + final Object[] args = inv.getArguments(); + assertThat(args, arrayWithSize(3)); + + assertThat(args[0], equalTo(authentication)); + + ActionListener listener = (ActionListener) args[args.length - 1]; + listener.onResponse(createApiKeyResponse); + + return null; + }).when(apiKeyGenerator).generateApiKey(any(Authentication.class), any(CreateApiKeyRequest.class), any(ActionListener.class)); + + action = + new TransportCreateEnrollmentTokenAction(transportService, mock(ActionFilters.class), + securityContext, env, nodeService, sslService, apiKeyGenerator); + } + + public void testDoExecute() throws IOException { + final CreateEnrollmentTokenRequest request = new CreateEnrollmentTokenRequest(); + final PlainActionFuture future = new PlainActionFuture<>(); + action.doExecute(mock(Task.class), request, future); + final CreateEnrollmentTokenResponse response = future.actionGet(); + + Map info = getDecoded(response); + assertEquals(dummyBoundTransportAddress.publishAddress().toString(), info.get("adr")); + assertEquals("598a35cd831ee6bb90e79aa80d6b073cda88b41d", info.get("fgr")); + assertEquals(key.toString(), info.get("key")); + } + + private Map getDecoded(CreateEnrollmentTokenResponse response) throws IOException { + assertNotEquals(response.getEnrollmentToken(), null); + String jsonString = new String(Base64.getDecoder().decode(response.getEnrollmentToken()), StandardCharsets.UTF_8); + try (XContentParser parser = createParser(JsonXContent.jsonXContent, jsonString)) { + Map info = parser.map(); + assertNotEquals(info, null); + return info.entrySet().stream() + .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue().toString())); + } + } +} From 77bf9d588988183bfc39194af9bf4891e4418e55 Mon Sep 17 00:00:00 2001 From: Lyudmila Fokina <35386883+BigPandaToo@users.noreply.github.com> Date: Wed, 12 May 2021 22:03:34 +0200 Subject: [PATCH 09/14] Addressing PR comments adding tests --- .../docs/en/rest-api/security/create-enrollment-token.asciidoc | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/docs/en/rest-api/security/create-enrollment-token.asciidoc b/x-pack/docs/en/rest-api/security/create-enrollment-token.asciidoc index 2cb950070f032..ed70c3b23bba8 100644 --- a/x-pack/docs/en/rest-api/security/create-enrollment-token.asciidoc +++ b/x-pack/docs/en/rest-api/security/create-enrollment-token.asciidoc @@ -32,6 +32,7 @@ communicate with a secured elasticsearch cluster. -------------------------------------------------- POST /_security/enrollment_token -------------------------------------------------- +// TEST[skip:Determine behavior for keystore with multiple keys] The API returns a response such as From 626d957b4bdcf0fcf4a1c4a0118a03bfc951771a Mon Sep 17 00:00:00 2001 From: Lyudmila Fokina <35386883+BigPandaToo@users.noreply.github.com> Date: Thu, 13 May 2021 18:05:34 +0200 Subject: [PATCH 10/14] fixing tests and style --- .../enrollment/TransportCreateEnrollmentTokenAction.java | 3 ++- .../resources/rest-api-spec/test/privileges/11_builtin.yml | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java index 3dcb8d760987e..dba3c9b92f0cb 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenAction.java @@ -110,7 +110,8 @@ protected void doExecute(Task task, CreateEnrollmentTokenRequest request, } else { final TimeValue expiration = TimeValue.timeValueSeconds(ENROLL_API_KEY_EXPIRATION_SEC); final String[] clusterPrivileges = { "enroll" }; - final List roleDescriptors = List.of(new RoleDescriptor("create_enrollment_token", clusterPrivileges, null, null)); + final List roleDescriptors = List.of(new RoleDescriptor("create_enrollment_token", clusterPrivileges, + null, null)); CreateApiKeyRequest apiRequest = new CreateApiKeyRequest("enrollment_token_API_key_" + UUIDs.base64UUID(), roleDescriptors, expiration); final String fingerprint = SslUtil.calculateFingerprint(httpCaKeysAndCertificates.get(0).v2()); diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/privileges/11_builtin.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/privileges/11_builtin.yml index be7d28af3ae42..18cc0abeedc61 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/privileges/11_builtin.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/privileges/11_builtin.yml @@ -9,6 +9,9 @@ setup: --- "Test get builtin privileges": + - skip: + version: " - 7.99.99" + reason: "new privileges in 8.0 only" - do: security.get_builtin_privileges: {} From d10763338a49ceee8742b97b265c74d7da810dc1 Mon Sep 17 00:00:00 2001 From: Lyudmila Fokina <35386883+BigPandaToo@users.noreply.github.com> Date: Thu, 13 May 2021 20:38:19 +0200 Subject: [PATCH 11/14] fixing tests --- x-pack/docs/en/rest-api/security.asciidoc | 2 +- .../rest-api-spec/test/privileges/11_builtin.yml | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/x-pack/docs/en/rest-api/security.asciidoc b/x-pack/docs/en/rest-api/security.asciidoc index 3f24967d197c8..e0566aeb53434 100644 --- a/x-pack/docs/en/rest-api/security.asciidoc +++ b/x-pack/docs/en/rest-api/security.asciidoc @@ -113,7 +113,7 @@ You can use the following API to generate a token with which a user can enroll a in an existing secured elasticsearch cluster, or a client can configure itself to communicate with a secured elasticsearch cluster. -* <> +* <> You can use the following APIs to allow new nodes to join an existing cluster with security enabled or to allow a client to configure itself to communicate with diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/privileges/11_builtin.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/privileges/11_builtin.yml index 18cc0abeedc61..3fd688feaab4a 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/privileges/11_builtin.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/privileges/11_builtin.yml @@ -20,3 +20,17 @@ setup: # an assertion for that - length: { "cluster" : 43 } - length: { "index" : 19 } + +--- +"Test get builtin privileges": + - skip: + version: "7.99.99 - " + reason: "new privileges in 8.0 only" + - do: + security.get_builtin_privileges: {} + + # 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" : 41 } + - length: { "index" : 19 } From f4be25be03c533b738f831c862ae8b1b7d9b7217 Mon Sep 17 00:00:00 2001 From: Lyudmila Fokina <35386883+BigPandaToo@users.noreply.github.com> Date: Thu, 13 May 2021 20:58:21 +0200 Subject: [PATCH 12/14] fixing tests --- .../resources/rest-api-spec/test/privileges/11_builtin.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/privileges/11_builtin.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/privileges/11_builtin.yml index 3fd688feaab4a..49ed55d2ddbee 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/privileges/11_builtin.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/privileges/11_builtin.yml @@ -8,10 +8,11 @@ setup: wait_for_status: yellow --- -"Test get builtin privileges": +"Test get builtin privileges 8.x": - skip: version: " - 7.99.99" reason: "new privileges in 8.0 only" + - do: security.get_builtin_privileges: {} @@ -22,10 +23,11 @@ setup: - length: { "index" : 19 } --- -"Test get builtin privileges": +"Test get builtin privileges 7.x": - skip: version: "7.99.99 - " reason: "new privileges in 8.0 only" + - do: security.get_builtin_privileges: {} From fb7b5239c9dd5e0bbf154544cdd802ee38adafee Mon Sep 17 00:00:00 2001 From: Lyudmila Fokina <35386883+BigPandaToo@users.noreply.github.com> Date: Fri, 14 May 2021 15:13:54 +0200 Subject: [PATCH 13/14] Adding test --- .../SecurityDocumentationIT.java | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java index 25bd06c10f754..6f6e83c2928a2 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/documentation/SecurityDocumentationIT.java @@ -30,6 +30,7 @@ import org.elasticsearch.client.security.CreateApiKeyRequest; import org.elasticsearch.client.security.CreateApiKeyRequestTests; import org.elasticsearch.client.security.CreateApiKeyResponse; +import org.elasticsearch.client.security.CreateEnrollmentTokenResponse; import org.elasticsearch.client.security.CreateTokenRequest; import org.elasticsearch.client.security.CreateTokenResponse; import org.elasticsearch.client.security.DelegatePkiAuthenticationRequest; @@ -2609,6 +2610,45 @@ public void onFailure(Exception e) { } } + @AwaitsFix(bugUrl = "Determine behavior for keystores with multiple keys") + public void testCreateEnrollmentToken() throws Exception { + RestHighLevelClient client = highLevelClient(); + + { + // tag::create-enrollment-token-execute + CreateEnrollmentTokenResponse response = client.security().createEnrollmentToken(RequestOptions.DEFAULT); + // end::create-enrollment-token-execute + + // tag::create-enrollment-token-response + String enrollmentToken = response.getEnrollmentToken(); // <1> + // end::create-enrollment-token-response + } + + { + // tag::create-enrollment-token-execute-listener + ActionListener listener = + new ActionListener() { + @Override + public void onResponse(CreateEnrollmentTokenResponse response) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + }}; + // end::create-enrollment-token-execute-listener + + final CountDownLatch latch = new CountDownLatch(1); + listener = new LatchedActionListener<>(listener, latch); + + // tag::create-enrollment-token-execute-async + client.security().createEnrollmentTokenAsync(RequestOptions.DEFAULT, listener); + // end::create-enrollment-token-execute-async + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } + } + private X509Certificate readCertForPkiDelegation(String certificateName) throws Exception { Path path = getDataPath("/org/elasticsearch/client/security/delegate_pki/" + certificateName); try (InputStream in = Files.newInputStream(path)) { From 1800069df67dde11ed162812f61b50a80740527b Mon Sep 17 00:00:00 2001 From: Lyudmila Fokina <35386883+BigPandaToo@users.noreply.github.com> Date: Fri, 14 May 2021 15:42:37 +0200 Subject: [PATCH 14/14] Disabling running enrollment test with Fips --- .../enrollment/TransportCreateEnrollmentTokenActionTests.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenActionTests.java index 94bcb174b42a1..59d9028b425c5 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/enrollment/TransportCreateEnrollmentTokenActionTests.java @@ -147,6 +147,8 @@ public void setupMocks() throws Exception { } public void testDoExecute() throws IOException { + assumeFalse("NodeEnrollment is not supported on FIPS because it requires a KeyStore", inFipsJvm()); + final CreateEnrollmentTokenRequest request = new CreateEnrollmentTokenRequest(); final PlainActionFuture future = new PlainActionFuture<>(); action.doExecute(mock(Task.class), request, future);