This is an automated email from the ASF dual-hosted git repository.
CalvinKirs pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/doris.git
The following commit(s) were added to refs/heads/master by this push:
new fccaa75d465 [fix](arrow-flight) Stop writing bearer tokens to fe.log
(#66572)
fccaa75d465 is described below
commit fccaa75d465da2af1615e98d8751f82591c304d2
Author: Calvin Kirs <[email protected]>
AuthorDate: Tue Aug 11 10:10:04 2026 +0800
[fix](arrow-flight) Stop writing bearer tokens to fe.log (#66572)
### What problem does this PR solve?
Issue Number: close #xxx
Related PR: #xxx
Problem Summary:
Arrow Flight SQL bearer tokens are written to `fe.log` in cleartext.
`FlightTokenManagerImpl` logs the token verbatim at INFO when it is
minted, evicted from either cache, and invalidated, and it also puts the
token into the `IllegalArgumentException` messages that
`FlightBearerTokenAuthenticator.validateBearer` logs at ERROR:
```java
LOG.info("Created flight token for user: {}, token: {}", username, token);
```
A bearer token is a complete credential until it expires —
`arrow_flight_token_alive_time_second` defaults to 86400s. So anyone who
can read `fe.log`, or the log aggregation platform it is shipped to, or
a backup of either, can take a live token, send it as `Authorization:
Bearer <token>` to the Arrow Flight SQL port (`arrow_flight_sql_port`,
default 8070), and run queries as that user without ever knowing their
password. Logs routinely reach a much wider audience than the credential
store does, which is what makes this worth fixing even though the log
file itself is not world readable.
**What this PR does**
Adds `org.apache.doris.common.util.TokenMasker`, which offers the two
renderings a secret can reasonably have in a message:
- `tokenId(t)` → `sha256:1a2b3c4d`, a truncated SHA-256. It is stable,
so a log line and the error message returned to the client still point
at the same token and can be matched up, but no part of the secret
survives in it. This is what the flight token paths now use. The
existing "search for this token in fe.log to see the evict reason" hint
therefore still works — it now says *token id*, and the id appears both
in the client's error and in the log.
- `maskPrefix(t)` → `abc***`, revealing only a short leading prefix, for
the case where a human has to recognize *which* configured secret was
involved (token rotation). This is the helper that already existed
privately in `MetaService`; it is moved into the utility and reused
rather than duplicated.
Every token-valued site in the Arrow Flight path is converted: the four
`LOG.info` calls in `FlightTokenManagerImpl`, the four
`IllegalArgumentException` messages in
`validateToken`/`getTokenDetails`, the one in
`FlightSessionsWithTokenManager.createConnectContext`, and the teardown
warning in `FlightSqlConnectPoolMgr.unregisterConnection`. That last one
is worth spelling out: a Flight SQL `ConnectContext`'s **`peerIdentity`
is the bearer token itself** —
`FlightBearerTokenAuthenticator.createAuthResultWithBearerToken` returns
the token as the peer identity, and `FlightSqlConnectPoolMgr` keys its
`flightToken2ConnectionId` map by it — so `ctx.getPeerIdentity()` in a
log line leaks a live token under a name that does not look like one.
Two more credentials with the same problem, found while auditing for
other instances:
- `Env` logs the cluster token adopted from a helper node at INFO (`get
token from helper node. token={}`). That token authenticates metadata
access between FE nodes, so it gets `maskPrefix`, consistent with how
`MetaService` already renders the same token.
- `Auth` echoes `initial_root_password` into a WARN — and it does so
from the branch that runs when the configured value failed 2-staged
SHA-1 validation, which is exactly the case where an operator put a
plaintext password in the config. The value is simply dropped from the
message; it adds nothing to the diagnosis that the config key name does
not already give.
Finally, a checkstyle rule rejects a value whose name says it holds a
token/password/secret/peer identity being passed straight into a
`LOG.x(...)` call, as a parameter or concatenated into the message. It
matches across lines, because the credential argument frequently sits on
a continuation line — that is true of the `FlightSqlConnectPoolMgr` case
above, which a line-based rule silently misses.
It is a backstop, not a substitute for review, and the honest limitation
is that it only knows the naming convention: `peerIdentity` had to be
taught to it by hand once it turned out to be a token, and any other
alias would be equally invisible. It reports **no violation anywhere in
`fe/`** after this PR, so it lands without a single suppression.
### Release note
Arrow Flight SQL bearer tokens are no longer written to `fe.log`. Log
lines and error messages now carry a non-reversible token id (`sha256:`
prefix) instead of the token itself.
### Check List (For Author)
- Test <!-- At least one of them must be included. -->
- [ ] Regression test
- [x] Unit Test
- [ ] Manual test (add detailed scripts or steps below)
- [ ] No need to test or manual test. Explain why:
- [ ] This is a refactor/code format and no logic has been changed.
- [ ] Previous test can cover this change.
- [ ] No code files have been changed.
- [ ] Other reason <!-- Add your reason? -->
`TokenMaskerTest` covers that the token id is a digest and cannot
contain any part of the token, that it is stable for the same token and
differs across tokens, the empty/null handling, and `maskPrefix`
including its too-short-to-reveal branch.
The checkstyle rule was verified to actually fire, not just to be quiet:
re-adding the original `LOG.info(..., username, token)` line, and
separately un-masking the multi-line `FlightSqlConnectPoolMgr` call,
each fail the build at that line with the new message; restoring them
goes back to green.
- Behavior changed:
- [ ] No.
- [x] Yes. <!-- Explain the behavior change -->
The text of some Arrow Flight error messages changes: where they used to
echo the bearer token, they now carry `token id: sha256:...`. Anything
that parsed the token out of an error message or out of `fe.log` would
need to use the id instead. No API, wire format or configuration
changes.
- Does this need documentation?
- [x] No.
- [ ] Yes. <!-- Add document PR link here. eg:
https://github.com/apache/doris-website/pull/1214 -->
---
fe/check/checkstyle/checkstyle.xml | 19 ++++++
.../main/java/org/apache/doris/catalog/Env.java | 5 +-
.../org/apache/doris/common/util/TokenMasker.java | 79 ++++++++++++++++++++++
.../org/apache/doris/httpv2/meta/MetaService.java | 24 +------
.../org/apache/doris/mysql/privilege/Auth.java | 5 +-
.../sessions/FlightSessionsWithTokenManager.java | 6 +-
.../sessions/FlightSqlConnectPoolMgr.java | 5 +-
.../arrowflight/tokens/FlightTokenManagerImpl.java | 40 ++++++-----
.../apache/doris/common/util/TokenMaskerTest.java | 63 +++++++++++++++++
9 files changed, 202 insertions(+), 44 deletions(-)
diff --git a/fe/check/checkstyle/checkstyle.xml
b/fe/check/checkstyle/checkstyle.xml
index 2b87546548b..c97fbeed110 100644
--- a/fe/check/checkstyle/checkstyle.xml
+++ b/fe/check/checkstyle/checkstyle.xml
@@ -66,6 +66,25 @@ under the License.
<property name="message" value="Trailing whitespace found."/>
<property name="fileExtensions" value=".java"/>
</module>
+ <!--
+ Best effort guard against writing credentials to the log: catches a
value whose name says it
+ holds a token/password/secret/peer identity being passed straight to a
log call, either as a
+ parameter or concatenated into the message. Logs are routinely shipped
off the host, so a
+ credential in a log line has a far wider audience than the credential
store itself.
+
+ Matching is multiline because the argument often sits on a continuation
line, and [^;] bounds
+ each match to a single statement. It only recognizes the naming
convention, so it is a
+ backstop and not a substitute for review: an alias that does not carry
the noun (peerIdentity
+ needed to be added by hand once it turned out to be the Arrow Flight
bearer token) is invisible
+ to it.
+ -->
+ <module name="RegexpMultiline">
+ <property name="format"
+
value="LOG\.(?:info|warn|error|debug|trace)\([^;]*[,+]\s*(?![Mm]asked)(?:[A-Za-z0-9_]+\.)*(?:get)?[A-Za-z0-9_]*(?:[Tt]oken|[Pp]assword|[Ss]ecret|[Pp]eerIdentity)(?:Value|Str|String|Text)?(?:\(\))?\s*[,)+]"/>
+ <property name="message"
+ value="Do not log credentials. Mask the value with
org.apache.doris.common.util.TokenMasker (or log only a non-reversible id); if
the value is not a secret, name it so."/>
+ <property name="fileExtensions" value=".java"/>
+ </module>
<module name="TreeWalker">
<!-- filter -->
diff --git a/fe/fe-core/src/main/java/org/apache/doris/catalog/Env.java
b/fe/fe-core/src/main/java/org/apache/doris/catalog/Env.java
index 2d0ccd486bc..50c1bfc2171 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/catalog/Env.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/catalog/Env.java
@@ -91,6 +91,7 @@ import org.apache.doris.common.util.PropertyAnalyzer;
import org.apache.doris.common.util.SmallFileMgr;
import org.apache.doris.common.util.SqlUtils;
import org.apache.doris.common.util.TimeUtils;
+import org.apache.doris.common.util.TokenMasker;
import org.apache.doris.common.util.Util;
import org.apache.doris.connector.ConnectorFactory;
import org.apache.doris.connector.ConnectorPluginManager;
@@ -1491,7 +1492,9 @@ public class Env {
}
String remoteToken =
conn.getHeaderField(MetaBaseAction.TOKEN);
if (token == null && remoteToken != null) {
- LOG.info("get token from helper node. token={}.",
remoteToken);
+ // Masked: the cluster token authenticates meta
access, so it must not
+ // reach fe.log. The prefix is enough to tell which
token was adopted.
+ LOG.info("get token from helper node. token={}.",
TokenMasker.maskPrefix(remoteToken));
token = remoteToken;
storage.writeClusterIdAndToken();
storage.reload();
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/common/util/TokenMasker.java
b/fe/fe-core/src/main/java/org/apache/doris/common/util/TokenMasker.java
new file mode 100644
index 00000000000..cd6f47503c1
--- /dev/null
+++ b/fe/fe-core/src/main/java/org/apache/doris/common/util/TokenMasker.java
@@ -0,0 +1,79 @@
+// 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.doris.common.util;
+
+import com.google.common.base.Strings;
+import com.google.common.hash.Hashing;
+
+import java.nio.charset.StandardCharsets;
+
+/**
+ * Helpers to render a secret (bearer token, auth token, ...) in log lines and
error messages
+ * without writing the secret itself, since logs are routinely shipped to
places that are far
+ * less protected than the credential store.
+ *
+ * <p>Two different renderings are offered, pick by what the reader of the
message needs:
+ * {@link #tokenId} when the reader only needs to correlate messages about the
same secret, and
+ * {@link #maskPrefix} when a human needs to recognize <i>which</i> configured
secret was used.
+ */
+public class TokenMasker {
+ public static final String EMPTY_TOKEN = "<empty>";
+
+ // A truncated digest: long enough that two live tokens are very unlikely
to collide,
+ // short enough that it is useless as a credential.
+ private static final int TOKEN_ID_LEN = 8;
+ private static final String TOKEN_ID_PREFIX = "sha256:";
+
+ // Minimum token length required before we reveal a masked prefix. Shorter
tokens would
+ // leak too large a fraction of the secret, so they are hidden entirely
with only a length hint.
+ private static final int MIN_TOKEN_LEN_FOR_PREFIX = 8;
+ private static final int TOKEN_PREFIX_LEN = 3;
+
+ private TokenMasker() {
+ }
+
+ /**
+ * Returns a stable, non-reversible handle for a token, e.g. {@code
sha256:1a2b3c4d}. The same
+ * token always renders to the same handle, so log lines and the error
message handed back to
+ * the client can be matched up, while no part of the secret is disclosed.
+ */
+ public static String tokenId(String token) {
+ if (Strings.isNullOrEmpty(token)) {
+ return EMPTY_TOKEN;
+ }
+ return TOKEN_ID_PREFIX + Hashing.sha256().hashString(token,
StandardCharsets.UTF_8).toString()
+ .substring(0, TOKEN_ID_LEN);
+ }
+
+ /**
+ * Masks a token by revealing only a short leading prefix (e.g. {@code
abc***}), so that a
+ * token mismatch is diagnosable during rotation, while never logging the
full secret. Empty
+ * tokens and tokens too short to safely show a prefix are hidden. Prefer
{@link #tokenId}
+ * unless the reader really needs to recognize the secret by sight.
+ */
+ public static String maskPrefix(String token) {
+ if (Strings.isNullOrEmpty(token)) {
+ return EMPTY_TOKEN;
+ }
+ if (token.length() < MIN_TOKEN_LEN_FOR_PREFIX) {
+ // Too short to reveal any prefix without leaking a large fraction
of the secret.
+ return "<hidden, token length " + token.length() + " < " +
MIN_TOKEN_LEN_FOR_PREFIX + ">";
+ }
+ return token.substring(0, TOKEN_PREFIX_LEN) + "***";
+ }
+}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/httpv2/meta/MetaService.java
b/fe/fe-core/src/main/java/org/apache/doris/httpv2/meta/MetaService.java
index 4a8acdce33d..fe88e854d35 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/httpv2/meta/MetaService.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/httpv2/meta/MetaService.java
@@ -22,6 +22,7 @@ import org.apache.doris.common.Config;
import org.apache.doris.common.DdlException;
import org.apache.doris.common.util.HttpURLUtil;
import org.apache.doris.common.util.NetUtils;
+import org.apache.doris.common.util.TokenMasker;
import org.apache.doris.ha.FrontendNodeType;
import
org.apache.doris.httpv2.controller.BaseController.ActionAuthorizationInfo;
import org.apache.doris.httpv2.entity.ResponseEntityBuilder;
@@ -97,7 +98,7 @@ public class MetaService extends RestBaseController {
LOG.warn("reject meta request with invalid token. client: {},
{}, request from: {}, "
+ "expected: {}, actual: {}",
clientHost, clientPort, request.getRemoteAddr(),
- maskToken(clusterToken), maskToken(requestToken));
+ TokenMasker.maskPrefix(clusterToken),
TokenMasker.maskPrefix(requestToken));
throw unauthorized(clientHost, clientPort, request);
}
}
@@ -108,27 +109,6 @@ public class MetaService extends RestBaseController {
+ ", request from " + request.getRemoteAddr());
}
- // Minimum token length required before we reveal a masked prefix in logs.
Shorter tokens would
- // leak too large a fraction of the secret, so they are hidden entirely
with only a length hint.
- private static final int MIN_TOKEN_LEN_FOR_PREFIX = 8;
- private static final int TOKEN_PREFIX_LEN = 3;
-
- /**
- * Masks a token for logging: reveals only a short leading prefix (e.g.
"abc***") so that a
- * token mismatch is diagnosable during rotation, while never logging the
full secret. Empty
- * tokens and tokens too short to safely show a prefix are hidden.
- */
- private static String maskToken(String token) {
- if (Strings.isNullOrEmpty(token)) {
- return "<empty>";
- }
- if (token.length() < MIN_TOKEN_LEN_FOR_PREFIX) {
- // Too short to reveal any prefix without leaking a large fraction
of the secret.
- return "<hidden, token length " + token.length() + " < " +
MIN_TOKEN_LEN_FOR_PREFIX + ">";
- }
- return token.substring(0, TOKEN_PREFIX_LEN) + "***";
- }
-
@RequestMapping(path = "/image", method = RequestMethod.GET)
public Object image(HttpServletRequest request, HttpServletResponse
response) {
checkFromValidFe(request);
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/Auth.java
b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/Auth.java
index 3d03be862fd..75fcced4f2a 100644
--- a/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/Auth.java
+++ b/fe/fe-core/src/main/java/org/apache/doris/mysql/privilege/Auth.java
@@ -1745,8 +1745,9 @@ public class Auth implements Writable {
scramble = MysqlPassword.checkPassword(initialRootPassword);
} catch (AnalysisException e) {
// Skip set root password if `initial_root_password` is not valid
2-staged SHA-1 encrypted
- LOG.warn("initial_root_password [{}] is not valid 2-staged SHA-1
encrypted, ignore it",
- initialRootPassword);
+ // Do not echo the configured value: this branch is reached
precisely when it is not a
+ // 2-staged SHA-1 hash, which usually means a plaintext password
was configured.
+ LOG.warn("initial_root_password is not valid 2-staged SHA-1
encrypted, ignore it");
return;
}
UserIdentity rootUser = new UserIdentity(ROOT_USER, "%");
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/service/arrowflight/sessions/FlightSessionsWithTokenManager.java
b/fe/fe-core/src/main/java/org/apache/doris/service/arrowflight/sessions/FlightSessionsWithTokenManager.java
index 8001998a66e..02e1389111d 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/service/arrowflight/sessions/FlightSessionsWithTokenManager.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/service/arrowflight/sessions/FlightSessionsWithTokenManager.java
@@ -18,6 +18,7 @@
package org.apache.doris.service.arrowflight.sessions;
import org.apache.doris.common.ErrorCode;
+import org.apache.doris.common.util.TokenMasker;
import org.apache.doris.common.util.Util;
import org.apache.doris.qe.ConnectContext;
import org.apache.doris.qe.ConnectScheduler;
@@ -59,8 +60,9 @@ public class FlightSessionsWithTokenManager implements
FlightSessionsManager {
final FlightTokenDetails flightTokenDetails =
flightTokenManager.validateToken(peerIdentity);
if (flightTokenDetails.getCreatedSession()) {
flightTokenManager.invalidateToken(peerIdentity);
- throw new IllegalArgumentException("UserSession expire after
access, try reconnect, bearer token: "
- + peerIdentity + ", a peerIdentity(bearer token) can only
create a ConnectContext once. "
+ throw new IllegalArgumentException("UserSession expire after
access, try reconnect, bearer token id: "
+ + TokenMasker.tokenId(peerIdentity)
+ + ", a peerIdentity(bearer token) can only create a
ConnectContext once. "
+ "if ConnectContext is deleted without operation for a
long time, it needs to be reconnected "
+ "(at the same time obtain a new bearer token).");
}
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/service/arrowflight/sessions/FlightSqlConnectPoolMgr.java
b/fe/fe-core/src/main/java/org/apache/doris/service/arrowflight/sessions/FlightSqlConnectPoolMgr.java
index 04982a7fedc..c8854507e00 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/service/arrowflight/sessions/FlightSqlConnectPoolMgr.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/service/arrowflight/sessions/FlightSqlConnectPoolMgr.java
@@ -17,6 +17,7 @@
package org.apache.doris.service.arrowflight.sessions;
+import org.apache.doris.common.util.TokenMasker;
import org.apache.doris.qe.ConnectContext;
import org.apache.doris.qe.ConnectContext.ConnectType;
import org.apache.doris.qe.ConnectPoolMgr;
@@ -67,8 +68,10 @@ public class FlightSqlConnectPoolMgr extends ConnectPoolMgr {
// RootAllocator.close() marks the allocator closed before it
reports outstanding
// bytes. The error is actionable, but session teardown must
still release the
// coordinator, transaction and pool/token bookkeeping below.
+ // For an Arrow Flight SQL connection the peer identity IS the
bearer token, so it is
+ // logged as a masked id, the same one FlightTokenManagerImpl
uses.
LOG.warn("failed to close Flight SQL channel while
unregistering connection {}, peer identity {}",
- ctx.getConnectionId(), ctx.getPeerIdentity(), t);
+ ctx.getConnectionId(),
TokenMasker.tokenId(ctx.getPeerIdentity()), t);
}
}
// Finalize any Arrow Flight query whose coordinator was kept alive
across the
diff --git
a/fe/fe-core/src/main/java/org/apache/doris/service/arrowflight/tokens/FlightTokenManagerImpl.java
b/fe/fe-core/src/main/java/org/apache/doris/service/arrowflight/tokens/FlightTokenManagerImpl.java
index 85d1a0bce35..a3c76da9f05 100644
---
a/fe/fe-core/src/main/java/org/apache/doris/service/arrowflight/tokens/FlightTokenManagerImpl.java
+++
b/fe/fe-core/src/main/java/org/apache/doris/service/arrowflight/tokens/FlightTokenManagerImpl.java
@@ -21,6 +21,7 @@ package org.apache.doris.service.arrowflight.tokens;
import org.apache.doris.catalog.Env;
import org.apache.doris.common.CustomThreadFactory;
+import org.apache.doris.common.util.TokenMasker;
import org.apache.doris.qe.ConnectContext;
import org.apache.doris.service.ExecuteEnv;
import org.apache.doris.service.arrowflight.auth2.FlightAuthResult;
@@ -79,12 +80,13 @@ public class FlightTokenManagerImpl implements
FlightTokenManager {
if (context != null) {
ExecuteEnv.getInstance().getScheduler().getFlightSqlConnectPoolMgr()
.unregisterConnection(context);
- LOG.info("evict bearer token: " + token + " from
tokenCache, reason: "
- + notification.getCause()
+ LOG.info("evict bearer token: " +
TokenMasker.tokenId(token) + " from tokenCache, "
+ + "reason: " + notification.getCause()
+ ", and unregister flight connection
context after evict bearer token");
} else {
- LOG.info("evict bearer token: " + token + " from
tokenCache, reason: "
- + notification.getCause() + ", and flight
connection context not exist");
+ LOG.info("evict bearer token: " +
TokenMasker.tokenId(token) + " from tokenCache, "
+ + "reason: " + notification.getCause()
+ + ", and flight connection context not
exist");
}
usersTokenLRU.get(tokenDetails.getUsername()).invalidate(token);
}
@@ -128,7 +130,7 @@ public class FlightTokenManagerImpl implements
FlightTokenManager {
public void onRemoval(@NotNull
RemovalNotification<String, Integer> notification) {
// TODO: broadcast this message to other FE
assert notification.getKey() != null;
- LOG.info("evict bearer token: " +
notification.getKey()
+ LOG.info("evict bearer token: " +
TokenMasker.tokenId(notification.getKey())
+ " from usersTokenLRU, reason: "
+ notification.getCause());
tokenCache.invalidate(notification.getKey());
}
@@ -141,7 +143,9 @@ public class FlightTokenManagerImpl implements
FlightTokenManager {
}));
}
usersTokenLRU.get(username).put(token, 1);
- LOG.info("Created flight token for user: {}, token: {}", username,
token);
+ // Never log the token itself: fe.log is routinely shipped off the FE
host, and the token is
+ // accepted as a full credential until it expires. The id is enough to
trace its lifecycle.
+ LOG.info("Created flight token for user: {}, token id: {}", username,
TokenMasker.tokenId(token));
return flightTokenDetails;
}
@@ -149,25 +153,28 @@ public class FlightTokenManagerImpl implements
FlightTokenManager {
public FlightTokenDetails validateToken(final String token) throws
IllegalArgumentException {
final FlightTokenDetails value = getTokenDetails(token);
if (value.getToken().equals("")) {
- throw new IllegalArgumentException("invalid bearer token: " + token
+ throw new IllegalArgumentException("invalid bearer token, token
id: " + TokenMasker.tokenId(token)
+ ", try reconnect, bearer token may not be created, or
may have been evict, search for this "
- + "token in fe.log to see the evict reason. currently in
fe.conf, `arrow_flight_max_connections`="
- + this.cacheSize + ",
`arrow_flight_token_alive_time_second`=" + this.cacheExpiration);
+ + "token id in fe.log to see the evict reason. currently
in fe.conf, "
+ + "`arrow_flight_max_connections`=" + this.cacheSize
+ + ", `arrow_flight_token_alive_time_second`=" +
this.cacheExpiration);
}
if (System.currentTimeMillis() >= value.getExpiresAt()) {
tokenCache.invalidate(token);
- throw new IllegalArgumentException("bearer token expired: " +
token + ", try reconnect, "
- + "currently in fe.conf,
`arrow_flight_token_alive_time_second`=" + this.cacheExpiration);
+ throw new IllegalArgumentException("bearer token expired, token
id: " + TokenMasker.tokenId(token)
+ + ", try reconnect, currently in fe.conf,
`arrow_flight_token_alive_time_second`="
+ + this.cacheExpiration);
}
if (usersTokenLRU.containsKey(value.getUsername())) {
try {
usersTokenLRU.get(value.getUsername()).get(token);
} catch (ExecutionException ignored) {
- throw new IllegalArgumentException("usersTokenLRU not exist
bearer token: " + token);
+ throw new IllegalArgumentException(
+ "usersTokenLRU not exist bearer token, token id: " +
TokenMasker.tokenId(token));
}
} else {
- throw new IllegalArgumentException(
- "bearer token not created: " + token + ", username: " +
value.getUsername());
+ throw new IllegalArgumentException("bearer token not created,
token id: " + TokenMasker.tokenId(token)
+ + ", username: " + value.getUsername());
}
LOG.info("Validated bearer token for user: {}", value.getUsername());
return value;
@@ -175,7 +182,7 @@ public class FlightTokenManagerImpl implements
FlightTokenManager {
@Override
public void invalidateToken(final String token) {
- LOG.info("Invalidate bearer token, {}", token);
+ LOG.info("Invalidate bearer token, token id: {}",
TokenMasker.tokenId(token));
tokenCache.invalidate(token);
}
@@ -185,7 +192,8 @@ public class FlightTokenManagerImpl implements
FlightTokenManager {
try {
value = tokenCache.getUnchecked(token);
} catch (CacheLoader.InvalidCacheLoadException ignored) {
- throw new IllegalArgumentException("InvalidCacheLoadException,
invalid bearer token: " + token);
+ throw new IllegalArgumentException(
+ "InvalidCacheLoadException, invalid bearer token, token
id: " + TokenMasker.tokenId(token));
}
return value;
diff --git
a/fe/fe-core/src/test/java/org/apache/doris/common/util/TokenMaskerTest.java
b/fe/fe-core/src/test/java/org/apache/doris/common/util/TokenMaskerTest.java
new file mode 100644
index 00000000000..fbd4e924ea9
--- /dev/null
+++ b/fe/fe-core/src/test/java/org/apache/doris/common/util/TokenMaskerTest.java
@@ -0,0 +1,63 @@
+// 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.doris.common.util;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+public class TokenMaskerTest {
+
+ // Same length and alphabet as the tokens FlightTokenManagerImpl mints
(130 random bits in
+ // base 32), but deliberately low entropy and readable, so that no secret
scanner has to
+ // decide whether a random-looking 26 character literal in the tree is a
real credential.
+ private static final String TOKEN = "notarealtokennotarealtoken";
+
+ @Test
+ public void testTokenIdHidesTheSecret() {
+ String id = TokenMasker.tokenId(TOKEN);
+ Assertions.assertFalse(id.contains(TOKEN));
+ // The id is a truncated digest and nothing else, so no part of the
token can survive in it.
+ Assertions.assertTrue(id.matches("sha256:[0-9a-f]{8}"), "unexpected
token id: " + id);
+ }
+
+ @Test
+ public void testTokenIdIsStableAndDistinguishing() {
+ // Same token always renders identically, so a log line and the error
message returned to
+ // the client can be matched up.
+ Assertions.assertEquals(TokenMasker.tokenId(TOKEN),
TokenMasker.tokenId(TOKEN));
+ Assertions.assertNotEquals(TokenMasker.tokenId(TOKEN),
TokenMasker.tokenId(TOKEN + "x"));
+ Assertions.assertEquals("sha256:",
TokenMasker.tokenId(TOKEN).substring(0, 7));
+ Assertions.assertEquals(15, TokenMasker.tokenId(TOKEN).length());
+ }
+
+ @Test
+ public void testTokenIdOfEmptyToken() {
+ Assertions.assertEquals(TokenMasker.EMPTY_TOKEN,
TokenMasker.tokenId(null));
+ Assertions.assertEquals(TokenMasker.EMPTY_TOKEN,
TokenMasker.tokenId(""));
+ }
+
+ @Test
+ public void testMaskPrefix() {
+ Assertions.assertEquals("not***", TokenMasker.maskPrefix(TOKEN));
+ Assertions.assertEquals(TokenMasker.EMPTY_TOKEN,
TokenMasker.maskPrefix(null));
+ Assertions.assertEquals(TokenMasker.EMPTY_TOKEN,
TokenMasker.maskPrefix(""));
+ // Too short to show a prefix: hidden entirely, only the length is
reported.
+ Assertions.assertEquals("<hidden, token length 7 < 8>",
TokenMasker.maskPrefix("1234567"));
+ Assertions.assertEquals("123***", TokenMasker.maskPrefix("12345678"));
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]