From c84c36fd22ff13c80dbb6700f99b9d4cb12cc4fa Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Wed, 11 Aug 2021 18:54:48 +0300 Subject: [PATCH 01/12] Enroll Kibana API uses Service Accounts This change introduces a Service Account for Kibana to use when authenticating to Elasticsearch. The Service Account with `kibana-system` service name under the `elastic` namespace, uses the same RoleDescriptor as the existing kibana_system built-in user and is its functional equivalent when it comes to AuthZ. It also changes the Enroll Kibana API to create and return a token for this service account, instead of setting and returning the password of the kibana_system built-in user. --- .../elasticsearch/client/EnrollmentIT.java | 4 +- .../security/KibanaEnrollmentResponse.java | 16 +- .../SecurityDocumentationIT.java | 8 +- .../KibanaErnollmentResponseTests.java | 14 +- .../security/enroll_kibana.asciidoc | 2 +- .../rest-api/security/enroll-kibana.asciidoc | 4 +- .../enrollment/KibanaEnrollmentResponse.java | 22 +- .../authz/store/ReservedRolesStore.java | 150 +++--- .../KibanaEnrollmentResponseTests.java | 2 +- .../TransportKibanaEnrollmentAction.java | 43 +- .../authc/service/ElasticServiceAccounts.java | 7 +- .../TransportKibanaEnrollmentActionTests.java | 40 +- .../service/ElasticServiceAccountsTests.java | 459 ++++++++++++++++++ 13 files changed, 614 insertions(+), 157 deletions(-) diff --git a/client/rest-high-level/qa/ssl-enabled/src/javaRestTest/java/org/elasticsearch/client/EnrollmentIT.java b/client/rest-high-level/qa/ssl-enabled/src/javaRestTest/java/org/elasticsearch/client/EnrollmentIT.java index 5266db022bd4a..fd245a4d230f7 100644 --- a/client/rest-high-level/qa/ssl-enabled/src/javaRestTest/java/org/elasticsearch/client/EnrollmentIT.java +++ b/client/rest-high-level/qa/ssl-enabled/src/javaRestTest/java/org/elasticsearch/client/EnrollmentIT.java @@ -25,6 +25,7 @@ import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.startsWith; public class EnrollmentIT extends ESRestHighLevelClientTestCase { private static Path httpTrustStore; @@ -76,7 +77,6 @@ public void testEnrollKibana() throws Exception { assertThat(kibanaResponse, notNullValue()); assertThat(kibanaResponse.getHttpCa() , endsWith("brcNC5xq6YE7C4/06nH7F6le4kE4Uo6c9fpkl4ehOxQxndNLn462tFF+8VBA8IftJ1PPWzqGxLsCTzM6p6w8sa+XhgNYglLfkRjirc=")); - assertNotNull(kibanaResponse.getPassword()); - assertThat(kibanaResponse.getPassword().toString().length(), equalTo(14)); + assertNotNull(kibanaResponse.getToken()); } } diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/KibanaEnrollmentResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/KibanaEnrollmentResponse.java index b9e71f2a08ed6..96dcdbba67bb7 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/KibanaEnrollmentResponse.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/KibanaEnrollmentResponse.java @@ -18,21 +18,21 @@ public final class KibanaEnrollmentResponse { - private SecureString password; + private SecureString token; private String httpCa; - public KibanaEnrollmentResponse(SecureString password, String httpCa) { - this.password = password; + public KibanaEnrollmentResponse(SecureString token, String httpCa) { + this.token = token; this.httpCa = httpCa; } - public SecureString getPassword() { return password; } + public SecureString getToken() { return token; } public String getHttpCa() { return httpCa; } - private static final ParseField PASSWORD = new ParseField("password"); + private static final ParseField TOKEN = new ParseField("token"); private static final ParseField HTTP_CA = new ParseField("http_ca"); @SuppressWarnings("unchecked") @@ -42,7 +42,7 @@ public String getHttpCa() { a -> new KibanaEnrollmentResponse(new SecureString(((String) a[0]).toCharArray()), (String) a[1])); static { - PARSER.declareString(ConstructingObjectParser.constructorArg(), PASSWORD); + PARSER.declareString(ConstructingObjectParser.constructorArg(), TOKEN); PARSER.declareString(ConstructingObjectParser.constructorArg(), HTTP_CA); } @@ -54,10 +54,10 @@ public static KibanaEnrollmentResponse fromXContent(XContentParser parser) throw if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; KibanaEnrollmentResponse that = (KibanaEnrollmentResponse) o; - return password.equals(that.password) && httpCa.equals(that.httpCa); + return token.equals(that.token) && httpCa.equals(that.httpCa); } @Override public int hashCode() { - return Objects.hash(password, httpCa); + return Objects.hash(token, httpCa); } } 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 3be558587db5a..da13b8f38a5a0 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 @@ -2875,7 +2875,7 @@ public void onFailure(Exception e) { } } - @AwaitsFix(bugUrl = "Determine behavior for keystores with multiple keys") + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/75097") public void testNodeEnrollment() throws Exception { RestHighLevelClient client = highLevelClient(); @@ -2918,7 +2918,7 @@ public void onFailure(Exception e) { } } - @AwaitsFix(bugUrl = "Determine behavior for keystores with multiple keys") + @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/75097") public void testKibanaEnrollment() throws Exception { RestHighLevelClient client = highLevelClient(); @@ -2928,10 +2928,10 @@ public void testKibanaEnrollment() throws Exception { // end::kibana-enrollment-execute // tag::kibana-enrollment-response - SecureString password = response.getPassword(); // <1> + SecureString token = response.getToken(); // <1> String httoCa = response.getHttpCa(); // <2> // end::kibana-enrollment-response - assertThat(password.length(), equalTo(14)); + assertNotNull(token); } { diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/KibanaErnollmentResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/KibanaErnollmentResponseTests.java index 937e750ccb72c..70f676e7f973b 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/KibanaErnollmentResponseTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/KibanaErnollmentResponseTests.java @@ -24,17 +24,17 @@ public class KibanaErnollmentResponseTests extends ESTestCase { public void testFromXContent() throws IOException { - final String password = randomAlphaOfLength(14); + final String token = randomAlphaOfLength(14); final String httpCa = randomAlphaOfLength(50); final List nodesAddresses = randomList(2, 10, () -> buildNewFakeTransportAddress().toString()); final XContentType xContentType = randomFrom(XContentType.values()); final XContentBuilder builder = XContentFactory.contentBuilder(xContentType); - builder.startObject().field("password", password).field("http_ca", httpCa).field("nodes_addresses", nodesAddresses).endObject(); + builder.startObject().field("token", token).field("http_ca", httpCa).field("nodes_addresses", nodesAddresses).endObject(); BytesReference xContent = BytesReference.bytes(builder); final KibanaEnrollmentResponse response = KibanaEnrollmentResponse.fromXContent(createParser(xContentType.xContent(), xContent)); - assertThat(response.getPassword(), equalTo(password)); + assertThat(response.getToken(), equalTo(token)); assertThat(response.getHttpCa(), equalTo(httpCa)); } @@ -44,10 +44,10 @@ public void testEqualsHashCode() { KibanaEnrollmentResponse kibanaEnrollmentResponse = new KibanaEnrollmentResponse(password, httpCa); EqualsHashCodeTestUtils.checkEqualsAndHashCode(kibanaEnrollmentResponse, - (original) -> new KibanaEnrollmentResponse(original.getPassword(), original.getHttpCa())); + (original) -> new KibanaEnrollmentResponse(original.getToken(), original.getHttpCa())); EqualsHashCodeTestUtils.checkEqualsAndHashCode(kibanaEnrollmentResponse, - (original) -> new KibanaEnrollmentResponse(original.getPassword(), original.getHttpCa()), + (original) -> new KibanaEnrollmentResponse(original.getToken(), original.getHttpCa()), KibanaErnollmentResponseTests::mutateTestItem); } @@ -57,9 +57,9 @@ private static KibanaEnrollmentResponse mutateTestItem(KibanaEnrollmentResponse return new KibanaEnrollmentResponse(new SecureString(randomAlphaOfLength(14).toCharArray()), original.getHttpCa()); case 1: - return new KibanaEnrollmentResponse(original.getPassword(), randomAlphaOfLength(51)); + return new KibanaEnrollmentResponse(original.getToken(), randomAlphaOfLength(51)); default: - return new KibanaEnrollmentResponse(original.getPassword(), + return new KibanaEnrollmentResponse(original.getToken(), original.getHttpCa()); } } diff --git a/docs/java-rest/high-level/security/enroll_kibana.asciidoc b/docs/java-rest/high-level/security/enroll_kibana.asciidoc index 6e9111375b9bf..d84dce665de09 100644 --- a/docs/java-rest/high-level/security/enroll_kibana.asciidoc +++ b/docs/java-rest/high-level/security/enroll_kibana.asciidoc @@ -21,7 +21,7 @@ executed operation as follows: -------------------------------------------------- include-tagged::{doc-tests-file}[{api-kibana-response] -------------------------------------------------- -<1> The password for the `kibana_system` user +<1> A token that can be used as a bearer token for the `kibana-system` service account. <2> The CA certificate that has signed the certificate that the cluster uses for TLS on the HTTP layer, as a Base64 encoded string of the ASN.1 DER encoding of the certificate. diff --git a/x-pack/docs/en/rest-api/security/enroll-kibana.asciidoc b/x-pack/docs/en/rest-api/security/enroll-kibana.asciidoc index 0736900ce2104..60b96b0bdcf74 100644 --- a/x-pack/docs/en/rest-api/security/enroll-kibana.asciidoc +++ b/x-pack/docs/en/rest-api/security/enroll-kibana.asciidoc @@ -35,10 +35,10 @@ The API returns the following response: [source,console_result] ---- { - "password" : "longsecurepassword", <1> + "token" : "AAEAAWVsYXN0aWM...vZmxlZXQtc2VydmVyL3Rva2VuMTo3TFdaSDZ", <1> "http_ca" : "MIIJlAIBAzCCCVoGCSqGSIb3....vsDfsA3UZBAjEPfhubpQysAICCAA=", <2> } ---- -<1> The password for the `kibana_system` user. +<1> A token that can be used as a bearer token for the `kibana-system` service account. <2> The CA certificate used to sign the node certificates that {es} uses for TLS on the HTTP layer. The certificate is returned as a Base64 encoded string of the ASN.1 DER encoding of the certificate. diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/KibanaEnrollmentResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/KibanaEnrollmentResponse.java index bad9b0fdb678d..0fc1a46bd0412 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/KibanaEnrollmentResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/KibanaEnrollmentResponse.java @@ -22,7 +22,7 @@ public final class KibanaEnrollmentResponse extends ActionResponse implements ToXContentObject { - private static final ParseField PASSWORD = new ParseField("password"); + private static final ParseField TOKEN = new ParseField("token"); private static final ParseField HTTP_CA = new ParseField("http_ca"); @SuppressWarnings("unchecked") @@ -32,25 +32,25 @@ public final class KibanaEnrollmentResponse extends ActionResponse implements To a -> new KibanaEnrollmentResponse(new SecureString(((String) a[0]).toCharArray()), (String) a[1])); static { - PARSER.declareString(ConstructingObjectParser.constructorArg(), PASSWORD); + PARSER.declareString(ConstructingObjectParser.constructorArg(), TOKEN); PARSER.declareString(ConstructingObjectParser.constructorArg(), HTTP_CA); } - private final SecureString password; + private final SecureString token; private final String httpCa; public KibanaEnrollmentResponse(StreamInput in) throws IOException { super(in); - password = in.readSecureString(); + token = in.readSecureString(); httpCa = in.readString(); } - public KibanaEnrollmentResponse(SecureString password, String httpCa) { - this.password = password; + public KibanaEnrollmentResponse(SecureString token, String httpCa) { + this.token = token; this.httpCa = httpCa; } - public SecureString getPassword() { return password; } + public SecureString getToken() { return token; } public String getHttpCa() { return httpCa; } @@ -58,13 +58,13 @@ public String getHttpCa() { @Override public XContentBuilder toXContent( XContentBuilder builder, Params params) throws IOException { builder.startObject(); - builder.field(PASSWORD.getPreferredName(), password.toString()); + builder.field(TOKEN.getPreferredName(), token.toString()); builder.field(HTTP_CA.getPreferredName(), httpCa); return builder.endObject(); } @Override public void writeTo(StreamOutput out) throws IOException { - out.writeSecureString(password); + out.writeSecureString(token); out.writeString(httpCa); } @@ -76,10 +76,10 @@ public static KibanaEnrollmentResponse fromXContent(XContentParser parser) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; KibanaEnrollmentResponse response = (KibanaEnrollmentResponse) o; - return password.equals(response.password) && httpCa.equals(response.httpCa); + return token.equals(response.token) && httpCa.equals(response.httpCa); } @Override public int hashCode() { - return Objects.hash(password, httpCa); + return Objects.hash(token, httpCa); } } diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java index bde1ef8978cf7..d1eb7adaac7f7 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/store/ReservedRolesStore.java @@ -21,7 +21,7 @@ import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivileges.ManageApplicationPrivileges; import org.elasticsearch.xpack.core.security.support.MetadataUtils; -import org.elasticsearch.xpack.core.security.user.KibanaUser; +import org.elasticsearch.xpack.core.security.user.KibanaSystemUser; import org.elasticsearch.xpack.core.security.user.UsernamesField; import org.elasticsearch.xpack.core.transform.transforms.persistence.TransformInternalIndexConstants; import org.elasticsearch.xpack.core.watcher.execution.TriggeredWatchStoreField; @@ -125,78 +125,7 @@ private static Map initializeReservedRoles() { null, null, MetadataUtils.getDeprecatedReservedMetadata("Please use Kibana feature privileges instead"), null)) - .put(KibanaUser.ROLE_NAME, new RoleDescriptor(KibanaUser.ROLE_NAME, - new String[] { - "monitor", "manage_index_templates", MonitoringBulkAction.NAME, "manage_saml", "manage_token", "manage_oidc", - InvalidateApiKeyAction.NAME, "grant_api_key", - GetBuiltinPrivilegesAction.NAME, "delegate_pki", GetLifecycleAction.NAME, PutLifecycleAction.NAME, - // To facilitate ML UI functionality being controlled using Kibana security privileges - "manage_ml", - // The symbolic constant for this one is in SecurityActionMapper, so not accessible from X-Pack core - "cluster:admin/analyze", - // To facilitate using the file uploader functionality - "monitor_text_structure", - // To cancel tasks and delete async searches - "cancel_task" - }, - new RoleDescriptor.IndicesPrivileges[] { - RoleDescriptor.IndicesPrivileges.builder() - .indices(".kibana*", ".reporting-*").privileges("all").build(), - RoleDescriptor.IndicesPrivileges.builder() - .indices(".monitoring-*").privileges("read", "read_cross_cluster").build(), - RoleDescriptor.IndicesPrivileges.builder() - .indices(".management-beats").privileges("create_index", "read", "write").build(), - // To facilitate ML UI functionality being controlled using Kibana security privileges - RoleDescriptor.IndicesPrivileges.builder() - .indices(".ml-anomalies*", ".ml-stats-*") - .privileges("read").build(), - RoleDescriptor.IndicesPrivileges.builder().indices(".ml-annotations*", ".ml-notifications*") - .privileges("read", "write").build(), - // APM agent configuration - RoleDescriptor.IndicesPrivileges.builder() - .indices(".apm-agent-configuration").privileges("all").build(), - // APM custom link index creation - RoleDescriptor.IndicesPrivileges.builder() - .indices(".apm-custom-link").privileges("all").build(), - // APM telemetry queries APM indices in kibana task runner - RoleDescriptor.IndicesPrivileges.builder() - .indices("apm-*") - .privileges("read", "read_cross_cluster").build(), - // Data telemetry reads mappings, metadata and stats of indices - RoleDescriptor.IndicesPrivileges.builder() - .indices("*") - .privileges("view_index_metadata", "monitor").build(), - // Endpoint diagnostic information. Kibana reads from these indices to send telemetry - RoleDescriptor.IndicesPrivileges.builder() - .indices(".logs-endpoint.diagnostic.collection-*") - .privileges("read").build(), - // Fleet Server indices. Kibana create this indice before Fleet Server use them. - // Fleet Server indices. Kibana read and write to this indice to manage Elastic Agents - RoleDescriptor.IndicesPrivileges.builder() - .indices(".fleet*") - .privileges("all").build(), - // Legacy "Alerts as data" index. Kibana user will create this index. - // Kibana user will read / write to these indices - RoleDescriptor.IndicesPrivileges.builder() - .indices(ReservedRolesStore.LEGACY_ALERTS_INDEX) - .privileges("all").build(), - // "Alerts as data" index. Kibana user will create this index. - // Kibana user will read / write to these indices - RoleDescriptor.IndicesPrivileges.builder() - .indices(ReservedRolesStore.ALERTS_INDEX) - .privileges("all").build(), - // Endpoint / Fleet policy responses. Kibana requires read access to send telemetry - RoleDescriptor.IndicesPrivileges.builder() - .indices("metrics-endpoint.policy-*") - .privileges("read").build(), - // Endpoint metrics. Kibana requires read access to send telemetry - RoleDescriptor.IndicesPrivileges.builder() - .indices("metrics-endpoint.metrics-*") - .privileges("read").build() - }, - null, - new ConfigurableClusterPrivilege[] { new ManageApplicationPrivileges(Collections.singleton("kibana-*")) }, - null, MetadataUtils.DEFAULT_RESERVED_METADATA, null)) + .put(KibanaSystemUser.ROLE_NAME, kibanaSystemUserRole(KibanaSystemUser.ROLE_NAME)) .put("logstash_system", new RoleDescriptor("logstash_system", new String[] { "monitor", MonitoringBulkAction.NAME}, null, null, MetadataUtils.DEFAULT_RESERVED_METADATA)) .put("beats_admin", new RoleDescriptor("beats_admin", @@ -434,6 +363,81 @@ private static RoleDescriptor kibanaAdminUser(String name, Map m null, null, metadata, null); } + public static RoleDescriptor kibanaSystemUserRole(String name) { + return new RoleDescriptor(name, + new String[] { + "monitor", "manage_index_templates", MonitoringBulkAction.NAME, "manage_saml", "manage_token", "manage_oidc", + InvalidateApiKeyAction.NAME, "grant_api_key", + GetBuiltinPrivilegesAction.NAME, "delegate_pki", GetLifecycleAction.NAME, PutLifecycleAction.NAME, + // To facilitate ML UI functionality being controlled using Kibana security privileges + "manage_ml", + // The symbolic constant for this one is in SecurityActionMapper, so not accessible from X-Pack core + "cluster:admin/analyze", + // To facilitate using the file uploader functionality + "monitor_text_structure", + // To cancel tasks and delete async searches + "cancel_task" + }, + new RoleDescriptor.IndicesPrivileges[] { + RoleDescriptor.IndicesPrivileges.builder() + .indices(".kibana*", ".reporting-*").privileges("all").build(), + RoleDescriptor.IndicesPrivileges.builder() + .indices(".monitoring-*").privileges("read", "read_cross_cluster").build(), + RoleDescriptor.IndicesPrivileges.builder() + .indices(".management-beats").privileges("create_index", "read", "write").build(), + // To facilitate ML UI functionality being controlled using Kibana security privileges + RoleDescriptor.IndicesPrivileges.builder() + .indices(".ml-anomalies*", ".ml-stats-*") + .privileges("read").build(), + RoleDescriptor.IndicesPrivileges.builder().indices(".ml-annotations*", ".ml-notifications*") + .privileges("read", "write").build(), + // APM agent configuration + RoleDescriptor.IndicesPrivileges.builder() + .indices(".apm-agent-configuration").privileges("all").build(), + // APM custom link index creation + RoleDescriptor.IndicesPrivileges.builder() + .indices(".apm-custom-link").privileges("all").build(), + // APM telemetry queries APM indices in kibana task runner + RoleDescriptor.IndicesPrivileges.builder() + .indices("apm-*") + .privileges("read", "read_cross_cluster").build(), + // Data telemetry reads mappings, metadata and stats of indices + RoleDescriptor.IndicesPrivileges.builder() + .indices("*") + .privileges("view_index_metadata", "monitor").build(), + // Endpoint diagnostic information. Kibana reads from these indices to send telemetry + RoleDescriptor.IndicesPrivileges.builder() + .indices(".logs-endpoint.diagnostic.collection-*") + .privileges("read").build(), + // Fleet Server indices. Kibana create this indice before Fleet Server use them. + // Fleet Server indices. Kibana read and write to this indice to manage Elastic Agents + RoleDescriptor.IndicesPrivileges.builder() + .indices(".fleet*") + .privileges("all").build(), + // Legacy "Alerts as data" index. Kibana user will create this index. + // Kibana user will read / write to these indices + RoleDescriptor.IndicesPrivileges.builder() + .indices(ReservedRolesStore.LEGACY_ALERTS_INDEX) + .privileges("all").build(), + // "Alerts as data" index. Kibana user will create this index. + // Kibana user will read / write to these indices + RoleDescriptor.IndicesPrivileges.builder() + .indices(ReservedRolesStore.ALERTS_INDEX) + .privileges("all").build(), + // Endpoint / Fleet policy responses. Kibana requires read access to send telemetry + RoleDescriptor.IndicesPrivileges.builder() + .indices("metrics-endpoint.policy-*") + .privileges("read").build(), + // Endpoint metrics. Kibana requires read access to send telemetry + RoleDescriptor.IndicesPrivileges.builder() + .indices("metrics-endpoint.metrics-*") + .privileges("read").build() + }, + null, + new ConfigurableClusterPrivilege[] { new ManageApplicationPrivileges(Collections.singleton("kibana-*")) }, + null, MetadataUtils.DEFAULT_RESERVED_METADATA, null); + } + public static boolean isReserved(String role) { return RESERVED_ROLES.containsKey(role) || UsernamesField.SYSTEM_ROLE.equals(role) || UsernamesField.XPACK_ROLE.equals(role) || UsernamesField.ASYNC_SEARCH_ROLE.equals(role); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/enrollment/KibanaEnrollmentResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/enrollment/KibanaEnrollmentResponseTests.java index 1868a2365a121..eacaf8d4f1325 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/enrollment/KibanaEnrollmentResponseTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/enrollment/KibanaEnrollmentResponseTests.java @@ -40,7 +40,7 @@ public void testSerialization() throws IOException{ try (StreamInput in = out.bytes().streamInput()) { KibanaEnrollmentResponse serialized = new KibanaEnrollmentResponse(in); assertThat(response.getHttpCa(), is(serialized.getHttpCa())); - assertThat(response.getPassword(), is(serialized.getPassword())); + assertThat(response.getToken(), is(serialized.getToken())); } } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportKibanaEnrollmentAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportKibanaEnrollmentAction.java index 1ca47ab7a8911..c3f5d36dff8f4 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportKibanaEnrollmentAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportKibanaEnrollmentAction.java @@ -16,28 +16,25 @@ import org.elasticsearch.client.Client; import org.elasticsearch.client.OriginSettingClient; import org.elasticsearch.common.inject.Inject; -import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.core.Tuple; import org.elasticsearch.env.Environment; import org.elasticsearch.tasks.Task; import org.elasticsearch.transport.TransportService; -import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.security.action.enrollment.KibanaEnrollmentAction; import org.elasticsearch.xpack.core.security.action.enrollment.KibanaEnrollmentRequest; import org.elasticsearch.xpack.core.security.action.enrollment.KibanaEnrollmentResponse; -import org.elasticsearch.xpack.core.security.action.user.ChangePasswordAction; -import org.elasticsearch.xpack.core.security.action.user.ChangePasswordRequest; -import org.elasticsearch.xpack.core.security.action.user.ChangePasswordRequestBuilder; -import org.elasticsearch.xpack.core.security.authc.support.Hasher; +import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenAction; +import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenRequest; import org.elasticsearch.xpack.core.ssl.KeyConfig; import org.elasticsearch.xpack.core.ssl.SSLService; import org.elasticsearch.xpack.core.ssl.StoreKeyConfig; import static org.elasticsearch.xpack.core.ClientHelper.SECURITY_ORIGIN; -import java.security.SecureRandom; import java.security.cert.CertificateEncodingException; import java.security.cert.X509Certificate; +import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.util.Base64; import java.util.List; import java.util.stream.Collectors; @@ -58,7 +55,6 @@ public class TransportKibanaEnrollmentAction extends HandledTransportAction { - logger.debug("Successfully set the password for user [kibana_system] during kibana enrollment"); - listener.onResponse(new KibanaEnrollmentResponse(new SecureString(password), httpCa)); - }, e -> listener.onFailure(new ElasticsearchException("Failed to set the password for user [kibana_system]", e)))); + final CreateServiceAccountTokenRequest createServiceAccountTokenRequest = + new CreateServiceAccountTokenRequest("elastic", "kibana-system", getTokenName()); + client.execute(CreateServiceAccountTokenAction.INSTANCE, createServiceAccountTokenRequest, ActionListener.wrap(response -> { + logger.debug("Successfully created credentials for the [kibana-system] service account during kibana enrollment"); + listener.onResponse(new KibanaEnrollmentResponse(response.getValue(), httpCa)); + }, e -> listener.onFailure( + new ElasticsearchException("Failed to create credentials for the [kibana-system] service account", e)))); } - } - private char[] generateKibanaSystemPassword() { - final char[] passwordChars = ("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789~!@#$%^&*-_=+?").toCharArray(); - final SecureRandom secureRandom = new SecureRandom(); - int passwordLength = 14; - char[] characters = new char[passwordLength]; - for (int i = 0; i < passwordLength; ++i) { - characters[i] = passwordChars[secureRandom.nextInt(passwordChars.length)]; - } - return characters; + protected static String getTokenName(){ + final ZonedDateTime autoConfigDate = ZonedDateTime.now(ZoneOffset.UTC); + final String prefix = "enroll-process-token-"; + return prefix + autoConfigDate.toInstant().getEpochSecond(); } - } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccounts.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccounts.java index 75d0ad260c37a..bf03f4cb3368d 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccounts.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccounts.java @@ -9,6 +9,7 @@ import org.elasticsearch.common.Strings; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; +import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; import org.elasticsearch.xpack.core.security.user.User; import java.util.List; @@ -19,7 +20,7 @@ final class ElasticServiceAccounts { - static final String NAMESPACE = "elastic"; + public static final String NAMESPACE = "elastic"; private static final ServiceAccount FLEET_ACCOUNT = new ElasticServiceAccount("fleet-server", new RoleDescriptor( @@ -43,8 +44,10 @@ final class ElasticServiceAccounts { null, null )); + private static final ServiceAccount KIBANA_SYSTEM_ACCOUNT = + new ElasticServiceAccount("kibana-system", ReservedRolesStore.kibanaSystemUserRole(NAMESPACE + "/kibana-system")); - static final Map ACCOUNTS = List.of(FLEET_ACCOUNT).stream() + static final Map ACCOUNTS = List.of(FLEET_ACCOUNT, KIBANA_SYSTEM_ACCOUNT).stream() .collect(Collectors.toMap(a -> a.id().asPrincipal(), Function.identity()));; private ElasticServiceAccounts() {} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/enrollment/TransportKibanaEnrollmentActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/enrollment/TransportKibanaEnrollmentActionTests.java index fdfedc7acfabc..678422a7ac29b 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/enrollment/TransportKibanaEnrollmentActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/enrollment/TransportKibanaEnrollmentActionTests.java @@ -9,12 +9,11 @@ import org.elasticsearch.ElasticsearchException; import org.elasticsearch.action.ActionListener; -import org.elasticsearch.action.ActionResponse; import org.elasticsearch.action.support.ActionFilters; import org.elasticsearch.action.support.PlainActionFuture; import org.elasticsearch.client.Client; -import org.elasticsearch.client.ValidationException; import org.elasticsearch.common.settings.MockSecureSettings; +import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.env.Environment; @@ -25,8 +24,9 @@ import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.core.security.action.enrollment.KibanaEnrollmentRequest; import org.elasticsearch.xpack.core.security.action.enrollment.KibanaEnrollmentResponse; -import org.elasticsearch.xpack.core.security.action.user.ChangePasswordAction; -import org.elasticsearch.xpack.core.security.action.user.ChangePasswordRequest; +import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenAction; +import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenRequest; +import org.elasticsearch.xpack.core.security.action.service.CreateServiceAccountTokenResponse; import org.elasticsearch.xpack.core.ssl.SSLConfiguration; import org.elasticsearch.xpack.core.ssl.SSLService; import org.junit.Before; @@ -48,9 +48,11 @@ import static org.mockito.Mockito.when; public class TransportKibanaEnrollmentActionTests extends ESTestCase { - private List changePasswordRequests; + private List createServiceAccountTokenRequests; private TransportKibanaEnrollmentAction action; private Client client; + private static final String TOKEN_NAME = TransportKibanaEnrollmentAction.getTokenName(); + private static final SecureString TOKEN_VALUE = new SecureString("token-value".toCharArray()); @BeforeClass public static void muteInFips(){ @@ -58,7 +60,7 @@ public static void muteInFips(){ } @Before @SuppressWarnings("unchecked") public void setup() throws Exception { - changePasswordRequests = new ArrayList<>(); + createServiceAccountTokenRequests = new ArrayList<>(); final Environment env = mock(Environment.class); final Path tempDir = createTempDir(); final Path httpCaPath = tempDir.resolve("httpCa.p12"); @@ -80,12 +82,13 @@ public static void muteInFips(){ client = mock(Client.class); when(client.threadPool()).thenReturn(threadPool); doAnswer(invocation -> { - ChangePasswordRequest changePasswordRequest = (ChangePasswordRequest) invocation.getArguments()[1]; - changePasswordRequests.add(changePasswordRequest); - ActionListener listener = (ActionListener) invocation.getArguments()[2]; - listener.onResponse(ActionResponse.Empty.INSTANCE); + CreateServiceAccountTokenRequest createServiceAccountTokenRequest = + (CreateServiceAccountTokenRequest) invocation.getArguments()[1]; + createServiceAccountTokenRequests.add(createServiceAccountTokenRequest); + ActionListener listener = (ActionListener) invocation.getArguments()[2]; + listener.onResponse(CreateServiceAccountTokenResponse.created(TOKEN_NAME, TOKEN_VALUE)); return null; - }).when(client).execute(eq(ChangePasswordAction.INSTANCE), any(), any()); + }).when(client).execute(eq(CreateServiceAccountTokenAction.INSTANCE), any(), any()); final TransportService transportService = new TransportService(Settings.EMPTY, mock(Transport.class), @@ -98,6 +101,7 @@ public static void muteInFips(){ } public void testKibanaEnrollment() { + assertThat(TOKEN_NAME, startsWith("enroll-process-token-")); final KibanaEnrollmentRequest request = new KibanaEnrollmentRequest(); final PlainActionFuture future = new PlainActionFuture<>(); action.doExecute(mock(Task.class), request, future); @@ -105,22 +109,22 @@ public void testKibanaEnrollment() { assertThat(response.getHttpCa(), startsWith("MIIDSjCCAjKgAwIBAgIVALCgZXvbceUrjJaQMheDCX0kXnRJMA0GCSqGSIb3DQEBCwUAMDQxMjAw" + "BgNVBAMTKUVsYXN0aWMgQ2VydGlmaWNhdGUgVG9vbCBBdXRvZ2VuZXJhdGVkIENBMB4XDTIxMDQyODEyNTY0MVoXDTI0MDQyNzEyNTY0MVowNDEyMDAGA1UEA" + "xMpRWxhc3RpYyBDZXJ0aWZpY2F0ZSBUb29sIEF1dG9nZW5lcmF0ZWQgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCCJbOU4JvxDD/F")); - assertNotNull(response.getPassword()); - assertThat(changePasswordRequests.size(), equalTo(1)); + assertThat(response.getToken(), equalTo(TOKEN_VALUE)); + assertThat(createServiceAccountTokenRequests.size(), equalTo(1)); } - public void testKibanaEnrollmentFailedPasswordChange() { + public void testKibanaEnrollmentFailedTokenCreation() { // Override change password mock doAnswer(invocation -> { @SuppressWarnings("unchecked") - ActionListener listener = (ActionListener) invocation.getArguments()[2]; - listener.onFailure(new ValidationException()); + ActionListener listener = (ActionListener) invocation.getArguments()[2]; + listener.onFailure(new IllegalStateException()); return null; - }).when(client).execute(eq(ChangePasswordAction.INSTANCE), any(), any()); + }).when(client).execute(eq(CreateServiceAccountTokenAction.INSTANCE), any(), any()); final KibanaEnrollmentRequest request = new KibanaEnrollmentRequest(); final PlainActionFuture future = new PlainActionFuture<>(); action.doExecute(mock(Task.class), request, future); ElasticsearchException e = expectThrows(ElasticsearchException.class, future::actionGet); - assertThat(e.getDetailedMessage(), containsString("Failed to set the password for user [kibana_system]")); + assertThat(e.getDetailedMessage(), containsString("Failed to create credentials for the [kibana-system] service account")); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccountsTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccountsTests.java index bf40fd4e351ab..852d2a02c9b22 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccountsTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ElasticServiceAccountsTests.java @@ -7,12 +7,21 @@ package org.elasticsearch.xpack.security.authc.service; +import org.elasticsearch.action.admin.cluster.health.ClusterHealthAction; +import org.elasticsearch.action.admin.cluster.reroute.ClusterRerouteAction; +import org.elasticsearch.action.admin.cluster.settings.ClusterUpdateSettingsAction; +import org.elasticsearch.action.admin.cluster.state.ClusterStateAction; +import org.elasticsearch.action.admin.cluster.stats.ClusterStatsAction; import org.elasticsearch.action.admin.indices.create.AutoCreateAction; import org.elasticsearch.action.admin.indices.create.CreateIndexAction; import org.elasticsearch.action.admin.indices.delete.DeleteIndexAction; +import org.elasticsearch.action.admin.indices.get.GetIndexAction; +import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsAction; import org.elasticsearch.action.admin.indices.mapping.put.AutoPutMappingAction; import org.elasticsearch.action.admin.indices.settings.put.UpdateSettingsAction; import org.elasticsearch.action.admin.indices.stats.IndicesStatsAction; +import org.elasticsearch.action.admin.indices.template.get.GetIndexTemplatesAction; +import org.elasticsearch.action.admin.indices.template.put.PutIndexTemplateAction; import org.elasticsearch.action.bulk.BulkAction; import org.elasticsearch.action.delete.DeleteAction; import org.elasticsearch.action.get.GetAction; @@ -20,21 +29,111 @@ import org.elasticsearch.action.index.IndexAction; import org.elasticsearch.action.search.MultiSearchAction; import org.elasticsearch.action.search.SearchAction; +import org.elasticsearch.action.update.UpdateAction; import org.elasticsearch.cluster.metadata.IndexAbstraction; import org.elasticsearch.common.Strings; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.transport.TransportRequest; +import org.elasticsearch.xpack.core.ilm.action.DeleteLifecycleAction; +import org.elasticsearch.xpack.core.ilm.action.GetLifecycleAction; +import org.elasticsearch.xpack.core.ilm.action.PutLifecycleAction; +import org.elasticsearch.xpack.core.ilm.action.StartILMAction; +import org.elasticsearch.xpack.core.ilm.action.StopILMAction; +import org.elasticsearch.xpack.core.ml.action.CloseJobAction; +import org.elasticsearch.xpack.core.ml.action.DeleteCalendarAction; +import org.elasticsearch.xpack.core.ml.action.DeleteCalendarEventAction; +import org.elasticsearch.xpack.core.ml.action.DeleteDatafeedAction; +import org.elasticsearch.xpack.core.ml.action.DeleteExpiredDataAction; +import org.elasticsearch.xpack.core.ml.action.DeleteFilterAction; +import org.elasticsearch.xpack.core.ml.action.DeleteForecastAction; +import org.elasticsearch.xpack.core.ml.action.DeleteJobAction; +import org.elasticsearch.xpack.core.ml.action.DeleteModelSnapshotAction; +import org.elasticsearch.xpack.core.ml.action.DeleteTrainedModelAction; +import org.elasticsearch.xpack.core.ml.action.EstimateModelMemoryAction; +import org.elasticsearch.xpack.core.ml.action.EvaluateDataFrameAction; +import org.elasticsearch.xpack.core.ml.action.ExplainDataFrameAnalyticsAction; +import org.elasticsearch.xpack.core.ml.action.FinalizeJobExecutionAction; +import org.elasticsearch.xpack.core.ml.action.FlushJobAction; +import org.elasticsearch.xpack.core.ml.action.ForecastJobAction; +import org.elasticsearch.xpack.core.ml.action.GetBucketsAction; +import org.elasticsearch.xpack.core.ml.action.GetCalendarEventsAction; +import org.elasticsearch.xpack.core.ml.action.GetCalendarsAction; +import org.elasticsearch.xpack.core.ml.action.GetCategoriesAction; +import org.elasticsearch.xpack.core.ml.action.GetDataFrameAnalyticsAction; +import org.elasticsearch.xpack.core.ml.action.GetDataFrameAnalyticsStatsAction; +import org.elasticsearch.xpack.core.ml.action.GetDatafeedsAction; +import org.elasticsearch.xpack.core.ml.action.GetDatafeedsStatsAction; +import org.elasticsearch.xpack.core.ml.action.GetFiltersAction; +import org.elasticsearch.xpack.core.ml.action.GetInfluencersAction; +import org.elasticsearch.xpack.core.ml.action.GetJobsAction; +import org.elasticsearch.xpack.core.ml.action.GetJobsStatsAction; +import org.elasticsearch.xpack.core.ml.action.GetModelSnapshotsAction; +import org.elasticsearch.xpack.core.ml.action.GetOverallBucketsAction; +import org.elasticsearch.xpack.core.ml.action.GetRecordsAction; +import org.elasticsearch.xpack.core.ml.action.GetTrainedModelsAction; +import org.elasticsearch.xpack.core.ml.action.GetTrainedModelsStatsAction; +import org.elasticsearch.xpack.core.ml.action.InternalInferModelAction; +import org.elasticsearch.xpack.core.ml.action.IsolateDatafeedAction; +import org.elasticsearch.xpack.core.ml.action.KillProcessAction; +import org.elasticsearch.xpack.core.ml.action.MlInfoAction; +import org.elasticsearch.xpack.core.ml.action.OpenJobAction; +import org.elasticsearch.xpack.core.ml.action.PersistJobAction; +import org.elasticsearch.xpack.core.ml.action.PostCalendarEventsAction; +import org.elasticsearch.xpack.core.ml.action.PostDataAction; +import org.elasticsearch.xpack.core.ml.action.PreviewDatafeedAction; +import org.elasticsearch.xpack.core.ml.action.PutCalendarAction; +import org.elasticsearch.xpack.core.ml.action.PutDataFrameAnalyticsAction; +import org.elasticsearch.xpack.core.ml.action.PutDatafeedAction; +import org.elasticsearch.xpack.core.ml.action.PutFilterAction; +import org.elasticsearch.xpack.core.ml.action.PutJobAction; +import org.elasticsearch.xpack.core.ml.action.PutTrainedModelAction; +import org.elasticsearch.xpack.core.ml.action.RevertModelSnapshotAction; +import org.elasticsearch.xpack.core.ml.action.SetUpgradeModeAction; +import org.elasticsearch.xpack.core.ml.action.StartDataFrameAnalyticsAction; +import org.elasticsearch.xpack.core.ml.action.StartDatafeedAction; +import org.elasticsearch.xpack.core.ml.action.StopDataFrameAnalyticsAction; +import org.elasticsearch.xpack.core.ml.action.StopDatafeedAction; +import org.elasticsearch.xpack.core.ml.action.UpdateCalendarJobAction; +import org.elasticsearch.xpack.core.ml.action.UpdateDatafeedAction; +import org.elasticsearch.xpack.core.ml.action.UpdateFilterAction; +import org.elasticsearch.xpack.core.ml.action.UpdateJobAction; +import org.elasticsearch.xpack.core.ml.action.UpdateModelSnapshotAction; +import org.elasticsearch.xpack.core.ml.action.UpdateProcessAction; +import org.elasticsearch.xpack.core.ml.action.ValidateDetectorAction; +import org.elasticsearch.xpack.core.ml.action.ValidateJobConfigAction; +import org.elasticsearch.xpack.core.monitoring.action.MonitoringBulkAction; import org.elasticsearch.xpack.core.security.action.CreateApiKeyAction; import org.elasticsearch.xpack.core.security.action.CreateApiKeyRequest; +import org.elasticsearch.xpack.core.security.action.DelegatePkiAuthenticationAction; import org.elasticsearch.xpack.core.security.action.GetApiKeyAction; import org.elasticsearch.xpack.core.security.action.GetApiKeyRequest; +import org.elasticsearch.xpack.core.security.action.GrantApiKeyAction; import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyAction; import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyRequest; +import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesAction; +import org.elasticsearch.xpack.core.security.action.privilege.DeletePrivilegesRequest; +import org.elasticsearch.xpack.core.security.action.privilege.GetBuiltinPrivilegesAction; +import org.elasticsearch.xpack.core.security.action.privilege.GetPrivilegesAction; +import org.elasticsearch.xpack.core.security.action.privilege.GetPrivilegesRequest; +import org.elasticsearch.xpack.core.security.action.privilege.PutPrivilegesAction; +import org.elasticsearch.xpack.core.security.action.privilege.PutPrivilegesRequest; +import org.elasticsearch.xpack.core.security.action.saml.SamlAuthenticateAction; +import org.elasticsearch.xpack.core.security.action.saml.SamlPrepareAuthenticationAction; +import org.elasticsearch.xpack.core.security.action.token.CreateTokenAction; +import org.elasticsearch.xpack.core.security.action.token.InvalidateTokenAction; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.permission.Role; +import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor; +import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; +import org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames; import org.elasticsearch.xpack.core.security.user.User; +import org.elasticsearch.xpack.core.textstructure.action.FindStructureAction; import org.elasticsearch.xpack.security.authc.service.ElasticServiceAccounts.ElasticServiceAccount; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -46,6 +145,280 @@ public class ElasticServiceAccountsTests extends ESTestCase { + public void testKibanaSystemPrivileges() { + final String readCrossClusterName = "internal:transport/proxy/indices:data/read/query"; + final Role role = Role.builder(ElasticServiceAccounts.ACCOUNTS.get("elastic/kibana-system").roleDescriptor(), null).build(); + final TransportRequest request = mock(TransportRequest.class); + final Authentication authentication = mock(Authentication.class); + assertThat(role.cluster().check(ClusterHealthAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(ClusterStateAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(ClusterStatsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(PutIndexTemplateAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetIndexTemplatesAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(ClusterRerouteAction.NAME, request, authentication), is(false)); + assertThat(role.cluster().check(ClusterUpdateSettingsAction.NAME, request, authentication), is(false)); + assertThat(role.cluster().check(MonitoringBulkAction.NAME, request, authentication), is(true)); + + // ILM + assertThat(role.cluster().check(GetLifecycleAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(PutLifecycleAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(DeleteLifecycleAction.NAME, request, authentication), is(false)); + assertThat(role.cluster().check(StartILMAction.NAME, request, authentication), is(false)); + assertThat(role.cluster().check(StopILMAction.NAME, request, authentication), is(false)); + + // SAML and token + assertThat(role.cluster().check(SamlPrepareAuthenticationAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(SamlAuthenticateAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(InvalidateTokenAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(CreateTokenAction.NAME, request, authentication), is(true)); + + // API keys + assertThat(role.cluster().check(InvalidateApiKeyAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GrantApiKeyAction.NAME, request, authentication), is(true)); + + // ML + assertRoleHasManageMl(role); + + // Text Structure + assertThat(role.cluster().check(FindStructureAction.NAME, request, authentication), is(true)); + + // Application Privileges + DeletePrivilegesRequest deleteKibanaPrivileges = new DeletePrivilegesRequest("kibana-.kibana", new String[]{ "all", "read" }); + DeletePrivilegesRequest deleteLogstashPrivileges = new DeletePrivilegesRequest("logstash", new String[]{ "all", "read" }); + assertThat(role.cluster().check(DeletePrivilegesAction.NAME, deleteKibanaPrivileges, authentication), is(true)); + assertThat(role.cluster().check(DeletePrivilegesAction.NAME, deleteLogstashPrivileges, authentication), is(false)); + + GetPrivilegesRequest getKibanaPrivileges = new GetPrivilegesRequest(); + getKibanaPrivileges.application("kibana-.kibana-sales"); + GetPrivilegesRequest getApmPrivileges = new GetPrivilegesRequest(); + getApmPrivileges.application("apm"); + assertThat(role.cluster().check(GetPrivilegesAction.NAME, getKibanaPrivileges, authentication), is(true)); + assertThat(role.cluster().check(GetPrivilegesAction.NAME, getApmPrivileges, authentication), is(false)); + + PutPrivilegesRequest putKibanaPrivileges = new PutPrivilegesRequest(); + putKibanaPrivileges.setPrivileges(Collections.singletonList(new ApplicationPrivilegeDescriptor( + "kibana-.kibana-" + randomAlphaOfLengthBetween(2,6), "all", Collections.emptySet(), Collections.emptyMap()))); + PutPrivilegesRequest putSwiftypePrivileges = new PutPrivilegesRequest(); + putSwiftypePrivileges.setPrivileges(Collections.singletonList(new ApplicationPrivilegeDescriptor( + "swiftype-kibana" , "all", Collections.emptySet(), Collections.emptyMap()))); + assertThat(role.cluster().check(PutPrivilegesAction.NAME, putKibanaPrivileges, authentication), is(true)); + assertThat(role.cluster().check(PutPrivilegesAction.NAME, putSwiftypePrivileges, authentication), is(false)); + + assertThat(role.cluster().check(GetBuiltinPrivilegesAction.NAME, request, authentication), is(true)); + + // Everything else + assertThat(role.runAs().check(randomAlphaOfLengthBetween(1, 12)), is(false)); + assertThat(role.cluster().check(DelegatePkiAuthenticationAction.NAME, request, authentication), is(true)); + + assertThat(role.indices().allowedIndicesMatcher(IndexAction.NAME).test(mockIndexAbstraction("foo")), is(false)); + assertThat(role.indices().allowedIndicesMatcher(IndexAction.NAME).test(mockIndexAbstraction(".reporting")), is(false)); + assertThat(role.indices().allowedIndicesMatcher("indices:foo").test(mockIndexAbstraction(randomAlphaOfLengthBetween(8, 24))), + is(false)); + + Arrays.asList( + ".kibana", + ".kibana-devnull", + ".reporting-" + randomAlphaOfLength(randomIntBetween(0, 13)), + ".apm-agent-configuration", + ".apm-custom-link", + ReservedRolesStore.LEGACY_ALERTS_INDEX + randomAlphaOfLength(randomIntBetween(0, 13)), + ReservedRolesStore.ALERTS_INDEX + randomAlphaOfLength(randomIntBetween(0, 13)) + ).forEach((index) -> { + logger.info("index name [{}]", index); + assertThat(role.indices().allowedIndicesMatcher("indices:foo").test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher("indices:bar").test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(DeleteIndexAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(CreateIndexAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(IndexAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(DeleteAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(UpdateSettingsAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(SearchAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(MultiSearchAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(GetAction.NAME).test(mockIndexAbstraction(index)), is(true)); + // inherits from 'all' + assertThat(role.indices().allowedIndicesMatcher(readCrossClusterName).test(mockIndexAbstraction(index)), is(true)); + }); + + // read-only index access, including cross cluster + Arrays.asList(".monitoring-" + randomAlphaOfLength(randomIntBetween(0, 13))).forEach((index) -> { + logger.info("index name [{}]", index); + assertThat(role.indices().allowedIndicesMatcher("indices:foo").test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher("indices:bar").test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(DeleteIndexAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(CreateIndexAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(IndexAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(DeleteAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(UpdateSettingsAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(SearchAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(MultiSearchAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(GetAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(readCrossClusterName).test(mockIndexAbstraction(index)), is(true)); + }); + + // read-only index access, excluding cross cluster + Arrays.asList( + ".ml-anomalies-" + randomAlphaOfLength(randomIntBetween(0, 13)), + ".ml-stats-" + randomAlphaOfLength(randomIntBetween(0, 13)) + ).forEach((index) -> { + logger.trace("index name [{}]", index); + assertThat(role.indices().allowedIndicesMatcher("indices:foo").test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher("indices:bar").test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(DeleteIndexAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(CreateIndexAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(IndexAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(DeleteAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(UpdateSettingsAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(SearchAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(MultiSearchAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(GetAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(readCrossClusterName).test(mockIndexAbstraction(index)), is(false)); + }); + + // read/write index access, excluding cross cluster + Arrays.asList( + ".ml-annotations-" + randomAlphaOfLength(randomIntBetween(0, 13)), + ".ml-notifications-" + randomAlphaOfLength(randomIntBetween(0, 13)) + ).forEach((index) -> { + logger.trace("index name [{}]", index); + assertThat(role.indices().allowedIndicesMatcher("indices:foo").test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher("indices:bar").test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(DeleteIndexAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(CreateIndexAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(IndexAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(DeleteAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(UpdateSettingsAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(SearchAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(MultiSearchAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(GetAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(readCrossClusterName).test(mockIndexAbstraction(index)), is(false)); + }); + + // read-only indices for APM telemetry + Arrays.asList("apm-*").forEach((index) -> { + assertThat(role.indices().allowedIndicesMatcher("indices:foo").test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher("indices:bar").test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(DeleteIndexAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(GetIndexAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(CreateIndexAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(IndexAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(DeleteAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(UpdateSettingsAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(SearchAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(MultiSearchAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(GetAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(readCrossClusterName).test(mockIndexAbstraction(index)), is(true)); + }); + + // read-only indices for Endpoint diagnostic information + Arrays.asList(".logs-endpoint.diagnostic.collection-" + randomAlphaOfLength(randomIntBetween(0, 13))).forEach((index) -> { + assertThat(role.indices().allowedIndicesMatcher("indices:foo").test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher("indices:bar").test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(DeleteIndexAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(GetIndexAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(CreateIndexAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(IndexAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(DeleteAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(UpdateSettingsAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(SearchAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(MultiSearchAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(GetAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(readCrossClusterName).test(mockIndexAbstraction(index)), is(false)); + }); + + Arrays.asList( + ".fleet", + ".fleet-agents", + ".fleet-actions", + ".fleet-enrollment-api-keys", + ".fleet-policies", + ".fleet-actions-results", + ".fleet-servers" + ).forEach((index) -> { + assertThat(role.indices().allowedIndicesMatcher("indices:foo").test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher("indices:bar").test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(DeleteIndexAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(GetIndexAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(CreateIndexAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(IndexAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(DeleteAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(UpdateSettingsAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(SearchAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(MultiSearchAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(GetAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(readCrossClusterName).test(mockIndexAbstraction(index)), is(true)); + }); + + + // Data telemetry reads mappings, metadata and stats of indices + Arrays.asList(randomAlphaOfLengthBetween(8, 24), "packetbeat-*", "logs-*").forEach((index) -> { + logger.info("index name [{}]", index); + assertThat(role.indices().allowedIndicesMatcher(GetIndexAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(GetMappingsAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(IndicesStatsAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher("indices:foo").test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher("indices:bar").test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(DeleteIndexAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(CreateIndexAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(IndexAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(DeleteAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(UpdateSettingsAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(SearchAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(MultiSearchAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(GetAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(readCrossClusterName).test(mockIndexAbstraction(index)), is(false)); + }); + + // read-only datastream for Endpoint policy responses + Arrays.asList("metrics-endpoint.policy-" + randomAlphaOfLength(randomIntBetween(0, 13))).forEach((index) -> { + assertThat(role.indices().allowedIndicesMatcher("indices:foo").test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher("indices:bar").test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(DeleteIndexAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(GetIndexAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(CreateIndexAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(IndexAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(DeleteAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(UpdateSettingsAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(SearchAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(MultiSearchAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(GetAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(readCrossClusterName).test(mockIndexAbstraction(index)), is(false)); + }); + + // read-only datastream for Endpoint metrics + Arrays.asList("metrics-endpoint.metrics-" + randomAlphaOfLength(randomIntBetween(0, 13))).forEach((index) -> { + assertThat(role.indices().allowedIndicesMatcher("indices:foo").test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher("indices:bar").test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(DeleteIndexAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(GetIndexAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(CreateIndexAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(IndexAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(DeleteAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(UpdateSettingsAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(SearchAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(MultiSearchAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(GetAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(readCrossClusterName).test(mockIndexAbstraction(index)), is(false)); + }); + + // Beats management index + final String index = ".management-beats"; + assertThat(role.indices().allowedIndicesMatcher("indices:foo").test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher("indices:bar").test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(DeleteIndexAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(CreateIndexAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(IndexAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(DeleteAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(UpdateSettingsAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(SearchAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(MultiSearchAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(GetAction.NAME).test(mockIndexAbstraction(index)), is(true)); + assertThat(role.indices().allowedIndicesMatcher(readCrossClusterName).test(mockIndexAbstraction(index)), is(false)); + + assertNoAccessAllowed(role, RestrictedIndicesNames.RESTRICTED_NAMES); + assertNoAccessAllowed(role, RestrictedIndicesNames.ASYNC_SEARCH_PREFIX + randomAlphaOfLengthBetween(0, 2)); + + } + public void testElasticFleetPrivileges() { final Role role = Role.builder(ElasticServiceAccounts.ACCOUNTS.get("elastic/fleet-server").roleDescriptor(), null).build(); final Authentication authentication = mock(Authentication.class); @@ -130,4 +503,90 @@ private IndexAbstraction mockIndexAbstraction(String name) { IndexAbstraction.Type.ALIAS, IndexAbstraction.Type.DATA_STREAM)); return mock; } + + private void assertNoAccessAllowed(Role role, Collection indices) { + for (String index : indices) { + assertNoAccessAllowed(role, index); + } + } + + private void assertNoAccessAllowed(Role role, String index) { + assertThat(role.indices().allowedIndicesMatcher(DeleteIndexAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(CreateIndexAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(UpdateSettingsAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(SearchAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(GetAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(IndexAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(UpdateAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(DeleteAction.NAME).test(mockIndexAbstraction(index)), is(false)); + assertThat(role.indices().allowedIndicesMatcher(BulkAction.NAME).test(mockIndexAbstraction(index)), is(false)); + } + + private void assertRoleHasManageMl(Role role) { + final TransportRequest request = mock(TransportRequest.class); + final Authentication authentication = mock(Authentication.class); + + assertThat(role.cluster().check(CloseJobAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(DeleteCalendarAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(DeleteCalendarEventAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(DeleteDatafeedAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(DeleteExpiredDataAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(DeleteFilterAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(DeleteForecastAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(DeleteJobAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(DeleteModelSnapshotAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(DeleteTrainedModelAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(EstimateModelMemoryAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(EvaluateDataFrameAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(ExplainDataFrameAnalyticsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(FinalizeJobExecutionAction.NAME, request, authentication), is(false)); // internal use only + assertThat(role.cluster().check(FlushJobAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(ForecastJobAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetBucketsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetCalendarEventsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetCalendarsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetCategoriesAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetDatafeedsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetDatafeedsStatsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetDataFrameAnalyticsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetDataFrameAnalyticsStatsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetFiltersAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetInfluencersAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetJobsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetJobsStatsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetModelSnapshotsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetOverallBucketsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetRecordsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetTrainedModelsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(GetTrainedModelsStatsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(InternalInferModelAction.NAME, request, authentication), is(false)); // internal use only + assertThat(role.cluster().check(IsolateDatafeedAction.NAME, request, authentication), is(false)); // internal use only + assertThat(role.cluster().check(KillProcessAction.NAME, request, authentication), is(false)); // internal use only + assertThat(role.cluster().check(MlInfoAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(OpenJobAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(PersistJobAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(PostCalendarEventsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(PostDataAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(PreviewDatafeedAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(PutCalendarAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(PutDatafeedAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(PutDataFrameAnalyticsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(PutFilterAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(PutJobAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(PutTrainedModelAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(RevertModelSnapshotAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(SetUpgradeModeAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(StartDatafeedAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(StartDataFrameAnalyticsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(StopDatafeedAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(StopDataFrameAnalyticsAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(UpdateCalendarJobAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(UpdateDatafeedAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(UpdateFilterAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(UpdateJobAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(UpdateModelSnapshotAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(UpdateProcessAction.NAME, request, authentication), is(false)); // internal use only + assertThat(role.cluster().check(ValidateDetectorAction.NAME, request, authentication), is(true)); + assertThat(role.cluster().check(ValidateJobConfigAction.NAME, request, authentication), is(true)); + } } From 39d6b92c3ae0f88526981d7d4b7beb7a12a3ee64 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Wed, 11 Aug 2021 19:13:31 +0300 Subject: [PATCH 02/12] minor updates --- .../java/org/elasticsearch/client/EnrollmentIT.java | 1 - .../client/security/KibanaErnollmentResponseTests.java | 6 +++--- .../action/enrollment/KibanaEnrollmentResponseTests.java | 2 +- .../security/authc/service/ElasticServiceAccounts.java | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/client/rest-high-level/qa/ssl-enabled/src/javaRestTest/java/org/elasticsearch/client/EnrollmentIT.java b/client/rest-high-level/qa/ssl-enabled/src/javaRestTest/java/org/elasticsearch/client/EnrollmentIT.java index fd245a4d230f7..5fb5e5469b0ed 100644 --- a/client/rest-high-level/qa/ssl-enabled/src/javaRestTest/java/org/elasticsearch/client/EnrollmentIT.java +++ b/client/rest-high-level/qa/ssl-enabled/src/javaRestTest/java/org/elasticsearch/client/EnrollmentIT.java @@ -25,7 +25,6 @@ import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.notNullValue; -import static org.hamcrest.Matchers.startsWith; public class EnrollmentIT extends ESRestHighLevelClientTestCase { private static Path httpTrustStore; diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/KibanaErnollmentResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/KibanaErnollmentResponseTests.java index 70f676e7f973b..fe0107c393086 100644 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/KibanaErnollmentResponseTests.java +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/KibanaErnollmentResponseTests.java @@ -24,7 +24,7 @@ public class KibanaErnollmentResponseTests extends ESTestCase { public void testFromXContent() throws IOException { - final String token = randomAlphaOfLength(14); + final String token = randomAlphaOfLengthBetween(30, 40); final String httpCa = randomAlphaOfLength(50); final List nodesAddresses = randomList(2, 10, () -> buildNewFakeTransportAddress().toString()); @@ -39,7 +39,7 @@ public void testFromXContent() throws IOException { } public void testEqualsHashCode() { - final SecureString password = new SecureString(randomAlphaOfLength(14).toCharArray()); + final SecureString password = new SecureString(randomAlphaOfLengthBetween(30, 40).toCharArray()); final String httpCa = randomAlphaOfLength(50); KibanaEnrollmentResponse kibanaEnrollmentResponse = new KibanaEnrollmentResponse(password, httpCa); @@ -54,7 +54,7 @@ public void testEqualsHashCode() { private static KibanaEnrollmentResponse mutateTestItem(KibanaEnrollmentResponse original) { switch (randomIntBetween(0, 1)) { case 0: - return new KibanaEnrollmentResponse(new SecureString(randomAlphaOfLength(14).toCharArray()), + return new KibanaEnrollmentResponse(new SecureString(randomAlphaOfLengthBetween(30, 40).toCharArray()), original.getHttpCa()); case 1: return new KibanaEnrollmentResponse(original.getToken(), randomAlphaOfLength(51)); diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/enrollment/KibanaEnrollmentResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/enrollment/KibanaEnrollmentResponseTests.java index eacaf8d4f1325..854f2cd835b4f 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/enrollment/KibanaEnrollmentResponseTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/enrollment/KibanaEnrollmentResponseTests.java @@ -21,7 +21,7 @@ public class KibanaEnrollmentResponseTests extends AbstractXContentTestCase Date: Wed, 11 Aug 2021 22:04:20 +0300 Subject: [PATCH 03/12] fix tests --- .../service/ServiceAccountServiceTests.java | 4 +- .../test/service_accounts/10_basic.yml | 68 +++++++++++++++---- 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java index d2c5e25be76ee..2f48125778571 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java @@ -65,6 +65,7 @@ import java.util.stream.IntStream; import java.util.stream.Stream; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; @@ -121,7 +122,8 @@ public void stopThreadPool() { } public void testGetServiceAccountPrincipals() { - assertThat(ServiceAccountService.getServiceAccountPrincipals(), equalTo(Set.of("elastic/fleet-server"))); + assertThat(ServiceAccountService.getServiceAccountPrincipals(), + containsInAnyOrder("elastic/fleet-server", "elastic/kibana-system")); } public void testTryParseToken() throws IOException, IllegalAccessException { diff --git a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/service_accounts/10_basic.yml b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/service_accounts/10_basic.yml index 31dc539e11bfe..9af355e6bd912 100644 --- a/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/service_accounts/10_basic.yml +++ b/x-pack/plugin/src/yamlRestTest/resources/rest-api-spec/test/service_accounts/10_basic.yml @@ -10,21 +10,30 @@ teardown: security.delete_service_token: namespace: elastic service: fleet-server - name: api-token-1 + name: api-token-fleet + ignore: 404 + + - do: + security.delete_service_token: + namespace: elastic + service: kibana-system + name: api-token-kibana ignore: 404 --- "Test get service accounts": - do: security.get_service_accounts: {} - - length: { '': 1 } + - length: { '': 2 } - is_true: "elastic/fleet-server" + - is_true: "elastic/kibana-system" - do: security.get_service_accounts: namespace: elastic - - length: { '': 1 } + - length: { '': 2 } - is_true: "elastic/fleet-server" + - is_true: "elastic/kibana-system" - do: security.get_service_accounts: @@ -41,26 +50,36 @@ teardown: security.create_service_token: namespace: elastic service: fleet-server - name: api-token-1 + name: api-token-fleet - is_true: created - - match: { "token.name": "api-token-1" } - - set: { "token.value": service_token } + - match: { "token.name": "api-token-fleet" } + - set: { "token.value": service_token_fleet } + + - do: + security.create_service_token: + namespace: elastic + service: kibana-system + name: api-token-kibana + + - is_true: created + - match: { "token.name": "api-token-kibana" } + - set: { "token.value": service_token_kibana } - do: headers: - Authorization: Bearer ${service_token} + Authorization: Bearer ${service_token_fleet} security.authenticate: {} - match: { username: "elastic/fleet-server" } - match: { roles: [] } - match: { full_name: "Service account - elastic/fleet-server" } - - match: { "token.name": "api-token-1" } + - match: { "token.name": "api-token-fleet" } - do: catch: forbidden headers: - Authorization: Bearer ${service_token} + Authorization: Bearer ${service_token_fleet} security.delete_user: username: foo @@ -68,6 +87,16 @@ teardown: - match: error.reason: "action [cluster:admin/xpack/security/user/delete] is unauthorized for user [elastic/fleet-server], this action is granted by the cluster privileges [manage_security,all]" + - do: + headers: + Authorization: Bearer ${service_token_kibana} + security.authenticate: {} + + - match: { username: "elastic/kibana-system" } + - match: { roles: [] } + - match: { full_name: "Service account - elastic/kibana-system" } + - match: { "token.name": "api-token-kibana" } + - do: security.get_service_credentials: namespace: elastic @@ -75,7 +104,7 @@ teardown: - match: { "service_account": "elastic/fleet-server" } - match: { "count": 2 } - - match: { "tokens": { "api-token-1": {} } } + - match: { "tokens": { "api-token-fleet": {} } } - match: { "nodes_credentials._nodes.failed": 0 } - is_true: nodes_credentials.file_tokens.token1 - is_true: nodes_credentials.file_tokens.token1.nodes @@ -85,7 +114,15 @@ teardown: security.clear_cached_service_tokens: namespace: elastic service: fleet-server - name: api-token-1 + name: api-token-fleet + + - match: { _nodes.failed: 0 } + + - do: + security.clear_cached_service_tokens: + namespace: elastic + service: kibana-system + name: api-token-kibana - match: { _nodes.failed: 0 } @@ -93,9 +130,16 @@ teardown: security.clear_cached_service_tokens: namespace: elastic service: fleet-server - name: [ "api-token-1", "does-not-exist" ] + name: [ "api-token-fleet", "does-not-exist" ] - match: { _nodes.failed: 0 } + - do: + security.clear_cached_service_tokens: + namespace: elastic + service: kibana-system + name: [ "api-token-kibana", "does-not-exist-either" ] + + - match: { _nodes.failed: 0 } From 29cf973b335f1614dbd1bf68c636cd909116d852 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Wed, 11 Aug 2021 22:08:48 +0300 Subject: [PATCH 04/12] unused import --- .../xpack/security/authc/service/ServiceAccountServiceTests.java | 1 - 1 file changed, 1 deletion(-) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java index 2f48125778571..544345b358ce9 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/service/ServiceAccountServiceTests.java @@ -59,7 +59,6 @@ import java.util.List; import java.util.Locale; import java.util.Map; -import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.stream.Collectors; import java.util.stream.IntStream; From 0180b161f4367d9aac1e1fe4b14e34e1933d780e Mon Sep 17 00:00:00 2001 From: Adam Locke Date: Wed, 11 Aug 2021 15:15:43 -0400 Subject: [PATCH 05/12] Update service accounts overview to include kibana-system --- .../en/security/authentication/service-accounts.asciidoc | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/x-pack/docs/en/security/authentication/service-accounts.asciidoc b/x-pack/docs/en/security/authentication/service-accounts.asciidoc index 4907ce37d20d6..fc762a06c6a83 100644 --- a/x-pack/docs/en/security/authentication/service-accounts.asciidoc +++ b/x-pack/docs/en/security/authentication/service-accounts.asciidoc @@ -40,11 +40,15 @@ the format of `/`, where the `namespace` is a top-level grouping of service accounts, and `service` is the name of the service and must be unique within its namespace. -Service accounts are predefined in code. Currently, only one service account is available: +Service accounts are predefined in code. The following service accounts are +available: `elastic/fleet-server`:: The service account used by the {fleet} server to communicate with {es}. +`elastic/kibana-system`:: The service account used by {kib} to communicate with +{es}. + // tag::service-accounts-usage[] IMPORTANT: Do not attempt to use service accounts for authenticating individual users. Service accounts can only be authenticated with service tokens, which are From 2e9c380316453985cf395cfe2d69fee411e6583c Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Wed, 11 Aug 2021 22:53:11 +0300 Subject: [PATCH 06/12] fix more tests --- ...TransportGetServiceAccountActionTests.java | 36 +++++++++++++------ 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountActionTests.java index 77af6fe2ef889..f65ddf662bd78 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/service/TransportGetServiceAccountActionTests.java @@ -14,11 +14,15 @@ import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountRequest; import org.elasticsearch.xpack.core.security.action.service.GetServiceAccountResponse; +import org.elasticsearch.xpack.core.security.action.service.ServiceAccountInfo; import org.elasticsearch.xpack.security.authc.support.HttpTlsRuntimeCheck; import org.junit.Before; +import java.util.Arrays; import java.util.Collections; +import java.util.stream.Collectors; +import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.equalTo; import static org.mockito.Matchers.any; import static org.mockito.Mockito.doAnswer; @@ -43,23 +47,33 @@ public void init() { } public void testDoExecute() { - final GetServiceAccountRequest request1 = randomFrom( - new GetServiceAccountRequest(null, null), - new GetServiceAccountRequest("elastic", null), - new GetServiceAccountRequest("elastic", "fleet-server")); + final GetServiceAccountRequest request1 = randomFrom(new GetServiceAccountRequest(null, null), + new GetServiceAccountRequest("elastic", null)); final PlainActionFuture future1 = new PlainActionFuture<>(); transportGetServiceAccountAction.doExecute(mock(Task.class), request1, future1); final GetServiceAccountResponse getServiceAccountResponse1 = future1.actionGet(); - assertThat(getServiceAccountResponse1.getServiceAccountInfos().length, equalTo(1)); - assertThat(getServiceAccountResponse1.getServiceAccountInfos()[0].getPrincipal(), equalTo("elastic/fleet-server")); + assertThat(getServiceAccountResponse1.getServiceAccountInfos().length, equalTo(2)); + assertThat( + Arrays.stream(getServiceAccountResponse1.getServiceAccountInfos()) + .map(ServiceAccountInfo::getPrincipal) + .collect(Collectors.toList()), + containsInAnyOrder("elastic/fleet-server", "elastic/kibana-system") + ); - final GetServiceAccountRequest request2 = randomFrom( - new GetServiceAccountRequest("foo", null), - new GetServiceAccountRequest("elastic", "foo"), - new GetServiceAccountRequest("foo", "bar")); + final GetServiceAccountRequest request2 = new GetServiceAccountRequest("elastic", "fleet-server"); final PlainActionFuture future2 = new PlainActionFuture<>(); transportGetServiceAccountAction.doExecute(mock(Task.class), request2, future2); final GetServiceAccountResponse getServiceAccountResponse2 = future2.actionGet(); - assertThat(getServiceAccountResponse2.getServiceAccountInfos().length, equalTo(0)); + assertThat(getServiceAccountResponse2.getServiceAccountInfos().length, equalTo(1)); + assertThat(getServiceAccountResponse2.getServiceAccountInfos()[0].getPrincipal(), equalTo("elastic/fleet-server")); + + final GetServiceAccountRequest request3 = randomFrom( + new GetServiceAccountRequest("foo", null), + new GetServiceAccountRequest("elastic", "foo"), + new GetServiceAccountRequest("foo", "bar")); + final PlainActionFuture future3 = new PlainActionFuture<>(); + transportGetServiceAccountAction.doExecute(mock(Task.class), request3, future3); + final GetServiceAccountResponse getServiceAccountResponse3 = future3.actionGet(); + assertThat(getServiceAccountResponse3.getServiceAccountInfos().length, equalTo(0)); } } From 52a3ac4bd186c871a647f78483b077c7baf73b18 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Fri, 13 Aug 2021 17:08:30 +0300 Subject: [PATCH 07/12] update service name --- docs/java-rest/high-level/security/enroll_kibana.asciidoc | 2 +- x-pack/docs/en/rest-api/security/enroll-kibana.asciidoc | 2 +- .../action/enrollment/TransportKibanaEnrollmentAction.java | 6 +++--- .../enrollment/TransportKibanaEnrollmentActionTests.java | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/java-rest/high-level/security/enroll_kibana.asciidoc b/docs/java-rest/high-level/security/enroll_kibana.asciidoc index d84dce665de09..7ba8ea94dbe20 100644 --- a/docs/java-rest/high-level/security/enroll_kibana.asciidoc +++ b/docs/java-rest/high-level/security/enroll_kibana.asciidoc @@ -21,7 +21,7 @@ executed operation as follows: -------------------------------------------------- include-tagged::{doc-tests-file}[{api-kibana-response] -------------------------------------------------- -<1> A token that can be used as a bearer token for the `kibana-system` service account. +<1> A token that can be used as a bearer token for the `elastic/kibana` service account. <2> The CA certificate that has signed the certificate that the cluster uses for TLS on the HTTP layer, as a Base64 encoded string of the ASN.1 DER encoding of the certificate. diff --git a/x-pack/docs/en/rest-api/security/enroll-kibana.asciidoc b/x-pack/docs/en/rest-api/security/enroll-kibana.asciidoc index 60b96b0bdcf74..dddd84956e910 100644 --- a/x-pack/docs/en/rest-api/security/enroll-kibana.asciidoc +++ b/x-pack/docs/en/rest-api/security/enroll-kibana.asciidoc @@ -39,6 +39,6 @@ The API returns the following response: "http_ca" : "MIIJlAIBAzCCCVoGCSqGSIb3....vsDfsA3UZBAjEPfhubpQysAICCAA=", <2> } ---- -<1> A token that can be used as a bearer token for the `kibana-system` service account. +<1> A token that can be used as a bearer token for the `elastic/kibana` service account. <2> The CA certificate used to sign the node certificates that {es} uses for TLS on the HTTP layer. The certificate is returned as a Base64 encoded string of the ASN.1 DER encoding of the certificate. diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportKibanaEnrollmentAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportKibanaEnrollmentAction.java index c3f5d36dff8f4..17230e87d5639 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportKibanaEnrollmentAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportKibanaEnrollmentAction.java @@ -95,12 +95,12 @@ public class TransportKibanaEnrollmentAction extends HandledTransportAction { - logger.debug("Successfully created credentials for the [kibana-system] service account during kibana enrollment"); + logger.debug("Successfully created credentials for the [elastic/kibana] service account during kibana enrollment"); listener.onResponse(new KibanaEnrollmentResponse(response.getValue(), httpCa)); }, e -> listener.onFailure( - new ElasticsearchException("Failed to create credentials for the [kibana-system] service account", e)))); + new ElasticsearchException("Failed to create credentials for the [elastic/kibana] service account", e)))); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/enrollment/TransportKibanaEnrollmentActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/enrollment/TransportKibanaEnrollmentActionTests.java index 678422a7ac29b..9feb2539dc0f2 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/enrollment/TransportKibanaEnrollmentActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/enrollment/TransportKibanaEnrollmentActionTests.java @@ -125,6 +125,6 @@ public void testKibanaEnrollmentFailedTokenCreation() { final PlainActionFuture future = new PlainActionFuture<>(); action.doExecute(mock(Task.class), request, future); ElasticsearchException e = expectThrows(ElasticsearchException.class, future::actionGet); - assertThat(e.getDetailedMessage(), containsString("Failed to create credentials for the [kibana-system] service account")); + assertThat(e.getDetailedMessage(), containsString("Failed to create credentials for the [elastic/kibana] service account")); } } From 89d0f383d987ebb6b9dcbb358c9d9b7a8fe61b33 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Fri, 13 Aug 2021 23:07:01 +0300 Subject: [PATCH 08/12] re-enable enrollment documentation tests --- .../qa/ssl-enabled/build.gradle | 8 ++ .../EnrollmentDocumentationIT.java | 133 ++++++++++++++++++ .../SecurityDocumentationIT.java | 86 ----------- .../security/enroll_kibana.asciidoc | 6 +- .../enroll_node.asciidoc | 6 +- 5 files changed, 147 insertions(+), 92 deletions(-) create mode 100644 client/rest-high-level/qa/ssl-enabled/src/javaRestTest/java/org/elasticsearch/client/documentation/EnrollmentDocumentationIT.java rename docs/java-rest/high-level/{cluster => security}/enroll_node.asciidoc (91%) diff --git a/client/rest-high-level/qa/ssl-enabled/build.gradle b/client/rest-high-level/qa/ssl-enabled/build.gradle index 164c74e6b292d..6f47ae32634ea 100644 --- a/client/rest-high-level/qa/ssl-enabled/build.gradle +++ b/client/rest-high-level/qa/ssl-enabled/build.gradle @@ -6,6 +6,14 @@ * Side Public License, v 1. */ +/* + * We need this separate project as tests related to the enrollment process require + * test clusters with a specific TLS setup which is also not FIPS 140-2 compliant + * (as it uses PKCS#12 keystores). In order to not disable the entire rest-high-level + * project when running in fips mode, we moved enrollment tests in this subproject. + * + */ + import org.elasticsearch.gradle.internal.test.RestIntegTestTask import org.elasticsearch.gradle.internal.info.BuildParams diff --git a/client/rest-high-level/qa/ssl-enabled/src/javaRestTest/java/org/elasticsearch/client/documentation/EnrollmentDocumentationIT.java b/client/rest-high-level/qa/ssl-enabled/src/javaRestTest/java/org/elasticsearch/client/documentation/EnrollmentDocumentationIT.java new file mode 100644 index 0000000000000..546bb1733922e --- /dev/null +++ b/client/rest-high-level/qa/ssl-enabled/src/javaRestTest/java/org/elasticsearch/client/documentation/EnrollmentDocumentationIT.java @@ -0,0 +1,133 @@ +/* + * 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.documentation; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.LatchedActionListener; +import org.elasticsearch.client.ESRestHighLevelClientTestCase; +import org.elasticsearch.client.RequestOptions; +import org.elasticsearch.client.RestHighLevelClient; +import org.elasticsearch.client.security.KibanaEnrollmentResponse; +import org.elasticsearch.client.security.NodeEnrollmentResponse; +import org.elasticsearch.common.settings.SecureString; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.util.concurrent.ThreadContext; +import org.elasticsearch.core.PathUtils; +import org.junit.BeforeClass; + +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +public class EnrollmentDocumentationIT extends ESRestHighLevelClientTestCase { + static Path HTTP_TRUSTSTORE; + + @BeforeClass + static void getResources() throws Exception{ + HTTP_TRUSTSTORE = PathUtils.get(EnrollmentDocumentationIT.class.getResource("/httpCa.p12").toURI()); + } + @Override + protected String getProtocol() { + return "https"; + } + + @Override + protected Settings restClientSettings() { + String token = basicAuthHeaderValue("admin_user", new SecureString("admin-password".toCharArray())); + + return Settings.builder() + .put(ThreadContext.PREFIX + ".Authorization", token) + .put(TRUSTSTORE_PATH, HTTP_TRUSTSTORE) + .put(TRUSTSTORE_PASSWORD, "password") + .build(); + } + + public void testNodeEnrollment() throws Exception { + RestHighLevelClient client = highLevelClient(); + + { + // tag::node-enrollment-execute + NodeEnrollmentResponse response = client.security().enrollNode(RequestOptions.DEFAULT); + // end::node-enrollment-execute + + // tag::node-enrollment-response + String httpCaKey = response.getHttpCaKey(); // <1> + String httpCaCert = response.getHttpCaCert(); // <2> + String transportKey = response.getTransportKey(); // <3> + String transportCert = response.getTransportCert(); // <4> + List nodesAddresses = response.getNodesAddresses(); // <5> + // end::node-enrollment-response + } + + { + // tag::node-enrollment-execute-listener + ActionListener listener = + new ActionListener() { + @Override + public void onResponse(NodeEnrollmentResponse response) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + }}; + // end::node-enrollment-execute-listener + + final CountDownLatch latch = new CountDownLatch(1); + listener = new LatchedActionListener<>(listener, latch); + + // tag::node-enrollment-execute-async + client.security().enrollNodeAsync(RequestOptions.DEFAULT, listener); + // end::node-enrollment-execute-async + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } + } + + public void testKibanaEnrollment() throws Exception { + RestHighLevelClient client = highLevelClient(); + + { + // tag::kibana-enrollment-execute + KibanaEnrollmentResponse response = client.security().enrollKibana(RequestOptions.DEFAULT); + // end::kibana-enrollment-execute + + // tag::kibana-enrollment-response + SecureString token = response.getToken(); // <1> + String httoCa = response.getHttpCa(); // <2> + // end::kibana-enrollment-response + assertNotNull(token); + } + + { + // tag::kibana-enrollment-execute-listener + ActionListener listener = + new ActionListener() { + @Override + public void onResponse(KibanaEnrollmentResponse response) { + // <1> + } + + @Override + public void onFailure(Exception e) { + // <2> + }}; + // end::kibana-enrollment-execute-listener + + final CountDownLatch latch = new CountDownLatch(1); + listener = new LatchedActionListener<>(listener, latch); + + // tag::kibana-enrollment-execute-async + client.security().enrollKibanaAsync(RequestOptions.DEFAULT, listener); + // end::kibana-enrollment-execute-async + assertTrue(latch.await(30L, TimeUnit.SECONDS)); + } + } +} 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 da13b8f38a5a0..045d5200033cd 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 @@ -75,7 +75,6 @@ import org.elasticsearch.client.security.InvalidateApiKeyResponse; import org.elasticsearch.client.security.InvalidateTokenRequest; import org.elasticsearch.client.security.InvalidateTokenResponse; -import org.elasticsearch.client.security.NodeEnrollmentResponse; import org.elasticsearch.client.security.PutPrivilegesRequest; import org.elasticsearch.client.security.PutPrivilegesResponse; import org.elasticsearch.client.security.PutRoleMappingRequest; @@ -101,7 +100,6 @@ import org.elasticsearch.client.security.user.privileges.Role.ClusterPrivilegeName; import org.elasticsearch.client.security.user.privileges.Role.IndexPrivilegeName; import org.elasticsearch.client.security.user.privileges.UserIndicesPrivileges; -import org.elasticsearch.client.security.KibanaEnrollmentResponse; import org.elasticsearch.core.CheckedConsumer; import org.elasticsearch.common.Strings; import org.elasticsearch.common.settings.SecureString; @@ -2875,90 +2873,6 @@ public void onFailure(Exception e) { } } - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/75097") - public void testNodeEnrollment() throws Exception { - RestHighLevelClient client = highLevelClient(); - - { - // tag::node-enrollment-execute - NodeEnrollmentResponse response = client.security().enrollNode(RequestOptions.DEFAULT); - // end::node-enrollment-execute - - // tag::node-enrollment-response - String httpCaKey = response.getHttpCaKey(); // <1> - String httpCaCert = response.getHttpCaCert(); // <2> - String transportKey = response.getTransportKey(); // <3> - String transportCert = response.getTransportCert(); // <4> - List nodesAddresses = response.getNodesAddresses(); // <5> - // end::node-enrollment-response - } - - { - // tag::node-enrollment-execute-listener - ActionListener listener = - new ActionListener() { - @Override - public void onResponse(NodeEnrollmentResponse response) { - // <1> - } - - @Override - public void onFailure(Exception e) { - // <2> - }}; - // end::node-enrollment-execute-listener - - final CountDownLatch latch = new CountDownLatch(1); - listener = new LatchedActionListener<>(listener, latch); - - // tag::node-enrollment-execute-async - client.security().enrollNodeAsync(RequestOptions.DEFAULT, listener); - // end::node-enrollment-execute-async - assertTrue(latch.await(30L, TimeUnit.SECONDS)); - } - } - - @AwaitsFix(bugUrl = "https://github.com/elastic/elasticsearch/issues/75097") - public void testKibanaEnrollment() throws Exception { - RestHighLevelClient client = highLevelClient(); - - { - // tag::kibana-enrollment-execute - KibanaEnrollmentResponse response = client.security().enrollKibana(RequestOptions.DEFAULT); - // end::kibana-enrollment-execute - - // tag::kibana-enrollment-response - SecureString token = response.getToken(); // <1> - String httoCa = response.getHttpCa(); // <2> - // end::kibana-enrollment-response - assertNotNull(token); - } - - { - // tag::kibana-enrollment-execute-listener - ActionListener listener = - new ActionListener() { - @Override - public void onResponse(KibanaEnrollmentResponse response) { - // <1> - } - - @Override - public void onFailure(Exception e) { - // <2> - }}; - // end::kibana-enrollment-execute-listener - - final CountDownLatch latch = new CountDownLatch(1); - listener = new LatchedActionListener<>(listener, latch); - - // tag::kibana-enrollment-execute-async - client.security().enrollKibanaAsync(RequestOptions.DEFAULT, listener); - // end::kibana-enrollment-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)) { diff --git a/docs/java-rest/high-level/security/enroll_kibana.asciidoc b/docs/java-rest/high-level/security/enroll_kibana.asciidoc index 7ba8ea94dbe20..11ff85df6d0c3 100644 --- a/docs/java-rest/high-level/security/enroll_kibana.asciidoc +++ b/docs/java-rest/high-level/security/enroll_kibana.asciidoc @@ -19,7 +19,7 @@ executed operation as follows: ["source","java",subs="attributes,callouts,macros"] -------------------------------------------------- -include-tagged::{doc-tests-file}[{api-kibana-response] +include-tagged::{doc-tests}/EnrollDocumentationIT.java[{api}-response] -------------------------------------------------- <1> A token that can be used as a bearer token for the `elastic/kibana` service account. <2> The CA certificate that has signed the certificate that the cluster uses for TLS on the HTTP layer, @@ -33,14 +33,14 @@ method: ["source","java",subs="attributes,callouts,macros"] -------------------------------------------------- -include-tagged::{doc-tests-file}[{api}-execute-async] +include-tagged::{doc-tests}/EnrollDocumentationIT.java[{api}-execute-async] -------------------------------------------------- A typical listener for a `KibanaEnrollmentResponse` looks like: ["source","java",subs="attributes,callouts,macros"] -------------------------------------------------- -include-tagged::{doc-tests-file}[{api}-execute-listener] +include-tagged::{doc-tests}/EnrollDocumentationIT.java[{api}-execute-listener] -------------------------------------------------- <1> Called when the execution is successfully completed. The response is provided as an argument diff --git a/docs/java-rest/high-level/cluster/enroll_node.asciidoc b/docs/java-rest/high-level/security/enroll_node.asciidoc similarity index 91% rename from docs/java-rest/high-level/cluster/enroll_node.asciidoc rename to docs/java-rest/high-level/security/enroll_node.asciidoc index e8bdaef30850f..25f2c8d9b00bc 100644 --- a/docs/java-rest/high-level/cluster/enroll_node.asciidoc +++ b/docs/java-rest/high-level/security/enroll_node.asciidoc @@ -27,7 +27,7 @@ executed operation as follows: ["source","java",subs="attributes,callouts,macros"] -------------------------------------------------- -include-tagged::{doc-tests-file}[{api}-response] +include-tagged::{doc-tests}/EnrollDocumentationIT.java[{api}-response] -------------------------------------------------- <1> The CA private key that can be used by the new node in order to sign its certificate for the HTTP layer, as a Base64 encoded string of the ASN.1 DER encoding of the key. @@ -49,14 +49,14 @@ method: ["source","java",subs="attributes,callouts,macros"] -------------------------------------------------- -include-tagged::{doc-tests-file}[{api}-execute-async] +include-tagged::{doc-tests}/EnrollDocumentationIT.java[{api}-execute-async] -------------------------------------------------- A typical listener for a `NodeEnrollmentResponse` looks like: ["source","java",subs="attributes,callouts,macros"] -------------------------------------------------- -include-tagged::{doc-tests-file}[{api}-execute-listener] +include-tagged::{doc-tests}/EnrollDocumentationIT.java[{api}-execute-listener] -------------------------------------------------- <1> Called when the execution is successfully completed. The response is provided as an argument From 8a3e89f3e2e1ce915773fb22213cc0d1b60e7673 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Mon, 16 Aug 2021 17:40:23 +0300 Subject: [PATCH 09/12] Address feedback - nits - removed unecessary parsing from KibanaEnrollmentResponse - added token name in the response - update docs --- .../qa/ssl-enabled/build.gradle | 2 +- .../elasticsearch/client/EnrollmentIT.java | 4 +- .../EnrollmentDocumentationIT.java | 13 ++- .../security/KibanaEnrollmentResponse.java | 47 ++++++++-- .../KibanaEnrollmentResponseTests.java | 91 +++++++++++++++++++ .../KibanaErnollmentResponseTests.java | 66 -------------- .../security/enroll_kibana.asciidoc | 15 +-- .../high-level/security/enroll_node.asciidoc | 6 +- .../rest-api/security/enroll-kibana.asciidoc | 13 ++- .../enrollment/KibanaEnrollmentResponse.java | 64 +++++-------- .../KibanaEnrollmentResponseTests.java | 88 +++++++++++++----- .../TransportKibanaEnrollmentAction.java | 11 ++- .../TransportKibanaEnrollmentActionTests.java | 4 +- 13 files changed, 259 insertions(+), 165 deletions(-) create mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/security/KibanaEnrollmentResponseTests.java delete mode 100644 client/rest-high-level/src/test/java/org/elasticsearch/client/security/KibanaErnollmentResponseTests.java diff --git a/client/rest-high-level/qa/ssl-enabled/build.gradle b/client/rest-high-level/qa/ssl-enabled/build.gradle index 6f47ae32634ea..8be09ae3b5e80 100644 --- a/client/rest-high-level/qa/ssl-enabled/build.gradle +++ b/client/rest-high-level/qa/ssl-enabled/build.gradle @@ -10,7 +10,7 @@ * We need this separate project as tests related to the enrollment process require * test clusters with a specific TLS setup which is also not FIPS 140-2 compliant * (as it uses PKCS#12 keystores). In order to not disable the entire rest-high-level - * project when running in fips mode, we moved enrollment tests in this subproject. + * project when running in fips mode, we moved enrollment tests in this subproject. * */ diff --git a/client/rest-high-level/qa/ssl-enabled/src/javaRestTest/java/org/elasticsearch/client/EnrollmentIT.java b/client/rest-high-level/qa/ssl-enabled/src/javaRestTest/java/org/elasticsearch/client/EnrollmentIT.java index 5fb5e5469b0ed..6940035fc72a5 100644 --- a/client/rest-high-level/qa/ssl-enabled/src/javaRestTest/java/org/elasticsearch/client/EnrollmentIT.java +++ b/client/rest-high-level/qa/ssl-enabled/src/javaRestTest/java/org/elasticsearch/client/EnrollmentIT.java @@ -25,6 +25,7 @@ import static org.hamcrest.Matchers.endsWith; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.startsWith; public class EnrollmentIT extends ESRestHighLevelClientTestCase { private static Path httpTrustStore; @@ -76,6 +77,7 @@ public void testEnrollKibana() throws Exception { assertThat(kibanaResponse, notNullValue()); assertThat(kibanaResponse.getHttpCa() , endsWith("brcNC5xq6YE7C4/06nH7F6le4kE4Uo6c9fpkl4ehOxQxndNLn462tFF+8VBA8IftJ1PPWzqGxLsCTzM6p6w8sa+XhgNYglLfkRjirc=")); - assertNotNull(kibanaResponse.getToken()); + assertNotNull(kibanaResponse.getTokenValue()); + assertNotNull(kibanaResponse.getTokenName(), startsWith("enroll-process-token-")); } } diff --git a/client/rest-high-level/qa/ssl-enabled/src/javaRestTest/java/org/elasticsearch/client/documentation/EnrollmentDocumentationIT.java b/client/rest-high-level/qa/ssl-enabled/src/javaRestTest/java/org/elasticsearch/client/documentation/EnrollmentDocumentationIT.java index 546bb1733922e..9511df7a4fd03 100644 --- a/client/rest-high-level/qa/ssl-enabled/src/javaRestTest/java/org/elasticsearch/client/documentation/EnrollmentDocumentationIT.java +++ b/client/rest-high-level/qa/ssl-enabled/src/javaRestTest/java/org/elasticsearch/client/documentation/EnrollmentDocumentationIT.java @@ -26,13 +26,16 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; +import static org.hamcrest.Matchers.startsWith; + public class EnrollmentDocumentationIT extends ESRestHighLevelClientTestCase { static Path HTTP_TRUSTSTORE; @BeforeClass - static void getResources() throws Exception{ + public static void getResources() throws Exception { HTTP_TRUSTSTORE = PathUtils.get(EnrollmentDocumentationIT.class.getResource("/httpCa.p12").toURI()); } + @Override protected String getProtocol() { return "https"; @@ -100,10 +103,12 @@ public void testKibanaEnrollment() throws Exception { // end::kibana-enrollment-execute // tag::kibana-enrollment-response - SecureString token = response.getToken(); // <1> - String httoCa = response.getHttpCa(); // <2> + String tokenName = response.getTokenName(); // <1> + SecureString tokenValue = response.getTokenValue(); // <2> + String httoCa = response.getHttpCa(); // <3> // end::kibana-enrollment-response - assertNotNull(token); + assertNotNull(tokenValue); + assertThat(tokenName, startsWith("enroll-process-token-")); } { diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/KibanaEnrollmentResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/KibanaEnrollmentResponse.java index 96dcdbba67bb7..2d1374b9bc4b3 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/KibanaEnrollmentResponse.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/KibanaEnrollmentResponse.java @@ -16,34 +16,51 @@ import java.io.IOException; import java.util.Objects; +import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg; + public final class KibanaEnrollmentResponse { - private SecureString token; + private String tokenName; + private SecureString tokenValue; private String httpCa; - public KibanaEnrollmentResponse(SecureString token, String httpCa) { - this.token = token; + public KibanaEnrollmentResponse(String tokenName, SecureString token, String httpCa) { + this.tokenName = tokenName; + this.tokenValue = token; this.httpCa = httpCa; } - public SecureString getToken() { return token; } + public String getTokenName() { return tokenName; } + + public SecureString getTokenValue() { return tokenValue; } public String getHttpCa() { return httpCa; } private static final ParseField TOKEN = new ParseField("token"); + private static final ParseField TOKEN_NAME = new ParseField("name"); + private static final ParseField TOKEN_VALUE = new ParseField("value"); private static final ParseField HTTP_CA = new ParseField("http_ca"); - @SuppressWarnings("unchecked") + static final ConstructingObjectParser TOKEN_PARSER = new ConstructingObjectParser<>( + KibanaEnrollmentResponse.class.getName(), true, + a -> new Token((String) a[0], (String) a[1]) + ); + private static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>( KibanaEnrollmentResponse.class.getName(), true, - a -> new KibanaEnrollmentResponse(new SecureString(((String) a[0]).toCharArray()), (String) a[1])); + a -> { + final Token token = (Token) a[0]; + return new KibanaEnrollmentResponse(token.name, new SecureString(token.value.toCharArray()), (String) a[1]); + }); static { - PARSER.declareString(ConstructingObjectParser.constructorArg(), TOKEN); - PARSER.declareString(ConstructingObjectParser.constructorArg(), HTTP_CA); + TOKEN_PARSER.declareString(constructorArg(), TOKEN_NAME); + TOKEN_PARSER.declareString(constructorArg(), TOKEN_VALUE); + PARSER.declareObject(constructorArg(), TOKEN_PARSER, TOKEN); + PARSER.declareString(constructorArg(), HTTP_CA); } public static KibanaEnrollmentResponse fromXContent(XContentParser parser) throws IOException { @@ -54,10 +71,20 @@ public static KibanaEnrollmentResponse fromXContent(XContentParser parser) throw if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; KibanaEnrollmentResponse that = (KibanaEnrollmentResponse) o; - return token.equals(that.token) && httpCa.equals(that.httpCa); + return tokenName.equals(that.tokenName) && tokenValue.equals(that.tokenValue) && httpCa.equals(that.httpCa); } @Override public int hashCode() { - return Objects.hash(token, httpCa); + return Objects.hash(tokenName, tokenValue, httpCa); + } + + private static class Token { + private final String name; + private final String value; + + Token(String name, String value) { + this.name = name; + this.value = value; + } } } diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/KibanaEnrollmentResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/KibanaEnrollmentResponseTests.java new file mode 100644 index 0000000000000..35d5453e798da --- /dev/null +++ b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/KibanaEnrollmentResponseTests.java @@ -0,0 +1,91 @@ +/* + * 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.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 org.elasticsearch.test.EqualsHashCodeTestUtils; + +import java.io.IOException; + +import static org.hamcrest.Matchers.equalTo; + +public class KibanaEnrollmentResponseTests extends ESTestCase { + + public void testFromXContent() throws IOException { + final String tokenName = randomAlphaOfLengthBetween(8 ,14); + final String tokenValue = randomAlphaOfLengthBetween(58, 70); + final String httpCa = randomAlphaOfLength(50); + + final XContentType xContentType = randomFrom(XContentType.values()); + final XContentBuilder builder = XContentFactory.contentBuilder(xContentType); + builder.startObject() + .startObject("token") + .field("name", tokenName) + .field("value", tokenValue) + .endObject() + .field("http_ca", httpCa) + .endObject(); + BytesReference xContent = BytesReference.bytes(builder); + + final KibanaEnrollmentResponse response = KibanaEnrollmentResponse.fromXContent(createParser(xContentType.xContent(), xContent)); + assertThat(response.getTokenName(), equalTo(tokenName)); + assertThat(response.getTokenValue(), equalTo(tokenValue)); + assertThat(response.getHttpCa(), equalTo(httpCa)); + } + + public void testEqualsHashCode() { + final String tokenName = randomAlphaOfLengthBetween(8 ,14); + final SecureString tokenValue = new SecureString(randomAlphaOfLengthBetween(58, 70).toCharArray()); + final String httpCa = randomAlphaOfLength(50); + KibanaEnrollmentResponse kibanaEnrollmentResponse = new KibanaEnrollmentResponse(tokenName, tokenValue, httpCa); + + EqualsHashCodeTestUtils.checkEqualsAndHashCode(kibanaEnrollmentResponse, + (original) -> new KibanaEnrollmentResponse(original.getTokenName(), original.getTokenValue(), original.getHttpCa())); + + EqualsHashCodeTestUtils.checkEqualsAndHashCode(kibanaEnrollmentResponse, + (original) -> new KibanaEnrollmentResponse(original.getTokenName(), original.getTokenValue(), original.getHttpCa()), + KibanaEnrollmentResponseTests::mutateTestItem); + } + + private static KibanaEnrollmentResponse mutateTestItem(KibanaEnrollmentResponse original) { + switch (randomIntBetween(0, 3)) { + case 0: + return new KibanaEnrollmentResponse( + randomAlphaOfLengthBetween(14, 20), + new SecureString(randomAlphaOfLengthBetween(71, 90).toCharArray()), + randomAlphaOfLength(52) + ); + case 1: + return new KibanaEnrollmentResponse( + original.getTokenName(), + new SecureString(randomAlphaOfLengthBetween(71, 90).toCharArray()), + randomAlphaOfLength(52) + ); + case 2: + return new KibanaEnrollmentResponse( + randomAlphaOfLengthBetween(14, 20), + original.getTokenValue(), + randomAlphaOfLength(52) + ); + case 3: + return new KibanaEnrollmentResponse( + randomAlphaOfLengthBetween(14, 20), + new SecureString(randomAlphaOfLengthBetween(71, 90).toCharArray()), + original.getHttpCa() + ); + } + // we never reach here + return null; + } +} diff --git a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/KibanaErnollmentResponseTests.java b/client/rest-high-level/src/test/java/org/elasticsearch/client/security/KibanaErnollmentResponseTests.java deleted file mode 100644 index fe0107c393086..0000000000000 --- a/client/rest-high-level/src/test/java/org/elasticsearch/client/security/KibanaErnollmentResponseTests.java +++ /dev/null @@ -1,66 +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.client.security; - -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 org.elasticsearch.test.EqualsHashCodeTestUtils; - -import java.io.IOException; -import java.util.List; - -import static org.hamcrest.Matchers.equalTo; - -public class KibanaErnollmentResponseTests extends ESTestCase { - - public void testFromXContent() throws IOException { - final String token = randomAlphaOfLengthBetween(30, 40); - final String httpCa = randomAlphaOfLength(50); - final List nodesAddresses = randomList(2, 10, () -> buildNewFakeTransportAddress().toString()); - - final XContentType xContentType = randomFrom(XContentType.values()); - final XContentBuilder builder = XContentFactory.contentBuilder(xContentType); - builder.startObject().field("token", token).field("http_ca", httpCa).field("nodes_addresses", nodesAddresses).endObject(); - BytesReference xContent = BytesReference.bytes(builder); - - final KibanaEnrollmentResponse response = KibanaEnrollmentResponse.fromXContent(createParser(xContentType.xContent(), xContent)); - assertThat(response.getToken(), equalTo(token)); - assertThat(response.getHttpCa(), equalTo(httpCa)); - } - - public void testEqualsHashCode() { - final SecureString password = new SecureString(randomAlphaOfLengthBetween(30, 40).toCharArray()); - final String httpCa = randomAlphaOfLength(50); - KibanaEnrollmentResponse kibanaEnrollmentResponse = new KibanaEnrollmentResponse(password, httpCa); - - EqualsHashCodeTestUtils.checkEqualsAndHashCode(kibanaEnrollmentResponse, - (original) -> new KibanaEnrollmentResponse(original.getToken(), original.getHttpCa())); - - EqualsHashCodeTestUtils.checkEqualsAndHashCode(kibanaEnrollmentResponse, - (original) -> new KibanaEnrollmentResponse(original.getToken(), original.getHttpCa()), - KibanaErnollmentResponseTests::mutateTestItem); - } - - private static KibanaEnrollmentResponse mutateTestItem(KibanaEnrollmentResponse original) { - switch (randomIntBetween(0, 1)) { - case 0: - return new KibanaEnrollmentResponse(new SecureString(randomAlphaOfLengthBetween(30, 40).toCharArray()), - original.getHttpCa()); - case 1: - return new KibanaEnrollmentResponse(original.getToken(), randomAlphaOfLength(51)); - default: - return new KibanaEnrollmentResponse(original.getToken(), - original.getHttpCa()); - } - } -} diff --git a/docs/java-rest/high-level/security/enroll_kibana.asciidoc b/docs/java-rest/high-level/security/enroll_kibana.asciidoc index 11ff85df6d0c3..e5cda66e1fe01 100644 --- a/docs/java-rest/high-level/security/enroll_kibana.asciidoc +++ b/docs/java-rest/high-level/security/enroll_kibana.asciidoc @@ -19,11 +19,14 @@ executed operation as follows: ["source","java",subs="attributes,callouts,macros"] -------------------------------------------------- -include-tagged::{doc-tests}/EnrollDocumentationIT.java[{api}-response] +include-tagged::{doc-tests}/EnrollmentDocumentationIT.java[{api}-response] -------------------------------------------------- -<1> A token that can be used as a bearer token for the `elastic/kibana` service account. -<2> The CA certificate that has signed the certificate that the cluster uses for TLS on the HTTP layer, -as a Base64 encoded string of the ASN.1 DER encoding of the certificate. +<1> The name of a token that can be used as a bearer token for the `elastic/kibana` service account. +See link:/security/authentication/service-accounts.html#authenticate-with-service-account-token.html[Authenticate with service account tokens] for more information +<2> The value of the aforementioned token. +<3> The CA certificate used to sign the node certificates that {es} uses for TLS on the HTTP layer. +The certificate is returned as a Base64 encoded string of the ASN.1 DER encoding of the certificate. + [id="{upid}-{api}-execute-async"] ==== Asynchronous Execution @@ -33,14 +36,14 @@ method: ["source","java",subs="attributes,callouts,macros"] -------------------------------------------------- -include-tagged::{doc-tests}/EnrollDocumentationIT.java[{api}-execute-async] +include-tagged::{doc-tests}/EnrollmentDocumentationIT.java[{api}-execute-async] -------------------------------------------------- A typical listener for a `KibanaEnrollmentResponse` looks like: ["source","java",subs="attributes,callouts,macros"] -------------------------------------------------- -include-tagged::{doc-tests}/EnrollDocumentationIT.java[{api}-execute-listener] +include-tagged::{doc-tests}/EnrollmentDocumentationIT.java[{api}-execute-listener] -------------------------------------------------- <1> Called when the execution is successfully completed. The response is provided as an argument diff --git a/docs/java-rest/high-level/security/enroll_node.asciidoc b/docs/java-rest/high-level/security/enroll_node.asciidoc index 25f2c8d9b00bc..374e2d46a8b41 100644 --- a/docs/java-rest/high-level/security/enroll_node.asciidoc +++ b/docs/java-rest/high-level/security/enroll_node.asciidoc @@ -27,7 +27,7 @@ executed operation as follows: ["source","java",subs="attributes,callouts,macros"] -------------------------------------------------- -include-tagged::{doc-tests}/EnrollDocumentationIT.java[{api}-response] +include-tagged::{doc-tests}/EnrollmentDocumentationIT.java[{api}-response] -------------------------------------------------- <1> The CA private key that can be used by the new node in order to sign its certificate for the HTTP layer, as a Base64 encoded string of the ASN.1 DER encoding of the key. @@ -49,14 +49,14 @@ method: ["source","java",subs="attributes,callouts,macros"] -------------------------------------------------- -include-tagged::{doc-tests}/EnrollDocumentationIT.java[{api}-execute-async] +include-tagged::{doc-tests}/EnrollmentDocumentationIT.java[{api}-execute-async] -------------------------------------------------- A typical listener for a `NodeEnrollmentResponse` looks like: ["source","java",subs="attributes,callouts,macros"] -------------------------------------------------- -include-tagged::{doc-tests}/EnrollDocumentationIT.java[{api}-execute-listener] +include-tagged::{doc-tests}/EnrollmentDocumentationIT.java[{api}-execute-listener] -------------------------------------------------- <1> Called when the execution is successfully completed. The response is provided as an argument diff --git a/x-pack/docs/en/rest-api/security/enroll-kibana.asciidoc b/x-pack/docs/en/rest-api/security/enroll-kibana.asciidoc index dddd84956e910..eeec138b2bffa 100644 --- a/x-pack/docs/en/rest-api/security/enroll-kibana.asciidoc +++ b/x-pack/docs/en/rest-api/security/enroll-kibana.asciidoc @@ -35,10 +35,15 @@ The API returns the following response: [source,console_result] ---- { - "token" : "AAEAAWVsYXN0aWM...vZmxlZXQtc2VydmVyL3Rva2VuMTo3TFdaSDZ", <1> - "http_ca" : "MIIJlAIBAzCCCVoGCSqGSIb3....vsDfsA3UZBAjEPfhubpQysAICCAA=", <2> + "token" : { + "name" : "enroll-process-token-1629123923000", <1> + "value": "AAEAAWVsYXN0aWM...vZmxlZXQtc2VydmVyL3Rva2VuMTo3TFdaSDZ" <2> + }, + "http_ca" : "MIIJlAIBAzCCCVoGCSqGSIb3....vsDfsA3UZBAjEPfhubpQysAICCAA=", <3> } ---- -<1> A token that can be used as a bearer token for the `elastic/kibana` service account. -<2> The CA certificate used to sign the node certificates that {es} uses for TLS on the HTTP layer. +<1> The name of a token that can be used as a bearer token for the `elastic/kibana` service account. +See link:/security/authentication/service-accounts.html#authenticate-with-service-account-token.html[Authenticate with service account tokens] for more information +<2> The value of the aforementioned token. +<3> The CA certificate used to sign the node certificates that {es} uses for TLS on the HTTP layer. The certificate is returned as a Base64 encoded string of the ASN.1 DER encoding of the certificate. diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/KibanaEnrollmentResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/KibanaEnrollmentResponse.java index 0fc1a46bd0412..5e35355a70869 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/KibanaEnrollmentResponse.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/enrollment/KibanaEnrollmentResponse.java @@ -11,75 +11,61 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.settings.SecureString; -import org.elasticsearch.common.xcontent.ConstructingObjectParser; -import org.elasticsearch.common.xcontent.ParseField; import org.elasticsearch.common.xcontent.ToXContentObject; import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.common.xcontent.XContentParser; - import java.io.IOException; import java.util.Objects; public final class KibanaEnrollmentResponse extends ActionResponse implements ToXContentObject { - private static final ParseField TOKEN = new ParseField("token"); - private static final ParseField HTTP_CA = new ParseField("http_ca"); - - @SuppressWarnings("unchecked") - private static final ConstructingObjectParser PARSER = - new ConstructingObjectParser<>( - KibanaEnrollmentResponse.class.getName(), true, - a -> new KibanaEnrollmentResponse(new SecureString(((String) a[0]).toCharArray()), (String) a[1])); - - static { - PARSER.declareString(ConstructingObjectParser.constructorArg(), TOKEN); - PARSER.declareString(ConstructingObjectParser.constructorArg(), HTTP_CA); - } - - private final SecureString token; + private final String tokenName; + private final SecureString tokenValue; private final String httpCa; public KibanaEnrollmentResponse(StreamInput in) throws IOException { super(in); - token = in.readSecureString(); + tokenName = in.readString(); + tokenValue = in.readSecureString(); httpCa = in.readString(); } - public KibanaEnrollmentResponse(SecureString token, String httpCa) { - this.token = token; + public KibanaEnrollmentResponse(String tokenName, SecureString tokenValue, String httpCa) { + this.tokenName = tokenName; + this.tokenValue = tokenValue; this.httpCa = httpCa; } - public SecureString getToken() { return token; } + public String getTokenName() { return tokenName; } + public SecureString getTokenValue() { return tokenValue; } public String getHttpCa() { return httpCa; } - @Override public XContentBuilder toXContent( - XContentBuilder builder, Params params) throws IOException { - builder.startObject(); - builder.field(TOKEN.getPreferredName(), token.toString()); - builder.field(HTTP_CA.getPreferredName(), httpCa); - return builder.endObject(); - } - @Override public void writeTo(StreamOutput out) throws IOException { - out.writeSecureString(token); + out.writeString(tokenName); + out.writeSecureString(tokenValue); out.writeString(httpCa); } - public static KibanaEnrollmentResponse fromXContent(XContentParser parser) { - return PARSER.apply(parser, null); - } - @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - KibanaEnrollmentResponse response = (KibanaEnrollmentResponse) o; - return token.equals(response.token) && httpCa.equals(response.httpCa); + KibanaEnrollmentResponse that = (KibanaEnrollmentResponse) o; + return tokenName.equals(that.tokenName) && tokenValue.equals(that.tokenValue) && httpCa.equals(that.httpCa); } @Override public int hashCode() { - return Objects.hash(token, httpCa); + return Objects.hash(tokenName, tokenValue, httpCa); + } + + @Override public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException { + builder.startObject() + .startObject("token") + .field("name", tokenName) + .field("value", tokenValue.toString()) + .endObject() + .field("http_ca", httpCa) + .endObject(); + return builder; } } diff --git a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/enrollment/KibanaEnrollmentResponseTests.java b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/enrollment/KibanaEnrollmentResponseTests.java index 854f2cd835b4f..ac56dd0c87009 100644 --- a/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/enrollment/KibanaEnrollmentResponseTests.java +++ b/x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/action/enrollment/KibanaEnrollmentResponseTests.java @@ -7,41 +7,81 @@ package org.elasticsearch.xpack.core.security.action.enrollment; -import org.elasticsearch.common.io.stream.BytesStreamOutput; -import org.elasticsearch.common.io.stream.StreamInput; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.common.io.stream.Writeable; import org.elasticsearch.common.settings.SecureString; -import org.elasticsearch.common.xcontent.XContentParser; -import org.elasticsearch.test.AbstractXContentTestCase; +import org.elasticsearch.common.xcontent.ToXContent; +import org.elasticsearch.common.xcontent.XContentBuilder; +import org.elasticsearch.common.xcontent.XContentFactory; +import org.elasticsearch.common.xcontent.XContentHelper; +import org.elasticsearch.test.AbstractWireSerializingTestCase; import java.io.IOException; +import java.util.Map; -import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.equalTo; -public class KibanaEnrollmentResponseTests extends AbstractXContentTestCase { +public class KibanaEnrollmentResponseTests extends AbstractWireSerializingTestCase { - @Override protected KibanaEnrollmentResponse createTestInstance() { - return new KibanaEnrollmentResponse( - new SecureString(randomAlphaOfLengthBetween(30, 40).toCharArray()), - randomAlphaOfLength(50)); - } - @Override protected KibanaEnrollmentResponse doParseInstance(XContentParser parser) throws IOException { - return KibanaEnrollmentResponse.fromXContent(parser); + @Override + protected Writeable.Reader instanceReader() { + return KibanaEnrollmentResponse::new; } - @Override protected boolean supportsUnknownFields() { - return false; + @Override + protected KibanaEnrollmentResponse createTestInstance() { + return new KibanaEnrollmentResponse( + randomAlphaOfLengthBetween(8, 12), + new SecureString(randomAlphaOfLengthBetween(58, 70).toCharArray()), + randomAlphaOfLength(50) + ); } - public void testSerialization() throws IOException{ - KibanaEnrollmentResponse response = createTestInstance(); - try (BytesStreamOutput out = new BytesStreamOutput()) { - response.writeTo(out); - try (StreamInput in = out.bytes().streamInput()) { - KibanaEnrollmentResponse serialized = new KibanaEnrollmentResponse(in); - assertThat(response.getHttpCa(), is(serialized.getHttpCa())); - assertThat(response.getToken(), is(serialized.getToken())); - } + @Override + protected KibanaEnrollmentResponse mutateInstance(KibanaEnrollmentResponse instance) throws IOException { + switch (randomIntBetween(0, 3)) { + case 0: + return new KibanaEnrollmentResponse( + randomAlphaOfLengthBetween(14, 20), + new SecureString(randomAlphaOfLengthBetween(71, 90).toCharArray()), + randomAlphaOfLength(52) + ); + case 1: + return new KibanaEnrollmentResponse( + instance.getTokenName(), + new SecureString(randomAlphaOfLengthBetween(71, 90).toCharArray()), + randomAlphaOfLength(52) + ); + case 2: + return new KibanaEnrollmentResponse( + randomAlphaOfLengthBetween(14, 20), + instance.getTokenValue(), + randomAlphaOfLength(52) + ); + case 3: + return new KibanaEnrollmentResponse( + randomAlphaOfLengthBetween(14, 20), + new SecureString(randomAlphaOfLengthBetween(71, 90).toCharArray()), + instance.getHttpCa() + ); } + // we never reach here + return null; } + + public void testToXContent() throws IOException { + final KibanaEnrollmentResponse response = createTestInstance(); + XContentBuilder jsonBuilder = XContentFactory.jsonBuilder(); + response.toXContent(jsonBuilder, ToXContent.EMPTY_PARAMS); + final Map responseMap = XContentHelper.convertToMap( + BytesReference.bytes(jsonBuilder), + false, jsonBuilder.contentType()).v2(); + + assertThat(responseMap, equalTo(Map.of( + "token", Map.of("name", response.getTokenName(), "value", response.getTokenValue().toString()), + "http_ca", response.getHttpCa() + ))); + } + } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportKibanaEnrollmentAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportKibanaEnrollmentAction.java index 17230e87d5639..ce4c77b2b6528 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportKibanaEnrollmentAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/enrollment/TransportKibanaEnrollmentAction.java @@ -97,16 +97,17 @@ public class TransportKibanaEnrollmentAction extends HandledTransportAction { - logger.debug("Successfully created credentials for the [elastic/kibana] service account during kibana enrollment"); - listener.onResponse(new KibanaEnrollmentResponse(response.getValue(), httpCa)); + logger.debug("Successfully created token [{}] for the [elastic/kibana] service account during kibana enrollment", + response.getName()); + listener.onResponse(new KibanaEnrollmentResponse(response.getName(), response.getValue(), httpCa)); }, e -> listener.onFailure( - new ElasticsearchException("Failed to create credentials for the [elastic/kibana] service account", e)))); + new ElasticsearchException("Failed to create token for the [elastic/kibana] service account", e)))); } } protected static String getTokenName(){ - final ZonedDateTime autoConfigDate = ZonedDateTime.now(ZoneOffset.UTC); + final ZonedDateTime enrollTime = ZonedDateTime.now(ZoneOffset.UTC); final String prefix = "enroll-process-token-"; - return prefix + autoConfigDate.toInstant().getEpochSecond(); + return prefix + enrollTime.toInstant().toEpochMilli(); } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/enrollment/TransportKibanaEnrollmentActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/enrollment/TransportKibanaEnrollmentActionTests.java index 9feb2539dc0f2..106f16687d753 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/enrollment/TransportKibanaEnrollmentActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/enrollment/TransportKibanaEnrollmentActionTests.java @@ -109,7 +109,7 @@ public void testKibanaEnrollment() { assertThat(response.getHttpCa(), startsWith("MIIDSjCCAjKgAwIBAgIVALCgZXvbceUrjJaQMheDCX0kXnRJMA0GCSqGSIb3DQEBCwUAMDQxMjAw" + "BgNVBAMTKUVsYXN0aWMgQ2VydGlmaWNhdGUgVG9vbCBBdXRvZ2VuZXJhdGVkIENBMB4XDTIxMDQyODEyNTY0MVoXDTI0MDQyNzEyNTY0MVowNDEyMDAGA1UEA" + "xMpRWxhc3RpYyBDZXJ0aWZpY2F0ZSBUb29sIEF1dG9nZW5lcmF0ZWQgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCCJbOU4JvxDD/F")); - assertThat(response.getToken(), equalTo(TOKEN_VALUE)); + assertThat(response.getTokenValue(), equalTo(TOKEN_VALUE)); assertThat(createServiceAccountTokenRequests.size(), equalTo(1)); } @@ -125,6 +125,6 @@ public void testKibanaEnrollmentFailedTokenCreation() { final PlainActionFuture future = new PlainActionFuture<>(); action.doExecute(mock(Task.class), request, future); ElasticsearchException e = expectThrows(ElasticsearchException.class, future::actionGet); - assertThat(e.getDetailedMessage(), containsString("Failed to create credentials for the [elastic/kibana] service account")); + assertThat(e.getDetailedMessage(), containsString("Failed to create token for the [elastic/kibana] service account")); } } From cd73a19a5d081a53664c66bd8b72f0b3782dd3e6 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Mon, 16 Aug 2021 17:44:58 +0300 Subject: [PATCH 10/12] update var name --- .../client/security/KibanaEnrollmentResponse.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/KibanaEnrollmentResponse.java b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/KibanaEnrollmentResponse.java index 2d1374b9bc4b3..56604d849ed1d 100644 --- a/client/rest-high-level/src/main/java/org/elasticsearch/client/security/KibanaEnrollmentResponse.java +++ b/client/rest-high-level/src/main/java/org/elasticsearch/client/security/KibanaEnrollmentResponse.java @@ -24,9 +24,9 @@ public final class KibanaEnrollmentResponse { private SecureString tokenValue; private String httpCa; - public KibanaEnrollmentResponse(String tokenName, SecureString token, String httpCa) { + public KibanaEnrollmentResponse(String tokenName, SecureString tokenValue, String httpCa) { this.tokenName = tokenName; - this.tokenValue = token; + this.tokenValue = tokenValue; this.httpCa = httpCa; } From 4dce3181912d2b709dbda488623281cbe32074b0 Mon Sep 17 00:00:00 2001 From: Adam Locke Date: Mon, 16 Aug 2021 15:27:45 -0400 Subject: [PATCH 11/12] Fix links, streamline grammar, and other editorial changes --- .../high-level/security/enroll_kibana.asciidoc | 11 ++++++----- x-pack/docs/en/rest-api/security.asciidoc | 2 +- .../en/rest-api/security/enroll-kibana.asciidoc | 13 +++++++------ .../docs/en/rest-api/security/enroll-node.asciidoc | 2 +- .../authentication/service-accounts.asciidoc | 4 +++- 5 files changed, 18 insertions(+), 14 deletions(-) diff --git a/docs/java-rest/high-level/security/enroll_kibana.asciidoc b/docs/java-rest/high-level/security/enroll_kibana.asciidoc index e5cda66e1fe01..c6a7da354d066 100644 --- a/docs/java-rest/high-level/security/enroll_kibana.asciidoc +++ b/docs/java-rest/high-level/security/enroll_kibana.asciidoc @@ -21,11 +21,12 @@ executed operation as follows: -------------------------------------------------- include-tagged::{doc-tests}/EnrollmentDocumentationIT.java[{api}-response] -------------------------------------------------- -<1> The name of a token that can be used as a bearer token for the `elastic/kibana` service account. -See link:/security/authentication/service-accounts.html#authenticate-with-service-account-token.html[Authenticate with service account tokens] for more information -<2> The value of the aforementioned token. -<3> The CA certificate used to sign the node certificates that {es} uses for TLS on the HTTP layer. -The certificate is returned as a Base64 encoded string of the ASN.1 DER encoding of the certificate. +<1> The bearer token for the `elastic/kibana` service account. +Use this token to {ref}/service-accounts.html#authenticate-with-service-account-token[authenticate the service account] with {es}. +<2> The value of the bearer token for the `elastic/kibana` service account. +<3> The CA certificate used to sign the node certificates that {es} uses for TLS +on the HTTP layer. The certificate is returned as a Base64 encoded string of the +ASN.1 DER encoding of the certificate. [id="{upid}-{api}-execute-async"] diff --git a/x-pack/docs/en/rest-api/security.asciidoc b/x-pack/docs/en/rest-api/security.asciidoc index 09b244fe32d00..e023094ef8d2a 100644 --- a/x-pack/docs/en/rest-api/security.asciidoc +++ b/x-pack/docs/en/rest-api/security.asciidoc @@ -140,7 +140,6 @@ include::security/clear-privileges-cache.asciidoc[] include::security/clear-api-key-cache.asciidoc[] include::security/clear-service-token-caches.asciidoc[] include::security/create-api-keys.asciidoc[] -include::security/enroll-kibana.asciidoc[] include::security/put-app-privileges.asciidoc[] include::security/create-role-mappings.asciidoc[] include::security/create-roles.asciidoc[] @@ -154,6 +153,7 @@ include::security/delete-service-token.asciidoc[] include::security/delete-users.asciidoc[] include::security/disable-users.asciidoc[] include::security/enable-users.asciidoc[] +include::security/enroll-kibana.asciidoc[] include::security/enroll-node.asciidoc[] include::security/get-api-keys.asciidoc[] include::security/get-app-privileges.asciidoc[] diff --git a/x-pack/docs/en/rest-api/security/enroll-kibana.asciidoc b/x-pack/docs/en/rest-api/security/enroll-kibana.asciidoc index eeec138b2bffa..e32da7adb8c1b 100644 --- a/x-pack/docs/en/rest-api/security/enroll-kibana.asciidoc +++ b/x-pack/docs/en/rest-api/security/enroll-kibana.asciidoc @@ -39,11 +39,12 @@ The API returns the following response: "name" : "enroll-process-token-1629123923000", <1> "value": "AAEAAWVsYXN0aWM...vZmxlZXQtc2VydmVyL3Rva2VuMTo3TFdaSDZ" <2> }, - "http_ca" : "MIIJlAIBAzCCCVoGCSqGSIb3....vsDfsA3UZBAjEPfhubpQysAICCAA=", <3> + "http_ca" : "MIIJlAIBAzVoGCSqGSIb3...vsDfsA3UZBAjEPfhubpQysAICAA=", <3> } ---- -<1> The name of a token that can be used as a bearer token for the `elastic/kibana` service account. -See link:/security/authentication/service-accounts.html#authenticate-with-service-account-token.html[Authenticate with service account tokens] for more information -<2> The value of the aforementioned token. -<3> The CA certificate used to sign the node certificates that {es} uses for TLS on the HTTP layer. -The certificate is returned as a Base64 encoded string of the ASN.1 DER encoding of the certificate. +<1> The bearer token for the `elastic/kibana` service account. +Use this token to {ref}/service-accounts.html#authenticate-with-service-account-token[authenticate the service account] with {es}. +<2> The value of the bearer token for the `elastic/kibana` service account. +<3> The CA certificate used to sign the node certificates that {es} uses for TLS +on the HTTP layer. The certificate is returned as a Base64 encoded string of the +ASN.1 DER encoding of the certificate. diff --git a/x-pack/docs/en/rest-api/security/enroll-node.asciidoc b/x-pack/docs/en/rest-api/security/enroll-node.asciidoc index d7d41d71f2367..a6a876b9d98c0 100644 --- a/x-pack/docs/en/rest-api/security/enroll-node.asciidoc +++ b/x-pack/docs/en/rest-api/security/enroll-node.asciidoc @@ -1,7 +1,7 @@ [[security-api-node-enrollment]] === Enroll Node API ++++ -Enroll Node +Enroll node ++++ Allows a new node to join an existing cluster with security features enabled. diff --git a/x-pack/docs/en/security/authentication/service-accounts.asciidoc b/x-pack/docs/en/security/authentication/service-accounts.asciidoc index 6964d2e0785d6..67cd98e1a1df2 100644 --- a/x-pack/docs/en/security/authentication/service-accounts.asciidoc +++ b/x-pack/docs/en/security/authentication/service-accounts.asciidoc @@ -29,7 +29,9 @@ traditional roles Service accounts are not included in the response of the <>. To retrieve a service account, use the -<>. +<>. Use the +<> +to retrieve all service credentials for a service account. [discrete] [[service-accounts-explanation]] From cca510c6d1ac2af877ddcc490b0084a40df2297e Mon Sep 17 00:00:00 2001 From: Adam Locke Date: Mon, 16 Aug 2021 17:37:00 -0400 Subject: [PATCH 12/12] Clarify the value of the token used to authenticate --- x-pack/docs/en/rest-api/security/enroll-kibana.asciidoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/docs/en/rest-api/security/enroll-kibana.asciidoc b/x-pack/docs/en/rest-api/security/enroll-kibana.asciidoc index e32da7adb8c1b..55de31b5407d1 100644 --- a/x-pack/docs/en/rest-api/security/enroll-kibana.asciidoc +++ b/x-pack/docs/en/rest-api/security/enroll-kibana.asciidoc @@ -42,9 +42,9 @@ The API returns the following response: "http_ca" : "MIIJlAIBAzVoGCSqGSIb3...vsDfsA3UZBAjEPfhubpQysAICAA=", <3> } ---- -<1> The bearer token for the `elastic/kibana` service account. -Use this token to {ref}/service-accounts.html#authenticate-with-service-account-token[authenticate the service account] with {es}. +<1> The name of the bearer token for the `elastic/kibana` service account. <2> The value of the bearer token for the `elastic/kibana` service account. +Use this value to {ref}/service-accounts.html#authenticate-with-service-account-token[authenticate the service account] with {es}. <3> The CA certificate used to sign the node certificates that {es} uses for TLS on the HTTP layer. The certificate is returned as a Base64 encoded string of the ASN.1 DER encoding of the certificate.