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 7b2773e43 fix(audit): record credential reveals on cloud credentials 
and ACL users (#5524)
7b2773e43 is described below

commit 7b2773e437905b705cf70e23b86de4d460f9a9a6
Author: Wang1rrr <[email protected]>
AuthorDate: Fri Oct 9 11:26:10 2026 +0800

    fix(audit): record credential reveals on cloud credentials and ACL users 
(#5524)
    
    Revealing a secret was the only operation on these resources that left no 
audit trace, even though
    `AclClusterConfigVO` documents that plaintext secrets come solely from the 
per-user credential
    endpoints. Both endpoints now record one audit event on the way out - 
`REVEAL_ACL_USER_CREDENTIALS`
    and `REVEAL_CLOUD_CREDENTIAL` - through a single exit point, so the Tencent 
path is covered too
    instead of returning before any audit. The detail names the resource and 
never carries the secret.
    Adds the two operations to the audit presentation table with zh/en labels.
---
 .../rocketmq/studio/instance/acl/AclService.java   | 22 +++++++++------
 .../credential/CloudCredentialService.java         |  7 ++++-
 .../studio/instance/acl/AclServiceTest.java        | 32 ++++++++++++++++++++++
 .../credential/CloudCredentialServiceTest.java     | 18 ++++++++++++
 web/src/i18n/translations.ts                       |  5 ++++
 .../pages/ops/__tests__/auditPresentation.test.ts  | 15 ++++++++++
 web/src/pages/ops/auditPresentation.ts             | 10 +++++++
 7 files changed, 100 insertions(+), 9 deletions(-)

diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclService.java 
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclService.java
index 5f7d4a0c4..385c44195 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/AclService.java
@@ -323,15 +323,21 @@ public class AclService {
      * The secret is stored base64-encoded in the database and decoded here.
      */
     public AclUserVO getUserCredentials(String id, String instanceId) {
+        AclUserVO user;
         if (isTencentInstance(instanceId)) {
-            return tencentAclService.getUserCredentials(instanceId, id);
-        }
-        if (!StringUtils.hasText(id)) {
-            throw new BusinessException(400, "ACL user id is required");
-        }
-        log.info("Revealing credentials for ACL user id={}", id);
-        return aclRepository.findUserById(EntityIds.parseId(id))
-                .orElseThrow(() -> new BusinessException(404, "ACL user not 
found: " + id));
+            user = tencentAclService.getUserCredentials(instanceId, id);
+        } else {
+            if (!StringUtils.hasText(id)) {
+                throw new BusinessException(400, "ACL user id is required");
+            }
+            log.info("Revealing credentials for ACL user id={}", id);
+            user = aclRepository.findUserById(EntityIds.parseId(id))
+                    .orElseThrow(() -> new BusinessException(404, "ACL user 
not found: " + id));
+        }
+        // Both vendor paths reveal a secret, so both are audited; the detail 
never carries it.
+        recordAudit("REVEAL_ACL_USER_CREDENTIALS", "ACL_USER", 
String.valueOf(id), null,
+                "instanceId=" + instanceId);
+        return user;
     }
 
     /**
diff --git 
a/server/src/main/java/org/apache/rocketmq/studio/provider/credential/CloudCredentialService.java
 
b/server/src/main/java/org/apache/rocketmq/studio/provider/credential/CloudCredentialService.java
index 505b2c107..1d30f548a 100644
--- 
a/server/src/main/java/org/apache/rocketmq/studio/provider/credential/CloudCredentialService.java
+++ 
b/server/src/main/java/org/apache/rocketmq/studio/provider/credential/CloudCredentialService.java
@@ -167,8 +167,13 @@ public class CloudCredentialService {
         if (id == null) {
             throw new BusinessException(400, "Cloud credential id is 
required");
         }
-        return credentialRepository.findById(id)
+        CloudCredentialVO credential = credentialRepository.findById(id)
                 .orElseThrow(() -> new BusinessException(404, "Cloud 
credential not found: " + id));
+        // Reading the secret is the one operation on this resource that 
leaves no other trace, so it
+        // is audited like the mutations: the detail names the credential but 
never the secret.
+        recordAudit("REVEAL_CLOUD_CREDENTIAL", "CLOUD_CREDENTIAL", 
String.valueOf(credential.getId()), null,
+                credentialAuditDetail(credential));
+        return credential;
     }
 
     private CloudCredentialVO maskAccessKey(CloudCredentialVO credential) {
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/instance/acl/AclServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/instance/acl/AclServiceTest.java
index e5bddf384..4f23095de 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/instance/acl/AclServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/instance/acl/AclServiceTest.java
@@ -589,6 +589,38 @@ class AclServiceTest {
         assertThat(captor.getValue().isAdmin()).isTrue();
     }
 
+    @Test
+    void 
getUserCredentialsShouldAuditTheRevealedSecretWithoutRecordingItTest() {
+        
when(aclRepository.findUserById(1L)).thenReturn(Optional.of(existingUser));
+
+        AclUserVO revealed = aclService.getUserCredentials("1", null);
+
+        assertThat(revealed.getSecretKey()).isEqualTo("secret-key-987654");
+        
verify(operationAuditService).record(eq("REVEAL_ACL_USER_CREDENTIALS"), 
eq("ACL_USER"), eq("1"),
+                eq(null), argThat((String detail) -> 
!detail.contains("secret-key-987654")
+                        && !detail.contains("access-key-123456")),
+                eq("SUCCESS"), eq(null));
+    }
+
+    @Test
+    void 
getUserCredentialsShouldAuditTheSecretRevealedThroughTheTencentPathTest() {
+        InstanceVO tencent = InstanceVO.builder()
+                .name("tencent-instance")
+                .vendor(InstanceVendor.TENCENT)
+                .type(InstanceType.CLOUD)
+                .build();
+        
when(instanceResolver.findByIdentifier("tencent-instance")).thenReturn(Optional.of(tencent));
+        when(tencentAclService.getUserCredentials("tencent-instance", 
"1")).thenReturn(existingUser);
+
+        AclUserVO revealed = aclService.getUserCredentials("1", 
"tencent-instance");
+
+        assertThat(revealed.getSecretKey()).isEqualTo("secret-key-987654");
+        
verify(operationAuditService).record(eq("REVEAL_ACL_USER_CREDENTIALS"), 
eq("ACL_USER"), eq("1"),
+                eq(null), argThat((String detail) -> 
detail.contains("tencent-instance")
+                        && !detail.contains("secret-key-987654")),
+                eq("SUCCESS"), eq(null));
+    }
+
     @Test
     void updateUserShouldRejectBlankUsernameWithoutSaving() {
         UpdateAclUserDTO input = new UpdateAclUserDTO();
diff --git 
a/server/src/test/java/org/apache/rocketmq/studio/provider/credential/CloudCredentialServiceTest.java
 
b/server/src/test/java/org/apache/rocketmq/studio/provider/credential/CloudCredentialServiceTest.java
index 82fd069bd..4f5a1e027 100644
--- 
a/server/src/test/java/org/apache/rocketmq/studio/provider/credential/CloudCredentialServiceTest.java
+++ 
b/server/src/test/java/org/apache/rocketmq/studio/provider/credential/CloudCredentialServiceTest.java
@@ -284,6 +284,24 @@ class CloudCredentialServiceTest {
         assertThat(revealed.getSecretKey()).isEqualTo("plain-secret");
     }
 
+    @Test
+    void revealShouldAuditTheSecretReadWithoutRecordingTheSecretTest() {
+        CloudCredentialVO stored = new CloudCredentialVO();
+        stored.setId(9L);
+        stored.setName("prod-aliyun");
+        stored.setVendor(InstanceVendor.ALIYUN);
+        stored.setAccessKey("LTAI5tRevealKey000000009");
+        stored.setSecretKey("plain-secret");
+        
when(credentialRepository.findById(9L)).thenReturn(Optional.of(stored));
+
+        service.reveal(9L);
+
+        verify(operationAuditService).record(eq("REVEAL_CLOUD_CREDENTIAL"), 
eq("CLOUD_CREDENTIAL"),
+                eq("9"), isNull(),
+                argThat((String detail) -> detail.contains("prod-aliyun") && 
!detail.contains("plain-secret")),
+                eq("SUCCESS"), isNull());
+    }
+
     @Test
     void repositoryShouldBase64EncodeSecretTest() {
         String encoded = CredentialUtils.encodeBase64("plain-secret");
diff --git a/web/src/i18n/translations.ts b/web/src/i18n/translations.ts
index b479e5546..f8f82a51f 100644
--- a/web/src/i18n/translations.ts
+++ b/web/src/i18n/translations.ts
@@ -1517,6 +1517,10 @@ const translations: Record<string, Record<Lang, string>> 
= {
   'audit.op.CREATE_ACL_USER': { zh: '创建 ACL 用户', en: 'Create ACL User' },
   'audit.op.UPDATE_ACL_USER': { zh: '更新 ACL 用户', en: 'Update ACL User' },
   'audit.op.DELETE_ACL_USER': { zh: '删除 ACL 用户', en: 'Delete ACL User' },
+  'audit.op.REVEAL_ACL_USER_CREDENTIALS': {
+    zh: '查看 ACL 用户凭据',
+    en: 'Reveal ACL User Credentials',
+  },
   'audit.op.UPSERT_PLAIN_ACCESS_CONFIG': {
     zh: '更新 Plain Access 配置',
     en: 'Upsert Plain Access Config',
@@ -1528,6 +1532,7 @@ const translations: Record<string, Record<Lang, string>> 
= {
   'audit.op.CREATE_CLOUD_CREDENTIAL': { zh: '创建云凭据', en: 'Create Cloud 
Credential' },
   'audit.op.UPDATE_CLOUD_CREDENTIAL': { zh: '更新云凭据', en: 'Update Cloud 
Credential' },
   'audit.op.DELETE_CLOUD_CREDENTIAL': { zh: '删除云凭据', en: 'Delete Cloud 
Credential' },
+  'audit.op.REVEAL_CLOUD_CREDENTIAL': { zh: '查看云凭据', en: 'Reveal Cloud 
Credential' },
   'audit.op.CREATE_ALERT_RULE': { zh: '创建告警规则', en: 'Create Alert Rule' },
   'audit.op.UPDATE_ALERT_RULE': { zh: '更新告警规则', en: 'Update Alert Rule' },
   'audit.op.TOGGLE_ALERT_RULE': { zh: '启停告警规则', en: 'Toggle Alert Rule' },
diff --git a/web/src/pages/ops/__tests__/auditPresentation.test.ts 
b/web/src/pages/ops/__tests__/auditPresentation.test.ts
index cfb7af35d..f74147a85 100644
--- a/web/src/pages/ops/__tests__/auditPresentation.test.ts
+++ b/web/src/pages/ops/__tests__/auditPresentation.test.ts
@@ -67,6 +67,21 @@ describe('audit presentation helpers', () => {
     });
   });
 
+  it('presents credential reveals as their own localized operations', () => {
+    
expect(getAuditOperationPresentation('REVEAL_ACL_USER_CREDENTIALS')).toEqual({
+      labelKey: 'audit.op.REVEAL_ACL_USER_CREDENTIALS',
+      label: 'Reveal ACL User Credentials',
+      color: 'gold',
+      category: 'security',
+    });
+    expect(getAuditOperationPresentation('REVEAL_CLOUD_CREDENTIAL')).toEqual({
+      labelKey: 'audit.op.REVEAL_CLOUD_CREDENTIAL',
+      label: 'Reveal Cloud Credential',
+      color: 'gold',
+      category: 'settings',
+    });
+  });
+
   it('falls back to title-cased labels for new operation codes', () => {
     expect(getAuditOperationPresentation('UPSERT_NEW_RESOURCE')).toEqual({
       label: 'Upsert New Resource',
diff --git a/web/src/pages/ops/auditPresentation.ts 
b/web/src/pages/ops/auditPresentation.ts
index e2fc110a6..588d10c90 100644
--- a/web/src/pages/ops/auditPresentation.ts
+++ b/web/src/pages/ops/auditPresentation.ts
@@ -78,6 +78,11 @@ const operationPresentation: Record<string, 
AuditOperationPresentation> = {
   CREATE_ACL_USER: { label: 'Create ACL User', color: 'blue', category: 
'security' },
   UPDATE_ACL_USER: { label: 'Update ACL User', color: 'cyan', category: 
'security' },
   DELETE_ACL_USER: { label: 'Delete ACL User', color: 'volcano', category: 
'security' },
+  REVEAL_ACL_USER_CREDENTIALS: {
+    label: 'Reveal ACL User Credentials',
+    color: 'gold',
+    category: 'security',
+  },
   UPSERT_PLAIN_ACCESS_CONFIG: {
     label: 'Upsert Plain Access Config',
     color: 'purple',
@@ -103,6 +108,11 @@ const operationPresentation: Record<string, 
AuditOperationPresentation> = {
     color: 'volcano',
     category: 'settings',
   },
+  REVEAL_CLOUD_CREDENTIAL: {
+    label: 'Reveal Cloud Credential',
+    color: 'gold',
+    category: 'settings',
+  },
 
   CREATE_ALERT_RULE: { label: 'Create Alert Rule', color: 'blue', category: 
'alerts' },
   UPDATE_ALERT_RULE: { label: 'Update Alert Rule', color: 'cyan', category: 
'alerts' },

Reply via email to