This is an automated email from the ASF dual-hosted git repository.
yashmayya pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git
The following commit(s) were added to refs/heads/master by this push:
new 8705c083394 Mask credential query parameters in the controller request
log (#19899)
8705c083394 is described below
commit 8705c08339413a43bf2e13b925e06a0c5833c59c
Author: Dylan Dreyer <[email protected]>
AuthorDate: Thu Oct 15 18:59:43 2026 -0700
Mask credential query parameters in the controller request log (#19899)
Co-authored-by: Claude Opus 5.5 <[email protected]>
---
.../api/resources/ControllerResponseFilter.java | 51 +++++++++++++++-
.../resources/ControllerResponseFilterTest.java | 71 ++++++++++++++++++++++
2 files changed, 121 insertions(+), 1 deletion(-)
diff --git
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/ControllerResponseFilter.java
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/ControllerResponseFilter.java
index c03bb345a89..3d478aedf88 100644
---
a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/ControllerResponseFilter.java
+++
b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/ControllerResponseFilter.java
@@ -18,13 +18,24 @@
*/
package org.apache.pinot.controller.api.resources;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.annotations.VisibleForTesting;
import java.io.IOException;
+import java.net.URI;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.StringJoiner;
import javax.inject.Inject;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerResponseContext;
import javax.ws.rs.container.ContainerResponseFilter;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.ext.Provider;
+import org.apache.pinot.spi.utils.Obfuscator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -42,7 +53,7 @@ public class ControllerResponseFilter implements
ContainerResponseFilter {
public void filter(ContainerRequestContext requestContext,
ContainerResponseContext responseContext)
throws IOException {
final String method = requestContext.getMethod();
- final String uri = requestContext.getUriInfo().getRequestUri().toString();
+ final String uri =
redactQueryParameters(requestContext.getUriInfo().getRequestUri());
final int respStatus = responseContext.getStatus();
final String reasonPhrase =
responseContext.getStatusInfo().getReasonPhrase();
final String srcIpAddr = _request.get().getRemoteAddr();
@@ -50,4 +61,42 @@ public class ControllerResponseFilter implements
ContainerResponseFilter {
LOGGER.info("Handled request from {} {} {}, content-type {} status code {}
{}", srcIpAddr, method, uri, contentType,
respStatus, reasonPhrase);
}
+
+ /// Returns the request URI with the values of credential-bearing query
parameters masked, as decided by
+ /// [Obfuscator#DEFAULT]. This line is logged at INFO for every request, so
a token passed as a query
+ /// parameter would otherwise be written to the controller log verbatim.
Other parameters are kept in
+ /// their original order with their values as sent, since they are what
makes the line useful when
+ /// debugging. Names are shown decoded, and repeated parameters are grouped
under their first occurrence.
+ @VisibleForTesting
+ static String redactQueryParameters(URI requestUri) {
+ String rawQuery = requestUri.getRawQuery();
+ String uri = requestUri.toString();
+ if (rawQuery == null || rawQuery.isEmpty()) {
+ return uri;
+ }
+ String withoutQuery = uri.substring(0, uri.indexOf('?'));
+ // Keyed by the decoded name, which is what the patterns judge; the values
stay raw. A URI has already
+ // rejected malformed escapes, so decoding cannot fail here.
+ Map<String, List<String>> parameters = new LinkedHashMap<>();
+ for (String pair : rawQuery.split("&")) {
+ int separator = pair.indexOf('=');
+ String rawName = separator < 0 ? pair : pair.substring(0, separator);
+ String rawValue = separator < 0 ? "" : pair.substring(separator + 1);
+ parameters.computeIfAbsent(URLDecoder.decode(rawName,
StandardCharsets.UTF_8), k -> new ArrayList<>())
+ .add(rawValue);
+ }
+ JsonNode redacted = Obfuscator.DEFAULT.toJson(parameters);
+ StringJoiner query = new StringJoiner("&", "?", "");
+ redacted.fields().forEachRemaining(parameter -> {
+ String name = parameter.getKey();
+ JsonNode value = parameter.getValue();
+ if (value.isArray()) {
+ value.forEach(element -> query.add(name + "=" + element.asText()));
+ } else {
+ // A masked parameter is replaced as a whole, so the number of values
it had is not disclosed.
+ query.add(name + "=" + value.asText());
+ }
+ });
+ return withoutQuery + query;
+ }
}
diff --git
a/pinot-controller/src/test/java/org/apache/pinot/controller/api/resources/ControllerResponseFilterTest.java
b/pinot-controller/src/test/java/org/apache/pinot/controller/api/resources/ControllerResponseFilterTest.java
new file mode 100644
index 00000000000..b284d449efd
--- /dev/null
+++
b/pinot-controller/src/test/java/org/apache/pinot/controller/api/resources/ControllerResponseFilterTest.java
@@ -0,0 +1,71 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.pinot.controller.api.resources;
+
+import java.net.URI;
+import org.testng.annotations.Test;
+
+import static org.testng.Assert.assertEquals;
+
+
+public class ControllerResponseFilterTest {
+ private static final String BASE = "https://controller:9000/tables/myTable";
+ private static final String SECRET = "FAKE-SECRET-VALUE";
+
+ @Test
+ public void testMasksCredentialQueryParametersAndKeepsTheRest() {
+ String logged = redact(BASE + "?type=OFFLINE&accessToken=" + SECRET +
"&password=" + SECRET + "&verbose=true");
+
+ assertEquals(logged, BASE +
"?type=OFFLINE&accessToken=*****&password=*****&verbose=true");
+ }
+
+ @Test
+ public void testJudgesTheDecodedName() {
+ // "access%54oken" is "accessToken" once decoded, so encoding the name
must not get it past the mask.
+ assertEquals(redact(BASE + "?access%54oken=" + SECRET), BASE +
"?accessToken=*****");
+ }
+
+ @Test
+ public void testKeepsNonSensitiveValuesAsSent() {
+ String query = "?segmentName=seg%200&segmentName=seg_1&filter=a%3Db";
+
+ assertEquals(redact(BASE + query), BASE + query);
+ }
+
+ @Test
+ public void testRepeatedSensitiveParameterCollapsesToOneMask() {
+ // Hides how many values were sent, too.
+ assertEquals(redact(BASE + "?token=" + SECRET + "&token=" + SECRET), BASE
+ "?token=*****");
+ }
+
+ @Test
+ public void testParameterWithoutValue() {
+ assertEquals(redact(BASE + "?verbose&password=" + SECRET), BASE +
"?verbose=&password=*****");
+ }
+
+ @Test
+ public void testUriWithoutQueryIsUnchanged() {
+ assertEquals(redact(BASE), BASE);
+ assertEquals(redact(BASE + "?"), BASE + "?");
+ }
+
+ private static String redact(String requestUri) {
+ return
ControllerResponseFilter.redactQueryParameters(URI.create(requestUri));
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]