Skip to content

Authenticate and wrap HTTP request headers with context supplier#93656

Closed
albertzaharovits wants to merge 43 commits intoelastic:mainfrom
albertzaharovits:header-validator-hook-in-channel-context
Closed

Authenticate and wrap HTTP request headers with context supplier#93656
albertzaharovits wants to merge 43 commits intoelastic:mainfrom
albertzaharovits:header-validator-hook-in-channel-context

Conversation

@albertzaharovits
Copy link
Copy Markdown
Contributor

@albertzaharovits albertzaharovits commented Feb 9, 2023

Builds on top of #92220 .

Adds authentication to HTTP headers, and packs the authenticated context to be used when executing the URL handler.

@albertzaharovits albertzaharovits self-assigned this Feb 9, 2023
@albertzaharovits albertzaharovits force-pushed the header-validator-hook-in-channel-context branch from 9eb56cc to 631e507 Compare February 9, 2023 16:19
@albertzaharovits albertzaharovits force-pushed the header-validator-hook-in-channel-context branch from 631e507 to d9da947 Compare February 10, 2023 11:40
Comment on lines 127 to +128
HttpServerTransport.Dispatcher dispatcher,
BiConsumer<BasicHttpRequest, ThreadContext> dispatcherContext,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We can choose to merge the dispatcherContext in the HttpServerTransport.Dispatcher interface, if we opt to go with presented approach where the RestController furnishes the thread context supplier, but it's the http server transport that employs it, because the Security plugin can alter the latter but not the former.

Comment on lines 377 to 381
if (badRequestCause != null) {
dispatcher.dispatchBadRequest(channel, threadContext, badRequestCause);
} else {
populateRequestThreadContext0(restRequest, channel, threadContext);
dispatcher.dispatchRequest(restRequest, channel, threadContext);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is the code that clears and scopes the thread context for a particular http request to be handled.
This is now extended to also populate the said thread context (with selected HTTP header values).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this happening before or after authentication ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

In this PR this is happening after authentication (on main it happens before authentication).

The populateRequestThreadContext hook method is used to instante the authentication context, for the authn that happened earlier.

import java.util.List;
import java.util.Map;

public interface BasicHttpRequest {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This interface is the common ground between the two request types, with or without a body.
It is now used in the AuthenticationService as well as the AudittingService.

* @return the {@link RestRequest.Method} used in the REST request
* @throws IllegalArgumentException if the HTTP method is invalid
*/
RestRequest.Method method();
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Method doesn't have to be a RestRequest inner enum, and can be moved here, but such a refactoring has a huge diff.

dispatcherContext.accept(httpRequest, threadContext);
populateClientCertificate.accept(channel, threadContext);
RemoteHostHeader.process(channel, threadContext);
};
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The thread context before running the authentication in the channel handler. It reuses that "dispatch" context that's a sort of baseline (contains selected HTTP header values).

public static void extractClientCertificates(Logger logger, ThreadContext threadContext, TcpChannel tcpChannel) {
SSLEngine sslEngine = getSSLEngine(tcpChannel);
extract(logger, threadContext, sslEngine, tcpChannel);
public static void extractClientCertificates(Logger logger, ThreadContext threadContext, Channel channel) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Extracting the client certificates must work even when a HttpChannel is not available (it only needs the neety channel anyway).

Comment on lines -96 to -103
if (extractClientCertificate) {
HttpChannel httpChannel = request.getHttpChannel();
SSLEngineUtils.extractClientCertificates(logger, threadContext, httpChannel);
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The Security plugin now incorporates this when it builds the thread context before running the authentication.

logger.trace("Found secondary authentication {} in REST request [{}]", secondaryAuthentication, request.uri());
logger.trace("Found secondary authentication {} in REST request [{}]", secondaryAuthentication, httpRequest.uri());
}
RemoteHostHeader.process(request, threadContext);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This has also been moved to the consolidated thread context populator, before running authentication (here it is "after", but moving it "before" allows more audit events to benefit, and aids the transion from RestRequest to BasicHttpRequest in the auditing service).

handleException(actionType, channel, e);
}

public static void handleAuthenticationFailed(RestChannel channel, Exception e) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

"Exports" the authn failure response building logic to be used in the channel handler.

// this throws if a new http request looks to be already authenticated
if (authentication != null
&& (context.getRequest() instanceof AuthenticationService.AuditableHttpRequest)
&& (((AuthenticationService.AuditableHttpRequest) context.getRequest()).request instanceof HttpRequest == false)) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Right now, HttpRequests are checked for authentication in two places: the original SecurityRestFilter and the new HttpHeadersAuthenticator, so this check here must account for this fact. But it's likely that the final PR drops the SecurityRestFilter.

// There should never be an existing audit-id when processing a rest request.
this.requestId = AuditUtil.generateRequestId(threadContext);
// There might be an existing audit-id (e.g. generated by the rest request) but there might not be (e.g. an internal action)
this.requestId = AuditUtil.getOrGenerateRequestId(threadContext);
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Using AuditUtil#getOrGenerateRequestId instead of AuditUtil#generateRequestId is just a temporary measure. Ultimately HttpRequests will only be intercepted once so we can fall back to the stricter generate.

}

LogEntryBuilder withRestOrigin(RestRequest request) {
LogEntryBuilder withRestOrigin(ThreadContext threadContext) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Some audit events already relied on the remote address to be set in the thread context.
This PR adopts that approach for all the audit events, because I think it's easier to adapt the auditing service to work in the channel handler this way.

Comment on lines +1641 to +1650
public void dispatchBadRequest(RestChannel channel, ThreadContext threadContext, Throwable cause) {
Exception authenticationException = HttpHeadersAuthenticator.extractAuthenticationException(
channel.request().getHttpRequest()
);
if (authenticationException != null) {
SecurityRestFilter.handleAuthenticationFailed(channel, authenticationException);
} else {
dispatcher.dispatchBadRequest(channel, threadContext, cause);
}
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately the dispatcher needs to have the the "bad request" branch overridden, because of the small differences between the 400 errors and authn failure errors.

@albertzaharovits albertzaharovits changed the title Header validator hook in channel context Authenticate and wrap request headers with context suppier Feb 20, 2023
@albertzaharovits albertzaharovits changed the title Authenticate and wrap request headers with context suppier Authenticate and wrap request headers with context supplier Feb 20, 2023
@albertzaharovits albertzaharovits changed the title Authenticate and wrap request headers with context supplier Authenticate and wrap HTTP request headers with context supplier Feb 20, 2023
Copy link
Copy Markdown
Contributor

@jakelandis jakelandis left a comment

Choose a reason for hiding this comment

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

I'm still working though this review , but here are a couple early comments. Also, it would be helpful to have some high level explaination of how this works. I think I understand most of it ...but the order of execution between netty functions and what is in thread context is hard to follow.

* server package's rest handling.
*/
public interface HttpRequest {
public interface HttpRequest extends BasicHttpRequest {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This relationship is backwards from what I would expect. I would expect an HttpRequestWithoutBody implements HttpRequest (or possibly PartialHttpRequest to contrast with io.netty.handler.codec.http.FullHttpRequest) and the unsuported methods to throw an exception. Then the usages of HttpRequestWithoutBody can be passed around with the explicit expectations that only part of the larger interface is implemented. i.e. don't use anonymous implementations of BasicHttpRequest instead use a concrete HttpRequestWithoutBody.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Is this a general preference of yours or it's just this particular case?

I think the two options are:

  • an interface extends another and adds more methods; in case the base interface is passed around and the code has some specialized logic it can use instance of checks to downcast
  • have a single rich interface and throw "not implemented"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

FWIW the body needed for auditing authn success (which, again, is one of the things that's not dealt with properly by this PR) so pending other discussions, we might not need a request interface with a request body....

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

don't use anonymous implementations of BasicHttpRequest instead use a concrete HttpRequestWithoutBody.

That's separate from the interface conversation, I think. There's going to be some light wrapping needed because the request classes that netty uses are not available to the Security plugin module.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this a general preference of yours or it's just this particular case?

I think general ? ... basically I think the interface should represent the concept and any sub interfaces should be specializations of that concept. (IMO) Here the concept is a http request and not having access to the body the a specialization. I understand the throwing not implemented from many methods is also it's own issue, so this is more subjective / preference than any rule. However, I do think as-is this has some readability / expectation issues. Maybe just changing the names would help ... as-is there no indication to what the difference between a HttpRequest and BasicHttpRequest means...java doc can help but good names > doc. I think i still prefer what I suggested above ...but having something like FullHttpRequest extends PartialHttpRequest makes more readily apparent what to expect from the contract.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

... has some specialized logic it can use instance of checks to downcast

I would strongly advise against a design that necessitates instanceof checks to downcast


private final TriConsumer<HttpRequest, Channel, ActionListener<Void>> validator;
private ArrayDeque<HttpObject> pending = new ArrayDeque<>(4);
private STATE state = STATE.WAITING_TO_START;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

does this class need to be threadsafe ? i.e. what is the cardinatlity of this class ? is it one instance per HTTP request, or one instance per channel ...if the later are there some garuntees for how many threads can call the channelRead method ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It is one instance per channel, given how it is instantiated in Netty4HttpServerTransport, with ch.pipeline().addLast(..., new Validator()).
There's always one thread per channel. This is core to netty's design and cannot be changed (ever).
So, no it's not required to be thread-safe.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

To be pedantic, there are multiple channels per thread, but for a given channel there's always the same single thread that calls the channel handlers.


import java.util.ArrayDeque;

public class Netty4HttpHeaderValidator extends ChannelInboundHandlerAdapter {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is it possible to get some javadoc for what this is doing ? I get that it is the hook in point to "validate headers" which can be used to perform authenticatation from the headers ...but all the technical details are beyond my understanding and docs for ChannelInboundHandlerAdapter does not help much. Also, I don't think my review for this class will be worth much ...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Of course we'll polish this, with javadoc, comments, etc. It's TimB creation from #92220 . It also does slightly more than what we strictly need (closing a connection in case authn fails), and there is a small thing that I need to clarify myself when we forward the request but drop the contents.

All that's to say is that this needs trimmings, and it will be a separate PR for it to review and merge.

I propose we sync to explain how it works/supposed to work before we get to the polishing stage.

Comment on lines 377 to 381
if (badRequestCause != null) {
dispatcher.dispatchBadRequest(channel, threadContext, badRequestCause);
} else {
populateRequestThreadContext0(restRequest, channel, threadContext);
dispatcher.dispatchRequest(restRequest, channel, threadContext);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this happening before or after authentication ?

@@ -28,50 +34,185 @@
public interface RestRequestFilter {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

why does this class need to change ?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good question!
Short answer is that this filtering, which is needed for auditing, currently works on RestRequest types, but we now need it to work on HTTP request bodies.

And that's because auditing works with HTTP requests, because authentication now has to work with HTTP requests, and the two are very much entangled.

BUT I have a proposal to decouple auditing for authentication success from the actual authentication (which sounds bad, I admit) and this is the main reason: today, when we only support authentication on the "REST" channel (no transport client), and the authentication happens before the request body is available, and we want to audit the request body, we cannot cleanly couple authentication and auditing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Basically, it has to do with auditing, which is not properly addressed by this PR anyway (authn success is audited but not the request body), and for which I have a different proposal anyway (that will not require such changes).

this.closeChannel = closeChannel;
}

public boolean shouldCloseChannel() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is it OK to close the channel for HTTP pipeline requests ? (will that kill the other requests)

Copy link
Copy Markdown
Contributor Author

@albertzaharovits albertzaharovits Feb 24, 2023

Choose a reason for hiding this comment

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

We don't need this functionality. We don't have it today on main and it's not something we plan to ship (confirmed with TimB).

It's added by TimB in #92220 , and I deliberately kept changes to the channel handler implementation to a minimum to ease the rewieving of this for him (but in the process also complicating it for you, sorry...).

try (ThreadContext.StoredContext ignore = threadContext.stashContext()) {
populateThreadContext.apply(requestToAuthenticate, channel, threadContext);
authenticate.accept(requestToAuthenticate, ActionListener.wrap(ignored -> {
final ThreadContext.StoredContext authenticatedContext = threadContext.newStoredContext();
Copy link
Copy Markdown
Contributor

@jakelandis jakelandis Feb 23, 2023

Choose a reason for hiding this comment

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

I think I remember you said that we need to associate this authenticated context with the request since the context will get lost since it may not part of the same call stack... but can you detail abit more why that is ? Isn't both this code (storing the authenticated context) and the code that restores the authenticated context happening on the same eventloop thread ? (also, what thread does the existing (before this PR) authentication happen on -> the eventloop thread?)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants