You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kylin.apache.org by ni...@apache.org on 2019/08/15 03:08:26 UTC

[kylin] branch master updated: KYLIN-4122 Add kylin user and group manage modules

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 4651737  KYLIN-4122 Add kylin user and group manage modules
4651737 is described below

commit 465173758773f947339a6568f5546afbb25551d0
Author: luguosheng1314 <55...@qq.com>
AuthorDate: Wed Aug 7 15:38:23 2019 +0800

    KYLIN-4122 Add kylin user and group manage modules
---
 .../org/apache/kylin/metadata/acl/UserGroup.java   |  59 ++++
 .../rest/controller/KylinUserGroupController.java  | 103 +++++-
 .../kylin/rest/controller/UserController.java      | 308 ++++++++++++++++
 .../kylin/rest/request/PasswdChangeRequest.java    |  62 ++++
 .../apache/kylin/rest/security/ManagedUser.java    |  10 +-
 .../kylin/rest/service/KylinUserGroupService.java  | 168 ++++++++-
 .../kylin/rest/service/KylinUserService.java       |  43 +++
 ...UserGroupService.java => UserGroupService.java} |  61 ++--
 .../org/apache/kylin/rest/service/UserService.java |  10 +-
 .../apache/kylin/rest/util/AclPermissionUtil.java  |   6 +-
 .../org/apache/kylin/rest/util/PagingUtil.java     |  29 +-
 .../java/org/apache/kylin/rest/DebugTomcat.java    |  10 +-
 server/src/main/resources/kylinSecurity.xml        |  34 +-
 .../rest/controller/AccessControllerTest.java      |  23 +-
 .../kylin/rest/controller/UserControllerTest.java  |  50 +--
 .../apache/kylin/rest/service/ServiceTestBase.java |  26 +-
 .../apache/kylin/rest/util/ValidateUtilTest.java   |  11 +-
 webapp/app/index.html                              |   4 +-
 webapp/app/js/app.js                               |   2 +-
 webapp/app/js/controllers/admin.js                 |  13 +-
 webapp/app/js/controllers/userGroup.js             | 387 +++++++++++++++++++++
 webapp/app/js/directives/directives.js             |   2 +-
 webapp/app/js/services/kylinProperties.js          |   9 +-
 webapp/app/js/{app.js => services/userGroup.js}    |  12 +-
 webapp/app/js/services/users.js                    |  12 +-
 webapp/app/js/{app.js => utils/response.js}        |  29 +-
 webapp/app/less/app.less                           |   3 +
 webapp/app/partials/admin/admin.html               |  14 +-
 webapp/app/partials/admin/change_pwd.html          |  78 +++++
 webapp/app/partials/admin/group.html               |  93 +++++
 webapp/app/partials/admin/group_assign.html        |  87 +++++
 webapp/app/partials/admin/group_create.html        |  48 +++
 webapp/app/partials/admin/user.html                | 108 ++++++
 webapp/app/partials/admin/user_assign.html         |  87 +++++
 webapp/app/partials/admin/user_create.html         |  73 ++++
 webapp/app/partials/common/access.html             |   5 +-
 36 files changed, 1940 insertions(+), 139 deletions(-)

diff --git a/core-metadata/src/main/java/org/apache/kylin/metadata/acl/UserGroup.java b/core-metadata/src/main/java/org/apache/kylin/metadata/acl/UserGroup.java
new file mode 100644
index 0000000..10712c2
--- /dev/null
+++ b/core-metadata/src/main/java/org/apache/kylin/metadata/acl/UserGroup.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.kylin.metadata.acl;
+
+import java.util.List;
+import java.util.TreeSet;
+
+import org.apache.kylin.common.persistence.RootPersistentEntity;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.google.common.collect.Lists;
+
+@SuppressWarnings("serial")
+@JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.NONE, getterVisibility = JsonAutoDetect.Visibility.NONE, isGetterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE)
+public class UserGroup extends RootPersistentEntity {
+    @JsonProperty
+    private TreeSet<String> groups = new TreeSet<>();
+
+    public List<String> getAllGroups() {
+        return Lists.newArrayList(this.groups);
+    }
+
+    public boolean exists(String name) {
+        return groups.contains(name);
+    }
+
+    public UserGroup add(String name) {
+        if (groups.contains(name)) {
+            throw new RuntimeException("Operation failed, group:" + name + " already exists");
+        }
+        this.groups.add(name);
+        return this;
+    }
+
+    public UserGroup delete(String name) {
+        if (!groups.contains(name)) {
+            throw new RuntimeException("Operation failed, group:" + name + " does not exists");
+        }
+        this.groups.remove(name);
+        return this;
+    }
+}
diff --git a/server-base/src/main/java/org/apache/kylin/rest/controller/KylinUserGroupController.java b/server-base/src/main/java/org/apache/kylin/rest/controller/KylinUserGroupController.java
index 5cfc869..06159ae 100644
--- a/server-base/src/main/java/org/apache/kylin/rest/controller/KylinUserGroupController.java
+++ b/server-base/src/main/java/org/apache/kylin/rest/controller/KylinUserGroupController.java
@@ -19,28 +19,123 @@
 package org.apache.kylin.rest.controller;
 
 import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 
-import org.apache.kylin.rest.service.IUserGroupService;
+import org.apache.commons.lang.StringUtils;
+import org.apache.kylin.common.util.StringUtil;
+import org.apache.kylin.rest.constant.Constant;
+import org.apache.kylin.rest.exception.InternalErrorException;
+import org.apache.kylin.rest.response.EnvelopeResponse;
+import org.apache.kylin.rest.response.ResponseCode;
+import org.apache.kylin.rest.security.ManagedUser;
+import org.apache.kylin.rest.service.UserGroupService;
+import org.apache.kylin.rest.util.PagingUtil;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestMethod;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.ResponseBody;
 
+import com.google.common.collect.Sets;
+
 @Controller
 @RequestMapping(value = "/user_group")
 public class KylinUserGroupController extends BasicController {
 
     @Autowired
     @Qualifier("userGroupService")
-    private IUserGroupService userGroupService;
+    private UserGroupService userGroupService;
+
+    @RequestMapping(value = "/groups", method = { RequestMethod.GET }, produces = { "application/json" })
+    @ResponseBody
+    public EnvelopeResponse<String> listUserAuthorities(
+            @RequestParam(value = "project", required = false) String project,
+            @RequestParam(value = "name", required = false) String name,
+            @RequestParam(value = "isFuzzMatch", required = false) boolean isFuzzMatch,
+            @RequestParam(value = "offset", required = false, defaultValue = "0") Integer offset,
+            @RequestParam(value = "limit", required = false, defaultValue = "10") Integer limit) throws IOException {
+        HashMap<String, Object> data = new HashMap<>();
+        Map<String, List<String>> userGroupMap = userGroupService.getGroupMembersMap();
+        Map<String, Set<String>> usersWithGroup = new HashMap<>();
+        List<String> groupsByFuzzyMatching = getManagedGroupsByFuzzyMatching(name, isFuzzMatch, getAllGroups(project));
+        List<String> subList = PagingUtil.cutPage(groupsByFuzzyMatching, offset, limit);
+        for (String g : subList) {
+            List<String> userNames = userGroupMap.get(g);
+            usersWithGroup.put(g, (userNames == null ? Sets.newHashSet() : Sets.newHashSet(userNames)));
+        }
+        data.put("groups", usersWithGroup);
+        data.put("size", groupsByFuzzyMatching.size());
+        return new EnvelopeResponse(ResponseCode.CODE_SUCCESS, data, "");
+    }
+
+    @RequestMapping(value = "/{name:.+}", method = { RequestMethod.POST }, produces = { "application/json" })
+    @ResponseBody
+    public EnvelopeResponse<String> addUserGroup(@PathVariable String name) throws IOException {
+        userGroupService.addGroup(name);
+        return new EnvelopeResponse<>(ResponseCode.CODE_SUCCESS, null, "");
+    }
+
+    @RequestMapping(value = "/{name:.+}", method = { RequestMethod.DELETE }, produces = { "application/json" })
+    @ResponseBody
+    public EnvelopeResponse<String> delUserGroup(@PathVariable String name) throws IOException {
+        if (StringUtils.equalsIgnoreCase(name, Constant.GROUP_ALL_USERS)
+                || StringUtils.equalsIgnoreCase(name, Constant.ROLE_ADMIN)) {
+            throw new InternalErrorException("Can not delete group " + name);
+        }
+        userGroupService.deleteGroup(name);
+        return new EnvelopeResponse<>(ResponseCode.CODE_SUCCESS, null, "");
+    }
 
-    @RequestMapping(value = "/groups", method = {RequestMethod.GET}, produces = {"application/json"})
+    //move users in/out from groups
+    @RequestMapping(value = "/users/{name:.+}", method = { RequestMethod.POST, RequestMethod.PUT }, produces = {
+            "application/json" })
     @ResponseBody
-    public List<String> listUserAuthorities(@RequestParam(value = "project") String project) throws IOException {
+    public EnvelopeResponse<String> addOrDelUsers(@PathVariable String name, @RequestBody List<String> users)
+            throws IOException {
+        if (StringUtil.equals(name, Constant.ROLE_ADMIN) && users.size() == 0) {
+            throw new InternalErrorException("role_admin must have at least one user");
+        }
+        userGroupService.modifyGroupUsers(name, users);
+        return new EnvelopeResponse<>(ResponseCode.CODE_SUCCESS, null, "");
+    }
+
+    //move users in/out from groups
+    @RequestMapping(value = "/users/{name:.+}", method = { RequestMethod.GET }, produces = { "application/json" })
+    @ResponseBody
+    public EnvelopeResponse<String> getUsersByGroup(@PathVariable String name) throws IOException {
+        HashMap<String, Object> data = new HashMap<>();
+        List<ManagedUser> users = userGroupService.getGroupMembersByName(name);
+        data.put("users", users);
+        return new EnvelopeResponse(ResponseCode.CODE_SUCCESS, data, "");
+    }
+
+    private List<String> getAllGroups(String project) throws IOException {
         return userGroupService.listAllAuthorities(project);
     }
+
+    private List<String> getManagedGroupsByFuzzyMatching(String nameSeg, boolean isFuzzyMatch, List<String> groups) {
+        //for name fuzzy matching
+        if (StringUtils.isBlank(nameSeg)) {
+            return groups;
+        }
+
+        List<String> groupsByFuzzyMatching = new ArrayList<>();
+        for (String u : groups) {
+            if (!isFuzzyMatch && StringUtils.equals(u, nameSeg)) {
+                groupsByFuzzyMatching.add(u);
+            }
+            if (isFuzzyMatch && StringUtils.containsIgnoreCase(u, nameSeg)) {
+                groupsByFuzzyMatching.add(u);
+            }
+        }
+        return groupsByFuzzyMatching;
+    }
 }
diff --git a/server-base/src/main/java/org/apache/kylin/rest/controller/UserController.java b/server-base/src/main/java/org/apache/kylin/rest/controller/UserController.java
index 884b949..d1ba18b 100644
--- a/server-base/src/main/java/org/apache/kylin/rest/controller/UserController.java
+++ b/server-base/src/main/java/org/apache/kylin/rest/controller/UserController.java
@@ -18,17 +18,50 @@
 
 package org.apache.kylin.rest.controller;
 
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.regex.Pattern;
+
+import javax.annotation.Nullable;
+import javax.annotation.PostConstruct;
+
+import org.apache.commons.lang.StringUtils;
+import org.apache.kylin.metadata.MetadataConstants;
+import org.apache.kylin.rest.constant.Constant;
+import org.apache.kylin.rest.exception.BadRequestException;
+import org.apache.kylin.rest.exception.ForbiddenException;
+import org.apache.kylin.rest.request.PasswdChangeRequest;
+import org.apache.kylin.rest.response.EnvelopeResponse;
+import org.apache.kylin.rest.response.ResponseCode;
+import org.apache.kylin.rest.security.ManagedUser;
+import org.apache.kylin.rest.service.AccessService;
+import org.apache.kylin.rest.service.UserGroupService;
 import org.apache.kylin.rest.service.UserService;
+import org.apache.kylin.rest.util.AclEvaluate;
+import org.apache.kylin.rest.util.PagingUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.Authentication;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.core.userdetails.UserDetails;
+import org.springframework.security.core.userdetails.UsernameNotFoundException;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import com.google.common.collect.Lists;
 
 /**
  * Handle user authentication request to protected kylin rest resources by
@@ -42,10 +75,34 @@ import org.springframework.web.bind.annotation.RequestMethod;
 public class UserController extends BasicController {
 
     private static final Logger logger = LoggerFactory.getLogger(UserController.class);
+
+    private static final SimpleGrantedAuthority ALL_USERS_AUTH = new SimpleGrantedAuthority(Constant.GROUP_ALL_USERS);
+
+    private static final String ADMIN = "ADMIN";
+    private static final String ANALYST = "ANALYST";
+    private static final String MODELER = "MODELER";
+    private static final String ADMIN_DEFAULT = "KYLIN";
+    private static final String ACTIVE_PROFILES_NAME = "spring.profiles.active";
+
     @Autowired
     @Qualifier("userService")
     UserService userService;
 
+    @Autowired
+    private AclEvaluate aclEvaluate;
+
+    @Autowired
+    @Qualifier("accessService")
+    private AccessService accessService;
+
+    @Autowired
+    @Qualifier("userGroupService")
+    private UserGroupService userGroupService;
+
+    private Pattern passwordPattern;
+    private Pattern bcryptPattern;
+    private BCryptPasswordEncoder pwdEncoder;
+
     @RequestMapping(value = "/authentication", method = RequestMethod.POST, produces = { "application/json" })
     public UserDetails authenticate() {
         UserDetails userDetails = authenticatedUser();
@@ -72,4 +129,255 @@ public class UserController extends BasicController {
 
         return null;
     }
+
+    @PostConstruct
+    public void init() throws IOException {
+        passwordPattern = Pattern.compile("^(?=.*\\d)(?=.*[a-zA-Z])(?=.*[~!@#$%^&*(){}|:\"<>?\\[\\];',./`]).{8,}$");
+        bcryptPattern = Pattern.compile("\\A\\$2a?\\$\\d\\d\\$[./0-9A-Za-z]{53}");
+        pwdEncoder = new BCryptPasswordEncoder();
+
+        List<ManagedUser> all = userService.listUsers();
+        logger.info("All {} users", all.size());
+        if (all.isEmpty() && "testing".equals(System.getProperty(ACTIVE_PROFILES_NAME))) {
+            create(ADMIN, new ManagedUser(ADMIN, ADMIN_DEFAULT, true, Constant.ROLE_ADMIN, Constant.GROUP_ALL_USERS));
+            create(ANALYST, new ManagedUser(ANALYST, ANALYST, true, Constant.GROUP_ALL_USERS));
+            create(MODELER, new ManagedUser(MODELER, MODELER, true, Constant.GROUP_ALL_USERS));
+        }
+
+    }
+
+    private void checkProfileEditAllowed() {
+        String activeProfiles = System.getProperty(ACTIVE_PROFILES_NAME);
+        if (!"testing".equals(activeProfiles) && !"custom".equals(activeProfiles)) {
+            throw new BadRequestException("Action not allowed!");
+        }
+    }
+
+    @RequestMapping(value = "/{userName:.+}", method = { RequestMethod.POST }, produces = { "application/json" })
+    @ResponseBody
+    @PreAuthorize(Constant.ACCESS_HAS_ROLE_ADMIN)
+    //do not use aclEvaluate, if getManagedUsersByFuzzMatching there's no users and will come into init() and will call save.
+    public ManagedUser create(@PathVariable("userName") String userName, @RequestBody ManagedUser user) {
+        checkProfileEditAllowed();
+
+        if (StringUtils.equals(getPrincipal(), user.getUsername()) && user.isDisabled()) {
+            throw new ForbiddenException("Action not allowed!");
+        }
+
+        checkUserName(userName);
+
+        user.setUsername(userName);
+        user.setPassword(pwdEncode(user.getPassword()));
+
+        logger.info("Creating {}", user);
+
+        completeAuthorities(user);
+        userService.createUser(user);
+        return get(userName);
+    }
+
+    @RequestMapping(value = "/{userName:.+}", method = { RequestMethod.PUT }, produces = { "application/json" })
+    @ResponseBody
+    @PreAuthorize(Constant.ACCESS_HAS_ROLE_ADMIN)
+    //do not use aclEvaluate, if there's no users and will come into init() and will call save.
+    public ManagedUser save(@PathVariable("userName") String userName, @RequestBody ManagedUser user) {
+        checkProfileEditAllowed();
+
+        if (StringUtils.equals(getPrincipal(), user.getUsername()) && user.isDisabled()) {
+            throw new ForbiddenException("Action not allowed!");
+        }
+
+        checkUserName(userName);
+
+        user.setUsername(userName);
+
+        // merge with existing user
+        try {
+            ManagedUser existing = get(userName);
+            if (existing != null) {
+                if (user.getPassword() == null)
+                    user.setPassword(existing.getPassword());
+                if (user.getAuthorities() == null || user.getAuthorities().isEmpty())
+                    user.setGrantedAuthorities(existing.getAuthorities());
+            }
+        } catch (UsernameNotFoundException ex) {
+            // that is OK, we create new
+        }
+        logger.info("Saving {}", user);
+
+        completeAuthorities(user);
+        userService.updateUser(user);
+        return get(userName);
+    }
+
+    @RequestMapping(value = "/password", method = { RequestMethod.PUT }, produces = { "application/json" })
+    @ResponseBody
+    //change passwd
+    public EnvelopeResponse save(@RequestBody PasswdChangeRequest user) {
+
+        checkProfileEditAllowed();
+
+        if (!this.isAdmin() && !StringUtils.equals(getPrincipal(), user.getUsername())) {
+            throw new ForbiddenException("Permission Denied");
+        }
+        ManagedUser existing = get(user.getUsername());
+        checkUserName(user.getUsername());
+        checkNewPwdRule(user.getNewPassword());
+
+        if (existing != null) {
+            if (!this.isAdmin() && !pwdEncoder.matches(user.getPassword(), existing.getPassword())) {
+                throw new BadRequestException("pwd update error");
+            }
+
+            existing.setPassword(pwdEncode(user.getNewPassword()));
+            existing.setDefaultPassword(false);
+            logger.info("update password for user {}", user);
+
+            completeAuthorities(existing);
+            userService.updateUser(existing);
+
+            // update authentication
+            if (StringUtils.equals(getPrincipal(), user.getUsername())) {
+                UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(existing,
+                        user.getNewPassword(), existing.getAuthorities());
+                token.setDetails(SecurityContextHolder.getContext().getAuthentication().getDetails());
+                SecurityContextHolder.getContext().setAuthentication(token);
+            }
+        }
+
+        return new EnvelopeResponse(ResponseCode.CODE_SUCCESS, get(user.getUsername()), "");
+    }
+
+    private String pwdEncode(String pwd) {
+        if (bcryptPattern.matcher(pwd).matches())
+            return pwd;
+
+        return pwdEncoder.encode(pwd);
+    }
+
+    private void checkUserName(String userName) {
+        if (userName == null || userName.isEmpty())
+            throw new BadRequestException("empty user name");
+    }
+
+    private void checkNewPwdRule(String newPwd) {
+        if (!checkPasswordLength(newPwd)) {
+            throw new BadRequestException("password length need more then 8 chars");
+        }
+
+        if (!checkPasswordCharacter(newPwd)) {
+            throw new BadRequestException("pwd update error");
+        }
+    }
+
+    private boolean checkPasswordLength(String password) {
+        return !(password == null || password.length() < 8);
+    }
+
+    private boolean checkPasswordCharacter(String password) {
+        return passwordPattern.matcher(password).matches();
+    }
+
+    @RequestMapping(value = "/{userName:.+}", method = { RequestMethod.GET }, produces = { "application/json" })
+    @ResponseBody
+    public EnvelopeResponse getUser(@PathVariable("userName") String userName) {
+
+        if (!this.isAdmin() && !StringUtils.equals(getPrincipal(), userName)) {
+            throw new ForbiddenException("...");
+        }
+        return new EnvelopeResponse(ResponseCode.CODE_SUCCESS, get(userName), "");
+    }
+
+    private ManagedUser get(@Nullable String userName) {
+        checkUserName(userName);
+
+        UserDetails details = userService.loadUserByUsername(userName);
+        if (details == null)
+            return null;
+        return (ManagedUser) details;
+    }
+
+    @RequestMapping(value = "/users", method = { RequestMethod.GET }, produces = { "application/json" })
+    @ResponseBody
+    public EnvelopeResponse listAllUsers(@RequestParam(value = "project", required = false) String project,
+            @RequestParam(value = "name", required = false) String name,
+            @RequestParam(value = "groupName", required = false) String groupName,
+            @RequestParam(value = "isFuzzMatch", required = false) boolean isFuzzMatch,
+            @RequestParam(value = "offset", required = false, defaultValue = "0") Integer offset,
+            @RequestParam(value = "limit", required = false, defaultValue = "10") Integer limit) throws IOException {
+        if (project == null) {
+            aclEvaluate.checkIsGlobalAdmin();
+        } else {
+            aclEvaluate.checkProjectAdminPermission(project);
+        }
+        HashMap<String, Object> data = new HashMap<>();
+        List<ManagedUser> usersByFuzzMatching = userService.listUsers(name, groupName, isFuzzMatch);
+        List<ManagedUser> subList = PagingUtil.cutPage(usersByFuzzMatching, offset, limit);
+        //LDAP users dose not have authorities
+        for (ManagedUser u : subList) {
+            userService.completeUserInfo(u);
+        }
+        data.put("users", subList);
+        data.put("size", usersByFuzzMatching.size());
+        return new EnvelopeResponse(ResponseCode.CODE_SUCCESS, data, "");
+    }
+
+    @RequestMapping(value = "/{userName:.+}", method = { RequestMethod.DELETE }, produces = { "application/json" })
+    @ResponseBody
+    public EnvelopeResponse delete(@PathVariable("userName") String userName) throws IOException {
+
+        checkProfileEditAllowed();
+
+        if (StringUtils.equals(getPrincipal(), userName)) {
+            throw new ForbiddenException("...");
+        }
+
+        //delete user's project ACL
+        accessService.revokeProjectPermission(userName, MetadataConstants.TYPE_USER);
+
+        //delete user's table/row/column ACL
+        //        ACLOperationUtil.delLowLevelACL(userName, MetadataConstants.TYPE_USER);
+
+        checkUserName(userName);
+        userService.deleteUser(userName);
+        return new EnvelopeResponse(ResponseCode.CODE_SUCCESS, userName, "");
+    }
+
+    private void completeAuthorities(ManagedUser managedUser) {
+        List<SimpleGrantedAuthority> detailRoles = Lists.newArrayList(managedUser.getAuthorities());
+        for (SimpleGrantedAuthority authority : detailRoles) {
+            try {
+                if (!userGroupService.exists(authority.getAuthority())) {
+                    throw new BadRequestException(String.format(Locale.ROOT,
+                            "user's authority:%s is not found in user group", authority.getAuthority()));
+                }
+            } catch (IOException e) {
+                logger.error("Get user group error: {}", e.getMessage());
+            }
+        }
+        if (!detailRoles.contains(ALL_USERS_AUTH)) {
+            detailRoles.add(ALL_USERS_AUTH);
+        }
+        managedUser.setGrantedAuthorities(detailRoles);
+    }
+
+    private String getPrincipal() {
+        String userName = null;
+
+        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
+        if (authentication == null) {
+            return null;
+        }
+
+        Object principal = authentication.getPrincipal();
+
+        if (principal instanceof UserDetails) {
+            userName = ((UserDetails) principal).getUsername();
+        } else if (authentication.getDetails() instanceof UserDetails) {
+            userName = ((UserDetails) authentication.getDetails()).getUsername();
+        } else {
+            userName = principal.toString();
+        }
+        return userName;
+    }
 }
diff --git a/server-base/src/main/java/org/apache/kylin/rest/request/PasswdChangeRequest.java b/server-base/src/main/java/org/apache/kylin/rest/request/PasswdChangeRequest.java
new file mode 100644
index 0000000..c1eb008
--- /dev/null
+++ b/server-base/src/main/java/org/apache/kylin/rest/request/PasswdChangeRequest.java
@@ -0,0 +1,62 @@
+/*
+ * 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.kylin.rest.request;
+
+import java.io.Serializable;
+
+public class PasswdChangeRequest implements Serializable {
+    private String username;
+    private String password;
+    private String newPassword;
+
+    public PasswdChangeRequest() {
+
+    }
+
+    // only for test now
+    public PasswdChangeRequest(String username, String password, String newPassword) {
+        this.username = username;
+        this.password = password;
+        this.newPassword = newPassword;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+
+    public void setUsername(String username) {
+        this.username = username;
+    }
+
+    public String getPassword() {
+        return password;
+    }
+
+    public void setPassword(String password) {
+        this.password = password;
+    }
+
+    public String getNewPassword() {
+        return newPassword;
+    }
+
+    public void setNewPassword(String newPassword) {
+        this.newPassword = newPassword;
+    }
+
+}
\ No newline at end of file
diff --git a/server-base/src/main/java/org/apache/kylin/rest/security/ManagedUser.java b/server-base/src/main/java/org/apache/kylin/rest/security/ManagedUser.java
index 0d5ce60..3a4f244 100644
--- a/server-base/src/main/java/org/apache/kylin/rest/security/ManagedUser.java
+++ b/server-base/src/main/java/org/apache/kylin/rest/security/ManagedUser.java
@@ -156,18 +156,24 @@ public class ManagedUser extends RootPersistentEntity implements UserDetails {
         }
     }
 
-    public void addAuthoritie(String auth) {
+    public void addAuthorities(String auth) {
         if (this.authorities == null) {
             this.authorities = Lists.newArrayList();
         }
         authorities.add(new SimpleGrantedAuthority(auth));
     }
 
-    public void removeAuthoritie(String auth) {
+    public void removeAuthorities(String auth) {
         Preconditions.checkNotNull(this.authorities);
         authorities.remove(new SimpleGrantedAuthority(auth));
     }
 
+    public void clearAuthenticateFailedRecord() {
+        this.wrongTime = 0;
+        this.locked = false;
+        this.lockedTime = 0;
+    }
+
     public boolean isDisabled() {
         return disabled;
     }
diff --git a/server-base/src/main/java/org/apache/kylin/rest/service/KylinUserGroupService.java b/server-base/src/main/java/org/apache/kylin/rest/service/KylinUserGroupService.java
index 9165e06..033353f 100644
--- a/server-base/src/main/java/org/apache/kylin/rest/service/KylinUserGroupService.java
+++ b/server-base/src/main/java/org/apache/kylin/rest/service/KylinUserGroupService.java
@@ -18,18 +18,39 @@
 
 package org.apache.kylin.rest.service;
 
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import org.apache.kylin.common.KylinConfig;
+import org.apache.kylin.common.persistence.JsonSerializer;
+import org.apache.kylin.common.persistence.ResourceStore;
+import org.apache.kylin.common.persistence.Serializer;
+import org.apache.kylin.common.persistence.WriteConflictException;
+import org.apache.kylin.metadata.MetadataConstants;
+import org.apache.kylin.metadata.acl.UserGroup;
+import org.apache.kylin.rest.constant.Constant;
+import org.apache.kylin.rest.security.ManagedUser;
 import org.apache.kylin.rest.util.AclEvaluate;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.security.core.GrantedAuthority;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.security.core.userdetails.UserDetails;
 
-public class KylinUserGroupService implements IUserGroupService {
+import javax.annotation.PostConstruct;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+public class KylinUserGroupService extends UserGroupService {
+    public static final Logger logger = LoggerFactory.getLogger(KylinUserGroupService.class);
 
+    private ResourceStore store = ResourceStore.getStore(KylinConfig.getInstanceFromEnv());
+    private static final String PATH = "/user_group/";
+    private static final Serializer<UserGroup> USER_GROUP_SERIALIZER = new JsonSerializer<>(UserGroup.class);
     @Autowired
     @Qualifier("userService")
     private UserService userService;
@@ -37,12 +58,6 @@ public class KylinUserGroupService implements IUserGroupService {
     @Autowired
     private AclEvaluate aclEvaluate;
 
-    @Override
-    public List<String> listAllAuthorities(String project) throws IOException {
-        aclEvaluate.checkProjectAdminPermission(project);
-        return getAllUserAuthorities();
-    }
-
     List<String> getAllUserAuthorities() throws IOException {
         List<String> all = new ArrayList<>();
         for (UserDetails user : userService.listUsers()) {
@@ -57,6 +72,135 @@ public class KylinUserGroupService implements IUserGroupService {
 
     @Override
     public boolean exists(String name) throws IOException {
-        return getAllUserAuthorities().contains(name);
+        return getUserGroup().getAllGroups().contains(name);
+    }
+
+    @Autowired
+    @Qualifier("accessService")
+    private AccessService accessService;
+
+    @PostConstruct
+    public void init() throws IOException, InterruptedException {
+        int retry = 100;
+        while (retry > 0) {
+            UserGroup userGroup = getUserGroup();
+            if (!userGroup.exists(Constant.GROUP_ALL_USERS)) {
+                userGroup.add(Constant.GROUP_ALL_USERS);
+            }
+            if (!userGroup.exists(Constant.ROLE_ADMIN)) {
+                userGroup.add(Constant.ROLE_ADMIN);
+            }
+            if (!userGroup.exists(Constant.ROLE_MODELER)) {
+                userGroup.add(Constant.ROLE_MODELER);
+            }
+            if (!userGroup.exists(Constant.ROLE_ANALYST)) {
+                userGroup.add(Constant.ROLE_ANALYST);
+            }
+
+            try {
+                store.checkAndPutResource(PATH, userGroup, USER_GROUP_SERIALIZER);
+                return;
+            } catch (WriteConflictException e) {
+                logger.info("Find WriteConflictException, sleep 100 ms.", e);
+                Thread.sleep(100L);
+                retry--;
+            }
+        }
+        logger.error("Failed to update user group's metadata.");
+    }
+
+    private UserGroup getUserGroup() throws IOException {
+        UserGroup userGroup = store.getResource(PATH, USER_GROUP_SERIALIZER);
+        if (userGroup == null) {
+            userGroup = new UserGroup();
+        }
+        return userGroup;
+    }
+
+    @Override
+    protected List<String> getAllUserGroups() throws IOException {
+        return getUserGroup().getAllGroups();
+    }
+
+    @Override
+    public Map<String, List<String>> getGroupMembersMap() throws IOException {
+        Map<String, List<String>> result = Maps.newHashMap();
+        List<ManagedUser> users = userService.listUsers();
+        for (ManagedUser user : users) {
+            for (SimpleGrantedAuthority authority : user.getAuthorities()) {
+                String role = authority.getAuthority();
+                List<String> usersInGroup = result.get(role);
+                if (usersInGroup == null) {
+                    result.put(role, Lists.newArrayList(user.getUsername()));
+                } else {
+                    usersInGroup.add(user.getUsername());
+                }
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public List<ManagedUser> getGroupMembersByName(String name) throws IOException {
+        List<ManagedUser> users = userService.listUsers();
+        for (Iterator<ManagedUser> it = users.iterator(); it.hasNext();) {
+            ManagedUser user = it.next();
+            if (!user.getAuthorities().contains(new SimpleGrantedAuthority(name))) {
+                it.remove();
+            }
+        }
+        return users;
+    }
+
+    @Override
+    public void addGroup(String name) throws IOException {
+        aclEvaluate.checkIsGlobalAdmin();
+        UserGroup userGroup = getUserGroup();
+        store.checkAndPutResource(PATH, userGroup.add(name), USER_GROUP_SERIALIZER);
+    }
+
+    @Override
+    public void deleteGroup(String name) throws IOException {
+        aclEvaluate.checkIsGlobalAdmin();
+        // remove retained user group in all users
+        List<ManagedUser> managedUsers = userService.listUsers();
+        for (ManagedUser managedUser : managedUsers) {
+            if (managedUser.getAuthorities().contains(new SimpleGrantedAuthority(name))) {
+                managedUser.removeAuthorities(name);
+                userService.updateUser(managedUser);
+            }
+        }
+        //delete group's project ACL
+        accessService.revokeProjectPermission(name, MetadataConstants.TYPE_GROUP);
+        //delete group's table/row/column ACL
+        //        ACLOperationUtil.delLowLevelACL(name, MetadataConstants.TYPE_GROUP);
+
+        store.checkAndPutResource(PATH, getUserGroup().delete(name), USER_GROUP_SERIALIZER);
+    }
+
+    //user's group information is stored by user its own.Object user group does not hold user's ref.
+    @Override
+    public void modifyGroupUsers(String groupName, List<String> users) throws IOException {
+        aclEvaluate.checkIsGlobalAdmin();
+        List<String> groupUsers = new ArrayList<>();
+        for (ManagedUser user : getGroupMembersByName(groupName)) {
+            groupUsers.add(user.getUsername());
+        }
+        List<String> moveInUsers = Lists.newArrayList(users);
+        List<String> moveOutUsers = Lists.newArrayList(groupUsers);
+        moveInUsers.removeAll(groupUsers);
+        moveOutUsers.removeAll(users);
+
+        for (String in : moveInUsers) {
+            ManagedUser managedUser = (ManagedUser) userService.loadUserByUsername(in);
+            managedUser.addAuthorities(groupName);
+            userService.updateUser(managedUser);
+        }
+
+        for (String out : moveOutUsers) {
+            ManagedUser managedUser = (ManagedUser) userService.loadUserByUsername(out);
+            managedUser.removeAuthorities(groupName);
+            userService.updateUser(managedUser);
+        }
     }
 }
diff --git a/server-base/src/main/java/org/apache/kylin/rest/service/KylinUserService.java b/server-base/src/main/java/org/apache/kylin/rest/service/KylinUserService.java
index eea8cd7..ea97118 100644
--- a/server-base/src/main/java/org/apache/kylin/rest/service/KylinUserService.java
+++ b/server-base/src/main/java/org/apache/kylin/rest/service/KylinUserService.java
@@ -25,6 +25,7 @@ import java.util.Locale;
 
 import javax.annotation.PostConstruct;
 
+import org.apache.commons.lang.StringUtils;
 import org.apache.kylin.common.KylinConfig;
 import org.apache.kylin.common.persistence.JsonSerializer;
 import org.apache.kylin.common.persistence.ResourceStore;
@@ -35,8 +36,10 @@ import org.apache.kylin.rest.msg.Message;
 import org.apache.kylin.rest.msg.MsgPicker;
 import org.apache.kylin.rest.security.KylinUserManager;
 import org.apache.kylin.rest.security.ManagedUser;
+import org.apache.kylin.rest.util.AclEvaluate;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.core.authority.SimpleGrantedAuthority;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
@@ -46,6 +49,8 @@ import com.google.common.base.Preconditions;
 public class KylinUserService implements UserService {
 
     private Logger logger = LoggerFactory.getLogger(KylinUserService.class);
+    @Autowired
+    private AclEvaluate aclEvaluate;
 
     public static final String DIR_PREFIX = "/user/";
 
@@ -131,6 +136,18 @@ public class KylinUserService implements UserService {
     }
 
     @Override
+    public List<ManagedUser> listUsers(String userName, Boolean isFuzzMatch) throws IOException {
+        List<ManagedUser> userList = getKylinUserManager().list();
+        return getManagedUsersByFuzzMatching(userName, isFuzzMatch, userList, null);
+    }
+
+    @Override
+    public List<ManagedUser> listUsers(String userName, String groupName, Boolean isFuzzMatch) throws IOException {
+        List<ManagedUser> userList = getKylinUserManager().list();
+        return getManagedUsersByFuzzMatching(userName, isFuzzMatch, userList, groupName);
+    }
+
+    @Override
     public List<String> listAdminUsers() throws IOException {
         List<String> adminUsers = new ArrayList<>();
         for (ManagedUser managedUser : listUsers()) {
@@ -152,4 +169,30 @@ public class KylinUserService implements UserService {
     private KylinUserManager getKylinUserManager() {
         return KylinUserManager.getInstance(KylinConfig.getInstanceFromEnv());
     }
+
+    private List<ManagedUser> getManagedUsersByFuzzMatching(String nameSeg, boolean isFuzzMatch,
+            List<ManagedUser> userList, String groupName) {
+        aclEvaluate.checkIsGlobalAdmin();
+        //for name fuzzy matching
+        if (StringUtils.isBlank(nameSeg) && StringUtils.isBlank(groupName)) {
+            return userList;
+        }
+
+        List<ManagedUser> usersByFuzzyMatching = new ArrayList<>();
+        for (ManagedUser u : userList) {
+            if (!isFuzzMatch && StringUtils.equals(u.getUsername(), nameSeg) && isUserInGroup(u, groupName)) {
+                usersByFuzzyMatching.add(u);
+            }
+            if (isFuzzMatch && StringUtils.containsIgnoreCase(u.getUsername(), nameSeg)
+                    && isUserInGroup(u, groupName)) {
+                usersByFuzzyMatching.add(u);
+            }
+
+        }
+        return usersByFuzzyMatching;
+    }
+
+    private boolean isUserInGroup(ManagedUser user, String groupName) {
+        return StringUtils.isBlank(groupName) || user.getAuthorities().contains(new SimpleGrantedAuthority(groupName));
+    }
 }
diff --git a/server-base/src/main/java/org/apache/kylin/rest/service/KylinUserGroupService.java b/server-base/src/main/java/org/apache/kylin/rest/service/UserGroupService.java
similarity index 53%
copy from server-base/src/main/java/org/apache/kylin/rest/service/KylinUserGroupService.java
copy to server-base/src/main/java/org/apache/kylin/rest/service/UserGroupService.java
index 9165e06..5f23f51 100644
--- a/server-base/src/main/java/org/apache/kylin/rest/service/KylinUserGroupService.java
+++ b/server-base/src/main/java/org/apache/kylin/rest/service/UserGroupService.java
@@ -6,57 +6,60 @@
  * 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.kylin.rest.service;
 
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
+import org.apache.kylin.rest.security.ManagedUser;
 import org.apache.kylin.rest.util.AclEvaluate;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
-import org.springframework.security.core.GrantedAuthority;
-import org.springframework.security.core.userdetails.UserDetails;
 
-public class KylinUserGroupService implements IUserGroupService {
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+public abstract class UserGroupService extends BasicService implements IUserGroupService {
 
     @Autowired
-    @Qualifier("userService")
-    private UserService userService;
+    AclEvaluate aclEvaluate;
 
     @Autowired
-    private AclEvaluate aclEvaluate;
+    @Qualifier("userService")
+    UserService userService;
 
-    @Override
+    // add param project to check user's permission
     public List<String> listAllAuthorities(String project) throws IOException {
-        aclEvaluate.checkProjectAdminPermission(project);
-        return getAllUserAuthorities();
-    }
-
-    List<String> getAllUserAuthorities() throws IOException {
-        List<String> all = new ArrayList<>();
-        for (UserDetails user : userService.listUsers()) {
-            for (GrantedAuthority auth : user.getAuthorities()) {
-                if (!all.contains(auth.getAuthority())) {
-                    all.add(auth.getAuthority());
-                }
-            }
+        if (project == null) {
+            aclEvaluate.checkIsGlobalAdmin();
+        } else {
+            aclEvaluate.checkProjectAdminPermission(project);
         }
-        return all;
+        return getAllUserGroups();
     }
 
-    @Override
     public boolean exists(String name) throws IOException {
-        return getAllUserAuthorities().contains(name);
+        return getAllUserGroups().contains(name);
     }
+
+    public abstract Map<String, List<String>> getGroupMembersMap() throws IOException;
+
+    public abstract List<ManagedUser> getGroupMembersByName(String name) throws IOException;
+
+    protected abstract List<String> getAllUserGroups() throws IOException;
+
+    public abstract void addGroup(String name) throws IOException;
+
+    public abstract void deleteGroup(String name) throws IOException;
+
+    //user's group information is stored by user its own.Object user group does not hold user's ref.
+    public abstract void modifyGroupUsers(String groupName, List<String> users) throws IOException;
 }
diff --git a/server-base/src/main/java/org/apache/kylin/rest/service/UserService.java b/server-base/src/main/java/org/apache/kylin/rest/service/UserService.java
index 21c4cf9..90107a1 100644
--- a/server-base/src/main/java/org/apache/kylin/rest/service/UserService.java
+++ b/server-base/src/main/java/org/apache/kylin/rest/service/UserService.java
@@ -18,12 +18,12 @@
 
 package org.apache.kylin.rest.service;
 
-import java.io.IOException;
-import java.util.List;
-
 import org.apache.kylin.rest.security.ManagedUser;
 import org.springframework.security.provisioning.UserDetailsManager;
 
+import java.io.IOException;
+import java.util.List;
+
 public interface UserService extends UserDetailsManager {
 
     boolean isEvictCacheFlag();
@@ -32,6 +32,10 @@ public interface UserService extends UserDetailsManager {
 
     List<ManagedUser> listUsers() throws IOException;
 
+    List<ManagedUser> listUsers(String userName, Boolean isFuzzyMatch) throws IOException;
+
+    List<ManagedUser> listUsers(String userName, String groupName, Boolean isFuzzyMatch) throws IOException;
+
     List<String> listAdminUsers() throws IOException;
 
     //For performance consideration, list all users may be incomplete(eg. not load user's authorities until authorities has benn used).
diff --git a/server-base/src/main/java/org/apache/kylin/rest/util/AclPermissionUtil.java b/server-base/src/main/java/org/apache/kylin/rest/util/AclPermissionUtil.java
index acf0f77..a0f2d0f 100644
--- a/server-base/src/main/java/org/apache/kylin/rest/util/AclPermissionUtil.java
+++ b/server-base/src/main/java/org/apache/kylin/rest/util/AclPermissionUtil.java
@@ -6,15 +6,15 @@
  * 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.kylin.rest.util;
 
diff --git a/webapp/app/js/app.js b/server-base/src/main/java/org/apache/kylin/rest/util/PagingUtil.java
similarity index 56%
copy from webapp/app/js/app.js
copy to server-base/src/main/java/org/apache/kylin/rest/util/PagingUtil.java
index e505ffc..ebc0d1d 100644
--- a/webapp/app/js/app.js
+++ b/server-base/src/main/java/org/apache/kylin/rest/util/PagingUtil.java
@@ -16,5 +16,30 @@
  * limitations under the License.
  */
 
-//Kylin Application Module
-KylinApp = angular.module('kylin', ['ngRoute', 'ngResource', 'ngGrid', 'ui.grid', 'ui.grid.resizeColumns', 'ui.grid.grouping', 'ui.bootstrap', 'ui.ace', 'base64', 'angularLocalStorage', 'localytics.directives', 'treeControl', 'ngLoadingRequest', 'oitozero.ngSweetAlert', 'ngCookies', 'angular-underscore', 'ngAnimate', 'ui.sortable', 'angularBootstrapNavTree', 'toggle-switch', 'ngSanitize', 'ui.select', 'ui.bootstrap.datetimepicker', 'nvd3', 'ngTagsInput']);
+package org.apache.kylin.rest.util;
+
+import java.util.Collections;
+import java.util.List;
+
+public class PagingUtil {
+
+    public static <T> List<T> cutPage(List<T> full, int offset, int limit) {
+        if (full == null)
+            return null;
+
+        int begin = offset;
+        int end = offset + limit;
+
+        return cut(full, begin, end);
+    }
+
+    private static <T> List<T> cut(List<T> full, int begin, int end) {
+        if (begin >= full.size())
+            return Collections.emptyList();
+
+        if (end > full.size())
+            end = full.size();
+
+        return full.subList(begin, end);
+    }
+}
diff --git a/server/src/main/java/org/apache/kylin/rest/DebugTomcat.java b/server/src/main/java/org/apache/kylin/rest/DebugTomcat.java
index f611403..a238a8f 100644
--- a/server/src/main/java/org/apache/kylin/rest/DebugTomcat.java
+++ b/server/src/main/java/org/apache/kylin/rest/DebugTomcat.java
@@ -18,11 +18,6 @@
 
 package org.apache.kylin.rest;
 
-import java.io.File;
-import java.io.IOException;
-import java.lang.reflect.Field;
-import java.lang.reflect.Modifier;
-
 import org.apache.catalina.Context;
 import org.apache.catalina.core.AprLifecycleListener;
 import org.apache.catalina.core.StandardServer;
@@ -34,6 +29,11 @@ import org.apache.hadoop.security.UserGroupInformation;
 import org.apache.hadoop.util.Shell;
 import org.apache.kylin.common.KylinConfig;
 
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+
 public class DebugTomcat {
 
     public static void setupDebugEnv() {
diff --git a/server/src/main/resources/kylinSecurity.xml b/server/src/main/resources/kylinSecurity.xml
index fe1aeec..a7197ff 100644
--- a/server/src/main/resources/kylinSecurity.xml
+++ b/server/src/main/resources/kylinSecurity.xml
@@ -47,13 +47,13 @@
           class="org.springframework.security.acls.domain.AclAuthorizationStrategyImpl">
         <constructor-arg>
             <list>
-				<bean class="org.springframework.security.core.authority.SimpleGrantedAuthority">
+                <bean class="org.springframework.security.core.authority.SimpleGrantedAuthority">
                     <constructor-arg value="ROLE_ADMIN"/>
                 </bean>
-				<bean class="org.springframework.security.core.authority.SimpleGrantedAuthority">
+                <bean class="org.springframework.security.core.authority.SimpleGrantedAuthority">
                     <constructor-arg value="ROLE_ADMIN"/>
                 </bean>
-				<bean class="org.springframework.security.core.authority.SimpleGrantedAuthority">
+                <bean class="org.springframework.security.core.authority.SimpleGrantedAuthority">
                     <constructor-arg value="ROLE_ADMIN"/>
                 </bean>
             </list>
@@ -68,9 +68,9 @@
         <constructor-arg ref="auditLogger"/>
     </bean>
 
-    <bean id="userService" class="org.apache.kylin.rest.service.KylinUserService" />
+    <bean id="userService" class="org.apache.kylin.rest.service.KylinUserService"/>
 
-    <bean id="userGroupService" class="org.apache.kylin.rest.service.KylinUserGroupService" />
+    <bean id="userGroupService" class="org.apache.kylin.rest.service.KylinUserGroupService"/>
 
     <beans profile="ldap,saml">
         <bean id="ldapSource"
@@ -202,15 +202,7 @@
             <constructor-arg>
                 <bean class="org.springframework.security.authentication.dao.DaoAuthenticationProvider">
                     <property name="userDetailsService">
-                        <bean class="org.springframework.security.provisioning.InMemoryUserDetailsManager">
-                            <constructor-arg>
-                                <util:list
-                                        value-type="org.springframework.security.core.userdetails.User">
-                                    <ref bean="adminUser"></ref>
-                                    <ref bean="modelerUser"></ref>
-                                    <ref bean="analystUser"></ref>
-                                </util:list>
-                            </constructor-arg>
+                        <bean class="org.apache.kylin.rest.service.KylinUserService">
                         </bean>
                     </property>
 
@@ -242,7 +234,7 @@
             <scr:intercept-url pattern="/api/metadata*/**" access="isAuthenticated()"/>
             <scr:intercept-url pattern="/api/**/metrics" access="permitAll"/>
             <scr:intercept-url pattern="/api/cache*/**" access="permitAll"/>
-            <scr:intercept-url pattern="/api/streaming_coordinator/**" access="permitAll" />
+            <scr:intercept-url pattern="/api/streaming_coordinator/**" access="permitAll"/>
             <scr:intercept-url pattern="/api/service_discovery/state/is_active_job_node" access="permitAll"/>
             <scr:intercept-url pattern="/api/cubes/src/tables" access="hasAnyRole('ROLE_ANALYST')"/>
             <scr:intercept-url pattern="/api/cubes*/**" access="isAuthenticated()"/>
@@ -255,8 +247,9 @@
             <scr:intercept-url pattern="/api/tables/**/snapshotLocalCache/**" access="permitAll"/>
             <scr:intercept-url pattern="/api/**" access="isAuthenticated()"/>
 
-            <scr:form-login login-page="/login" />
-            <scr:logout invalidate-session="true" delete-cookies="JSESSIONID" logout-url="/j_spring_security_logout" logout-success-url="/." />
+            <scr:form-login login-page="/login"/>
+            <scr:logout invalidate-session="true" delete-cookies="JSESSIONID" logout-url="/j_spring_security_logout"
+                        logout-success-url="/."/>
             <scr:session-management session-fixation-protection="newSession"/>
         </scr:http>
     </beans>
@@ -290,7 +283,7 @@
             <scr:intercept-url pattern="/api/metadata*/**" access="isAuthenticated()"/>
             <scr:intercept-url pattern="/api/**/metrics" access="permitAll"/>
             <scr:intercept-url pattern="/api/cache*/**" access="permitAll"/>
-            <scr:intercept-url pattern="/api/streaming_coordinator/**" access="permitAll" />
+            <scr:intercept-url pattern="/api/streaming_coordinator/**" access="permitAll"/>
             <scr:intercept-url pattern="/api/cubes/src/tables" access="hasAnyRole('ROLE_ANALYST')"/>
             <scr:intercept-url pattern="/api/cubes*/**" access="isAuthenticated()"/>
             <scr:intercept-url pattern="/api/models*/**" access="isAuthenticated()"/>
@@ -302,8 +295,9 @@
             <scr:intercept-url pattern="/api/tables/**/snapshotLocalCache/**" access="permitAll"/>
             <scr:intercept-url pattern="/api/**" access="isAuthenticated()"/>
 
-            <scr:form-login login-page="/login" />
-            <scr:logout invalidate-session="true" delete-cookies="JSESSIONID" logout-url="/j_spring_security_logout" logout-success-url="/." />
+            <scr:form-login login-page="/login"/>
+            <scr:logout invalidate-session="true" delete-cookies="JSESSIONID" logout-url="/j_spring_security_logout"
+                        logout-success-url="/."/>
             <scr:session-management session-fixation-protection="newSession"/>
         </scr:http>
 
diff --git a/server/src/test/java/org/apache/kylin/rest/controller/AccessControllerTest.java b/server/src/test/java/org/apache/kylin/rest/controller/AccessControllerTest.java
index dea37f5..d0f4c66 100644
--- a/server/src/test/java/org/apache/kylin/rest/controller/AccessControllerTest.java
+++ b/server/src/test/java/org/apache/kylin/rest/controller/AccessControllerTest.java
@@ -18,13 +18,6 @@
 
 package org.apache.kylin.rest.controller;
 
-import static junit.framework.TestCase.fail;
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertTrue;
-
-import java.io.IOException;
-import java.util.List;
-
 import org.apache.kylin.cube.CubeInstance;
 import org.apache.kylin.metadata.project.ProjectInstance;
 import org.apache.kylin.rest.request.AccessRequest;
@@ -34,9 +27,9 @@ import org.apache.kylin.rest.security.AclEntityType;
 import org.apache.kylin.rest.security.AclPermissionType;
 import org.apache.kylin.rest.security.ManagedUser;
 import org.apache.kylin.rest.service.CubeService;
-import org.apache.kylin.rest.service.IUserGroupService;
 import org.apache.kylin.rest.service.ProjectService;
 import org.apache.kylin.rest.service.ServiceTestBase;
+import org.apache.kylin.rest.service.UserGroupService;
 import org.apache.kylin.rest.service.UserService;
 import org.junit.Assert;
 import org.junit.Before;
@@ -48,6 +41,13 @@ import org.springframework.security.authentication.TestingAuthenticationToken;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
 
+import java.io.IOException;
+import java.util.List;
+
+import static junit.framework.TestCase.fail;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
 /**
  * @author xduo
  */
@@ -81,7 +81,7 @@ public class AccessControllerTest extends ServiceTestBase implements AclEntityTy
 
     @Autowired
     @Qualifier("userGroupService")
-    private IUserGroupService userGroupService;
+    private UserGroupService userGroupService;
 
     @Before
     public void setup() throws Exception {
@@ -97,6 +97,11 @@ public class AccessControllerTest extends ServiceTestBase implements AclEntityTy
         List<ProjectInstance> projects = projectController.getProjects(10000, 0);
         assertTrue(projects.size() > 0);
         ProjectInstance project = projects.get(0);
+        userGroupService.addGroup("g1");
+        userGroupService.addGroup("g2");
+        userGroupService.addGroup("g3");
+        userGroupService.addGroup("g4");
+
         ManagedUser user = new ManagedUser("u", "kylin", false, "all_users", "g1", "g2", "g3", "g4");
         userService.createUser(user);
 
diff --git a/server/src/test/java/org/apache/kylin/rest/controller/UserControllerTest.java b/server/src/test/java/org/apache/kylin/rest/controller/UserControllerTest.java
index f6b4ae1..2c21e0a 100644
--- a/server/src/test/java/org/apache/kylin/rest/controller/UserControllerTest.java
+++ b/server/src/test/java/org/apache/kylin/rest/controller/UserControllerTest.java
@@ -18,50 +18,50 @@
 
 package org.apache.kylin.rest.controller;
 
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
 import org.apache.kylin.rest.constant.Constant;
 import org.apache.kylin.rest.security.ManagedUser;
 import org.apache.kylin.rest.service.ServiceTestBase;
 import org.junit.Assert;
-import org.junit.Before;
-import org.junit.BeforeClass;
 import org.junit.Test;
-import org.springframework.security.authentication.TestingAuthenticationToken;
-import org.springframework.security.core.Authentication;
-import org.springframework.security.core.GrantedAuthority;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.core.userdetails.UserDetails;
 
+import java.io.IOException;
+
 /**
  * @author xduo
  */
 public class UserControllerTest extends ServiceTestBase {
 
+    @Autowired
     private UserController userController;
 
-    @BeforeClass
-    public static void setupResource() {
-        staticCreateTestMetadata();
-        List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
-        ManagedUser user = new ManagedUser("ADMIN", "ADMIN", false, authorities);
-        Authentication authentication = new TestingAuthenticationToken(user, "ADMIN", Constant.ROLE_ADMIN);
-        SecurityContextHolder.getContext().setAuthentication(authentication);
-    }
+    private ManagedUser userAdmin = new ManagedUser("ADMIN", "KYLIN", false, Constant.ROLE_ADMIN);
 
-    @Before
-    public void setup() throws Exception {
-        super.setup();
+    private ManagedUser userModeler = new ManagedUser("MODELER", "MODELER", false, Constant.ROLE_MODELER);
 
-        userController = new UserController();
+    @Test
+    public void testLogin() throws IOException {
+        logInWithUser(userAdmin);
+        UserDetails userDetail = userController.authenticatedUser();
+        ManagedUser user = (ManagedUser) userDetail;
+        Assert.assertTrue(user.equals(userAdmin));
     }
 
     @Test
-    public void testBasics() throws IOException {
-        UserDetails user = userController.authenticate();
-        Assert.assertNotNull(user);
-        Assert.assertTrue(user.getUsername().equals("ADMIN"));
+    public void testLoginAsAnotherUser() throws IOException {
+        logInWithUser(userModeler);
+        UserDetails userDetail = userController.authenticate();
+        ManagedUser user = (ManagedUser) userDetail;
+        Assert.assertTrue(user.equals(userModeler));
+    }
+
+    private void logInWithUser(ManagedUser user) {
+        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(user, user.getPassword(),
+                user.getAuthorities());
+        token.setDetails(SecurityContextHolder.getContext().getAuthentication().getDetails());
+        SecurityContextHolder.getContext().setAuthentication(token);
     }
 }
diff --git a/server/src/test/java/org/apache/kylin/rest/service/ServiceTestBase.java b/server/src/test/java/org/apache/kylin/rest/service/ServiceTestBase.java
index ee5cbd1..36a36cd 100644
--- a/server/src/test/java/org/apache/kylin/rest/service/ServiceTestBase.java
+++ b/server/src/test/java/org/apache/kylin/rest/service/ServiceTestBase.java
@@ -20,6 +20,7 @@ package org.apache.kylin.rest.service;
 
 import java.io.IOException;
 import java.util.Arrays;
+import java.util.List;
 
 import org.apache.curator.test.TestingServer;
 import org.apache.kylin.common.KylinConfig;
@@ -58,6 +59,10 @@ public class ServiceTestBase extends LocalFileMetadataTestCase {
     @Qualifier("userService")
     UserService userService;
 
+    @Autowired
+    @Qualifier("userGroupService")
+    UserGroupService userGroupService;
+
     @BeforeClass
     public static void setupResource() throws Exception {
         staticCreateTestMetadata();
@@ -76,9 +81,23 @@ public class ServiceTestBase extends LocalFileMetadataTestCase {
     @Before
     public void setup() throws Exception {
         this.createTestMetadata();
-
+        Authentication authentication = new TestingAuthenticationToken("ADMIN", "ADMIN", Constant.ROLE_ADMIN);
+        SecurityContextHolder.getContext().setAuthentication(authentication);
         KylinConfig config = KylinConfig.getInstanceFromEnv();
         Broadcaster.getInstance(config).notifyClearAll();
+        List<String> userGroups = userGroupService.getAllUserGroups();
+        if (!userGroups.contains(Constant.GROUP_ALL_USERS)) {
+            userGroupService.addGroup(Constant.GROUP_ALL_USERS);
+        }
+        if (!userGroups.contains(Constant.ROLE_ADMIN)) {
+            userGroupService.addGroup(Constant.ROLE_ADMIN);
+        }
+        if (!userGroups.contains(Constant.ROLE_MODELER)) {
+            userGroupService.addGroup(Constant.ROLE_MODELER);
+        }
+        if (!userGroups.contains(Constant.ROLE_ANALYST)) {
+            userGroupService.addGroup(Constant.ROLE_ANALYST);
+        }
 
         if (!userService.userExists("ADMIN")) {
             userService.createUser(new ManagedUser("ADMIN", "KYLIN", false, Arrays.asList(//
@@ -88,14 +107,15 @@ public class ServiceTestBase extends LocalFileMetadataTestCase {
 
         if (!userService.userExists("MODELER")) {
             userService.createUser(new ManagedUser("MODELER", "MODELER", false, Arrays.asList(//
-                            new SimpleGrantedAuthority(Constant.ROLE_ANALYST),
-                            new SimpleGrantedAuthority(Constant.ROLE_MODELER))));
+                    new SimpleGrantedAuthority(Constant.ROLE_ANALYST),
+                    new SimpleGrantedAuthority(Constant.ROLE_MODELER))));
         }
 
         if (!userService.userExists("ANALYST")) {
             userService.createUser(new ManagedUser("ANALYST", "ANALYST", false, Arrays.asList(//
                     new SimpleGrantedAuthority(Constant.ROLE_ANALYST))));
         }
+
     }
 
     @After
diff --git a/server/src/test/java/org/apache/kylin/rest/util/ValidateUtilTest.java b/server/src/test/java/org/apache/kylin/rest/util/ValidateUtilTest.java
index 62bd203..c35420b 100644
--- a/server/src/test/java/org/apache/kylin/rest/util/ValidateUtilTest.java
+++ b/server/src/test/java/org/apache/kylin/rest/util/ValidateUtilTest.java
@@ -18,11 +18,7 @@
 
 package org.apache.kylin.rest.util;
 
-import static org.apache.kylin.metadata.MetadataConstants.TYPE_GROUP;
-import static org.apache.kylin.metadata.MetadataConstants.TYPE_USER;
-
-import java.io.IOException;
-
+import com.google.common.collect.Lists;
 import org.apache.kylin.common.persistence.RootPersistentEntity;
 import org.apache.kylin.rest.security.AclPermission;
 import org.apache.kylin.rest.service.AccessService;
@@ -32,7 +28,10 @@ import org.junit.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 
-import com.google.common.collect.Lists;
+import java.io.IOException;
+
+import static org.apache.kylin.metadata.MetadataConstants.TYPE_GROUP;
+import static org.apache.kylin.metadata.MetadataConstants.TYPE_USER;
 
 public class ValidateUtilTest extends ServiceTestBase {
     private final String PROJECT = "default";
diff --git a/webapp/app/index.html b/webapp/app/index.html
index eac7909..193f903 100644
--- a/webapp/app/index.html
+++ b/webapp/app/index.html
@@ -155,6 +155,7 @@
 <script src="js/services/hybridInstance.js"></script>
 <script src="js/services/dashboard.js"></script>
 <script src="js/services/instance.js"></script>
+<script src="js/services/userGroup.js"></script>
 
 <script src="js/model/cubeConfig.js"></script>
 <script src="js/model/jobConfig.js"></script>
@@ -182,6 +183,7 @@
 <script src="js/services/badQuery.js"></script>
 <script src="js/utils/utils.js"></script>
 <script src="js/utils/liquidFillGauge.js"></script>
+<script src="js/utils/response.js"></script>
 <script src="js/controllers/page.js"></script>
 <script src="js/controllers/index.js"></script>
 <script src="js/controllers/access.js"></script>
@@ -224,7 +226,7 @@
 <script src="js/controllers/streamingBalanceAssignGroup.js"></script>
 <script src="js/controllers/adminStreaming.js"></script>
 <script src="js/controllers/instances.js"></script>
-
+<script src="js/controllers/userGroup.js"></script>
 <!-- endref -->
 
 <!-- ref:remove -->
diff --git a/webapp/app/js/app.js b/webapp/app/js/app.js
index e505ffc..a7f43b7 100644
--- a/webapp/app/js/app.js
+++ b/webapp/app/js/app.js
@@ -17,4 +17,4 @@
  */
 
 //Kylin Application Module
-KylinApp = angular.module('kylin', ['ngRoute', 'ngResource', 'ngGrid', 'ui.grid', 'ui.grid.resizeColumns', 'ui.grid.grouping', 'ui.bootstrap', 'ui.ace', 'base64', 'angularLocalStorage', 'localytics.directives', 'treeControl', 'ngLoadingRequest', 'oitozero.ngSweetAlert', 'ngCookies', 'angular-underscore', 'ngAnimate', 'ui.sortable', 'angularBootstrapNavTree', 'toggle-switch', 'ngSanitize', 'ui.select', 'ui.bootstrap.datetimepicker', 'nvd3', 'ngTagsInput']);
+KylinApp = angular.module('kylin', ['ngRoute', 'ngResource', 'ngGrid', 'ui.grid', 'ui.grid.resizeColumns', 'ui.grid.grouping', 'ui.bootstrap', 'ui.bootstrap.pagination', 'ui.ace', 'base64', 'angularLocalStorage', 'localytics.directives', 'treeControl', 'ngLoadingRequest', 'oitozero.ngSweetAlert', 'ngCookies', 'angular-underscore', 'ngAnimate', 'ui.sortable', 'angularBootstrapNavTree', 'toggle-switch', 'ngSanitize', 'ui.select', 'ui.bootstrap.datetimepicker', 'nvd3', 'ngTagsInput']);
diff --git a/webapp/app/js/controllers/admin.js b/webapp/app/js/controllers/admin.js
index fb0dac2..9fce648 100644
--- a/webapp/app/js/controllers/admin.js
+++ b/webapp/app/js/controllers/admin.js
@@ -21,7 +21,18 @@
 KylinApp.controller('AdminCtrl', function ($scope, AdminService, CacheService, TableService, loadingRequest, MessageService, ProjectService, $modal, SweetAlert,kylinConfig,ProjectModel,$window, MessageBox) {
   $scope.configStr = "";
   $scope.envStr = "";
-
+  $scope.active = {
+    tab_instance: true
+  }
+  $scope.tabData = {}
+  $scope.activateTab = function(tab) {
+    $scope.active = {}; //reset
+    $scope.active[tab] = true;
+  }
+  $scope.$on('change.active', function(event, data) {  
+    $scope.activateTab(data.activeTab);
+    $scope.tabData.groupName = data.groupName
+  });
   $scope.isCacheEnabled = function(){
     return kylinConfig.isCacheEnabled();
   }
diff --git a/webapp/app/js/controllers/userGroup.js b/webapp/app/js/controllers/userGroup.js
new file mode 100644
index 0000000..d2c4b6d
--- /dev/null
+++ b/webapp/app/js/controllers/userGroup.js
@@ -0,0 +1,387 @@
+/*
+ * 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.
+*/
+
+'use strict';
+
+KylinApp
+  .controller('UserGroupCtrl', function ($scope, kylinConfig, UserGroupService, ProjectModel, SweetAlert, $modal, UserService, ResponseUtil) {
+    $scope.grouploading = false;
+    $scope.userloading = false;
+    $scope.dialogActionLoading = false;
+    $scope.groups = [];
+    $scope.groupsTotal = 0;
+    $scope.users = [];
+    $scope.usersTotal = 0;
+    kylinConfig.init().$promise.then(function() {
+      $scope.securityType = kylinConfig.getSecurityType();
+      $scope.allowUseUserAndGroupModule = ['testing', 'custom'].indexOf($scope.securityType) >= 0;
+    })
+    $scope.page = {
+      curpage: kylinConfig.page.offset,
+      limit:  kylinConfig.page.limit
+    }
+    $scope.editPage = {
+      curpage: kylinConfig.page.offset,
+      limit: kylinConfig.page.limit
+    }
+    $scope.selectGroups = {};
+    $scope.selectUsers = {};
+    
+    
+    $scope.filter = {
+      name: '',
+      groupName: ''
+    };
+    var createChangePwdMeta = function () {
+      return {
+        repeatPassword: '',
+        newPassword:''
+      }
+    }
+    var createUserMeta = function () {
+      return {
+        name: '',
+        password: '',
+        isAdmin: false,
+        authorities: ["ALL_USERS"]
+      }
+    }
+    var createGroupMeta = function () {
+      return {
+        name: ''
+      }
+    }
+    var createFilter = function (offset, limit, pageObj) {
+      offset = (!!offset) ? offset : pageObj.curpage;
+      if (pageObj) {
+        pageObj.curpage = offset;
+      }
+      offset = offset - 1;
+      limit = (!!limit) ? limit : pageObj.limit;
+      return {
+        offset: offset * limit,
+        limit: (offset + 1) * limit,
+        name: $scope.filter.name,
+        groupName: $scope.tabData.groupName,
+        isFuzzMatch: true,
+        project: ProjectModel.selectedProject
+      };
+    }
+    $scope.showUserListByGroup = function (groupName) {
+      $scope.$emit('change.active', {
+        activeTab: 'tab_users',
+        groupName: groupName
+      });
+    };
+    $scope.removeGroupFilter = function () {
+      $scope.$emit('change.active', {
+        activeTab: 'tab_users',
+        groupName: ''
+      });
+      $scope.listUsers();
+    }
+    $scope.user = createUserMeta();
+    $scope.group = createGroupMeta();
+    $scope.getGroupList = function (offset, limit, pageObj) {
+      var queryParam = createFilter(offset, limit, pageObj);
+      $scope.grouploading = true;
+      UserGroupService.listGroups(queryParam, function (res) {
+        $scope.groups = res && res.data.groups || [];
+        $scope.groupsTotal = res.data.size || 0;
+        $scope.grouploading = false;
+      }, function (res) {
+        $scope.grouploading = false;
+        ResponseUtil.handleError(res);
+      });
+    }
+    $scope.getUserList = function (offset, limit, pageObj) {
+      var queryParam = createFilter(offset, limit, pageObj);
+      $scope.grouploading = true;
+      UserService.listUsers(queryParam, function (res) {
+        $scope.users = res && res.data.users || [];
+        $scope.usersTotal = res.data.size || 0;
+        $scope.userloading = false;
+      }, function (res) {
+        $scope.userloading = false;
+        ResponseUtil.handleError(res);
+      });
+    }
+    $scope.listUsers = function (offset, limit) {
+      $scope.getUserList(offset, limit, $scope.page);
+    }
+    $scope.listGroups = function (offset, limit) {
+      $scope.getGroupList(offset, limit, $scope.page);
+    }
+    $scope.listEditUsers = function (offset, limit) {
+      $scope.getUserList(offset, limit, $scope.editPage);
+    }
+    $scope.listEditGroups = function (offset, limit) {
+      $scope.getGroupList(offset, limit, $scope.editPage);
+    }
+
+    $scope.delUser = function (userName) {
+      SweetAlert.swal({
+        title: '',
+        text: "Are you sure to delete the user " + userName + "?",
+        showCancelButton: true,
+        confirmButtonColor: '#DD6B55',
+        confirmButtonText: "Yes",
+        closeOnConfirm: true
+      }, function(isConfirm) {
+        if(isConfirm){
+          UserService.delUser({
+            userName: userName
+          }, function () {
+            SweetAlert.swal('Delete successfuly', null, 'success');
+            $scope.listUsers($scope.page.curpage);
+          }, function (e) {
+            ResponseUtil.handleError(e);
+          })
+        }
+      })
+    }
+    var updateUser = function (user, isDisable) {
+      let updateUser = angular.extend({}, user)
+      updateUser.disabled = isDisable
+      UserService.updateUser({userName: updateUser.username}, updateUser, function () {
+        $scope.listUsers($scope.page.curpage);
+        SweetAlert.swal('User status update successfuly', null, 'success');
+      }, function (e) {
+        ResponseUtil.handleError(e);
+      })
+    }
+    $scope.disableUser = function (user) {
+      updateUser(user, true);
+    }
+    $scope.enableUser = function (user) {
+      updateUser(user, false);
+    }
+    $scope.delGroup = function (groupName) {
+      SweetAlert.swal({
+        title: '',
+        text: "Are you sure to delete the goup " + groupName + "?",
+        showCancelButton: true,
+        confirmButtonColor: '#DD6B55',
+        confirmButtonText: "Yes",
+        closeOnConfirm: true
+      }, function(isConfirm) {
+        if(isConfirm){
+          UserGroupService.delGroup({
+            group: groupName
+          }, function () {
+            SweetAlert.swal('Delete successfuly', null, 'success');
+            $scope.listGroups($scope.page.curpage);
+          }, function (res) {
+            ResponseUtil.handleError(res);
+          })
+        }
+      })
+    }
+    $scope.isAllchecked = function(selectedItems, items) {
+      if (items && items.hasOwnProperty('length')) {
+        for(let i = 0; i < items.length; i++) {
+          var item = typeof items[i] === "object" ? items[i].username : items[i];
+          if (!selectedItems[item]) {
+            return false;
+          }
+        }
+      } else {
+        for (let i in items) {
+          if (!selectedItems[i]) {
+            return false;
+          }
+        }
+      }
+      return true;
+    }
+    $scope.selectAllUsers = function(element){
+      setTimeout(function() {
+        $scope.isChecked = element.checked;
+        $scope.users.forEach((u) => {
+          $scope.selectUsers[u.username] = $scope.isChecked;
+        })
+        $scope.$apply()
+      }, 1);
+    }
+    $scope.selectAllGroups = function(element){
+      setTimeout(function() {
+        $scope.isChecked = element.checked;
+        for (let g in $scope.groups) {
+          $scope.selectGroups[g] = $scope.isChecked;
+        }
+        $scope.$apply()
+      }, 1);
+    }
+    $scope.selectGroup = function (groupName) {
+      $scope.selectGroups[groupName] = !$scope.selectGroups[groupName];
+    }
+    var userEditCtr = function ($scope, $modalInstance, UserService,SweetAlert, kylinConfig) {
+      $scope.userPattern = /^[\w.@]+$/;
+      $scope.groupPattern = /^[\w.@]+$/;
+      $scope.pwdPattern = /^(?=.*\d)(?=.*[a-z])(?=.*[~!@#$%^&*(){}|:"<>?[\];',./`]).{8,}$/;
+      $scope.saveAssignGroup = function () {
+        $scope.user.authorities = [];
+        for (let i in $scope.selectGroups) {
+          if ($scope.selectGroups[i]) {
+            $scope.user.authorities.push(i);
+          }
+        }
+        $scope.dialogActionLoading = true;
+        UserService.assignGroup({
+          userName: $scope.user.name
+        }, $scope.user, function () {
+          $scope.dialogActionLoading = false;
+          $modalInstance.dismiss('cancel');
+          SweetAlert.swal('Assign successfuly', null, 'success');
+          $scope.listUsers($scope.page.curpage);
+        }, function (res){
+          $scope.dialogActionLoading = false;
+          ResponseUtil.handleError(res);
+        })
+      }
+
+      $scope.saveAssignUser = function () {
+        let users = []
+        for (let i in $scope.selectUsers) {
+          if ($scope.selectUsers[i]) {
+            users.push(i);
+          }
+        }
+        $scope.dialogActionLoading = true;
+        UserGroupService.assignUsers({
+          group: $scope.group.name
+        }, users, function () {
+          $scope.dialogActionLoading = false;
+          $modalInstance.dismiss('cancel');
+          SweetAlert.swal('Assign successfuly', null, 'success');
+          $scope.listGroups($scope.page.curpage);
+        }, function (res){
+          $scope.dialogActionLoading = false;
+          ResponseUtil.handleError(res);
+        })
+      }
+
+      $scope.cancel = function () {
+        $modalInstance.dismiss('cancel');
+      }
+      $scope.saveUser = function () {
+        $scope.dialogActionLoading = true;
+        if ($scope.user.isAdmin) {
+          $scope.user.authorities.push('ROLE_ADMIN');
+        }
+        UserService.addUser({
+          userName: $scope.user.name
+        }, $scope.user, function() {
+          $scope.dialogActionLoading = false;
+          $modalInstance.dismiss('cancel');
+          SweetAlert.swal('Add user successfuly', null, 'success');
+          $scope.listUsers();
+        }, function (res){
+          $scope.dialogActionLoading = false;
+          ResponseUtil.handleError(res)
+        })
+      }
+      $scope.saveGroup = function () {
+        $scope.dialogActionLoading = true;
+        UserGroupService.addGroup({
+          group: $scope.group.name
+        }, null, function () {
+          $modalInstance.dismiss('cancel');
+          SweetAlert.swal('Add group successfuly', null, 'success');
+          $scope.listGroups();
+          $scope.dialogActionLoading = false;
+        }, function (res){
+          $scope.dialogActionLoading = false;
+          ResponseUtil.handleError(res)
+        })
+      }
+      $scope.saveNewPassword = function () {
+        $scope.dialogActionLoading = true;
+        UserService.changePwd($scope.changePwdUser, function () {
+          $modalInstance.dismiss('cancel');
+          SweetAlert.swal('Change password successfuly', null, 'success');
+          $scope.listUsers();
+          $scope.dialogActionLoading = false;
+        }, function (e) {
+          $scope.dialogActionLoading = false;
+          ResponseUtil.handleError(e);
+        })
+      }
+    }
+    $scope.createUser = function () {
+      $scope.user = createUserMeta();
+      $modal.open({
+        templateUrl: 'addUser.html',
+        controller: userEditCtr,
+        scope: $scope
+      });
+    }
+    $scope.createGroup = function () {
+      $scope.group = createGroupMeta();
+      $modal.open({
+        templateUrl: 'addGroup.html',
+        controller: userEditCtr,
+        scope: $scope
+      });
+    }
+    $scope.assignToGroup = function (userName, authorities) {
+      $scope.listEditGroups();
+      $scope.user.name = userName;
+      $scope.selectGroups = {};
+      authorities.forEach(auth => {
+        $scope.selectGroups[auth.authority] = true;
+      })
+      $modal.open({
+        templateUrl: 'assignGroup.html',
+        controller: userEditCtr,
+        scope: $scope
+      });
+    }
+    $scope.assignToUser = function (groupName) {
+      $scope.listEditUsers();
+      $scope.group.name = groupName;
+      UserGroupService.getUsersByGroup({
+        group: groupName
+      }, {}, function(res) {
+        ResponseUtil.handleSuccess(res, function(data) {
+          let groupUsers = data.users || []
+          $scope.selectUsers = {};
+          groupUsers.forEach(function(user){
+            $scope.selectUsers[user.username] = true;
+          })
+          $modal.open({
+            templateUrl: 'assignUser.html',
+            controller: userEditCtr,
+            scope: $scope
+          });
+        })
+      })  
+    }
+    $scope.changePwdUser = createChangePwdMeta()
+    $scope.changePwd = function (user) {
+      $scope.changePwdUser.username = user.username;
+      $scope.changePwdUser.password = user.password;
+      $scope.changePwdUser.repeatPassword = '';
+      $scope.changePwdUser.newPassword = '';
+      $modal.open({
+        templateUrl: 'changePwd.html',
+        controller: userEditCtr,
+        scope: $scope
+      });
+    }
+  });
diff --git a/webapp/app/js/directives/directives.js b/webapp/app/js/directives/directives.js
index 1999cb8..1ba9687 100644
--- a/webapp/app/js/directives/directives.js
+++ b/webapp/app/js/directives/directives.js
@@ -99,7 +99,7 @@ KylinApp.directive('kylinPagination', function ($parse, $q) {
     }
   };
 })
-  .directive('loading', function ($parse, $q) {
+.directive('loading', function ($parse, $q) {
     return {
       restrict: 'E',
       scope: {},
diff --git a/webapp/app/js/services/kylinProperties.js b/webapp/app/js/services/kylinProperties.js
index 3c7a66f..fed071b 100644
--- a/webapp/app/js/services/kylinProperties.js
+++ b/webapp/app/js/services/kylinProperties.js
@@ -21,7 +21,6 @@ KylinApp.service('kylinConfig', function (AdminService, $log) {
   var timezone;
   var deployEnv;
 
-
   this.init = function () {
     return AdminService.publicConfig({}, function (config) {
       _config = config.config;
@@ -170,5 +169,13 @@ KylinApp.service('kylinConfig', function (AdminService, $log) {
     }
     return this.sourceType;
   }
+  this.getSecurityType = function () {
+    this.securityType = this.getProperty("kylin.security.profile").trim();
+    return this.securityType;
+  }
+  this.page = {
+    offset: 1,
+    limit: 15
+  }
 });
 
diff --git a/webapp/app/js/app.js b/webapp/app/js/services/userGroup.js
similarity index 58%
copy from webapp/app/js/app.js
copy to webapp/app/js/services/userGroup.js
index e505ffc..22172d6 100644
--- a/webapp/app/js/app.js
+++ b/webapp/app/js/services/userGroup.js
@@ -16,5 +16,13 @@
  * limitations under the License.
  */
 
-//Kylin Application Module
-KylinApp = angular.module('kylin', ['ngRoute', 'ngResource', 'ngGrid', 'ui.grid', 'ui.grid.resizeColumns', 'ui.grid.grouping', 'ui.bootstrap', 'ui.ace', 'base64', 'angularLocalStorage', 'localytics.directives', 'treeControl', 'ngLoadingRequest', 'oitozero.ngSweetAlert', 'ngCookies', 'angular-underscore', 'ngAnimate', 'ui.sortable', 'angularBootstrapNavTree', 'toggle-switch', 'ngSanitize', 'ui.select', 'ui.bootstrap.datetimepicker', 'nvd3', 'ngTagsInput']);
+KylinApp.factory('UserGroupService', ['$resource', function ($resource, config) {
+  return $resource(Config.service.url + 'user_group/:action/:group', {}, {
+    listGroups: {method: 'GET', params: {action:'groups'}, isArray: false},
+    addGroup: {method: 'POST', params: {}, isArray: false},
+    delGroup: {method: 'DELETE', params: {}, isArray: false},
+    editGroup: {method: 'PUT', params: {}, isArray: false},
+    assignUsers: {method: 'PUT', params: {action: 'users'}, isArray: false},
+    getUsersByGroup: {method: 'GET', params: {action:'users'}, isArray: false}
+  });
+}]);
diff --git a/webapp/app/js/services/users.js b/webapp/app/js/services/users.js
index bccd414..97d5de4 100644
--- a/webapp/app/js/services/users.js
+++ b/webapp/app/js/services/users.js
@@ -16,7 +16,7 @@
  * limitations under the License.
 */
 
-KylinApp.service('UserService', function ($http, $q) {
+KylinApp.service('UserService', function ($resource) {
     var curUser = {};
 
     this.getCurUser = function () {
@@ -43,4 +43,14 @@ KylinApp.service('UserService', function ($http, $q) {
     this.getHomePage = function () {
         return this.isAuthorized()? "/models" : "/login";
     }
+
+    var apiService = $resource(Config.service.url + 'user/:action/:userName', {}, {
+      listUsers: {method: 'GET', params: {action: 'users'}, isArray: false},
+      delUser: {method: 'DELETE', isArray: false},
+      addUser: {method: 'POST',  params: {} ,isArray: false},
+      changePwd: {method: 'PUT', params: {action: 'password'}, isArray: false},
+      updateUser: {method: 'PUT', params: {}, isArray: false},
+      assignGroup: {method: 'PUT', params: {}, isArray: false}
+    });
+    angular.extend(this, apiService)
 });
diff --git a/webapp/app/js/app.js b/webapp/app/js/utils/response.js
similarity index 51%
copy from webapp/app/js/app.js
copy to webapp/app/js/utils/response.js
index e505ffc..5ebf5a6 100644
--- a/webapp/app/js/app.js
+++ b/webapp/app/js/utils/response.js
@@ -16,5 +16,30 @@
  * limitations under the License.
  */
 
-//Kylin Application Module
-KylinApp = angular.module('kylin', ['ngRoute', 'ngResource', 'ngGrid', 'ui.grid', 'ui.grid.resizeColumns', 'ui.grid.grouping', 'ui.bootstrap', 'ui.ace', 'base64', 'angularLocalStorage', 'localytics.directives', 'treeControl', 'ngLoadingRequest', 'oitozero.ngSweetAlert', 'ngCookies', 'angular-underscore', 'ngAnimate', 'ui.sortable', 'angularBootstrapNavTree', 'toggle-switch', 'ngSanitize', 'ui.select', 'ui.bootstrap.datetimepicker', 'nvd3', 'ngTagsInput']);
+'use strict';
+
+/* utils */
+
+KylinApp.factory('ResponseUtil', function (SweetAlert) {
+  return {
+    handleError: function (e, cb) {
+      if (typeof cb === 'function') {
+        return cb(e)
+      }
+      if (e.data&& e.data.exception){
+        var message =e.data.exception;
+        var msg = !!(message) ? message : 'Failed to take action.';
+        SweetAlert.swal('Oops...', msg, 'error');
+      } else{
+        SweetAlert.swal('Oops...', "Failed to take action.", 'error');
+      }
+    },
+    // use in standard api response  res = {data: {}, code:000, msg: ''}
+    handleSuccess: function (res, cb) {
+      let data = res && res.data || {}
+      if (typeof cb === 'function') {
+        return cb(data, res.code, res.msg)
+      }
+    }
+  }
+});
diff --git a/webapp/app/less/app.less b/webapp/app/less/app.less
index 35631c9..483fd74 100644
--- a/webapp/app/less/app.less
+++ b/webapp/app/less/app.less
@@ -1136,4 +1136,7 @@ tags-input .tags .tag-item {
 .receiver-stats-modal .modal-dialog{
   margin-top: 100px;
   max-width: 600px;
+}
+.pagination{
+  cursor: pointer;
 }
\ No newline at end of file
diff --git a/webapp/app/partials/admin/admin.html b/webapp/app/partials/admin/admin.html
index 5512386..fb5acaf 100644
--- a/webapp/app/partials/admin/admin.html
+++ b/webapp/app/partials/admin/admin.html
@@ -18,16 +18,22 @@
 
 <div class="row"  style="padding-top:10px;padding-left: 5px;">
   <div ng-class="row">
-    <tabset>
-      <tab heading="Configuration" select="getEnv();getConfig();">
+    <tabset active="activeTab">
+      <tab active="active['tab_config']" heading="Configuration" select="getEnv();getConfig();">
         <div class="col-xs-12" ng-include src="'partials/admin/config.html'"></div>
       </tab>
-      <tab ng-if="isCuratorScheduler()" heading="Instances" select="list()" ng-controller="InstanceCtrl">
+      <tab  active="active['tab_instance']" ng-if="isCuratorScheduler()" heading="Instances" select="list()" ng-controller="InstanceCtrl">
         <div class="col-xs-12" ng-include src="'partials/admin/instances.html'"></div>
       </tab>
-      <tab heading="Streaming" select="listReplicaSet()" ng-controller="AdminStreamingCtrl">
+      <tab active="active['tab_streaming']" heading="Streaming" select="listReplicaSet()" ng-controller="AdminStreamingCtrl">
         <div class="col-xs-12" ng-include src="'partials/admin/streaming.html'"></div>
       </tab>
+      <tab active="active['tab_users']" heading="User" select="listUsers()" ng-controller="UserGroupCtrl">
+        <div class="col-xs-12" ng-include src="'partials/admin/user.html'"></div>
+      </tab>
+      <tab active="active['tab_groups']"heading="Group" select="listGroups()" ng-controller="UserGroupCtrl">
+        <div class="col-xs-12" ng-include src="'partials/admin/group.html'"></div>
+      </tab>
     </tabset>
   </div>
 </div>
diff --git a/webapp/app/partials/admin/change_pwd.html b/webapp/app/partials/admin/change_pwd.html
new file mode 100644
index 0000000..6ae1679
--- /dev/null
+++ b/webapp/app/partials/admin/change_pwd.html
@@ -0,0 +1,78 @@
+<!--
+* 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.
+-->
+
+<script type="text/ng-template" id="changePwd.html">
+  <ng-form name="change_pwd_form">
+    <div class="modal-header">
+      <h4>Change Password</h4>
+    </div>
+    <input name="username" type="text" style="display:none"/>
+    <input name="password" type="password" style="width:1px;height:0;border:none"/>
+    <div class="modal-body" style="background-color: white">
+      <div class="form-group">
+        <label><b>User Name</b></label>
+        <div class="clearfix">
+          <input name="name_input" type="text" class="form-control" readonly ng-model="changePwdUser.username" required
+                 placeholder="You can use letters, numbers, and underscore characters '_'"
+                 ng-maxlength=100 ng-pattern="userPattern"/>
+                    <span class="text-warning"
+                          ng-if="change_pwd_form.name_input.$error.required  && change_pwd_form.name_input.$dirty"
+                    >&nbsp;The project name is required</span>
+                    <span class="text-warning"
+                          ng-if="change_pwd_form.name_input.$invalid && change_pwd_form.name_input.$dirty && !change_pwd_form.name_input.$error.required"
+                    >&nbsp;The project name is invalid</span>
+        </div>
+      </div>
+      <div class="form-group">
+        <label><b>User New Password</b></label>
+        <div class="clearfix">
+          <input required ng-pattern="pwdPattern" name="pwd_input" type="password" class="form-control" 
+                 placeholder="The password should contain at least one number, letter and special character(~!@#$%^&*(){}|:&quot;\<\>?[];\',./`)."
+                 ng-model="changePwdUser.newPassword"/>
+          <span class="text-warning"
+                          ng-if="change_pwd_form.pwd_input.$error.required  && change_pwd_form.pwd_input.$dirty"
+                    >&nbsp;The password is required</span>
+                    <span class="text-warning"
+                          ng-if="change_pwd_form.pwd_input.$invalid && change_pwd_form.pwd_input.$dirty && !change_pwd_form.pwd_input.$error.required"
+                    >&nbsp;The password is invalid</span>
+        </div>
+      </div>
+      <div class="form-group">
+        <label><b>Confirm New Password</b></label>
+        <div class="clearfix">
+          <input required name="pwd_new_input" type="password" class="form-control" 
+                 placeholder="The password should contain at least one number, letter and special character(~!@#$%^&*(){}|:&quot;\<\>?[];\',./`)." 
+                 ng-model="changePwdUser.repeatPassword"/>
+            <span class="text-warning"
+                 ng-if="user_create_form.pwd_new_input.$error.required  && user_create_form.pwd_new_input.$dirty"
+                    >&nbsp;The password is required</span>
+                    <span class="text-warning"
+                    ng-if="changePwdUser.repeatPassword && changePwdUser.newPassword !== changePwdUser.repeatPassword"
+              >&nbsp;Password and confirm password are not the same.</span>
+        </div>
+      </div>
+    </div>
+    <div class="modal-footer">
+      <button class="btn btn-primary" ng-click="cancel()">Close</button>
+      <button class="btn btn-success" ng-click="saveNewPassword()" ng-disabled="(changePwdUser.newPassword !== changePwdUser.repeatPassword) || change_pwd_form.$invalid  || dialogActionLoading">
+        Submit
+      </button>
+    </div>
+  </ng-form>
+</script>
+
diff --git a/webapp/app/partials/admin/group.html b/webapp/app/partials/admin/group.html
new file mode 100644
index 0000000..a674d4c
--- /dev/null
+++ b/webapp/app/partials/admin/group.html
@@ -0,0 +1,93 @@
+<!--
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements.  See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership.  The ASF licenses this file
+* to you under the Apache License, Version 2.0 (the
+* "License"); you may not use this file except in compliance
+* with the License.  You may obtain a copy of the License at
+*
+*     http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+-->
+<div class="container">
+  <div class="row" style="margin-top:15px;" ng-if="!allowUseUserAndGroupModule">User management does not apply to the current security configuration, go to the correct permissions management page for editing.</div>
+  <div class="row" style="margin-top:15px;" ng-if="allowUseUserAndGroupModule">
+    
+    <!--No Instances-->
+    <div ng-if="!grouploading && groupsTotal == 0">
+      <div no-result text="No user group"></div>
+    </div>
+    <!--Loading Instances-->
+    <div ng-if="grouploading">
+      <loading text="Loading user group ..."></loading>
+    </div>
+    <div class="row">
+      <div class="col-xs-9">
+        <label class="table-header-text">Group List</label>
+      </div>
+      <div class="col-xs-3">
+      <form style="float:right;" >
+          <span class="input-icon input-icon-right nav-search">
+            <input type="text" placeholder="Search by group name" class="nav-search-input" ng-model="filter.name" />
+            <i class="ace-icon fa fa-search blue" ng-click="listGroups();"></i>
+          </span>
+      </form>
+      </div>
+    </div>
+    <button class="btn btn-primary btn-sm" ng-click="createGroup()"><i class="fa fa-plus"></i> Group</button>
+    <!--Queries Table Content-->
+    <table ng-if="groupsTotal > 0" style="margin-top:20px;table-layout:fixed;" class="table table-striped table-bordered table-hover dataTable no-footer">
+      <thead>
+      <tr style="cursor: pointer">
+        <th width="7%">
+          ID
+        </th>
+        <th>
+          Group Name
+        </th>
+        <th>
+          User Count
+        </th>
+        <th width="17%">
+          Actions
+        </th>
+      </tr>
+      </thead>
+      <tbody class="odd table table-striped table-bordered table-hover dataTable no-footer">
+      <tr ng-repeat="(group, value) in groups" style="cursor: pointer">
+        <td>
+          {{$index+1 + page.limit * (page.curpage - 1)}}
+        </td>
+        <td>
+          {{group}}
+        </td>
+        <td>
+          <a ng-click="showUserListByGroup(group)">{{value.length}}</a>
+        </td>
+        <td>
+          <button type="button" tooltip="Add user to group" class="btn btn-default btn-xs" ng-if="group!=='ALL_USERS'" ng-click="assignToUser(group)">
+            <i class="fa fa-user-plus fa-fw"></i>
+          </button>
+          <button type="button" tooltip="Delete group" ng-if="group!=='ROLE_ADMIN' && group!=='ALL_USERS'" class="btn btn-default btn-xs" ng-click="delGroup(group)">
+            <i class="fa fa-trash-o  fa-fw"></i>
+          </button>
+        </td>
+      </tr>
+      </tbody>
+    </table>
+    <div class="row">
+      <div class="col-xs-12">
+          <pagination total-items="groupsTotal" boundary-link-numbers="true" rotate="false" boundary-links="true" force-ellipses="true" page="page.curpage" max-size="10" items-per-page="page.limit" on-select-page="listGroups(page)"  class="pagination-sm"></pagination>
+      </div>
+    </div>
+  </div>
+</div>
+
+<div ng-include="'partials/admin/group_create.html'"></div>
+<div ng-include="'partials/admin/user_assign.html'"></div>
diff --git a/webapp/app/partials/admin/group_assign.html b/webapp/app/partials/admin/group_assign.html
new file mode 100644
index 0000000..9fd82e8
--- /dev/null
+++ b/webapp/app/partials/admin/group_assign.html
@@ -0,0 +1,87 @@
+<!--
+* 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.
+-->
+
+<script type="text/ng-template" id="assignGroup.html">
+  <ng-form name="group_create_form">
+    <div class="modal-header">
+      <h4>New Group</h4>
+    </div>
+    <div class="modal-body" style="background-color: white">
+        <div class="container">
+          <div class="row" style="margin-top:15px;">
+        
+            <!--No Instances-->
+            <div ng-if="!loading && groupsTotal==0">
+              <div no-result text="No user group"></div>
+            </div>
+            <!--Loading Instances-->
+            <div ng-if="loading">
+              <loading text="Loading user group ..."></loading>
+            </div>
+            <div class="row">
+              <div class="col-xs-9">
+                <label class="table-header-text">Group List</label>
+              </div>
+              <div class="col-xs-3">
+              <form style="float:right;" >
+                  <span class="input-icon input-icon-right nav-search">
+                    <input type="text" placeholder="Search by group name" class="nav-search-input" ng-model="filter.name" />
+                    <i class="ace-icon fa fa-search blue" ng-click="listEditGroups();"></i>
+                  </span>
+              </form>
+              </div>
+            </div>
+            <table ng-if="groupsTotal > 0" style="margin-top:20px;table-layout:fixed;" class="table table-striped table-bordered table-hover dataTable no-footer">
+              <thead>
+              <tr style="cursor: pointer">
+                <th width="7%">
+                  <input type="checkbox" onchange="angular.element(this).scope().selectAllGroups(this)" ng-checked="isAllchecked(selectGroups,groups)" >
+                </th>
+                <th >
+                  Group Name
+                </th>
+              </tr>
+              </thead>
+              <tbody class="odd table table-striped table-bordered table-hover dataTable no-footer">
+              <tr ng-repeat="(group, val) in groups" style="cursor: pointer">
+                <td>
+                    <input type="checkbox" ng-model="selectGroups[group]" >
+                </td>
+                <td>
+                  {{group}}
+                </td>
+              </tr>
+              </tbody>
+            </table>
+            <div class="row" v-if="">
+              <div class="col-xs-12">
+                  <pagination total-items="groupsTotal" boundary-link-numbers="true" rotate="false" boundary-links="true" force-ellipses="true" page="editPage.curpage" max-size="10" items-per-page="editPage.limit" on-select-page="listEditGroups(page)"  class="pagination-sm"></pagination>
+              </div>
+            </div>
+          </div>
+        </div>
+    </div>
+    <div class="modal-footer">
+      <button class="btn btn-primary" ng-click="cancel()">Close</button>
+      <button class="btn btn-success" ng-click="saveAssignGroup()" ng-disabled="group_create_form.$invalid || dialogActionLoading">
+        Submit
+      </button>
+    </div>
+  </ng-form>
+</script>
+
diff --git a/webapp/app/partials/admin/group_create.html b/webapp/app/partials/admin/group_create.html
new file mode 100644
index 0000000..e838245
--- /dev/null
+++ b/webapp/app/partials/admin/group_create.html
@@ -0,0 +1,48 @@
+<!--
+* 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.
+-->
+
+<script type="text/ng-template" id="addGroup.html">
+  <ng-form name="group_create_form">
+    <div class="modal-header">
+      <h4>New Group</h4>
+    </div>
+    <div class="modal-body" style="background-color: white">
+      <div class="form-group">
+        <label><b>Group Name</b></label>
+        <div class="clearfix">
+          <input name="name_input" type="text" class="form-control" ng-model="group.name" required
+                 placeholder="You can use letters, numbers, and underscore characters '_'"
+                 ng-maxlength=100 ng-pattern="groupPattern"/>
+                    <span class="text-warning"
+                          ng-if="group_create_form.name_input.$error.required  && group_create_form.name_input.$dirty"
+                    >&nbsp;The group name is required</span>
+                    <span class="text-warning"
+                          ng-if="group_create_form.name_input.$invalid && group_create_form.name_input.$dirty && !group_create_form.name_input.$error.required"
+                    >&nbsp;The project name is invalid</span>
+        </div>
+      </div>
+    </div>
+    <div class="modal-footer">
+      <button class="btn btn-primary" ng-click="cancel()">Close</button>
+      <button class="btn btn-success" ng-click="saveGroup()" ng-disabled="group_create_form.$invalid || dialogActionLoading">
+        Submit
+      </button>
+    </div>
+  </ng-form>
+</script>
+
diff --git a/webapp/app/partials/admin/user.html b/webapp/app/partials/admin/user.html
new file mode 100644
index 0000000..e0c4529
--- /dev/null
+++ b/webapp/app/partials/admin/user.html
@@ -0,0 +1,108 @@
+<!--
+* Licensed to the Apache Software Foundation (ASF) under one
+* or more contributor license agreements.  See the NOTICE file
+* distributed with this work for additional information
+* regarding copyright ownership.  The ASF licenses this file
+* to you under the Apache License, Version 2.0 (the
+* "License"); you may not use this file except in compliance
+* with the License.  You may obtain a copy of the License at
+*
+*     http://www.apache.org/licenses/LICENSE-2.0
+*
+* Unless required by applicable law or agreed to in writing, software
+* distributed under the License is distributed on an "AS IS" BASIS,
+* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+* See the License for the specific language governing permissions and
+* limitations under the License.
+-->
+<div class="container">
+    <div class="row" style="margin-top:15px;" ng-if="!allowUseUserAndGroupModule">User management does not apply to the current security configuration, go to the correct permissions management page for editing.</div>
+    <div class="row" style="margin-top:15px;" ng-if="allowUseUserAndGroupModule">
+    <!--No Instances-->
+    <div ng-if="!userloading && usersTotal == 0">
+      <div no-result text="No user"></div>
+    </div>
+    <!--Loading Instances-->
+    <div ng-if="userloading">
+      <loading text="Loading user list ..."></loading>
+    </div>
+    <div class="row">
+      <div class="col-xs-9">
+        <label class="table-header-text">User List</label>
+      </div>
+      <div class="col-xs-3">
+      <form style="float:right;" >
+          <span class="input-icon input-icon-right nav-search">
+            <input type="text" placeholder="Search by user name" class="nav-search-input" ng-model="filter.name" />
+            <i class="ace-icon fa fa-search blue" ng-click="listUsers();"></i>
+          </span>
+      </form>
+      </div>
+    </div>
+    <button class="btn btn-primary btn-sm" ng-click="createUser()"><i class="fa fa-plus"></i> User</button>
+    <span class="label label-success" style="float:right;margin-top:10px;" ng-if="tabData.groupName">Group Name: {{tabData.groupName}} <i class="fa fa-close" style="cursor: pointer;" ng-click="removeGroupFilter()"></i></span>
+    <!--Queries Table Content-->
+    <table ng-if="usersTotal > 0" style="margin-top:20px;table-layout: fixed;" class="table table-striped table-bordered table-hover  table-condensed dataTable no-footer">
+      <thead>
+      <tr style="cursor: pointer">
+        <th width="7%">
+          ID
+        </th>
+        <th width="15%">
+          User Name
+        </th>
+        <th>
+          Group
+        </th>
+        <th width="8%">
+          Status
+        </th>
+        <th width="15%">
+          Actions
+        </th>
+      </tr>
+      </thead>
+      <tbody class="odd table table-striped table-bordered table-hover dataTable no-footer">
+      <tr ng-repeat="(i, user) in users" style="cursor: pointer">
+        <td>
+          {{i+1 + page.limit * (page.curpage - 1)}}
+        </td>
+        <td>
+          {{user.username}}
+        </td>
+        <th>
+          <span class="label label-primary" style="margin-left:4px;display:inline-block" ng-repeat="auth in user.authorities">{{auth.authority}}</span>
+        </th>
+        <th >
+          <span ng-if="!user.disabled" class="label label-success">Enabled</span>
+          <span ng-if="user.disabled" class="label label-warning">Disabled</span>
+        </th>
+        <td>
+          <div class="btn-group">
+            <button type="button" tooltip="Assign group" class="btn btn-default btn-xs" ng-if="user.username!='ADMIN'" ng-click="assignToGroup(user.username, user.authorities)">
+              <i class="fa fa-group fa-fw"></i>
+            </button>
+            <button type="button" tooltip="Delete user" class="btn btn-default btn-xs" ng-if="user.username!='ADMIN'" ng-click="delUser(user.username)">
+              <i class="fa fa-trash-o fa-fw"></i>
+            </button>
+            <button type="button" class="btn btn-default btn-xs dropdown-toggle" data-toggle="dropdown">Actions<span class="ace-icon fa fa-caret-down icon-on-right"></span></button>
+            <ul class="dropdown-menu" role="menu" style="right:0;left:auto;">
+              <li ng-if="user.disabled"><a ng-click="enableUser(user)">Enable</a></li>
+              <li ng-if="!user.disabled"><a ng-click="disableUser(user)">Disable</a></li>
+              <li><a ng-click="changePwd(user)">Change Password</a></li>
+            </ul>
+          </div>
+        </td>
+      </tr>
+      </tbody>
+    </table>
+    <div class="row" ng-if="users.length>0">
+        <div class="col-xs-12">
+            <pagination total-items="usersTotal" boundary-link-numbers="true" rotate="false" boundary-links="true" force-ellipses="true" page="page.curpage" max-size="10" items-per-page="page.limit" on-select-page="listUsers(page)"  class="pagination-sm"></pagination>
+        </div>
+    </div>
+  </div>
+</div>
+<div ng-include="'partials/admin/user_create.html'"></div>
+<div ng-include="'partials/admin/change_pwd.html'"></div>
+<div ng-include="'partials/admin/group_assign.html'"></div>
\ No newline at end of file
diff --git a/webapp/app/partials/admin/user_assign.html b/webapp/app/partials/admin/user_assign.html
new file mode 100644
index 0000000..70ac67d
--- /dev/null
+++ b/webapp/app/partials/admin/user_assign.html
@@ -0,0 +1,87 @@
+<!--
+* 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.
+-->
+
+<script type="text/ng-template" id="assignUser.html">
+  <ng-form name="group_create_form">
+    <div class="modal-header">
+      <h4>Select User</h4>
+    </div>
+    <div class="modal-body" style="background-color: white">
+        <div class="container">
+          <div class="row" style="margin-top:15px;">
+        
+            <!--No Instances-->
+            <div ng-if="!loading && usersTotal == 0">
+              <div no-result text="No user group"></div>
+            </div>
+            <!--Loading Instances-->
+            <div ng-if="loading">
+              <loading text="Loading user group ..."></loading>
+            </div>
+            <div class="row">
+              <div class="col-xs-9">
+                <label class="table-header-text">User List</label>
+              </div>
+              <div class="col-xs-3">
+              <form style="float:right;" >
+                  <span class="input-icon input-icon-right nav-search">
+                    <input type="text" placeholder="Search by user name" class="nav-search-input" ng-model="filter.name" />
+                    <i class="ace-icon fa fa-search blue" ng-click="listEditUsers();"></i>
+                  </span>
+              </form>
+              </div>
+            </div>
+            <table ng-if="usersTotal > 0" style="margin-top:20px;table-layout:fixed;" class="table table-striped table-bordered table-hover dataTable no-footer">
+              <thead>
+              <tr style="cursor: pointer">
+                <th width="7%">
+                  <input type="checkbox" onchange="angular.element(this).scope().selectAllUsers(this)" ng-checked="isAllchecked(selectUsers, users)" >
+                </th>
+                <th >
+                  User Name
+                </th>
+              </tr>
+              </thead>
+              <tbody class="odd table table-striped table-bordered table-hover dataTable no-footer">
+              <tr ng-repeat="user in users" style="cursor: pointer">
+                <td>
+                    <input type="checkbox" ng-model="selectUsers[user.username]" >
+                </td>
+                <td>
+                  {{user.username}}
+                </td>
+              </tr>
+              </tbody>
+            </table>
+            <div class="row" v-if="">
+              <div class="col-xs-12">
+                  <pagination total-items="usersTotal" boundary-link-numbers="true" rotate="false" boundary-links="true" force-ellipses="true" page="editPage.curpage" max-size="10" items-per-page="editPage.limit" on-select-page="listEditUsers(page)"  class="pagination-sm"></pagination>
+              </div>
+            </div>
+          </div>
+        </div>
+    </div>
+    <div class="modal-footer">
+      <button class="btn btn-primary" ng-click="cancel()">Close</button>
+      <button class="btn btn-success" ng-click="saveAssignUser()" ng-disabled="group_create_form.$invalid || dialogActionLoading">
+        Submit
+      </button>
+    </div>
+  </ng-form>
+</script>
+
diff --git a/webapp/app/partials/admin/user_create.html b/webapp/app/partials/admin/user_create.html
new file mode 100644
index 0000000..4c60060
--- /dev/null
+++ b/webapp/app/partials/admin/user_create.html
@@ -0,0 +1,73 @@
+<!--
+* 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.
+-->
+
+<script type="text/ng-template" id="addUser.html">
+  <ng-form name="user_create_form">
+    <div class="modal-header">
+      <h4>New User</h4>
+    </div>
+    <input name="username" type="text" style="display:none"/>
+    <input name="password" type="password" style="width:1px;height:0;border:none"/>
+    <div class="modal-body" style="background-color: white">
+      <div class="form-group">
+        <label><b>User Name</b></label>
+        <div class="clearfix">
+          <input name="name_input" type="text" class="form-control" ng-model="user.name" required
+                 placeholder="You can use letters, numbers, and underscore characters '_'"
+                 ng-maxlength=100 ng-pattern="userPattern"/>
+                    <span class="text-warning"
+                          ng-if="user_create_form.name_input.$error.required  && user_create_form.name_input.$dirty"
+                    >&nbsp;The project name is required</span>
+                    <span class="text-warning"
+                          ng-if="user_create_form.name_input.$invalid && user_create_form.name_input.$dirty && !user_create_form.name_input.$error.required"
+                    >&nbsp;The project name is invalid</span>
+        </div>
+      </div>
+      <div class="form-group">
+        <label><b>User Password</b></label>
+        <div class="clearfix">
+          <input ng-pattern="pwdPattern" required name="pwd_input" type="password" class="form-control" placeholder="The password should contain at least one number, letter and special character(~!@#$%^&*(){}|:&quot;\<\>?[];\',./`)." ng-model="user.password"/>
+          <span class="text-warning"
+                          ng-if="user_create_form.pwd_input.$error.required  && user_create_form.pwd_input.$dirty"
+                    >&nbsp;The password is required</span>
+                    <span class="text-warning"
+                          ng-if="user_create_form.pwd_input.$invalid && user_create_form.pwd_input.$dirty && !user_create_form.pwd_input.$error.required"
+                    >&nbsp;The password is invalid</span>
+        </div>
+      </div>
+      <div class="form-group">
+          <label><b>User Role</b></label>
+          <div class="clearfix">
+              <label>
+                  <input type="radio" name="isadmin" ng-model="user.isAdmin" ng-value="true"/> Administrator
+              </label>
+              <label>
+                  <input type="radio" name="isadmin" ng-model="user.isAdmin" ng-value="false"/> User
+              </label>
+          </div>
+        </div>
+    </div>
+    <div class="modal-footer">
+      <button class="btn btn-primary" ng-click="cancel()">Close</button>
+      <button class="btn btn-success" ng-click="saveUser()" ng-disabled="user_create_form.$invalid || dialogActionLoading">
+        Submit
+      </button>
+    </div>
+  </ng-form>
+</script>
+
diff --git a/webapp/app/partials/common/access.html b/webapp/app/partials/common/access.html
index ff54ed9..3b62e68 100644
--- a/webapp/app/partials/common/access.html
+++ b/webapp/app/partials/common/access.html
@@ -41,11 +41,12 @@
                 <td style="width: 40%" >
                     <label><b>Name&nbsp;</b> </label>
                     <input ng-model="newAccess.sid" ng-if="newAccess.principal==true" placeholder=" User NT Account..." style="width: 80%" />
+                    <input ng-model="newAccess.sid" ng-if="newAccess.principal==false" placeholder=" User NT Account..." style="width: 80%" />
 
-                    <select chosen ng-model="newAccess.sid" ng-if="newAccess.principal==false" style="width: 80%"
+                    <!-- <select chosen ng-model="newAccess.sid" ng-if="newAccess.principal==false" style="width: 80%"
                             ng-options="authority as authority for authority in authorities">
                             <option value=""></option>
-                            </select>
+                            </select> -->
                 </td>
                 <td >
                     <label><b>Permission&nbsp;</b> </label>