-
Notifications
You must be signed in to change notification settings - Fork 110
Add request filter for early user and query parsing #748
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
base: main
Are you sure you want to change the base?
Conversation
gateway-ha/src/main/java/io/trino/gateway/ha/config/HaGatewayConfiguration.java
Outdated
Show resolved
Hide resolved
gateway-ha/src/main/java/io/trino/gateway/ha/router/TrinoQueryProperties.java
Outdated
Show resolved
Hide resolved
gateway-ha/src/main/java/io/trino/gateway/ha/router/TrinoQueryProperties.java
Outdated
Show resolved
Hide resolved
gateway-ha/src/main/java/io/trino/gateway/ha/router/TrinoQueryProperties.java
Outdated
Show resolved
Hide resolved
gateway-ha/src/main/java/io/trino/gateway/ha/router/TrinoQueryProperties.java
Show resolved
Hide resolved
5c57364
to
28caef2
Compare
Addressed the review comments. |
{ | ||
String path = requestContext.getUriInfo().getRequestUri().getPath(); | ||
RequestAnalyzerConfig reqAnalyzerconfig = haGatewayConfiguration.getRequestAnalyzerConfig(); | ||
if (!reqAnalyzerconfig.isAnalyzeRequest() || |
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.
why do you in some places (like here) explicitly call reqAnalyzerconfig.isAnalyzeRequest()
, and in others (FileBasedRoutingGroupSelector innit) do you do analyzeRequest = requestAnalyzerConfig.isAnalyzeRequest();
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.
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"); |
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.
- maybe add queryID?
- maybe change to debug and not info? could flood logs as its done for every req.
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.
We may not have query id on certain requests but converting it into debug.
/** | ||
* | ||
* 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 | ||
*/ |
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.
/** | |
* | |
* 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) |
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.
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(); |
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.
RequestAnalyzerConfig reqAnalyzerconfig = haGatewayConfiguration.getRequestAnalyzerConfig(); | |
RequestAnalyzerConfig reqAnalyzerConfig = haGatewayConfiguration.getRequestAnalyzerConfig(); |
missing caps
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.
.httpHeader("X-Trino-Catalog", defaultCatalog) | ||
.httpHeader("X-trino-catalog", defaultCatalog) |
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.
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.
Making it consistent.
private ContainerRequestContext requestContext = mock(ContainerRequest.class); | ||
private HttpServletRequest mockRequest = mock(HttpServletRequest.class); |
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.
mockRequest and requestContext are reused, do you want to retain state across multiple test runs ?
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.
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"; |
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.
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(); |
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.
you check here the mock was called, perhaps check also that buffering worked or query was parsed?
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.
Added the buffering check.
{ | ||
HaGatewayConfiguration config = new HaGatewayConfiguration(); | ||
requestAnalyzerConfig = new RequestAnalyzerConfig(); | ||
requestAnalyzerConfig.setAnalyzeRequest(true); |
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.
Only tests the happy path when = true. What about false?
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.
Added some false paths too.
import static org.mockito.Mockito.verify; | ||
import static org.mockito.Mockito.when; | ||
|
||
class TestQueryUserInfoParser |
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.
https://trino.io/docs/current/develop/tests.html
Test classes should be defined as package-private and final.
{ | ||
private QueryUserInfoParser filter; | ||
|
||
@BeforeEach |
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.
catch (URISyntaxException e) { | ||
throw new RuntimeException(e); | ||
} |
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.
Remove redundant catch block.
|
||
UriInfo uriInfo = mock(ExtendedUriInfo.class); | ||
try { | ||
when(uriInfo.getRequestUri()).thenReturn(new URI("http://localhost" + HttpUtils.V1_STATEMENT_PATH)); |
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.
Static import V1_STATEMENT_PATH.
@@ -89,7 +89,7 @@ static RulesExternalConfiguration provideRoutingRuleExternalConfig() | |||
|
|||
@Test | |||
void testByRoutingRulesExternalEngine() | |||
throws URISyntaxException | |||
throws URISyntaxException, IOException |
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.
Throwing Exception
is fine in test methods.
*/ | ||
package io.trino.gateway.ha.security.util; | ||
|
||
public final class Priorities |
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.
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; |
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.
Remove unsued codes.
implements ContainerRequestFilter | ||
{ | ||
private static final Logger log = Logger.get(QueryUserInfoParser.class); | ||
private final HaGatewayConfiguration haGatewayConfiguration; |
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.
Don't put mutable objects in fields. Same for other classes.
throws IOException | ||
{ | ||
String path = requestContext.getUriInfo().getRequestUri().getPath(); | ||
RequestAnalyzerConfig reqAnalyzerconfig = haGatewayConfiguration.getRequestAnalyzerConfig(); |
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.
239f66a
to
68e1860
Compare
How about using a single filter to parse all the data instead of having multiple filters? |
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. |
68e1860
to
b97df7f
Compare
b97df7f
to
b202da0
Compare
Added a new |
@ebyhr - I have addressed the review comments. |
|
||
String charset = mediaType.getParameters().get("charset"); | ||
if (!StandardCharsets.UTF_8.name().equalsIgnoreCase(charset)) { | ||
return; |
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.
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; |
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.
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")); |
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.
Nit.
log.debug("Parsed user %s", user.getUser().orElse("None")); | |
log.debug("Parsed user: %s", user.getUser().orElse("None")); |
return; | ||
} | ||
|
||
RequestAnalyzerConfig requestAnalyzerConfig = new RequestAnalyzerConfig(); |
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.
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); |
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.
nit. log that it cannot be null?
this.statementPaths = Set.copyOf(statementPaths); | |
this.statementPaths = Set.copyOf(requireNonNull(statementPaths, "statementPaths cannot be null")); |
this.extraWhitelistPatterns = requireNonNull(extraWhitelistPaths).stream() | ||
.map(Pattern::compile) | ||
.collect(toImmutableList()); |
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.
same. tell pattern cannot be null and throw the pattern exception log
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"); |
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.
nit. make it for useful and tell path
log.debug("Processing query metadata"); | |
log.debug("Processing query metadata for path: %s", path); |
TrinoQueryProperties queryProps = new TrinoQueryProperties(requestContext, | ||
isClientsUseV2Format, | ||
maxBodySize); | ||
|
||
requestContext.setProperty(TRINO_QUERY_PROPERTIES, queryProps); |
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.
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)) { |
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.
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(); | ||
} |
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.
can you add test for configuration validatinons? Test null statement paths, null whitelist paths, invalid regex pattern etc.
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
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: