You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@rocketmq.apache.org by st...@apache.org on 2022/03/05 02:34:43 UTC

[rocketmq-dashboard] branch master updated: [ISSUE #70] The rocketmq-dashboard supports ACL configuration (#71)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 4269879  [ISSUE #70] The rocketmq-dashboard supports ACL configuration (#71)
4269879 is described below

commit 4269879d93c5e9d6c02ed70ba540de3ac7d5b7f2
Author: Xiaodong Xu <76...@qq.com>
AuthorDate: Sat Mar 5 10:34:39 2022 +0800

    [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
---
 .../dashboard/controller/AclController.java        | 146 ++++++
 .../dashboard/model/request/AclRequest.java        |  30 ++
 .../rocketmq/dashboard/service/AclService.java     |  47 ++
 .../dashboard/service/client/MQAdminExtImpl.java   |  23 +-
 .../dashboard/service/impl/AclServiceImpl.java     | 359 ++++++++++++++
 src/main/resources/application.yml                 |   4 +-
 src/main/resources/role-permission.yml             |   1 +
 src/main/resources/static/index.html               |   1 +
 src/main/resources/static/src/acl.js               | 540 +++++++++++++++++++++
 src/main/resources/static/src/app.js               |  12 +
 src/main/resources/static/src/i18n/en.js           |  12 +-
 src/main/resources/static/src/i18n/zh.js           |  12 +-
 src/main/resources/static/style/app.css            |  12 +
 src/main/resources/static/view/layout/_header.html |   1 +
 src/main/resources/static/view/pages/acl.html      | 483 ++++++++++++++++++
 .../dashboard/controller/AclControllerTest.java    | 368 ++++++++++++++
 .../rocketmq/dashboard/util/MockObjectUtil.java    |  27 ++
 17 files changed, 2065 insertions(+), 13 deletions(-)

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 @@ public class MQAdminExtImpl implements MQAdminExt {
         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 @@ rocketmq:
     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 @@ rolePerms:
     - /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 @@ var app = angular.module('app', [
                 }
                 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 @@ app.config(['$routeProvider', '$httpProvider','$cookiesProvider','getDictNamePro
         }).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 @@ var en = {
     "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 @@ var zh = {
     "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 java.util.concurrent.ConcurrentMap;
 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.protocol.route.TopicRouteData;
 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 @@ public class MockObjectUtil {
         }
         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;
+    }
 }