Skip to content
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

Introducing ClientRequestFilter.java: A New Plugin for Applying Request Headers in the Authentication Filter Class #23380

Open
wants to merge 16 commits into
base: master
Choose a base branch
from

Conversation

SthuthiGhosh9400
Copy link

@SthuthiGhosh9400 SthuthiGhosh9400 commented Aug 5, 2024

Description

  1. Created an interface (RequestModifier.java) in Presto to add additional headers in request.
  2. Setting extra credential in request header is a use case.

Motivation and Context

Impact

Test Plan

Contributor checklist

  • Please make sure your submission complies with our development, formatting, commit message, and attribution guidelines.
  • PR description addresses the issue accurately and concisely. If the change is non-trivial, a GitHub Issue is referenced.
  • Documented new properties (with its default value), SQL syntax, functions, or other functionality.
  • If release notes are required, they follow the release notes guidelines.
  • Adequate tests were added if applicable.
  • CI passed.

Release Notes

Please follow release notes guidelines and fill in the release notes below.

== RELEASE NOTES ==

General Changes

* Add  `RequestModifier.java` interface in Presto-spi :pr:`23380`
* Improve Request Headers in the Authentication Filter Class :pr:`23380`

Copy link
Contributor

@tdcmeehan tdcmeehan left a comment

Choose a reason for hiding this comment

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

This PR needs some test cases.

@steveburnett
Copy link
Contributor

Nit rephrasing of release note entries following the Order of changes in the Release Notes Guidelines.

== RELEASE NOTES ==

General Changes

* Add  `RequestModifier.java` interface in Presto-spi :pr:`23380`
* Improve Request Headers in the Authentication Filter Class :pr:`23380`

@SthuthiGhosh9400
Copy link
Author

@tdcmeehan

I have added a test case to verify setting values in the request header and committed it.
Could you have a check and confirm ?
Thanks.

@@ -499,6 +499,11 @@
<artifactId>ratis-common</artifactId>
<optional>true</optional>
</dependency>
<dependency>
Copy link
Contributor

Choose a reason for hiding this comment

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

We do not use Mockito in this project. Can you please refactor your code to avoid its use?

Copy link
Author

@SthuthiGhosh9400 SthuthiGhosh9400 Sep 4, 2024

Choose a reason for hiding this comment

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

@tdcmeehan
Instead of using Mockito to create mock objects and define their behavior, can I manually write simple classes (stubs) that simulate the behavior of the actual classes?

Do you have any suggestion here in order to verify the codebase if value correctly passed to request header?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, that is how we go about this.

Copy link
Author

Choose a reason for hiding this comment

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

@tdcmeehan
I have refactored the code that was added for the test case.

assertEquals("CustomValue", wrappedRequest.getHeader("X-Custom-Header"));
}

abstract static class HttpServletRequestAdapter
Copy link
Contributor

Choose a reason for hiding this comment

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

There's already a MockHttpServletRequest, can you use that?

Copy link
Author

Choose a reason for hiding this comment

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

@tdcmeehan
I have refactored the code to use MockHttpServletRequest. Could you check it?
Thanks.

@tdcmeehan
Copy link
Contributor

While we don't use Mockito, the stubs in the test are a little out of control. We need to reduce the size of this simple test. I have a couple of suggestions that you can research to make the testing simpler.

  • Can you look at TestHttpRemoteTask and examine how they implemented an in-memory endpoint for the purposes of testing? Perhaps you can build something similar.
  • TestingPrestoServer has all sorts of helper methods to retrieve individual classes from Presto. Perhaps you can add a new getter to retrieve the RequestModifierManager, register a simple new request modifier that just adds a dummy header, and make a client call that proves the header is added.

@SthuthiGhosh9400
Copy link
Author

  • TestingPrestoServer has all sorts of helper methods to retrieve individual classes from Presto. Perhaps you can add a new getter to retrieve the RequestModifierManager, register a simple new request modifier that just adds a dummy header, and make a client call that proves the header is added.

@tdcmeehan
I have attempted to improve the test case based on the suggestion. Could you have a check?
Thanks.

@@ -825,4 +830,25 @@ private static int driftServerPort(DriftServer server)
{
return ((DriftNettyServerTransport) server.getServerTransport()).getPort();
}

public RequestModifierManager getRequestModifierManager()
Copy link
Contributor

Choose a reason for hiding this comment

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

Please, just add a method to return the RequestModifierManager, and let users inject whatever custom modifier they wish.


public class RequestModifierManager
{
private final List<RequestModifier> requestModifiers;
Copy link
Contributor

Choose a reason for hiding this comment

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

This needs to be thread safe. I recommend using a CopyOnWriteArrayList.

// authentication succeeded
nextFilter.doFilter(withPrincipal(request, principal), response);
CustomHttpServletRequestWrapper wrappedRequest = withPrincipal(request, principal);
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of modifying the authentication filter, this should just go in its own filter that is applied after the authentication filter.

this.customHeaders = new HashMap<>();
}

public void addHeader(String name, String value)
Copy link
Contributor

Choose a reason for hiding this comment

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

Unused

public static class CustomHttpServletRequestWrapper
extends HttpServletRequestWrapper
{
private final Map<String, String> customHeaders;
Copy link
Contributor

Choose a reason for hiding this comment

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

Please use a concurrent map

import java.util.Map;
import java.util.Optional;

public interface RequestModifier
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's call this something more descriptive, such as ClientRequestFilter.

@SthuthiGhosh9400
Copy link
Author

@tdcmeehan
I have incorporated the review comments. Could you please verify them?
Thanks.

@SthuthiGhosh9400 SthuthiGhosh9400 changed the title Introducing RequestModifier.java: A New Plugin for Modifying Request Headers in the Authentication Filter Class Introducing ClientRequestFilter.java: A New Plugin for Applying Request Headers in the Authentication Filter Class Sep 10, 2024

public class ClientRequestFilterManager
{
private final CopyOnWriteArrayList<ClientRequestFilter> clientRequestFilters;
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
private final CopyOnWriteArrayList<ClientRequestFilter> clientRequestFilters;
private final List<ClientRequestFilter> clientRequestFilters;

Comment on lines 24 to 28
private final CopyOnWriteArrayList<ClientRequestFilter> clientRequestFilters;
public ClientRequestFilterManager()
{
this.clientRequestFilters = new CopyOnWriteArrayList<>();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
private final CopyOnWriteArrayList<ClientRequestFilter> clientRequestFilters;
public ClientRequestFilterManager()
{
this.clientRequestFilters = new CopyOnWriteArrayList<>();
}
private final List<ClientRequestFilter> clientRequestFilters = new CopyOnWriteArrayList<>();


public List<ClientRequestFilter> getClientRequestFilters()
{
return Collections.unmodifiableList(clientRequestFilters);
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
return Collections.unmodifiableList(clientRequestFilters);
return ImmutableList.copyOf(clientRequestFilters);

extraHeaderValueMap.ifPresent(map -> {
for (Map.Entry<String, String> extraHeaderEntry : map.entrySet()) {
if (request.getHeader(extraHeaderEntry.getKey()) == null) {
extraHeadersMap.putIfAbsent(extraHeaderEntry.getKey(), extraHeaderEntry.getValue());
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think we should silently not put the header if there's an existing header. I think we should consider the following:

  1. The SPI contract mandates that any potential header is known upfront.
  2. The runtime will validate that the headers that are requested to be modified match with what is actually returned.
  3. There is a blocklist of headers that the plugin should not modify (such as query ID).
  4. The runtime will validate that all registered filters only return a disjoint set of headers being added. If there is a conflict, this is a fatal error that will require an administrator to fix (but un-registering one of these plugins).
  5. If there is a conflict here, then an exception is thrown.

Copy link
Author

Choose a reason for hiding this comment

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

@tdcmeehan
I have gone through your comments and added a few validations in the current implementation. I can verify that with you. However, I have to know about the headers in the blocklist that should not be modified.
Could you help?

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's start with the query ID, trace ID and transaction ID in PrestoHeaders.java.

Copy link
Author

Choose a reason for hiding this comment

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

I have incorporated the changes mentioned in your comment. Could you please review them and share your suggestions?

Comment on lines 203 to 209
private final Map<String, String> customHeaders;

public CustomHttpServletRequestWrapper(HttpServletRequest request)
{
super(request);
this.customHeaders = new ConcurrentHashMap<>();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
private final Map<String, String> customHeaders;
public CustomHttpServletRequestWrapper(HttpServletRequest request)
{
super(request);
this.customHeaders = new ConcurrentHashMap<>();
}
private final Map<String, String> customHeaders = new ConcurrentHashMap<>();

Copy link
Contributor

@tdcmeehan tdcmeehan left a comment

Choose a reason for hiding this comment

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

Some feedback is still unadressed.

for (Map.Entry<String, String> extraHeaderEntry : map.entrySet()) {
String headerKey = extraHeaderEntry.getKey();
if (headersBlockList.contains(headerKey)) {
throw new RuntimeException("Modification attempt detected: The header " + headerKey + " is present in the blocked headers list.");
Copy link
Contributor

Choose a reason for hiding this comment

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

Please throw a PrestoException and introduce a new error code for this. It should be a system error.

while (originalHeaderNames.hasMoreElements()) {
headerNames.add(originalHeaderNames.nextElement());
}
return Collections.enumeration(headerNames);
Copy link
Contributor

Choose a reason for hiding this comment

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

Please use Immutable collections.

@SthuthiGhosh9400
Copy link
Author

Some feedback is still unadressed.

@tdcmeehan Could you help me identify which feedback is still pending? I believe I have addressed feedback items 2, 3, 4, and 5 from the list above.

@tdcmeehan
Copy link
Contributor

Some feedback is still unadressed.

@tdcmeehan Could you help me identify which feedback is still pending? I believe I have addressed feedback items 2, 3, 4, and 5 from the list above.

Please scroll up in this PR and examine feedback on the code that is still remaining.

@SthuthiGhosh9400
Copy link
Author

Please scroll up in this PR and examine feedback on the code that is still remaining.

Yeah. @tdcmeehan I have incorporated all feedback items now. Kindly review them and share your suggestions?

Thanks.

Comment on lines 215 to 244
ClientRequestFilter customModifier = new ClientRequestFilter()
{
@Override
public List<String> getHeaderNames()
{
return Collections.singletonList("X-Custom-Header");
}
@Override
public <T> Optional<Map<String, String>> getExtraHeaders(T additionalInfo)
{
Map<String, String> headers = new HashMap<>();
headers.put("X-Custom-Header", "CustomValue_1");
return Optional.of(headers);
}
};

ClientRequestFilter customModifierConflict = new ClientRequestFilter()
{
@Override
public List<String> getHeaderNames()
{
return Collections.singletonList("X-Custom-Header");
}
@Override
public <T> Optional<Map<String, String>> getExtraHeaders(T additionalInfo)
{
Map<String, String> headers = new HashMap<>();
headers.put("X-Custom-Header", "CustomValue_2");
return Optional.of(headers);
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Please reduce duplicate code. Consider an inner helper class.

}

static class ConcreteHttpServletRequest
extends MockHttpServletRequest
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not modify MockHttpServletRequest directly?

Copy link
Author

Choose a reason for hiding this comment

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

In ConcreteHttpServletRequest, I am storing additional headers in the customHeaders map and overriding the getHeaders method to manage them. This functionality isn't natively provided by MockHttpServletRequest, so extending it makes sense.

@SthuthiGhosh9400
Copy link
Author

@tdcmeehan
I have incorporated the review comments. Kindly review them.
If it looks good, I can squash the commits and edit the commit message according to the guidelines.

Copy link
Contributor

@tdcmeehan tdcmeehan left a comment

Choose a reason for hiding this comment

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

Please refactor tests to use the code under test.

for (Map.Entry<String, String> extraHeaderEntry : map.entrySet()) {
String headerKey = extraHeaderEntry.getKey();
if (headersBlockList.contains(headerKey)) {
throw new PrestoException(HEADER_MODIFICATION_ATTEMPT, "Modification attempt detected: The header " + headerKey + " is present in the blocked headers list.");
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of mentioning a "blocked headers list", just list out the headers that are not allowed to be modified.

throw new PrestoException(HEADER_MODIFICATION_ATTEMPT, "Modification attempt detected: The header " + headerKey + " is present in the blocked headers list.");
}
if (globallyAddedHeaders.contains(headerKey)) {
throw new RuntimeException("Header conflict detected: " + headerKey + " already added by another filter.");
Copy link
Contributor

Choose a reason for hiding this comment

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

Do not throw RuntimeException, throw PrestoException.

for (Map.Entry<String, String> extraHeaderEntry : map.entrySet()) {
String headerKey = extraHeaderEntry.getKey();
if (headersBlockList.contains(headerKey)) {
throw new RuntimeException("Modification attempt detected: The header " + headerKey + " is present in the blocked headers list.");
Copy link
Contributor

Choose a reason for hiding this comment

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

Why are we duplicating code here? I am very confused about how this test works.

@SthuthiGhosh9400
Copy link
Author

@tdcmeehan
I have refactored the test cases and used a separate method for adding headers to the request in the Authentication filter.
Could you review it and share your suggestions?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants