-
Notifications
You must be signed in to change notification settings - Fork 25.8k
PKI Authentication Delegation in new endpoint #43796
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a47a53b
3c3b3da
d982d14
685e3d1
7410765
6c13a12
6095a20
53c35b3
86caa78
a86de5e
2180f1a
9297562
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 { | ||
|
|
||
| private X509Certificate[] certificates; | ||
|
|
||
| public DelegatePkiRequest(X509Certificate[] certificates) { | ||
| this.certificates = certificates; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. null and empty check
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| } | ||
|
|
||
| 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; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. make it final? |
||
| private TimeValue expiresIn; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
|---|---|---|
| @@ -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 { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
|
|
||
| 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); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
make it final?
There was a problem hiding this comment.
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.