This is an automated email from the ASF dual-hosted git repository.

yiguolei pushed a commit to branch branch-4.1
in repository https://gitbox.apache.org/repos/asf/doris.git


The following commit(s) were added to refs/heads/branch-4.1 by this push:
     new c85a5b607f5 branch-4.1: [fix](arrow-flight) Stop writing bearer tokens 
to fe.log (#67146)
c85a5b607f5 is described below

commit c85a5b607f50e5e41d8745e2a267787887a7b8de
Author: Calvin Kirs <[email protected]>
AuthorDate: Fri Aug 28 01:22:42 2026 +0800

    branch-4.1: [fix](arrow-flight) Stop writing bearer tokens to fe.log 
(#67146)
    
    #66572
---
 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 86f303d84c8..5cf7f608361 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 43235d9ac51..3dad10564d1 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
@@ -92,6 +92,7 @@ import org.apache.doris.common.util.PrintableMap;
 import org.apache.doris.common.util.PropertyAnalyzer;
 import org.apache.doris.common.util.SmallFileMgr;
 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.consistency.ConsistencyChecker;
 import org.apache.doris.cooldown.CooldownConfHandler;
@@ -1434,7 +1435,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 983d5301556..3e9cea55cdf 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 08c0e8243e4..ce3faecc1ca 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
@@ -1746,8 +1746,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]

Reply via email to