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 da34a5890 fix(acl): keep the stored admin flag on a partial
plain-access update (#7139)
da34a5890 is described below
commit da34a5890decb280c24816ee3156bf1159de5387
Author: btlqql <[email protected]>
AuthorDate: Mon Oct 26 20:40:18 2026 +0800
fix(acl): keep the stored admin flag on a partial plain-access update
(#7139)
* fix(acl): keep the stored admin flag on a partial plain-access update
admin is optional in the documented plain-access write contract, but the
write path could not tell "not provided" from "explicitly false": the request
DTO and the write model carried a primitive boolean and
createAndUpdatePlainAccessConfig always wrote it. Saving an existing admin
account without naming the flag - rotating its secret, editing its whitelist or
its permission entries - silently demoted it, answered admin:false and dropped
it out of the ACL risk view's admin count. Follo [...]
* docs(api-spec): spell out the tri-state admin flag on plain-access upsert
The flag became optional with keep-stored semantics; the request row still
read as
a plain boolean and the response said nothing about which value comes back.
---
docs/api-spec.md | 5 +-
.../rocketmq/studio/instance/acl/AclService.java | 2 +-
.../instance/acl/MybatisPlusAclRepository.java | 9 +++-
.../studio/instance/acl/PlainAccessConfigVO.java | 7 ++-
.../instance/acl/UpsertPlainAccessConfigDTO.java | 6 ++-
.../instance/acl/MybatisPlusAclRepositoryTest.java | 63 +++++++++++++++++++++-
6 files changed, 83 insertions(+), 9 deletions(-)
diff --git a/docs/api-spec.md b/docs/api-spec.md
index 8c9782aa3..eaac7d7f5 100644
--- a/docs/api-spec.md
+++ b/docs/api-spec.md
@@ -1772,13 +1772,14 @@ POST /api/acl/plain-access-config
| `accessKey` | `string` | 是 | 账号 AccessKey(更新时不可变更) |
| `secretKey` | `string` | 条件 | 新建账号必填;更新时留空表示保留原密钥 |
| `whiteRemoteAddress` | `string` | 否 | IP 白名单,持久化存储,空表示不限制 |
-| `admin` | `boolean` | 否 | 是否管理员 |
+| `admin` | `boolean` | 否 | 是否管理员;**省略 = 保留存量值**(新建账号默认 `false`),显式传值才覆盖 |
| `defaultTopicPerm` | `string` | 否 | 默认 Topic 权限 |
| `defaultGroupPerm` | `string` | 否 | 默认 Group 权限 |
| `topicPerms` | `string[]` | 否 | 逐 Topic 权限,格式 `resource=action` |
| `groupPerms` | `string[]` | 否 | 逐 Group 权限,格式 `resource=action` |
-**Response `data`:** `PlainAccessConfig`。`secretKey` 仅在本次显式提交时回显,保留原密钥时返回
`null`。
+**Response `data`:** `PlainAccessConfig`。`secretKey` 仅在本次显式提交时回显,保留原密钥时返回
`null`;
+`admin` 回显**生效值**(省略写入时为保留后的存量值)。
### 7.10 查看单个用户明文凭证
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 7c7536659..224d2482d 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
@@ -333,7 +333,7 @@ public class AclService {
}
log.info("Creating/updating plain access config accessKey={}",
config.getAccessKey());
PlainAccessConfigVO saved =
aclRepository.createAndUpdatePlainAccessConfig(config);
- String auditDetail = "admin=" + saved.isAdmin()
+ String auditDetail = "admin=" + Boolean.TRUE.equals(saved.getAdmin())
+ ", whiteRemoteAddressConfigured="
+ StringUtils.hasText(saved.getWhiteRemoteAddress());
recordAudit("UPSERT_PLAIN_ACCESS_CONFIG", "ACL_USER",
saved.getAccessKey(), null,
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/MybatisPlusAclRepository.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/MybatisPlusAclRepository.java
index 5f751e992..f51eee559 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/MybatisPlusAclRepository.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/MybatisPlusAclRepository.java
@@ -262,7 +262,12 @@ public class MybatisPlusAclRepository implements
AclRepository {
// Blank secret on an existing account keeps the stored secret
unchanged.
entity.setSecretKey(existing.getSecretKey());
}
- entity.setAdmin(config.isAdmin());
+ // The flag is optional in the write contract: an omitted flag keeps
the stored value for an
+ // existing account (as the sibling user update does), a new account
defaults to false, and
+ // an explicit value always wins.
+ boolean admin = config.getAdmin() != null ? config.getAdmin()
+ : existing != null && Boolean.TRUE.equals(existing.getAdmin());
+ entity.setAdmin(admin);
entity.setClusters(null);
entity.setWhiteRemoteAddress(normalizeWhiteRemoteAddress(config.getWhiteRemoteAddress()));
entity.setGmtModified(LocalDateTime.now());
@@ -300,7 +305,7 @@ public class MybatisPlusAclRepository implements
AclRepository {
// hidden (read-back views always mask it).
.secretKey(secretProvided ? config.getSecretKey() : null)
.whiteRemoteAddress(entity.getWhiteRemoteAddress())
- .admin(config.isAdmin())
+ .admin(admin)
.defaultTopicPerm(config.getDefaultTopicPerm())
.defaultGroupPerm(config.getDefaultGroupPerm())
.topicPerms(config.getTopicPerms() == null ? null : new
ArrayList<>(config.getTopicPerms()))
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/PlainAccessConfigVO.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/PlainAccessConfigVO.java
index e1c167c39..95be05839 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/PlainAccessConfigVO.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/PlainAccessConfigVO.java
@@ -51,8 +51,11 @@ public class PlainAccessConfigVO {
/** IP whitelist pattern for this account; persisted, empty/null means no
restriction. */
private String whiteRemoteAddress;
- /** Whether this account has admin privileges. */
- private boolean admin;
+ /**
+ * Whether this account has admin privileges. Null on a write that did not
name the flag: the
+ * write path then keeps the stored value, and the response carries the
effective value.
+ */
+ private Boolean admin;
/** Default permission applied to topics, e.g. DENY / PUB / SUB / ALL. */
private String defaultTopicPerm;
diff --git
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/UpsertPlainAccessConfigDTO.java
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/UpsertPlainAccessConfigDTO.java
index 4eb259c9d..c045abdb6 100644
---
a/server/src/main/java/org/apache/rocketmq/studio/instance/acl/UpsertPlainAccessConfigDTO.java
+++
b/server/src/main/java/org/apache/rocketmq/studio/instance/acl/UpsertPlainAccessConfigDTO.java
@@ -39,7 +39,11 @@ public class UpsertPlainAccessConfigDTO {
private String whiteRemoteAddress;
- private boolean admin;
+ /**
+ * Null when the admin flag was not part of the write, in which case an
existing account keeps
+ * its stored value and a new account defaults to false; an explicit value
always wins.
+ */
+ private Boolean admin;
private String defaultTopicPerm;
diff --git
a/server/src/test/java/org/apache/rocketmq/studio/instance/acl/MybatisPlusAclRepositoryTest.java
b/server/src/test/java/org/apache/rocketmq/studio/instance/acl/MybatisPlusAclRepositoryTest.java
index 6b5b984d9..e4b071b96 100644
---
a/server/src/test/java/org/apache/rocketmq/studio/instance/acl/MybatisPlusAclRepositoryTest.java
+++
b/server/src/test/java/org/apache/rocketmq/studio/instance/acl/MybatisPlusAclRepositoryTest.java
@@ -458,7 +458,68 @@ class MybatisPlusAclRepositoryTest {
.isEqualTo(CredentialUtils.encodeBase64("kept-secret-value"));
// The kept secret is not echoed back.
assertThat(result.getSecretKey()).isNull();
- assertThat(result.isAdmin()).isTrue();
+ assertThat(result.getAdmin()).isTrue();
+ }
+
+ @Test
+ void updateWithoutTheAdminFlagShouldKeepTheStoredAdminFlag() {
+ RmqAclUser existing = userEntity(1L, "svc-x",
+ CredentialUtils.encodeBase64("kept-secret-value"));
+ existing.setAdmin(true);
+
when(userMapper.selectList(any(QueryWrapper.class))).thenReturn(List.of(existing));
+ when(userMapper.updateById(any(RmqAclUser.class))).thenReturn(1);
+ when(ruleMapper.delete(any(QueryWrapper.class))).thenReturn(0);
+
+ // admin is optional in the write contract: rotating the secret must
not demote the account.
+ PlainAccessConfigVO config =
PlainAccessConfigVO.builder().accessKey("svc-x").build();
+
+ PlainAccessConfigVO result =
repository.createAndUpdatePlainAccessConfig(config);
+
+ ArgumentCaptor<RmqAclUser> captor =
ArgumentCaptor.forClass(RmqAclUser.class);
+ verify(userMapper).updateById(captor.capture());
+ assertThat(captor.getValue().getAdmin()).isTrue();
+ assertThat(result.getAdmin()).isTrue();
+ }
+
+ @Test
+ void updateWithAnExplicitFalseShouldClearTheStoredAdminFlag() {
+ RmqAclUser existing = userEntity(1L, "svc-x",
+ CredentialUtils.encodeBase64("kept-secret-value"));
+ existing.setAdmin(true);
+
when(userMapper.selectList(any(QueryWrapper.class))).thenReturn(List.of(existing));
+ when(userMapper.updateById(any(RmqAclUser.class))).thenReturn(1);
+ when(ruleMapper.delete(any(QueryWrapper.class))).thenReturn(0);
+
+ PlainAccessConfigVO config = PlainAccessConfigVO.builder()
+ .accessKey("svc-x")
+ .admin(false)
+ .build();
+
+ PlainAccessConfigVO result =
repository.createAndUpdatePlainAccessConfig(config);
+
+ ArgumentCaptor<RmqAclUser> captor =
ArgumentCaptor.forClass(RmqAclUser.class);
+ verify(userMapper).updateById(captor.capture());
+ assertThat(captor.getValue().getAdmin()).isFalse();
+ assertThat(result.getAdmin()).isFalse();
+ }
+
+ @Test
+ void createWithoutTheAdminFlagShouldStoreFalse() {
+
when(userMapper.selectList(any(QueryWrapper.class))).thenReturn(List.of());
+ when(userMapper.insert(any(RmqAclUser.class))).thenReturn(1);
+ when(ruleMapper.delete(any(QueryWrapper.class))).thenReturn(0);
+
+ PlainAccessConfigVO config = PlainAccessConfigVO.builder()
+ .accessKey("svc-new")
+ .secretKey("secret-new")
+ .build();
+
+ PlainAccessConfigVO result =
repository.createAndUpdatePlainAccessConfig(config);
+
+ ArgumentCaptor<RmqAclUser> captor =
ArgumentCaptor.forClass(RmqAclUser.class);
+ verify(userMapper).insert(captor.capture());
+ assertThat(captor.getValue().getAdmin()).isFalse();
+ assertThat(result.getAdmin()).isFalse();
}
@Test