Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
cb78577
Add delegation feature to PKI realm (#44106)
albertzaharovits Jul 18, 2019
7f999de
Merge branch 'master' into proxied-pki
albertzaharovits Jul 18, 2019
2a3d1b5
Merge branch 'master' into proxied-pki
albertzaharovits Jul 23, 2019
7538b1a
Streamable adjustments
albertzaharovits Jul 23, 2019
dbe6bbe
Merge branch 'master' into proxied-pki
albertzaharovits Jul 24, 2019
a4bbba3
Adjust PKI bootstrap check for delegation (#44767)
albertzaharovits Jul 24, 2019
a17ea2d
PKIRealm with delegation enabled requires token service (#44821)
albertzaharovits Jul 26, 2019
938a3cb
Merge branch 'master' into proxied-pki
albertzaharovits Jul 29, 2019
c21025d
Merge branch 'master' into proxied-pki
albertzaharovits Jul 31, 2019
16496d6
PKIRealm delegation unsupported without a truststore (#45011)
albertzaharovits Aug 1, 2019
6cf863f
X509AuthnToken cache key change for proxied-PKI (#44810)
albertzaharovits Aug 1, 2019
f71cd90
Add REST handler for PKI delegation (#44561)
albertzaharovits Aug 1, 2019
433a331
Merge branch 'master' into proxied-pki
albertzaharovits Aug 5, 2019
e599560
Add pki_delegated_by_* to User metadata (#44873)
albertzaharovits Aug 5, 2019
9277980
PKI realm usage stats includes delegation (#44871)
albertzaharovits Aug 5, 2019
6e240fc
Merge branch 'master' into proxied-pki
albertzaharovits Aug 6, 2019
1d6e31b
Cluster privilege for proxied PKI (#44862)
albertzaharovits Aug 7, 2019
c63dc62
Merge branch 'master' into proxied-pki
albertzaharovits Aug 9, 2019
c89a225
Merge branch 'master' into proxied-pki
albertzaharovits Aug 15, 2019
0017a77
ClusterPrivilegeResolver merge conflict
albertzaharovits Aug 15, 2019
d4fddb1
Merge branch 'master' into proxied-pki
albertzaharovits Aug 22, 2019
69c8b22
Merge branch 'master' into proxied-pki
albertzaharovits Aug 23, 2019
57d435d
Reference DOCS for proxied PKI (#45259)
albertzaharovits Aug 23, 2019
2519a64
Docs reference fix
albertzaharovits Aug 26, 2019
4b8027f
Merge branch 'master' into proxied-pki
albertzaharovits Aug 26, 2019
47a93b2
Async returns cancellable
albertzaharovits Aug 26, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions client/rest-high-level/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ processTestResources {
from({ zipTree(configurations.restSpec.singleFile) }) {
include 'rest-api-spec/api/**'
}
from(project(':client:rest-high-level').file('src/test/resources'))
}

dependencyLicenses {
Expand Down Expand Up @@ -117,6 +118,7 @@ if (isEclipse) {

File nodeCert = file("./testnode.crt")
File nodeTrustStore = file("./testnode.jks")
File pkiTrustCert = file("./src/test/resources/org/elasticsearch/client/security/delegate_pki/testRootCA.crt")

integTest.runner {
systemProperty 'tests.rest.cluster.username', System.getProperty('tests.rest.cluster.username', 'test_user')
Expand All @@ -134,11 +136,18 @@ testClusters.integTest {
// Truststore settings are not used since TLS is not enabled. Included for testing the get certificates API
setting 'xpack.security.http.ssl.certificate_authorities', 'testnode.crt'
setting 'xpack.security.transport.ssl.truststore.path', 'testnode.jks'
setting 'xpack.security.authc.realms.file.default_file.order', '0'
setting 'xpack.security.authc.realms.native.default_native.order', '1'
setting 'xpack.security.authc.realms.pki.pki1.order', '2'
setting 'xpack.security.authc.realms.pki.pki1.certificate_authorities', '[ "testRootCA.crt" ]'
setting 'xpack.security.authc.realms.pki.pki1.delegation.enabled', 'true'

setting 'indices.lifecycle.poll_interval', '1000ms'
keystore 'xpack.security.transport.ssl.truststore.secure_password', 'testnode'
user username: System.getProperty('tests.rest.cluster.username', 'test_user'),
password: System.getProperty('tests.rest.cluster.password', 'test-password')

extraConfigFile nodeCert.name, nodeCert
extraConfigFile nodeTrustStore.name, nodeTrustStore
extraConfigFile pkiTrustCert.name, pkiTrustCert
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
import org.elasticsearch.client.security.CreateApiKeyResponse;
import org.elasticsearch.client.security.CreateTokenRequest;
import org.elasticsearch.client.security.CreateTokenResponse;
import org.elasticsearch.client.security.DelegatePkiAuthenticationRequest;
import org.elasticsearch.client.security.DelegatePkiAuthenticationResponse;
import org.elasticsearch.client.security.DeletePrivilegesRequest;
import org.elasticsearch.client.security.DeletePrivilegesResponse;
import org.elasticsearch.client.security.DeleteRoleMappingRequest;
Expand Down Expand Up @@ -1005,4 +1007,39 @@ public Cancellable invalidateApiKeyAsync(final InvalidateApiKeyRequest request,
return restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::invalidateApiKey, options,
InvalidateApiKeyResponse::fromXContent, listener, emptySet());
}

/**
* Get an Elasticsearch access token from an {@code X509Certificate} chain. The certificate chain is that of the client from a mutually
* authenticated TLS session, and it is validated by the PKI realms with {@code delegation.enabled} toggled to {@code true}.<br>
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-delegate-pki-authentication.html"> the
* docs</a> for more details.
*
* @param request the request containing the certificate chain
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
* @return the response from the delegate-pki-authentication API key call
* @throws IOException in case there is a problem sending the request or parsing back the response
*/
public DelegatePkiAuthenticationResponse delegatePkiAuthentication(DelegatePkiAuthenticationRequest request, RequestOptions options)
throws IOException {
return restHighLevelClient.performRequestAndParseEntity(request, SecurityRequestConverters::delegatePkiAuthentication, options,
DelegatePkiAuthenticationResponse::fromXContent, emptySet());
}

/**
* Asynchronously get an Elasticsearch access token from an {@code X509Certificate} chain. The certificate chain is that of the client
* from a mutually authenticated TLS session, and it is validated by the PKI realms with {@code delegation.enabled} toggled to
* {@code true}.<br>
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-delegate-pki-authentication.html"> the
* docs</a> for more details.
*
* @param request the request containing the certificate chain
* @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
* @return cancellable that may be used to cancel the request
*/
public Cancellable delegatePkiAuthenticationAsync(DelegatePkiAuthenticationRequest request, RequestOptions options,
ActionListener<DelegatePkiAuthenticationResponse> listener) {
return restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::delegatePkiAuthentication, options,
DelegatePkiAuthenticationResponse::fromXContent, listener, emptySet());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import org.elasticsearch.client.security.ClearRolesCacheRequest;
import org.elasticsearch.client.security.CreateApiKeyRequest;
import org.elasticsearch.client.security.CreateTokenRequest;
import org.elasticsearch.client.security.DelegatePkiAuthenticationRequest;
import org.elasticsearch.client.security.DeletePrivilegesRequest;
import org.elasticsearch.client.security.DeleteRoleMappingRequest;
import org.elasticsearch.client.security.DeleteRoleRequest;
Expand Down Expand Up @@ -221,6 +222,12 @@ static Request createToken(CreateTokenRequest createTokenRequest) throws IOExcep
return request;
}

static Request delegatePkiAuthentication(DelegatePkiAuthenticationRequest delegatePkiAuthenticationRequest) throws IOException {
Request request = new Request(HttpPost.METHOD_NAME, "/_security/delegate_pki");
request.setEntity(createEntity(delegatePkiAuthenticationRequest, REQUEST_BODY_CONTENT_TYPE));
return request;
}

static Request invalidateToken(InvalidateTokenRequest invalidateTokenRequest) throws IOException {
Request request = new Request(HttpDelete.METHOD_NAME, "/_security/oauth2/token");
request.setEntity(createEntity(invalidateTokenRequest, REQUEST_BODY_CONTENT_TYPE));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/*
* 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.Validatable;
import org.elasticsearch.client.ValidationException;
import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;

import java.io.IOException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

import static java.util.Collections.unmodifiableList;

public final class DelegatePkiAuthenticationRequest implements Validatable, ToXContentObject {

private final List<X509Certificate> x509CertificateChain;

public DelegatePkiAuthenticationRequest(final List<X509Certificate> x509CertificateChain) {
if (x509CertificateChain == null || x509CertificateChain.isEmpty()) {
throw new IllegalArgumentException("certificate chain must not be empty or null");
}
this.x509CertificateChain = unmodifiableList(x509CertificateChain);
}

@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject().startArray("x509_certificate_chain");
try {
for (X509Certificate cert : x509CertificateChain) {
builder.value(Base64.getEncoder().encodeToString(cert.getEncoded()));
}
} catch (CertificateEncodingException e) {
throw new IOException(e);
}
return builder.endArray().endObject();
}

public List<X509Certificate> getCertificateChain() {
return this.x509CertificateChain;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final DelegatePkiAuthenticationRequest that = (DelegatePkiAuthenticationRequest) o;
return Objects.equals(x509CertificateChain, that.x509CertificateChain);
}

@Override
public int hashCode() {
return Objects.hash(x509CertificateChain);
}

@Override
public Optional<ValidationException> validate() {
ValidationException validationException = new ValidationException();
if (false == isOrderedCertificateChain(x509CertificateChain)) {
validationException.addValidationError("certificates chain must be an ordered chain");
}
return validationException.validationErrors().isEmpty() ? Optional.empty() : Optional.of(validationException);
}

/**
* Checks that the {@code X509Certificate} list is ordered, such that the end-entity certificate is first and it is followed by any
* certificate authorities'. The check validates that the {@code issuer} of every certificate is the {@code subject} of the certificate
* in the next array position. No other certificate attributes are checked.
*/
private static boolean isOrderedCertificateChain(List<X509Certificate> chain) {
for (int i = 1; i < chain.size(); i++) {
X509Certificate cert = chain.get(i - 1);
X509Certificate issuer = chain.get(i);
if (false == cert.getIssuerX500Principal().equals(issuer.getSubjectX500Principal())) {
return false;
}
}
return true;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* 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.common.ParseField;
import org.elasticsearch.common.unit.TimeValue;
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;

public final class DelegatePkiAuthenticationResponse {

private final String accessToken;
private final String type;
private final TimeValue expiresIn;

public DelegatePkiAuthenticationResponse(String accessToken, String type, TimeValue expiresIn) {
this.accessToken = accessToken;
this.type = type;
this.expiresIn = expiresIn;
}

public String getAccessToken() {
return accessToken;
}

public String getType() {
return type;
}

public TimeValue getExpiresIn() {
return expiresIn;
}

@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
final DelegatePkiAuthenticationResponse that = (DelegatePkiAuthenticationResponse) o;
return Objects.equals(accessToken, that.accessToken) &&
Objects.equals(type, that.type) &&
Objects.equals(expiresIn, that.expiresIn);
}

@Override
public int hashCode() {
return Objects.hash(accessToken, type, expiresIn);
}

private static final ConstructingObjectParser<DelegatePkiAuthenticationResponse, Void> PARSER = new ConstructingObjectParser<>(
"delegate_pki_response", true,
args -> new DelegatePkiAuthenticationResponse((String) args[0], (String) args[1], TimeValue.timeValueSeconds((Long) args[2])));

static {
PARSER.declareString(constructorArg(), new ParseField("access_token"));
PARSER.declareString(constructorArg(), new ParseField("type"));
PARSER.declareLong(constructorArg(), new ParseField("expires_in"));
}

public static DelegatePkiAuthenticationResponse fromXContent(XContentParser parser) throws IOException {
return PARSER.parse(parser, null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import org.elasticsearch.client.security.ChangePasswordRequest;
import org.elasticsearch.client.security.CreateApiKeyRequest;
import org.elasticsearch.client.security.CreateTokenRequest;
import org.elasticsearch.client.security.DelegatePkiAuthenticationRequest;
import org.elasticsearch.client.security.DeletePrivilegesRequest;
import org.elasticsearch.client.security.DeleteRoleMappingRequest;
import org.elasticsearch.client.security.DeleteRoleRequest;
Expand Down Expand Up @@ -58,6 +59,7 @@
import org.elasticsearch.test.ESTestCase;

import java.io.IOException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
Expand All @@ -67,6 +69,8 @@

import static org.elasticsearch.client.RequestConvertersTests.assertToXContentBody;
import static org.hamcrest.Matchers.equalTo;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class SecurityRequestConvertersTests extends ESTestCase {

Expand Down Expand Up @@ -304,6 +308,18 @@ public void testCreateTokenWithClientCredentialsGrant() throws Exception {
assertToXContentBody(createTokenRequest, request.getEntity());
}

public void testDelegatePkiAuthentication() throws Exception {
X509Certificate mockCertificate = mock(X509Certificate.class);
when(mockCertificate.getEncoded()).thenReturn(new byte[0]);
DelegatePkiAuthenticationRequest delegatePkiAuthenticationRequest = new DelegatePkiAuthenticationRequest(
Arrays.asList(mockCertificate));
Request request = SecurityRequestConverters.delegatePkiAuthentication(delegatePkiAuthenticationRequest);
assertEquals(HttpPost.METHOD_NAME, request.getMethod());
assertEquals("/_security/delegate_pki", request.getEndpoint());
assertEquals(0, request.getParameters().size());
assertToXContentBody(delegatePkiAuthenticationRequest, request.getEntity());
}

public void testGetApplicationPrivilege() throws Exception {
final String application = randomAlphaOfLength(6);
final String privilege = randomAlphaOfLength(4);
Expand Down
Loading