You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@zeppelin.apache.org by pr...@apache.org on 2016/06/07 05:18:23 UTC

incubator-zeppelin git commit: Auto-suggestion feature for notebook permissions

Repository: incubator-zeppelin
Updated Branches:
  refs/heads/master 486ed2375 -> de5f55a3c


Auto-suggestion feature for notebook permissions

### What is this PR for?
This PR will provide user-friendly way to  give notebook permissions. It will show suggestions on the basis of usernames matching the input characters.
It will  communicate with Shiro to fetch usernames from the configured realms and select usernames matching the input character and shows it to the user as suggestions.
Currently, it works with shiro.ini file, LDAP, JDBC. In future we can extend it to work with other realms too.

### What type of PR is it?
Improvement

### Todos

### What is the Jira issue?
[ZEPPELIN-933](https://issues.apache.org/jira/browse/ZEPPELIN-933)
### How should this be tested?
1. Authenticate Zeppelin with any of the realms mention above(LDAP, JDBC,ini).
2. In notebook permission section, try giving permissions to some notebook and press some initial characters . It should show matching users as suggestions and you should be able to select users from the suggestions.
3. You should be able to give usernames separated by comma and finally save it.

### Screenshots (if appropriate)
![screen shot 2016-06-03 at 5 56 01 pm](https://cloud.githubusercontent.com/assets/7026661/15778542/8a208492-29b4-11e6-8bf9-3a31ce90223a.png)

### Questions:
* Does the licenses files need update?no
* Is there breaking changes for older versions?no
* Does this needs documentation?maybe

Author: Ravi Ranjan <ra...@gmail.com>
Author: Prabhjyot Singh <pr...@gmail.com>

Closes #937 from ravicodder/auto-suggestion and squashes the following commits:

4b4c826 [Ravi Ranjan] Merge branch 'master' of https://github.com/apache/incubator-zeppelin into auto-suggestion
b4ef169 [Ravi Ranjan] use width in palce of margin-right
30bdae4 [Ravi Ranjan] Add log and make UI look better
b7333d6 [Ravi Ranjan] reduce the size of autosugesstion and add sepreator
95a420e [Ravi Ranjan] change codeing style
140af69 [Ravi Ranjan] Merge branch 'master' of https://github.com/apache/incubator-zeppelin into auto-suggestion
6627019 [Ravi Ranjan] Merge branch 'master' of https://github.com/apache/incubator-zeppelin into auto-suggestion
cdc50b3 [Ravi Ranjan]  Modified notbook.html , include alert on save, modify condition to identify the realm
80ace5f [Prabhjyot Singh] aa
ef3658c [Ravi Ranjan]  add GetUserList.java file
060f27a [Ravi Ranjan] add auto-suggest option


Project: http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/commit/de5f55a3
Tree: http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/tree/de5f55a3
Diff: http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/diff/de5f55a3

Branch: refs/heads/master
Commit: de5f55a3c6460a7941baa14fa10b8fc7c5da76e4
Parents: 486ed23
Author: Ravi Ranjan <ra...@gmail.com>
Authored: Mon Jun 6 10:47:07 2016 +0530
Committer: Prabhjyot Singh <pr...@gmail.com>
Committed: Tue Jun 7 10:48:16 2016 +0530

----------------------------------------------------------------------
 .../org/apache/zeppelin/rest/GetUserList.java   | 153 +++++++++++
 .../apache/zeppelin/rest/SecurityRestApi.java   |  62 ++++-
 .../src/app/notebook/notebook.controller.js     | 256 +++++++++++++++++--
 zeppelin-web/src/app/notebook/notebook.css      |  63 +++++
 zeppelin-web/src/app/notebook/notebook.html     |  46 +++-
 5 files changed, 552 insertions(+), 28 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/de5f55a3/zeppelin-server/src/main/java/org/apache/zeppelin/rest/GetUserList.java
----------------------------------------------------------------------
diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/GetUserList.java b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/GetUserList.java
new file mode 100644
index 0000000..c603fe9
--- /dev/null
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/GetUserList.java
@@ -0,0 +1,153 @@
+/*
+ * 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.zeppelin.rest;
+
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.shiro.realm.jdbc.JdbcRealm;
+import org.apache.shiro.realm.ldap.JndiLdapContextFactory;
+import org.apache.shiro.realm.ldap.JndiLdapRealm;
+import org.apache.shiro.realm.text.IniRealm;
+import org.apache.shiro.util.JdbcUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.naming.NamingEnumeration;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.SearchControls;
+import javax.naming.directory.SearchResult;
+import javax.naming.ldap.LdapContext;
+import javax.sql.DataSource;
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This is class which help fetching users from different realms.
+ * getUserList() function is overloaded and according to the realm passed to the function it
+ * extracts users from its respective realm
+ */
+public class GetUserList {
+
+  private static final Logger LOG = LoggerFactory.getLogger(GetUserList.class);
+
+  /**
+   * function to extract users from shiro.ini
+   */
+  public List<String> getUserList(IniRealm r) {
+    List<String> userList = new ArrayList<>();
+    Map getIniUser = r.getIni().get("users");
+    Iterator it = getIniUser.entrySet().iterator();
+    while (it.hasNext()) {
+      Map.Entry pair = (Map.Entry) it.next();
+      userList.add(pair.getKey().toString());
+    }
+    return userList;
+  }
+
+  /**
+   * function to extract users from LDAP
+   */
+  public List<String> getUserList(JndiLdapRealm r) {
+    List<String> userList = new ArrayList<>();
+    String userDnTemplate = r.getUserDnTemplate();
+    String userDn[] = userDnTemplate.split(",", 2);
+    String userDnPrefix = userDn[0].split("=")[0];
+    String userDnSuffix = userDn[1];
+    JndiLdapContextFactory CF = (JndiLdapContextFactory) r.getContextFactory();
+    try {
+      LdapContext ctx = CF.getSystemLdapContext();
+      SearchControls constraints = new SearchControls();
+      constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
+      String[] attrIDs = {userDnPrefix};
+      constraints.setReturningAttributes(attrIDs);
+      NamingEnumeration result = ctx.search(userDnSuffix, "(objectclass=*)", constraints);
+      while (result.hasMore()) {
+        Attributes attrs = ((SearchResult) result.next()).getAttributes();
+        if (attrs.get(userDnPrefix) != null) {
+          String currentUser = attrs.get(userDnPrefix).toString();
+          userList.add(currentUser.split(":")[1]);
+        }
+      }
+    } catch (Exception e) {
+      LOG.error("Error retrieving User list from Ldap Realm", e);
+    }
+    return userList;
+  }
+
+  /**
+   * function to extract users from JDBCs
+   */
+  public List<String> getUserList(JdbcRealm obj) {
+    List<String> userlist = new ArrayList<>();
+    PreparedStatement ps = null;
+    ResultSet rs = null;
+    DataSource dataSource = null;
+    String authQuery = "";
+    String retval[];
+    String tablename = "";
+    String username = "";
+    String userquery = "";
+    try {
+      dataSource = (DataSource) FieldUtils.readField(obj, "dataSource", true);
+      authQuery = (String) FieldUtils.readField(obj, "DEFAULT_AUTHENTICATION_QUERY", true);
+      LOG.info(authQuery);
+      String authQueryLowerCase = authQuery.toLowerCase();
+      retval = authQueryLowerCase.split("from", 2);
+      if (retval.length >= 2) {
+        retval = retval[1].split("with|where", 2);
+        tablename = retval[0];
+        retval = retval[1].split("where", 2);
+        if (retval.length >= 2)
+          retval = retval[1].split("=", 2);
+        else
+          retval = retval[0].split("=", 2);
+        username = retval[0];
+      }
+
+      if (username.equals("") || tablename.equals("")){
+        return userlist;
+      }
+
+      userquery = "select " + username + " from " + tablename;
+
+    } catch (IllegalAccessException e) {
+      LOG.error("Error while accessing dataSource for JDBC Realm", e);
+      return null;
+    }
+
+    try {
+      Connection con = dataSource.getConnection();
+      ps = con.prepareStatement(userquery);
+      rs = ps.executeQuery();
+      while (rs.next()) {
+        userlist.add(rs.getString(1));
+      }
+    } catch (Exception e) {
+      LOG.error("Error retrieving User list from JDBC Realm", e);
+    } finally {
+      JdbcUtils.closeResultSet(rs);
+      JdbcUtils.closeStatement(ps);
+    }
+    return userlist;
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/de5f55a3/zeppelin-server/src/main/java/org/apache/zeppelin/rest/SecurityRestApi.java
----------------------------------------------------------------------
diff --git a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/SecurityRestApi.java b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/SecurityRestApi.java
index 342cb00..e344956 100644
--- a/zeppelin-server/src/main/java/org/apache/zeppelin/rest/SecurityRestApi.java
+++ b/zeppelin-server/src/main/java/org/apache/zeppelin/rest/SecurityRestApi.java
@@ -17,6 +17,13 @@
 
 package org.apache.zeppelin.rest;
 
+
+import org.apache.shiro.realm.Realm;
+import org.apache.shiro.realm.jdbc.JdbcRealm;
+import org.apache.shiro.realm.ldap.JndiLdapRealm;
+import org.apache.shiro.realm.text.IniRealm;
+import org.apache.shiro.util.ThreadContext;
+import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
 import org.apache.zeppelin.annotation.ZeppelinApi;
 import org.apache.zeppelin.conf.ZeppelinConfiguration;
 import org.apache.zeppelin.server.JsonResponse;
@@ -27,11 +34,10 @@ import org.slf4j.LoggerFactory;
 
 import javax.ws.rs.GET;
 import javax.ws.rs.Path;
+import javax.ws.rs.PathParam;
 import javax.ws.rs.Produces;
 import javax.ws.rs.core.Response;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Map;
+import java.util.*;
 
 /**
  * Zeppelin security rest api endpoint.
@@ -81,4 +87,54 @@ public class SecurityRestApi {
     LOG.warn(response.toString());
     return response.build();
   }
+
+  /**
+   * Get userlist
+   * Returns list of all user from available realms
+   *
+   * @return 200 response
+   */
+  @GET
+  @Path("userlist/{searchText}")
+  public Response getUserList(@PathParam("searchText") String searchText) {
+
+    List<String> usersList = new ArrayList<>();
+    try {
+      GetUserList getUserListObj = new GetUserList();
+      DefaultWebSecurityManager defaultWebSecurityManager;
+      String key = ThreadContext.SECURITY_MANAGER_KEY;
+      defaultWebSecurityManager = (DefaultWebSecurityManager) ThreadContext.get(key);
+      Collection<Realm> realms = defaultWebSecurityManager.getRealms();
+      List realmsList = new ArrayList(realms);
+      for (int i = 0; i < realmsList.size(); i++) {
+        String name = ((Realm) realmsList.get(i)).getName();
+        if (name.equals("iniRealm")) {
+          usersList.addAll(getUserListObj.getUserList((IniRealm) realmsList.get(i)));
+        } else if (name.equals("ldapRealm")) {
+          usersList.addAll(getUserListObj.getUserList((JndiLdapRealm) realmsList.get(i)));
+        } else if (name.equals("jdbcRealm")) {
+          usersList.addAll(getUserListObj.getUserList((JdbcRealm) realmsList.get(i)));
+        }
+      }
+
+    } catch (Exception e) {
+      LOG.error("Exception in retrieving Users from realms ", e);
+    }
+    List<String> autoSuggestList = new ArrayList<>();
+    Collections.sort(usersList);
+    int maxLength = 0;
+    for (int i = 0; i < usersList.size(); i++) {
+      String userLowerCase = usersList.get(i).toLowerCase();
+      String searchTextLowerCase = searchText.toLowerCase();
+      if (userLowerCase.indexOf(searchTextLowerCase) != -1) {
+        maxLength++;
+        autoSuggestList.add(usersList.get(i));
+      }
+      if (maxLength == 5) {
+        break;
+      }
+    }
+    return new JsonResponse<>(Response.Status.OK, "", autoSuggestList).build();
+  }
+
 }

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/de5f55a3/zeppelin-web/src/app/notebook/notebook.controller.js
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/notebook/notebook.controller.js b/zeppelin-web/src/app/notebook/notebook.controller.js
index 4149311..32c659f 100644
--- a/zeppelin-web/src/app/notebook/notebook.controller.js
+++ b/zeppelin-web/src/app/notebook/notebook.controller.js
@@ -43,6 +43,18 @@ angular.module('zeppelinWebApp').controller('NotebookCtrl',
 
   var connectedOnce = false;
 
+  // user auto complete related
+  $scope.suggestions = [];
+  $scope.selectIndex = -1;
+  var selectedUser = '';
+  var selectedUserIndex = 0;
+  var previousSelectedList = [];
+  var previousSelectedListOwners = [];
+  var previousSelectedListReaders = [];
+  var previousSelectedListWriters = [];
+  var searchText = [];
+  $scope.role = '';
+
   $scope.$on('setConnectedStatus', function(event, param) {
     if(connectedOnce && param){
       initNotebook();
@@ -683,36 +695,57 @@ angular.module('zeppelinWebApp').controller('NotebookCtrl',
     }
   };
 
-  $scope.savePermissions = function() {
-    $http.put(baseUrlSrv.getRestApiBase() + '/notebook/' +$scope.note.id + '/permissions',
+  function convertPermissionsToArray() {
+    if (!angular.isArray($scope.permissions.owners)) {
+      $scope.permissions.owners = $scope.permissions.owners.split(',');
+    }
+    if (!angular.isArray($scope.permissions.readers)) {
+      $scope.permissions.readers = $scope.permissions.readers.split(',');
+    }
+    if (!angular.isArray($scope.permissions.writers)) {
+      $scope.permissions.writers = $scope.permissions.writers.split(',');
+    }
+  }
+
+  $scope.savePermissions = function () {
+    convertPermissionsToArray();
+    $http.put(baseUrlSrv.getRestApiBase() + '/notebook/' + $scope.note.id + '/permissions',
       $scope.permissions, {withCredentials: true}).
-    success(function(data, status, headers, config) {
-      console.log('Note permissions %o saved', $scope.permissions);
-      $scope.showPermissions = false;
-    }).
-    error(function(data, status, headers, config) {
-      console.log('Error %o %o', status, data.message);
-      BootstrapDialog.show({
+      success(function (data, status, headers, config) {
+        console.log('Note permissions %o saved', $scope.permissions);
+        BootstrapDialog.alert({
           closable: true,
-          title: 'Insufficient privileges', 
+          title: 'Permissions Saved Successfully!!!',
+          message: 'Owners : ' + $scope.permissions.owners + '\n\n' + 'Readers : ' + $scope.permissions.readers + '\n\n' + 'Writers  : ' + $scope.permissions.writers
+        });
+        $scope.showPermissions = false;
+      }).
+      error(function (data, status, headers, config) {
+        console.log('Error %o %o', status, data.message);
+        BootstrapDialog.show({
+          closable: true,
+          title: 'Insufficient privileges',
           message: data.message,
-          buttons: [{
+          buttons: [
+            {
               label: 'Login',
-              action: function(dialog) {
-                  dialog.close();
-                  angular.element('#loginModal').modal({
-                     show: 'true'
-                    });
+              action: function (dialog) {
+                dialog.close();
+                angular.element('#loginModal').modal({
+                  show: 'true'
+                });
               }
-          }, {
+            },
+            {
               label: 'Cancel',
-              action: function(dialog){
-                  dialog.close();
+              action: function (dialog) {
+                dialog.close();
               }
-          }]
+            }
+          ]
+        });
       });
-    });
-  };
+    };
 
   $scope.togglePermissions = function() {
     if ($scope.showPermissions) {
@@ -739,4 +772,183 @@ angular.module('zeppelinWebApp').controller('NotebookCtrl',
     }
   };
 
+  function checkPreviousRole(role) {
+    var i = 0;
+    if (role !== $scope.role) {
+      if ($scope.role === 'owners') {
+        previousSelectedListOwners = [];
+        for (i = 0; i < previousSelectedList.length; i++) {
+          previousSelectedListOwners[i] = previousSelectedList[i];
+        }
+      }
+      if ($scope.role === 'readers') {
+        previousSelectedListReaders = [];
+        for (i = 0; i < previousSelectedList.length; i++) {
+          previousSelectedListReaders[i] = previousSelectedList[i];
+        }
+      }
+      if ($scope.role === 'writers') {
+        previousSelectedListWriters = [];
+        for (i = 0; i < previousSelectedList.length; i++) {
+          previousSelectedListWriters[i] = previousSelectedList[i];
+        }
+      }
+
+      $scope.role = role;
+      previousSelectedList = [];
+      if (role === 'owners') {
+        for (i = 0; i < previousSelectedListOwners.length; i++) {
+          previousSelectedList[i] = previousSelectedListOwners[i];
+        }
+      }
+      if (role === 'readers') {
+        for (i = 0; i < previousSelectedListReaders.length; i++) {
+          previousSelectedList[i] = previousSelectedListReaders[i];
+        }
+      }
+      if (role === 'writers') {
+        for (i = 0; i < previousSelectedListWriters.length; i++) {
+          previousSelectedList[i] = previousSelectedListWriters[i];
+        }
+      }
+    }
+  }
+
+
+  function convertToArray(role) {
+    if (role === 'owners') {
+      searchText = $scope.permissions.owners.split(',');
+    }
+    else if (role === 'readers') {
+      searchText = $scope.permissions.readers.split(',');
+    }
+    else if (role === 'writers') {
+      searchText = $scope.permissions.writers.split(',');
+    }
+    for (var i = 0; i < searchText.length; i++) {
+      searchText[i] = searchText[i].trim();
+    }
+  }
+
+
+  function convertToString(role) {
+    if (role === 'owners') {
+      $scope.permissions.owners = searchText.join();
+    }
+    else if (role === 'readers') {
+      $scope.permissions.readers = searchText.join();
+    }
+    else if (role === 'writers') {
+      $scope.permissions.writers = searchText.join();
+    }
+  }
+
+  function getSuggestions (searchQuery) {
+    $scope.suggestions =[];
+    $http.get(baseUrlSrv.getRestApiBase() + '/security/userlist/' + searchQuery ).then(function
+    (response) {
+      var userlist = angular.fromJson(response.data).body;
+      for (var k in userlist) {
+        $scope.suggestions.push(userlist[k]);
+      }
+    });
+  }
+
+  function updatePreviousList() {
+    for (var i = 0; i < searchText.length; i++) {
+      previousSelectedList[i] = searchText[i];
+    }
+  }
+
+
+  var getChangedIndex = function() {
+    if (previousSelectedList.length === 0) {
+      selectedUserIndex = searchText.length - 1;
+    }
+    else {
+      for (var i = 0; i < searchText.length; i++) {
+        if (previousSelectedList[i] !== searchText[i]) {
+          selectedUserIndex = i;
+          previousSelectedList = [];
+          break;
+        }
+      }
+    }
+    updatePreviousList();
+  };
+
+  // function to find suggestion list on change
+  $scope.search = function(role) {
+    convertToArray(role);
+    checkPreviousRole(role);
+    getChangedIndex();
+    $scope.selectIndex = -1;
+    $scope.suggestions = [];
+    selectedUser = searchText[selectedUserIndex];
+    if(selectedUser !== ''){
+    getSuggestions(selectedUser);
+    }
+    else
+    {
+     $scope.suggestions = [];
+    }
+  };
+
+
+  var checkIfSelected = function() {
+    if (($scope.suggestions.length === 0) && ($scope.selectIndex < 0 || $scope.selectIndex >= $scope.suggestions.length) || ( $scope.suggestions.length !== 0 && ( $scope.selectIndex < 0 || $scope.selectIndex >= $scope.suggestions.length   ))) {
+      searchText[selectedUserIndex] = selectedUser;
+      $scope.suggestions = [];
+      return true;
+    }
+    else {
+      return false;
+    }
+  };
+
+
+  $scope.checkKeyDown = function(event, role) {
+    if (event.keyCode === 40) {
+      event.preventDefault();
+      if ($scope.selectIndex + 1 !== $scope.suggestions.length) {
+        $scope.selectIndex++;
+      }
+    }
+    else if (event.keyCode === 38) {
+      event.preventDefault();
+
+      if ($scope.selectIndex - 1 !== -1) {
+        $scope.selectIndex--;
+
+      }
+    }
+    else if (event.keyCode === 13) {
+      event.preventDefault();
+      if (!checkIfSelected()) {
+        selectedUser = $scope.suggestions[$scope.selectIndex];
+        searchText[selectedUserIndex] = $scope.suggestions[$scope.selectIndex];
+        updatePreviousList();
+        convertToString(role);
+        $scope.suggestions = [];
+      }
+    }
+  };
+
+  $scope.checkKeyUp = function(event) {
+    if (event.keyCode !== 8 || event.keyCode !== 46) {
+      if (searchText[selectedUserIndex] === '') {
+        $scope.suggestions = [];
+      }
+    }
+  };
+
+
+  $scope.assignValueAndHide = function(index, role) {
+    searchText[selectedUserIndex] = $scope.suggestions[index];
+    updatePreviousList();
+    convertToString(role);
+    $scope.suggestions = [];
+  };
+
+
 });

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/de5f55a3/zeppelin-web/src/app/notebook/notebook.css
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/notebook/notebook.css b/zeppelin-web/src/app/notebook/notebook.css
index 9169e80..6cdc407 100644
--- a/zeppelin-web/src/app/notebook/notebook.css
+++ b/zeppelin-web/src/app/notebook/notebook.css
@@ -154,6 +154,20 @@
   border: 1px solid #E5E5E5;
 }
 
+.permissions .owners {
+    width:60px;
+    display: inline-block;
+}
+
+.permissions .readers {
+    width:60px;
+    display: inline-block;
+}
+
+.permissions .writers {
+    width:60px;
+    display: inline-block;
+}
 /*
   Note Setting Panel
 */
@@ -222,3 +236,52 @@
   text-decoration: none;
   cursor: default;
 }
+
+.userlist {
+  width: 230px;
+  font-family: Georgia, Times, serif;
+  font-size: 15px;
+  position: absolute;
+  z-index: 9999;
+}
+
+.userlist ul {
+  list-style: none;
+}
+
+.userlist ul li {
+  box-shadow: 3px 3px 5px #888888;
+  display: list-item;
+  text-decoration: none;
+  color: #000000;
+  background-color: #FFFFFF;
+  line-height: 30px;
+  border-bottom-style: none;
+  border-bottom-width: 1px;
+  border-bottom:1px  #CCCCCC solid;
+  padding-left: 10px;
+  cursor: pointer;
+}
+
+.userlist ul li:first-child {
+  border-top-right-radius: 5px;
+  border-top-left-radius: 5px;
+}
+
+.userlist ul li:last-child {
+  border-bottom-right-radius: 5px;
+  border-bottom-left-radius: 5px;
+}
+
+.userlist ul li strong {
+  margin-right: 10px;
+}
+
+.userlist li:hover {
+  background-color: #E0E0E0;
+}
+
+.userlist li:active,
+.userlist li.active {
+  background-color: #428BCA;
+}

http://git-wip-us.apache.org/repos/asf/incubator-zeppelin/blob/de5f55a3/zeppelin-web/src/app/notebook/notebook.html
----------------------------------------------------------------------
diff --git a/zeppelin-web/src/app/notebook/notebook.html b/zeppelin-web/src/app/notebook/notebook.html
index d4b72bc..de8cbdf 100644
--- a/zeppelin-web/src/app/notebook/notebook.html
+++ b/zeppelin-web/src/app/notebook/notebook.html
@@ -70,9 +70,49 @@ limitations under the License.
       </p>
       <div class="permissionsForm"
            data-ng-model="permissions">
-        <p>Owners : <input ng-list ng-model="permissions.owners" placeholder="*"> Owners can change permissions, read and write the note. </p>
-        <p>Readers : <input ng-list ng-model="permissions.readers" placeholder="*"> Readers can only read the note.</p>
-        <p>Writers : <input ng-list ng-model="permissions.writers" placeholder="*"> Writers can read and write the note.</p>
+        <p><span  class="owners">Owners </span><input   ng-model="permissions.owners"
+                           placeholder="search for users"
+                           class="input"  ng-change="search('owners')"
+                           ng-keydown="checkKeyDown($event,'owners')"
+                           ng-keyup="checkKeyUp($event)"> Owners can change permissions,read
+          and write the note.</p>
+        <div ng-if="role === 'owners'" class="userlist" >
+          <ul>
+            <li ng-repeat="suggestion in suggestions"
+                ng-class="{active : selectIndex === $index  }"
+                ng-click="assignValueAndHide($index,'owners')" >
+              {{suggestion}}
+            </li>
+          </ul>
+        </div>
+        <p><span  class="readers">Readers </span><input   ng-model="permissions.readers"
+                             placeholder="search for users"
+                             class="input"  ng-change="search('readers')"
+                             ng-keydown="checkKeyDown($event,'readers')"
+                             ng-keyup="checkKeyUp($event)"> Readers can only read the note.</p>
+        <div ng-if="role === 'readers'" class="userlist">
+          <ul>
+            <li ng-repeat="suggestion in suggestions"
+                ng-class="{active : selectIndex === $index  }"
+                ng-click="assignValueAndHide($index,'readers')" >
+              {{suggestion}}
+            </li>
+          </ul>
+        </div>
+        <p><span  class="writers">Writers </span><input   ng-model="permissions.writers"
+                              placeholder="search for users"
+                              class="input"  ng-change="search('writers')"
+                              ng-keydown="checkKeyDown($event,'writers')"
+                              ng-keyup="checkKeyUp($event)"> Writers can read and write the note.</p>
+        <div ng-if="role === 'writers'" class="userlist">
+          <ul>
+            <li ng-repeat="suggestion in suggestions"
+                ng-class="{active : selectIndex === $index  }"
+                ng-click="assignValueAndHide($index,'writers')">
+              {{suggestion}}
+            </li>
+          </ul>
+        </div>
       </div>
     </div>
     <br />