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]

Reply via email to