diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/DelegatePkiRequest.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/DelegatePkiRequest.java new file mode 100644 index 0000000000000..4fd1ea5cbd7a0 --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/DelegatePkiRequest.java @@ -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 { + + private X509Certificate[] certificates; + + public DelegatePkiRequest(X509Certificate[] certificates) { + this.certificates = certificates; + } + + 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); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/DelegatePkiResponse.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/DelegatePkiResponse.java new file mode 100644 index 0000000000000..8db03169c66fd --- /dev/null +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/DelegatePkiResponse.java @@ -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; + private TimeValue expiresIn; + + DelegatePkiResponse() { } + + public DelegatePkiResponse(String tokenString, TimeValue expiresIn) { + this.tokenString = Objects.requireNonNull(tokenString); + 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); + } +} diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/pki/PkiRealmSettings.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/pki/PkiRealmSettings.java index cd153c9009ed6..7297a607af0ae 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/pki/PkiRealmSettings.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authc/pki/PkiRealmSettings.java @@ -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 ALLOW_DELEGATION_SETTING = Setting.affixKeySetting( + RealmSettings.realmSettingPrefix(TYPE), "allow_delegation", + key -> Setting.boolSetting(key, false, Setting.Property.NodeScope)); + public static final Setting.AffixSetting> TRUST_STORE_PATH; public static final Setting.AffixSetting> TRUST_STORE_TYPE; public static final Setting.AffixSetting TRUST_STORE_PASSWORD; diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java index 1f4f87e858176..565cd94b843f5 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java @@ -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; @@ -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; @@ -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); } @@ -770,7 +773,8 @@ public List 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()) ); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportDelegatePkiAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportDelegatePkiAction.java new file mode 100644 index 0000000000000..5ea601510b856 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/action/TransportDelegatePkiAction.java @@ -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 { + + private static final String ACTION_NAME = "cluster:admin/xpack/security/delegate_pki"; + public static final ActionType 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 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); + })); + } + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/AuthenticationDelegateeInfo.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/AuthenticationDelegateeInfo.java new file mode 100644 index 0000000000000..806f815a85c37 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/AuthenticationDelegateeInfo.java @@ -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 { + + 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); + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java index 92a521789ec86..a077366545374 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/PkiRealm.java @@ -76,6 +76,7 @@ public class PkiRealm extends Realm implements CachingRealm { private final UserRoleMapper roleMapper; private final Cache cache; private DelegatedAuthorizationSupport delegatedRealms; + private final boolean allowAuthenticationDelegation; public PkiRealm(RealmConfig config, ResourceWatcherService watcherService, NativeRoleMappingStore nativeRoleMappingStore) { this(config, new CompositeRoleMapper(config, watcherService, nativeRoleMappingStore)); @@ -93,6 +94,7 @@ public PkiRealm(RealmConfig config, ResourceWatcherService watcherService, Nativ .setMaximumWeight(config.getSetting(PkiRealmSettings.CACHE_MAX_USERS_SETTING)) .build(); this.delegatedRealms = null; + this.allowAuthenticationDelegation = config.getSetting(PkiRealmSettings.ALLOW_DELEGATION_SETTING); } @Override @@ -110,7 +112,13 @@ public boolean supports(AuthenticationToken token) { @Override public X509AuthenticationToken token(ThreadContext context) { - return token(context.getTransient(PKI_CERT_HEADER_NAME), principalPattern, logger); + Object pkiHeaderValue = context.getTransient(PKI_CERT_HEADER_NAME); + if (pkiHeaderValue == null) { + return null; + } + assert pkiHeaderValue instanceof X509Certificate[]; + X509Certificate[] certificates = (X509Certificate[]) pkiHeaderValue; + return token(certificates); } @Override @@ -122,25 +130,34 @@ public void authenticate(AuthenticationToken authToken, ActionListener cachingListener = ActionListener.wrap(result -> { - if (result.isAuthenticated()) { - try (ReleasableLock ignored = readLock.acquire()) { - cache.put(fingerprint, result.getUser()); + String principal = getPrincipalFromSubjectDN(principalPattern, token, logger); + if (principal == null) { + listener.onResponse(AuthenticationResult.unsuccessful("Could not parse principal from Subject DN " + token.dn(), null)); + } else { + final ActionListener cachingListener = ActionListener.wrap(result -> { + if (result.isAuthenticated()) { + try (ReleasableLock ignored = readLock.acquire()) { + cache.put(fingerprint, result.getUser()); + } } + listener.onResponse(result); + }, listener::onFailure); + if (delegatedRealms.hasDelegation()) { + delegatedRealms.resolve(principal, cachingListener); + } else { + buildUser(token, principal, cachingListener); } - listener.onResponse(result); - }, listener::onFailure); - if (delegatedRealms.hasDelegation()) { - delegatedRealms.resolve(token.principal(), cachingListener); - } else { - this.buildUser(token, cachingListener); } } } catch (CertificateEncodingException e) { @@ -148,12 +165,16 @@ public void authenticate(AuthenticationToken authToken, ActionListener listener) { - final Map metadata = Map.of("pki_dn", token.dn()); - final UserRoleMapper.UserData userData = new UserRoleMapper.UserData(token.principal(), token.dn(), Set.of(), metadata, config); + private void buildUser(X509AuthenticationToken token, String principal, ActionListener listener) { + final Map metadata; + if (token.isDelegated()) { + metadata = Map.of("pki_dn", token.dn(), "delegatee_info", token.getDelegateeInfo()); + } else { + metadata = Map.of("pki_dn", token.dn()); + } + final UserRoleMapper.UserData userData = new UserRoleMapper.UserData(principal, token.dn(), Set.of(), metadata, config); roleMapper.resolveRoles(userData, ActionListener.wrap(roles -> { - final User computedUser = - new User(token.principal(), roles.toArray(new String[roles.size()]), null, null, metadata, true); + final User computedUser = new User(principal, roles.toArray(new String[roles.size()]), null, null, metadata, true); listener.onResponse(AuthenticationResult.success(computedUser)); }, listener::onFailure)); } @@ -163,47 +184,40 @@ public void lookupUser(String username, ActionListener listener) { listener.onResponse(null); } - static X509AuthenticationToken token(Object pkiHeaderValue, Pattern principalPattern, Logger logger) { - if (pkiHeaderValue == null) { - return null; - } - - assert pkiHeaderValue instanceof X509Certificate[]; - X509Certificate[] certificates = (X509Certificate[]) pkiHeaderValue; + static X509AuthenticationToken token(X509Certificate[] certificates) { if (certificates.length == 0) { return null; } + return new X509AuthenticationToken(certificates); + } - String dn = certificates[0].getSubjectX500Principal().toString(); + static String getPrincipalFromSubjectDN(Pattern principalPattern, X509AuthenticationToken token, Logger logger) { + String dn = token.credentials()[0].getSubjectX500Principal().toString(); Matcher matcher = principalPattern.matcher(dn); - if (!matcher.find()) { - if (logger.isDebugEnabled()) { - logger.debug("certificate authentication succeeded for [{}] but could not extract principal from DN", dn); - } + if (false == matcher.find()) { + logger.debug((Supplier) () -> new ParameterizedMessage( + "Certificate authentication succeeded for [{}] but could not extract principal from DN", dn)); return null; } - String principal = matcher.group(1); if (Strings.isNullOrEmpty(principal)) { - if (logger.isDebugEnabled()) { - logger.debug("certificate authentication succeeded for [{}] but extracted principal was empty", dn); - } + logger.debug((Supplier) () -> new ParameterizedMessage( + "Certificate authentication succeeded for [{}] but the extracted principal was empty", dn)); return null; } - return new X509AuthenticationToken(certificates, principal, dn); + return principal; } - static boolean isCertificateChainTrusted(X509TrustManager trustManager, X509AuthenticationToken token, Logger logger) { + private static boolean isCertificateChainTrusted(X509TrustManager trustManager, X509AuthenticationToken token, Logger logger) { if (trustManager != null) { try { trustManager.checkClientTrusted(token.credentials(), AUTH_TYPE); return true; } catch (CertificateException e) { if (logger.isTraceEnabled()) { - logger.trace((Supplier) - () -> new ParameterizedMessage("failed certificate validation for principal [{}]", token.principal()), e); + logger.trace("failed certificate validation for Subject DN [" + token.dn() + "]", e); } else if (logger.isDebugEnabled()) { - logger.debug("failed certificate validation for principal [{}]", token.principal()); + logger.debug("failed certificate validation for Subbject DN [{}]", token.dn()); } } return false; @@ -213,7 +227,7 @@ static boolean isCertificateChainTrusted(X509TrustManager trustManager, X509Auth return true; } - X509TrustManager trustManagers(RealmConfig realmConfig) { + private X509TrustManager trustManagers(RealmConfig realmConfig) { final List certificateAuthorities = realmConfig.hasSetting(PkiRealmSettings.CAPATH_SETTING) ? realmConfig.getSetting(PkiRealmSettings.CAPATH_SETTING) : null; String truststorePath = realmConfig.getSetting(PkiRealmSettings.TRUST_STORE_PATH).orElse(null); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/X509AuthenticationToken.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/X509AuthenticationToken.java index 8603a662efa4c..15a8b9997c28d 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/X509AuthenticationToken.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/pki/X509AuthenticationToken.java @@ -11,19 +11,23 @@ public class X509AuthenticationToken implements AuthenticationToken { - private final String principal; private final String dn; - private X509Certificate[] credentials; + private final X509Certificate[] credentials; + private AuthenticationDelegateeInfo delegateeInfo; - public X509AuthenticationToken(X509Certificate[] certificates, String principal, String dn) { - this.principal = principal; + public X509AuthenticationToken(X509Certificate[] certificates) { + this(certificates, null); + } + + public X509AuthenticationToken(X509Certificate[] certificates, AuthenticationDelegateeInfo delegateeInfo) { + this.dn = certificates == null || certificates.length == 0 ? null : certificates[0].getSubjectX500Principal().toString(); this.credentials = certificates; - this.dn = dn; + this.delegateeInfo = delegateeInfo; } @Override public String principal() { - return principal; + return "X500SubjectDN(" + dn() + ")"; } @Override @@ -35,8 +39,16 @@ public String dn() { return dn; } + public boolean isDelegated() { + return delegateeInfo != null; + } + + public AuthenticationDelegateeInfo getDelegateeInfo() { + return delegateeInfo; + } + @Override public void clearCredentials() { - credentials = null; + // noop } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestDelegatePkiAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestDelegatePkiAction.java new file mode 100644 index 0000000000000..9a3e1c1d227b4 --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestDelegatePkiAction.java @@ -0,0 +1,88 @@ +/* + * 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.rest.action; + +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.ParseField; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.common.xcontent.ConstructingObjectParser; +import org.elasticsearch.common.xcontent.XContentParser; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.rest.RestController; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.rest.BytesRestResponse; +import org.elasticsearch.rest.RestResponse; +import org.elasticsearch.rest.RestStatus; +import org.elasticsearch.rest.action.RestBuilderListener; +import org.elasticsearch.xpack.core.security.action.DelegatePkiRequest; +import org.elasticsearch.xpack.security.action.TransportDelegatePkiAction; +import org.elasticsearch.xpack.security.rest.action.oauth2.TokenBaseRestHandler; +import org.elasticsearch.xpack.core.security.action.DelegatePkiResponse; +import org.elasticsearch.common.xcontent.XContentBuilder; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.Base64; +import java.util.List; + +import static org.elasticsearch.rest.RestRequest.Method.POST; + +public class RestDelegatePkiAction extends TokenBaseRestHandler { + + private static final ParseField X5C_FIELD = new ParseField("x5c"); + + static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("delegate_pki", true, a -> { + final List encodedCertificatesList = (List) a[0]; + final X509Certificate[] certificates = new X509Certificate[encodedCertificatesList.size()]; + try { + final CertificateFactory certificateFactory = CertificateFactory.getInstance("X.509"); + for (int i = 0; i < encodedCertificatesList.size(); i++) { + try (ByteArrayInputStream bis = new ByteArrayInputStream(Base64.getDecoder().decode(encodedCertificatesList.get(i)))) { + certificates[i] = (X509Certificate) certificateFactory.generateCertificate(bis); + } catch (CertificateException | IOException e) { + throw new RuntimeException(e); + } + } + } catch (CertificateException e) { + throw new RuntimeException(e); + } + return new DelegatePkiRequest(certificates); + }); + + static { + PARSER.declareStringArray(ConstructingObjectParser.constructorArg(), X5C_FIELD); + } + + public RestDelegatePkiAction(Settings settings, RestController controller, XPackLicenseState xPackLicenseState) { + super(settings, xPackLicenseState); + controller.registerHandler(POST, "/_security/delegate_pki", this); + } + + @Override + protected RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) throws IOException { + try (XContentParser parser = request.contentParser()) { + final DelegatePkiRequest delegatePkiRequest = PARSER.parse(parser, null); + return channel -> client.execute(TransportDelegatePkiAction.TYPE, delegatePkiRequest, + new RestBuilderListener(channel) { + @Override + public RestResponse buildResponse(DelegatePkiResponse delegatePkiResponse, XContentBuilder builder) + throws Exception { + delegatePkiResponse.toXContent(builder, channel.request()); + return new BytesRestResponse(RestStatus.OK, builder); + } + }); + } + } + + @Override + public String getName() { + return "delegate_pki_action"; + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oauth2/TokenBaseRestHandler.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oauth2/TokenBaseRestHandler.java index 7111a5387fe5b..10e8aa63f33d8 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oauth2/TokenBaseRestHandler.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oauth2/TokenBaseRestHandler.java @@ -17,11 +17,11 @@ /** * A base rest handler that handles licensing for Token actions */ -abstract class TokenBaseRestHandler extends SecurityBaseRestHandler { +public abstract class TokenBaseRestHandler extends SecurityBaseRestHandler { protected Logger logger = LogManager.getLogger(getClass()); - TokenBaseRestHandler(Settings settings, XPackLicenseState licenseState) { + protected TokenBaseRestHandler(Settings settings, XPackLicenseState licenseState) { super(settings, licenseState); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiAuthenticationTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiAuthenticationTests.java index 5b2ab36426363..bc765088ccfb5 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiAuthenticationTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiAuthenticationTests.java @@ -101,7 +101,8 @@ public void testRestAuthenticationFailure() throws Exception { try (CloseableHttpResponse response = SocketAccess.doPrivileged(() -> client.execute(put))) { assertThat(response.getStatusLine().getStatusCode(), is(401)); String body = EntityUtils.toString(response.getEntity()); - assertThat(body, containsString("unable to authenticate user [Elasticsearch Test Client]")); + assertThat(body, containsString( + "unable to authenticate user [X500SubjectDN(CN=Elasticsearch Test Client, OU=elasticsearch, O=org)]")); } } } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java index 2d46d96a914b7..03cdf29e2d7ba 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/pki/PkiRealmTests.java @@ -79,7 +79,7 @@ public void testTokenSupport() { assertThat(realm.supports(null), is(false)); assertThat(realm.supports(new UsernamePasswordToken("", new SecureString(new char[0]))), is(false)); - assertThat(realm.supports(new X509AuthenticationToken(new X509Certificate[0], "", "")), is(true)); + assertThat(realm.supports(new X509AuthenticationToken(new X509Certificate[0])), is(true)); } public void testExtractToken() throws Exception { @@ -90,9 +90,11 @@ public void testExtractToken() throws Exception { TestEnvironment.newEnvironment(globalSettings), threadContext), mock(UserRoleMapper.class)); X509AuthenticationToken token = realm.token(threadContext); + final String expectedUsername = PkiRealm.getPrincipalFromSubjectDN(Pattern.compile(PkiRealmSettings.DEFAULT_USERNAME_PATTERN), + token, NoOpLogger.INSTANCE); assertThat(token, is(notNullValue())); assertThat(token.dn(), is("CN=Elasticsearch Test Node, OU=elasticsearch, O=org")); - assertThat(token.principal(), is("Elasticsearch Test Node")); + assertThat(expectedUsername, is("Elasticsearch Test Node")); } public void testAuthenticateBasedOnCertToken() throws Exception { @@ -112,7 +114,8 @@ private void assertSuccessfulAuthentication(Set roles) throws Exception PkiRealm realm = buildRealm(roleMapper, globalSettings); verify(roleMapper).refreshRealmOnChange(realm); - final String expectedUsername = token.principal(); + final String expectedUsername = PkiRealm.getPrincipalFromSubjectDN(Pattern.compile(PkiRealmSettings.DEFAULT_USERNAME_PATTERN), + token, NoOpLogger.INSTANCE); final AuthenticationResult result = authenticate(token, realm); final PlainActionFuture future; assertThat(result.getStatus(), is(AuthenticationResult.Status.SUCCESS)); @@ -172,7 +175,7 @@ private PkiRealm buildRealm(UserRoleMapper roleMapper, Settings settings, Realm. private X509AuthenticationToken buildToken() throws Exception { X509Certificate certificate = readCert(getDataPath("/org/elasticsearch/xpack/security/transport/ssl/certs/simple/testnode.crt")); - return new X509AuthenticationToken(new X509Certificate[]{certificate}, "Elasticsearch Test Node", "CN=Elasticsearch Test Node,"); + return new X509AuthenticationToken(new X509Certificate[]{certificate}); } private AuthenticationResult authenticate(X509AuthenticationToken token, PkiRealm realm) { @@ -306,10 +309,11 @@ public void testCertificateWithOnlyCnExtractsProperly() throws Exception { X500Principal principal = new X500Principal("CN=PKI Client"); when(certificate.getSubjectX500Principal()).thenReturn(principal); - X509AuthenticationToken token = PkiRealm.token(new X509Certificate[]{certificate}, - Pattern.compile(PkiRealmSettings.DEFAULT_USERNAME_PATTERN), NoOpLogger.INSTANCE); + X509AuthenticationToken token = PkiRealm.token(new X509Certificate[]{certificate}); + String parsedPrincipal = PkiRealm.getPrincipalFromSubjectDN(Pattern.compile(PkiRealmSettings.DEFAULT_USERNAME_PATTERN), token, + NoOpLogger.INSTANCE); assertThat(token, notNullValue()); - assertThat(token.principal(), is("PKI Client")); + assertThat(parsedPrincipal, is("PKI Client")); assertThat(token.dn(), is("CN=PKI Client")); } @@ -318,10 +322,11 @@ public void testCertificateWithCnAndOuExtractsProperly() throws Exception { X500Principal principal = new X500Principal("CN=PKI Client, OU=Security"); when(certificate.getSubjectX500Principal()).thenReturn(principal); - X509AuthenticationToken token = PkiRealm.token(new X509Certificate[]{certificate}, - Pattern.compile(PkiRealmSettings.DEFAULT_USERNAME_PATTERN), NoOpLogger.INSTANCE); + X509AuthenticationToken token = PkiRealm.token(new X509Certificate[]{certificate}); + String parsedPrincipal = PkiRealm.getPrincipalFromSubjectDN(Pattern.compile(PkiRealmSettings.DEFAULT_USERNAME_PATTERN), token, + NoOpLogger.INSTANCE); assertThat(token, notNullValue()); - assertThat(token.principal(), is("PKI Client")); + assertThat(parsedPrincipal, is("PKI Client")); assertThat(token.dn(), is("CN=PKI Client, OU=Security")); } @@ -330,10 +335,11 @@ public void testCertificateWithCnInMiddle() throws Exception { X500Principal principal = new X500Principal("EMAILADDRESS=pki@elastic.co, CN=PKI Client, OU=Security"); when(certificate.getSubjectX500Principal()).thenReturn(principal); - X509AuthenticationToken token = PkiRealm.token(new X509Certificate[]{certificate}, - Pattern.compile(PkiRealmSettings.DEFAULT_USERNAME_PATTERN), NoOpLogger.INSTANCE); + X509AuthenticationToken token = PkiRealm.token(new X509Certificate[]{certificate}); + String parsedPrincipal = PkiRealm.getPrincipalFromSubjectDN(Pattern.compile(PkiRealmSettings.DEFAULT_USERNAME_PATTERN), token, + NoOpLogger.INSTANCE); assertThat(token, notNullValue()); - assertThat(token.principal(), is("PKI Client")); + assertThat(parsedPrincipal, is("PKI Client")); assertThat(token.dn(), is("EMAILADDRESS=pki@elastic.co, CN=PKI Client, OU=Security")); } @@ -355,10 +361,12 @@ public void testPKIRealmSettingsPassValidation() throws Exception { public void testDelegatedAuthorization() throws Exception { final X509AuthenticationToken token = buildToken(); + String parsedPrincipal = PkiRealm.getPrincipalFromSubjectDN(Pattern.compile(PkiRealmSettings.DEFAULT_USERNAME_PATTERN), token, + NoOpLogger.INSTANCE); final MockLookupRealm otherRealm = new MockLookupRealm(new RealmConfig(new RealmConfig.RealmIdentifier("mock", "other_realm"), globalSettings, TestEnvironment.newEnvironment(globalSettings), new ThreadContext(globalSettings))); - final User lookupUser = new User(token.principal()); + final User lookupUser = new User(parsedPrincipal); otherRealm.registerUser(lookupUser); final Settings realmSettings = Settings.builder() @@ -373,7 +381,7 @@ public void testDelegatedAuthorization() throws Exception { assertThat(result.getUser(), sameInstance(lookupUser)); // check that the authorizing realm is consulted even for cached principals - final User lookupUser2 = new User(token.principal()); + final User lookupUser2 = new User(parsedPrincipal); otherRealm.registerUser(lookupUser2); result = authenticate(token, pkiRealm);