HLRC: Get SSL Certificates API (#34135)

This change adds support for the get SSL certificate API to 
the high level rest client.
This commit is contained in:
Ioannis Kakavas 2018-10-15 17:20:34 +01:00 committed by GitHub
parent 9bb620eece
commit 55eaf7a3ff
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 462 additions and 5 deletions

View File

@ -76,6 +76,8 @@ forbiddenApisMain {
addSignatureFiles 'http-signatures'
signaturesFiles += files('src/main/resources/forbidden/rest-high-level-signatures.txt')
}
File nodeCert = file("./testnode.crt")
File nodeTrustStore = file("./testnode.jks")
integTestRunner {
systemProperty 'tests.rest.cluster.username', System.getProperty('tests.rest.cluster.username', 'test_user')
@ -85,11 +87,17 @@ integTestRunner {
integTestCluster {
setting 'xpack.license.self_generated.type', 'trial'
setting 'xpack.security.enabled', 'true'
// Truststore settings are not used since TLS is not enabled. Included for testing the get certificates API
setting 'xpack.ssl.certificate_authorities', 'testnode.crt'
setting 'xpack.security.transport.ssl.truststore.path', 'testnode.jks'
setting 'xpack.security.transport.ssl.truststore.password', 'testnode'
setupCommand 'setupDummyUser',
'bin/elasticsearch-users',
'useradd', System.getProperty('tests.rest.cluster.username', 'test_user'),
'-p', System.getProperty('tests.rest.cluster.password', 'test-password'),
'-r', 'superuser'
extraConfigFile nodeCert.name, nodeCert
extraConfigFile nodeTrustStore.name, nodeTrustStore
waitCondition = { node, ant ->
File tmpFile = new File(node.cwd, 'wait.success')
ant.get(src: "http://${node.httpUri()}/_cluster/health?wait_for_nodes=>=${numNodes}&wait_for_status=yellow",

View File

@ -22,6 +22,8 @@ package org.elasticsearch.client;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.client.security.DisableUserRequest;
import org.elasticsearch.client.security.EnableUserRequest;
import org.elasticsearch.client.security.GetSslCertificatesRequest;
import org.elasticsearch.client.security.GetSslCertificatesResponse;
import org.elasticsearch.client.security.PutUserRequest;
import org.elasticsearch.client.security.PutUserResponse;
import org.elasticsearch.client.security.EmptyResponse;
@ -133,6 +135,33 @@ public final class SecurityClient {
EmptyResponse::fromXContent, listener, emptySet());
}
/**
* Synchronously retrieve the X.509 certificates that are used to encrypt communications in an Elasticsearch cluster.
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-ssl.html">
* the docs</a> for more.
*
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
* @return the response from the get certificates call
* @throws IOException in case there is a problem sending the request or parsing back the response
*/
public GetSslCertificatesResponse getSslCertificates(RequestOptions options) throws IOException {
return restHighLevelClient.performRequestAndParseEntity(GetSslCertificatesRequest.INSTANCE, GetSslCertificatesRequest::getRequest,
options, GetSslCertificatesResponse::fromXContent, emptySet());
}
/**
* Asynchronously retrieve the X.509 certificates that are used to encrypt communications in an Elasticsearch cluster.
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-ssl.html">
* the docs</a> for more.
*
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
* @param listener the listener to be notified upon request completion
*/
public void getSslCertificatesAsync(RequestOptions options, ActionListener<GetSslCertificatesResponse> listener) {
restHighLevelClient.performRequestAsyncAndParseEntity(GetSslCertificatesRequest.INSTANCE, GetSslCertificatesRequest::getRequest,
options, GetSslCertificatesResponse::fromXContent, listener, emptySet());
}
/**
* Change the password of a user of a native realm or built-in user synchronously.
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-change-password.html">

View File

@ -0,0 +1,49 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.client.security;
import org.apache.http.client.methods.HttpGet;
import org.elasticsearch.client.Request;
import org.elasticsearch.client.Validatable;
import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;
import java.io.IOException;
/**
* Request object to retrieve the X.509 certificates that are used to encrypt communications in an Elasticsearch cluster.
*/
public final class GetSslCertificatesRequest implements Validatable, ToXContentObject {
public static final GetSslCertificatesRequest INSTANCE = new GetSslCertificatesRequest();
private final Request request;
private GetSslCertificatesRequest() {
request = new Request(HttpGet.METHOD_NAME, "/_xpack/ssl/certificates");
}
public Request getRequest() {
return request;
}
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
return builder.startObject().endObject();
}
}

View File

@ -0,0 +1,68 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.client.security;
import org.elasticsearch.client.security.support.CertificateInfo;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentParserUtils;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
/**
* Response object when retrieving the X.509 certificates that are used to encrypt communications in an Elasticsearch cluster.
* Returns a list of {@link CertificateInfo} objects describing each of the certificates.
*/
public final class GetSslCertificatesResponse {
private final List<CertificateInfo> certificates;
public GetSslCertificatesResponse(List<CertificateInfo> certificates) {
this.certificates = certificates;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
final GetSslCertificatesResponse that = (GetSslCertificatesResponse) o;
return Objects.equals(this.certificates, that.certificates);
}
@Override
public int hashCode() {
return Objects.hash(certificates);
}
public static GetSslCertificatesResponse fromXContent(XContentParser parser) throws IOException {
List<CertificateInfo> certificates = new ArrayList<>();
XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_ARRAY, parser.nextToken(), parser::getTokenLocation);
while (parser.nextToken() != XContentParser.Token.END_ARRAY) {
certificates.add(CertificateInfo.PARSER.parse(parser, null));
}
return new GetSslCertificatesResponse(certificates);
}
public List<CertificateInfo> getCertificates() {
return certificates;
}
}

View File

@ -0,0 +1,133 @@
/*
* Licensed to Elasticsearch under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.elasticsearch.client.security.support;
import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.ParseField;
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
import org.elasticsearch.common.xcontent.XContentParser;
import java.io.IOException;
import java.util.Objects;
import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
/**
* Simple model of an X.509 certificate
*/
public final class CertificateInfo {
public static final ParseField PATH = new ParseField("path");
public static final ParseField FORMAT = new ParseField("format");
public static final ParseField ALIAS = new ParseField("alias");
public static final ParseField SUBJECT_DN = new ParseField("subject_dn");
public static final ParseField SERIAL_NUMBER = new ParseField("serial_number");
public static final ParseField HAS_PRIVATE_KEY = new ParseField("has_private_key");
public static final ParseField EXPIRY = new ParseField("expiry");
private final String path;
private final String format;
private final String alias;
private final String subjectDn;
private final String serialNumber;
private final boolean hasPrivateKey;
private final String expiry;
public CertificateInfo(String path, String format, @Nullable String alias, String subjectDn, String serialNumber, boolean hasPrivateKey,
String expiry) {
this.path = path;
this.format = format;
this.alias = alias;
this.subjectDn = subjectDn;
this.serialNumber = serialNumber;
this.hasPrivateKey = hasPrivateKey;
this.expiry = expiry;
}
public String getPath() {
return path;
}
public String getFormat() {
return format;
}
public String getAlias() {
return alias;
}
public String getSubjectDn() {
return subjectDn;
}
public String getSerialNumber() {
return serialNumber;
}
public boolean isHasPrivateKey() {
return hasPrivateKey;
}
public String getExpiry() {
return expiry;
}
@SuppressWarnings("unchecked")
public static final ConstructingObjectParser<CertificateInfo, Void> PARSER = new ConstructingObjectParser<>("certificate_info",
true, args -> new CertificateInfo((String) args[0], (String) args[1], (String) args[2], (String) args[3], (String) args[4],
(boolean) args[5], (String) args[6]));
static {
PARSER.declareString(constructorArg(), PATH);
PARSER.declareString(constructorArg(), FORMAT);
PARSER.declareStringOrNull(constructorArg(), ALIAS);
PARSER.declareString(constructorArg(), SUBJECT_DN);
PARSER.declareString(constructorArg(), SERIAL_NUMBER);
PARSER.declareBoolean(constructorArg(), HAS_PRIVATE_KEY);
PARSER.declareString(constructorArg(), EXPIRY);
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (other == null || getClass() != other.getClass()) {
return false;
}
final CertificateInfo that = (CertificateInfo) other;
return this.path.equals(that.path)
&& this.format.equals(that.format)
&& this.hasPrivateKey == that.hasPrivateKey
&& Objects.equals(this.alias, that.alias)
&& this.serialNumber.equals(that.serialNumber)
&& this.subjectDn.equals(that.subjectDn)
&& this.expiry.equals(that.expiry);
}
@Override
public int hashCode() {
return Objects.hash(path, format, alias, subjectDn, serialNumber, hasPrivateKey, expiry);
}
public static CertificateInfo fromXContent(XContentParser parser) throws IOException {
return PARSER.parse(parser, null);
}
}

View File

@ -721,10 +721,16 @@ public class RestHighLevelClientTests extends ESTestCase {
methods.containsKey(apiName.substring(0, apiName.length() - 6)));
assertThat(method.getReturnType(), equalTo(Void.TYPE));
assertEquals(0, method.getExceptionTypes().length);
if (apiName.equals("security.get_ssl_certificates_async")) {
assertEquals(2, method.getParameterTypes().length);
assertThat(method.getParameterTypes()[0], equalTo(RequestOptions.class));
assertThat(method.getParameterTypes()[1], equalTo(ActionListener.class));
} else {
assertEquals(3, method.getParameterTypes().length);
assertThat(method.getParameterTypes()[0].getSimpleName(), endsWith("Request"));
assertThat(method.getParameterTypes()[1], equalTo(RequestOptions.class));
assertThat(method.getParameterTypes()[2], equalTo(ActionListener.class));
}
} else {
//A few methods return a boolean rather than a response object
if (apiName.equals("ping") || apiName.contains("exist")) {
@ -735,7 +741,7 @@ public class RestHighLevelClientTests extends ESTestCase {
assertEquals(1, method.getExceptionTypes().length);
//a few methods don't accept a request object as argument
if (apiName.equals("ping") || apiName.equals("info")) {
if (apiName.equals("ping") || apiName.equals("info") || apiName.equals("security.get_ssl_certificates")) {
assertEquals(1, method.getParameterTypes().length);
assertThat(method.getParameterTypes()[0], equalTo(RequestOptions.class));
} else {

View File

@ -27,12 +27,17 @@ import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.security.ChangePasswordRequest;
import org.elasticsearch.client.security.DisableUserRequest;
import org.elasticsearch.client.security.EnableUserRequest;
import org.elasticsearch.client.security.GetSslCertificatesResponse;
import org.elasticsearch.client.security.PutUserRequest;
import org.elasticsearch.client.security.PutUserResponse;
import org.elasticsearch.client.security.RefreshPolicy;
import org.elasticsearch.client.security.EmptyResponse;
import org.elasticsearch.client.security.support.CertificateInfo;
import org.hamcrest.Matchers;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@ -175,6 +180,87 @@ public class SecurityDocumentationIT extends ESRestHighLevelClientTestCase {
}
}
public void testGetSslCertificates() throws Exception {
RestHighLevelClient client = highLevelClient();
{
//tag::get-certificates-execute
GetSslCertificatesResponse response = client.security().getSslCertificates(RequestOptions.DEFAULT);
//end::get-certificates-execute
assertNotNull(response);
//tag::get-certificates-response
List<CertificateInfo> certificates = response.getCertificates(); // <1>
//end::get-certificates-response
assertThat(certificates.size(), Matchers.equalTo(9));
final Iterator<CertificateInfo> it = certificates.iterator();
CertificateInfo c = it.next();
assertThat(c.getSubjectDn(), Matchers.equalTo("CN=testnode-client-profile"));
assertThat(c.getPath(), Matchers.equalTo("testnode.jks"));
assertThat(c.getFormat(), Matchers.equalTo("jks"));
c = it.next();
assertThat(c.getSubjectDn(), Matchers.equalTo("CN=Elasticsearch Test Node, OU=elasticsearch, O=org"));
assertThat(c.getPath(), Matchers.equalTo("testnode.crt"));
assertThat(c.getFormat(), Matchers.equalTo("PEM"));
c = it.next();
assertThat(c.getSubjectDn(), Matchers.equalTo("CN=OpenLDAP, OU=Elasticsearch, O=Elastic, L=Mountain View, ST=CA, C=US"));
assertThat(c.getPath(), Matchers.equalTo("testnode.jks"));
assertThat(c.getFormat(), Matchers.equalTo("jks"));
c = it.next();
assertThat(c.getSubjectDn(), Matchers.equalTo("CN=Elasticsearch Test Node, OU=elasticsearch, O=org"));
assertThat(c.getPath(), Matchers.equalTo("testnode.jks"));
assertThat(c.getFormat(), Matchers.equalTo("jks"));
c = it.next();
assertThat(c.getSubjectDn(), Matchers.equalTo("CN=Elasticsearch Test Client, OU=elasticsearch, O=org"));
assertThat(c.getPath(), Matchers.equalTo("testnode.jks"));
assertThat(c.getFormat(), Matchers.equalTo("jks"));
c = it.next();
assertThat(c.getSubjectDn(), Matchers.equalTo("CN=ad-ELASTICSEARCHAD-CA, DC=ad, DC=test, DC=elasticsearch, DC=com"));
assertThat(c.getPath(), Matchers.equalTo("testnode.jks"));
assertThat(c.getFormat(), Matchers.equalTo("jks"));
c = it.next();
assertThat(c.getSubjectDn(), Matchers.equalTo("CN=Elasticsearch Test Node"));
assertThat(c.getPath(), Matchers.equalTo("testnode.jks"));
assertThat(c.getFormat(), Matchers.equalTo("jks"));
c = it.next();
assertThat(c.getSubjectDn(), Matchers.equalTo("CN=samba4"));
assertThat(c.getPath(), Matchers.equalTo("testnode.jks"));
assertThat(c.getFormat(), Matchers.equalTo("jks"));
c = it.next();
assertThat(c.getSubjectDn(), Matchers.equalTo("CN=Elasticsearch Test Node"));
assertThat(c.getPath(), Matchers.equalTo("testnode.jks"));
assertThat(c.getFormat(), Matchers.equalTo("jks"));
}
{
// tag::get-certificates-execute-listener
ActionListener<GetSslCertificatesResponse> listener = new ActionListener<GetSslCertificatesResponse>() {
@Override
public void onResponse(GetSslCertificatesResponse getSslCertificatesResponse) {
// <1>
}
@Override
public void onFailure(Exception e) {
// <2>
}
};
// end::get-certificates-execute-listener
// Replace the empty listener by a blocking listener in test
final CountDownLatch latch = new CountDownLatch(1);
listener = new LatchedActionListener<>(listener, latch);
// tag::get-certificates-execute-async
client.security().getSslCertificatesAsync(RequestOptions.DEFAULT, listener); // <1>
// end::end-certificates-execute-async
assertTrue(latch.await(30L, TimeUnit.SECONDS));
}
}
public void testChangePassword() throws Exception {
RestHighLevelClient client = highLevelClient();
char[] password = new char[]{'p', 'a', 's', 's', 'w', 'o', 'r', 'd'};

View File

@ -0,0 +1,23 @@
-----BEGIN CERTIFICATE-----
MIID0zCCArugAwIBAgIJALi5bDfjMszLMA0GCSqGSIb3DQEBCwUAMEgxDDAKBgNV
BAoTA29yZzEWMBQGA1UECxMNZWxhc3RpY3NlYXJjaDEgMB4GA1UEAxMXRWxhc3Rp
Y3NlYXJjaCBUZXN0IE5vZGUwHhcNMTUwOTIzMTg1MjU3WhcNMTkwOTIyMTg1MjU3
WjBIMQwwCgYDVQQKEwNvcmcxFjAUBgNVBAsTDWVsYXN0aWNzZWFyY2gxIDAeBgNV
BAMTF0VsYXN0aWNzZWFyY2ggVGVzdCBOb2RlMIIBIjANBgkqhkiG9w0BAQEFAAOC
AQ8AMIIBCgKCAQEA3rGZ1QbsW0+MuyrSLmMfDFKtLBkIFW8V0gRuurFg1PUKKNR1
Mq2tMVwjjYETAU/UY0iKZOzjgvYPKhDTYBTte/WHR1ZK4CYVv7TQX/gtFQG/ge/c
7u0sLch9p7fbd+/HZiLS/rBEZDIohvgUvzvnA8+OIYnw4kuxKo/5iboAIS41klMg
/lATm8V71LMY68inht71/ZkQoAHKgcR9z4yNYvQ1WqKG8DG8KROXltll3sTrKbl5
zJhn660es/1ZnR6nvwt6xnSTl/mNHMjkfv1bs4rJ/py3qPxicdoSIn/KyojUcgHV
F38fuAy2CQTdjVG5fWj9iz+mQvLm3+qsIYQdFwIDAQABo4G/MIG8MAkGA1UdEwQC
MAAwHQYDVR0OBBYEFEMMWLWQi/g83PzlHYqAVnty5L7HMIGPBgNVHREEgYcwgYSC
CWxvY2FsaG9zdIIVbG9jYWxob3N0LmxvY2FsZG9tYWluggpsb2NhbGhvc3Q0ghds
b2NhbGhvc3Q0LmxvY2FsZG9tYWluNIIKbG9jYWxob3N0NoIXbG9jYWxob3N0Ni5s
b2NhbGRvbWFpbjaHBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwDQYJKoZIhvcNAQEL
BQADggEBAMjGGXT8Nt1tbl2GkiKtmiuGE2Ej66YuZ37WSJViaRNDVHLlg87TCcHe
k2rdO+6sFqQbbzEfwQ05T7xGmVu7tm54HwKMRugoQ3wct0bQC5wEWYN+oMDvSyO6
M28mZwWb4VtR2IRyWP+ve5DHwTM9mxWa6rBlGzsQqH6YkJpZojzqk/mQTug+Y8aE
mVoqRIPMHq9ob+S9qd5lp09+MtYpwPfTPx/NN+xMEooXWW/ARfpGhWPkg/FuCu4z
1tFmCqHgNcWirzMm3dQpF78muE9ng6OB2MXQwL4VgnVkxmlZNHbkR2v/t8MyZJxC
y4g6cTMM3S/UMt5/+aIB2JAuMKyuD+A=
-----END CERTIFICATE-----

Binary file not shown.

View File

@ -0,0 +1,53 @@
[[java-rest-high-security-get-certificates]]
=== SSL Certificate API
[[java-rest-high-security-get-certificates-execution]]
==== Execution
The X.509 Certificates that are used to encrypt communications in an
Elasticsearch cluster using the `security().getSslCertificates()` method:
["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests}/SecurityDocumentationIT.java[get-certificates-execute]
--------------------------------------------------
[[java-rest-high-security-get-certificates-response]]
==== Response
The returned `GetSslCertificatesResponse` contains a single field, `certificates`.
This field, accessed with `getCertificates` returns a List of `CertificateInfo`
objects containing the information for all the certificates used.
["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests}/SecurityDocumentationIT.java[get-certificates-response]
--------------------------------------------------
<1> `certificates` is a List of `CertificateInfo`
[[java-rest-high-security-get-certificates-execute-async]]
==== Asynchronous Execution
This request can be executed asynchronously using the `security().getSslCertificatesAsync()`
method:
["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests}/SecurityDocumentationIT.java[get-certificates-execute-async]
--------------------------------------------------
<1> The `ActionListener` to use when the execution completes.
The asynchronous method does not block and returns immediately. Once the request
has completed the `ActionListener` is called back using the `onResponse` method
if the execution successfully completed or using the `onFailure` method if
it failed.
A typical listener for a `GetSslCertificatesResponse` looks like:
["source","java",subs="attributes,callouts,macros"]
--------------------------------------------------
include-tagged::{doc-tests}/SecurityDocumentationIT.java[get-certificates-execute-listener]
--------------------------------------------------
<1> Called when the execution is successfully completed. The response is
provided as an argument.
<2> Called in case of failure. The raised exception is provided as an argument.

View File

@ -305,11 +305,13 @@ The Java High Level REST Client supports the following Security APIs:
* <<java-rest-high-security-enable-user>>
* <<java-rest-high-security-disable-user>>
* <<java-rest-high-security-change-password>>
* <<java-rest-high-security-get-certificates>>
include::security/put-user.asciidoc[]
include::security/enable-user.asciidoc[]
include::security/disable-user.asciidoc[]
include::security/change-password.asciidoc[]
include::security/get-certificates.asciidoc[]
== Watcher APIs