[ISSUE #70] The rocketmq-dashboard supports ACL configuration (#71)
* Add Acl menu, support config acl.
* Optimize one line code.
* Add some unit tests for acl.
* Add permission control by role and optimize some code.
* The secret keys are hidden by asterisks.
* Search acl data will exclude secretKey info if the login role is not admin in the background.
* Optimize some code again.
* recover default application.yml config
diff --git a/src/main/java/org/apache/rocketmq/dashboard/controller/AclController.java b/src/main/java/org/apache/rocketmq/dashboard/controller/AclController.java
new file mode 100644
index 0000000..7aef786
--- /dev/null
+++ b/src/main/java/org/apache/rocketmq/dashboard/controller/AclController.java
@@ -0,0 +1,146 @@
+/*
+ * 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.dashboard.controller;
+
+import com.google.common.base.Preconditions;
+import java.util.List;
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletRequest;
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.rocketmq.common.AclConfig;
+import org.apache.rocketmq.common.PlainAccessConfig;
+import org.apache.rocketmq.dashboard.config.RMQConfigure;
+import org.apache.rocketmq.dashboard.model.User;
+import org.apache.rocketmq.dashboard.model.UserInfo;
+import org.apache.rocketmq.dashboard.model.request.AclRequest;
+import org.apache.rocketmq.dashboard.permisssion.Permission;
+import org.apache.rocketmq.dashboard.service.AclService;
+import org.apache.rocketmq.dashboard.support.JsonResult;
+import org.apache.rocketmq.dashboard.util.WebUtil;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/acl")
+@Permission
+public class AclController {
+
+ @Resource
+ private AclService aclService;
+
+ @Resource
+ private RMQConfigure configure;
+
+ @GetMapping("/enable.query")
+ public Object isEnableAcl() {
+ return new JsonResult<>(configure.isACLEnabled());
+ }
+
+ @GetMapping("/config.query")
+ public AclConfig getAclConfig(HttpServletRequest request) {
+ if (!configure.isLoginRequired()) {
+ return aclService.getAclConfig(false);
+ }
+ UserInfo userInfo = (UserInfo) WebUtil.getValueFromSession(request, WebUtil.USER_INFO);
+ // if user info is null but reach here, must exclude secret key for safety.
+ return aclService.getAclConfig(userInfo == null || userInfo.getUser().getType() != User.ADMIN);
+ }
+
+ @PostMapping("/add.do")
+ public Object addAclConfig(@RequestBody PlainAccessConfig config) {
+ Preconditions.checkArgument(StringUtils.isNotEmpty(config.getAccessKey()), "accessKey is null");
+ Preconditions.checkArgument(StringUtils.isNotEmpty(config.getSecretKey()), "secretKey is null");
+ aclService.addAclConfig(config);
+ return true;
+ }
+
+ @PostMapping("/delete.do")
+ public Object deleteAclConfig(@RequestBody PlainAccessConfig config) {
+ Preconditions.checkArgument(StringUtils.isNotEmpty(config.getAccessKey()), "accessKey is null");
+ aclService.deleteAclConfig(config);
+ return true;
+ }
+
+ @PostMapping("/update.do")
+ public Object updateAclConfig(@RequestBody PlainAccessConfig config) {
+ Preconditions.checkArgument(StringUtils.isNotEmpty(config.getSecretKey()), "secretKey is null");
+ aclService.updateAclConfig(config);
+ return true;
+ }
+
+ @PostMapping("/topic/add.do")
+ public Object addAclTopicConfig(@RequestBody AclRequest request) {
+ Preconditions.checkArgument(StringUtils.isNotEmpty(request.getConfig().getAccessKey()), "accessKey is null");
+ Preconditions.checkArgument(StringUtils.isNotEmpty(request.getConfig().getSecretKey()), "secretKey is null");
+ Preconditions.checkArgument(CollectionUtils.isNotEmpty(request.getConfig().getTopicPerms()), "topic perms is null");
+ Preconditions.checkArgument(StringUtils.isNotEmpty(request.getTopicPerm()), "topic perm is null");
+ aclService.addOrUpdateAclTopicConfig(request);
+ return true;
+ }
+
+ @PostMapping("/group/add.do")
+ public Object addAclGroupConfig(@RequestBody AclRequest request) {
+ Preconditions.checkArgument(StringUtils.isNotEmpty(request.getConfig().getAccessKey()), "accessKey is null");
+ Preconditions.checkArgument(StringUtils.isNotEmpty(request.getConfig().getSecretKey()), "secretKey is null");
+ Preconditions.checkArgument(CollectionUtils.isNotEmpty(request.getConfig().getGroupPerms()), "group perms is null");
+ Preconditions.checkArgument(StringUtils.isNotEmpty(request.getGroupPerm()), "group perm is null");
+ aclService.addOrUpdateAclGroupConfig(request);
+ return true;
+ }
+
+ @PostMapping("/perm/delete.do")
+ public Object deletePermConfig(@RequestBody AclRequest request) {
+ Preconditions.checkArgument(StringUtils.isNotEmpty(request.getConfig().getAccessKey()), "accessKey is null");
+ Preconditions.checkArgument(StringUtils.isNotEmpty(request.getConfig().getSecretKey()), "secretKey is null");
+ aclService.deletePermConfig(request);
+ return true;
+ }
+
+ @PostMapping("/sync.do")
+ public Object syncConfig(@RequestBody PlainAccessConfig config) {
+ Preconditions.checkArgument(StringUtils.isNotEmpty(config.getAccessKey()), "accessKey is null");
+ Preconditions.checkArgument(StringUtils.isNotEmpty(config.getSecretKey()), "secretKey is null");
+ aclService.syncData(config);
+ return true;
+ }
+
+ @PostMapping("/white/list/add.do")
+ public Object addWhiteList(@RequestBody List<String> whiteList) {
+ Preconditions.checkArgument(CollectionUtils.isNotEmpty(whiteList), "white list is null");
+ aclService.addWhiteList(whiteList);
+ return true;
+ }
+
+ @DeleteMapping("/white/list/delete.do")
+ public Object deleteWhiteAddr(@RequestParam String request) {
+ aclService.deleteWhiteAddr(request);
+ return true;
+ }
+
+ @PostMapping("/white/list/sync.do")
+ public Object synchronizeWhiteList(@RequestBody List<String> whiteList) {
+ Preconditions.checkArgument(CollectionUtils.isNotEmpty(whiteList), "white list is null");
+ aclService.synchronizeWhiteList(whiteList);
+ return true;
+ }
+}
diff --git a/src/main/java/org/apache/rocketmq/dashboard/model/request/AclRequest.java b/src/main/java/org/apache/rocketmq/dashboard/model/request/AclRequest.java
new file mode 100644
index 0000000..8f11e7b
--- /dev/null
+++ b/src/main/java/org/apache/rocketmq/dashboard/model/request/AclRequest.java
@@ -0,0 +1,30 @@
+/*
+ * 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.dashboard.model.request;
+
+import lombok.Data;
+import org.apache.rocketmq.common.PlainAccessConfig;
+
+@Data
+public class AclRequest {
+
+ private PlainAccessConfig config;
+
+ private String topicPerm;
+
+ private String groupPerm;
+}
diff --git a/src/main/java/org/apache/rocketmq/dashboard/service/AclService.java b/src/main/java/org/apache/rocketmq/dashboard/service/AclService.java
new file mode 100644
index 0000000..96b6e06
--- /dev/null
+++ b/src/main/java/org/apache/rocketmq/dashboard/service/AclService.java
@@ -0,0 +1,47 @@
+/*
+ * 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.dashboard.service;
+
+import java.util.List;
+import org.apache.rocketmq.common.AclConfig;
+import org.apache.rocketmq.common.PlainAccessConfig;
+import org.apache.rocketmq.dashboard.model.request.AclRequest;
+
+public interface AclService {
+
+ AclConfig getAclConfig(boolean excludeSecretKey);
+
+ void addAclConfig(PlainAccessConfig config);
+
+ void deleteAclConfig(PlainAccessConfig config);
+
+ void updateAclConfig(PlainAccessConfig config);
+
+ void addOrUpdateAclTopicConfig(AclRequest request);
+
+ void addOrUpdateAclGroupConfig(AclRequest request);
+
+ void deletePermConfig(AclRequest request);
+
+ void syncData(PlainAccessConfig config);
+
+ void addWhiteList(List<String> whiteList);
+
+ void deleteWhiteAddr(String addr);
+
+ void synchronizeWhiteList(List<String> whiteList);
+}
diff --git a/src/main/java/org/apache/rocketmq/dashboard/service/client/MQAdminExtImpl.java b/src/main/java/org/apache/rocketmq/dashboard/service/client/MQAdminExtImpl.java
index 5b76f99..6788522 100644
--- a/src/main/java/org/apache/rocketmq/dashboard/service/client/MQAdminExtImpl.java
+++ b/src/main/java/org/apache/rocketmq/dashboard/service/client/MQAdminExtImpl.java
@@ -91,29 +91,34 @@
MQAdminInstance.threadLocalMQAdminExt().createAndUpdateTopicConfig(addr, config);
}
- @Override public void createAndUpdatePlainAccessConfig(String addr,
+ @Override
+ public void createAndUpdatePlainAccessConfig(String addr,
PlainAccessConfig plainAccessConfig) throws RemotingException, MQBrokerException, InterruptedException, MQClientException {
-
+ MQAdminInstance.threadLocalMQAdminExt().createAndUpdatePlainAccessConfig(addr, plainAccessConfig);
}
- @Override public void deletePlainAccessConfig(String addr,
+ @Override
+ public void deletePlainAccessConfig(String addr,
String accessKey) throws RemotingException, MQBrokerException, InterruptedException, MQClientException {
-
+ MQAdminInstance.threadLocalMQAdminExt().deletePlainAccessConfig(addr, accessKey);
}
- @Override public void updateGlobalWhiteAddrConfig(String addr,
+ @Override
+ public void updateGlobalWhiteAddrConfig(String addr,
String globalWhiteAddrs) throws RemotingException, MQBrokerException, InterruptedException, MQClientException {
-
+ MQAdminInstance.threadLocalMQAdminExt().updateGlobalWhiteAddrConfig(addr, globalWhiteAddrs);
}
- @Override public ClusterAclVersionInfo examineBrokerClusterAclVersionInfo(
+ @Override
+ public ClusterAclVersionInfo examineBrokerClusterAclVersionInfo(
String addr) throws RemotingException, MQBrokerException, InterruptedException, MQClientException {
return null;
}
- @Override public AclConfig examineBrokerClusterAclConfig(
+ @Override
+ public AclConfig examineBrokerClusterAclConfig(
String addr) throws RemotingException, MQBrokerException, InterruptedException, MQClientException {
- return null;
+ return MQAdminInstance.threadLocalMQAdminExt().examineBrokerClusterAclConfig(addr);
}
@Override
diff --git a/src/main/java/org/apache/rocketmq/dashboard/service/impl/AclServiceImpl.java b/src/main/java/org/apache/rocketmq/dashboard/service/impl/AclServiceImpl.java
new file mode 100644
index 0000000..1e7e294
--- /dev/null
+++ b/src/main/java/org/apache/rocketmq/dashboard/service/impl/AclServiceImpl.java
@@ -0,0 +1,359 @@
+/*
+ * 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.dashboard.service.impl;
+
+import com.google.common.base.Throwables;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.rocketmq.client.exception.MQBrokerException;
+import org.apache.rocketmq.client.exception.MQClientException;
+import org.apache.rocketmq.common.AclConfig;
+import org.apache.rocketmq.common.MixAll;
+import org.apache.rocketmq.common.PlainAccessConfig;
+import org.apache.rocketmq.common.protocol.body.ClusterInfo;
+import org.apache.rocketmq.common.protocol.route.BrokerData;
+import org.apache.rocketmq.dashboard.model.request.AclRequest;
+import org.apache.rocketmq.dashboard.service.AbstractCommonService;
+import org.apache.rocketmq.dashboard.service.AclService;
+import org.apache.rocketmq.remoting.exception.RemotingConnectException;
+import org.apache.rocketmq.remoting.exception.RemotingException;
+import org.apache.rocketmq.remoting.exception.RemotingSendRequestException;
+import org.apache.rocketmq.remoting.exception.RemotingTimeoutException;
+import org.springframework.stereotype.Service;
+
+@Service
+@Slf4j
+public class AclServiceImpl extends AbstractCommonService implements AclService {
+
+ @Override
+ public AclConfig getAclConfig(boolean excludeSecretKey) {
+ try {
+ Optional<String> addr = getMasterSet().stream().findFirst();
+ if (addr.isPresent()) {
+ if (!excludeSecretKey) {
+ return mqAdminExt.examineBrokerClusterAclConfig(addr.get());
+ } else {
+ AclConfig aclConfig = mqAdminExt.examineBrokerClusterAclConfig(addr.get());
+ if (CollectionUtils.isNotEmpty(aclConfig.getPlainAccessConfigs())) {
+ aclConfig.getPlainAccessConfigs().forEach(pac -> pac.setSecretKey(null));
+ }
+ return aclConfig;
+ }
+ }
+ } catch (Exception e) {
+ log.error("getAclConfig error.", e);
+ throw Throwables.propagate(e);
+ }
+ AclConfig aclConfig = new AclConfig();
+ aclConfig.setGlobalWhiteAddrs(Collections.emptyList());
+ aclConfig.setPlainAccessConfigs(Collections.emptyList());
+ return aclConfig;
+ }
+
+ @Override
+ public void addAclConfig(PlainAccessConfig config) {
+ try {
+ Set<String> masterSet = getMasterSet();
+
+ if (masterSet.isEmpty()) {
+ throw new IllegalStateException("broker addr list is empty");
+ }
+ // check to see if account is exists
+ for (String addr : masterSet) {
+ AclConfig aclConfig = mqAdminExt.examineBrokerClusterAclConfig(addr);
+ List<PlainAccessConfig> plainAccessConfigs = aclConfig.getPlainAccessConfigs();
+ for (PlainAccessConfig pac : plainAccessConfigs) {
+ if (pac.getAccessKey().equals(config.getAccessKey())) {
+ throw new IllegalArgumentException(String.format("broker: %s, exist accessKey: %s", addr, config.getAccessKey()));
+ }
+ }
+ }
+
+ // all broker
+ for (String addr : getBrokerAddrs()) {
+ mqAdminExt.createAndUpdatePlainAccessConfig(addr, config);
+ }
+ } catch (Exception e) {
+ throw Throwables.propagate(e);
+ }
+
+ }
+
+ @Override
+ public void deleteAclConfig(PlainAccessConfig config) {
+ try {
+ for (String addr : getBrokerAddrs()) {
+ log.info("Start to delete acl [{}] from broker [{}]", config.getAccessKey(), addr);
+ if (isExistAccessKey(config.getAccessKey(), addr)) {
+ mqAdminExt.deletePlainAccessConfig(addr, config.getAccessKey());
+ }
+ log.info("Delete acl [{}] from broker [{}] complete", config.getAccessKey(), addr);
+ }
+ } catch (Exception e) {
+ throw Throwables.propagate(e);
+ }
+ }
+
+ @Override
+ public void updateAclConfig(PlainAccessConfig config) {
+ try {
+ for (String addr : getBrokerAddrs()) {
+ AclConfig aclConfig = mqAdminExt.examineBrokerClusterAclConfig(addr);
+ if (aclConfig.getPlainAccessConfigs() != null) {
+ PlainAccessConfig remoteConfig = null;
+ for (PlainAccessConfig pac : aclConfig.getPlainAccessConfigs()) {
+ if (pac.getAccessKey().equals(config.getAccessKey())) {
+ remoteConfig = pac;
+ break;
+ }
+ }
+ if (remoteConfig != null) {
+ remoteConfig.setSecretKey(config.getSecretKey());
+ remoteConfig.setAdmin(config.isAdmin());
+ config = remoteConfig;
+ }
+ }
+ mqAdminExt.createAndUpdatePlainAccessConfig(addr, config);
+ }
+ } catch (Exception e) {
+ throw Throwables.propagate(e);
+ }
+ }
+
+ @Override
+ public void addOrUpdateAclTopicConfig(AclRequest request) {
+ try {
+ PlainAccessConfig addConfig = request.getConfig();
+ for (String addr : getBrokerAddrs()) {
+ AclConfig aclConfig = mqAdminExt.examineBrokerClusterAclConfig(addr);
+ PlainAccessConfig remoteConfig = null;
+ if (aclConfig.getPlainAccessConfigs() != null) {
+ for (PlainAccessConfig config : aclConfig.getPlainAccessConfigs()) {
+ if (config.getAccessKey().equals(addConfig.getAccessKey())) {
+ remoteConfig = config;
+ break;
+ }
+ }
+ }
+ if (remoteConfig == null) {
+ // Maybe the broker no acl config of the access key, therefore add it;
+ mqAdminExt.createAndUpdatePlainAccessConfig(addr, addConfig);
+ } else {
+ if (remoteConfig.getTopicPerms() == null) {
+ remoteConfig.setTopicPerms(new ArrayList<>());
+ }
+ removeExist(remoteConfig.getTopicPerms(), request.getTopicPerm().split("=")[0]);
+ remoteConfig.getTopicPerms().add(request.getTopicPerm());
+ mqAdminExt.createAndUpdatePlainAccessConfig(addr, remoteConfig);
+ }
+ }
+ } catch (Exception e) {
+ throw Throwables.propagate(e);
+ }
+ }
+
+ @Override
+ public void addOrUpdateAclGroupConfig(AclRequest request) {
+ try {
+ PlainAccessConfig addConfig = request.getConfig();
+ for (String addr : getBrokerAddrs()) {
+ AclConfig aclConfig = mqAdminExt.examineBrokerClusterAclConfig(addr);
+ PlainAccessConfig remoteConfig = null;
+ if (aclConfig.getPlainAccessConfigs() != null) {
+ for (PlainAccessConfig config : aclConfig.getPlainAccessConfigs()) {
+ if (config.getAccessKey().equals(addConfig.getAccessKey())) {
+ remoteConfig = config;
+ break;
+ }
+ }
+ }
+ if (remoteConfig == null) {
+ // May be the broker no acl config of the access key, therefore add it;
+ mqAdminExt.createAndUpdatePlainAccessConfig(addr, addConfig);
+ } else {
+ if (remoteConfig.getGroupPerms() == null) {
+ remoteConfig.setGroupPerms(new ArrayList<>());
+ }
+ removeExist(remoteConfig.getGroupPerms(), request.getGroupPerm().split("=")[0]);
+ remoteConfig.getGroupPerms().add(request.getGroupPerm());
+ mqAdminExt.createAndUpdatePlainAccessConfig(addr, remoteConfig);
+ }
+ }
+ } catch (Exception e) {
+ throw Throwables.propagate(e);
+ }
+ }
+
+ @Override
+ public void deletePermConfig(AclRequest request) {
+ try {
+ PlainAccessConfig deleteConfig = request.getConfig();
+
+ String topic = StringUtils.isNotEmpty(request.getTopicPerm()) ? request.getTopicPerm().split("=")[0] : null;
+ String group = StringUtils.isNotEmpty(request.getGroupPerm()) ? request.getGroupPerm().split("=")[0] : null;
+ if (deleteConfig.getTopicPerms() != null && topic != null) {
+ removeExist(deleteConfig.getTopicPerms(), topic);
+ }
+ if (deleteConfig.getGroupPerms() != null && group != null) {
+ removeExist(deleteConfig.getGroupPerms(), group);
+ }
+
+ for (String addr : getBrokerAddrs()) {
+ AclConfig aclConfig = mqAdminExt.examineBrokerClusterAclConfig(addr);
+ PlainAccessConfig remoteConfig = null;
+ if (aclConfig.getPlainAccessConfigs() != null) {
+ for (PlainAccessConfig config : aclConfig.getPlainAccessConfigs()) {
+ if (config.getAccessKey().equals(deleteConfig.getAccessKey())) {
+ remoteConfig = config;
+ break;
+ }
+ }
+ }
+ if (remoteConfig == null) {
+ // Maybe the broker no acl config of the access key, therefore add it;
+ mqAdminExt.createAndUpdatePlainAccessConfig(addr, deleteConfig);
+ } else {
+ if (remoteConfig.getTopicPerms() != null && topic != null) {
+ removeExist(remoteConfig.getTopicPerms(), topic);
+ }
+ if (remoteConfig.getGroupPerms() != null && group != null) {
+ removeExist(remoteConfig.getGroupPerms(), group);
+ }
+ mqAdminExt.createAndUpdatePlainAccessConfig(addr, remoteConfig);
+ }
+ }
+ } catch (Exception e) {
+ throw Throwables.propagate(e);
+ }
+
+ }
+
+ @Override
+ public void syncData(PlainAccessConfig config) {
+ try {
+ for (String addr : getBrokerAddrs()) {
+ mqAdminExt.createAndUpdatePlainAccessConfig(addr, config);
+ }
+ } catch (Exception e) {
+ throw Throwables.propagate(e);
+ }
+ }
+
+ @Override
+ public void addWhiteList(List<String> whiteList) {
+ if (whiteList == null) {
+ return;
+ }
+ try {
+ for (String addr : getBrokerAddrs()) {
+ AclConfig aclConfig = mqAdminExt.examineBrokerClusterAclConfig(addr);
+ if (aclConfig.getGlobalWhiteAddrs() != null) {
+ aclConfig.setGlobalWhiteAddrs(Stream.of(whiteList, aclConfig.getGlobalWhiteAddrs()).flatMap(Collection::stream).distinct().collect(Collectors.toList()));
+ } else {
+ aclConfig.setGlobalWhiteAddrs(whiteList);
+ }
+ mqAdminExt.updateGlobalWhiteAddrConfig(addr, StringUtils.join(aclConfig.getGlobalWhiteAddrs(), ","));
+ }
+ } catch (Exception e) {
+ throw Throwables.propagate(e);
+ }
+ }
+
+ @Override
+ public void deleteWhiteAddr(String deleteAddr) {
+ try {
+ for (String addr : getBrokerAddrs()) {
+ AclConfig aclConfig = mqAdminExt.examineBrokerClusterAclConfig(addr);
+ if (aclConfig.getGlobalWhiteAddrs() == null || aclConfig.getGlobalWhiteAddrs().isEmpty()) {
+ continue;
+ }
+ aclConfig.getGlobalWhiteAddrs().remove(deleteAddr);
+ mqAdminExt.updateGlobalWhiteAddrConfig(addr, StringUtils.join(aclConfig.getGlobalWhiteAddrs(), ","));
+ }
+ } catch (Exception e) {
+ throw Throwables.propagate(e);
+ }
+ }
+
+ @Override
+ public void synchronizeWhiteList(List<String> whiteList) {
+ if (whiteList == null) {
+ return;
+ }
+ try {
+ for (String addr : getBrokerAddrs()) {
+ mqAdminExt.updateGlobalWhiteAddrConfig(addr, StringUtils.join(whiteList, ","));
+ }
+ } catch (Exception e) {
+ throw Throwables.propagate(e);
+ }
+ }
+
+ private void removeExist(List<String> list, String name) {
+ Iterator<String> iterator = list.iterator();
+ while (iterator.hasNext()) {
+ String v = iterator.next();
+ String cmp = v.split("=")[0];
+ if (cmp.equals(name)) {
+ iterator.remove();
+ }
+ }
+ }
+
+ private boolean isExistAccessKey(String accessKey,
+ String addr) throws InterruptedException, RemotingException, MQClientException, MQBrokerException {
+ AclConfig aclConfig = mqAdminExt.examineBrokerClusterAclConfig(addr);
+ List<PlainAccessConfig> plainAccessConfigs = aclConfig.getPlainAccessConfigs();
+ if (plainAccessConfigs == null || plainAccessConfigs.isEmpty()) {
+ return false;
+ }
+ for (PlainAccessConfig config : plainAccessConfigs) {
+ if (accessKey.equals(config.getAccessKey())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private Set<BrokerData> getBrokerDataSet() throws InterruptedException, RemotingConnectException, RemotingTimeoutException, RemotingSendRequestException, MQBrokerException {
+ ClusterInfo clusterInfo = mqAdminExt.examineBrokerClusterInfo();
+ Map<String, BrokerData> brokerDataMap = clusterInfo.getBrokerAddrTable();
+ return new HashSet<>(brokerDataMap.values());
+ }
+
+ private Set<String> getMasterSet() throws InterruptedException, MQBrokerException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException {
+ return getBrokerDataSet().stream().map(data -> data.getBrokerAddrs().get(MixAll.MASTER_ID)).collect(Collectors.toSet());
+ }
+
+ private Set<String> getBrokerAddrs() throws InterruptedException, MQBrokerException, RemotingTimeoutException, RemotingSendRequestException, RemotingConnectException {
+ Set<String> brokerAddrs = new HashSet<>();
+ getBrokerDataSet().forEach(data -> brokerAddrs.addAll(data.getBrokerAddrs().values()));
+ return brokerAddrs;
+ }
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 02d11e2..0ab405e 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -58,8 +58,8 @@
loginRequired: false
useTLS: false
# set the accessKey and secretKey if you used acl
- accessKey: # if version > 4.4.0
- secretKey: # if version > 4.4.0
+ accessKey: # if version > 4.4.0
+ secretKey: # if version > 4.4.0
threadpool:
config:
diff --git a/src/main/resources/role-permission.yml b/src/main/resources/role-permission.yml
index f95fa2f..9676b39 100644
--- a/src/main/resources/role-permission.yml
+++ b/src/main/resources/role-permission.yml
@@ -37,3 +37,4 @@
- /dlqMessage/*.query
- /dlqMessage/exportDlqMessage.do
- /dlqMessage/batchResendDlqMessage.do
+ - /acl/*.query
diff --git a/src/main/resources/static/index.html b/src/main/resources/static/index.html
index f212527..c2bf349 100644
--- a/src/main/resources/static/index.html
+++ b/src/main/resources/static/index.html
@@ -113,5 +113,6 @@
<script type="text/javascript" src="src/remoteApi/remoteApi.js"></script>
<script type="text/javascript" src="vendor/preLoading/main.js"></script>
<script type="text/javascript" src="src/login.js"></script>
+<script type="text/javascript" src="src/acl.js"></script>
</body>
</html>
diff --git a/src/main/resources/static/src/acl.js b/src/main/resources/static/src/acl.js
new file mode 100644
index 0000000..879412e
--- /dev/null
+++ b/src/main/resources/static/src/acl.js
@@ -0,0 +1,540 @@
+/*
+ * 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.
+ */
+
+var module = app;
+
+module.controller('aclController', ['$scope', 'ngDialog', '$http', 'Notification', '$window', function ($scope, ngDialog, $http, Notification, $window) {
+ $scope.paginationConf = {
+ currentPage: 1,
+ totalItems: 0,
+ itemsPerPage: 10,
+ pagesLength: 15,
+ perPageOptions: [10],
+ rememberPerPage: 'perPageItems',
+ onChange: function () {
+ $scope.showPlainAccessConfigs(this.currentPage, this.totalItems);
+ }
+ };
+
+ $scope.plainAccessConfigs = [];
+ $scope.allPlainAccessConfigs = [];
+ $scope.globalWhiteAddrs = [];
+ $scope.allGlobalWhiteAddrs = [];
+ $scope.userRole = $window.sessionStorage.getItem("userrole");
+ $scope.writeOperationEnabled = $scope.userRole == null ? true : ($scope.userRole == 1 ? true : false);
+ $scope.showSecretKeyType = {};
+
+ $scope.refreshPlainAccessConfigs = function () {
+ $http({
+ method: "GET",
+ url: "acl/config.query",
+ params: {}
+ }).success(function (resp) {
+
+ // globalWhiteAddrs
+ // plainAccessConfigs
+ if (resp.status == 0) {
+ $scope.allPlainAccessConfigs = resp.data.plainAccessConfigs;
+ $scope.allGlobalWhiteAddrs = resp.data.globalWhiteAddrs;
+ $scope.showSecretKeyType = {};
+ $scope.allPlainAccessConfigs.forEach(e => $scope.showSecretKeyType[e.accessKey] = {
+ type: 'password',
+ action: 'SHOW'
+ });
+ $scope.showPlainAccessConfigs(1, $scope.allPlainAccessConfigs.length);
+ } else {
+ Notification.error({message: resp.errMsg, delay: 2000});
+ }
+ });
+ }
+ $scope.refreshPlainAccessConfigs();
+ $scope.filterStr = "";
+ $scope.$watch('filterStr', function () {
+ $scope.paginationConf.currentPage = 1;
+ $scope.filterList(1);
+ });
+
+ $scope.filterList = function (currentPage) {
+ var lowExceptStr = $scope.filterStr.toLowerCase();
+ var canShowList = [];
+
+ $scope.allPlainAccessConfigs.forEach(function (element) {
+ if (element.accessKey.toLowerCase().indexOf(lowExceptStr) != -1) {
+ canShowList.push(element);
+ }
+ });
+ $scope.paginationConf.totalItems = canShowList.length;
+ var perPage = $scope.paginationConf.itemsPerPage;
+ var from = (currentPage - 1) * perPage;
+ var to = (from + perPage) > canShowList.length ? canShowList.length : from + perPage;
+ $scope.plainAccessConfigs = canShowList.slice(from, to);
+ };
+
+ $scope.showPlainAccessConfigs = function (currentPage, totalItem) {
+ var perPage = $scope.paginationConf.itemsPerPage;
+ var from = (currentPage - 1) * perPage;
+ var to = (from + perPage) > totalItem ? totalItem : from + perPage;
+ $scope.plainAccessConfigs = $scope.allPlainAccessConfigs.slice(from, to);
+ $scope.paginationConf.totalItems = totalItem;
+ $scope.filterList($scope.paginationConf.currentPage)
+ };
+
+
+ // add acl account
+ $scope.openAddDialog = function () {
+ var request = {};
+ request.accessKey = '';
+ request.secretKey = '';
+ request.admin = false;
+ request.defaultTopicPerm = 'DENY';
+ request.defaultGroupPerm = 'SUB';
+ ngDialog.open({
+ preCloseCallback: function (value) {
+ $scope.refreshPlainAccessConfigs();
+ },
+ template: 'addAclAccountDialog',
+ controller: 'addAclAccountDialogController',
+ data: request
+ });
+ }
+
+ $scope.deleteAclConfig = function (accessKey) {
+ $http({
+ method: "POST",
+ url: "acl/delete.do",
+ data: {accessKey: accessKey}
+ }).success(function (resp) {
+ if (resp.status == 0) {
+ Notification.info({message: "success!", delay: 2000});
+ $scope.refreshPlainAccessConfigs();
+ } else {
+ Notification.error({message: resp.errMsg, delay: 2000});
+ }
+ });
+ }
+ $scope.openUpdateDialog = function (request) {
+ ngDialog.open({
+ preCloseCallback: function (value) {
+ $scope.refreshPlainAccessConfigs();
+ },
+ template: 'updateAclAccountDialog',
+ controller: 'updateAclAccountDialogController',
+ data: request
+ });
+ }
+
+ // add acl topic permission
+ $scope.openAddTopicDialog = function (request) {
+ $.extend(request, {pub: true, sub: true, deny: false})
+ ngDialog.open({
+ preCloseCallback: function (value) {
+ $scope.refreshPlainAccessConfigs();
+ },
+ template: 'addAclTopicDialog',
+ controller: 'addAclTopicDialogController',
+ data: request
+ });
+ }
+
+ // update acl topic permission
+ $scope.openUpdateTopicDialog = function (request, topic) {
+ var perm = {pub: false, sub: false, deny: false};
+ var topicInfo = topic.split('=');
+ $.each(topicInfo[1].split('|'), function (i, e) {
+ switch (e) {
+ case 'PUB':
+ perm.pub = true;
+ break;
+ case 'SUB':
+ perm.sub = true;
+ break;
+ case 'DENY':
+ perm.deny = true;
+ break;
+ default:
+ break;
+ }
+ });
+
+ $.extend(request, perm, {topic: topicInfo[0]});
+ ngDialog.open({
+ preCloseCallback: function (value) {
+ $scope.refreshPlainAccessConfigs();
+ },
+ template: 'updateAclTopicDialog',
+ controller: 'updateAclTopicDialogController',
+ data: request
+ });
+ }
+
+ // add acl group permission
+ $scope.openAddGroupDialog = function (request) {
+ $.extend(request, {pub: true, sub: true, deny: false})
+ ngDialog.open({
+ preCloseCallback: function (value) {
+ $scope.refreshPlainAccessConfigs();
+ },
+ template: 'addAclGroupDialog',
+ controller: 'addAclGroupDialogController',
+ data: request
+ });
+ }
+
+ // update acl group permission
+ $scope.openUpdateGroupDialog = function (request, group) {
+ var perm = {pub: false, sub: false, deny: false};
+ var groupInfo = group.split('=');
+ $.each(groupInfo[1].split('|'), function (i, e) {
+ switch (e) {
+ case 'PUB':
+ perm.pub = true;
+ break;
+ case 'SUB':
+ perm.sub = true;
+ break;
+ case 'DENY':
+ perm.deny = true;
+ break;
+ default:
+ break;
+ }
+ });
+
+ $.extend(request, perm, {group: groupInfo[0]});
+ ngDialog.open({
+ preCloseCallback: function (value) {
+ $scope.refreshPlainAccessConfigs();
+ },
+ template: 'updateAclGroupDialog',
+ controller: 'updateAclGroupDialogController',
+ data: request
+ });
+ }
+
+ $scope.deletePermConfig = function (config, name, type) {
+ var request = {config: config};
+ switch (type) {
+ case 'topic':
+ request.topicPerm = name;
+ break;
+ case 'group':
+ request.groupPerm = name;
+ break;
+ default:
+ break;
+ }
+ $http({
+ method: "POST",
+ url: "acl/perm/delete.do",
+ data: request
+ }).success(function (resp) {
+ if (resp.status == 0) {
+ Notification.info({message: "success!", delay: 2000});
+ $scope.refreshPlainAccessConfigs();
+ } else {
+ Notification.error({message: resp.errMsg, delay: 2000});
+ }
+ });
+ }
+
+ $scope.synchronizeData = function (request) {
+ $http({
+ method: "POST",
+ url: "acl/sync.do",
+ data: request
+ }).success(function (resp) {
+ if (resp.status == 0) {
+ Notification.info({message: "success!", delay: 2000});
+ $scope.refreshPlainAccessConfigs();
+ } else {
+ Notification.error({message: resp.errMsg, delay: 2000});
+ }
+ });
+ }
+
+ $scope.openAddAddrDialog = function () {
+ ngDialog.open({
+ preCloseCallback: function (value) {
+ $scope.refreshPlainAccessConfigs();
+ },
+ template: 'addWhiteListDialog',
+ controller: 'addWhiteListDialogController'
+ });
+ }
+
+ $scope.deleteGlobalWhiteAddr = function (request) {
+ $http({
+ method: "DELETE",
+ url: "acl/white/list/delete.do?request=" + request
+ }).success(function (resp) {
+ if (resp.status == 0) {
+ Notification.info({message: "success!", delay: 2000});
+ $scope.refreshPlainAccessConfigs();
+ } else {
+ Notification.error({message: resp.errMsg, delay: 2000});
+ }
+ });
+ }
+
+ $scope.synchronizeWhiteList = function (request) {
+ $http({
+ method: "POST",
+ url: "acl/white/list/sync.do",
+ data: request
+ }).success(function (resp) {
+ if (resp.status == 0) {
+ Notification.info({message: "success!", delay: 2000});
+ $scope.refreshPlainAccessConfigs();
+ } else {
+ Notification.error({message: resp.errMsg, delay: 2000});
+ }
+ });
+ }
+
+ $scope.switchSecretKeyType = function (accessKey) {
+ if ($scope.showSecretKeyType[accessKey].type == 'password') {
+ $scope.showSecretKeyType[accessKey] = {type: 'text', action: 'HIDE'};
+ } else {
+ $scope.showSecretKeyType[accessKey] = {type: 'password', action: 'SHOW'};
+ }
+ }
+}]);
+
+module.controller('addAclAccountDialogController', ['$scope', 'ngDialog', '$http', 'Notification', function ($scope, ngDialog, $http, Notification) {
+ $scope.addRequest = function (requestItem) {
+ $http({
+ method: "POST",
+ url: "acl/add.do",
+ data: requestItem
+ }).success(function (resp) {
+ if (resp.status == 0) {
+ Notification.info({message: "success!", delay: 2000});
+ } else {
+ Notification.error({message: resp.errMsg, delay: 2000});
+ }
+ });
+ }
+ }]
+);
+
+module.controller('updateAclAccountDialogController', ['$scope', 'ngDialog', '$http', 'Notification', function ($scope, ngDialog, $http, Notification) {
+ $scope.updateAclAccountRequest = function (requestItem) {
+ $http({
+ method: "POST",
+ url: "acl/update.do",
+ data: requestItem
+ }).success(function (resp) {
+ if (resp.status == 0) {
+ Notification.info({message: "success!", delay: 2000});
+ } else {
+ Notification.error({message: resp.errMsg, delay: 2000});
+ }
+ });
+ }
+ }]
+);
+
+module.controller('addAclTopicDialogController', ['$scope', 'ngDialog', '$http', 'Notification', function ($scope, ngDialog, $http, Notification) {
+ $scope.updateAclAccountRequest = function (requestItem) {
+ if ((requestItem.deny && requestItem.sub) || (requestItem.deny && requestItem.pub)) {
+ alert("Forbid deny && pub/sub.");
+ return false;
+ }
+ if (!requestItem.topic) {
+ alert("topic is null");
+ return false;
+ }
+ //var request = requestItem.originalData;
+ var originalData = $.extend(true, {}, requestItem.originalData);
+ if (!originalData.topicPerms) {
+ originalData.topicPerms = new Array();
+ }
+ var topicPerm = concatPerm(requestItem.topic, requestItem.pub ? 0x01 : 0, requestItem.sub ? 0x02 : 0, requestItem.deny ? 0x04 : 0);
+ originalData.topicPerms.push(topicPerm);
+ var request = {topicPerm: topicPerm, config: originalData};
+ $http({
+ method: "POST",
+ url: "acl/topic/add.do",
+ data: request
+ }).success(function (resp) {
+ if (resp.status == 0) {
+ Notification.info({message: "success!", delay: 2000});
+ } else {
+ Notification.error({message: resp.errMsg, delay: 2000});
+ }
+ });
+ }
+ }]
+);
+
+module.controller('updateAclTopicDialogController', ['$scope', 'ngDialog', '$http', 'Notification', function ($scope, ngDialog, $http, Notification) {
+ $scope.updateAclAccountRequest = function (requestItem) {
+ if ((requestItem.deny && requestItem.sub) || (requestItem.deny && requestItem.pub)) {
+ alert("Forbid deny && pub/sub.");
+ return false;
+ }
+ var originalData = $.extend(true, {}, requestItem.originalData);
+ if (!originalData.topicPerms) {
+ originalData.topicPerms = new Array();
+ }
+ var topicPerm = concatPerm(requestItem.topic, requestItem.pub ? 0x01 : 0, requestItem.sub ? 0x02 : 0, requestItem.deny ? 0x04 : 0);
+
+ for (var i = 0; i < originalData.topicPerms.length; i++) {
+ if (originalData.topicPerms[i].split('=')[0] == requestItem.topic) {
+ originalData.topicPerms[i] = topicPerm;
+ }
+ }
+ var request = {topicPerm: topicPerm, config: originalData};
+ $http({
+ method: "POST",
+ url: "acl/topic/add.do",
+ data: request
+ }).success(function (resp) {
+ if (resp.status == 0) {
+ Notification.info({message: "success!", delay: 2000});
+ } else {
+ Notification.error({message: resp.errMsg, delay: 2000});
+ }
+ });
+ }
+ }]
+);
+
+module.controller('addAclGroupDialogController', ['$scope', 'ngDialog', '$http', 'Notification', function ($scope, ngDialog, $http, Notification) {
+ $scope.updateAclAccountRequest = function (requestItem) {
+ if ((requestItem.deny && requestItem.sub) || (requestItem.deny && requestItem.pub)) {
+ alert("Forbid deny && pub/sub.");
+ return false;
+ }
+ //var request = requestItem.originalData;
+ var originalData = $.extend(true, {}, requestItem.originalData);
+ if (!originalData.groupPerms) {
+ originalData.groupPerms = new Array();
+ }
+ var groupPerm = concatPerm(requestItem.group, requestItem.pub ? 0x01 : 0, requestItem.sub ? 0x02 : 0, requestItem.deny ? 0x04 : 0);
+ originalData.groupPerms.push(groupPerm);
+ var request = {groupPerm: groupPerm, config: originalData};
+ $http({
+ method: "POST",
+ url: "acl/group/add.do",
+ data: request
+ }).success(function (resp) {
+ if (resp.status == 0) {
+ Notification.info({message: "success!", delay: 2000});
+ } else {
+ Notification.error({message: resp.errMsg, delay: 2000});
+ }
+ });
+ }
+ }]
+);
+
+module.controller('updateAclGroupDialogController', ['$scope', 'ngDialog', '$http', 'Notification', function ($scope, ngDialog, $http, Notification) {
+ $scope.updateAclAccountRequest = function (requestItem) {
+ if ((requestItem.deny && requestItem.sub) || (requestItem.deny && requestItem.pub)) {
+ alert("Forbid deny && pub/sub.");
+ return false;
+ }
+ var originalData = $.extend(true, {}, requestItem.originalData);
+ if (!originalData.groupPerms) {
+ originalData.groupPerms = new Array();
+ }
+ var groupPerm = concatPerm(requestItem.group, requestItem.pub ? 0x01 : 0, requestItem.sub ? 0x02 : 0, requestItem.deny ? 0x04 : 0);
+
+ for (var i = 0; i < originalData.groupPerms.length; i++) {
+ if (originalData.groupPerms[i].split('=')[0] == requestItem.group) {
+ originalData.groupPerms[i] = groupPerm;
+ }
+ }
+ var request = {groupPerm: groupPerm, config: originalData};
+ $http({
+ method: "POST",
+ url: "acl/group/add.do",
+ data: request
+ }).success(function (resp) {
+ if (resp.status == 0) {
+ Notification.info({message: "success!", delay: 2000});
+ } else {
+ Notification.error({message: resp.errMsg, delay: 2000});
+ }
+ });
+ }
+ }]
+);
+
+/**
+ *
+ * pub: 0x01, sub: 0x02, deny: 0x04
+ */
+function concatPerm(name, pub, sub, deny) {
+ var perm = '';
+
+ switch (pub | sub | deny) {
+ case 0x01:
+ perm = 'PUB';
+ break;
+ case 0x02:
+ perm = 'SUB';
+ break;
+ case 0x03:
+ perm = 'PUB|SUB';
+ break;
+ case 0x04:
+ perm = 'DENY';
+ break;
+ default:
+ perm = 'DENY';
+ break;
+ }
+
+ return name + '=' + perm;
+}
+
+module.controller('addWhiteListDialogController', ['$scope', 'ngDialog', '$http', 'Notification', function ($scope, ngDialog, $http, Notification) {
+ $scope.addWhiteListRequest = function (requestItem) {
+ $http({
+ method: "POST",
+ url: "acl/white/list/add.do",
+ data: requestItem.split(',')
+ }).success(function (resp) {
+ if (resp.status == 0) {
+ Notification.info({message: "success!", delay: 2000});
+ } else {
+ Notification.error({message: resp.errMsg, delay: 2000});
+ }
+ });
+ }
+ }]
+);
+
+module.controller('aclBelongItemDialogController', ['$scope', 'ngDialog', '$http', 'Notification', function ($scope, ngDialog, $http, Notification) {
+ $scope.postBelongItemRequest = function (topicRequestItem) {
+ topicRequestItem.type = 1
+ $http({
+ method: "POST",
+ url: "acl/belong/item/add.do",
+ data: topicRequestItem
+ }).success(function (resp) {
+ if (resp.status == 0) {
+ Notification.info({message: "success!", delay: 2000});
+ } else {
+ Notification.error({message: resp.errMsg, delay: 2000});
+ }
+ });
+ }
+ }]
+);
\ No newline at end of file
diff --git a/src/main/resources/static/src/app.js b/src/main/resources/static/src/app.js
index 7fa68dc..a7ca1be 100644
--- a/src/main/resources/static/src/app.js
+++ b/src/main/resources/static/src/app.js
@@ -49,6 +49,15 @@
}
console.log('initFlag0='+ initFlag + ' loginFlag0==='+loginFlag);
+ $http({
+ method: "GET",
+ url: "acl/enable.query"
+ }).success(function (resp) {
+ if (resp && resp.status == 0) {
+ $rootScope.show = resp.data;
+ }
+ });
+
$rootScope.$on('$locationChangeStart', function (event, next, current) {
// redirect to login page if not logged in and trying to access a restricted page
init(function(resp){
@@ -204,6 +213,9 @@
}).when('/ops', {
templateUrl: 'view/pages/ops.html',
controller:'opsController'
+ }).when('/acl', {
+ templateUrl: 'view/pages/acl.html',
+ controller: 'aclController'
}).when('/404', {
templateUrl: 'view/pages/404.html'
}).otherwise('/404');
diff --git a/src/main/resources/static/src/i18n/en.js b/src/main/resources/static/src/i18n/en.js
index 9873429..f9a4e3c 100644
--- a/src/main/resources/static/src/i18n/en.js
+++ b/src/main/resources/static/src/i18n/en.js
@@ -113,5 +113,15 @@
"EXPORT": "export",
"NO_MATCH_RESULT": "no match result",
"BATCH_RESEND": "batchReSend",
- "BATCH_EXPORT": "batchExport"
+ "BATCH_EXPORT": "batchExport",
+ "WHITE_LIST":"White List",
+ "ACCOUNT_INFO":"Account Info",
+ "IS_ADMIN":"Is Admin",
+ "DEFAULT_TOPIC_PERM":"Default Topic Permission",
+ "DEFAULT_GROUP_PERM":"Default Group Permission",
+ "TOPIC_PERM":"Topic Permission",
+ "GROUP_PERM":"Group Permission",
+ "SYNCHRONIZE":"Synchronize Data",
+ "SHOW":"Show",
+ "HIDE":"Hide"
}
diff --git a/src/main/resources/static/src/i18n/zh.js b/src/main/resources/static/src/i18n/zh.js
index 9779d91..b6fa589 100644
--- a/src/main/resources/static/src/i18n/zh.js
+++ b/src/main/resources/static/src/i18n/zh.js
@@ -114,5 +114,15 @@
"EXPORT": "导出",
"NO_MATCH_RESULT": "没有查到符合条件的结果",
"BATCH_RESEND": "批量重发",
- "BATCH_EXPORT": "批量导出"
+ "BATCH_EXPORT": "批量导出",
+ "WHITE_LIST":"白名单",
+ "ACCOUNT_INFO":"账户信息",
+ "IS_ADMIN":"是否管理员",
+ "DEFAULT_TOPIC_PERM":"topic默认权限",
+ "DEFAULT_GROUP_PERM":"消费组默认权限",
+ "TOPIC_PERM":"topic权限",
+ "GROUP_PERM":"消费组权限",
+ "SYNCHRONIZE":"同步",
+ "SHOW":"显示",
+ "HIDE":"隐藏"
}
\ No newline at end of file
diff --git a/src/main/resources/static/style/app.css b/src/main/resources/static/style/app.css
index d9ac084..f58e5a4 100644
--- a/src/main/resources/static/style/app.css
+++ b/src/main/resources/static/style/app.css
@@ -289,3 +289,15 @@
.table.text-middle>tbody>tr>td,.table.text-middle>tbody>tr>th{
vertical-align: middle;
}
+
+.perm-table{width: 100%;}
+.perm-table .perm-tg{width: 70%;}
+.perm-table .center{border-left: 1px solid #e4dddd; border-right: 1px solid #e4dddd;}
+
+.input-none {
+ border: 0;
+ outline: none;
+ background-color: rgba(0, 0, 0, 0);
+ cursor: text !important;
+ width: 60%;
+}
\ No newline at end of file
diff --git a/src/main/resources/static/view/layout/_header.html b/src/main/resources/static/view/layout/_header.html
index 0309947..f448541 100644
--- a/src/main/resources/static/view/layout/_header.html
+++ b/src/main/resources/static/view/layout/_header.html
@@ -36,6 +36,7 @@
<li ng-class="path =='message' ? 'active':''"><a ng-href="#/message">{{'MESSAGE' | translate}}</a></li>
<li ng-class="path =='dlqMessage' ? 'active':''"><a ng-href="#/dlqMessage">{{'DLQ_MESSAGE' | translate}}</a></li>
<li ng-class="path =='messageTrace' ? 'active':''"><a ng-href="#/messageTrace">{{'MESSAGETRACE' | translate}}</a></li>
+ <li ng-show="{{ show }}" ng-class="path =='acl' ? 'active':''"><a ng-href="#/acl">Acl</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li class="dropdown">
diff --git a/src/main/resources/static/view/pages/acl.html b/src/main/resources/static/view/pages/acl.html
new file mode 100644
index 0000000..1b9827f
--- /dev/null
+++ b/src/main/resources/static/view/pages/acl.html
@@ -0,0 +1,483 @@
+<!--
+ ~ 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.
+ -->
+<div class="container-fluid" id="deployHistoryList">
+ <div class="modal-body">
+ <div ng-cloak="" class="tabsdemoDynamicHeight">
+ <md-content>
+ <md-tabs md-dynamic-height="" md-border-bottom="">
+ <md-tab label="Account Info">
+ <md-content class="md-padding" style="min-height:600px">
+ <form class="form-inline pull-left col-sm-12">
+ <div class="form-group">
+ <label>Access Key:</label>
+ <input class="form-control" style="width: 450px" type="text" ng-model="filterStr"/>
+ </div>
+
+ <button ng-show="writeOperationEnabled" class="btn btn-raised btn-sm btn-primary"
+ type="button"
+ ng-click="openAddDialog()">{{'ADD' |
+ translate}}
+ </button>
+ </form>
+ <table class="table table-bordered">
+ <tr>
+ <th class="text-center">Access Key</th>
+ <th ng-show="writeOperationEnabled" class="text-center">Secret Key</th>
+ <th class="text-center">{{'IS_ADMIN' | translate}}</th>
+ <th class="text-center">{{'DEFAULT_TOPIC_PERM' | translate}}</th>
+ <th class="text-center">{{'DEFAULT_GROUP_PERM' | translate}}</th>
+ <th class="text-center">{{'TOPIC_PERM' | translate}}</th>
+ <th class="text-center">{{'GROUP_PERM' | translate}}</th>
+ <th ng-show="writeOperationEnabled" class="text-center">
+ {{'OPERATION' | translate}}
+ </th>
+ </tr>
+ <tr ng-repeat="item in plainAccessConfigs">
+ <td class="text-center">{{item.accessKey}}</td>
+ <td ng-show="writeOperationEnabled" class="text-center">
+ <input type="{{showSecretKeyType[item.accessKey].type}}"
+ value="{{item.secretKey}}" class="input-none" ng-disabled="true"/>
+ <a href="javascript:;"
+ ng-click="switchSecretKeyType(item.accessKey)">{{showSecretKeyType[item.accessKey].action | translate}}</a>
+ </td>
+ <td class="text-center">{{item.admin}}</td>
+ <td class="text-center">{{item.defaultTopicPerm}}</td>
+ <td class="text-center">{{item.defaultGroupPerm}}</td>
+ <td class="text-center">
+ <table ng-repeat="topic in item.topicPerms" class="perm-table">
+ <tr>
+ <td class="perm-tg">{{topic}}</td>
+ <td ng-show="writeOperationEnabled" class="center"><a
+ href="javascript:;"
+ ng-click="openUpdateTopicDialog(item, topic)">
+ {{'UPDATE' | translate}}</a></td>
+ <td ng-show="writeOperationEnabled"><a href="javascript:;"
+ ng-confirm-click="Are you sure to delete {{topic}}?"
+ confirmed-click="deletePermConfig(item, topic, 'topic')">{{'DELETE' | translate}}</a>
+ </td>
+ </tr>
+ </table>
+ </td>
+ <td class="text-center">
+ <table ng-repeat="group in item.groupPerms" class="perm-table">
+ <tr>
+ <td class="perm-tg">{{group}}</td>
+ <td ng-show="writeOperationEnabled" class="center"><a
+ href="javascript:;"
+ ng-click="openUpdateGroupDialog(item, group)">
+ {{'UPDATE' | translate}}</a></td>
+ <td ng-show="writeOperationEnabled"><a href="javascript:;"
+ ng-confirm-click="Are you sure to delete {{group}}?"
+ confirmed-click="deletePermConfig(item, group, 'group')">{{'DELETE' | translate}}</a>
+ </td>
+ </tr>
+ </table>
+ </td>
+ <td ng-show="writeOperationEnabled" class="text-center">
+ <button class="btn btn-raised btn-sm btn-primary" type="button"
+ ng-click="openAddTopicDialog(item)">
+ {{'ADD' | translate}}topic
+ </button>
+ <button class="btn btn-raised btn-sm btn-primary" type="button"
+ ng-click="openAddGroupDialog(item)">
+ {{'ADD' | translate}}group
+ </button>
+ <button class="btn btn-raised btn-sm btn-primary" type="button"
+ ng-click="openUpdateDialog(item)">
+ {{'UPDATE' | translate}}
+ </button>
+ <button class="btn btn-raised btn-sm btn-danger" type="button"
+ ng-confirm-click="Are you sure to delete {{item.accessKey}}?"
+ confirmed-click="deleteAclConfig(item.accessKey)">{{'DELETE' | translate}}
+ </button>
+ <button class="btn btn-raised btn-sm btn-danger" type="button"
+ ng-click="synchronizeData(item)">{{'SYNCHRONIZE' | translate}}
+ </button>
+ </td>
+ </tr>
+ </table>
+ <tm-pagination conf="paginationConf"></tm-pagination>
+ </md-content>
+ </md-tab>
+ <md-tab label="Global White List">
+ <md-content class="md-padding" style="min-height:600px">
+ <form class="form-inline pull-left col-sm-12">
+ <button ng-show="writeOperationEnabled" class="btn btn-raised btn-sm btn-primary"
+ type="button"
+ ng-click="openAddAddrDialog()">{{'ADD' |
+ translate}}
+ </button>
+ <button ng-show="writeOperationEnabled" class="btn btn-raised btn-sm btn-danger"
+ type="button"
+ ng-confirm-click="Are you sure to synchronize white list to all broker int the cluster?"
+ confirmed-click="synchronizeWhiteList(allGlobalWhiteAddrs)">
+ {{'SYNCHRONIZE' | translate}}
+ </button>
+ </form>
+ <table class="table table-bordered">
+ <tr>
+ <th class="text-center">{{'WHITE_LIST' | translate}}</th>
+ <th ng-show="writeOperationEnabled" class="text-center">
+ {{'OPERATION' | translate}}
+ </th>
+ </tr>
+ <tr ng-repeat="item in allGlobalWhiteAddrs">
+ <td class="text-center">{{item}}
+ </td>
+ <td ng-show="writeOperationEnabled" class="text-center">
+ <button class="btn btn-raised btn-sm btn-danger" type="button"
+ ng-confirm-click="Are you sure to delete {{item}}?"
+ confirmed-click="deleteGlobalWhiteAddr(item)">{{'DELETE' | translate}}
+ </button>
+ </td>
+ </tr>
+ </table>
+ </md-content>
+ </md-tab>
+ </md-tabs>
+ </md-content>
+ </div>
+ </div>
+</div>
+
+<script type="text/ng-template" id="addAclAccountDialog">
+ <div class="modal-header">
+ <h4 class="modal-title">{{'ADD' | translate }}</h4>
+ </div>
+ <div class="modal-body ">
+ <form id="addAppForm" name="addAppForm" class="form-horizontal" novalidate>
+ <div class="form-group">
+ <label class="control-label col-sm-2">Access Key:</label>
+ <div class="col-sm-10">
+ <input class="form-control" ng-model="ngDialogData.accessKey" type="text" required/>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="control-label col-sm-2">Secret Key:</label>
+ <div class="col-sm-10">
+ <input class="form-control" ng-model="ngDialogData.secretKey" type="text" required/>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="control-label col-sm-2">{{'IS_ADMIN' | translate}}:</label>
+ <div class="col-sm-8">
+ <md-switch class="md-primary" md-no-ink aria-label="Switch No Ink"
+ ng-model="ngDialogData.admin">
+ </md-switch>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="control-label col-sm-2">{{'DEFAULT_TOPIC_PERM' | translate}}:</label>
+ <div class="col-sm-10">
+ <input class="form-control" ng-model="ngDialogData.defaultTopicPerm" type="text" readonly/>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="control-label col-sm-2">{{'DEFAULT_GROUP_PERM' | translate}}:</label>
+ <div class="col-sm-10">
+ <input class="form-control" ng-model="ngDialogData.defaultGroupPerm" type="text" readonly/>
+ </div>
+ </div>
+ </form>
+ <div class="modal-footer">
+ <div class="ngdialog-buttons">
+ <button type="button" class="ngdialog-button ngdialog-button-primary"
+ ng-click="addRequest({'accessKey':ngDialogData.accessKey, 'secretKey': ngDialogData.secretKey, 'admin': ngDialogData.admin, 'defaultTopicPerm': ngDialogData.defaultTopicPerm, 'defaultGroupPerm': ngDialogData.defaultGroupPerm})">
+ {{ 'COMMIT' | translate }}
+ </button>
+ <button type="button" class="ngdialog-button ngdialog-button-secondary"
+ ng-click="closeThisDialog('Cancel')">{{ 'CLOSE' | translate }}
+ </button>
+ </div>
+ </div>
+ </div>
+
+</script>
+
+<script type="text/ng-template" id="updateAclAccountDialog">
+ <div class="modal-header">
+ <h4 class="modal-title">{{'UPDATE' | translate }}</h4>
+ </div>
+ <div class="modal-body ">
+ <form id="updateAccountForm" name="updateAccountForm" class="form-horizontal" novalidate>
+ <div class="form-group">
+ <label class="control-label col-sm-2">Access Key:</label>
+ <div class="col-sm-10">
+ <input class="form-control" ng-model="ngDialogData.accessKey" type="text" disabled/>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="control-label col-sm-2">Secret Key:</label>
+ <div class="col-sm-10">
+ <input class="form-control" ng-model="ngDialogData.secretKey" type="text" required/>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="control-label col-sm-2">{{'IS_ADMIN' | translate}}:</label>
+ <div class="col-sm-8">
+ <md-switch class="md-primary" md-no-ink aria-label="Switch No Ink"
+ ng-model="ngDialogData.admin">
+ </md-switch>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="control-label col-sm-2">{{'DEFAULT_TOPIC_PERM' | translate}}:</label>
+ <div class="col-sm-10">
+ <input class="form-control" ng-model="ngDialogData.defaultTopicPerm" type="text" disabled/>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="control-label col-sm-2">{{'DEFAULT_GROUP_PERM' | translate}}:</label>
+ <div class="col-sm-10">
+ <input class="form-control" ng-model="ngDialogData.defaultGroupPerm" type="text" disabled/>
+ </div>
+ </div>
+ </form>
+ <div class="modal-footer">
+ <div class="ngdialog-buttons">
+ <button type="button" class="ngdialog-button ngdialog-button-primary"
+ ng-click="updateAclAccountRequest({'accessKey':ngDialogData.accessKey, 'secretKey': ngDialogData.secretKey, 'admin': ngDialogData.admin, 'defaultTopicPerm': ngDialogData.defaultTopicPerm, 'defaultGroupPerm': ngDialogData.defaultGroupPerm})">
+ {{ 'COMMIT' | translate }}
+ </button>
+ <button type="button" class="ngdialog-button ngdialog-button-secondary"
+ ng-click="closeThisDialog('Cancel')">{{ 'CLOSE' | translate }}
+ </button>
+ </div>
+ </div>
+ </div>
+
+</script>
+
+<script type="text/ng-template" id="addAclTopicDialog">
+ <div class="modal-header">
+ <h4 class="modal-title">{{'ADD' | translate }}{{'TOPIC_PERM' | translate }}</h4>
+ </div>
+ <div class="modal-body ">
+ <form id="addAclTopicForm" name="addAclTopicForm" class="form-horizontal" novalidate>
+ <div class="form-group">
+ <label class="control-label col-sm-2">Access Key:</label>
+ <div class="col-sm-10">
+ <input class="form-control" ng-model="ngDialogData.accessKey" type="text" disabled/>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="control-label col-sm-2">Secret Key:</label>
+ <div class="col-sm-10">
+ <input class="form-control" ng-model="ngDialogData.secretKey" type="text" disabled/>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="control-label col-sm-2">{{'TOPIC' | translate}}:</label>
+ <div class="col-sm-10">
+ <input class="form-control" ng-model="topic" type="text" required/>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="control-label col-sm-2">{{'TOPIC_PERM' | translate}}:</label>
+ <div class="col-sm-8">
+ <md-checkbox class="md-primary" ng-model="ngDialogData.pub">PUB</md-checkbox>
+ <md-checkbox class="md-primary" ng-model="ngDialogData.sub">SUB</md-checkbox>
+ <md-checkbox class="md-primary" ng-model="ngDialogData.deny">DENY</md-checkbox>
+ </div>
+ </div>
+ </form>
+ <div class="modal-footer">
+ <div class="ngdialog-buttons">
+ <button type="button" class="ngdialog-button ngdialog-button-primary"
+ ng-click="updateAclAccountRequest({'originalData':ngDialogData,'topic': topic , 'pub': ngDialogData.pub, 'sub': ngDialogData.sub, 'deny': ngDialogData.deny})">
+ {{ 'COMMIT' | translate }}
+ </button>
+ <button type="button" class="ngdialog-button ngdialog-button-secondary"
+ ng-click="closeThisDialog('Cancel')">{{ 'CLOSE' | translate }}
+ </button>
+ </div>
+ </div>
+ </div>
+
+</script>
+
+<script type="text/ng-template" id="updateAclTopicDialog">
+ <div class="modal-header">
+ <h4 class="modal-title">{{'UPDATE' | translate }}{{'TOPIC_PERM' | translate }}</h4>
+ </div>
+ <div class="modal-body ">
+ <form id="updateAclTopicForm" name="updateAclTopicForm" class="form-horizontal" novalidate>
+ <div class="form-group">
+ <label class="control-label col-sm-2">Access Key:</label>
+ <div class="col-sm-10">
+ <input class="form-control" ng-model="ngDialogData.accessKey" type="text" disabled/>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="control-label col-sm-2">Secret Key:</label>
+ <div class="col-sm-10">
+ <input class="form-control" ng-model="ngDialogData.secretKey" type="text" disabled/>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="control-label col-sm-2">{{'TOPIC' | translate}}:</label>
+ <div class="col-sm-10">
+ <input class="form-control" ng-model="ngDialogData.topic" type="text" disabled/>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="control-label col-sm-2">{{'TOPIC_PERM' | translate}}:</label>
+ <div class="col-sm-8">
+ <md-checkbox class="md-primary" ng-model="ngDialogData.pub">PUB</md-checkbox>
+ <md-checkbox class="md-primary" ng-model="ngDialogData.sub">SUB</md-checkbox>
+ <md-checkbox class="md-primary" ng-model="ngDialogData.deny">DENY</md-checkbox>
+ </div>
+ </div>
+ </form>
+ <div class="modal-footer">
+ <div class="ngdialog-buttons">
+ <button type="button" class="ngdialog-button ngdialog-button-primary"
+ ng-click="updateAclAccountRequest({'originalData':ngDialogData,'topic': ngDialogData.topic , 'pub': ngDialogData.pub, 'sub': ngDialogData.sub, 'deny': ngDialogData.deny})">
+ {{ 'COMMIT' | translate }}
+ </button>
+ <button type="button" class="ngdialog-button ngdialog-button-secondary"
+ ng-click="closeThisDialog('Cancel')">{{ 'CLOSE' | translate }}
+ </button>
+ </div>
+ </div>
+ </div>
+
+</script>
+
+<script type="text/ng-template" id="addAclGroupDialog">
+ <div class="modal-header">
+ <h4 class="modal-title">{{'ADD' | translate }}{{'GROUP_PERM' | translate }}</h4>
+ </div>
+ <div class="modal-body ">
+ <form id="addAclGroupForm" name="addAclGroupForm" class="form-horizontal" novalidate>
+ <div class="form-group">
+ <label class="control-label col-sm-2">Access Key:</label>
+ <div class="col-sm-10">
+ <input class="form-control" ng-model="ngDialogData.accessKey" type="text" disabled/>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="control-label col-sm-2">Secret Key:</label>
+ <div class="col-sm-10">
+ <input class="form-control" ng-model="ngDialogData.secretKey" type="text" disabled/>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="control-label col-sm-2">{{'CONSUMER' | translate}}:</label>
+ <div class="col-sm-10">
+ <input class="form-control" ng-model="group" type="text" required/>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="control-label col-sm-2">{{'GROUP_PERM' | translate}}:</label>
+ <div class="col-sm-8">
+ <md-checkbox class="md-primary" ng-model="ngDialogData.pub">PUB</md-checkbox>
+ <md-checkbox class="md-primary" ng-model="ngDialogData.sub">SUB</md-checkbox>
+ <md-checkbox class="md-primary" ng-model="ngDialogData.deny">DENY</md-checkbox>
+ </div>
+ </div>
+ </form>
+ <div class="modal-footer">
+ <div class="ngdialog-buttons">
+ <button type="button" class="ngdialog-button ngdialog-button-primary"
+ ng-click="updateAclAccountRequest({'originalData':ngDialogData,'group': group , 'pub': ngDialogData.pub, 'sub': ngDialogData.sub, 'deny': ngDialogData.deny})">
+ {{ 'COMMIT' | translate }}
+ </button>
+ <button type="button" class="ngdialog-button ngdialog-button-secondary"
+ ng-click="closeThisDialog('Cancel')">{{ 'CLOSE' | translate }}
+ </button>
+ </div>
+ </div>
+ </div>
+
+</script>
+
+<script type="text/ng-template" id="updateAclGroupDialog">
+ <div class="modal-header">
+ <h4 class="modal-title">{{'UPDATE' | translate }}{{'GROUP_PERM' | translate }}</h4>
+ </div>
+ <div class="modal-body ">
+ <form id="updateAclGroupForm" name="updateAclGroupForm" class="form-horizontal" novalidate>
+ <div class="form-group">
+ <label class="control-label col-sm-2">Access Key:</label>
+ <div class="col-sm-10">
+ <input class="form-control" ng-model="ngDialogData.accessKey" type="text" disabled/>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="control-label col-sm-2">Secret Key:</label>
+ <div class="col-sm-10">
+ <input class="form-control" ng-model="ngDialogData.secretKey" type="text" disabled/>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="control-label col-sm-2">{{'CONSUMER' | translate}}:</label>
+ <div class="col-sm-10">
+ <input class="form-control" ng-model="ngDialogData.group" type="text" disabled/>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="control-label col-sm-2">{{'GROUP_PERM' | translate}}:</label>
+ <div class="col-sm-8">
+ <md-checkbox class="md-primary" ng-model="ngDialogData.pub">PUB</md-checkbox>
+ <md-checkbox class="md-primary" ng-model="ngDialogData.sub">SUB</md-checkbox>
+ <md-checkbox class="md-primary" ng-model="ngDialogData.deny">DENY</md-checkbox>
+ </div>
+ </div>
+ </form>
+ <div class="modal-footer">
+ <div class="ngdialog-buttons">
+ <button type="button" class="ngdialog-button ngdialog-button-primary"
+ ng-click="updateAclAccountRequest({'originalData':ngDialogData,'group': ngDialogData.group , 'pub': ngDialogData.pub, 'sub': ngDialogData.sub, 'deny': ngDialogData.deny})">
+ {{ 'COMMIT' | translate }}
+ </button>
+ <button type="button" class="ngdialog-button ngdialog-button-secondary"
+ ng-click="closeThisDialog('Cancel')">{{ 'CLOSE' | translate }}
+ </button>
+ </div>
+ </div>
+ </div>
+
+</script>
+
+<script type="text/ng-template" id="addWhiteListDialog">
+ <div class="modal-header">
+ <h4 class="modal-title">{{'ADD' | translate }}{{'WHITE_LIST' | translate }}</h4>
+ </div>
+ <div class="modal-body ">
+ <form id="addWhiteListForm" name="addWhiteListForm" class="form-horizontal" novalidate>
+ <div class="form-group">
+ <label class="control-label col-sm-2">{{'WHITE_LIST' | translate }}:</label>
+ <div class="col-sm-10">
+ <input class="form-control" ng-model="ip" type="text"/>
+ </div>
+ </div>
+ </form>
+ <div class="modal-footer">
+ <div class="ngdialog-buttons">
+ <button type="button" class="ngdialog-button ngdialog-button-primary"
+ ng-click="addWhiteListRequest(ip)">
+ {{ 'COMMIT' | translate }}
+ </button>
+ <button type="button" class="ngdialog-button ngdialog-button-secondary"
+ ng-click="closeThisDialog('Cancel')">{{ 'CLOSE' | translate }}
+ </button>
+ </div>
+ </div>
+ </div>
+
+</script>
\ No newline at end of file
diff --git a/src/test/java/org/apache/rocketmq/dashboard/controller/AclControllerTest.java b/src/test/java/org/apache/rocketmq/dashboard/controller/AclControllerTest.java
new file mode 100644
index 0000000..8899b84
--- /dev/null
+++ b/src/test/java/org/apache/rocketmq/dashboard/controller/AclControllerTest.java
@@ -0,0 +1,368 @@
+/*
+ * 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.dashboard.controller;
+
+import com.alibaba.fastjson.JSON;
+import com.google.common.collect.Lists;
+import java.util.List;
+import org.apache.rocketmq.common.AclConfig;
+import org.apache.rocketmq.common.PlainAccessConfig;
+import org.apache.rocketmq.common.protocol.body.ClusterInfo;
+import org.apache.rocketmq.dashboard.model.request.AclRequest;
+import org.apache.rocketmq.dashboard.service.impl.AclServiceImpl;
+import org.apache.rocketmq.dashboard.util.MockObjectUtil;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Spy;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.when;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+public class AclControllerTest extends BaseControllerTest {
+
+ @InjectMocks
+ private AclController aclController;
+
+ @Spy
+ private AclServiceImpl aclService;
+
+ @Before
+ public void init() throws Exception {
+ AclConfig aclConfig = MockObjectUtil.createAclConfig();
+ when(mqAdminExt.examineBrokerClusterAclConfig(anyString())).thenReturn(aclConfig);
+ ClusterInfo clusterInfo = MockObjectUtil.createClusterInfo();
+ when(mqAdminExt.examineBrokerClusterInfo()).thenReturn(clusterInfo);
+ doNothing().when(mqAdminExt).createAndUpdatePlainAccessConfig(anyString(), any(PlainAccessConfig.class));
+ doNothing().when(mqAdminExt).deletePlainAccessConfig(anyString(), anyString());
+ doNothing().when(mqAdminExt).updateGlobalWhiteAddrConfig(anyString(), anyString());
+ }
+
+ @Test
+ public void testIsEnableAcl() throws Exception {
+ final String url = "/acl/enable.query";
+ // 1. disable acl.
+ requestBuilder = MockMvcRequestBuilders.get(url);
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().isOk())
+ .andExpect(jsonPath("$.data").value(false));
+
+ // 2.enable acl.
+ super.mockRmqConfigure();
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().isOk())
+ .andExpect(jsonPath("$.data").value(true));
+ }
+
+ @Test
+ public void testGetAclConfig() throws Exception {
+ final String url = "/acl/config.query";
+
+ // 1. broker addr table is not empty.
+ ClusterInfo clusterInfo = MockObjectUtil.createClusterInfo();
+ when(mqAdminExt.examineBrokerClusterInfo()).thenReturn(clusterInfo);
+ requestBuilder = MockMvcRequestBuilders.get(url);
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().isOk())
+ .andExpect(jsonPath("$.data").isMap())
+ .andExpect(jsonPath("$.data.globalWhiteAddrs").isNotEmpty())
+ .andExpect(jsonPath("$.data.plainAccessConfigs").isNotEmpty())
+ .andExpect(jsonPath("$.data.plainAccessConfigs[0].secretKey").isNotEmpty());
+
+ // 2. broker addr table is empty.
+ clusterInfo.getBrokerAddrTable().clear();
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().isOk())
+ .andExpect(jsonPath("$.data").isMap())
+ .andExpect(jsonPath("$.data.globalWhiteAddrs").isEmpty())
+ .andExpect(jsonPath("$.data.plainAccessConfigs").isEmpty());
+
+ // 3. login required and user info is null.
+ when(configure.isLoginRequired()).thenReturn(true);
+ when(mqAdminExt.examineBrokerClusterInfo()).thenReturn(MockObjectUtil.createClusterInfo());
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().isOk())
+ .andExpect(jsonPath("$.data").isMap())
+ .andExpect(jsonPath("$.data.globalWhiteAddrs").isNotEmpty())
+ .andExpect(jsonPath("$.data.plainAccessConfigs").isNotEmpty())
+ .andExpect(jsonPath("$.data.plainAccessConfigs[0].secretKey").isEmpty());
+ // 4. login required, but user is not admin. emmmm, Mockito may can not mock static method.
+ }
+
+ @Test
+ public void testAddAclConfig() throws Exception {
+ final String url = "/acl/add.do";
+ PlainAccessConfig accessConfig = new PlainAccessConfig();
+ requestBuilder = MockMvcRequestBuilders.post(url);
+ requestBuilder.contentType(MediaType.APPLICATION_JSON_UTF8);
+
+ // 1. access key is null.
+ requestBuilder.content(JSON.toJSONString(accessConfig));
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().isOk())
+ .andExpect(jsonPath("$.status").value(-1))
+ .andExpect(jsonPath("$.errMsg").exists());
+
+ // 2. secret key is null.
+ accessConfig.setAccessKey("test-access-key");
+ requestBuilder.content(JSON.toJSONString(accessConfig));
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().isOk())
+ .andExpect(jsonPath("$.status").value(-1))
+ .andExpect(jsonPath("$.errMsg").exists());
+
+ ClusterInfo clusterInfo = MockObjectUtil.createClusterInfo();
+ when(mqAdminExt.examineBrokerClusterInfo()).thenReturn(clusterInfo);
+
+ // 3. add if the access key not exist.
+ accessConfig.setSecretKey("12345678");
+ requestBuilder.content(JSON.toJSONString(accessConfig));
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().isOk())
+ .andExpect(jsonPath("$.status").value(0));
+
+ // 4. add failed if the access key is existed.
+ accessConfig.setAccessKey("rocketmq2");
+ requestBuilder.content(JSON.toJSONString(accessConfig));
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().isOk())
+ .andExpect(jsonPath("$.status").value(-1))
+ .andExpect(jsonPath("$.errMsg").exists());
+
+ // 5. add failed if there is no alive broker.
+ clusterInfo.getBrokerAddrTable().clear();
+ accessConfig.setAccessKey("test-access-key");
+ requestBuilder.content(JSON.toJSONString(accessConfig));
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().isOk())
+ .andExpect(jsonPath("$.status").value(-1))
+ .andExpect(jsonPath("$.errMsg").exists());
+ }
+
+ @Test
+ public void testDeleteAclConfig() throws Exception {
+ final String url = "/acl/delete.do";
+ PlainAccessConfig accessConfig = new PlainAccessConfig();
+ requestBuilder = MockMvcRequestBuilders.post(url);
+ requestBuilder.contentType(MediaType.APPLICATION_JSON_UTF8);
+
+ // 1. access key is null.
+ requestBuilder.content(JSON.toJSONString(accessConfig));
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().isOk())
+ .andExpect(jsonPath("$.status").value(-1))
+ .andExpect(jsonPath("$.errMsg").exists());
+
+ // 2. access key is not null.
+ accessConfig.setAccessKey("rocketmq");
+ requestBuilder.content(JSON.toJSONString(accessConfig));
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().isOk())
+ .andExpect(jsonPath("$.status").value(0));
+ }
+
+ @Test
+ public void testUpdateAclConfig() throws Exception {
+ final String url = "/acl/update.do";
+ PlainAccessConfig accessConfig = new PlainAccessConfig();
+ requestBuilder = MockMvcRequestBuilders.post(url);
+ requestBuilder.contentType(MediaType.APPLICATION_JSON_UTF8);
+
+ // 1. secret key is null.
+ accessConfig.setAccessKey("rocketmq");
+ requestBuilder.content(JSON.toJSONString(accessConfig));
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().isOk())
+ .andExpect(jsonPath("$.status").value(-1))
+ .andExpect(jsonPath("$.errMsg").exists());
+
+ // 2. update.
+ accessConfig.setSecretKey("abcdefghjkl");
+ requestBuilder.content(JSON.toJSONString(accessConfig));
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().isOk())
+ .andExpect(jsonPath("$.status").value(0));
+ }
+
+ @Test
+ public void testAddAclTopicConfig() throws Exception {
+ final String url = "/acl/topic/add.do";
+ AclRequest request = new AclRequest();
+ request.setConfig(createDefaultPlainAccessConfig());
+
+ // 1. if not exist.
+ request.setTopicPerm("test_topic=PUB");
+ requestBuilder = MockMvcRequestBuilders.post(url);
+ requestBuilder.contentType(MediaType.APPLICATION_JSON_UTF8);
+ requestBuilder.content(JSON.toJSONString(request));
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().isOk())
+ .andExpect(jsonPath("$.status").value(0));
+
+ // 2. if exist.
+ request.setTopicPerm("topicA=PUB");
+ requestBuilder.content(JSON.toJSONString(request));
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().isOk())
+ .andExpect(jsonPath("$.status").value(0));
+
+ // 3. if access key not exist.
+ request.getConfig().setAccessKey("test_access_key123");
+ requestBuilder.content(JSON.toJSONString(request));
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().isOk())
+ .andExpect(jsonPath("$.status").value(0));
+ }
+
+ @Test
+ public void testAddAclGroupConfig() throws Exception {
+ final String url = "/acl/group/add.do";
+ AclRequest request = new AclRequest();
+ request.setConfig(createDefaultPlainAccessConfig());
+
+ // 1. if not exist.
+ request.setGroupPerm("test_consumer=PUB|SUB");
+ requestBuilder = MockMvcRequestBuilders.post(url);
+ requestBuilder.contentType(MediaType.APPLICATION_JSON_UTF8);
+ requestBuilder.content(JSON.toJSONString(request));
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().isOk())
+ .andExpect(jsonPath("$.status").value(0));
+
+ // 2. if exist.
+ request.setGroupPerm("groupA=PUB|SUB");
+ requestBuilder.content(JSON.toJSONString(request));
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().isOk())
+ .andExpect(jsonPath("$.status").value(0));
+
+ // 3. if access key not exist.
+ request.getConfig().setAccessKey("test_access_key123");
+ requestBuilder.content(JSON.toJSONString(request));
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().isOk())
+ .andExpect(jsonPath("$.status").value(0));
+ }
+
+ @Test
+ public void testDeletePermConfig() throws Exception {
+ final String url = "/acl/perm/delete.do";
+ AclRequest request = new AclRequest();
+ request.setConfig(createDefaultPlainAccessConfig());
+ requestBuilder = MockMvcRequestBuilders.post(url);
+ requestBuilder.contentType(MediaType.APPLICATION_JSON_UTF8);
+ requestBuilder.content(JSON.toJSONString(request));
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().isOk())
+ .andExpect(jsonPath("$.status").value(0));
+
+ // if access key not exist.
+ request.getConfig().setAccessKey("test_access_key123");
+ requestBuilder.content(JSON.toJSONString(request));
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().isOk())
+ .andExpect(jsonPath("$.status").value(0));
+ }
+
+ @Test
+ public void testSyncConfig() throws Exception {
+ final String url = "/acl/sync.do";
+ requestBuilder = MockMvcRequestBuilders.post(url);
+ requestBuilder.contentType(MediaType.APPLICATION_JSON_UTF8);
+ requestBuilder.content(JSON.toJSONString(createDefaultPlainAccessConfig()));
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().isOk())
+ .andExpect(jsonPath("$.status").value(0));
+ }
+
+ @Test
+ public void testAddWhiteList() throws Exception {
+ final String url = "/acl/white/list/add.do";
+ List<String> whiteList = Lists.newArrayList("192.168.0.1");
+
+ // 1. if global white list is not null.
+ requestBuilder = MockMvcRequestBuilders.post(url);
+ requestBuilder.contentType(MediaType.APPLICATION_JSON_UTF8);
+ requestBuilder.content(JSON.toJSONString(whiteList));
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().isOk())
+ .andExpect(jsonPath("$.status").value(0));
+
+ // 2. if global white list is null.
+ AclConfig aclConfig = MockObjectUtil.createAclConfig();
+ aclConfig.setGlobalWhiteAddrs(null);
+ when(mqAdminExt.examineBrokerClusterAclConfig(anyString())).thenReturn(aclConfig);
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().isOk())
+ .andExpect(jsonPath("$.status").value(0));
+ }
+
+ @Test
+ public void testDeleteWhiteAddr() throws Exception {
+ final String url = "/acl/white/list/delete.do";
+ requestBuilder = MockMvcRequestBuilders.delete(url);
+ requestBuilder.param("request", "localhost");
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().isOk())
+ .andExpect(jsonPath("$.status").value(0));
+ }
+
+ @Test
+ public void testSynchronizeWhiteList() throws Exception {
+ final String url = "/acl/white/list/sync.do";
+ List<String> whiteList = Lists.newArrayList();
+
+ // 1. if white list for syncing is empty.
+ requestBuilder = MockMvcRequestBuilders.post(url);
+ requestBuilder.contentType(MediaType.APPLICATION_JSON_UTF8);
+ requestBuilder.content(JSON.toJSONString(whiteList));
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().isOk())
+ .andExpect(jsonPath("$.status").value(-1))
+ .andExpect(jsonPath("$.errMsg").exists());
+
+ // 2. if white list for syncing is not empty.
+ whiteList.add("localhost");
+ requestBuilder.content(JSON.toJSONString(whiteList));
+ perform = mockMvc.perform(requestBuilder);
+ perform.andExpect(status().isOk())
+ .andExpect(jsonPath("$.status").value(0));
+ }
+
+ @Override protected Object getTestController() {
+ return aclController;
+ }
+
+ private PlainAccessConfig createDefaultPlainAccessConfig() {
+ PlainAccessConfig config = new PlainAccessConfig();
+ config.setAdmin(false);
+ config.setAccessKey("rocketmq");
+ config.setSecretKey("123456789");
+ config.setDefaultGroupPerm("SUB");
+ config.setDefaultTopicPerm("DENY");
+ config.setTopicPerms(Lists.newArrayList("topicA=DENY", "topicB=PUB|SUB"));
+ config.setGroupPerms(Lists.newArrayList("groupA=DENY", "groupB=PUB|SUB"));
+
+ return config;
+ }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/rocketmq/dashboard/util/MockObjectUtil.java b/src/test/java/org/apache/rocketmq/dashboard/util/MockObjectUtil.java
index 658a169..fe7ac23 100644
--- a/src/test/java/org/apache/rocketmq/dashboard/util/MockObjectUtil.java
+++ b/src/test/java/org/apache/rocketmq/dashboard/util/MockObjectUtil.java
@@ -16,8 +16,10 @@
*/
package org.apache.rocketmq.dashboard.util;
+import com.google.common.collect.Lists;
import java.net.InetSocketAddress;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -30,8 +32,10 @@
import org.apache.rocketmq.client.producer.LocalTransactionState;
import org.apache.rocketmq.client.trace.TraceConstants;
import org.apache.rocketmq.client.trace.TraceType;
+import org.apache.rocketmq.common.AclConfig;
import org.apache.rocketmq.common.DataVersion;
import org.apache.rocketmq.common.MixAll;
+import org.apache.rocketmq.common.PlainAccessConfig;
import org.apache.rocketmq.common.TopicConfig;
import org.apache.rocketmq.common.admin.ConsumeStats;
import org.apache.rocketmq.common.admin.OffsetWrapper;
@@ -59,6 +63,7 @@
import org.apache.rocketmq.common.subscription.SubscriptionGroupConfig;
import org.apache.rocketmq.dashboard.model.DlqMessageRequest;
import org.apache.rocketmq.remoting.protocol.LanguageCode;
+import org.checkerframework.checker.units.qual.A;
import static org.apache.rocketmq.common.protocol.heartbeat.ConsumeType.CONSUME_ACTIVELY;
@@ -311,4 +316,26 @@
}
return dlqMessages;
}
+
+ public static AclConfig createAclConfig() {
+ PlainAccessConfig adminConfig = new PlainAccessConfig();
+ adminConfig.setAdmin(true);
+ adminConfig.setAccessKey("rocketmq2");
+ adminConfig.setSecretKey("12345678");
+
+ PlainAccessConfig normalConfig = new PlainAccessConfig();
+ normalConfig.setAdmin(false);
+ normalConfig.setAccessKey("rocketmq");
+ normalConfig.setSecretKey("123456789");
+ normalConfig.setDefaultGroupPerm("SUB");
+ normalConfig.setDefaultTopicPerm("DENY");
+ normalConfig.setTopicPerms(Lists.newArrayList("topicA=DENY", "topicB=PUB|SUB"));
+ normalConfig.setGroupPerms(Lists.newArrayList("groupA=DENY", "groupB=PUB|SUB"));
+
+
+ AclConfig aclConfig = new AclConfig();
+ aclConfig.setPlainAccessConfigs(Lists.newArrayList(adminConfig, normalConfig));
+ aclConfig.setGlobalWhiteAddrs(Lists.newArrayList("localhost"));
+ return aclConfig;
+ }
}