You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pinot.apache.org by xi...@apache.org on 2022/05/06 05:57:20 UTC

[pinot] branch master updated: Access Control management Development (#8314) (#8314)

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

xiangfu pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/pinot.git


The following commit(s) were added to refs/heads/master by this push:
     new 328c0c23ec Access Control management Development (#8314) (#8314)
328c0c23ec is described below

commit 328c0c23ec2b0a10302a476dfeba2fd5f7a69ac6
Author: INNOCENT-BOY <38...@users.noreply.github.com>
AuthorDate: Fri May 6 13:57:15 2022 +0800

    Access Control management Development (#8314) (#8314)
    
    Co-authored-by: xuejyu <xu...@cisco.com>
---
 .../pinot/broker/broker/AccessControlFactory.java  |  21 +-
 .../broker/BasicAuthAccessControlFactory.java      |   2 +
 .../broker/ZkBasicAuthAccessControlFactory.java    | 122 +++++++++
 .../broker/broker/helix/BaseBrokerStarter.java     |   3 +-
 pinot-common/pom.xml                               |   5 +
 .../config/provider/AccessControlUserCache.java    | 177 ++++++++++++
 .../pinot/common/metadata/ZKMetadataProvider.java  |  67 +++++
 .../org/apache/pinot/common/utils/BcryptUtils.java |  50 ++++
 .../utils/config/AccessControlUserConfigUtils.java |  88 ++++++
 .../pinot-common-jdk8/pom.xml                      |   5 +
 .../pinot/controller/BaseControllerStarter.java    |   2 +-
 .../apache/pinot/controller/ControllerConf.java    |  21 ++
 .../api/ControllerAdminApiApplication.java         |   6 +
 .../pinot/controller/api/access/AccessControl.java |   4 +
 .../api/access/AccessControlFactory.java           |  15 ++
 .../controller/api/access/AccessControlUtils.java  |  14 +
 .../api/access/BasicAuthAccessControlFactory.java  |   1 +
 .../access/ZkBasicAuthAccessControlFactory.java    | 131 +++++++++
 .../api/exception/UserAlreadyExistsException.java} |  17 +-
 .../pinot/controller/api/resources/Constants.java  |  19 +-
 .../PinotAccessControlUserRestletResource.java     | 243 +++++++++++++++++
 .../helix/core/PinotHelixResourceManager.java      |  56 +++-
 pinot-controller/src/main/resources/app/App.tsx    |  30 ++-
 .../src/main/resources/app/app_state.ts            |   2 +
 .../main/resources/app/components/Breadcrumbs.tsx  |   3 +-
 .../src/main/resources/app/components/Layout.tsx   |  12 +-
 .../main/resources/app/components/User/AddUser.tsx | 243 +++++++++++++++++
 .../resources/app/components/User/UpdateUser.tsx   | 235 ++++++++++++++++
 .../src/main/resources/app/interfaces/types.d.ts   |  17 ++
 .../src/main/resources/app/pages/LoginPage.tsx     |   1 +
 .../src/main/resources/app/pages/UserPage.tsx      | 296 +++++++++++++++++++++
 .../src/main/resources/app/requests/index.ts       |  19 +-
 pinot-controller/src/main/resources/app/router.tsx |   2 +
 .../main/resources/app/utils/PinotMethodUtils.ts   |  46 +++-
 .../PinotAccessControlUserRestletResourceTest.java | 138 ++++++++++
 .../org/apache/pinot/core/auth/BasicAuthUtils.java |  87 +++++-
 .../pinot/core/auth/ZkBasicAuthPrincipal.java      |  56 ++++
 .../pinot/server/access/AccessControlFactory.java  |  14 +
 .../server/access/BasicAuthAccessFactory.java      |   1 +
 .../server/access/ZkBasicAuthAccessFactory.java    | 121 +++++++++
 .../apache/pinot/core/auth/ZkBasicAuthTest.java    |  59 ++++
 .../pinot/server/starter/ServerInstance.java       |   2 +-
 .../apache/pinot/spi/config/user/AccessType.java   |  11 +-
 .../pinot/spi/config/user/ComponentType.java       |  11 +-
 .../org/apache/pinot/spi/config/user/RoleType.java |  11 +-
 .../apache/pinot/spi/config/user/UserConfig.java   | 140 ++++++++++
 .../utils/builder/ControllerRequestURLBuilder.java |  21 ++
 .../pinot/spi/utils/builder/UserConfigBuilder.java |  70 +++++
 48 files changed, 2655 insertions(+), 62 deletions(-)

diff --git a/pinot-broker/src/main/java/org/apache/pinot/broker/broker/AccessControlFactory.java b/pinot-broker/src/main/java/org/apache/pinot/broker/broker/AccessControlFactory.java
index 49bfd3a52c..8e9cb1c695 100644
--- a/pinot-broker/src/main/java/org/apache/pinot/broker/broker/AccessControlFactory.java
+++ b/pinot-broker/src/main/java/org/apache/pinot/broker/broker/AccessControlFactory.java
@@ -18,6 +18,8 @@
  */
 package org.apache.pinot.broker.broker;
 
+import org.apache.helix.ZNRecord;
+import org.apache.helix.store.zk.ZkHelixPropertyStore;
 import org.apache.pinot.broker.api.AccessControl;
 import org.apache.pinot.spi.env.PinotConfiguration;
 import org.slf4j.Logger;
@@ -28,11 +30,24 @@ public abstract class AccessControlFactory {
   public static final Logger LOGGER = LoggerFactory.getLogger(AccessControlFactory.class);
   public static final String ACCESS_CONTROL_CLASS_CONFIG = "class";
 
-  public abstract void init(PinotConfiguration confguration);
+  public void init(PinotConfiguration configuration) {
+  };
+
+  /**
+   * Extend original init method inorder to support Zookeeper BasicAuthAccessControlFactory
+   * Because ZKBasicAuthAccessControlFactory need to acquire users info from HelixPropertyStore
+   *
+   * @param configuration pinot configuration
+   * @param propertyStore Helix PropertyStore
+   */
+  public void init(PinotConfiguration configuration, ZkHelixPropertyStore<ZNRecord> propertyStore) {
+     init(configuration);
+  }
 
   public abstract AccessControl create();
 
-  public static AccessControlFactory loadFactory(PinotConfiguration configuration) {
+  public static AccessControlFactory loadFactory(PinotConfiguration configuration,
+      ZkHelixPropertyStore<ZNRecord> propertyStore) {
     AccessControlFactory accessControlFactory;
     String accessControlFactoryClassName = configuration.getProperty(ACCESS_CONTROL_CLASS_CONFIG);
     if (accessControlFactoryClassName == null) {
@@ -42,7 +57,7 @@ public abstract class AccessControlFactory {
       LOGGER.info("Instantiating Access control factory class {}", accessControlFactoryClassName);
       accessControlFactory = (AccessControlFactory) Class.forName(accessControlFactoryClassName).newInstance();
       LOGGER.info("Initializing Access control factory class {}", accessControlFactoryClassName);
-      accessControlFactory.init(configuration);
+      accessControlFactory.init(configuration, propertyStore);
       return accessControlFactory;
     } catch (Exception e) {
       throw new RuntimeException(e);
diff --git a/pinot-broker/src/main/java/org/apache/pinot/broker/broker/BasicAuthAccessControlFactory.java b/pinot-broker/src/main/java/org/apache/pinot/broker/broker/BasicAuthAccessControlFactory.java
index 8c0e75827e..91ae183e8c 100644
--- a/pinot-broker/src/main/java/org/apache/pinot/broker/broker/BasicAuthAccessControlFactory.java
+++ b/pinot-broker/src/main/java/org/apache/pinot/broker/broker/BasicAuthAccessControlFactory.java
@@ -55,10 +55,12 @@ public class BasicAuthAccessControlFactory extends AccessControlFactory {
     // left blank
   }
 
+  @Override
   public void init(PinotConfiguration configuration) {
     _accessControl = new BasicAuthAccessControl(BasicAuthUtils.extractBasicAuthPrincipals(configuration, PREFIX));
   }
 
+  @Override
   public AccessControl create() {
     return _accessControl;
   }
diff --git a/pinot-broker/src/main/java/org/apache/pinot/broker/broker/ZkBasicAuthAccessControlFactory.java b/pinot-broker/src/main/java/org/apache/pinot/broker/broker/ZkBasicAuthAccessControlFactory.java
new file mode 100644
index 0000000000..8d47977517
--- /dev/null
+++ b/pinot-broker/src/main/java/org/apache/pinot/broker/broker/ZkBasicAuthAccessControlFactory.java
@@ -0,0 +1,122 @@
+/**
+ * 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.pinot.broker.broker;
+
+import com.google.common.base.Preconditions;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import org.apache.helix.ZNRecord;
+import org.apache.helix.store.zk.ZkHelixPropertyStore;
+import org.apache.pinot.broker.api.AccessControl;
+import org.apache.pinot.broker.api.HttpRequesterIdentity;
+import org.apache.pinot.broker.api.RequesterIdentity;
+import org.apache.pinot.common.config.provider.AccessControlUserCache;
+import org.apache.pinot.common.request.BrokerRequest;
+import org.apache.pinot.common.utils.BcryptUtils;
+import org.apache.pinot.core.auth.BasicAuthPrincipal;
+import org.apache.pinot.core.auth.BasicAuthUtils;
+import org.apache.pinot.core.auth.ZkBasicAuthPrincipal;
+import org.apache.pinot.spi.env.PinotConfiguration;
+
+
+/**
+ * Zookeeper Basic Authentication based on Pinot Controller UI.
+ * The user role has been distinguished by user and admin. Only admin can have access to the
+ * user console page in Pinot controller UI. And admin can change user info (table permission/
+ * number of tables/password etc.) or add/delete user without restarting your Pinot clusters,
+ * and these changes happen immediately.
+ * Users Configuration store in Helix Zookeeper and encrypted user password via Bcrypt Encryption Algorithm.
+ *
+ */
+public class ZkBasicAuthAccessControlFactory extends AccessControlFactory {
+    private static final String HEADER_AUTHORIZATION = "authorization";
+
+    private AccessControl _accessControl;
+
+    public ZkBasicAuthAccessControlFactory() {
+        // left blank
+    }
+
+    @Override
+    public void init(PinotConfiguration configuration, ZkHelixPropertyStore<ZNRecord> propertyStore) {
+        _accessControl = new BasicAuthAccessControl(new AccessControlUserCache(propertyStore));
+    }
+
+    @Override
+    public AccessControl create() {
+        return _accessControl;
+    }
+
+    /**
+     * Access Control using header-based basic http authentication
+     */
+    private static class BasicAuthAccessControl implements AccessControl {
+        private Map<String, ZkBasicAuthPrincipal> _name2principal;
+        private final AccessControlUserCache _userCache;
+
+        public BasicAuthAccessControl(AccessControlUserCache userCache) {
+            _userCache = userCache;
+        }
+
+        @Override
+        public boolean hasAccess(RequesterIdentity requesterIdentity) {
+            return hasAccess(requesterIdentity, null);
+        }
+
+        @Override
+        public boolean hasAccess(RequesterIdentity requesterIdentity, BrokerRequest brokerRequest) {
+            Preconditions.checkArgument(requesterIdentity instanceof HttpRequesterIdentity,
+                "HttpRequesterIdentity required");
+            HttpRequesterIdentity identity = (HttpRequesterIdentity) requesterIdentity;
+
+            Collection<String> tokens = identity.getHttpHeaders().get(HEADER_AUTHORIZATION);
+
+            _name2principal = BasicAuthUtils.extractBasicAuthPrincipals(_userCache.getAllBrokerUserConfig())
+                .stream().collect(Collectors.toMap(BasicAuthPrincipal::getName, p -> p));
+
+
+            Map<String, String> name2password = tokens.stream().collect(Collectors
+              .toMap(BasicAuthUtils::extractUsername, BasicAuthUtils::extractPassword));
+            Map<String, ZkBasicAuthPrincipal> password2principal = name2password.keySet().stream()
+              .collect(Collectors.toMap(name2password::get, _name2principal::get));
+
+            Optional<ZkBasicAuthPrincipal> principalOpt =
+              password2principal.entrySet().stream()
+                .filter(entry -> BcryptUtils.checkpw(entry.getKey(), entry.getValue().getPassword()))
+                .map(u -> u.getValue()).filter(Objects::nonNull).findFirst();
+
+            if (!principalOpt.isPresent()) {
+                // no matching token? reject
+                return false;
+            }
+
+            ZkBasicAuthPrincipal principal = principalOpt.get();
+            if (brokerRequest == null || !brokerRequest.isSetQuerySource() || !brokerRequest.getQuerySource()
+                .isSetTableName()) {
+                // no table restrictions? accept
+                return true;
+            }
+
+            return principal.hasTable(brokerRequest.getQuerySource().getTableName());
+        }
+    }
+}
diff --git a/pinot-broker/src/main/java/org/apache/pinot/broker/broker/helix/BaseBrokerStarter.java b/pinot-broker/src/main/java/org/apache/pinot/broker/broker/helix/BaseBrokerStarter.java
index 8e232fb49e..676f8001cc 100644
--- a/pinot-broker/src/main/java/org/apache/pinot/broker/broker/helix/BaseBrokerStarter.java
+++ b/pinot-broker/src/main/java/org/apache/pinot/broker/broker/helix/BaseBrokerStarter.java
@@ -232,7 +232,8 @@ public abstract class BaseBrokerStarter implements ServiceStartable {
     // Set up request handling classes
     _routingManager = new BrokerRoutingManager(_brokerMetrics);
     _routingManager.init(_spectatorHelixManager);
-    _accessControlFactory = AccessControlFactory.loadFactory(_brokerConf.subset(Broker.ACCESS_CONTROL_CONFIG_PREFIX));
+    _accessControlFactory = AccessControlFactory.loadFactory(_brokerConf.subset(Broker.ACCESS_CONTROL_CONFIG_PREFIX),
+        _propertyStore);
     HelixExternalViewBasedQueryQuotaManager queryQuotaManager =
         new HelixExternalViewBasedQueryQuotaManager(_brokerMetrics, _instanceId);
     queryQuotaManager.init(_spectatorHelixManager);
diff --git a/pinot-common/pom.xml b/pinot-common/pom.xml
index 2bb5b8127b..ee40642b05 100644
--- a/pinot-common/pom.xml
+++ b/pinot-common/pom.xml
@@ -435,6 +435,11 @@
       <groupId>org.apache.yetus</groupId>
       <artifactId>audience-annotations</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.mindrot</groupId>
+      <artifactId>jbcrypt</artifactId>
+      <version>0.4</version>
+    </dependency>
   </dependencies>
   <profiles>
     <profile>
diff --git a/pinot-common/src/main/java/org/apache/pinot/common/config/provider/AccessControlUserCache.java b/pinot-common/src/main/java/org/apache/pinot/common/config/provider/AccessControlUserCache.java
new file mode 100644
index 0000000000..102affcdeb
--- /dev/null
+++ b/pinot-common/src/main/java/org/apache/pinot/common/config/provider/AccessControlUserCache.java
@@ -0,0 +1,177 @@
+/**
+ * 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.pinot.common.config.provider;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+import javax.annotation.Nullable;
+import org.I0Itec.zkclient.IZkChildListener;
+import org.I0Itec.zkclient.IZkDataListener;
+import org.apache.commons.collections.CollectionUtils;
+import org.apache.helix.AccessOption;
+import org.apache.helix.ZNRecord;
+import org.apache.helix.store.zk.ZkHelixPropertyStore;
+import org.apache.pinot.common.utils.config.AccessControlUserConfigUtils;
+import org.apache.pinot.spi.config.user.ComponentType;
+import org.apache.pinot.spi.config.user.UserConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The {@code AccessControlUserCache} caches all the user configs within the cluster, and listens on ZK changes to keep
+ * them in sync. It also maintains several component user config maps.
+ */
+public class AccessControlUserCache {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(AccessControlUserCache.class);
+    private static final String USER_CONFIG_PARENT_PATH = "/CONFIGS/USER";
+    private static final String USER_CONFIG_PATH_PREFIX = "/CONFIGS/USER/";
+
+    private final ZkHelixPropertyStore<ZNRecord> _propertyStore;
+
+    private final UserConfigChangeListener _userConfigChangeListener = new UserConfigChangeListener();
+
+    private final Map<String, UserConfig> _userConfigMap = new ConcurrentHashMap<>();
+    private final Map<String, UserConfig> _userControllerConfigMap = new ConcurrentHashMap<>();
+    private final Map<String, UserConfig> _userBrokerConfigMap = new ConcurrentHashMap<>();
+    private final Map<String, UserConfig> _userServerConfigMap = new ConcurrentHashMap<>();
+
+    public AccessControlUserCache(ZkHelixPropertyStore<ZNRecord> propertyStore) {
+        _propertyStore = propertyStore;
+        _propertyStore.subscribeChildChanges(USER_CONFIG_PARENT_PATH, _userConfigChangeListener);
+
+        List<String> users = _propertyStore.getChildNames(USER_CONFIG_PARENT_PATH, AccessOption.PERSISTENT);
+        if (CollectionUtils.isNotEmpty(users)) {
+            List<String> pathsToAdd = new ArrayList<>(users.size());
+            for (String user : users) {
+                pathsToAdd.add(USER_CONFIG_PATH_PREFIX + user);
+            }
+            addUserConfigs(pathsToAdd);
+        }
+    }
+
+    @Nullable
+    public List<UserConfig> getAllUserConfig() {
+        return _userConfigMap.values().stream().collect(Collectors.toList());
+    }
+
+    public List<UserConfig> getAllControllerUserConfig() {
+        return _userControllerConfigMap.values().stream().collect(Collectors.toList());
+    }
+
+    public List<UserConfig> getAllBrokerUserConfig() {
+        return _userBrokerConfigMap.values().stream().collect(Collectors.toList());
+    }
+
+    public List<UserConfig> getAllServerUserConfig() {
+        return _userServerConfigMap.values().stream().collect(Collectors.toList());
+    }
+
+    @Nullable
+    public List<String> getAllUserName() {
+        return _userConfigMap.keySet().stream().collect(Collectors.toList());
+    }
+
+    private void addUserConfigs(List<String> allUserZKStorePath) {
+        for (String userStoreZKPath : allUserZKStorePath) {
+            _propertyStore.subscribeDataChanges(userStoreZKPath, _userConfigChangeListener);
+        }
+        List<ZNRecord> allUserRecords = _propertyStore.get(allUserZKStorePath, null, AccessOption.PERSISTENT, false);
+        for (ZNRecord userRecord : allUserRecords) {
+            if (userRecord != null) {
+                try {
+                    UserConfig userConfig = AccessControlUserConfigUtils.fromZNRecord(userRecord);
+                    String username = userConfig.getUsernameWithComponent();
+
+                    if (userConfig.getComponentType().equals(ComponentType.CONTROLLER)) {
+                        _userControllerConfigMap.put(username, userConfig);
+                    } else if (userConfig.getComponentType().equals(ComponentType.BROKER)) {
+                        _userBrokerConfigMap.put(username, userConfig);
+                    } else if (userConfig.getComponentType().equals(ComponentType.SERVER)) {
+                        _userServerConfigMap.put(username, userConfig);
+                    }
+                } catch (Exception e) {
+                    LOGGER.error("Caught exception while adding user config for ZNRecord:{}", userRecord.getId(), e);
+                }
+            }
+        }
+    }
+
+    private void removeUserConfig(String userStoreZKPath) {
+        _propertyStore.unsubscribeDataChanges(userStoreZKPath, _userConfigChangeListener);
+        String usernameWithComponentType = userStoreZKPath.substring(USER_CONFIG_PATH_PREFIX.length());
+        if (usernameWithComponentType.endsWith("BROKER")) {
+            _userBrokerConfigMap.remove(usernameWithComponentType);
+        } else if (usernameWithComponentType.endsWith("CONTROLLER")) {
+            _userControllerConfigMap.remove(usernameWithComponentType);
+        } else if (usernameWithComponentType.endsWith("SERVER")) {
+            _userServerConfigMap.remove(usernameWithComponentType);
+        }
+    }
+
+    private class UserConfigChangeListener implements IZkChildListener, IZkDataListener {
+        @Override
+        public void handleChildChange(String userStoreZKPath, List<String> allUserList) throws Exception {
+            if (CollectionUtils.isEmpty(allUserList)) {
+                return;
+            }
+
+            List<String> newUserPathToAdd = new ArrayList<>();
+            for (String user : allUserList) {
+                if (!_userControllerConfigMap.containsKey(user) && !_userBrokerConfigMap.containsKey(user)
+                && !_userServerConfigMap.containsKey(user)) {
+                    newUserPathToAdd.add(USER_CONFIG_PATH_PREFIX + user);
+                }
+            }
+            if (!newUserPathToAdd.isEmpty()) {
+                addUserConfigs(newUserPathToAdd);
+            }
+        }
+
+        @Override
+        public void handleDataChange(String userStoreZKPath, Object changedUserConfig) throws Exception {
+            if (changedUserConfig != null) {
+                ZNRecord userRecord = (ZNRecord) changedUserConfig;
+                try {
+                    UserConfig userConfig = AccessControlUserConfigUtils.fromZNRecord(userRecord);
+                    String usernameWithComponentType = userConfig.getUsernameWithComponent();
+                    if (userConfig.getComponentType().equals(ComponentType.CONTROLLER)) {
+                        _userControllerConfigMap.put(usernameWithComponentType, userConfig);
+                    } else if (userConfig.getComponentType().equals(ComponentType.BROKER)) {
+                        _userBrokerConfigMap.put(usernameWithComponentType, userConfig);
+                    } else if (userConfig.getComponentType().equals(ComponentType.SERVER)) {
+                        _userServerConfigMap.put(usernameWithComponentType, userConfig);
+                    }
+                } catch (Exception e) {
+                    LOGGER.error("caught exception while refreshing table config for ZNRecord: {}",
+                        userRecord.getId(), e);
+                }
+            }
+        }
+
+        @Override
+        public void handleDataDeleted(String path) throws Exception {
+            String username = path.substring(path.lastIndexOf('/') + 1);
+            removeUserConfig(USER_CONFIG_PATH_PREFIX + username);
+        }
+    }
+}
diff --git a/pinot-common/src/main/java/org/apache/pinot/common/metadata/ZKMetadataProvider.java b/pinot-common/src/main/java/org/apache/pinot/common/metadata/ZKMetadataProvider.java
index 38b74f31f5..0633e32672 100644
--- a/pinot-common/src/main/java/org/apache/pinot/common/metadata/ZKMetadataProvider.java
+++ b/pinot-common/src/main/java/org/apache/pinot/common/metadata/ZKMetadataProvider.java
@@ -21,6 +21,9 @@ package org.apache.pinot.common.metadata;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 import org.I0Itec.zkclient.exception.ZkBadVersionException;
 import org.apache.helix.AccessOption;
@@ -30,10 +33,12 @@ import org.apache.pinot.common.metadata.instance.InstanceZKMetadata;
 import org.apache.pinot.common.metadata.segment.SegmentZKMetadata;
 import org.apache.pinot.common.utils.SchemaUtils;
 import org.apache.pinot.common.utils.SegmentName;
+import org.apache.pinot.common.utils.config.AccessControlUserConfigUtils;
 import org.apache.pinot.common.utils.config.TableConfigUtils;
 import org.apache.pinot.spi.config.ConfigUtils;
 import org.apache.pinot.spi.config.table.TableConfig;
 import org.apache.pinot.spi.config.table.TableType;
+import org.apache.pinot.spi.config.user.UserConfig;
 import org.apache.pinot.spi.data.Schema;
 import org.apache.pinot.spi.utils.CommonConstants;
 import org.apache.pinot.spi.utils.StringUtil;
@@ -54,11 +59,17 @@ public class ZKMetadataProvider {
   private static final String PROPERTYSTORE_SCHEMAS_PREFIX = "/SCHEMAS";
   private static final String PROPERTYSTORE_INSTANCE_PARTITIONS_PREFIX = "/INSTANCE_PARTITIONS";
   private static final String PROPERTYSTORE_TABLE_CONFIGS_PREFIX = "/CONFIGS/TABLE";
+  private static final String PROPERTYSTORE_USER_CONFIGS_PREFIX = "/CONFIGS/USER";
   private static final String PROPERTYSTORE_INSTANCE_CONFIGS_PREFIX = "/CONFIGS/INSTANCE";
   private static final String PROPERTYSTORE_CLUSTER_CONFIGS_PREFIX = "/CONFIGS/CLUSTER";
   private static final String PROPERTYSTORE_SEGMENT_LINEAGE = "/SEGMENT_LINEAGE";
   private static final String PROPERTYSTORE_MINION_TASK_METADATA_PREFIX = "/MINION_TASK_METADATA";
 
+  public static void setUserConfig(ZkHelixPropertyStore<ZNRecord> propertyStore, String username, ZNRecord znRecord) {
+    propertyStore
+        .set(constructPropertyStorePathForUserConfig(username), znRecord, AccessOption.PERSISTENT);
+  }
+
   public static void setRealtimeTableConfig(ZkHelixPropertyStore<ZNRecord> propertyStore, String realtimeTableName,
       ZNRecord znRecord) {
     propertyStore.set(constructPropertyStorePathForResourceConfig(realtimeTableName), znRecord,
@@ -107,6 +118,10 @@ public class ZKMetadataProvider {
     return StringUtil.join("/", PROPERTYSTORE_TABLE_CONFIGS_PREFIX, resourceName);
   }
 
+  public static String constructPropertyStorePathForUserConfig(String resourceName) {
+    return StringUtil.join("/", PROPERTYSTORE_USER_CONFIGS_PREFIX, resourceName);
+  }
+
   public static String constructPropertyStorePathForControllerConfig(String controllerConfigKey) {
     return StringUtil.join("/", PROPERTYSTORE_CLUSTER_CONFIGS_PREFIX, controllerConfigKey);
   }
@@ -141,6 +156,14 @@ public class ZKMetadataProvider {
     }
   }
 
+  public static void removeUserConfigFromPropertyStore(ZkHelixPropertyStore<ZNRecord> propertyStore,
+      String username) {
+    String propertyStorePath = constructPropertyStorePathForUserConfig(username);
+    if (propertyStore.exists(propertyStorePath, AccessOption.PERSISTENT)) {
+      propertyStore.remove(propertyStorePath, AccessOption.PERSISTENT);
+    }
+  }
+
   /**
    * Creates a new znode for SegmentZkMetadata. This call is atomic. If there are concurrent calls trying to create the
    * same znode, only one of them would succeed.
@@ -199,6 +222,50 @@ public class ZKMetadataProvider {
     return znRecord != null ? new SegmentZKMetadata(znRecord) : null;
   }
 
+  @Nullable
+  public static UserConfig getUserConfig(ZkHelixPropertyStore<ZNRecord> propertyStore, String username) {
+    ZNRecord znRecord = propertyStore
+        .get(constructPropertyStorePathForUserConfig(username), null, AccessOption.PERSISTENT);
+    if (znRecord == null) {
+      return null;
+    }
+    try {
+      UserConfig userConfig = AccessControlUserConfigUtils.fromZNRecord(znRecord);
+      return (UserConfig) ConfigUtils.applyConfigWithEnvVariables(userConfig);
+    } catch (Exception e) {
+      LOGGER.error("Caught exception while getting user configuration for user: {}", username, e);
+      return null;
+    }
+  }
+
+  @Nullable
+  public static List<UserConfig> getAllUserConfig(ZkHelixPropertyStore<ZNRecord> propertyStore) {
+    List<ZNRecord> znRecordss = propertyStore
+        .getChildren(PROPERTYSTORE_USER_CONFIGS_PREFIX, null, AccessOption.PERSISTENT);
+
+    try {
+      return Optional.ofNullable(znRecordss)
+          .orElseGet(() -> {
+            return new ArrayList<>();
+          }).stream().map(AccessControlUserConfigUtils::fromZNRecord).collect(Collectors.toList());
+    } catch (Exception e) {
+      LOGGER.error("Caught exception while getting user list configuration", e);
+      return null;
+    }
+  }
+
+  @Nullable
+  public static List<String> getAllUserName(ZkHelixPropertyStore<ZNRecord> propertyStore) {
+    return propertyStore
+        .getChildNames(PROPERTYSTORE_USER_CONFIGS_PREFIX, AccessOption.PERSISTENT);
+  }
+
+  @Nullable
+  public static Map<String, UserConfig> getAllUserInfo(ZkHelixPropertyStore<ZNRecord> propertyStore) {
+    return getAllUserConfig(propertyStore).stream()
+        .collect(Collectors.toMap(UserConfig::getUsernameWithComponent, u -> u));
+  }
+
   @Nullable
   public static TableConfig getTableConfig(ZkHelixPropertyStore<ZNRecord> propertyStore, String tableNameWithType) {
     return toTableConfig(propertyStore.get(constructPropertyStorePathForResourceConfig(tableNameWithType), null,
diff --git a/pinot-common/src/main/java/org/apache/pinot/common/utils/BcryptUtils.java b/pinot-common/src/main/java/org/apache/pinot/common/utils/BcryptUtils.java
new file mode 100644
index 0000000000..ca068ae720
--- /dev/null
+++ b/pinot-common/src/main/java/org/apache/pinot/common/utils/BcryptUtils.java
@@ -0,0 +1,50 @@
+/**
+ * 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.pinot.common.utils;
+
+import org.mindrot.jbcrypt.BCrypt;
+
+public class BcryptUtils {
+
+    private static final int DEFALUT_LOG_ROUNDS = 10;
+    private static String _bcryptPassword = null;
+
+    private BcryptUtils() {
+    }
+
+    public static String encrypt(String password) {
+        return encrypt(password, DEFALUT_LOG_ROUNDS);
+    }
+
+    public static String encrypt(String password, int saltLogRrounds) {
+        _bcryptPassword = BCrypt.hashpw(password, BCrypt.gensalt(saltLogRrounds));
+        return _bcryptPassword;
+    }
+
+    public static boolean checkpw(String pasword, String encrypedPassword) {
+        boolean isMatch = false;
+        try {
+            isMatch = BCrypt.checkpw(pasword, encrypedPassword);
+        } catch (Exception e) {
+            System.out.println(e.getMessage());
+        } finally {
+            return isMatch;
+        }
+    }
+}
diff --git a/pinot-common/src/main/java/org/apache/pinot/common/utils/config/AccessControlUserConfigUtils.java b/pinot-common/src/main/java/org/apache/pinot/common/utils/config/AccessControlUserConfigUtils.java
new file mode 100644
index 0000000000..810b3441de
--- /dev/null
+++ b/pinot-common/src/main/java/org/apache/pinot/common/utils/config/AccessControlUserConfigUtils.java
@@ -0,0 +1,88 @@
+/**
+ * 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.pinot.common.utils.config;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+import org.apache.helix.ZNRecord;
+import org.apache.pinot.spi.config.user.AccessType;
+import org.apache.pinot.spi.config.user.UserConfig;
+
+/**
+ * UserConfigUtils is responsible for two things:
+ * 1. Used to acquire user config by parsing znRecord that stored in Zookeeper
+ * 2. Used to construct znRecord by packaging user config
+ */
+public class AccessControlUserConfigUtils {
+    private AccessControlUserConfigUtils() {
+    }
+
+    public static UserConfig fromZNRecord(ZNRecord znRecord) {
+        Map<String, String> simpleFields = znRecord.getSimpleFields();
+
+        // Mandatory fields
+        String username = simpleFields.get(UserConfig.USERNAME_KEY);
+        String password = simpleFields.get(UserConfig.PASSWORD_KEY);
+        String component = simpleFields.get(UserConfig.COMPONET_KEY);
+        String role = simpleFields.get(UserConfig.ROLE_KEY);
+
+        List<String> tableList = znRecord.getListField(UserConfig.TABLES_KEY);
+
+        List<String> permissionListFromZNRecord = znRecord.getListField(UserConfig.PERMISSIONS_KEY);
+        List<AccessType> permissionList = null;
+        if (permissionListFromZNRecord != null) {
+            permissionList = permissionListFromZNRecord.stream()
+                .map(x -> AccessType.valueOf(x)).collect(Collectors.toList());
+        }
+        return new UserConfig(username, password, component, role, tableList, permissionList);
+    }
+
+    public static ZNRecord toZNRecord(UserConfig userConfig)
+        throws JsonProcessingException {
+        Map<String, String> simpleFields = new HashMap<>();
+
+        // Mandatory fields
+        simpleFields.put(UserConfig.USERNAME_KEY, userConfig.getUserName());
+        simpleFields.put(UserConfig.PASSWORD_KEY, userConfig.getPassword());
+        simpleFields.put(UserConfig.COMPONET_KEY, userConfig.getComponentType().toString());
+        simpleFields.put(UserConfig.ROLE_KEY, userConfig.getRoleType().toString());
+
+        Map<String, List<String>> listFields = new HashMap<>();
+
+        // Optional fields
+        List<String> tableList = userConfig.getTables();
+        if (tableList != null) {
+            listFields.put(UserConfig.TABLES_KEY, userConfig.getTables());
+        }
+
+        List<AccessType> permissionList = userConfig.getPermissios();
+        if (permissionList != null) {
+            listFields.put(UserConfig.PERMISSIONS_KEY, userConfig.getPermissios().stream()
+                .map(e -> e.toString()).collect(Collectors.toList()));
+        }
+
+        ZNRecord znRecord = new ZNRecord(userConfig.getUserName());
+        znRecord.setSimpleFields(simpleFields);
+        znRecord.setListFields(listFields);
+        return znRecord;
+    }
+}
diff --git a/pinot-connectors/prestodb-pinot-dependencies/pinot-common-jdk8/pom.xml b/pinot-connectors/prestodb-pinot-dependencies/pinot-common-jdk8/pom.xml
index 095cfca22c..d544fa26ba 100644
--- a/pinot-connectors/prestodb-pinot-dependencies/pinot-common-jdk8/pom.xml
+++ b/pinot-connectors/prestodb-pinot-dependencies/pinot-common-jdk8/pom.xml
@@ -419,5 +419,10 @@
       <groupId>org.apache.yetus</groupId>
       <artifactId>audience-annotations</artifactId>
     </dependency>
+    <dependency>
+      <groupId>org.mindrot</groupId>
+      <artifactId>jbcrypt</artifactId>
+      <version>0.4</version>
+    </dependency>
   </dependencies>
 </project>
diff --git a/pinot-controller/src/main/java/org/apache/pinot/controller/BaseControllerStarter.java b/pinot-controller/src/main/java/org/apache/pinot/controller/BaseControllerStarter.java
index 6fe183a128..5dcc864ca3 100644
--- a/pinot-controller/src/main/java/org/apache/pinot/controller/BaseControllerStarter.java
+++ b/pinot-controller/src/main/java/org/apache/pinot/controller/BaseControllerStarter.java
@@ -435,7 +435,7 @@ public abstract class BaseControllerStarter implements ServiceStartable {
     final AccessControlFactory accessControlFactory;
     try {
       accessControlFactory = (AccessControlFactory) Class.forName(accessControlFactoryClass).newInstance();
-      accessControlFactory.init(_config);
+      accessControlFactory.init(_config, _helixResourceManager);
     } catch (Exception e) {
       throw new RuntimeException("Caught exception while creating new AccessControlFactory instance", e);
     }
diff --git a/pinot-controller/src/main/java/org/apache/pinot/controller/ControllerConf.java b/pinot-controller/src/main/java/org/apache/pinot/controller/ControllerConf.java
index ef031362b4..a7f54b39c6 100644
--- a/pinot-controller/src/main/java/org/apache/pinot/controller/ControllerConf.java
+++ b/pinot-controller/src/main/java/org/apache/pinot/controller/ControllerConf.java
@@ -216,6 +216,8 @@ public class ControllerConf extends PinotConfiguration {
   private static final String JERSEY_ADMIN_API_PORT = "jersey.admin.api.port";
   private static final String JERSEY_ADMIN_IS_PRIMARY = "jersey.admin.isprimary";
   public static final String ACCESS_CONTROL_FACTORY_CLASS = "controller.admin.access.control.factory.class";
+  public static final String ACCESS_CONTROL_USERNAME = "access.control.init.username";
+  public static final String ACCESS_CONTROL_PASSWORD = "access.control.init.password";
   // Amount of the time the segment can take from the beginning of upload to the end of upload. Used when parallel push
   // protection is enabled. If the upload does not finish within the timeout, next upload can override the previous one.
   private static final String SEGMENT_UPLOAD_TIMEOUT_IN_MILLIS = "controller.segment.upload.timeoutInMillis";
@@ -240,6 +242,8 @@ public class ControllerConf extends PinotConfiguration {
   private static final int DEFAULT_JERSEY_ADMIN_PORT = 21000;
   private static final String DEFAULT_ACCESS_CONTROL_FACTORY_CLASS =
       "org.apache.pinot.controller.api.access.AllowAllAccessFactory";
+  private static final String DEFAULT_ACCESS_CONTROL_USERNAME = "admin";
+  private static final String DEFAULT_ACCESS_CONTROL_PASSWORD = "admin";
   private static final long DEFAULT_SEGMENT_UPLOAD_TIMEOUT_IN_MILLIS = 600_000L; // 10 minutes
   private static final int DEFAULT_REALTIME_SEGMENT_METADATA_COMMIT_NUMLOCKS = 64;
   private static final boolean DEFAULT_ENABLE_STORAGE_QUOTA_CHECK = true;
@@ -709,10 +713,27 @@ public class ControllerConf extends PinotConfiguration {
     return getProperty(JERSEY_ADMIN_API_PORT, String.valueOf(DEFAULT_JERSEY_ADMIN_PORT));
   }
 
+  public void setInitAccessControlUsername(String username) {
+    setProperty(ACCESS_CONTROL_USERNAME, username);
+  }
+
+  public void setInitAccessControlPassword(String password) {
+    setProperty(ACCESS_CONTROL_PASSWORD, password);
+  }
+
+  public String getInitAccessControlUsername() {
+    return getProperty(ACCESS_CONTROL_USERNAME, DEFAULT_ACCESS_CONTROL_USERNAME);
+  }
+
+  public String getInitAccessControlPassword() {
+    return getProperty(ACCESS_CONTROL_PASSWORD, DEFAULT_ACCESS_CONTROL_PASSWORD);
+  }
+
   public String getAccessControlFactoryClass() {
     return getProperty(ACCESS_CONTROL_FACTORY_CLASS, DEFAULT_ACCESS_CONTROL_FACTORY_CLASS);
   }
 
+
   public void setAccessControlFactoryClass(String accessControlFactoryClass) {
     setProperty(ACCESS_CONTROL_FACTORY_CLASS, accessControlFactoryClass);
   }
diff --git a/pinot-controller/src/main/java/org/apache/pinot/controller/api/ControllerAdminApiApplication.java b/pinot-controller/src/main/java/org/apache/pinot/controller/api/ControllerAdminApiApplication.java
index 3f060dead9..de74d90304 100644
--- a/pinot-controller/src/main/java/org/apache/pinot/controller/api/ControllerAdminApiApplication.java
+++ b/pinot-controller/src/main/java/org/apache/pinot/controller/api/ControllerAdminApiApplication.java
@@ -23,6 +23,7 @@ import java.io.IOException;
 import java.net.URL;
 import java.net.URLClassLoader;
 import java.util.List;
+import javax.servlet.http.HttpServletResponse;
 import javax.ws.rs.container.ContainerRequestContext;
 import javax.ws.rs.container.ContainerResponseContext;
 import javax.ws.rs.container.ContainerResponseFilter;
@@ -137,6 +138,11 @@ public class ControllerAdminApiApplication extends ResourceConfig {
         ContainerResponseContext containerResponseContext)
         throws IOException {
       containerResponseContext.getHeaders().add("Access-Control-Allow-Origin", "*");
+      containerResponseContext.getHeaders().add("Access-Control-Allow-Methods", "GET, POST, PUT, OPTIONS, DELETE");
+      containerResponseContext.getHeaders().add("Access-Control-Allow-Headers", "*");
+      if (containerRequestContext.getMethod().equals("OPTIONS")) {
+        containerResponseContext.setStatus(HttpServletResponse.SC_OK);
+      }
     }
   }
 }
diff --git a/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/AccessControl.java b/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/AccessControl.java
index be44be364e..3e19a9a45c 100644
--- a/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/AccessControl.java
+++ b/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/AccessControl.java
@@ -67,6 +67,10 @@ public interface AccessControl {
     return true;
   }
 
+  default boolean hasAccess(HttpHeaders httpHeaders) {
+    return true;
+  }
+
   /**
    * Determine whether authentication is required for annotated (controller) endpoints only
    *
diff --git a/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/AccessControlFactory.java b/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/AccessControlFactory.java
index 2c7978493c..9ce1df3d31 100644
--- a/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/AccessControlFactory.java
+++ b/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/AccessControlFactory.java
@@ -18,6 +18,8 @@
  */
 package org.apache.pinot.controller.api.access;
 
+import java.io.IOException;
+import org.apache.pinot.controller.helix.core.PinotHelixResourceManager;
 import org.apache.pinot.spi.annotations.InterfaceAudience;
 import org.apache.pinot.spi.annotations.InterfaceStability;
 import org.apache.pinot.spi.env.PinotConfiguration;
@@ -29,6 +31,19 @@ public interface AccessControlFactory {
   default void init(PinotConfiguration pinotConfiguration) {
     // left blank
   }
+  /**
+   * Extend the original init method to support Zookeeper BasicAuthAccessControlFactory.
+   * Because ZKBasicAuthAccessControlFactory needs to acquire users' info from HelixPropertyStore.
+   * The reason why passed helixResourceManager param other than HelixPropertyStore is that
+   * we need to init UserACLConfig via helixResourceManager.
+   *
+   * @param configuration pinot configuration
+   * @param helixResourceManager Helix resource manager
+   */
+  default void init(PinotConfiguration configuration, PinotHelixResourceManager helixResourceManager)
+    throws IOException {
+    init(configuration);
+  }
 
   AccessControl create();
 }
diff --git a/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/AccessControlUtils.java b/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/AccessControlUtils.java
index 95010c4648..c12ba307bd 100644
--- a/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/AccessControlUtils.java
+++ b/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/AccessControlUtils.java
@@ -93,4 +93,18 @@ public class AccessControlUtils {
           Response.Status.FORBIDDEN);
     }
   }
+
+  /**
+   * Validate permission for the given access type against the given table
+   *
+   * @param httpHeaders HTTP headers containing requester identity required by access control object
+   * @param endpointUrl the request url for which this access control is called
+   */
+  public void validatePermission(HttpHeaders httpHeaders, String endpointUrl,
+      AccessControl accessControl) {
+    if (!accessControl.hasAccess(httpHeaders)) {
+      throw new ControllerApplicationException(LOGGER, "Permission is denied for " + endpointUrl,
+          Response.Status.FORBIDDEN);
+    }
+  }
 }
diff --git a/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/BasicAuthAccessControlFactory.java b/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/BasicAuthAccessControlFactory.java
index 378c58f675..9f53659c13 100644
--- a/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/BasicAuthAccessControlFactory.java
+++ b/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/BasicAuthAccessControlFactory.java
@@ -50,6 +50,7 @@ public class BasicAuthAccessControlFactory implements AccessControlFactory {
 
   private AccessControl _accessControl;
 
+  @Override
   public void init(PinotConfiguration configuration) {
     _accessControl = new BasicAuthAccessControl(BasicAuthUtils.extractBasicAuthPrincipals(configuration, PREFIX));
   }
diff --git a/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/ZkBasicAuthAccessControlFactory.java b/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/ZkBasicAuthAccessControlFactory.java
new file mode 100644
index 0000000000..d032b98681
--- /dev/null
+++ b/pinot-controller/src/main/java/org/apache/pinot/controller/api/access/ZkBasicAuthAccessControlFactory.java
@@ -0,0 +1,131 @@
+/**
+ * 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.pinot.controller.api.access;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import javax.ws.rs.core.HttpHeaders;
+import org.apache.pinot.common.config.provider.AccessControlUserCache;
+import org.apache.pinot.common.utils.BcryptUtils;
+import org.apache.pinot.controller.ControllerConf;
+import org.apache.pinot.controller.helix.core.PinotHelixResourceManager;
+import org.apache.pinot.core.auth.BasicAuthUtils;
+import org.apache.pinot.core.auth.ZkBasicAuthPrincipal;
+import org.apache.pinot.spi.config.user.ComponentType;
+import org.apache.pinot.spi.config.user.RoleType;
+import org.apache.pinot.spi.env.PinotConfiguration;
+
+
+/**
+ * Zookeeper Basic Authentication based on Pinot Controller UI.
+ * The user role has been distinguished by user and admin. Only admin can have access to the
+ * user console page in Pinot controller UI. And admin can change user info (table permission/
+ * number of tables/password etc.) or add/delete user without restarting your Pinot clusters,
+ * and these changes happen immediately.
+ * Users Configuration store in Helix Zookeeper and encrypted user password via Bcrypt Encryption Algorithm.
+ *
+ */
+public class ZkBasicAuthAccessControlFactory implements AccessControlFactory {
+    private static final String HEADER_AUTHORIZATION = "Authorization";
+
+    private AccessControl _accessControl;
+
+    @Override
+    public void init(PinotConfiguration pinotConfiguration, PinotHelixResourceManager pinotHelixResourceManager)
+      throws IOException {
+        pinotHelixResourceManager.initUserACLConfig((ControllerConf) pinotConfiguration);
+        _accessControl = new BasicAuthAccessControl(new AccessControlUserCache(
+          pinotHelixResourceManager.getPropertyStore()));
+    }
+
+    @Override
+    public AccessControl create() {
+        return _accessControl;
+    }
+
+    /**
+     * Access Control using header-based basic http authentication
+     */
+    private static class BasicAuthAccessControl implements AccessControl {
+        private Map<String, ZkBasicAuthPrincipal> _name2principal;
+        private final AccessControlUserCache _userCache;
+
+        public BasicAuthAccessControl(AccessControlUserCache userCache) {
+            _userCache = userCache;
+        }
+
+        @Override
+        public boolean protectAnnotatedOnly() {
+            return false;
+        }
+
+        @Override
+        public boolean hasDataAccess(HttpHeaders httpHeaders, String tableName) {
+            return getPrincipal(httpHeaders).filter(p -> p.hasTable(tableName)).isPresent();
+        }
+
+        @Override
+        public boolean hasAccess(String tableName, AccessType accessType, HttpHeaders httpHeaders, String endpointUrl) {
+            return getPrincipal(httpHeaders)
+                .filter(p -> p.hasTable(tableName) && p.hasPermission(Objects.toString(accessType))).isPresent();
+        }
+
+        @Override
+        public boolean hasAccess(AccessType accessType, HttpHeaders httpHeaders, String endpointUrl) {
+            return getPrincipal(httpHeaders).isPresent();
+        }
+
+        @Override
+        public boolean hasAccess(HttpHeaders httpHeaders) {
+            return getPrincipal(httpHeaders)
+                .filter(p -> p.hasPermission(RoleType.ADMIN, ComponentType.CONTROLLER)).isPresent();
+        }
+
+        private Optional<ZkBasicAuthPrincipal> getPrincipal(HttpHeaders headers) {
+            if (headers == null) {
+                return Optional.empty();
+            }
+
+            _name2principal = BasicAuthUtils.extractBasicAuthPrincipals(_userCache.getAllControllerUserConfig())
+                .stream().collect(Collectors.toMap(ZkBasicAuthPrincipal::getName, p -> p));
+
+            List<String> authHeaders = headers.getRequestHeader(HEADER_AUTHORIZATION);
+            if (authHeaders == null) {
+                return Optional.empty();
+            }
+            Map<String, String> name2password = authHeaders.stream().collect(Collectors
+              .toMap(BasicAuthUtils::extractUsername, BasicAuthUtils::extractPassword));
+            Map<String, ZkBasicAuthPrincipal> password2principal = name2password.keySet().stream()
+              .collect(Collectors.toMap(name2password::get, _name2principal::get));
+
+            return password2principal.entrySet().stream()
+              .filter(entry -> BcryptUtils.checkpw(entry.getKey(), entry.getValue().getPassword()))
+              .map(u -> u.getValue()).filter(Objects::nonNull).findFirst();
+        }
+
+        @Override
+        public AuthWorkflowInfo getAuthWorkflowInfo() {
+            return new AuthWorkflowInfo(AccessControl.WORKFLOW_BASIC);
+        }
+    }
+}
diff --git a/pinot-controller/src/main/resources/app/app_state.ts b/pinot-controller/src/main/java/org/apache/pinot/controller/api/exception/UserAlreadyExistsException.java
similarity index 72%
copy from pinot-controller/src/main/resources/app/app_state.ts
copy to pinot-controller/src/main/java/org/apache/pinot/controller/api/exception/UserAlreadyExistsException.java
index 0ef89dc3c4..87ae4ba01f 100644
--- a/pinot-controller/src/main/resources/app/app_state.ts
+++ b/pinot-controller/src/main/java/org/apache/pinot/controller/api/exception/UserAlreadyExistsException.java
@@ -16,13 +16,14 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+package org.apache.pinot.controller.api.exception;
 
- import { AuthWorkflow } from "Models";
-class app_state {
-  queryConsoleOnlyView: boolean;
-  authWorkflow: AuthWorkflow;
-  authToken: string | null;
-  columnNameSeparator: string = '#$%';
-}
+public class UserAlreadyExistsException extends RuntimeException {
+    public UserAlreadyExistsException(String message) {
+        super(message);
+    }
 
-export default new app_state();
\ No newline at end of file
+    public UserAlreadyExistsException(String message, Throwable cause) {
+        super(message, cause);
+    }
+}
diff --git a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/Constants.java b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/Constants.java
index 714fbc4ff0..109bdfc565 100644
--- a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/Constants.java
+++ b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/Constants.java
@@ -20,7 +20,9 @@ package org.apache.pinot.controller.api.resources;
 
 import javax.ws.rs.WebApplicationException;
 import javax.ws.rs.core.Response.Status;
+import org.apache.commons.lang3.StringUtils;
 import org.apache.pinot.spi.config.table.TableType;
+import org.apache.pinot.spi.config.user.ComponentType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -33,6 +35,7 @@ public class Constants {
 
   public static final String CLUSTER_TAG = "Cluster";
   public static final String TABLE_TAG = "Table";
+  public static final String USER_TAG = "User";
   public static final String VERSION_TAG = "Version";
   public static final String HEALTH_TAG = "Health";
   public static final String INSTANCE_TAG = "Instance";
@@ -49,7 +52,7 @@ public class Constants {
   public static final String UPSERT_RESOURCE_TAG = "Upsert";
 
   public static TableType validateTableType(String tableTypeStr) {
-    if (tableTypeStr == null || tableTypeStr.isEmpty()) {
+    if (StringUtils.isBlank(tableTypeStr)) {
       return null;
     }
     try {
@@ -60,8 +63,20 @@ public class Constants {
     }
   }
 
+  public static ComponentType validateComponentType(String componentTypeStr) {
+    if (StringUtils.isBlank(componentTypeStr)) {
+      return null;
+    }
+    try {
+      return ComponentType.valueOf(componentTypeStr.toUpperCase());
+    } catch (IllegalArgumentException e) {
+      LOGGER.info("Illegal table type '{}'", componentTypeStr);
+      throw new WebApplicationException("Illegal table type '" + componentTypeStr + "'", Status.BAD_REQUEST);
+    }
+  }
+
   public static StateType validateState(String stateStr) {
-    if (stateStr == null) {
+    if (StringUtils.isBlank(stateStr)) {
       return null;
     }
     try {
diff --git a/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotAccessControlUserRestletResource.java b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotAccessControlUserRestletResource.java
new file mode 100644
index 0000000000..24b1b26a42
--- /dev/null
+++ b/pinot-controller/src/main/java/org/apache/pinot/controller/api/resources/PinotAccessControlUserRestletResource.java
@@ -0,0 +1,243 @@
+/**
+ * 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.pinot.controller.api.resources;
+
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import javax.inject.Inject;
+import javax.ws.rs.DELETE;
+import javax.ws.rs.GET;
+import javax.ws.rs.POST;
+import javax.ws.rs.PUT;
+import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
+import javax.ws.rs.Produces;
+import javax.ws.rs.QueryParam;
+import javax.ws.rs.core.Context;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.Response;
+import org.apache.helix.ZNRecord;
+import org.apache.helix.store.zk.ZkHelixPropertyStore;
+import org.apache.pinot.common.metadata.ZKMetadataProvider;
+import org.apache.pinot.common.utils.BcryptUtils;
+import org.apache.pinot.controller.api.access.AccessControlFactory;
+import org.apache.pinot.controller.api.access.AccessControlUtils;
+import org.apache.pinot.controller.api.access.AccessType;
+import org.apache.pinot.controller.api.access.Authenticate;
+import org.apache.pinot.controller.api.exception.ControllerApplicationException;
+import org.apache.pinot.controller.api.exception.UserAlreadyExistsException;
+import org.apache.pinot.controller.helix.core.PinotHelixResourceManager;
+import org.apache.pinot.spi.config.user.ComponentType;
+import org.apache.pinot.spi.config.user.UserConfig;
+import org.apache.pinot.spi.utils.JsonUtils;
+import org.glassfish.grizzly.http.server.Request;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+@Api(tags = Constants.USER_TAG)
+@Path("/")
+public class PinotAccessControlUserRestletResource {
+    /**
+     * URI Mappings:
+     * - "/user", "/users/": List all the users
+     * - "/users/{username}", "/users/{username}/": List config for specified username.
+     *
+     * - "/user", "/users/" : Add a user
+     * <pre>
+     *       POST Request Body Example :
+     *        {
+     *         "username": "user1",
+     *         "password": "user1@passwd",
+     *         "component": "BROKER",
+     *         "role" : "ADMIN",
+     *         "tables": ["table1", "table2"],
+     *         "permissions": ["READ"]
+     *        }
+     *  </pre>
+     *
+     *  - "/users/{username}", "/users/{username}/"
+     *  PUT Request body example : same as POST Request Body
+     * {@inheritDoc}
+     */
+    public static final Logger LOGGER = LoggerFactory.getLogger(PinotAccessControlUserRestletResource.class);
+
+    @Inject
+    PinotHelixResourceManager _pinotHelixResourceManager;
+
+
+    @Inject
+    AccessControlFactory _accessControlFactory;
+    AccessControlUtils _accessControlUtils = new AccessControlUtils();
+
+
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("/users")
+    @ApiOperation(value = "List all uses in cluster", notes = "List all users in cluster")
+    public String listUers(@Context HttpHeaders httpHeaders, @Context Request request) {
+        try {
+            String endpointUrl = request.getRequestURL().toString();
+            _accessControlUtils
+                .validatePermission(httpHeaders, endpointUrl, _accessControlFactory.create());
+            ZkHelixPropertyStore<ZNRecord> propertyStore = _pinotHelixResourceManager.getPropertyStore();
+            Map<String, UserConfig> allUserInfo = ZKMetadataProvider.getAllUserInfo(propertyStore);
+            return JsonUtils.newObjectNode().set("users", JsonUtils.objectToJsonNode(allUserInfo)).toString();
+        } catch (Exception e) {
+            throw new ControllerApplicationException(LOGGER, e.getMessage(), Response.Status.BAD_REQUEST, e);
+        }
+    }
+
+    @GET
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("/users/{username}")
+    @ApiOperation(value = "Get an user in cluster", notes = "Get an user in cluster")
+    public String getUser(@PathParam("username") String username, @QueryParam("component") String componentTypeStr,
+        @Context HttpHeaders httpHeaders, @Context Request request) {
+        try {
+            String endpointUrl = request.getRequestURL().toString();
+            _accessControlUtils
+                .validatePermission(httpHeaders, endpointUrl, _accessControlFactory.create());
+            ZkHelixPropertyStore<ZNRecord> propertyStore = _pinotHelixResourceManager.getPropertyStore();
+            ComponentType componentType = Constants.validateComponentType(componentTypeStr);
+            String usernameWithType = username + "_" + componentType.name();
+
+            UserConfig userConfig = ZKMetadataProvider.getUserConfig(propertyStore, usernameWithType);
+            return JsonUtils.newObjectNode().set(usernameWithType, JsonUtils.objectToJsonNode(userConfig)).toString();
+        } catch (Exception e) {
+            throw new ControllerApplicationException(LOGGER, e.getMessage(), Response.Status.BAD_REQUEST, e);
+        }
+    }
+
+    @POST
+    @Produces(MediaType.APPLICATION_JSON)
+    @Path("/users")
+    @ApiOperation(value = "Add a user", notes = "Add a user")
+    public SuccessResponse addUser(String userConfigStr, @Context HttpHeaders httpHeaders, @Context Request request) {
+        // TODO introduce a table config ctor with json string.
+
+        UserConfig userConfig;
+        String username;
+        try {
+            userConfig = JsonUtils.stringToObject(userConfigStr, UserConfig.class);
+            username = userConfig.getUserName();
+            String endpointUrl = request.getRequestURL().toString();
+            _accessControlUtils
+                .validatePermission(httpHeaders, endpointUrl, _accessControlFactory.create());
+            if (username.contains(".") || username.contains(" ")) {
+                throw new IllegalStateException("Username: " + username + " containing '.' or space is not allowed");
+            }
+        } catch (Exception e) {
+            throw new ControllerApplicationException(LOGGER, e.getMessage(), Response.Status.BAD_REQUEST, e);
+        }
+        try {
+            _pinotHelixResourceManager.addUser(userConfig);
+            return new SuccessResponse(String.format("User %s has been successfully added!",
+                userConfig.getUserName() + '_' + userConfig.getComponentType()));
+        } catch (Exception e) {
+            if (e instanceof UserAlreadyExistsException) {
+                throw new ControllerApplicationException(LOGGER, e.getMessage(), Response.Status.CONFLICT, e);
+            } else {
+                throw new ControllerApplicationException(LOGGER, e.getMessage(), Response.Status.BAD_REQUEST, e);
+            }
+        }
+    }
+
+    @DELETE
+    @Path("/users/{username}")
+    @Authenticate(AccessType.DELETE)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(value = "Delete a user", notes = "Delete a user")
+    public SuccessResponse deleteUser(@PathParam("username") String username,
+        @QueryParam("component") String componentTypeStr,
+        @Context HttpHeaders httpHeaders, @Context Request request) {
+
+        List<String> usersDeleted = new LinkedList<>();
+        String usernameWithComponentType = username + "_" + componentTypeStr;
+
+        try {
+
+            boolean userExist = false;
+            userExist = _pinotHelixResourceManager.hasUser(username, componentTypeStr);
+
+            String endpointUrl = request.getRequestURL().toString();
+            _accessControlUtils.validatePermission(httpHeaders, endpointUrl, _accessControlFactory.create());
+
+            _pinotHelixResourceManager.deleteUser(usernameWithComponentType);
+            if (userExist) {
+                usersDeleted.add(username);
+            }
+            if (!usersDeleted.isEmpty()) {
+                return new SuccessResponse("User: " + usernameWithComponentType + " has been successfully deleted");
+            }
+        } catch (Exception e) {
+            throw new ControllerApplicationException(LOGGER, e.getMessage(), Response.Status.BAD_REQUEST, e);
+        }
+
+        throw new ControllerApplicationException(LOGGER,
+            "User " + usernameWithComponentType + " does not exists", Response.Status.NOT_FOUND);
+    }
+
+
+    @PUT
+    @Path("/users/{username}")
+    @Authenticate(AccessType.UPDATE)
+    @Produces(MediaType.APPLICATION_JSON)
+    @ApiOperation(value = "Update user config for a user", notes = "Update user config for user")
+    public SuccessResponse updateUserConfig(
+        @PathParam("username") String username,
+        @QueryParam("component") String componentTypeStr,
+        @QueryParam("passwordChanged") boolean passwordChanged,
+        String userConfigString,
+        @Context HttpHeaders httpHeaders,
+        @Context Request request) {
+
+        UserConfig userConfig;
+        String usernameWithComponentType = username + "_" + componentTypeStr;
+        try {
+            String endpointUrl = request.getRequestURL().toString();
+            _accessControlUtils
+                .validatePermission(httpHeaders, endpointUrl, _accessControlFactory.create());
+
+            userConfig = JsonUtils.stringToObject(userConfigString, UserConfig.class);
+            if (passwordChanged) {
+                userConfig.setPassword(BcryptUtils.encrypt(userConfig.getPassword()));
+            }
+            String usernameWithComponentTypeFromUserConfig = userConfig.getUsernameWithComponent();
+            if (!usernameWithComponentType.equals(usernameWithComponentTypeFromUserConfig)) {
+                throw new ControllerApplicationException(LOGGER,
+                    String.format("Request user %s does not match %s in the Request body",
+                        usernameWithComponentType, usernameWithComponentTypeFromUserConfig),
+                    Response.Status.BAD_REQUEST);
+            }
+            if (!_pinotHelixResourceManager.hasUser(username, componentTypeStr)) {
+                throw new ControllerApplicationException(LOGGER,
+                    "Request user " + usernameWithComponentType + " does not exist",
+                    Response.Status.NOT_FOUND);
+            }
+            _pinotHelixResourceManager.updateUserConfig(userConfig);
+        } catch (Exception e) {
+            throw new ControllerApplicationException(LOGGER, e.getMessage(), Response.Status.BAD_REQUEST, e);
+        }
+        return new SuccessResponse("User config update for " + usernameWithComponentType);
+    }
+}
diff --git a/pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/PinotHelixResourceManager.java b/pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/PinotHelixResourceManager.java
index 0e4d25c9ab..1104f8738f 100644
--- a/pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/PinotHelixResourceManager.java
+++ b/pinot-controller/src/main/java/org/apache/pinot/controller/helix/core/PinotHelixResourceManager.java
@@ -37,6 +37,7 @@ import java.util.Iterator;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.TimeZone;
 import java.util.TreeMap;
@@ -97,7 +98,9 @@ import org.apache.pinot.common.metadata.ZKMetadataProvider;
 import org.apache.pinot.common.metadata.instance.InstanceZKMetadata;
 import org.apache.pinot.common.metadata.segment.SegmentZKMetadata;
 import org.apache.pinot.common.minion.MinionTaskMetadataUtils;
+import org.apache.pinot.common.utils.BcryptUtils;
 import org.apache.pinot.common.utils.HashUtil;
+import org.apache.pinot.common.utils.config.AccessControlUserConfigUtils;
 import org.apache.pinot.common.utils.config.InstanceUtils;
 import org.apache.pinot.common.utils.config.TableConfigUtils;
 import org.apache.pinot.common.utils.config.TagNameUtils;
@@ -107,6 +110,7 @@ import org.apache.pinot.controller.ControllerConf;
 import org.apache.pinot.controller.api.exception.ControllerApplicationException;
 import org.apache.pinot.controller.api.exception.InvalidTableConfigException;
 import org.apache.pinot.controller.api.exception.TableAlreadyExistsException;
+import org.apache.pinot.controller.api.exception.UserAlreadyExistsException;
 import org.apache.pinot.controller.api.resources.InstanceInfo;
 import org.apache.pinot.controller.api.resources.StateType;
 import org.apache.pinot.controller.helix.core.assignment.instance.InstanceAssignmentDriver;
@@ -133,6 +137,9 @@ import org.apache.pinot.spi.config.table.TenantConfig;
 import org.apache.pinot.spi.config.table.UpsertConfig;
 import org.apache.pinot.spi.config.table.assignment.InstancePartitionsType;
 import org.apache.pinot.spi.config.tenant.Tenant;
+import org.apache.pinot.spi.config.user.ComponentType;
+import org.apache.pinot.spi.config.user.RoleType;
+import org.apache.pinot.spi.config.user.UserConfig;
 import org.apache.pinot.spi.data.Schema;
 import org.apache.pinot.spi.stream.StreamConfig;
 import org.apache.pinot.spi.utils.CommonConstants;
@@ -159,7 +166,7 @@ public class PinotHelixResourceManager {
 
   // TODO: make this configurable
   public static final long EXTERNAL_VIEW_ONLINE_SEGMENTS_MAX_WAIT_MS = 10 * 60_000L; // 10 minutes
-  public static final long EXTERNAL_VIEW_CHECK_INTERVAL_MS = 1_000L; // 1 second
+  public static final long EXTERNAL_VIEW_CHECK_INTERVAL_MS = 1_000L; // 1 secondL
 
   private static final SimpleDateFormat SIMPLE_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'");
 
@@ -1378,6 +1385,35 @@ public class PinotHelixResourceManager {
             AccessOption.PERSISTENT);
   }
 
+  public void initUserACLConfig(ControllerConf controllerConf) throws IOException {
+    if (CollectionUtils.isEmpty(ZKMetadataProvider.getAllUserName(_propertyStore))) {
+      String initUsername = controllerConf.getInitAccessControlUsername();
+      String initPassword = controllerConf.getInitAccessControlPassword();
+      addUser(new UserConfig(initUsername, initPassword, ComponentType.CONTROLLER.name(),
+          RoleType.ADMIN.name(), null, null));
+      addUser(new UserConfig(initUsername, initPassword, ComponentType.BROKER.name(),
+          RoleType.ADMIN.name(), null, null));
+      addUser(new UserConfig(initUsername, initPassword, ComponentType.SERVER.name(),
+          RoleType.ADMIN.name(), null, null));
+    }
+  }
+
+  public void addUser(UserConfig userConfig)
+      throws IOException {
+    String usernamePrefix = userConfig.getUserName() + "_" + userConfig.getComponentType();
+    boolean isExists = Optional.ofNullable(ZKMetadataProvider.getAllUserConfig(_propertyStore))
+        .orElseGet(() -> {
+          return new ArrayList();
+        }).contains(userConfig);
+    if (isExists) {
+      throw new UserAlreadyExistsException("User " + usernamePrefix + " already exists");
+    }
+    userConfig.setPassword(BcryptUtils.encrypt(userConfig.getPassword()));
+    ZKMetadataProvider
+        .setUserConfig(_propertyStore, usernamePrefix, AccessControlUserConfigUtils.toZNRecord(userConfig));
+    LOGGER.info("Successfully add user:{}", usernamePrefix);
+  }
+
   /**
    * Performs validations of table config and adds the table to zookeeper
    * @throws InvalidTableConfigException if validations fail
@@ -1675,6 +1711,13 @@ public class PinotHelixResourceManager {
     }
   }
 
+  public void updateUserConfig(UserConfig userConfig)
+      throws IOException {
+    String usernameWithComponent = userConfig.getUsernameWithComponent();
+    ZKMetadataProvider.setUserConfig(_propertyStore, usernameWithComponent,
+      AccessControlUserConfigUtils.toZNRecord(userConfig));
+  }
+
   /**
    * Validate the table config and update it
    * @throws IOException
@@ -1769,6 +1812,12 @@ public class PinotHelixResourceManager {
     setExistingTableConfig(tableConfig);
   }
 
+  public void deleteUser(String username) {
+    ZKMetadataProvider.removeUserConfigFromPropertyStore(_propertyStore, username);
+    LOGGER.info("Deleting user{}: Removed from user resouces", username);
+    LOGGER.info("Deleting user{} finished", username);
+  }
+
   public void deleteOfflineTable(String tableName) {
     String offlineTableName = TableNameBuilder.OFFLINE.tableNameWithType(tableName);
     LOGGER.info("Deleting table {}: Start", offlineTableName);
@@ -2592,6 +2641,11 @@ public class PinotHelixResourceManager {
     return "/SEGMENTS/" + tableNameWithType + "/" + segmentName;
   }
 
+  public boolean hasUser(String username, String component) {
+    return ZKMetadataProvider.getAllUserConfig(_propertyStore)
+        .stream().anyMatch(user -> user.isExist(username, ComponentType.valueOf(component)));
+  }
+
   public boolean hasTable(String tableNameWithType) {
     return getAllResources().contains(tableNameWithType);
   }
diff --git a/pinot-controller/src/main/resources/app/App.tsx b/pinot-controller/src/main/resources/app/App.tsx
index 0e44fd961f..1e54b37161 100644
--- a/pinot-controller/src/main/resources/app/App.tsx
+++ b/pinot-controller/src/main/resources/app/App.tsx
@@ -52,6 +52,29 @@ const App = () => {
   const [authorizationEndpoint, setAuthorizationEndpoint] = React.useState(
     null
   );
+  const [role, setRole] = React.useState('');
+
+  const fetchUserRole = async()=>{
+    const userListResponse = await PinotMethodUtils.getUserList();
+    let userObj = userListResponse.users;
+    let userData = [];
+    for (let key in userObj) {
+      if(userObj.hasOwnProperty(key)){
+        userData.push(userObj[key]);
+      }
+    }
+    let role = "";
+
+    for (let item of userData) {
+      if (item.username === app_state.username && item.component === 'CONTROLLER') {
+        role = item.role;
+      }
+    }
+    if (role === 'ADMIN') {
+      app_state.role = "ADMIN";
+      setRole('ADMIN')
+    }
+  }
 
   const fetchClusterName = async () => {
     const clusterNameResponse = await PinotMethodUtils.getClusterName();
@@ -154,6 +177,7 @@ const App = () => {
     if (isAuthenticated) {
       fetchClusterConfig();
       fetchClusterName();
+      fetchUserRole();
     }
   }, [isAuthenticated]);
 
@@ -165,10 +189,10 @@ const App = () => {
     )
   };
 
-  const componentRender = (Component, props) => {
+  const componentRender = (Component, props, role) => {
     return (
       <div className="p-8">
-        <Layout clusterName={clusterName} {...props}>
+        <Layout clusterName={clusterName} {...props} role={role}>
           <Component {...props} />
         </Layout>
       </div>
@@ -218,7 +242,7 @@ const App = () => {
                           return loginRender(Component, props);
                         } else if (isAuthenticated) {
                           // default render
-                          return componentRender(Component, props);
+                          return componentRender(Component, props, role);
                         } else {
                           return <Redirect to="/login" />;
                         }
diff --git a/pinot-controller/src/main/resources/app/app_state.ts b/pinot-controller/src/main/resources/app/app_state.ts
index 0ef89dc3c4..61ca255b2f 100644
--- a/pinot-controller/src/main/resources/app/app_state.ts
+++ b/pinot-controller/src/main/resources/app/app_state.ts
@@ -23,6 +23,8 @@ class app_state {
   authWorkflow: AuthWorkflow;
   authToken: string | null;
   columnNameSeparator: string = '#$%';
+  username: string;
+  role: string;
 }
 
 export default new app_state();
\ No newline at end of file
diff --git a/pinot-controller/src/main/resources/app/components/Breadcrumbs.tsx b/pinot-controller/src/main/resources/app/components/Breadcrumbs.tsx
index e0de1eb659..18b967a903 100644
--- a/pinot-controller/src/main/resources/app/components/Breadcrumbs.tsx
+++ b/pinot-controller/src/main/resources/app/components/Breadcrumbs.tsx
@@ -58,7 +58,8 @@ const breadcrumbNameMap: { [key: string]: string } = {
   '/tables': 'Tables',
   '/query': 'Query Console',
   '/cluster': 'Cluster Manager',
-  '/zookeeper': 'Zookeeper Browser'
+  '/zookeeper': 'Zookeeper Browser',
+  '/user': 'User Console'
 };
 
 const BreadcrumbsComponent = ({ ...props }) => {
diff --git a/pinot-controller/src/main/resources/app/components/Layout.tsx b/pinot-controller/src/main/resources/app/components/Layout.tsx
index e910218ac9..0d316ff0f8 100644
--- a/pinot-controller/src/main/resources/app/components/Layout.tsx
+++ b/pinot-controller/src/main/resources/app/components/Layout.tsx
@@ -26,8 +26,9 @@ import SwaggerIcon from './SvgIcons/SwaggerIcon';
 import ClusterManagerIcon from './SvgIcons/ClusterManagerIcon';
 import ZookeeperIcon from './SvgIcons/ZookeeperIcon';
 import app_state from '../app_state';
+import AccountCircleOutlinedIcon from '@material-ui/icons/AccountCircleOutlined';
 
-const navigationItems = [
+let navigationItems = [
   { id: 1, name: 'Cluster Manager', link: '/', icon: <ClusterManagerIcon /> },
   { id: 2, name: 'Query Console', link: '/query', icon: <QueryConsoleIcon /> },
   { id: 3, name: 'Zookeeper Browser', link: '/zookeeper', icon: <ZookeeperIcon /> },
@@ -35,6 +36,15 @@ const navigationItems = [
 ];
 
 const Layout = (props) => {
+  const role = props.role;
+  if(role === 'ADMIN'){
+    if(navigationItems.length <5){
+      navigationItems = [
+        ...navigationItems,
+        {id: 5, name: "User Console", link: '/user', icon: <AccountCircleOutlinedIcon style={{ width: 24, height: 24, verticalAlign: 'sub' }}/>}
+      ]
+    }
+  }
   const hash = `/${window.location.hash.split('/')[1]}`;
   const routeObj = navigationItems.find((obj)=>{ return obj.link === hash;});
 
diff --git a/pinot-controller/src/main/resources/app/components/User/AddUser.tsx b/pinot-controller/src/main/resources/app/components/User/AddUser.tsx
new file mode 100644
index 0000000000..08002a1f58
--- /dev/null
+++ b/pinot-controller/src/main/resources/app/components/User/AddUser.tsx
@@ -0,0 +1,243 @@
+/**
+ * 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.
+ */
+
+import React, { useState } from 'react';
+import {
+  Checkbox,
+  DialogContent,
+  FormControl,
+  Input,
+  InputLabel, ListItemText,
+  MenuItem,
+  Select,
+  TextField,
+  InputAdornment,
+  IconButton,
+} from '@material-ui/core';
+import Visibility from '@material-ui/icons/Visibility';
+import VisibilityOff from '@material-ui/icons/VisibilityOff';
+import {cloneDeep} from "lodash";
+type Props = {
+  tableList: Array<{
+    name: string,
+    checked: boolean,
+    disabled: boolean
+  }>,
+  setUserInfo: Function,
+  userProp?: {
+    username: string,
+    password: string,
+    component: string,
+    role: string,
+    tables: Array<string>,
+    permissions: Array<string>
+  }
+}
+
+export default function AddUser({
+  tableList,
+  setUserInfo,
+  userProp
+}: Props) {
+  const PERMISSIONS = ["READ", "CREATE", "UPDATE", "DELETE"];
+  const [user, setUser] = useState({
+      ...userProp,
+      tables: ["DUAL"]
+  });
+  const [tables, setTables] = useState(tableList);
+  const [showPassword, setShowPassword] = useState(false);
+  const handleClickShowPassword = () => {
+    setShowPassword(!showPassword);
+  };
+  const handleMouseDownPassword = (event) => {
+    event.preventDefault();
+  };
+  const changeHandler = (field, e)=>{
+    let val = e.target.value;
+    let userCopy = cloneDeep(user);
+    switch (field) {
+      case 'username':
+        userCopy.username = val;
+        break;
+      case 'password':
+        userCopy.password = val;
+        break;
+      case 'component':
+        userCopy.component = val;
+        break;
+      case 'role':
+        userCopy.role = val;
+        break;
+      case 'permissions':
+        userCopy.permissions = val;
+        break;
+    }
+    setUser(userCopy);
+    setUserInfo(userCopy);
+  }
+  const tableCheckChange = (event)=>{
+    let name = event.target.name;
+    let checked = event.target.checked;
+    if(name){
+      let table = cloneDeep(tables);
+      if(name === 'ALL' && checked === true){
+        table = table.map(item =>{
+          return {
+            ...item,
+            checked: item.name === 'ALL',
+            disabled: item.name !== 'ALL'
+          }
+        })
+      } else if(name === 'ALL' && checked === false){
+        table = table.map(item =>{
+          return {
+            ...item,
+            checked: item.name === 'ALL' ? false: item.checked,
+            disabled: false
+          }
+        })
+      } else{
+        table = table.map(item =>{
+          return {
+            ...item,
+            checked: item.name === name ? checked: item.checked
+          }
+        })
+      }
+      setTables(table);
+      let checkedCollection = [];
+      for(let item of table){
+        if(item.checked){
+          checkedCollection.push(item.name);
+        }
+      }
+      let newUser = {
+          ...user,
+        tables: checkedCollection
+      }
+      setUser(newUser)
+      setUserInfo(newUser);
+    }
+  }
+  const menuItemStyle = {
+    fontSize: '16px',
+  }
+  return (
+      <DialogContent style={{fontSize: '14px'}}>
+        <TextField
+            autoFocus
+            margin="dense"
+            id="name"
+            label="User Name"
+            fullWidth
+            value={user.username}
+            onChange={(e)=>changeHandler('username', e)}
+        />
+        <InputLabel htmlFor="standard-adornment-password" style={{marginTop: '10px'}}>Password</InputLabel>
+        <Input
+            id="standard-adornment-password"
+            type={showPassword ? 'text' : 'password'}
+            autoComplete='new-password'
+            value={user.password}
+            onChange={(e)=>changeHandler('password', e)}
+            endAdornment={
+              <InputAdornment position="end">
+                <IconButton
+                    aria-label="toggle password visibility"
+                    onClick={handleClickShowPassword}
+                    onMouseDown={handleMouseDownPassword}
+                >
+                  {showPassword ? <Visibility /> : <VisibilityOff />}
+                </IconButton>
+              </InputAdornment>
+            }
+        />
+        <FormControl fullWidth style={{margin: '10px 0'}}>
+          <InputLabel id="component">Component</InputLabel>
+          <Select
+              autoWidth={true}
+              labelId="component"
+              id="component"
+              value={user.component}
+              label="Component"
+              required
+              onChange={(e)=>changeHandler('component', e)}
+
+          >
+            <MenuItem value={'CONTROLLER'} style={menuItemStyle}>CONTROLLER</MenuItem>
+            <MenuItem value={'BROKER'} style={menuItemStyle}>BROKER</MenuItem>
+            <MenuItem value={'SERVER'} style={menuItemStyle}>SERVER</MenuItem>
+          </Select>
+        </FormControl>
+        <FormControl fullWidth style={{margin: '10px 0'}}>
+          <InputLabel id="role">Role</InputLabel>
+          <Select
+              labelId="role"
+              id="role"
+              value={user.role}
+              required
+              onChange={(e)=>changeHandler('role', e)}
+          >
+            <MenuItem value={'ADMIN'} style={menuItemStyle}>ADMIN</MenuItem>
+            <MenuItem value={'USER'} style={menuItemStyle}>USER</MenuItem>
+          </Select>
+        </FormControl>
+        <FormControl fullWidth style={{margin: '10px 0'}}>
+          <InputLabel id="permission">Permissions</InputLabel>
+          <Select
+              autoWidth={true}
+              labelId="permission"
+              id="permission"
+              value={user.permissions}
+              label="Permissions"
+              multiple={true}
+              required
+              renderValue={(selected: Array<String>) => selected.join(', ')}
+              onChange={(e)=>changeHandler('permissions', e)}
+          >
+            {PERMISSIONS.map((name) => (
+                <MenuItem key={name} value={name} style={menuItemStyle}>
+                  <Checkbox size="small" checked={user.permissions.indexOf(name) > -1} color="primary"/>
+                  <span style={{fontSize: '16px'}}>{name}</span>
+                </MenuItem>
+            ))}
+          </Select>
+        </FormControl>
+        <FormControl fullWidth style={{margin: '10px 0'}}>
+          <InputLabel id="tables">Tables</InputLabel>
+          <Select
+              labelId="tables"
+              id="tables"
+              value={user.tables}
+              multiple={true}
+              required
+              renderValue={(selected: Array<String>) => selected.join(', ')}
+              onChange={(e)=>changeHandler('tables', e)}
+          >
+            {tables.map(table=>(
+                <MenuItem key={table.name} value={table.name} style={menuItemStyle}>
+                  <Checkbox checked={table.checked} name={table.name} disabled={table.disabled} size="small" color="primary" onChange={tableCheckChange}/>
+                  <ListItemText primary={table.name} style={{fontSize: '14px'}}/>
+                </MenuItem>
+            ))}
+          </Select>
+        </FormControl>
+      </DialogContent>
+  );
+}
\ No newline at end of file
diff --git a/pinot-controller/src/main/resources/app/components/User/UpdateUser.tsx b/pinot-controller/src/main/resources/app/components/User/UpdateUser.tsx
new file mode 100644
index 0000000000..bdcbe29f99
--- /dev/null
+++ b/pinot-controller/src/main/resources/app/components/User/UpdateUser.tsx
@@ -0,0 +1,235 @@
+/**
+ * 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.
+ */
+
+import React, { useState } from 'react';
+import {
+  Checkbox,
+  DialogContent,
+  FormControl,
+  IconButton,
+  Input, InputAdornment,
+  InputLabel, ListItemText,
+  makeStyles,
+  MenuItem,
+  Select,
+} from '@material-ui/core';
+import Visibility from "@material-ui/icons/Visibility";
+import VisibilityOff from "@material-ui/icons/VisibilityOff";
+import {cloneDeep} from "lodash";
+type Props = {
+  tableList: Array<{
+    name: string,
+    checked: boolean,
+    disabled: boolean
+  }>,
+  setUserInfo: Function,
+  userProp?: {
+    username: string,
+    password: string,
+    component: string,
+    role: string,
+    tables: Array<string>,
+    permissions: Array<string>
+  }
+}
+
+export default function UpdateUser({
+  tableList,
+  setUserInfo,
+  userProp
+}: Props) {
+
+  const PERMISSIONS = ["READ", "CREATE", "UPDATE", "DELETE"];
+  const [user, setUser] = useState(userProp);
+  const transferTable = tableList.map(item=>{
+    return {
+        ...item,
+      checked: !!userProp.tables.includes(item.name)
+    }
+  })
+  const [tables, setTables] = useState(transferTable);
+  const [showPassword, setShowPassword] = useState(false);
+  const handleClickShowPassword = () => {
+    setShowPassword(!showPassword);
+  };
+  const handleMouseDownPassword = (event) => {
+    event.preventDefault();
+  };
+  const changeHandler = (field, e)=>{
+    let val = e.target.value;
+    let userCopy = cloneDeep(user);
+    switch (field) {
+      case 'username':
+        userCopy.username = val;
+        break;
+      case 'password':
+        userCopy.password = val;
+        break;
+      case 'component':
+        userCopy.component = val;
+        break;
+      case 'role':
+        userCopy.role = val;
+        break;
+      case 'permissions':
+        userCopy.permissions = val;
+        break;
+    }
+    setUser(userCopy);
+    setUserInfo(userCopy);
+  }
+
+  const tableCheckChange = (event)=>{
+    let name = event.target.name;
+    let checked = event.target.checked;
+    if(name){
+      let table = cloneDeep(tables);
+      console.log("table: ", table);
+      if(name === 'ALL' && checked === true){
+        table = table.map(item =>{
+          return {
+            ...item,
+            checked: item.name === 'ALL',
+            disabled: item.name !== 'ALL'
+          }
+        })
+      } else if(name === 'ALL' && checked === false){
+        table = table.map(item =>{
+          return {
+            ...item,
+            checked: item.name === 'ALL' ? false: item.checked,
+            disabled: false
+          }
+        })
+      } else{
+        table = table.map(item =>{
+          return {
+            ...item,
+            checked: item.name === name ? checked: item.checked
+          }
+        })
+      }
+      setTables(table);
+      let checkedCollection = [];
+      for(let item of table){
+        if(item.checked){
+          checkedCollection.push(item.name);
+        }
+      }
+      let newUser = {
+        ...user,
+        tables: checkedCollection
+      }
+      setUser(newUser)
+      setUserInfo(newUser);
+    }
+  }
+
+  const menuItemStyle = {
+    fontSize: '16px',
+  }
+
+  const useStyles = makeStyles({
+     text: {
+       fontSize: '18px',
+       color: 'rgba(0, 0, 0, 0.87)'
+     },
+     field: {
+       marginRight: '10px',
+       color: 'rgba(0, 0, 0, 0.57)'
+     }
+  });
+  const classes = useStyles();
+
+  return (
+      <DialogContent style={{fontSize: '14px'}}>
+        <div className={classes.text} style={{marginBottom: '10px'}}><span className={classes.field}>User Name:</span> {user.username}</div>
+        <div className={classes.text}><span className={classes.field}>Component:</span> {user.component}</div>
+        <InputLabel htmlFor="standard-adornment-password" style={{marginTop: '10px'}}>Password</InputLabel>
+        <Input
+            id="standard-adornment-password"
+            type={showPassword ? 'text' : 'password'}
+            autoComplete='new-password'
+            value={user.password}
+            onChange={(e)=>changeHandler('password', e)}
+            endAdornment={
+              <InputAdornment position="end">
+                <IconButton
+                    aria-label="toggle password visibility"
+                    onClick={handleClickShowPassword}
+                    onMouseDown={handleMouseDownPassword}
+                >
+                  {showPassword ? <Visibility /> : <VisibilityOff />}
+                </IconButton>
+              </InputAdornment>
+            }
+        />
+        <FormControl fullWidth style={{margin: '10px 0'}}>
+          <InputLabel id="role">Role</InputLabel>
+          <Select
+              labelId="role"
+              id="role"
+              value={user.role}
+              onChange={(e)=>changeHandler('role', e)}
+          >
+            <MenuItem value={'ADMIN'} style={menuItemStyle}>ADMIN</MenuItem>
+            <MenuItem value={'USER'} style={menuItemStyle}>USER</MenuItem>
+          </Select>
+        </FormControl>
+        <FormControl fullWidth style={{margin: '10px 0'}}>
+          <InputLabel id="permission">Permissions</InputLabel>
+          <Select
+              autoWidth={true}
+              labelId="permission"
+              id="permission"
+              value={user.permissions}
+              label="Permissions"
+              multiple={true}
+              renderValue={(selected: Array<String>) => selected.join(', ')}
+              onChange={(e)=>changeHandler('permissions', e)}
+          >
+            {PERMISSIONS.map((name) => (
+                <MenuItem key={name} value={name} style={menuItemStyle}>
+                  <Checkbox size="small" checked={user.permissions.indexOf(name) > -1} color="primary"/>
+                  <span style={{fontSize: '16px'}}>{name}</span>
+                </MenuItem>
+            ))}
+          </Select>
+        </FormControl>
+        <FormControl fullWidth style={{margin: '10px 0'}}>
+          <InputLabel id="tables">Tables</InputLabel>
+          <Select
+              labelId="tables"
+              id="tables"
+              value={user.tables}
+              multiple={true}
+              renderValue={(selected: Array<String>) => selected.join(', ')}
+              onChange={(e)=>changeHandler('tables', e)}
+          >
+            {tables.map(table=>(
+                <MenuItem key={table.name} value={table.name} style={menuItemStyle}>
+                  <Checkbox checked={table.checked} name={table.name} disabled={table.disabled} size="small" color="primary" onChange={tableCheckChange}/>
+                  <ListItemText primary={table.name} style={{fontSize: '14px'}}/>
+                </MenuItem>
+            ))}
+          </Select>
+        </FormControl>
+      </DialogContent>
+  );
+}
\ No newline at end of file
diff --git a/pinot-controller/src/main/resources/app/interfaces/types.d.ts b/pinot-controller/src/main/resources/app/interfaces/types.d.ts
index afa20fed5d..609671d873 100644
--- a/pinot-controller/src/main/resources/app/interfaces/types.d.ts
+++ b/pinot-controller/src/main/resources/app/interfaces/types.d.ts
@@ -161,4 +161,21 @@ declare module 'Models' {
     BASIC = 'BASIC',
     OIDC = 'OIDC',
   }
+
+  export type TableList = {
+    tables: Array<string>
+  }
+
+  export type UserObject = {
+    username: string,
+    password: string,
+    component: string,
+    role: string,
+    tables: Array<string>,
+    permissions: Array<string>
+  }
+
+  export type UserList = {
+    users: UserObject
+  }
 }
diff --git a/pinot-controller/src/main/resources/app/pages/LoginPage.tsx b/pinot-controller/src/main/resources/app/pages/LoginPage.tsx
index 320f40481e..37d4b3b18e 100644
--- a/pinot-controller/src/main/resources/app/pages/LoginPage.tsx
+++ b/pinot-controller/src/main/resources/app/pages/LoginPage.tsx
@@ -81,6 +81,7 @@ const LoginPage = (props) => {
       app_state.authWorkflow = AuthWorkflow.BASIC;
       app_state.authToken = authToken;
       props.history.push(app_state.queryConsoleOnlyView ? '/query' : '/');
+      app_state.username = data.username;
     } else {
       setInvalidToken(true);
     }
diff --git a/pinot-controller/src/main/resources/app/pages/UserPage.tsx b/pinot-controller/src/main/resources/app/pages/UserPage.tsx
new file mode 100644
index 0000000000..71c8130413
--- /dev/null
+++ b/pinot-controller/src/main/resources/app/pages/UserPage.tsx
@@ -0,0 +1,296 @@
+/* eslint-disable no-nested-ternary */
+/**
+ * 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.
+ */
+
+import React, {useEffect, useState} from 'react';
+import {Button, Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Dialog, DialogActions, Snackbar, DialogTitle} from '@material-ui/core';
+import Alert from '@material-ui/lab/Alert';
+import { makeStyles } from '@material-ui/core/styles';
+import PinotMethodUtils from '../utils/PinotMethodUtils';
+import '../styles/styles.css';
+import AddUser from "../components/User/AddUser";
+import UpdateUser from "../components/User/UpdateUser";
+import * as _ from "lodash";
+import AppLoader from "../components/AppLoader";
+
+const initialUser = {
+    username: "",
+    password: "",
+    component: "",
+    role: "",
+    tables: [],
+    permissions: ['READ']
+}
+const PERMISSIONS = ["READ", "CREATE", "UPDATE", "DELETE"];
+const columns = [
+    { id: 'User', label: 'User'},
+    { id: 'Component', label: 'Component'},
+    { id: 'Role', label: 'Role'},
+    { id: 'Permissions', label: 'Permissions'},
+    { id: 'Tables', label: 'Tables'},
+    { id: 'Actions', label: 'Actions'}
+];
+let oldPassword = "";
+const UserPage = () => {
+    const [userList, setUserList] = useState([]);
+    const [addUserPanel, setAddUserPanel] = useState(false);
+    const [deleteUserPanel, setDeleteUserPanel] = useState(false);
+    const [updateUserPanel, setUpdateUserPanel] = useState(false);
+    const [tableList, setTableList] = useState([]);
+    const [userInfo, updateUserInfo] = useState(initialUser);
+    const [errorTip, setErrorTip] = useState(null);
+    const [showError, setShowError] = useState(false);
+    const [successTip, setSuccessTip] = useState(null);
+    const [showSuccess, setShowSuccess] = useState(false);
+    const [checkedUser, setCheckedUser] = useState(initialUser);
+    const [fetching, setFetching] = useState(false);
+    const getUserList = async () => {
+        setFetching(true);
+        const tablesResponse = await PinotMethodUtils.getTable();
+        let tables = tablesResponse.tables;
+        tables.unshift("DUAL", "ALL");
+        let tableList = tables.map(item=>{
+            return {
+                name: item,
+                checked: item === 'DUAL',
+                disabled: false
+            }
+        })
+        setTableList(tableList);
+        const userListResponse = await PinotMethodUtils.getUserList();
+        let userObj = userListResponse.users;
+        let userData = [];
+        for (let key in userObj) {
+            if(userObj.hasOwnProperty(key)){
+                userData.push(userObj[key]);
+            }
+        }
+        userData = userData.map(item => {
+            return {
+                component: item.component,
+                role: item.role,
+                username: item.username,
+                usernameWithComponent: item.usernameWithComponent,
+                tables: item.tables ? item.tables: ["ALL"],
+                permissions: item.permissions ? item.permissions : PERMISSIONS,
+                password: item.password
+            }
+        })
+        userData.sort((prev, next)=>{
+            let prevUser = prev.username.toUpperCase();
+            let nextUser = next.username.toUpperCase();
+            if (prevUser < nextUser) {
+                return -1;
+            }
+            if (prevUser > nextUser) {
+                return 1;
+            }
+            return 0;
+        })
+        setUserList(userData);
+        setFetching(false);
+    };
+
+    const toggleAddUserPanel = ()=>{
+        updateUserInfo(initialUser);
+        setAddUserPanel(true);
+    };
+    const closeAddUserPanel = ()=>{
+        setAddUserPanel(false)
+    };
+
+    const submitAddUser = async()=>{
+        let addUserParam = _.cloneDeep(userInfo);
+        if(addUserParam.tables.includes('ALL')){
+            delete addUserParam.tables
+        }
+        const addUserResponse = await PinotMethodUtils.addUser(addUserParam);
+        if(addUserResponse.code && addUserResponse.code !== 200){
+            setShowError(true);
+            setErrorTip(addUserResponse.error);
+        }else{
+            setAddUserPanel(false);
+            setShowSuccess(true);
+            setSuccessTip("Add user success");
+            getUserList();
+        }
+    };
+
+    const closeTip = (type)=>{
+        type === 'error' ? setShowError(false): setShowSuccess(false);
+    };
+
+    const deleteUser = async()=>{
+        const deleteUserResponse = await PinotMethodUtils.deleteUser(checkedUser);
+        if(deleteUserResponse.code && deleteUserResponse.code !== 200){
+            setShowError(true);
+            setErrorTip(deleteUserResponse.error);
+        }else{
+            setDeleteUserPanel(false);
+            setShowSuccess(true);
+            setSuccessTip("delete user success");
+            getUserList();
+        }
+    }
+
+    const clickDeleteUser = (row)=>{
+        setCheckedUser(row);
+        setDeleteUserPanel(true);
+    }
+
+    const clickEditUser = (row)=>{
+        oldPassword = row.password;
+        updateUserInfo(row);
+        setUpdateUserPanel(true);
+    }
+
+    const closeUpdateUserPanel = ()=>{
+        setUpdateUserPanel(false);
+    }
+
+    const submitUpdateUser = async()=>{
+        let updateUserParam = _.cloneDeep(userInfo);
+        if(updateUserParam.tables.includes('ALL')){
+            delete updateUserParam.tables
+        }
+        const passwordChanged = updateUserParam.password !== oldPassword ? true: false;
+        const updateUserResponse = await PinotMethodUtils.updateUser(updateUserParam, passwordChanged);
+        if(updateUserResponse.code && updateUserResponse.code !== 200){
+            setShowError(true);
+            setErrorTip(updateUserResponse.error);
+        }else{
+            setUpdateUserPanel(false);
+            setShowSuccess(true);
+            setSuccessTip("Update user success");
+            getUserList();
+        }
+    }
+
+    useEffect(() => {
+        getUserList();
+    }, []);
+
+    const useStyles = makeStyles({
+        userContainer: {
+            width: 'calc(100% - 260px)', padding: '32px', height: 'calc(100vh - 70px)', fontSize: '14px'
+        },
+        tableBody: {
+            maxHeight: 'calc(100vh - 254px)', overflowY: 'auto'
+        },
+        tableColItemStyle: {
+            background: "#1170CF", borderRadius: '4px', padding: '4px 8px', display: 'inline-block', color: '#fff',
+            fontSize: '14px', marginRight: '18px', marginTop: '5px', marginBottom: '5px'
+        },
+        delButton: {
+            borderRadius: '16px',
+            color: 'rgb(220, 0, 78)',
+            border: '1px solid rgba(220, 0, 78, 0.5)',
+            "&:hover": {
+                background: 'rgba(220, 0, 78, 0.2)'
+            }
+        },
+
+    });
+    const classes = useStyles();
+    return (
+        <div className={classes.userContainer}>
+            <Button variant="contained" onClick={toggleAddUserPanel} color="primary" style={{marginBottom: '20px', borderRadius: '16px'}}>Add User</Button>
+            {fetching ? <AppLoader/>: <TableContainer component={Paper} style={{height: 'calc(100% - 60px)'}}>
+                <Table aria-label="simple table">
+                    <TableHead>
+                        <TableRow>
+                            {columns.map((column) => (
+                                <TableCell style={{fontWeight: 700}} key={column.id}>
+                                    {column.label}
+                                </TableCell>
+                            ))}
+                        </TableRow>
+                    </TableHead>
+                    <TableBody className={classes.tableBody}>
+                        {userList.map((row) => (
+                            <TableRow key={row.usernameWithComponent}>
+                                <TableCell component="th" scope="row">
+                                    {row.username}
+                                </TableCell>
+                                <TableCell align="justify" >{row.component}</TableCell>
+                                <TableCell align="justify">{row.role}</TableCell>
+                                <TableCell align="justify">{
+                                    row.permissions && row.permissions.length >0 && row.permissions.map(permission => {
+                                        return <span className={classes.tableColItemStyle} key={permission}>{permission}</span>
+                                    })}
+                                </TableCell>
+                                <TableCell align="justify" style={{wordWrap: "break-word"}}>
+                                    {row.tables && row.tables.length > 0 && row.tables.map(table =>{
+                                        return <span className={classes.tableColItemStyle} key={table}>{table}</span>
+                                    })}
+                                </TableCell>
+                                <TableCell align="justify" style={{minWidth: '200px'}}>
+                                    <Button variant="outlined" size="small" style={{marginRight: '10px', borderRadius: '16px'}} onClick={()=>clickEditUser(row)}>Edit</Button>&nbsp;
+                                    <Button variant="outlined" onClick={()=>clickDeleteUser(row)} size="small" className={classes.delButton}>Delete</Button>
+                                </TableCell>
+                            </TableRow>
+                        ))}
+                    </TableBody>
+                </Table>
+            </TableContainer>}
+            <Dialog open={addUserPanel} onClose={closeAddUserPanel}>
+                <DialogTitle>Add User</DialogTitle>
+                <AddUser tableList={tableList} userProp={userInfo} setUserInfo={(userInfo) =>{updateUserInfo(userInfo)}}/>
+                <DialogActions>
+                    <Button onClick={closeAddUserPanel} size="small">Cancel</Button>
+                    <Button onClick={submitAddUser} size="small">Submit</Button>
+                </DialogActions>
+            </Dialog>
+            <Dialog open={updateUserPanel} onClose={closeUpdateUserPanel}>
+                <DialogTitle>Edit User</DialogTitle>
+                <UpdateUser tableList={tableList} userProp={userInfo} setUserInfo={(userInfo) =>{updateUserInfo(userInfo)}}/>
+                <DialogActions>
+                    <Button onClick={closeUpdateUserPanel} size="small">Cancel</Button>
+                    <Button onClick={submitUpdateUser} size="small">Submit</Button>
+                </DialogActions>
+            </Dialog>
+            <Dialog
+                open={deleteUserPanel}
+                onClose={()=>{setDeleteUserPanel(false)}}
+                aria-labelledby="alert-dialog-title"
+            >
+                <DialogTitle id="alert-dialog-title">{`Confirm Delete user ${checkedUser.username}_${checkedUser.component}?`}</DialogTitle>
+                <DialogActions>
+                    <Button onClick={()=>{setDeleteUserPanel(false)}} color="primary">
+                        Cancel
+                    </Button>
+                    <Button onClick={deleteUser} color="primary" autoFocus>
+                        Confirm
+                    </Button>
+                </DialogActions>
+            </Dialog>
+            <Snackbar open={showError} autoHideDuration={6000} onClose={()=>{closeTip('error')}}>
+                <Alert onClose={()=>{closeTip('error')}} severity="error">
+                    {errorTip}
+                </Alert>
+            </Snackbar>
+            <Snackbar open={showSuccess} autoHideDuration={1000} onClose={()=>{closeTip('success')}}>
+                <Alert onClose={()=>{closeTip('success')}} severity="success">
+                    {successTip}
+                </Alert>
+            </Snackbar>
+        </div>
+    )
+}
+export default UserPage;
diff --git a/pinot-controller/src/main/resources/app/requests/index.ts b/pinot-controller/src/main/resources/app/requests/index.ts
index c94c43b46a..2ab7b7f997 100644
--- a/pinot-controller/src/main/resources/app/requests/index.ts
+++ b/pinot-controller/src/main/resources/app/requests/index.ts
@@ -20,7 +20,7 @@
 import { AxiosResponse } from 'axios';
 import { TableData, Instances, Instance, Tenants, ClusterConfig, TableName, TableSize,
   IdealState, QueryTables, TableSchema, SQLResult, ClusterName, ZKGetList, ZKConfig, OperationResponse,
-  BrokerList, ServerList
+  BrokerList, ServerList, UserList, TableList, UserObject
 } from 'Models';
 
 const headers = {
@@ -172,4 +172,19 @@ export const authenticateUser = (authToken): Promise<AxiosResponse<OperationResp
   baseApi.get(`/auth/verify`, {headers:{"Authorization": authToken}});
 
 export const getSegmentDebugInfo = (tableName: string, tableType: string): Promise<AxiosResponse<OperationResponse>> =>
-  baseApi.get(`debug/tables/${tableName}?type=${tableType}&verbosity=10`);
\ No newline at end of file
+  baseApi.get(`debug/tables/${tableName}?type=${tableType}&verbosity=10`);
+
+export const requestTable = (): Promise<AxiosResponse<TableList>> =>
+    baseApi.get(`/tables`);
+
+export const requestUserList = (): Promise<AxiosResponse<UserList>> =>
+    baseApi.get(`/users`);
+
+export const requestAddUser = (userObject: UserObject): Promise<AxiosResponse<any>> =>
+    baseApi.post('/users', JSON.stringify(userObject), {headers});
+
+export const requestDeleteUser = (userObject: UserObject): Promise<AxiosResponse<any>> =>
+    baseApi.delete(`/users/${userObject.username}?component=${userObject.component}`);
+
+export const requestUpdateUser = (userObject: UserObject, passwordChanged: boolean): Promise<AxiosResponse<any>> =>
+    baseApi.put(`/users/${userObject.username}?component=${userObject.component}&passwordChanged=${passwordChanged}`, JSON.stringify(userObject), {headers});
\ No newline at end of file
diff --git a/pinot-controller/src/main/resources/app/router.tsx b/pinot-controller/src/main/resources/app/router.tsx
index 69719451fe..a969a68831 100644
--- a/pinot-controller/src/main/resources/app/router.tsx
+++ b/pinot-controller/src/main/resources/app/router.tsx
@@ -29,6 +29,7 @@ import InstanceDetails from './pages/InstanceDetails';
 import ZookeeperPage from './pages/ZookeeperPage';
 import SchemaPageDetails from './pages/SchemaPageDetails';
 import LoginPage from './pages/LoginPage';
+import UserPage from "./pages/UserPage";
 
 export default [
   { path: '/', Component: HomePage },
@@ -49,4 +50,5 @@ export default [
   { path: '/instance/:instanceName/table/:tableName/:segmentName', Component: SegmentDetails },
   { path: '/zookeeper', Component: ZookeeperPage },
   { path: '/login', Component: LoginPage },
+  { path: '/user', Component: UserPage}
 ];
\ No newline at end of file
diff --git a/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts b/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts
index f46973b21f..ff84d81046 100644
--- a/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts
+++ b/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts
@@ -67,7 +67,12 @@ import {
   getState,
   getInfo,
   authenticateUser,
-  getSegmentDebugInfo
+  getSegmentDebugInfo,
+  requestTable,
+  requestUserList,
+  requestAddUser,
+  requestDeleteUser,
+  requestUpdateUser
 } from '../requests';
 import { baseApi } from './axios-config';
 import Utils from './Utils';
@@ -887,6 +892,36 @@ const getURLWithoutAccessToken = (fallbackUrl = '/'): string => {
   return `${prefix}${url}`;
 };
 
+const getTable = ()=>{
+  return requestTable().then(response=>{
+    return response.data;
+  })
+};
+
+const getUserList = ()=>{
+  return requestUserList().then(response=>{
+    return response.data;
+  })
+};
+
+const addUser = (userObject)=>{
+  return requestAddUser(userObject).then(response=>{
+    return response.data;
+  })
+};
+
+const deleteUser = (userObject)=>{
+  return requestDeleteUser(userObject).then(response=>{
+    return response.data;
+  })
+};
+
+const updateUser = (userObject, passwordChanged) =>{
+  return requestUpdateUser(userObject, passwordChanged).then(response=>{
+    return response.data;
+  })
+}
+
 export default {
   getTenantsData,
   getAllInstances,
@@ -940,5 +975,10 @@ export default {
   getWellKnownOpenIdConfiguration,
   verifyAuth,
   getAccessTokenFromHashParams,
-  getURLWithoutAccessToken
-};
+  getURLWithoutAccessToken,
+  getTable,
+  getUserList,
+  addUser,
+  deleteUser,
+  updateUser
+};
\ No newline at end of file
diff --git a/pinot-controller/src/test/java/org/apache/pinot/controller/api/PinotAccessControlUserRestletResourceTest.java b/pinot-controller/src/test/java/org/apache/pinot/controller/api/PinotAccessControlUserRestletResourceTest.java
new file mode 100644
index 0000000000..cd31fbc99b
--- /dev/null
+++ b/pinot-controller/src/test/java/org/apache/pinot/controller/api/PinotAccessControlUserRestletResourceTest.java
@@ -0,0 +1,138 @@
+/**
+ * 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.pinot.controller.api;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import java.io.IOException;
+import org.apache.pinot.common.utils.BcryptUtils;
+import org.apache.pinot.controller.ControllerTestUtils;
+import org.apache.pinot.spi.config.user.ComponentType;
+import org.apache.pinot.spi.config.user.RoleType;
+import org.apache.pinot.spi.config.user.UserConfig;
+import org.apache.pinot.spi.utils.JsonUtils;
+import org.apache.pinot.spi.utils.StringUtil;
+import org.apache.pinot.spi.utils.builder.UserConfigBuilder;
+import org.testng.Assert;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.Test;
+
+public class PinotAccessControlUserRestletResourceTest {
+
+    private String _createUserUrl;
+    private final UserConfigBuilder _userConfigBuilder = new UserConfigBuilder();
+
+    @BeforeClass
+    public void setup()
+        throws Exception {
+        ControllerTestUtils.setupClusterAndValidate();
+
+        _createUserUrl = ControllerTestUtils.getControllerRequestURLBuilder().forUserCreate();
+        _userConfigBuilder.setUsername("testUser").setPassword("123456").setComponentType(ComponentType.CONTROLLER)
+            .setRoleType(RoleType.USER);
+    }
+
+    @Test
+    public void testAddUser()
+        throws Exception {
+        String userconfigString = _userConfigBuilder.setUsername("bad.user.with.dot").build().toJsonString();
+        try {
+            ControllerTestUtils.sendPostRequest(_createUserUrl, userconfigString);
+            Assert.fail("Adding a user with dot in username does not fail");
+        } catch (IOException e) {
+            Assert.assertTrue(e.getMessage().contains(
+              "Username: bad.user.with.dot containing '.' or space is not allowed"));
+        }
+
+        // Creating an user with a valid username should succeed
+        userconfigString = _userConfigBuilder.setUsername("valid_table_name").build().toJsonString();
+        ControllerTestUtils.sendPostRequest(_createUserUrl, userconfigString);
+
+        // Create an user that already exists should fail
+        try {
+            ControllerTestUtils.sendPostRequest(_createUserUrl, userconfigString);
+            Assert.fail("Creation of an existing user does not fail");
+        } catch (IOException e) {
+            Assert.assertTrue(e.getMessage().contains("User valid_table_name_CONTROLLER already exists"));
+        }
+    }
+
+    private UserConfig getUserConfig(String username, String componentType)
+        throws Exception {
+        String userConfigString =
+            ControllerTestUtils.sendGetRequest(ControllerTestUtils.getControllerRequestURLBuilder()
+                .forUserGet(username, componentType));
+        String usernameWithType = username + "_" + componentType;
+        return JsonUtils.jsonNodeToObject(JsonUtils.stringToJsonNode(userConfigString)
+            .get(usernameWithType), UserConfig.class);
+    }
+
+    @Test
+    public void testUpdateUserConfig()
+        throws Exception {
+        String username = "updateTC";
+        String userConfigString = _userConfigBuilder.setUsername(username).setComponentType(ComponentType.CONTROLLER)
+            .build().toJsonString();
+        ControllerTestUtils.sendPostRequest(_createUserUrl, userConfigString);
+        // user creation should succeed
+        UserConfig userConfig = getUserConfig(username, "CONTROLLER");
+        Assert.assertEquals(userConfig.getRoleType().toString(), RoleType.USER.toString());
+        Assert.assertTrue(BcryptUtils.checkpw("123456", userConfig.getPassword()));
+        userConfig.setRole("ADMIN");
+        userConfig.setPassword("654321");
+
+        JsonNode jsonResponse = JsonUtils.stringToJsonNode(ControllerTestUtils
+            .sendPutRequest(ControllerTestUtils.getControllerRequestURLBuilder()
+                    .forUpdateUserConfig(username, "CONTROLLER", true),
+                userConfig.toString()));
+        Assert.assertTrue(jsonResponse.has("status"));
+
+        UserConfig modifiedConfig = getUserConfig(username, "CONTROLLER");
+        Assert.assertEquals(modifiedConfig.getRoleType().toString(), "ADMIN");
+        Assert.assertTrue(BcryptUtils.checkpw("654321", modifiedConfig.getPassword()));
+    }
+
+    @Test
+    public void testDeleteUser()
+        throws Exception {
+        // Case 1: Create a CONTORLLER user and delete it directly w/o using query param.
+        UserConfig controllerUserConfig = _userConfigBuilder.setUsername("user1")
+            .setComponentType(ComponentType.CONTROLLER).build();
+        String creationResponse = ControllerTestUtils
+            .sendPostRequest(_createUserUrl, controllerUserConfig.toJsonString());
+        Assert.assertEquals(creationResponse, "{\"status\":\"User user1_CONTROLLER has been successfully added!\"}");
+
+        // Delete controller user using CONTROLLER suffix
+        String deleteResponse = ControllerTestUtils.sendDeleteRequest(
+            StringUtil.join("/", ControllerTestUtils.getControllerBaseApiUrl(),
+                "users", "user1?component=CONTROLLER"));
+        Assert.assertEquals(deleteResponse, "{\"status\":\"User: user1_CONTROLLER has been successfully deleted\"}");
+
+        // Case 2: Create a BROKER user and delete it directly w/o using query param.
+        UserConfig brokerUserConfig = _userConfigBuilder.setUsername("user1").setComponentType(ComponentType.BROKER)
+            .build();
+        creationResponse = ControllerTestUtils.sendPostRequest(_createUserUrl, brokerUserConfig.toJsonString());
+        Assert.assertEquals(creationResponse, "{\"status\":\"User user1_BROKER has been successfully added!\"}");
+
+        // Delete controller user using BROKER suffix
+        deleteResponse = ControllerTestUtils.sendDeleteRequest(
+            StringUtil.join("/", ControllerTestUtils.getControllerBaseApiUrl(), "users", "user1?component=BROKER")
+        );
+        Assert.assertEquals(deleteResponse, "{\"status\":\"User: user1_BROKER has been successfully deleted\"}");
+    }
+}
diff --git a/pinot-core/src/main/java/org/apache/pinot/core/auth/BasicAuthUtils.java b/pinot-core/src/main/java/org/apache/pinot/core/auth/BasicAuthUtils.java
index c17c2d8972..271fd44450 100644
--- a/pinot-core/src/main/java/org/apache/pinot/core/auth/BasicAuthUtils.java
+++ b/pinot-core/src/main/java/org/apache/pinot/core/auth/BasicAuthUtils.java
@@ -23,10 +23,13 @@ import java.util.Arrays;
 import java.util.Base64;
 import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
 import javax.annotation.Nullable;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.pinot.common.utils.BcryptUtils;
+import org.apache.pinot.spi.config.user.UserConfig;
 import org.apache.pinot.spi.env.PinotConfiguration;
 
 
@@ -61,21 +64,43 @@ public final class BasicAuthUtils {
    * @return list of BasicAuthPrincipals
    */
   public static List<BasicAuthPrincipal> extractBasicAuthPrincipals(PinotConfiguration configuration, String prefix) {
-    String principalNames = configuration.getProperty(prefix);
-    Preconditions.checkArgument(StringUtils.isNotBlank(principalNames), "must provide principals");
+      String principalNames = configuration.getProperty(prefix);
+      Preconditions.checkArgument(StringUtils.isNotBlank(principalNames), "must provide principals");
 
-    return Arrays.stream(principalNames.split(",")).map(rawName -> {
-      String name = rawName.trim();
-      Preconditions.checkArgument(StringUtils.isNotBlank(name), "%s is not a valid name", name);
+      return Arrays.stream(principalNames.split(",")).map(rawName -> {
+          String name = rawName.trim();
+          Preconditions.checkArgument(StringUtils.isNotBlank(name), "%s is not a valid name", name);
 
-      String password = configuration.getProperty(String.format("%s.%s.%s", prefix, name, PASSWORD));
-      Preconditions.checkArgument(StringUtils.isNotBlank(password), "must provide a password for %s", name);
+          String password = configuration.getProperty(String.format("%s.%s.%s", prefix, name, PASSWORD));
+          Preconditions.checkArgument(StringUtils.isNotBlank(password), "must provide a password for %s", name);
 
-      Set<String> tables = extractSet(configuration, String.format("%s.%s.%s", prefix, name, TABLES));
-      Set<String> permissions = extractSet(configuration, String.format("%s.%s.%s", prefix, name, PERMISSIONS));
+          Set<String> tables = extractSet(configuration, String.format("%s.%s.%s", prefix, name, TABLES));
+          Set<String> permissions = extractSet(configuration, String.format("%s.%s.%s", prefix, name, PERMISSIONS));
 
-      return new BasicAuthPrincipal(name, toBasicAuthToken(name, password), tables, permissions);
-    }).collect(Collectors.toList());
+          return new BasicAuthPrincipal(name, toBasicAuthToken(name, password), tables, permissions);
+      }).collect(Collectors.toList());
+  }
+
+  public static List<ZkBasicAuthPrincipal> extractBasicAuthPrincipals(List<UserConfig> userConfigList) {
+    return userConfigList.stream()
+        .map(user -> {
+          String name = user.getUserName().trim();
+          Preconditions.checkArgument(StringUtils.isNotBlank(name), "%s is not a valid username", name);
+          String password = user.getPassword().trim();
+          Preconditions.checkArgument(StringUtils.isNotBlank(password), "must provide a password for %s", name);
+          String component = user.getComponentType().toString();
+          String role = user.getRoleType().toString();
+
+          Set<String> tables = Optional.ofNullable(user.getTables())
+              .orElseGet(() -> Collections.emptyList())
+              .stream().collect(Collectors.toSet());
+          Set<String> permissions = Optional.ofNullable(user.getPermissios())
+              .orElseGet(() -> Collections.emptyList())
+              .stream().map(x -> x.toString())
+              .collect(Collectors.toSet());
+          return new ZkBasicAuthPrincipal(name, toBasicAuthToken(name, password), password,
+              component, role, tables, permissions);
+        }).collect(Collectors.toList());
   }
 
   private static Set<String> extractSet(PinotConfiguration configuration, String key) {
@@ -102,6 +127,46 @@ public final class BasicAuthUtils {
     return normalizeBase64Token(String.format("Basic %s", Base64.getEncoder().encodeToString(identifier.getBytes())));
   }
 
+  public static String decodeBasicAuthToken(String auth) {
+    if (StringUtils.isBlank(auth)) {
+      return null;
+    }
+    String replacedAuth = StringUtils.replace(auth, "Basic ", "");
+    byte[] decodedBytes = Base64.getDecoder().decode(replacedAuth);
+    String decodedString = new String(decodedBytes);
+    return decodedString;
+  }
+
+  public static String extractUsername(String auth) {
+    String decodedString = decodeBasicAuthToken(auth);
+    return StringUtils.split(decodedString, ":")[0];
+  }
+
+  public static String extractPassword(String auth) {
+    String decodedString = decodeBasicAuthToken(auth);
+    return StringUtils.split(decodedString, ":")[1];
+  }
+
+  /**
+   * Convert http header-compliant base64 encoded token to password-encrypted base64 token
+   *
+   * @param auth http base64 token
+   * @return base64 encoded basic auth token
+   */
+  public static String toEncryptBasicAuthToken(String auth) {
+    if (StringUtils.isBlank(auth)) {
+      return null;
+    }
+    String replacedAuth = StringUtils.replace(auth, "Basic ", "");
+    byte[] decodedBytes = Base64.getDecoder().decode(replacedAuth);
+    String decodedString = new String(decodedBytes);
+    String[] cretential = StringUtils.split(decodedString, ":");
+    String rawUsername = cretential[0];
+    String rawPassword = cretential[1];
+    String encryptedPassword = BcryptUtils.encrypt(rawPassword);
+    return toBasicAuthToken(rawUsername, encryptedPassword);
+  }
+
   /**
    * Normalize a base64 encoded auth token by stripping redundant padding (spaces, '=')
    *
diff --git a/pinot-core/src/main/java/org/apache/pinot/core/auth/ZkBasicAuthPrincipal.java b/pinot-core/src/main/java/org/apache/pinot/core/auth/ZkBasicAuthPrincipal.java
new file mode 100644
index 0000000000..93f97943ce
--- /dev/null
+++ b/pinot-core/src/main/java/org/apache/pinot/core/auth/ZkBasicAuthPrincipal.java
@@ -0,0 +1,56 @@
+/**
+ * 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.pinot.core.auth;
+
+import java.util.Set;
+import org.apache.pinot.spi.config.user.ComponentType;
+import org.apache.pinot.spi.config.user.RoleType;
+
+/**
+ * Container object for basic auth principal
+ */
+public class ZkBasicAuthPrincipal extends BasicAuthPrincipal {
+    private final String _password;
+    private final String _component;
+    private final String _role;
+
+    public ZkBasicAuthPrincipal(String name, String token, String password, String component, String role,
+        Set<String> tables, Set<String> permissions) {
+        super(name, token, tables, permissions);
+        _component = component;
+        _role = role;
+        _password = password;
+    }
+
+    public String getRole() {
+        return _role;
+    }
+
+    public String getComponent() {
+        return _component;
+    }
+
+    public String getPassword() {
+        return _password;
+    }
+
+    public boolean hasPermission(RoleType role, ComponentType component) {
+        return RoleType.valueOf(_role).equals(role) && ComponentType.valueOf(_component).equals(component);
+    }
+}
diff --git a/pinot-core/src/main/java/org/apache/pinot/server/access/AccessControlFactory.java b/pinot-core/src/main/java/org/apache/pinot/server/access/AccessControlFactory.java
index 6253d679e4..f90e6493ea 100644
--- a/pinot-core/src/main/java/org/apache/pinot/server/access/AccessControlFactory.java
+++ b/pinot-core/src/main/java/org/apache/pinot/server/access/AccessControlFactory.java
@@ -18,6 +18,7 @@
  */
 package org.apache.pinot.server.access;
 
+import org.apache.helix.HelixManager;
 import org.apache.pinot.spi.annotations.InterfaceAudience;
 import org.apache.pinot.spi.annotations.InterfaceStability;
 import org.apache.pinot.spi.env.PinotConfiguration;
@@ -30,5 +31,18 @@ public interface AccessControlFactory {
   default void init(PinotConfiguration configuration) {
   };
 
+  /**
+   * Extend the original init method to support Zookeeper BasicAuthAccessControlFactory.
+   * Because ZKBasicAuthAccessControlFactory needs to acquire users' info from HelixPropertyStore.
+   * The reason why passed helixManager param other than HelixPropertyStore is that
+   * HelixManager variable hasn't connected to Real Helix manager when invoke init method.
+   *
+   * @param configuration pinot configuration
+   * @param helixManager Helix Manager
+   */
+  default void init(PinotConfiguration configuration, HelixManager helixManager) {
+    init(configuration);
+  }
+
   AccessControl create();
 }
diff --git a/pinot-core/src/main/java/org/apache/pinot/server/access/BasicAuthAccessFactory.java b/pinot-core/src/main/java/org/apache/pinot/server/access/BasicAuthAccessFactory.java
index 3431b5cf2f..09187ece7b 100644
--- a/pinot-core/src/main/java/org/apache/pinot/server/access/BasicAuthAccessFactory.java
+++ b/pinot-core/src/main/java/org/apache/pinot/server/access/BasicAuthAccessFactory.java
@@ -37,6 +37,7 @@ public class BasicAuthAccessFactory implements AccessControlFactory {
 
   private AccessControl _accessControl;
 
+  @Override
   public void init(PinotConfiguration configuration) {
     _accessControl = new BasicAuthAccessControl(BasicAuthUtils.extractBasicAuthPrincipals(configuration, PREFIX));
   }
diff --git a/pinot-core/src/main/java/org/apache/pinot/server/access/ZkBasicAuthAccessFactory.java b/pinot-core/src/main/java/org/apache/pinot/server/access/ZkBasicAuthAccessFactory.java
new file mode 100644
index 0000000000..6606a2b026
--- /dev/null
+++ b/pinot-core/src/main/java/org/apache/pinot/server/access/ZkBasicAuthAccessFactory.java
@@ -0,0 +1,121 @@
+/**
+ * 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.pinot.server.access;
+
+import io.netty.channel.ChannelHandlerContext;
+import java.util.Collection;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+import org.apache.commons.lang.StringUtils;
+import org.apache.helix.HelixManager;
+import org.apache.pinot.common.config.provider.AccessControlUserCache;
+import org.apache.pinot.common.utils.BcryptUtils;
+import org.apache.pinot.core.auth.BasicAuthUtils;
+import org.apache.pinot.core.auth.ZkBasicAuthPrincipal;
+import org.apache.pinot.spi.env.PinotConfiguration;
+import org.apache.pinot.spi.utils.builder.TableNameBuilder;
+
+/**
+ * Zookeeper Basic Authentication based on Pinot Controller UI.
+ * The user role has been distinguished by user and admin. Only admin can have access to the
+ * user console page in Pinot controller UI. And admin can change user info (table permission/
+ * number of tables/password etc.) or add/delete user without restarting your Pinot clusters,
+ * and these changes happen immediately.
+ * Users Configuration store in Helix Zookeeper and encrypted user password via Bcrypt Encryption Algorithm.
+ *
+ */
+public class ZkBasicAuthAccessFactory implements AccessControlFactory {
+    private static final String AUTHORIZATION_KEY = "authorization";
+
+    private HelixManager _helixManager;
+    private AccessControl _accessControl;
+
+    @Override
+    public void init(PinotConfiguration configuration, HelixManager helixManager) {
+        _helixManager = helixManager;
+    }
+
+    @Override
+    public AccessControl create() {
+        if (_accessControl == null) {
+            _accessControl = new BasicAuthAccessControl(_helixManager);
+        }
+        return _accessControl;
+    }
+
+    /**
+     * Access Control using metadata-based basic grpc authentication
+     */
+    private static class BasicAuthAccessControl implements AccessControl {
+        private Map<String, ZkBasicAuthPrincipal> _name2principal;
+        private AccessControlUserCache _userCache;
+        private HelixManager _innerHelixManager;
+
+        public BasicAuthAccessControl(HelixManager helixManager) {
+            _innerHelixManager = helixManager;
+        }
+
+        public void initUserCache() {
+            if (_userCache == null) {
+                _userCache = new AccessControlUserCache(_innerHelixManager.getHelixPropertyStore());
+            }
+        }
+
+        @Override
+        public boolean isAuthorizedChannel(ChannelHandlerContext channelHandlerContext) {
+            return true;
+        }
+
+        @Override
+        public boolean hasDataAccess(RequesterIdentity requesterIdentity, String tableName) {
+            if (_userCache == null) {
+                initUserCache();
+            }
+            Collection<String> tokens = getTokens(requesterIdentity);
+            _name2principal = BasicAuthUtils.extractBasicAuthPrincipals(_userCache.getAllServerUserConfig())
+                .stream().collect(Collectors.toMap(ZkBasicAuthPrincipal::getName, p -> p));
+
+            Map<String, String> name2password = tokens.stream().collect(Collectors
+              .toMap(BasicAuthUtils::extractUsername, BasicAuthUtils::extractPassword));
+            Map<String, ZkBasicAuthPrincipal> password2principal = name2password.keySet().stream()
+              .collect(Collectors.toMap(name2password::get, _name2principal::get));
+            return password2principal.entrySet().stream()
+                .filter(entry -> BcryptUtils.checkpw(entry.getKey(), entry.getValue().getPassword()))
+                .map(u -> u.getValue())
+                .filter(Objects::nonNull)
+                .findFirst()
+                .map(zkprincipal -> StringUtils.isEmpty(tableName) || zkprincipal.hasTable(
+                    TableNameBuilder.extractRawTableName(tableName)))
+                .orElse(false);
+        }
+
+        private Collection<String> getTokens(RequesterIdentity requesterIdentity) {
+            if (requesterIdentity instanceof GrpcRequesterIdentity) {
+                GrpcRequesterIdentity identity = (GrpcRequesterIdentity) requesterIdentity;
+                return identity.getGrpcMetadata().get(AUTHORIZATION_KEY);
+            }
+            if (requesterIdentity instanceof HttpRequesterIdentity) {
+                HttpRequesterIdentity identity = (HttpRequesterIdentity) requesterIdentity;
+                return identity.getHttpHeaders().get(AUTHORIZATION_KEY);
+            }
+            throw new UnsupportedOperationException("GrpcRequesterIdentity or HttpRequesterIdentity is required");
+        }
+    }
+}
diff --git a/pinot-core/src/test/java/org/apache/pinot/core/auth/ZkBasicAuthTest.java b/pinot-core/src/test/java/org/apache/pinot/core/auth/ZkBasicAuthTest.java
new file mode 100644
index 0000000000..274505a543
--- /dev/null
+++ b/pinot-core/src/test/java/org/apache/pinot/core/auth/ZkBasicAuthTest.java
@@ -0,0 +1,59 @@
+/**
+ * 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.pinot.core.auth;
+
+import com.google.common.collect.ImmutableSet;
+import org.apache.pinot.spi.config.user.ComponentType;
+import org.apache.pinot.spi.config.user.RoleType;
+import org.testng.Assert;
+import org.testng.annotations.Test;
+
+
+public class ZkBasicAuthTest {
+
+    @Test
+    public void testBasicAuthPrincipal()
+        throws Exception {
+        Assert.assertTrue(new ZkBasicAuthPrincipal("name", "token", "password",
+          ComponentType.CONTROLLER.name(), RoleType.ADMIN.name(), ImmutableSet.of("myTable"),
+          ImmutableSet.of("READ")).hasTable("myTable"));
+        Assert.assertTrue(new ZkBasicAuthPrincipal("name", "token", "password",
+          ComponentType.CONTROLLER.name(), RoleType.ADMIN.name(), ImmutableSet.of("myTable", "myTable1"),
+          ImmutableSet.of("Read")).hasTable("myTable1"));
+        Assert.assertFalse(new ZkBasicAuthPrincipal("name", "token", "password",
+          ComponentType.CONTROLLER.name(), RoleType.ADMIN.name(), ImmutableSet.of("myTable"),
+          ImmutableSet.of("read")).hasTable("myTable1"));
+        Assert.assertFalse(new ZkBasicAuthPrincipal("name", "token", "password",
+          ComponentType.CONTROLLER.name(), RoleType.ADMIN.name(), ImmutableSet.of("myTable", "myTable1"),
+          ImmutableSet.of("read")).hasTable("myTable2"));
+
+        Assert.assertTrue(new ZkBasicAuthPrincipal("name", "token", "password",
+          ComponentType.CONTROLLER.name(), RoleType.ADMIN.name(), ImmutableSet.of("myTable"),
+          ImmutableSet.of("READ")).hasPermission("read"));
+        Assert.assertTrue(new ZkBasicAuthPrincipal("name", "token", "password",
+          ComponentType.CONTROLLER.name(), RoleType.ADMIN.name(), ImmutableSet.of("myTable"),
+          ImmutableSet.of("Read")).hasPermission("READ"));
+        Assert.assertTrue(new ZkBasicAuthPrincipal("name", "token", "password",
+          ComponentType.CONTROLLER.name(), RoleType.ADMIN.name(), ImmutableSet.of("myTable"),
+          ImmutableSet.of("read")).hasPermission("Read"));
+        Assert.assertFalse(new ZkBasicAuthPrincipal("name", "token", "password",
+          ComponentType.CONTROLLER.name(), RoleType.ADMIN.name(), ImmutableSet.of("myTable"),
+          ImmutableSet.of("read")).hasPermission("write"));
+    }
+}
diff --git a/pinot-server/src/main/java/org/apache/pinot/server/starter/ServerInstance.java b/pinot-server/src/main/java/org/apache/pinot/server/starter/ServerInstance.java
index 0817080ed0..7041a40b24 100644
--- a/pinot-server/src/main/java/org/apache/pinot/server/starter/ServerInstance.java
+++ b/pinot-server/src/main/java/org/apache/pinot/server/starter/ServerInstance.java
@@ -99,7 +99,7 @@ public class ServerInstance {
     NettyConfig nettyConfig =
         NettyConfig.extractNettyConfig(serverConf.getPinotConfig(), CommonConstants.Server.SERVER_NETTY_PREFIX);
     accessControlFactory.init(
-        serverConf.getPinotConfig().subset(CommonConstants.Server.PREFIX_OF_CONFIG_OF_ACCESS_CONTROL));
+      serverConf.getPinotConfig().subset(CommonConstants.Server.PREFIX_OF_CONFIG_OF_ACCESS_CONTROL), helixManager);
     _accessControl = accessControlFactory.create();
 
     if (serverConf.isNettyServerEnabled()) {
diff --git a/pinot-controller/src/main/resources/app/app_state.ts b/pinot-spi/src/main/java/org/apache/pinot/spi/config/user/AccessType.java
similarity index 78%
copy from pinot-controller/src/main/resources/app/app_state.ts
copy to pinot-spi/src/main/java/org/apache/pinot/spi/config/user/AccessType.java
index 0ef89dc3c4..b4181b3dd4 100644
--- a/pinot-controller/src/main/resources/app/app_state.ts
+++ b/pinot-spi/src/main/java/org/apache/pinot/spi/config/user/AccessType.java
@@ -16,13 +16,8 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+package org.apache.pinot.spi.config.user;
 
- import { AuthWorkflow } from "Models";
-class app_state {
-  queryConsoleOnlyView: boolean;
-  authWorkflow: AuthWorkflow;
-  authToken: string | null;
-  columnNameSeparator: string = '#$%';
+public enum AccessType {
+    CREATE, READ, UPDATE, DELETE
 }
-
-export default new app_state();
\ No newline at end of file
diff --git a/pinot-controller/src/main/resources/app/app_state.ts b/pinot-spi/src/main/java/org/apache/pinot/spi/config/user/ComponentType.java
similarity index 78%
copy from pinot-controller/src/main/resources/app/app_state.ts
copy to pinot-spi/src/main/java/org/apache/pinot/spi/config/user/ComponentType.java
index 0ef89dc3c4..96b6359a9b 100644
--- a/pinot-controller/src/main/resources/app/app_state.ts
+++ b/pinot-spi/src/main/java/org/apache/pinot/spi/config/user/ComponentType.java
@@ -16,13 +16,8 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+package org.apache.pinot.spi.config.user;
 
- import { AuthWorkflow } from "Models";
-class app_state {
-  queryConsoleOnlyView: boolean;
-  authWorkflow: AuthWorkflow;
-  authToken: string | null;
-  columnNameSeparator: string = '#$%';
+public enum ComponentType {
+    CONTROLLER, BROKER, SERVER, MINION, PROXY
 }
-
-export default new app_state();
\ No newline at end of file
diff --git a/pinot-controller/src/main/resources/app/app_state.ts b/pinot-spi/src/main/java/org/apache/pinot/spi/config/user/RoleType.java
similarity index 78%
copy from pinot-controller/src/main/resources/app/app_state.ts
copy to pinot-spi/src/main/java/org/apache/pinot/spi/config/user/RoleType.java
index 0ef89dc3c4..dfab3a2aa6 100644
--- a/pinot-controller/src/main/resources/app/app_state.ts
+++ b/pinot-spi/src/main/java/org/apache/pinot/spi/config/user/RoleType.java
@@ -16,13 +16,8 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+package org.apache.pinot.spi.config.user;
 
- import { AuthWorkflow } from "Models";
-class app_state {
-  queryConsoleOnlyView: boolean;
-  authWorkflow: AuthWorkflow;
-  authToken: string | null;
-  columnNameSeparator: string = '#$%';
+public enum RoleType {
+    ADMIN, USER
 }
-
-export default new app_state();
\ No newline at end of file
diff --git a/pinot-spi/src/main/java/org/apache/pinot/spi/config/user/UserConfig.java b/pinot-spi/src/main/java/org/apache/pinot/spi/config/user/UserConfig.java
new file mode 100644
index 0000000000..bfe9d0a8c9
--- /dev/null
+++ b/pinot-spi/src/main/java/org/apache/pinot/spi/config/user/UserConfig.java
@@ -0,0 +1,140 @@
+/**
+ * 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.pinot.spi.config.user;
+
+import com.fasterxml.jackson.annotation.JsonCreator;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyDescription;
+import com.google.common.base.Preconditions;
+import java.util.List;
+import java.util.Objects;
+import javax.annotation.Nullable;
+import org.apache.pinot.spi.config.BaseJsonConfig;
+
+
+public class UserConfig extends BaseJsonConfig {
+    public static final String USERNAME_KEY = "username";
+    public static final String PASSWORD_KEY = "password";
+    public static final String COMPONET_KEY = "component";
+    public static final String ROLE_KEY = "role";
+    public static final String AUTH_TOKEN_KEY = "authToken";
+    public static final String TABLES_KEY = "tables";
+    public static final String PERMISSIONS_KEY = "permissions";
+
+    @JsonPropertyDescription("The name of User")
+    private String _username;
+
+    @JsonPropertyDescription("The password of User")
+    private String _password;
+
+    @JsonPropertyDescription("The name of Component")
+    private ComponentType _componentType;
+
+    @JsonPropertyDescription("The role of user")
+    private RoleType _roleType;
+
+    @JsonPropertyDescription("The tables owned of User")
+    private List<String> _tables;
+
+    @JsonPropertyDescription("The table permission of User")
+    private List<AccessType> _permissions;
+
+    @JsonCreator
+    public UserConfig(@JsonProperty(value = USERNAME_KEY, required = true) String username,
+        @JsonProperty(value = PASSWORD_KEY, required = true) String password,
+        @JsonProperty(value = COMPONET_KEY, required = true) String component,
+        @JsonProperty(value = ROLE_KEY, required = true) String role,
+        @JsonProperty(value = TABLES_KEY) @Nullable List<String> tableList,
+        @JsonProperty(value = PERMISSIONS_KEY) @Nullable List<AccessType> permissionList
+    ) {
+        Preconditions.checkArgument(username != null, "'username' must be configured");
+        Preconditions.checkArgument(password != null, "'password' must be configured");
+
+        // NOTE: Handle lower case table type and raw table name for backward-compatibility
+        _username = username;
+        _password = password;
+        _componentType = ComponentType.valueOf(component.toUpperCase());
+        _roleType = RoleType.valueOf(role.toUpperCase());
+        _tables = tableList;
+        _permissions = permissionList;
+    }
+
+    @JsonProperty(USERNAME_KEY)
+    public String getUserName() {
+        return _username;
+    }
+
+    public String getUsernameWithComponent() {
+        return getUserName() + "_" + getComponentType().toString();
+    }
+
+    public boolean isExist(String username, ComponentType component) {
+        return _username.equals(username) && _componentType.equals(component);
+    }
+
+    @JsonProperty(PASSWORD_KEY)
+    public String getPassword() {
+        return _password;
+    }
+
+    @JsonProperty(TABLES_KEY)
+    public List<String> getTables() {
+        return _tables;
+    }
+
+    @JsonProperty(PERMISSIONS_KEY)
+    public List<AccessType> getPermissios() {
+        return _permissions;
+    }
+
+    @JsonProperty(COMPONET_KEY)
+    public ComponentType getComponentType() {
+        return _componentType;
+    }
+
+    @JsonProperty(ROLE_KEY)
+    public RoleType getRoleType() {
+        return _roleType;
+    }
+
+    public void setRole(String roleTypeStr) {
+        _roleType = RoleType.valueOf(roleTypeStr);
+    }
+
+    public void setPassword(String password) {
+        _password = password;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o == null || getClass() != o.getClass()) {
+            return false;
+        }
+        UserConfig that = (UserConfig) o;
+        return _username.equals(that._username) && _componentType == that._componentType;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(super.hashCode(), _username, _componentType);
+    }
+}
diff --git a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/builder/ControllerRequestURLBuilder.java b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/builder/ControllerRequestURLBuilder.java
index 2cf765ff6b..b0a9f7e139 100644
--- a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/builder/ControllerRequestURLBuilder.java
+++ b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/builder/ControllerRequestURLBuilder.java
@@ -90,10 +90,31 @@ public class ControllerRequestURLBuilder {
     return StringUtil.join("/", _baseUrl, "tenants");
   }
 
+  public String forUserCreate() {
+    return StringUtil.join("/", _baseUrl, "users");
+  }
+
   public String forTenantGet() {
     return StringUtil.join("/", _baseUrl, "tenants");
   }
 
+  public String forUserGet(String username, String componentTypeStr) {
+    StringBuilder params = new StringBuilder();
+    if (StringUtils.isNotBlank(username)) {
+      params.append("?component=" + componentTypeStr);
+    }
+    return StringUtil.join("/", _baseUrl, "users", username, params.toString());
+  }
+
+  public String forUpdateUserConfig(String username, String componentTypeStr, boolean passwordChanged) {
+    StringBuilder params = new StringBuilder();
+    if (StringUtils.isNotBlank(username)) {
+      params.append("?component=" + componentTypeStr);
+    }
+    params.append(String.format("&&passwordChanged=%s", passwordChanged));
+    return StringUtil.join("/", _baseUrl, "users", username, params.toString());
+  }
+
   public String forTenantGet(String tenantName) {
     return StringUtil.join("/", _baseUrl, "tenants", tenantName);
   }
diff --git a/pinot-spi/src/main/java/org/apache/pinot/spi/utils/builder/UserConfigBuilder.java b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/builder/UserConfigBuilder.java
new file mode 100644
index 0000000000..5afbf639ac
--- /dev/null
+++ b/pinot-spi/src/main/java/org/apache/pinot/spi/utils/builder/UserConfigBuilder.java
@@ -0,0 +1,70 @@
+/**
+ * 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.pinot.spi.utils.builder;
+
+import java.util.List;
+import org.apache.pinot.spi.config.user.AccessType;
+import org.apache.pinot.spi.config.user.ComponentType;
+import org.apache.pinot.spi.config.user.RoleType;
+import org.apache.pinot.spi.config.user.UserConfig;
+
+public class UserConfigBuilder {
+
+    private ComponentType _componentType;
+    private String _username;
+    private String _password;
+    private RoleType _roleType;
+    private List<String> _tableList;
+    private List<AccessType> _permissionList;
+
+    public UserConfigBuilder setComponentType(ComponentType componentType) {
+        _componentType = componentType;
+        return this;
+    }
+
+    public UserConfigBuilder setUsername(String username) {
+        _username = username;
+        return this;
+    }
+
+    public UserConfigBuilder setPassword(String password) {
+        _password = password;
+        return this;
+    }
+
+    public UserConfigBuilder setRoleType(RoleType roleType) {
+        _roleType = roleType;
+        return this;
+    }
+
+    public UserConfigBuilder setTableList(List<String> tableList) {
+        _tableList = tableList;
+        return this;
+    }
+
+    public UserConfigBuilder setPermissionList(List<AccessType> permissionList) {
+        _permissionList = permissionList;
+        return this;
+    }
+
+    public UserConfig build() {
+        return new UserConfig(_username, _password, _componentType.toString(), _roleType.toString(), _tableList,
+            _permissionList);
+    }
+}


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For additional commands, e-mail: commits-help@pinot.apache.org