Skip to content

Commit 55eaf7a

Browse files
authored
HLRC: Get SSL Certificates API (#34135)
This change adds support for the get SSL certificate API to the high level rest client.
1 parent 9bb620e commit 55eaf7a

File tree

11 files changed

+462
-5
lines changed

11 files changed

+462
-5
lines changed

client/rest-high-level/build.gradle

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ forbiddenApisMain {
7676
addSignatureFiles 'http-signatures'
7777
signaturesFiles += files('src/main/resources/forbidden/rest-high-level-signatures.txt')
7878
}
79+
File nodeCert = file("./testnode.crt")
80+
File nodeTrustStore = file("./testnode.jks")
7981

8082
integTestRunner {
8183
systemProperty 'tests.rest.cluster.username', System.getProperty('tests.rest.cluster.username', 'test_user')
@@ -85,11 +87,17 @@ integTestRunner {
8587
integTestCluster {
8688
setting 'xpack.license.self_generated.type', 'trial'
8789
setting 'xpack.security.enabled', 'true'
90+
// Truststore settings are not used since TLS is not enabled. Included for testing the get certificates API
91+
setting 'xpack.ssl.certificate_authorities', 'testnode.crt'
92+
setting 'xpack.security.transport.ssl.truststore.path', 'testnode.jks'
93+
setting 'xpack.security.transport.ssl.truststore.password', 'testnode'
8894
setupCommand 'setupDummyUser',
8995
'bin/elasticsearch-users',
9096
'useradd', System.getProperty('tests.rest.cluster.username', 'test_user'),
9197
'-p', System.getProperty('tests.rest.cluster.password', 'test-password'),
9298
'-r', 'superuser'
99+
extraConfigFile nodeCert.name, nodeCert
100+
extraConfigFile nodeTrustStore.name, nodeTrustStore
93101
waitCondition = { node, ant ->
94102
File tmpFile = new File(node.cwd, 'wait.success')
95103
ant.get(src: "http://${node.httpUri()}/_cluster/health?wait_for_nodes=>=${numNodes}&wait_for_status=yellow",

client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import org.elasticsearch.action.ActionListener;
2323
import org.elasticsearch.client.security.DisableUserRequest;
2424
import org.elasticsearch.client.security.EnableUserRequest;
25+
import org.elasticsearch.client.security.GetSslCertificatesRequest;
26+
import org.elasticsearch.client.security.GetSslCertificatesResponse;
2527
import org.elasticsearch.client.security.PutUserRequest;
2628
import org.elasticsearch.client.security.PutUserResponse;
2729
import org.elasticsearch.client.security.EmptyResponse;
@@ -133,6 +135,33 @@ public void disableUserAsync(DisableUserRequest request, RequestOptions options,
133135
EmptyResponse::fromXContent, listener, emptySet());
134136
}
135137

138+
/**
139+
* Synchronously retrieve the X.509 certificates that are used to encrypt communications in an Elasticsearch cluster.
140+
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-ssl.html">
141+
* the docs</a> for more.
142+
*
143+
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
144+
* @return the response from the get certificates call
145+
* @throws IOException in case there is a problem sending the request or parsing back the response
146+
*/
147+
public GetSslCertificatesResponse getSslCertificates(RequestOptions options) throws IOException {
148+
return restHighLevelClient.performRequestAndParseEntity(GetSslCertificatesRequest.INSTANCE, GetSslCertificatesRequest::getRequest,
149+
options, GetSslCertificatesResponse::fromXContent, emptySet());
150+
}
151+
152+
/**
153+
* Asynchronously retrieve the X.509 certificates that are used to encrypt communications in an Elasticsearch cluster.
154+
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-ssl.html">
155+
* the docs</a> for more.
156+
*
157+
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
158+
* @param listener the listener to be notified upon request completion
159+
*/
160+
public void getSslCertificatesAsync(RequestOptions options, ActionListener<GetSslCertificatesResponse> listener) {
161+
restHighLevelClient.performRequestAsyncAndParseEntity(GetSslCertificatesRequest.INSTANCE, GetSslCertificatesRequest::getRequest,
162+
options, GetSslCertificatesResponse::fromXContent, listener, emptySet());
163+
}
164+
136165
/**
137166
* Change the password of a user of a native realm or built-in user synchronously.
138167
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-change-password.html">
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.client.security;
21+
22+
import org.apache.http.client.methods.HttpGet;
23+
import org.elasticsearch.client.Request;
24+
import org.elasticsearch.client.Validatable;
25+
import org.elasticsearch.common.xcontent.ToXContentObject;
26+
import org.elasticsearch.common.xcontent.XContentBuilder;
27+
28+
import java.io.IOException;
29+
30+
/**
31+
* Request object to retrieve the X.509 certificates that are used to encrypt communications in an Elasticsearch cluster.
32+
*/
33+
public final class GetSslCertificatesRequest implements Validatable, ToXContentObject {
34+
35+
public static final GetSslCertificatesRequest INSTANCE = new GetSslCertificatesRequest();
36+
private final Request request;
37+
38+
private GetSslCertificatesRequest() {
39+
request = new Request(HttpGet.METHOD_NAME, "/_xpack/ssl/certificates");
40+
}
41+
42+
public Request getRequest() {
43+
return request;
44+
}
45+
46+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
47+
return builder.startObject().endObject();
48+
}
49+
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.client.security;
21+
22+
import org.elasticsearch.client.security.support.CertificateInfo;
23+
import org.elasticsearch.common.xcontent.XContentParser;
24+
import org.elasticsearch.common.xcontent.XContentParserUtils;
25+
26+
import java.io.IOException;
27+
import java.util.ArrayList;
28+
import java.util.List;
29+
import java.util.Objects;
30+
31+
/**
32+
* Response object when retrieving the X.509 certificates that are used to encrypt communications in an Elasticsearch cluster.
33+
* Returns a list of {@link CertificateInfo} objects describing each of the certificates.
34+
*/
35+
public final class GetSslCertificatesResponse {
36+
37+
private final List<CertificateInfo> certificates;
38+
39+
public GetSslCertificatesResponse(List<CertificateInfo> certificates) {
40+
this.certificates = certificates;
41+
}
42+
43+
@Override
44+
public boolean equals(Object o) {
45+
if (this == o) return true;
46+
if (o == null || getClass() != o.getClass()) return false;
47+
final GetSslCertificatesResponse that = (GetSslCertificatesResponse) o;
48+
return Objects.equals(this.certificates, that.certificates);
49+
}
50+
51+
@Override
52+
public int hashCode() {
53+
return Objects.hash(certificates);
54+
}
55+
56+
public static GetSslCertificatesResponse fromXContent(XContentParser parser) throws IOException {
57+
List<CertificateInfo> certificates = new ArrayList<>();
58+
XContentParserUtils.ensureExpectedToken(XContentParser.Token.START_ARRAY, parser.nextToken(), parser::getTokenLocation);
59+
while (parser.nextToken() != XContentParser.Token.END_ARRAY) {
60+
certificates.add(CertificateInfo.PARSER.parse(parser, null));
61+
}
62+
return new GetSslCertificatesResponse(certificates);
63+
}
64+
65+
public List<CertificateInfo> getCertificates() {
66+
return certificates;
67+
}
68+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.client.security.support;
21+
22+
import org.elasticsearch.common.Nullable;
23+
import org.elasticsearch.common.ParseField;
24+
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
25+
import org.elasticsearch.common.xcontent.XContentParser;
26+
27+
import java.io.IOException;
28+
import java.util.Objects;
29+
30+
import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
31+
32+
/**
33+
* Simple model of an X.509 certificate
34+
*/
35+
public final class CertificateInfo {
36+
public static final ParseField PATH = new ParseField("path");
37+
public static final ParseField FORMAT = new ParseField("format");
38+
public static final ParseField ALIAS = new ParseField("alias");
39+
public static final ParseField SUBJECT_DN = new ParseField("subject_dn");
40+
public static final ParseField SERIAL_NUMBER = new ParseField("serial_number");
41+
public static final ParseField HAS_PRIVATE_KEY = new ParseField("has_private_key");
42+
public static final ParseField EXPIRY = new ParseField("expiry");
43+
44+
private final String path;
45+
private final String format;
46+
private final String alias;
47+
private final String subjectDn;
48+
private final String serialNumber;
49+
private final boolean hasPrivateKey;
50+
private final String expiry;
51+
52+
public CertificateInfo(String path, String format, @Nullable String alias, String subjectDn, String serialNumber, boolean hasPrivateKey,
53+
String expiry) {
54+
this.path = path;
55+
this.format = format;
56+
this.alias = alias;
57+
this.subjectDn = subjectDn;
58+
this.serialNumber = serialNumber;
59+
this.hasPrivateKey = hasPrivateKey;
60+
this.expiry = expiry;
61+
}
62+
63+
public String getPath() {
64+
return path;
65+
}
66+
67+
public String getFormat() {
68+
return format;
69+
}
70+
71+
public String getAlias() {
72+
return alias;
73+
}
74+
75+
public String getSubjectDn() {
76+
return subjectDn;
77+
}
78+
79+
public String getSerialNumber() {
80+
return serialNumber;
81+
}
82+
83+
public boolean isHasPrivateKey() {
84+
return hasPrivateKey;
85+
}
86+
87+
public String getExpiry() {
88+
return expiry;
89+
}
90+
91+
@SuppressWarnings("unchecked")
92+
public static final ConstructingObjectParser<CertificateInfo, Void> PARSER = new ConstructingObjectParser<>("certificate_info",
93+
true, args -> new CertificateInfo((String) args[0], (String) args[1], (String) args[2], (String) args[3], (String) args[4],
94+
(boolean) args[5], (String) args[6]));
95+
96+
static {
97+
PARSER.declareString(constructorArg(), PATH);
98+
PARSER.declareString(constructorArg(), FORMAT);
99+
PARSER.declareStringOrNull(constructorArg(), ALIAS);
100+
PARSER.declareString(constructorArg(), SUBJECT_DN);
101+
PARSER.declareString(constructorArg(), SERIAL_NUMBER);
102+
PARSER.declareBoolean(constructorArg(), HAS_PRIVATE_KEY);
103+
PARSER.declareString(constructorArg(), EXPIRY);
104+
}
105+
106+
@Override
107+
public boolean equals(Object other) {
108+
if (this == other) {
109+
return true;
110+
}
111+
if (other == null || getClass() != other.getClass()) {
112+
return false;
113+
}
114+
115+
final CertificateInfo that = (CertificateInfo) other;
116+
return this.path.equals(that.path)
117+
&& this.format.equals(that.format)
118+
&& this.hasPrivateKey == that.hasPrivateKey
119+
&& Objects.equals(this.alias, that.alias)
120+
&& this.serialNumber.equals(that.serialNumber)
121+
&& this.subjectDn.equals(that.subjectDn)
122+
&& this.expiry.equals(that.expiry);
123+
}
124+
125+
@Override
126+
public int hashCode() {
127+
return Objects.hash(path, format, alias, subjectDn, serialNumber, hasPrivateKey, expiry);
128+
}
129+
130+
public static CertificateInfo fromXContent(XContentParser parser) throws IOException {
131+
return PARSER.parse(parser, null);
132+
}
133+
}

client/rest-high-level/src/test/java/org/elasticsearch/client/RestHighLevelClientTests.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -721,10 +721,16 @@ public void testApiNamingConventions() throws Exception {
721721
methods.containsKey(apiName.substring(0, apiName.length() - 6)));
722722
assertThat(method.getReturnType(), equalTo(Void.TYPE));
723723
assertEquals(0, method.getExceptionTypes().length);
724-
assertEquals(3, method.getParameterTypes().length);
725-
assertThat(method.getParameterTypes()[0].getSimpleName(), endsWith("Request"));
726-
assertThat(method.getParameterTypes()[1], equalTo(RequestOptions.class));
727-
assertThat(method.getParameterTypes()[2], equalTo(ActionListener.class));
724+
if (apiName.equals("security.get_ssl_certificates_async")) {
725+
assertEquals(2, method.getParameterTypes().length);
726+
assertThat(method.getParameterTypes()[0], equalTo(RequestOptions.class));
727+
assertThat(method.getParameterTypes()[1], equalTo(ActionListener.class));
728+
} else {
729+
assertEquals(3, method.getParameterTypes().length);
730+
assertThat(method.getParameterTypes()[0].getSimpleName(), endsWith("Request"));
731+
assertThat(method.getParameterTypes()[1], equalTo(RequestOptions.class));
732+
assertThat(method.getParameterTypes()[2], equalTo(ActionListener.class));
733+
}
728734
} else {
729735
//A few methods return a boolean rather than a response object
730736
if (apiName.equals("ping") || apiName.contains("exist")) {
@@ -735,7 +741,7 @@ public void testApiNamingConventions() throws Exception {
735741

736742
assertEquals(1, method.getExceptionTypes().length);
737743
//a few methods don't accept a request object as argument
738-
if (apiName.equals("ping") || apiName.equals("info")) {
744+
if (apiName.equals("ping") || apiName.equals("info") || apiName.equals("security.get_ssl_certificates")) {
739745
assertEquals(1, method.getParameterTypes().length);
740746
assertThat(method.getParameterTypes()[0], equalTo(RequestOptions.class));
741747
} else {

0 commit comments

Comments
 (0)