This is an automated email from the ASF dual-hosted git repository.
RongtongJin pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/rocketmq.git
The following commit(s) were added to refs/heads/develop by this push:
new 66362b9c9f fix(auth): address ACL follow-up regressions (#11009)
66362b9c9f is described below
commit 66362b9c9f013f3ee7e510f5dbfb792c2053d3bd
Author: dingshuangxi888 <[email protected]>
AuthorDate: Tue Sep 8 09:42:40 2026 +0800
fix(auth): address ACL follow-up regressions (#11009)
Co-authored-by: shuangxi.dsx <[email protected]>
---
.../DefaultAuthorizationContextBuilder.java | 6 +-
.../DefaultAuthorizationContextBuilderTest.java | 6 ++
.../broker/processor/AdminBrokerProcessor.java | 17 ++-
.../AdminBrokerProcessorConfigSanitizeTest.java | 33 ++++--
.../container/BrokerContainerProcessor.java | 17 ++-
...BrokerContainerProcessorConfigSanitizeTest.java | 33 ++++--
.../apache/rocketmq/remoting/Configuration.java | 33 ++++--
.../rocketmq/remoting/ConfigurationTest.java | 46 +++++++++
.../tools/command/auth/CopyUsersSubCommand.java | 21 +++-
.../command/auth/CopyUsersSubCommandTest.java | 115 +++++++++++++++++++++
10 files changed, 272 insertions(+), 55 deletions(-)
diff --git
a/auth/src/main/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilder.java
b/auth/src/main/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilder.java
index af090b5f0f..54625ca527 100644
---
a/auth/src/main/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilder.java
+++
b/auth/src/main/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilder.java
@@ -257,8 +257,10 @@ public class DefaultAuthorizationContextBuilder implements
AuthorizationContextB
}
break;
case RequestCode.VIEW_MESSAGE_BY_ID:
- topic =
Resource.ofTopic(requireResource(fields.get(TOPIC), "topic"));
- result.add(DefaultAuthorizationContext.of(subject, topic,
Action.GET, sourceIp));
+ String viewTopic = requireResource(fields.get(TOPIC),
"topic");
+ Resource viewResource =
NamespaceUtil.isRetryTopic(viewTopic)
+ ? Resource.ofGroup(viewTopic) :
Resource.ofTopic(viewTopic);
+ result.add(DefaultAuthorizationContext.of(subject,
viewResource, Action.GET, sourceIp));
break;
case RequestCode.CONSUMER_SEND_MSG_BACK:
group =
Resource.ofGroup(requireResource(fields.get(GROUP), "consumer group"));
diff --git
a/auth/src/test/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilderTest.java
b/auth/src/test/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilderTest.java
index f90352ef09..8be1f126d9 100644
---
a/auth/src/test/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilderTest.java
+++
b/auth/src/test/java/org/apache/rocketmq/auth/authorization/builder/DefaultAuthorizationContextBuilderTest.java
@@ -827,6 +827,12 @@ public class DefaultAuthorizationContextBuilderTest {
assertResourceOrder(result, "Topic:viewTopic");
assertActions(result, "Topic:viewTopic", Action.GET);
+ viewMessageHeader.setTopic("%RETRY%retryGroup");
+ result = builder.build(channelHandlerContext,
+ remotingRequest(RequestCode.VIEW_MESSAGE_BY_ID, viewMessageHeader,
null));
+ assertResourceOrder(result, "Group:retryGroup");
+ assertActions(result, "Group:retryGroup", Action.GET);
+
BatchAck firstAck = batchAck("topicA", "groupA", "0");
BatchAck duplicateAck = batchAck("topicA", "groupA", "0");
BatchAck secondAck = batchAck("topicB", "groupB", "0");
diff --git
a/broker/src/main/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessor.java
b/broker/src/main/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessor.java
index 602a8efce0..8083d7307c 100644
---
a/broker/src/main/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessor.java
+++
b/broker/src/main/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessor.java
@@ -1178,7 +1178,8 @@ public class AdminBrokerProcessor implements
NettyRequestProcessor {
final RemotingCommand response =
RemotingCommand.createResponseCommand(GetBrokerConfigResponseHeader.class);
final GetBrokerConfigResponseHeader responseHeader =
(GetBrokerConfigResponseHeader) response.readCustomHeader();
- String content =
sanitizeConfigForResponse(this.brokerController.getConfiguration().getAllConfigsFormatString());
+ String content = sanitizeConfigForResponse(
+ this.brokerController.getConfiguration().getAllConfigsSnapshot());
if (content == null) {
LOGGER.error("AdminBrokerProcessor#getBrokerConfig: failed to
sanitize broker config, caller={}",
RemotingHelper.parseChannelRemoteAddr(ctx.channel()));
@@ -1213,18 +1214,14 @@ public class AdminBrokerProcessor implements
NettyRequestProcessor {
};
/**
- * Remove sensitive entries from the exported config content. Returns null
when the
- * content cannot be parsed, so callers must fail closed instead of
returning the
- * original content.
+ * Remove sensitive entries from a snapshot of the broker configuration.
*/
- static String sanitizeConfigForResponse(String content) {
- if (content == null || content.isEmpty()) {
- return content;
- }
- Properties properties = MixAll.string2Properties(content);
- if (properties == null) {
+ static String sanitizeConfigForResponse(Properties source) {
+ if (source == null) {
return null;
}
+ Properties properties = new Properties();
+ properties.putAll(source);
for (String key : SENSITIVE_CONFIG_KEYS) {
properties.remove(key);
}
diff --git
a/broker/src/test/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessorConfigSanitizeTest.java
b/broker/src/test/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessorConfigSanitizeTest.java
index be78a8908f..0a51449c3f 100644
---
a/broker/src/test/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessorConfigSanitizeTest.java
+++
b/broker/src/test/java/org/apache/rocketmq/broker/processor/AdminBrokerProcessorConfigSanitizeTest.java
@@ -16,6 +16,7 @@
*/
package org.apache.rocketmq.broker.processor;
+import java.util.Properties;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
@@ -27,29 +28,39 @@ public class AdminBrokerProcessorConfigSanitizeTest {
@Test
public void testSanitizeRemovesSensitiveKeys() {
- String content = "brokerName=broker-a\n"
- +
"initAuthenticationUser={\"username\":\"rocketmq\",\"password\":\"secret\"}\n"
- +
"innerClientAuthenticationCredentials={\"accessKey\":\"ak\",\"secretKey\":\"sk\"}\n"
- + "listenPort=10911\n";
+ Properties properties = new Properties();
+ properties.setProperty("brokerName", "broker-a");
+ properties.setProperty("initAuthenticationUser",
"sensitive-user-config");
+ properties.setProperty("innerClientAuthenticationCredentials",
"sensitive-client-config");
+ properties.setProperty("listenPort", "10911");
- String sanitized =
AdminBrokerProcessor.sanitizeConfigForResponse(content);
+ String sanitized =
AdminBrokerProcessor.sanitizeConfigForResponse(properties);
assertFalse(sanitized.contains("initAuthenticationUser"));
assertFalse(sanitized.contains("innerClientAuthenticationCredentials"));
- assertFalse(sanitized.contains("secret"));
+ assertFalse(sanitized.contains("sensitive"));
assertTrue(sanitized.contains("brokerName=broker-a"));
assertTrue(sanitized.contains("listenPort=10911"));
+ assertEquals(4, properties.size());
}
@Test
- public void testSanitizeFailsClosedOnUnparsableContent() {
- // malformed unicode escape makes Properties.load throw
-
assertNull(AdminBrokerProcessor.sanitizeConfigForResponse("key=\\uZZZZ\n"));
+ public void testSanitizePreservesConfigValues() {
+ Properties properties = new Properties();
+ properties.setProperty("customPath", "C:\\rocketmq\\store");
+ properties.setProperty("label", "\u4e2d\u6587");
+ properties.setProperty("regex", "^foo\\d+$");
+
+ String sanitized =
AdminBrokerProcessor.sanitizeConfigForResponse(properties);
+
+ assertTrue(sanitized.contains("customPath=C:\\rocketmq\\store"));
+ assertTrue(sanitized.contains("label=\u4e2d\u6587"));
+ assertTrue(sanitized.contains("regex=^foo\\d+$"));
}
@Test
- public void testSanitizePassesThroughNullOrEmpty() {
+ public void testSanitizeHandlesNullOrEmpty() {
assertNull(AdminBrokerProcessor.sanitizeConfigForResponse(null));
- assertEquals("", AdminBrokerProcessor.sanitizeConfigForResponse(""));
+ assertEquals("", AdminBrokerProcessor.sanitizeConfigForResponse(new
Properties()));
}
}
diff --git
a/container/src/main/java/org/apache/rocketmq/container/BrokerContainerProcessor.java
b/container/src/main/java/org/apache/rocketmq/container/BrokerContainerProcessor.java
index 5d534965fe..b4a6b145a4 100644
---
a/container/src/main/java/org/apache/rocketmq/container/BrokerContainerProcessor.java
+++
b/container/src/main/java/org/apache/rocketmq/container/BrokerContainerProcessor.java
@@ -286,18 +286,14 @@ public class BrokerContainerProcessor implements
NettyRequestProcessor {
};
/**
- * Remove sensitive entries from the exported config content. Returns null
when the
- * content cannot be parsed, so callers must fail closed instead of
returning the
- * original content.
+ * Remove sensitive entries from a snapshot of the broker configuration.
*/
- static String sanitizeConfigForResponse(String content) {
- if (content == null || content.isEmpty()) {
- return content;
- }
- Properties properties = MixAll.string2Properties(content);
- if (properties == null) {
+ static String sanitizeConfigForResponse(Properties source) {
+ if (source == null) {
return null;
}
+ Properties properties = new Properties();
+ properties.putAll(source);
for (String key : SENSITIVE_CONFIG_KEYS) {
properties.remove(key);
}
@@ -318,7 +314,8 @@ public class BrokerContainerProcessor implements
NettyRequestProcessor {
final RemotingCommand response =
RemotingCommand.createResponseCommand(GetBrokerConfigResponseHeader.class);
final GetBrokerConfigResponseHeader responseHeader =
(GetBrokerConfigResponseHeader) response.readCustomHeader();
- String content =
sanitizeConfigForResponse(this.brokerContainer.getConfiguration().getAllConfigsFormatString());
+ String content = sanitizeConfigForResponse(
+ this.brokerContainer.getConfiguration().getAllConfigsSnapshot());
if (content == null) {
LOGGER.error("BrokerContainerProcessor#getBrokerConfig: failed to
sanitize broker config");
response.setCode(ResponseCode.SYSTEM_ERROR);
diff --git
a/container/src/test/java/org/apache/rocketmq/container/BrokerContainerProcessorConfigSanitizeTest.java
b/container/src/test/java/org/apache/rocketmq/container/BrokerContainerProcessorConfigSanitizeTest.java
index 0aaa6d2c69..051b6dee2a 100644
---
a/container/src/test/java/org/apache/rocketmq/container/BrokerContainerProcessorConfigSanitizeTest.java
+++
b/container/src/test/java/org/apache/rocketmq/container/BrokerContainerProcessorConfigSanitizeTest.java
@@ -16,6 +16,7 @@
*/
package org.apache.rocketmq.container;
+import java.util.Properties;
import org.junit.Test;
import static org.junit.Assert.assertEquals;
@@ -27,29 +28,39 @@ public class BrokerContainerProcessorConfigSanitizeTest {
@Test
public void testSanitizeRemovesSensitiveKeys() {
- String content = "brokerName=broker-a\n"
- +
"initAuthenticationUser={\"username\":\"rocketmq\",\"password\":\"secret\"}\n"
- +
"innerClientAuthenticationCredentials={\"accessKey\":\"ak\",\"secretKey\":\"sk\"}\n"
- + "listenPort=10911\n";
+ Properties properties = new Properties();
+ properties.setProperty("brokerName", "broker-a");
+ properties.setProperty("initAuthenticationUser",
"sensitive-user-config");
+ properties.setProperty("innerClientAuthenticationCredentials",
"sensitive-client-config");
+ properties.setProperty("listenPort", "10911");
- String sanitized =
BrokerContainerProcessor.sanitizeConfigForResponse(content);
+ String sanitized =
BrokerContainerProcessor.sanitizeConfigForResponse(properties);
assertFalse(sanitized.contains("initAuthenticationUser"));
assertFalse(sanitized.contains("innerClientAuthenticationCredentials"));
- assertFalse(sanitized.contains("secret"));
+ assertFalse(sanitized.contains("sensitive"));
assertTrue(sanitized.contains("brokerName=broker-a"));
assertTrue(sanitized.contains("listenPort=10911"));
+ assertEquals(4, properties.size());
}
@Test
- public void testSanitizeFailsClosedOnUnparsableContent() {
- // malformed unicode escape makes Properties.load throw
-
assertNull(BrokerContainerProcessor.sanitizeConfigForResponse("key=\\uZZZZ\n"));
+ public void testSanitizePreservesConfigValues() {
+ Properties properties = new Properties();
+ properties.setProperty("customPath", "C:\\rocketmq\\store");
+ properties.setProperty("label", "\u4e2d\u6587");
+ properties.setProperty("regex", "^foo\\d+$");
+
+ String sanitized =
BrokerContainerProcessor.sanitizeConfigForResponse(properties);
+
+ assertTrue(sanitized.contains("customPath=C:\\rocketmq\\store"));
+ assertTrue(sanitized.contains("label=\u4e2d\u6587"));
+ assertTrue(sanitized.contains("regex=^foo\\d+$"));
}
@Test
- public void testSanitizePassesThroughNullOrEmpty() {
+ public void testSanitizeHandlesNullOrEmpty() {
assertNull(BrokerContainerProcessor.sanitizeConfigForResponse(null));
- assertEquals("",
BrokerContainerProcessor.sanitizeConfigForResponse(""));
+ assertEquals("",
BrokerContainerProcessor.sanitizeConfigForResponse(new Properties()));
}
}
diff --git
a/remoting/src/main/java/org/apache/rocketmq/remoting/Configuration.java
b/remoting/src/main/java/org/apache/rocketmq/remoting/Configuration.java
index 5b3e5ca379..649d59b83b 100644
--- a/remoting/src/main/java/org/apache/rocketmq/remoting/Configuration.java
+++ b/remoting/src/main/java/org/apache/rocketmq/remoting/Configuration.java
@@ -278,24 +278,39 @@ public class Configuration {
return null;
}
+ public Properties getAllConfigsSnapshot() {
+ try {
+ readWriteLock.readLock().lockInterruptibly();
+
+ try {
+ refreshAllConfigs();
+ Properties snapshot = new Properties();
+ snapshot.putAll(this.allConfigs);
+ return snapshot;
+ } finally {
+ readWriteLock.readLock().unlock();
+ }
+ } catch (InterruptedException e) {
+ log.error("getAllConfigsSnapshot lock error");
+ }
+
+ return null;
+ }
+
private String getAllConfigsInternal() {
- StringBuilder stringBuilder = new StringBuilder();
+ refreshAllConfigs();
+ return MixAll.properties2String(this.allConfigs, true);
+ }
- // reload from config object ?
+ private void refreshAllConfigs() {
for (Object configObject : this.configObjectList) {
Properties properties = MixAll.object2Properties(configObject);
if (properties != null) {
merge(properties, this.allConfigs);
} else {
- log.warn("getAllConfigsInternal object2Properties is null,
{}", configObject.getClass());
+ log.warn("refreshAllConfigs object2Properties is null, {}",
configObject.getClass());
}
}
-
- {
- stringBuilder.append(MixAll.properties2String(this.allConfigs,
true));
- }
-
- return stringBuilder.toString();
}
private String getClientConfigsInternal(List<String> clientConigKeys) {
diff --git
a/remoting/src/test/java/org/apache/rocketmq/remoting/ConfigurationTest.java
b/remoting/src/test/java/org/apache/rocketmq/remoting/ConfigurationTest.java
new file mode 100644
index 0000000000..2698e8e72e
--- /dev/null
+++ b/remoting/src/test/java/org/apache/rocketmq/remoting/ConfigurationTest.java
@@ -0,0 +1,46 @@
+/*
+ * 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.remoting;
+
+import java.util.Properties;
+import org.apache.rocketmq.logging.org.slf4j.Logger;
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotSame;
+import static org.mockito.Mockito.mock;
+
+public class ConfigurationTest {
+
+ @Test
+ public void testGetAllConfigsSnapshotRefreshesAndCopiesProperties() {
+ TestConfig testConfig = new TestConfig();
+ Configuration configuration = new Configuration(mock(Logger.class),
testConfig);
+ testConfig.customPath = "C:\\rocketmq\\store";
+
+ Properties snapshot = configuration.getAllConfigsSnapshot();
+
+ assertEquals("C:\\rocketmq\\store",
snapshot.getProperty("customPath"));
+ assertNotSame(configuration.getAllConfigs(), snapshot);
+ snapshot.remove("customPath");
+ assertEquals("C:\\rocketmq\\store",
configuration.getAllConfigs().getProperty("customPath"));
+ }
+
+ private static class TestConfig {
+ private String customPath = "initial";
+ }
+}
diff --git
a/tools/src/main/java/org/apache/rocketmq/tools/command/auth/CopyUsersSubCommand.java
b/tools/src/main/java/org/apache/rocketmq/tools/command/auth/CopyUsersSubCommand.java
index 7f2c224ad8..703db77395 100644
---
a/tools/src/main/java/org/apache/rocketmq/tools/command/auth/CopyUsersSubCommand.java
+++
b/tools/src/main/java/org/apache/rocketmq/tools/command/auth/CopyUsersSubCommand.java
@@ -64,7 +64,7 @@ public class CopyUsersSubCommand implements SubCommand {
public void execute(CommandLine commandLine, Options options,
RPCHook rpcHook) throws SubCommandException {
- DefaultMQAdminExt defaultMQAdminExt = new DefaultMQAdminExt(rpcHook);
+ DefaultMQAdminExt defaultMQAdminExt = createAdminExt(rpcHook);
defaultMQAdminExt.setInstanceName(Long.toString(System.currentTimeMillis()));
try {
@@ -84,7 +84,16 @@ public class CopyUsersSubCommand implements SubCommand {
}
}
} else {
- userInfos = defaultMQAdminExt.listUser(sourceBroker, null);
+ List<UserInfo> listedUserInfos =
defaultMQAdminExt.listUser(sourceBroker, null);
+ if (CollectionUtils.isNotEmpty(listedUserInfos)) {
+ for (UserInfo listedUserInfo : listedUserInfos) {
+ UserInfo userInfo = defaultMQAdminExt.getUser(
+ sourceBroker, listedUserInfo.getUsername());
+ if (userInfo != null) {
+ userInfos.add(userInfo);
+ }
+ }
+ }
}
if (CollectionUtils.isEmpty(userInfos)) {
@@ -92,6 +101,10 @@ public class CopyUsersSubCommand implements SubCommand {
}
for (UserInfo userInfo : userInfos) {
+ if (StringUtils.isBlank(userInfo.getPassword())) {
+ throw new IllegalStateException("Password is
unavailable for user "
+ + userInfo.getUsername() + ". Use a source broker
super user to copy users.");
+ }
if (defaultMQAdminExt.getUser(targetBroker,
userInfo.getUsername()) == null) {
defaultMQAdminExt.createUser(targetBroker, userInfo);
} else {
@@ -110,4 +123,8 @@ public class CopyUsersSubCommand implements SubCommand {
defaultMQAdminExt.shutdown();
}
}
+
+ DefaultMQAdminExt createAdminExt(RPCHook rpcHook) {
+ return new DefaultMQAdminExt(rpcHook);
+ }
}
diff --git
a/tools/src/test/java/org/apache/rocketmq/tools/command/auth/CopyUsersSubCommandTest.java
b/tools/src/test/java/org/apache/rocketmq/tools/command/auth/CopyUsersSubCommandTest.java
new file mode 100644
index 0000000000..fb10029e5e
--- /dev/null
+++
b/tools/src/test/java/org/apache/rocketmq/tools/command/auth/CopyUsersSubCommandTest.java
@@ -0,0 +1,115 @@
+/*
+ * 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.tools.command.auth;
+
+import java.util.Collections;
+import org.apache.commons.cli.CommandLine;
+import org.apache.commons.cli.Options;
+import org.apache.rocketmq.remoting.RPCHook;
+import org.apache.rocketmq.remoting.protocol.body.UserInfo;
+import org.apache.rocketmq.tools.admin.DefaultMQAdminExt;
+import org.apache.rocketmq.tools.command.SubCommandException;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+public class CopyUsersSubCommandTest {
+ private static final String SOURCE_BROKER = "127.0.0.1:10911";
+ private static final String TARGET_BROKER = "127.0.0.1:20911";
+
+ private DefaultMQAdminExt adminExt;
+ private CommandLine commandLine;
+ private CopyUsersSubCommand command;
+
+ @Before
+ public void setUp() {
+ adminExt = mock(DefaultMQAdminExt.class);
+ commandLine = mock(CommandLine.class);
+ command = new CopyUsersSubCommand() {
+ @Override
+ DefaultMQAdminExt createAdminExt(RPCHook rpcHook) {
+ return adminExt;
+ }
+ };
+ when(commandLine.hasOption("f")).thenReturn(true);
+ when(commandLine.hasOption("t")).thenReturn(true);
+ when(commandLine.getOptionValue("f")).thenReturn(SOURCE_BROKER);
+ when(commandLine.getOptionValue("t")).thenReturn(TARGET_BROKER);
+ }
+
+ @Test
+ public void testCopyAllFetchesCompleteUserBeforeCreate() throws Exception {
+ UserInfo summary = UserInfo.of("alice", null, "Normal", "Enable");
+ UserInfo complete = UserInfo.of("alice", "dummy-password", "Normal",
"Enable");
+ when(adminExt.listUser(SOURCE_BROKER,
null)).thenReturn(Collections.singletonList(summary));
+ when(adminExt.getUser(SOURCE_BROKER, "alice")).thenReturn(complete);
+ when(adminExt.getUser(TARGET_BROKER, "alice")).thenReturn(null);
+
+ command.execute(commandLine, new Options(), null);
+
+ verify(adminExt).getUser(SOURCE_BROKER, "alice");
+ verify(adminExt).createUser(TARGET_BROKER, complete);
+ }
+
+ @Test
+ public void testCopyAllFetchesCompleteUserBeforeUpdate() throws Exception {
+ UserInfo summary = UserInfo.of("alice", null, "Normal", "Enable");
+ UserInfo complete = UserInfo.of("alice", "dummy-password", "Normal",
"Enable");
+ UserInfo target = UserInfo.of("alice", null, "Normal", "Enable");
+ when(adminExt.listUser(SOURCE_BROKER,
null)).thenReturn(Collections.singletonList(summary));
+ when(adminExt.getUser(SOURCE_BROKER, "alice")).thenReturn(complete);
+ when(adminExt.getUser(TARGET_BROKER, "alice")).thenReturn(target);
+
+ command.execute(commandLine, new Options(), null);
+
+ verify(adminExt).getUser(SOURCE_BROKER, "alice");
+ verify(adminExt).updateUser(TARGET_BROKER, complete);
+ }
+
+ @Test
+ public void testCopySelectedUserStillUsesCompleteUser() throws Exception {
+ UserInfo complete = UserInfo.of("alice", "dummy-password", "Normal",
"Enable");
+ when(commandLine.getOptionValue('u')).thenReturn("alice");
+ when(adminExt.getUser(SOURCE_BROKER, "alice")).thenReturn(complete);
+ when(adminExt.getUser(TARGET_BROKER, "alice")).thenReturn(null);
+
+ command.execute(commandLine, new Options(), null);
+
+ verify(adminExt, never()).listUser(SOURCE_BROKER, null);
+ verify(adminExt).createUser(TARGET_BROKER, complete);
+ }
+
+ @Test
+ public void testCopyAllFailsWhenPasswordIsUnavailable() throws Exception {
+ UserInfo summary = UserInfo.of("alice", null, "Normal", "Enable");
+ when(adminExt.listUser(SOURCE_BROKER,
null)).thenReturn(Collections.singletonList(summary));
+ when(adminExt.getUser(SOURCE_BROKER, "alice")).thenReturn(summary);
+
+ SubCommandException exception =
Assert.assertThrows(SubCommandException.class,
+ () -> command.execute(commandLine, new Options(), null));
+
+ Assert.assertTrue(exception.getCause().getMessage().contains("Password
is unavailable for user alice"));
+ verify(adminExt, never()).getUser(TARGET_BROKER, "alice");
+ verify(adminExt, never()).createUser(TARGET_BROKER, summary);
+ verify(adminExt, never()).updateUser(TARGET_BROKER, summary);
+ }
+}