jerryshao commented on code in PR #12891:
URL: https://github.com/apache/gravitino/pull/12891#discussion_r3932090729
##########
core/src/main/java/org/apache/gravitino/audit/AuditLogRedactor.java:
##########
@@ -61,10 +105,18 @@ public static Map<String, String>
redactCustomInfo(Map<String, String> customInf
* @return the redacted value for sensitive keys, otherwise the original
value
*/
public static String redactValue(String key, String value) {
- return isSensitiveCustomInfoKey(key) ? REDACTED_VALUE : value;
+ return isSensitiveKey(key) ? REDACTED_VALUE : value;
}
- private static boolean isSensitiveCustomInfoKey(String key) {
- return key != null &&
MASKED_CUSTOM_INFO_KEYS.contains(key.toLowerCase(Locale.ROOT));
+ private static boolean isSensitiveKey(String key) {
+ if (key == null) {
+ return false;
+ }
+ String lowerCaseKey = key.toLowerCase(Locale.ROOT);
+ if (NEVER_SENSITIVE_KEYS.contains(lowerCaseKey)) {
+ return false;
+ }
+ return MASKED_CUSTOM_INFO_KEYS.contains(lowerCaseKey)
+ || SENSITIVE_KEY_SUBSTRINGS.stream().anyMatch(lowerCaseKey::contains);
Review Comment:
Fixed — added a normalization step (`replaceAll("[^a-z0-9]", "")`) before
the substring check, so `api-key`, `x-api-key`, `access-key-id`, `private_key`,
etc. are now caught. The exact-match/exemption checks still run against the raw
(only case-folded) key first, so `auth.method` is unaffected. See
`AuditLogRedactor.isSensitiveKey()` in commit 4afae98.
##########
core/src/test/java/org/apache/gravitino/audit/TestAuditLogRedactor.java:
##########
@@ -0,0 +1,131 @@
+/*
+ * 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.gravitino.audit;
+
+import java.util.Collections;
+import java.util.Map;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TestAuditLogRedactor {
+
+ @Test
+ public void testNullCustomInfoReturnsEmptyMap() {
+ Assertions.assertTrue(AuditLogRedactor.redactCustomInfo(null).isEmpty());
+ }
+
+ @Test
+ public void testNonSensitiveKeyKeptAsIs() {
+ Map<String, String> info = Collections.singletonMap("details", "true");
+ Assertions.assertEquals("true",
AuditLogRedactor.redactCustomInfo(info).get("details"));
+ }
+
+ // ─── exact-match (MASKED_CUSTOM_INFO_KEYS)
───────────────────────────────────
+
+ @Test
+ public void testExactMatchKeyIsRedacted() {
+ Map<String, String> info = Collections.singletonMap("authorization",
"Bearer secret-token");
+ Assertions.assertEquals(
+ AuditLogRedactor.REDACTED_VALUE,
+ AuditLogRedactor.redactCustomInfo(info).get("authorization"));
+ }
+
+ @Test
+ public void testExactMatchIsCaseInsensitive() {
+ Map<String, String> info = Collections.singletonMap("AUTHORIZATION",
"Bearer secret-token");
+ Assertions.assertEquals(
+ AuditLogRedactor.REDACTED_VALUE,
+ AuditLogRedactor.redactCustomInfo(info).get("AUTHORIZATION"));
+ }
+
+ /**
+ * "cookie" is a known internal key with no sensitive substring in its name
(it doesn't contain
+ * "password", "token", etc.), so only the exact-match list catches it.
+ */
+ @Test
+ public void testExactMatchCatchesKeyWithNoSensitiveSubstring() {
+ Map<String, String> info = Collections.singletonMap("cookie",
"session=abc123");
+ Assertions.assertEquals(
+ AuditLogRedactor.REDACTED_VALUE,
AuditLogRedactor.redactCustomInfo(info).get("cookie"));
+ }
+
+ // ─── substring match (SENSITIVE_KEY_SUBSTRINGS)
──────────────────────────────
Review Comment:
Added `testSubstringMatchIgnoresSeparators()` covering exactly the four
names you listed (`api-key`, `x-api-key`, `access-key-id`, `private_key`).
Commit 4afae98.
##########
core/src/main/java/org/apache/gravitino/listener/api/event/Event.java:
##########
@@ -51,4 +69,34 @@ protected Event(String user, NameIdentifier identifier) {
public String remoteAddress() {
return remoteAddress;
}
+
+ /**
+ * Returns the current request's (raw) query parameters merged with this
event's own facts (from
+ * {@link #ownCustomInfo()}); the subclass's own values win on a key
collision. This method is
+ * {@code final} — override {@link #ownCustomInfo()} to contribute
subclass-specific facts.
+ *
+ * @return the merged custom info, or just the automatically captured query
parameters if the
+ * subclass contributes nothing of its own.
+ */
+ @Override
+ public final Map<String, String> customInfo() {
Review Comment:
Went with your second option — documented the semantics rather than
reworking the design: added a paragraph to `Event`'s class Javadoc and a note
in `docs/gravitino-server-config.md`'s "Event Listeners" section stating
plainly that a custom `EventListenerPlugin` receives `customInfo()` unredacted,
and that a plugin forwarding it elsewhere is responsible for its own redaction.
Commit 4afae98.
##########
core/src/main/java/org/apache/gravitino/listener/api/event/Event.java:
##########
@@ -30,16 +33,31 @@
* <p>The client remote address is captured from {@link RequestContext} at
construction time on the
* servlet thread, so async listener threads can safely call {@link
#remoteAddress()} without
* accessing thread-local storage.
+ *
+ * <p>The current request's query parameters are captured the same way, raw
(not redacted — see
+ * {@code org.apache.gravitino.audit.AuditLogRedactor}), and merged into
{@link #customInfo()}
+ * automatically. This gives every event in the system — including ones with
no subclass-specific
+ * {@code customInfo} — automatic audit coverage of the parameters that
produced it, with no
+ * per-event-class wiring required.
+ *
+ * <p>{@link #customInfo()} itself is {@code final}: a subclass that wants to
contribute its own
+ * facts must override {@link #ownCustomInfo()} instead, never {@code
customInfo()} directly. This
+ * is deliberate — an earlier version of this class let subclasses override
{@code customInfo()}
+ * directly, which let several of them (accidentally) discard the
automatically captured query
+ * parameters instead of merging with them. Sealing the merge here makes that
class of bug
+ * impossible to reintroduce.
*/
@DeveloperApi
public abstract class Event extends BaseEvent {
Review Comment:
Agreed on both points. Softened the class doc's "impossible to reintroduce"
claim to scope it explicitly to the `Event` subtree (not
`BaseEvent`/`PreEvent`), and added a note about the source/binary compatibility
break for `@DeveloperApi` consumers. Also moved this under "Does this PR
introduce any user-facing change?" in the PR description, flagging it
explicitly against the `branch-1.3` label. Commit 4afae98.
##########
server-common/src/main/java/org/apache/gravitino/server/web/RequestContextFilter.java:
##########
@@ -54,7 +86,11 @@ public void doFilter(ServletRequest request, ServletResponse
response, FilterCha
throws IOException, ServletException {
try {
if (request instanceof HttpServletRequest) {
-
RequestContext.setRemoteAddress(resolveClientAddress((HttpServletRequest)
request));
+ HttpServletRequest httpRequest = (HttpServletRequest) request;
+ RequestContext.setRemoteAddress(resolveClientAddress(httpRequest));
+ if (eventBus.isPresent()) {
+
RequestContext.setRequestQueryParams(flattenParameterMap(httpRequest.getParameterMap()));
Review Comment:
Switched to parsing `getQueryString()` directly instead of
`getParameterMap()`, exactly per your suggestion — closer to what the class
already claims to capture and removes the form-body-consumption hazard
entirely. Commit 4afae98.
##########
server-common/src/main/java/org/apache/gravitino/server/web/RequestContextFilter.java:
##########
@@ -72,4 +108,20 @@ private String resolveClientAddress(HttpServletRequest
request) {
}
return request.getRemoteAddr();
}
+
+ /**
+ * Flattens a servlet-style parameter map to a single value per name
(joining multi-valued
+ * parameters with a comma), with no redaction — see the class doc for why
that happens later.
+ */
+ private static Map<String, String> flattenParameterMap(Map<String, String[]>
parameterMap) {
Review Comment:
Added `MAX_PARAMETERS` (50) and `MAX_VALUE_LENGTH` (256, truncated with a
marker) caps in the same rewrite. Once the parameter-name cap is hit, only
additional values for already-tracked names are accumulated — no new names are
added. Commit 4afae98.
##########
core/src/main/java/org/apache/gravitino/utils/RequestContext.java:
##########
@@ -87,16 +104,50 @@ public static void markOperationFailureFired() {
* @return {@code true} if the flag is set, {@code false} otherwise.
*/
public static boolean isOperationFailureFired() {
- return Boolean.TRUE.equals(OPERATION_FAILURE_FIRED.get());
+ return OPERATION_OUTCOME.get() == OperationOutcome.FAILURE;
}
/**
- * Clears the operation-failure flag for the current request thread. Must be
called in a {@code
- * finally} block by {@code HttpAuditFilter} at the end of each request to
prevent stale values
- * from leaking to the next request on the same Jetty thread.
+ * Clears the recorded operation outcome for the current request thread.
Must be called in a
+ * {@code finally} block by {@code HttpAuditFilter} at the end of each
request to prevent stale
+ * values from leaking to the next request on the same Jetty thread.
+ *
+ * <p>Named after the failure flag for symmetry with {@link
#resetOperationSuccessFired()}, but
+ * clears the single underlying outcome either way — see the class-level doc.
*/
public static void resetOperationFailureFired() {
- OPERATION_FAILURE_FIRED.remove();
+ OPERATION_OUTCOME.remove();
+ }
+
+ /**
+ * Marks that an operation-layer success {@code Event} has been dispatched
for the current
+ * request. Called by {@code EventBus.dispatchPostEvent()} for every success
event that is not
+ * itself an {@code HttpRequestEvent}.
+ */
+ public static void markOperationSuccessFired() {
+ OPERATION_OUTCOME.set(OperationOutcome.SUCCESS);
Review Comment:
Applied your one-line fix verbatim — `markOperationSuccessFired()` is now a
no-op if a failure was already recorded. Added
`testMarkOperationSuccessFiredDoesNotOverwriteFailure()` and updated the class
doc to state this as an enforced invariant rather than an unenforced comment.
Commit 4afae98.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]