Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

package org.elasticsearch.xpack.core.security.action;

import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.ActionRequestValidationException;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.security.cert.CertificateEncodingException;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Arrays;

public class DelegatePkiRequest extends ActionRequest {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make it final?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will make it final. But, in general, I think we should make final only the classes with algorithms inside, POJOs like requests and responses would generally benefit from inheritance.


private X509Certificate[] certificates;

public DelegatePkiRequest(X509Certificate[] certificates) {
this.certificates = certificates;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

null and empty check

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess the expectation is that this will be ordered chain, shall we check that here? or try to order it before setting it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are already in the correct order (as the client provided them), and the order is checked by trustManager.checkClientTrusted . The client should dump the cert chain as captured from the TLS connection.

}

public DelegatePkiRequest(StreamInput in) throws IOException {
this.readFrom(in);
}

@Override
public ActionRequestValidationException validate() {
return null;
}

public X509Certificate[] getCertificates() {
return certificates;
}

@Override
public void readFrom(StreamInput input) throws IOException {
super.readFrom(input);
try {
final CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509");
input.readArray(in -> {
try (ByteArrayInputStream bis = new ByteArrayInputStream(in.readByteArray())) {
return (X509Certificate) certificateFactory.generateCertificate(bis);
} catch (CertificateException e) {
throw new IOException(e);
}
}, X509Certificate[]::new);
} catch (CertificateException e) {
throw new IOException(e);
}
}

@Override
public void writeTo(StreamOutput output) throws IOException {
super.writeTo(output);
output.writeArray((out, cert) -> {
try {
out.writeByteArray(cert.getEncoded());
} catch (CertificateEncodingException e) {
throw new IOException(e);
}
}, certificates);
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
DelegatePkiRequest that = (DelegatePkiRequest) o;
return Arrays.equals(certificates, that.certificates);
}

@Override
public int hashCode() {
return Arrays.hashCode(certificates);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

package org.elasticsearch.xpack.core.security.action;

import org.elasticsearch.action.ActionResponse;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;

import java.io.IOException;
import java.util.Objects;

public class DelegatePkiResponse extends ActionResponse implements ToXContentObject {

private String tokenString;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make it final?

private TimeValue expiresIn;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make it final?


DelegatePkiResponse() { }

public DelegatePkiResponse(String tokenString, TimeValue expiresIn) {
this.tokenString = Objects.requireNonNull(tokenString);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

null or empty check

this.expiresIn = Objects.requireNonNull(expiresIn);
}

public DelegatePkiResponse(StreamInput input) throws IOException {
this.readFrom(input);
}

public String getTokenString() {
return tokenString;
}

public TimeValue getExpiresIn() {
return expiresIn;
}

@Override
public void writeTo(StreamOutput out) throws IOException {
super.writeTo(out);
out.writeString(tokenString);
out.writeTimeValue(expiresIn);
}

@Override
public void readFrom(StreamInput in) throws IOException {
super.readFrom(in);
tokenString = in.readString();
expiresIn = in.readTimeValue();
}

@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
builder.startObject()
.field("access_token", tokenString)
.field("type", "Bearer")
.field("expires_in", expiresIn.seconds());
return builder.endObject();
}

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

@Override
public int hashCode() {
return Objects.hash(tokenString, expiresIn);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ public final class PkiRealmSettings {
RealmSettings.realmSettingPrefix(TYPE), "cache.max_users",
key -> Setting.intSetting(key, DEFAULT_MAX_USERS, Setting.Property.NodeScope));

public static final Setting.AffixSetting<Boolean> ALLOW_DELEGATION_SETTING = Setting.affixKeySetting(
RealmSettings.realmSettingPrefix(TYPE), "allow_delegation",
key -> Setting.boolSetting(key, false, Setting.Property.NodeScope));

public static final Setting.AffixSetting<Optional<String>> TRUST_STORE_PATH;
public static final Setting.AffixSetting<Optional<String>> TRUST_STORE_TYPE;
public static final Setting.AffixSetting<SecureString> TRUST_STORE_PASSWORD;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@
import org.elasticsearch.xpack.core.ssl.action.TransportGetCertificateInfoAction;
import org.elasticsearch.xpack.core.ssl.rest.RestGetCertificateInfoAction;
import org.elasticsearch.xpack.security.action.TransportCreateApiKeyAction;
import org.elasticsearch.xpack.security.action.TransportDelegatePkiAction;
import org.elasticsearch.xpack.security.action.TransportGetApiKeyAction;
import org.elasticsearch.xpack.security.action.TransportInvalidateApiKeyAction;
import org.elasticsearch.xpack.security.action.filter.SecurityActionFilter;
Expand Down Expand Up @@ -194,6 +195,7 @@
import org.elasticsearch.xpack.security.ingest.SetSecurityUserProcessor;
import org.elasticsearch.xpack.security.rest.SecurityRestFilter;
import org.elasticsearch.xpack.security.rest.action.RestAuthenticateAction;
import org.elasticsearch.xpack.security.rest.action.RestDelegatePkiAction;
import org.elasticsearch.xpack.security.rest.action.apikey.RestCreateApiKeyAction;
import org.elasticsearch.xpack.security.rest.action.apikey.RestGetApiKeyAction;
import org.elasticsearch.xpack.security.rest.action.apikey.RestInvalidateApiKeyAction;
Expand Down Expand Up @@ -718,6 +720,7 @@ public void onIndexModule(IndexModule module) {
new ActionHandler<>(CreateApiKeyAction.INSTANCE, TransportCreateApiKeyAction.class),
new ActionHandler<>(InvalidateApiKeyAction.INSTANCE, TransportInvalidateApiKeyAction.class),
new ActionHandler<>(GetApiKeyAction.INSTANCE, TransportGetApiKeyAction.class),
new ActionHandler<>(TransportDelegatePkiAction.TYPE, TransportDelegatePkiAction.class),
usageAction,
infoAction);
}
Expand Down Expand Up @@ -770,7 +773,8 @@ public List<RestHandler> getRestHandlers(Settings settings, RestController restC
new RestDeletePrivilegesAction(settings, restController, getLicenseState()),
new RestCreateApiKeyAction(settings, restController, getLicenseState()),
new RestInvalidateApiKeyAction(settings, restController, getLicenseState()),
new RestGetApiKeyAction(settings, restController, getLicenseState())
new RestGetApiKeyAction(settings, restController, getLicenseState()),
new RestDelegatePkiAction(settings, restController, getLicenseState())
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

package org.elasticsearch.xpack.security.action;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.message.ParameterizedMessage;
import org.apache.logging.log4j.util.Supplier;
import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.ActionType;
import org.elasticsearch.action.support.ActionFilters;
import org.elasticsearch.action.support.HandledTransportAction;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.tasks.Task;
import org.elasticsearch.threadpool.ThreadPool;
import org.elasticsearch.transport.TransportService;
import org.elasticsearch.xpack.core.security.action.DelegatePkiRequest;
import org.elasticsearch.xpack.core.security.action.DelegatePkiResponse;
import org.elasticsearch.xpack.core.security.authc.Authentication;
import org.elasticsearch.xpack.security.authc.AuthenticationService;
import org.elasticsearch.xpack.security.authc.TokenService;
import org.elasticsearch.xpack.security.authc.pki.AuthenticationDelegateeInfo;
import org.elasticsearch.xpack.security.authc.pki.X509AuthenticationToken;

import java.util.Map;

public class TransportDelegatePkiAction extends HandledTransportAction<DelegatePkiRequest, DelegatePkiResponse> {

private static final String ACTION_NAME = "cluster:admin/xpack/security/delegate_pki";
public static final ActionType<DelegatePkiResponse> TYPE = new ActionType<>(ACTION_NAME, DelegatePkiResponse::new);
private static final Logger logger = LogManager.getLogger(TransportDelegatePkiAction.class);

private final ThreadPool threadPool;
private final AuthenticationService authenticationService;
private final TokenService tokenService;

@Inject
public TransportDelegatePkiAction(ThreadPool threadPool, TransportService transportService, ActionFilters actionFilters,
AuthenticationService authenticationService, TokenService tokenService) {
super(ACTION_NAME, transportService, actionFilters, DelegatePkiRequest::new);
this.threadPool = threadPool;
this.authenticationService = authenticationService;
this.tokenService = tokenService;
}

@Override
protected void doExecute(Task task, DelegatePkiRequest request, ActionListener<DelegatePkiResponse> listener) {
final ThreadContext threadContext = threadPool.getThreadContext();
Authentication delegateeAuthentication = Authentication.getAuthentication(threadContext);
final X509AuthenticationToken x509DelegatedToken = new X509AuthenticationToken(request.getCertificates(),
new AuthenticationDelegateeInfo(delegateeAuthentication));
logger.trace(
(Supplier<?>) () -> new ParameterizedMessage("Attempting to authenticate delegated x509Token [{}]", x509DelegatedToken));
try (ThreadContext.StoredContext ignore = threadContext.stashContext()) {
authenticationService.authenticate(ACTION_NAME, request, x509DelegatedToken, ActionListener.wrap(authentication -> {
assert authentication != null : "authentication should never be null at this point";
tokenService.createOAuth2Tokens(authentication, delegateeAuthentication,
Map.of(), false, ActionListener.wrap(tuple -> {
final TimeValue expiresIn = tokenService.getExpirationDelay();
listener.onResponse(new DelegatePkiResponse(tuple.v1(), expiresIn));
}, listener::onFailure));
}, e -> {
logger.debug((Supplier<?>) () -> new ParameterizedMessage("Delegated x509Token [{}] could not be authenticated",
x509DelegatedToken), e);
listener.onFailure(e);
}));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

package org.elasticsearch.xpack.security.authc.pki;

import org.elasticsearch.common.xcontent.ToXContentObject;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.xpack.core.security.authc.Authentication;

import java.io.IOException;
import java.util.Objects;

public class AuthenticationDelegateeInfo implements ToXContentObject {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am trying to understand the purpose of this class. It feels like a wrapper around the original Authentication and we add it to the user metadata. Do we intend to use this information somewhere after authentication? Could you please elaborate, also if you could add java docs. Thank you.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class should encapsulate all the details based on which the realm allows the delegation or not. At its simplest it could be a boolean flag saying that this token is delegated (as opposed to locally crafted), but here it encapsulates the authentication of the client. In this case the realm could restrict delegation to only certain users. I will leave it as a boolean, before we decide on the means to restrict delegation.


final Authentication delegateeClientAuthentication;

public AuthenticationDelegateeInfo(Authentication delegateeClientAuthentication) {
this.delegateeClientAuthentication = delegateeClientAuthentication;
}

public Authentication getDelegateeClientAuthentication() {
return delegateeClientAuthentication;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AuthenticationDelegateeInfo that = (AuthenticationDelegateeInfo) o;
return delegateeClientAuthentication.equals(that.delegateeClientAuthentication);
}

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

@Override
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
return delegateeClientAuthentication.toXContent(builder, params);
}
}
Loading