Skip to content

Conversation

vishalya
Copy link
Member

Description

The parsing of the user and query was happening at different places and at a later stages, this PR structures it to happen at a early stage and a single point in time.

Additional context and related issues

  • Early Request Parsing: New JAX-RS filters have been added to intercept incoming requests. This filter is responsible for parsing essential user and query information at the earliest stage of processing under the PRE_AUTHENTICATION and PRE_AUTHORIZATION.
  • Use of ContainerRequestContext: The implementation now uses ContainerRequestContext instead of HttpServletRequest to access request details, as this is the object available within the JAX-RS filter context.
  • Injecting Request Attributes: The extracted user and query information is now passed along via request attributes, making it easily accessible in the later stages of request routing

Release notes

(X) This is not user-visible or is docs only, and no release notes are required.
( ) Release notes are required, with the following suggested text:

@vishalya
Copy link
Member Author

Addressed the review comments.

{
String path = requestContext.getUriInfo().getRequestUri().getPath();
RequestAnalyzerConfig reqAnalyzerconfig = haGatewayConfiguration.getRequestAnalyzerConfig();
if (!reqAnalyzerconfig.isAnalyzeRequest() ||
Copy link
Contributor

Choose a reason for hiding this comment

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

why do you in some places (like here) explicitly call reqAnalyzerconfig.isAnalyzeRequest(), and in others (FileBasedRoutingGroupSelector innit) do you do analyzeRequest = requestAnalyzerConfig.isAnalyzeRequest();

Copy link
Member Author

Choose a reason for hiding this comment

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

Here, I need a reference to haGatewayConfiguration for the isPathWhiteListed in addition to RequestAnalyzerConfig, so the pattern of usage is different.

return;
}

log.info("Processing query metadata");
Copy link
Contributor

Choose a reason for hiding this comment

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

  1. maybe add queryID?
  2. maybe change to debug and not info? could flood logs as its done for every req.

Copy link
Member Author

Choose a reason for hiding this comment

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

We may not have query id on certain requests but converting it into debug.

Comment on lines 31 to 37
/**
*
* A filter to parse the query request headers related to user and store the user information as a property
* in the request for later use
*/
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
/**
*
* A filter to parse the query request headers related to user and store the user information as a property
* in the request for later use
*/
/**
* A filter which parses and extracts Trino user identity from incoming request headers
* and stores it in the request context property TRINO_REQUEST_USER
* for downstream filters and handlers to use.
*/

}

@Override
public void filter(ContainerRequestContext requestContext)
Copy link
Contributor

Choose a reason for hiding this comment

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

NIT: filter both parses user info and stores it in request context. Perhaps split user extraction logic into a util func?

throws IOException
{
String path = requestContext.getUriInfo().getRequestUri().getPath();
RequestAnalyzerConfig reqAnalyzerconfig = haGatewayConfiguration.getRequestAnalyzerConfig();
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
RequestAnalyzerConfig reqAnalyzerconfig = haGatewayConfiguration.getRequestAnalyzerConfig();
RequestAnalyzerConfig reqAnalyzerConfig = haGatewayConfiguration.getRequestAnalyzerConfig();

missing caps

Copy link
Member

Choose a reason for hiding this comment

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

Comment on lines 193 to 194
.httpHeader("X-Trino-Catalog", defaultCatalog)
.httpHeader("X-trino-catalog", defaultCatalog)
Copy link
Contributor

Choose a reason for hiding this comment

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

why is there both capital T and lower t? in -Trino-?
official docs only has uppercase.
(applies to below as well)

image

Copy link
Member Author

Choose a reason for hiding this comment

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

Making it consistent.

Comment on lines +55 to +57
private ContainerRequestContext requestContext = mock(ContainerRequest.class);
private HttpServletRequest mockRequest = mock(HttpServletRequest.class);
Copy link
Contributor

Choose a reason for hiding this comment

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

mockRequest and requestContext are reused, do you want to retain state across multiple test runs ?

Copy link
Member Author

Choose a reason for hiding this comment

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

The idea is that a new object of QueryRequestMock will be created for each test.

MediaType mediaType = new MediaType("application", "json", java.util.Map.of("charset", "UTF-8"));
when(requestContext.getMediaType()).thenReturn(mediaType);

String json = "Select xyz from cat1.schema1.table1";
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
String json = "Select xyz from cat1.schema1.table1";
String queryText = "Select xyz from cat1.schema1.table1";

verify(requestContext).setProperty(eq(TRINO_QUERY_PROPERTIES), captor.capture());

// Optionally, check that the entity stream was read (if your filter does so)
verify(requestContext).getEntityStream();
Copy link
Contributor

Choose a reason for hiding this comment

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

you check here the mock was called, perhaps check also that buffering worked or query was parsed?

Copy link
Member Author

Choose a reason for hiding this comment

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

Added the buffering check.

{
HaGatewayConfiguration config = new HaGatewayConfiguration();
requestAnalyzerConfig = new RequestAnalyzerConfig();
requestAnalyzerConfig.setAnalyzeRequest(true);
Copy link
Contributor

Choose a reason for hiding this comment

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

Only tests the happy path when = true. What about false?

Copy link
Member Author

Choose a reason for hiding this comment

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

Added some false paths too.

import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

class TestQueryUserInfoParser
Copy link
Member

Choose a reason for hiding this comment

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

https://trino.io/docs/current/develop/tests.html

Test classes should be defined as package-private and final.

{
private QueryUserInfoParser filter;

@BeforeEach
Copy link
Member

Choose a reason for hiding this comment

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

Comment on lines 65 to 67
catch (URISyntaxException e) {
throw new RuntimeException(e);
}
Copy link
Member

Choose a reason for hiding this comment

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

Remove redundant catch block.


UriInfo uriInfo = mock(ExtendedUriInfo.class);
try {
when(uriInfo.getRequestUri()).thenReturn(new URI("http://localhost" + HttpUtils.V1_STATEMENT_PATH));
Copy link
Member

Choose a reason for hiding this comment

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

Static import V1_STATEMENT_PATH.

@@ -89,7 +89,7 @@ static RulesExternalConfiguration provideRoutingRuleExternalConfig()

@Test
void testByRoutingRulesExternalEngine()
throws URISyntaxException
throws URISyntaxException, IOException
Copy link
Member

Choose a reason for hiding this comment

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

Throwing Exception is fine in test methods.

*/
package io.trino.gateway.ha.security.util;

public final class Priorities
Copy link
Member

Choose a reason for hiding this comment

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

The name is too general.

public static final int HEADER_DECORATOR = 3000;
public static final int ENTITY_CODER = 4000;
public static final int USER = 5000;
public static final int ROUTE_TO_BACKEND = 6000;
Copy link
Member

Choose a reason for hiding this comment

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

Remove unsued codes.

implements ContainerRequestFilter
{
private static final Logger log = Logger.get(QueryUserInfoParser.class);
private final HaGatewayConfiguration haGatewayConfiguration;
Copy link
Member

Choose a reason for hiding this comment

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

Don't put mutable objects in fields. Same for other classes.

throws IOException
{
String path = requestContext.getUriInfo().getRequestUri().getPath();
RequestAnalyzerConfig reqAnalyzerconfig = haGatewayConfiguration.getRequestAnalyzerConfig();
Copy link
Member

Choose a reason for hiding this comment

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

@vishalya vishalya force-pushed the parse-user-query branch 2 times, most recently from 239f66a to 68e1860 Compare August 29, 2025 20:45
@oneonestar
Copy link
Member

How about using a single filter to parse all the data instead of having multiple filters?
We can store all the necessary data in a single object.
I think this would simplify the code and also help avoid dependency or execution order issues between multiple filters in the future.

@vishalya
Copy link
Member Author

vishalya commented Sep 3, 2025

I would like to keep it separate as they are different and will be parsed at different priorities (user info might need to be parsed at pre-authentication and query might be parsed at pre-authorization), The query information may not be present for all the requests.

@vishalya
Copy link
Member Author

Added a new class PathFilter to reuse the logic in the filters.

@vishalya
Copy link
Member Author

@ebyhr - I have addressed the review comments.


String charset = mediaType.getParameters().get("charset");
if (!StandardCharsets.UTF_8.name().equalsIgnoreCase(charset)) {
return;
Copy link
Member

@Chaho12 Chaho12 Sep 17, 2025

Choose a reason for hiding this comment

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

should we log a warning that it is missing charset? because request without charset is quite common isn't it?
sth like this?

log.debug("Request charset is not UTF-8 (%s), skipping query parsing", charset);

import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
Copy link
Member

Choose a reason for hiding this comment

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

in like 93, we already imported static StandardCharsets.UTF_8. We should use that instead


TrinoRequestUser user = new TrinoRequestUser.TrinoRequestUserProvider(requestAnalyzerConfig).getInstance(requestContext);
requestContext.setProperty(TRINO_REQUEST_USER, user);
log.debug("Parsed user %s", user.getUser().orElse("None"));
Copy link
Member

Choose a reason for hiding this comment

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

Nit.

Suggested change
log.debug("Parsed user %s", user.getUser().orElse("None"));
log.debug("Parsed user: %s", user.getUser().orElse("None"));

return;
}

RequestAnalyzerConfig requestAnalyzerConfig = new RequestAnalyzerConfig();
Copy link
Member

@Chaho12 Chaho12 Sep 17, 2025

Choose a reason for hiding this comment

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

Do we need to create a new RequestAnalyzerConfig object instead of reusing the configuration that was already injected into the class? Then there is no need to extract and recreate fields.

@Named("statementPaths") List<String> statementPaths,
@Named("extraWhitelistPaths") List<String> extraWhitelistPaths)
{
this.statementPaths = Set.copyOf(statementPaths);
Copy link
Member

Choose a reason for hiding this comment

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

nit. log that it cannot be null?

Suggested change
this.statementPaths = Set.copyOf(statementPaths);
this.statementPaths = Set.copyOf(requireNonNull(statementPaths, "statementPaths cannot be null"));

Comment on lines +49 to +51
this.extraWhitelistPatterns = requireNonNull(extraWhitelistPaths).stream()
.map(Pattern::compile)
.collect(toImmutableList());
Copy link
Member

Choose a reason for hiding this comment

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

same. tell pattern cannot be null and throw the pattern exception log

Suggested change
this.extraWhitelistPatterns = requireNonNull(extraWhitelistPaths).stream()
.map(Pattern::compile)
.collect(toImmutableList());
this.extraWhitelistPatterns = requireNonNull(extraWhitelistPaths, "extraWhitelistPaths cannot be null").stream()
.map(pattern -> {
try {
return Pattern.compile(pattern);
} catch (PatternSyntaxException e) {
throw new IllegalArgumentException("Invalid regex pattern: " + pattern, e);
}
})
.collect(toImmutableList());

return;
}

log.debug("Processing query metadata");
Copy link
Member

Choose a reason for hiding this comment

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

nit. make it for useful and tell path

Suggested change
log.debug("Processing query metadata");
log.debug("Processing query metadata for path: %s", path);

Comment on lines +73 to +77
TrinoQueryProperties queryProps = new TrinoQueryProperties(requestContext,
isClientsUseV2Format,
maxBodySize);

requestContext.setProperty(TRINO_QUERY_PROPERTIES, queryProps);
Copy link
Member

Choose a reason for hiding this comment

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

How about try catch exception as TrinoQueryProperties could throw exception?
Cause gateway should continue processing even if query parsing fails

}

String charset = mediaType.getParameters().get("charset");
if (!StandardCharsets.UTF_8.name().equalsIgnoreCase(charset)) {
Copy link
Member

Choose a reason for hiding this comment

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

We should first check if charset is null first.

// Should not allow other paths
assertThat(statementOnlyFilter.isPathWhiteListed("/api/custom")).isFalse();
assertThat(statementOnlyFilter.isPathWhiteListed("/health")).isFalse();
}
Copy link
Member

Choose a reason for hiding this comment

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

can you add test for configuration validatinons? Test null statement paths, null whitelist paths, invalid regex pattern etc.

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

Successfully merging this pull request may close these issues.

5 participants