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

lizhimins pushed a commit to branch rocketmq-studio
in repository https://gitbox.apache.org/repos/asf/rocketmq-dashboard.git


The following commit(s) were added to refs/heads/rocketmq-studio by this push:
     new 9ec3a5914 fix(auth): preserve monotonic session activity timestamps 
(#7230)
9ec3a5914 is described below

commit 9ec3a5914dd54f88e119284677b2d3d51f2201cb
Author: aias00 <[email protected]>
AuthorDate: Mon Oct 26 16:35:06 2026 +0800

    fix(auth): preserve monotonic session activity timestamps (#7230)
    
    * fix(auth): preserve monotonic session activity timestamps
    * test(auth): use explicit activity timestamps across timezones
---
 .../apache/rocketmq/studio/auth/AuthService.java   |   2 +
 .../auth/AuthSessionActivityIntegrationTest.java   | 109 +++++++++++++++++++++
 2 files changed, 111 insertions(+)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/auth/AuthService.java 
b/server/src/main/java/org/apache/rocketmq/studio/auth/AuthService.java
index 782608abd..3c0b18e62 100644
--- a/server/src/main/java/org/apache/rocketmq/studio/auth/AuthService.java
+++ b/server/src/main/java/org/apache/rocketmq/studio/auth/AuthService.java
@@ -445,6 +445,8 @@ public class AuthService {
                 || 
!session.getLastSeenAt().plus(LAST_SEEN_UPDATE_INTERVAL).isAfter(current)) {
             sessionMapper.update(null, new UpdateWrapper<RmqStudioSession>()
                     .eq("id", session.getId())
+                    // A delayed request must not overwrite activity a newer 
request already recorded.
+                    .and(activity -> 
activity.isNull("last_seen_at").or().lt("last_seen_at", current))
                     .set("last_seen_at", current));
         }
         return Optional.of(userInfo(user));
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/auth/AuthSessionActivityIntegrationTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/auth/AuthSessionActivityIntegrationTest.java
new file mode 100644
index 000000000..25bbc4b7b
--- /dev/null
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/auth/AuthSessionActivityIntegrationTest.java
@@ -0,0 +1,109 @@
+/*
+ * 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.rocketmq.studio.auth;
+
+import org.apache.rocketmq.studio.persistence.entity.RmqStudioSession;
+import org.apache.rocketmq.studio.persistence.entity.RmqStudioUser;
+import org.apache.rocketmq.studio.persistence.mapper.RmqStudioSessionMapper;
+import org.apache.rocketmq.studio.persistence.mapper.RmqStudioUserMapper;
+import org.apache.rocketmq.studio.settings.SettingsRepository;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.time.Clock;
+import java.time.Instant;
+import java.time.LocalDateTime;
+import java.time.ZoneOffset;
+import java.time.temporal.ChronoUnit;
+import java.util.HexFormat;
+import java.util.UUID;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.AdditionalAnswers.delegatesTo;
+
+@SpringBootTest
+@ActiveProfiles("dev")
+class AuthSessionActivityIntegrationTest {
+    @Autowired
+    private RmqStudioUserMapper userMapper;
+    @Autowired
+    private RmqStudioSessionMapper sessionMapper;
+    @Autowired
+    private SettingsRepository settingsRepository;
+    @Autowired
+    private PasswordHasher passwordHasher;
+
+    @ParameterizedTest
+    @ValueSource(ints = {300, 600})
+    void delayedRefreshCannotOverwriteNewerSessionActivityTest(int 
activityAgeSeconds) throws Exception {
+        Instant earlier = Instant.now().truncatedTo(ChronoUnit.SECONDS);
+        Instant later = earlier.plusSeconds(60);
+        RmqStudioUser user = new RmqStudioUser();
+        user.setUsername("activity-" + UUID.randomUUID());
+        user.setPasswordHash("unused-by-token-authentication");
+        user.setEnabled(true);
+        user.setAdmin(false);
+        userMapper.insert(user);
+        RmqStudioSession session = new RmqStudioSession();
+        String token = UUID.randomUUID().toString();
+        session.setUserId(user.getId());
+        
session.setTokenHash(HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256")
+                .digest(token.getBytes(StandardCharsets.UTF_8))));
+        session.setExpiresAt(LocalDateTime.ofInstant(later.plusSeconds(3600), 
ZoneOffset.UTC));
+        LocalDateTime initialActivity = 
LocalDateTime.ofInstant(earlier.minusSeconds(activityAgeSeconds),
+                ZoneOffset.UTC);
+        session.setLastSeenAt(initialActivity);
+        sessionMapper.insert(session);
+        try {
+            
assertThat(sessionMapper.selectById(session.getId()).getLastSeenAt()).isEqualTo(initialActivity);
+            RmqStudioSessionMapper delayedMapper = 
mock(RmqStudioSessionMapper.class, delegatesTo(sessionMapper));
+            AuthService olderRequest = serviceAtTest(earlier, delayedMapper);
+            AuthService newerRequest = serviceAtTest(later, sessionMapper);
+            doAnswer(invocation -> {
+                // Pause the older refresh just before SQL execution and let a 
newer
+                // request read the same session and commit its refresh first.
+                assertThat(newerRequest.getAuthenticatedUser("Bearer " + 
token)).isPresent();
+                return sessionMapper.update(invocation.getArgument(0), 
invocation.getArgument(1));
+            }).when(delayedMapper).update(isNull(), any());
+
+            assertThat(olderRequest.getAuthenticatedUser("Bearer " + 
token)).isPresent();
+
+            
assertThat(sessionMapper.selectById(session.getId()).getLastSeenAt())
+                    .isEqualTo(LocalDateTime.ofInstant(later, ZoneOffset.UTC));
+            
assertThat(newerRequest.listActiveSessionSummaries(java.util.List.of(user.getId()))
+                    .get(user.getId()).getLastSessionSeenAt())
+                    .isEqualTo(LocalDateTime.ofInstant(later, ZoneOffset.UTC));
+        } finally {
+            sessionMapper.deleteById(session.getId());
+            userMapper.deleteById(user.getId());
+        }
+    }
+
+    private AuthService serviceAtTest(Instant instant, RmqStudioSessionMapper 
sessions) {
+        return new AuthService(new AuthProperties(), settingsRepository, 
Clock.fixed(instant, ZoneOffset.UTC),
+                userMapper, sessions, passwordHasher);
+    }
+}

Reply via email to