소스 검색

[sentry] Skeleton pages for new Hive and Solr pages

Romain Rigaux 10 년 전
부모
커밋
e2e9d7c

+ 423 - 0
apps/security/src/security/api/hive2.py

@@ -0,0 +1,423 @@
+#!/usr/bin/env python
+# Licensed to Cloudera, Inc. under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  Cloudera, Inc. licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import json
+import logging
+import time
+
+from django.utils.translation import ugettext as _
+
+from desktop.lib.django_util import JsonResponse
+from libsentry.api import get_api
+from libsentry.sentry_site import get_sentry_server_admin_groups
+from hadoop.cluster import get_defaultfs
+
+from beeswax.api import autocomplete
+
+
+LOG = logging.getLogger(__name__)
+
+
+def fetch_hive_path(request):
+  path = request.GET['path']
+
+  database = None
+  table = None
+  if path:
+    database = path
+  if '/' in path:
+    database, table = path.split('/')
+
+  resp = autocomplete(request, database, table)
+
+  if database and request.GET['doas'] != request.user.username:
+    request.GET = request.GET.copy()
+    request.GET['doas'] = request.GET['doas']
+
+    resp = autocomplete(request, database, table)
+
+  return resp
+
+
+def list_sentry_roles_by_group(request):
+  result = {'status': -1, 'message': 'Error'}
+
+  try:
+    if request.POST['groupName']:
+      groupName = request.POST['groupName']
+    else:
+      # Admins can see everything, other only the groups they belong too
+      groupName = None if request.user.groups.filter(name__in=get_sentry_server_admin_groups()).exists() else '*'
+    roles = get_api(request.user).list_sentry_roles_by_group(groupName)
+    result['roles'] = sorted(roles, key=lambda role: role['name'])
+    result['message'] = ''
+    result['status'] = 0
+  except Exception, e:
+    LOG.exception("could not retrieve roles")
+
+    if "couldn't be retrieved." in str(e):
+      result['roles'] = []
+      result['status'] = 0
+    else:
+      result['message'] = unicode(str(e), "utf8")
+
+  return JsonResponse(result)
+
+
+def list_sentry_privileges_by_role(request):
+  result = {'status': -1, 'message': 'Error'}
+
+  try:
+    roleName = request.POST['roleName']
+    sentry_privileges = get_api(request.user).list_sentry_privileges_by_role(roleName)
+    result['sentry_privileges'] = sorted(sentry_privileges, key=lambda privilege: '%s.%s.%s.%s' % (privilege['server'], privilege['database'], privilege['table'], privilege['URI']))
+    result['message'] = ''
+    result['status'] = 0
+  except Exception, e:
+    LOG.exception("could not list sentry privileges")
+
+    result['message'] = unicode(str(e), "utf8")
+
+  return JsonResponse(result)
+
+
+def _to_sentry_privilege(privilege):
+  return {
+      'privilegeScope': privilege['privilegeScope'],
+      'serverName': privilege['serverName'],
+      'dbName': privilege['dbName'],
+      'tableName': privilege['tableName'],
+      'columnName': privilege['columnName'],
+      'URI': _massage_uri(privilege['URI']),
+      'action': privilege['action'],
+      'createTime': privilege['timestamp'],
+      'grantOption': 1 if privilege['grantOption'] else 0,
+  }
+
+
+def _hive_add_privileges(user, role, privileges):
+    api = get_api(user)
+
+    _privileges = []
+
+    for privilege in privileges:
+      if privilege['status'] not in ('deleted',):
+        api.alter_sentry_role_grant_privilege(role['name'], _to_sentry_privilege(privilege))
+        # Mocked until Sentry API returns the info. Not used currently as we refresh the whole role.
+        _privileges.append({
+            'timestamp': int(time.time()),
+            'database': privilege.get('dbName'),
+            'action': privilege.get('action'),
+            'scope': privilege.get('privilegeScope'),
+            'table': privilege.get('tableName'),
+            'column': privilege.get('columnName'),
+            'URI': privilege.get('URI'),
+            'server': privilege.get('serverName'),
+            'grantOption': privilege.get('grantOption') == 1
+        })
+
+    return _privileges
+
+
+def _massage_uri(uri):
+  if uri:
+    if uri.startswith('hdfs:///'):
+      uri = uri.replace('hdfs://', get_defaultfs())
+    elif uri.startswith('/'):
+      uri = get_defaultfs() + uri
+
+  return uri
+
+
+def _get_splitted_path(path):
+  parts = path.split('.')
+  db, table, column = '', '', ''
+
+  if len(parts) >= 1:
+    db = parts[0]
+  if len(parts) >= 2:
+    table = parts[1]
+  if len(parts) >= 3:
+    column = parts[2]
+
+  return db, table, column
+
+
+def _drop_sentry_privilege(user, role, authorizable):
+  return get_api(user).alter_sentry_role_revoke_privilege(role['name'], _to_sentry_privilege(authorizable))
+
+
+def create_role(request):
+  result = {'status': -1, 'message': 'Error'}
+
+  try:
+    role = json.loads(request.POST['role'])
+
+    api = get_api(request.user)
+
+    api.create_sentry_role(role['name'])
+
+    privileges = [privilege for privilege in role['privileges'] if privilege['status'] not in ('deleted', 'alreadydeleted')]
+    result['privileges'] = _hive_add_privileges(request.user, role, privileges)
+    api.alter_sentry_role_add_groups(role['name'], role['groups'])
+
+    result['role'] = {"name": role['name'], "groups": role['groups']}
+
+    result['message'] = _('Role created!')
+    result['status'] = 0
+  except Exception, e:
+    LOG.exception("could not create role")
+
+    result['message'] = unicode(str(e), "utf8")
+
+  return JsonResponse(result)
+
+
+def update_role_groups(request):
+  result = {'status': -1, 'message': 'Error'}
+
+  try:
+    role = json.loads(request.POST['role'])
+
+    new_groups = set(role['groups']) - set(role['originalGroups'])
+    deleted_groups = set(role['originalGroups']) - set(role['groups'])
+
+    api = get_api(request.user)
+
+    if new_groups:
+      api.alter_sentry_role_add_groups(role['name'], new_groups)
+    if deleted_groups:
+      api.alter_sentry_role_delete_groups(role['name'], deleted_groups)
+
+    result['message'] = ''
+    result['status'] = 0
+  except Exception, e:
+    LOG.exception("could not update role groups")
+
+    result['message'] = unicode(str(e), "utf8")
+
+  return JsonResponse(result)
+
+
+def save_privileges(request):
+  result = {'status': -1, 'message': 'Error'}
+
+  try:
+    role = json.loads(request.POST['role'])
+
+    new_privileges = [privilege for privilege in role['privilegesChanged'] if privilege['status'] == 'new']
+    result['privileges'] = _hive_add_privileges(request.user, role, new_privileges)
+
+    deleted_privileges = [privilege for privilege in role['privilegesChanged'] if privilege['status'] == 'deleted']
+    for privilege in deleted_privileges:
+      _drop_sentry_privilege(request.user, role, privilege)
+
+    modified_privileges = [privilege for privilege in role['privilegesChanged'] if privilege['status'] == 'modified']
+    old_privileges_ids = [privilege['id'] for privilege in modified_privileges]
+    _hive_add_privileges(request.user, role, modified_privileges)
+    for privilege in role['originalPrivileges']:
+      if privilege['id'] in old_privileges_ids:
+        _drop_sentry_privilege(request.user, role, privilege)
+
+    result['message'] = _('Privileges updated')
+    result['status'] = 0
+  except Exception, e:
+    LOG.exception("could not save privileges")
+
+    result['message'] = unicode(str(e), "utf8")
+
+  return JsonResponse(result)
+
+
+def grant_privilege(request):
+  result = {'status': -1, 'message': 'Error'}
+
+  try:
+    roleName = json.loads(request.POST['roleName'])
+    privilege = json.loads(request.POST['privilege'])
+
+    result['privileges'] = _hive_add_privileges(request.user, {'name': roleName}, [privilege])
+
+    result['message'] = _('Privilege granted successfully to %s.') % roleName
+    result['status'] = 0
+  except Exception, e:
+    LOG.exception("could not grant privileges")
+
+    result['message'] = unicode(str(e), "utf8")
+
+  return JsonResponse(result)
+
+
+def create_sentry_role(request):
+  result = {'status': -1, 'message': 'Error'}
+
+  try:
+    roleName = request.POST['roleName']
+
+    get_api(request.user).create_sentry_role(roleName)
+    result['message'] = _('Role and privileges created.')
+    result['status'] = 0
+  except Exception, e:
+    LOG.exception("could not create role")
+
+    result['message'] = unicode(str(e), "utf8")
+
+  return JsonResponse(result)
+
+
+def drop_sentry_role(request):
+  result = {'status': -1, 'message': 'Error'}
+
+  try:
+    roleName = request.POST['roleName']
+
+    get_api(request.user).drop_sentry_role(roleName)
+    result['message'] = _('Role and privileges deleted.')
+    result['status'] = 0
+  except Exception, e:
+    LOG.exception("could not drop role")
+
+    result['message'] = unicode(str(e), "utf8")
+
+  return JsonResponse(result)
+
+
+def list_sentry_privileges_by_authorizable(request):
+  result = {'status': -1, 'message': 'Error'}
+
+  try:
+    groups = [request.POST['groupName']] if request.POST['groupName'] else None
+    authorizableSet = [json.loads(request.POST['authorizableHierarchy'])]
+
+    _privileges = []
+
+    for authorizable, roles in get_api(request.user).list_sentry_privileges_by_authorizable(authorizableSet=authorizableSet, groups=groups):
+      for role, privileges in roles.iteritems():
+        for privilege in privileges:
+          privilege['roleName'] = role
+          _privileges.append(privilege)
+
+    result['privileges'] = sorted(_privileges, key=lambda privilege: privilege['roleName'])
+
+    result['message'] = ''
+    result['status'] = 0
+  except Exception, e:
+    LOG.exception("could not list privileges by authorizable")
+
+    result['message'] = unicode(str(e), "utf8")
+
+  return JsonResponse(result)
+
+
+def bulk_delete_privileges(request):
+  result = {'status': -1, 'message': 'Error'}
+
+  try:
+    checkedPaths = json.loads(request.POST['checkedPaths'])
+    authorizableHierarchy = json.loads(request.POST['authorizableHierarchy'])
+
+    for path in [path['path'] for path in checkedPaths]:
+      db, table, column = _get_splitted_path(path)
+      authorizableHierarchy.update({
+        'db': db,
+        'table': table,
+        'column': column,
+      })
+      get_api(request.user).drop_sentry_privileges(authorizableHierarchy)
+    result['message'] = _('Privileges deleted.')
+    result['status'] = 0
+  except Exception, e:
+    LOG.exception("could not bulk delete privileges")
+
+    result['message'] = unicode(str(e), "utf8")
+
+  return JsonResponse(result)
+
+
+def bulk_add_privileges(request):
+  result = {'status': -1, 'message': 'Error'}
+
+  try:
+    privileges = json.loads(request.POST['privileges'])
+    checkedPaths = json.loads(request.POST['checkedPaths'])
+    authorizableHierarchy = json.loads(request.POST['authorizableHierarchy'])
+
+    privileges = [privilege for privilege in privileges if privilege['status'] == '']
+
+    for path in [path['path'] for path in checkedPaths]:
+      db, table, column = _get_splitted_path(path)
+
+      privilegeScope = 'COLUMN' if column else 'TABLE' if table else 'DATABASE' if db else 'SERVER'
+      authorizableHierarchy.update({
+        'db': db,
+        'table': table,
+        'column': column,
+      })
+
+      for privilege in privileges:
+        privilege['dbName'] = db
+        privilege['tableName'] = table
+        privilege['columnName'] = column
+        privilege['privilegeScope'] = privilegeScope
+        _hive_add_privileges(request.user, {'name': privilege['roleName']}, [privilege])
+
+    result['message'] = _('Privileges added.')
+    result['status'] = 0
+  except Exception, e:
+    LOG.exception("could not bulk add privileges")
+
+    result['message'] = unicode(str(e), "utf8")
+
+  return JsonResponse(result)
+
+
+def rename_sentry_privilege(request):
+  result = {'status': -1, 'message': 'Error'}
+
+  try:
+    oldAuthorizable = json.loads(request.POST['oldAuthorizable'])
+    newAuthorizable = json.loads(request.POST['newAuthorizable'])
+
+    get_api(request.user).rename_sentry_privilege(oldAuthorizable, newAuthorizable)
+    result['message'] = _('Privilege deleted.')
+    result['status'] = 0
+  except Exception, e:
+    LOG.exception("could not rename privilege")
+
+    result['message'] = unicode(str(e), "utf8")
+
+  return JsonResponse(result)
+
+
+def list_sentry_privileges_for_provider(request):
+  result = {'status': -1, 'message': 'Error'}
+
+  try:
+    groups = json.loads(request.POST['groups'])
+    roleSet = json.loads(request.POST['roleSet'])
+    authorizableHierarchy = json.loads(request.POST['authorizableHierarchy'])
+
+    sentry_privileges = get_api(request.user).list_sentry_privileges_for_provider(groups=groups, roleSet=roleSet, authorizableHierarchy=authorizableHierarchy)
+    result['sentry_privileges'] = sentry_privileges
+    result['message'] = ''
+    result['status'] = 0
+  except Exception, e:
+    LOG.exception("could not list privileges for provider")
+
+    result['message'] = unicode(str(e), "utf8")
+
+  return JsonResponse(result)

+ 779 - 0
apps/security/src/security/templates/hive2.mako

@@ -0,0 +1,779 @@
+## Licensed to Cloudera, Inc. under one
+## or more contributor license agreements.  See the NOTICE file
+## distributed with this work for additional information
+## regarding copyright ownership.  Cloudera, Inc. 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.
+
+<%!
+from desktop.views import commonheader, commonfooter, _ko
+from django.utils.translation import ugettext as _
+%>
+
+<%namespace name="actionbar" file="actionbar.mako" />
+<%namespace name="layout" file="layout.mako" />
+<%namespace name="tree" file="common_tree.mako" />
+
+${ commonheader(_('Hadoop Security'), "security", user) | n,unicode }
+${ layout.menubar(section='hive2') }
+
+
+<script type="text/html" id="role">
+  <div class="acl-block-title">
+    <i class="fa fa-cube muted"></i> <a class="pointer" data-bind="click: function(){  $root.showRole($data); }"><span data-bind="text: name"></span></a>
+  </div>
+  <div data-bind="template: { name: 'privilege', foreach: privilegesForView }"></div>
+  <!-- ko ifnot: $root.isApplyingBulk() -->
+  <div class="acl-block acl-actions">
+    <span class="pointer" data-bind="visible: privilegesForViewTo() < privileges().length, click: function(){ privilegesForViewTo(privilegesForViewTo() + 50) }" title="${ _('Show 50 more...') }"><i class="fa fa-ellipsis-h"></i></span>
+    <span class="pointer" data-bind="click: addPrivilege, visible: $root.is_sentry_admin" title="${ _('Add privilege') }"><i class="fa fa-plus"></i></span>
+    <span class="pointer" data-bind="click: function() { $root.list_sentry_privileges_by_authorizable() }, visible: privilegesChanged().length > 0" title="${ _('Undo') }"> &nbsp; <i class="fa fa-undo"></i></span>
+    <span class="pointer" data-bind="click: function() { deletePrivilegeModal($data) }, visible: privilegesChanged().length > 0" title="${ _('Save') }"> &nbsp; <i class="fa fa-save"></i></span>
+  </div>
+  <!-- /ko -->
+</script>
+
+
+<script type="text/html" id="privilege">
+<div data-bind="visible: status() != 'deleted' && status() != 'alreadydeleted'" class="acl-block acl-block-airy">
+
+  <!-- ko if: editing() -->
+    <div class="pull-right privilege-actions" data-bind="visible: grantOption() || $root.is_sentry_admin">
+      <a title="${ _('Grant this privilege') }" class="pointer" style="margin-right: 4px" data-bind="click: function(){ $root.grantToPrivilege($data); $('#grantPrivilegeModal').modal('show'); }"><i class="fa fa-send"></i></a>
+      <a class="pointer" style="margin-right: 4px" data-bind="click: function() { if (editing()) { editing(false); }}"><i class="fa fa-eye"></i></a>
+      <a class="pointer" style="margin-right: 4px" data-bind="click: remove"><i class="fa fa-times"></i></a>
+    </div>
+
+    <div class="inline-block" style="vertical-align: middle">
+      <a class="pointer" style="padding-top: 4px" data-bind="click: function(){ privilegeType('db'); action($root.availableActions()[0]) }">
+        <i class="fa fa-fw fa-1halfx muted" data-bind="css: {'fa-circle-o': privilegeType() != 'db' , 'fa-check-circle-o': privilegeType() == 'db'}"></i>
+      </a>
+    </div>
+    <input type="text" data-bind="hivechooser: $data.path, enable: privilegeType() == 'db'" placeholder="dbName.tableName <CTRL+SPACE>">
+
+    <div class="inline-block" style="vertical-align: middle">
+      <a class="pointer" style="padding-top: 4px" data-bind="click: function(){ privilegeType('uri'); action('ALL'); }">
+        <i class="fa fa-fw fa-1halfx muted" data-bind="css: {'fa-circle-o': privilegeType() != 'uri' , 'fa-check-circle-o': privilegeType() == 'uri'}"></i>
+      </a>
+    </div>
+    <input type="text" data-bind="filechooser: $data.URI, enable: privilegeType() == 'uri'" placeholder="URI">
+
+    <select data-bind="options: $root.availableActions, value: $data.action, enable: (privilegeType() == 'db')" style="width: 100px; margin-bottom: 0"></select>
+
+    <div class="new-line-if-small">
+      <label class="checkbox"><input type="checkbox" data-bind="checked: grantOption"> ${ _('With grant') }</label>
+      <a class="pointer showAdvanced" data-bind="click: function(){ showAdvanced(true); }, visible: ! showAdvanced()"><i class="fa fa-cog"></i> ${ _('Show advanced') }</a>
+      <a class="pointer showAdvanced" data-bind="click: function(){ showAdvanced(false); }, visible: showAdvanced()"><i class="fa fa-cog"></i> ${ _('Hide advanced') }</a>
+      <div class="clearfix"></div>
+    </div>
+
+    <div class="acl-block-section" data-bind="visible: showAdvanced" style="margin-top: 0">
+      ${ _('Server') } <input type="text" data-bind="value: serverName" placeholder="serverName" style="margin-left: 6px">
+    </div>
+  <!-- /ko -->
+
+  <!-- ko ifnot: editing() -->
+    <!-- ko ifnot: $root.isApplyingBulk() -->
+    <div class="pull-right privilege-actions" data-bind="visible: grantOption() || $root.is_sentry_admin">
+      <a title="${ _('Grant this privilege') }" class="pointer" style="margin-right: 4px" data-bind="click: function(){ $root.grantToPrivilege($data); $('#grantPrivilegeModal').modal('show'); }"><i class="fa fa-send"></i></a>
+      <a title="${ _('Edit this privilege') }" class="pointer" style="margin-right: 4px" data-bind="visible: $root.is_sentry_admin, click: function() { if (! editing()) { editing(true); }}"><i class="fa fa-pencil"></i></a>
+      <a title="${ _('Delete this privilege') }" class="pointer" style="margin-right: 4px" data-bind="visible: $root.is_sentry_admin, click: remove"><i class="fa fa-times"></i></a>
+    </div>
+    <!-- /ko -->
+
+    <span class="muted" data-bind="text: privilegeScope, attr: {title: moment(timestamp()).fromNow()}"></span>
+    <!-- ko if: grantOption -->
+      <i class="fa fa-unlock muted" title="${ _('With grant option') }"></i>
+    <!-- /ko -->
+    <span data-bind="visible: metastorePath() != '' && privilegeType() == 'db'">
+      <a data-bind="attr: { href: metastorePath() }" class="muted" target="_blank" style="margin-left: 4px" title="${ _('Open in Metastore') }"><i class="fa fa-external-link"></i></a>
+    </span>
+    <br/>
+
+    server=<span data-bind="text: serverName"></span>
+
+    <!-- ko if: privilegeType() == 'db' -->
+      <span data-bind="visible: dbName">
+        <i class="fa fa-long-arrow-right"></i> db=<a class="pointer" data-bind="click: function(){ $root.linkToBrowse(dbName()) }" title="${ _('Browse db privileges') }"><span data-bind="text: dbName"></span></a>
+      </span>
+      <span data-bind="visible: tableName">
+        <i class="fa fa-long-arrow-right"></i> table=<a class="pointer" data-bind="click: function(){ $root.linkToBrowse(dbName() + '.' + tableName()) }" title="${ _('Browse table privileges') }"><span data-bind="text: tableName"></span></a>
+      </span>
+      <span data-bind="visible: columnName">
+        <i class="fa fa-long-arrow-right"></i> column=<a class="pointer" data-bind="click: function(){ $root.linkToBrowse(dbName() + '.' + tableName() + '.' + columnName()) }" title="${ _('Browse column privileges') }"><span data-bind="text: columnName"></span></a>
+      </span>
+    <!-- /ko -->
+
+    <!-- ko if: privilegeType() == 'uri' -->
+      <i class="fa fa-long-arrow-right"></i> <i class="fa fa-file-o"></i> <i class="fa fa-long-arrow-right"></i> <a data-bind="attr: { href: '/filebrowser/view=/' + URI().split('/')[3] }" target="_blank"><span data-bind="text: URI"></span></a>
+    <!-- /ko -->
+
+    <i class="fa fa-long-arrow-right"></i> action=<span data-bind="text: action"></span>
+
+  <!-- /ko -->
+</div>
+</script>
+
+<div class="container-fluid">
+  <div class="row-fluid">
+    <div class="span2">
+      <div class="sidebar-nav">
+        <ul class="nav nav-list">
+          <li class="nav-header">${ _('Privileges') }</li>
+          <li class="active"><a href="javascript:void(0)" data-toggleSection="edit"><i class="fa fa-sitemap fa-rotate-270"></i> ${ _('Browse') }</a></li>
+          <li><a href="javascript:void(0)" data-toggleSection="roles"><i class="fa fa-cubes"></i> ${ _('Roles') }</a></li>
+          <li class="nav-header"><i class="fa fa-group"></i> ${ _('Groups') }
+            <div>
+            <br/>
+             <select id="selectedGroup" data-bind="options: $root.selectableHadoopGroups, select2: { dropdownAutoWidth: true, update: $data.action, type: 'action', allowClear: true }" style="width: 100%"></select>
+            </div>
+          </li>
+        </ul>
+      </div>
+    </div>
+
+    <div class="span10">
+
+      <div id="edit" class="mainSection card card-small">
+        <h1 class="card-heading simple">
+          ${ _('Database and Table privileges') }
+          <div id="help-content" class="hide">
+            ${ _('Check the') } <a href="http://gethue.com/apache-sentry-made-easy-with-the-new-hue-security-app/#howto" target="_blank">${ _('documentation!') }</a>
+          </div>
+          <div data-bind="visible: ! $root.is_sentry_admin" class="pull-right" rel="tooltip" data-original-title="${ _('Click to see some Help') }" data-placement="top">
+            <i class="fa fa-question-circle help" style="cursor: pointer"></i>
+          </div>
+        </h1>
+
+        <div class="card-body">
+          <div class="row-fluid" data-bind="visible: $root.doAs() != '${ user.username }' && ! $root.assist.isDiffMode()">
+            <div class="span12">
+              <div class="alert"><i class="fa fa-warning"></i> ${ _('You are currently impersonating the user') } <strong data-bind="text: $root.doAs"></strong></div>
+            </div>
+          </div>
+          <div class="row-fluid">
+            <div class="span6">
+              <div class="path-container">
+                <div class="input-append span12">
+                  <input id="path" class="path" type="text" autocomplete="off" />
+                  <a data-bind="attr: { href: $root.assist.metastorePath() }" target="_blank" title="${ _('Open in Metastore Browser') }" class="btn btn-inverse">
+                    <i class="fa fa-external-link"></i>
+                  </a>
+                </div>
+                <div class="clearfix"></div>
+                <div class="tree-toolbar">
+                  <div class="pull-right">
+                    % if has_impersonation_perm:
+                    <div class="dropdown inline-block" style="margin-right: 6px">
+                      <a class="dropdown-toggle" data-toggle="dropdown" href="#">
+                        <i class="fa fa-eye-slash" data-bind="visible: $root.assist.isDiffMode"></i>
+                        <i class="fa fa-eye" data-bind="visible: ! $root.assist.isDiffMode()"></i>
+                        <span data-bind="visible: $root.assist.isDiffMode">${ _('Show non accessible paths for') }</span>
+                        <span data-bind="visible: ! $root.assist.isDiffMode()">${ _('Impersonate the user') }</span>
+                      </a>
+                      <ul class="dropdown-menu">
+                        <li data-bind="visible: ! $root.assist.isDiffMode(), click: function() { $root.assist.isDiffMode(true); }">
+                          <a tabindex="-1" href="#">${ _('Show non accessible paths for') }</a>
+                        </li>
+                        <li data-bind="visible: $root.assist.isDiffMode(), click: function() { $root.assist.isDiffMode(false); }">
+                          <a tabindex="-1" href="#">${ _('Impersonate the user') }</a>
+                        </li>
+                      </ul>
+                    </div>
+                    <select class="user-list" data-bind="options: $root.selectableHadoopUsers, select2: { placeholder: '${ _ko("Select a user") }', update: $root.doAs, type: 'user'}" style="width: 120px"></select>
+                    % endif
+                  </div>
+                  <div>
+                    <a class="pointer" data-bind="click: $root.assist.collapseOthers" rel="tooltip" data-placement="right" title="${_('Close other nodes')}">
+                      <i class="fa fa-compress"></i>
+                    </a>
+                    &nbsp;
+                    <a class="pointer" data-bind="click: $root.assist.refreshTree" rel="tooltip" data-placement="right" title="${_('Refresh the tree')}">
+                      <i class="fa fa-refresh"></i>
+                    </a>
+                    &nbsp;
+                    <a class="pointer" data-bind="visible: $root.assist.checkedItems().length > 0, click: function(){ $root.isApplyingBulk(true); $('#bulkActionsModal').modal('show'); }" rel="tooltip" data-placement="right" title="${ _('Add, replace or remove ACLs for the checked paths') }">
+                      <i class="fa fa-copy"></i>
+                    </a>
+                    &nbsp;
+                    <i class="fa fa-spinner fa-spin" data-bind="visible: $root.assist.isLoadingTree()"></i>
+                  </div>
+
+                </div>
+              </div>
+
+              ${ tree.render(id='expandableTree', data='$root.assist.treeData', afterRender='$root.assist.afterRender') }
+
+            </div>
+            <div class="span6 acl-panel">
+              <div class="acl-panel-content">
+                <div class="pull-right">
+                  <input class="input-medium no-margin" type="text" placeholder="${ _('Search privileges...') }" data-bind="value: privilegeFilter, valueUpdate: 'afterkeydown', visible: $root.assist.privileges().length > 1"> &nbsp;
+                  <a data-bind="visible: $root.assist.privileges().length > 0 && $root.is_sentry_admin, click: function(){ $root.showCreateRole(true); $('#createRoleModal').modal('show'); }" class="btn pointer">
+                    <i class="fa fa-plus-circle"></i> ${ _('Add role') }
+                  </a>
+                </div>
+                <div data-bind="visible: $root.assist.privileges().length == 0 && $root.isLoadingPrivileges()"><i class="fa fa-spinner fa-spin" data-bind="visible: $root.isLoadingPrivileges()"></i> <em class="muted">${ _('Loading privileges...')}</em></div>
+                <h4 style="margin-top: 4px" data-bind="visible: $root.assist.privileges().length > 0 && ! $root.isLoadingPrivileges()">${ _('Privileges') } &nbsp;</h4>
+                <div data-bind="visible: $root.assist.privileges().length == 0 && ! $root.isLoadingPrivileges()">
+                  <div class="span10 offset1 center" style="cursor: pointer" data-bind="click: function(){ if ($root.is_sentry_admin) { $root.showCreateRole(true); $('#createRoleModal').modal('show'); } }">
+                    <i data-bind="visible: $root.is_sentry_admin" class="fa fa-plus-circle waiting"></i>
+                    <h1 class="emptyMessage">
+                      ${ _('No privileges found for the selected item') }<br/>
+                      <a class="pointer" data-bind="visible: $root.is_sentry_admin">${ _('Click here to add some') }</a>
+                    </h1>
+                  </div>
+                </div>
+                <div data-bind="template: { name: 'role', foreach: $root.assist.roles }"></div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+
+      <div id="roles" class="mainSection hide card card-small">
+        <h1 class="card-heading simple">
+          ${ _('Roles') }
+          <div data-bind="visible: ! $root.is_sentry_admin" class="pull-right" rel="tooltip" data-original-title="${ _('Click to see some Help') }" data-placement="top">
+            <i class="fa fa-question-circle help" style="cursor: pointer"></i>
+          </div>
+        </h1>
+
+        <div class="card-body">
+          <h1 class="muted" data-bind="visible: $root.isLoadingRoles()"><i class="fa fa-spinner fa-spin"></i></h1>
+          <div class="span10 offset1 center" style="cursor: pointer" data-bind="visible: $root.roles().length == 0 && ! $root.isLoadingRoles(), click: function(){ if ($root.is_sentry_admin) { $root.showCreateRole(true); $('#createRoleModal').modal('show'); } }">
+            <i data-bind="visible: $root.is_sentry_admin" class="fa fa-plus-circle waiting"></i>
+            <h1 class="emptyMessage">
+              ${ _('There are currently no roles defined') }<br/>
+              <a class="pointer" data-bind="visible: $root.is_sentry_admin">${ _('Click here to add one') }</a>
+              <br/>
+            </h1>
+          </div>
+          <div class="clearfix" data-bind="visible: $root.roles().length == 0 && ! $root.isLoadingRoles()"></div>
+          <div data-bind="visible: $root.roles().length > 0 && ! $root.isLoadingRoles()">
+            <%actionbar:render>
+              <%def name="search()">
+                <input id="filterInput" type="text" class="input-xlarge search-query" placeholder="${_('Search roles by name, groups, etc...')}" data-bind="value: $root.roleFilter, valueUpdate: 'afterkeydown'">
+              </%def>
+
+              <%def name="actions()">
+                <button class="btn toolbarBtn" data-bind="click: $root.expandSelectedRoles, enable: $root.selectedRoles().length > 0"><i class="fa fa-expand"></i> ${ _('Expand') }</button>
+                <button class="btn toolbarBtn" data-bind="visible: $root.is_sentry_admin, click: function(){ $('#deleteRoleModal').modal('show'); }, enable: $root.selectedRoles().length > 0">
+                  <i class="fa fa-times"></i> ${ _('Delete') }
+                </button>
+              </%def>
+
+              <%def name="creation()">
+                <a data-bind="visible: $root.is_sentry_admin, click: function(){ $root.showCreateRole(true); $('#createRoleModal').modal('show'); }" class="btn pointer">
+                  <i class="fa fa-plus-circle"></i> ${ _('Add') }
+                </a>
+              </%def>
+            </%actionbar:render>
+          </div>
+
+          <table class="card-marginbottom" data-bind="visible: $root.roles().length > 0 && ! $root.isLoadingRoles()">
+            <thead>
+              <th width="1%"><div data-bind="click: $root.selectAllRoles, css: { hueCheckbox: true, 'fa': true, 'fa-check': allRolesSelected }"></div></th>
+              <th width="2%"></th>
+              <th width="20%" style="text-align:left">${ _('Name') }</th>
+              <th width="74%" style="text-align:left">${ _('Groups') }</th>
+              <th width="3%"></th>
+            </thead>
+            <tbody data-bind="foreach: $root.filteredRoles">
+              <tr>
+                <td class="center" data-bind="click: handleSelect" style="cursor: default">
+                  <div data-bind="css: { hueCheckbox: true, 'fa': true, 'fa-check': selected }"></div>
+                </td>
+                <td class="center">
+                  <a href="javascript:void(0);" title="${ _('Show privileges') }">
+                    <i class="fa fa-2x fa-caret" data-bind="click: function() { if (showPrivileges()) { showPrivileges(false); } else { $root.list_sentry_privileges_by_role($data);} }, css: {'fa-caret-right' : ! showPrivileges(), 'fa-caret-down': showPrivileges() }"></i>
+                  </a>
+                </td>
+                <td data-bind="click: function() { if (showPrivileges()) { showPrivileges(false); } else { $root.list_sentry_privileges_by_role($data);} }" class="pointer">
+                  <a data-bind="attr: {'href': name}"></a>
+                  <i class="fa fa-cube muted"></i>
+                  <span data-bind="text: name"/>
+                </td>
+                <td>
+                  <!-- ko if: $root.is_sentry_admin -->
+                  <a class="pointer" data-bind="click: function() { if ($root.is_sentry_admin) { showEditGroups(true); } }">
+                    <span data-bind="foreach: groups, visible: ! showEditGroups() && ! groupsChanged()">
+                      <span data-bind="text: $data"></span>
+                    </span>
+                    <span data-bind="visible: ! showEditGroups() && ! groupsChanged() && groups().length == 0">
+                      <i class="fa fa-plus"></i> ${ _('Add a group') }
+                    </span>
+                  </a>
+                  <!-- /ko -->
+                  <!-- ko ifnot: $root.is_sentry_admin -->
+                    <span data-bind="foreach: groups">
+                      <span data-bind="text: $data"></span>
+                    </span>
+                  <!-- /ko -->
+                  <div data-bind="visible: showEditGroups() || (groupsChanged() && ! $root.isLoadingRoles())">
+                    <select data-bind="options: $root.selectableHadoopGroups, selectedOptions: groups, select2: { update: groups, type: 'group', onBlur: function(){ showEditGroups(false); } }" size="5" multiple="true" style="width: 400px"></select>
+                    &nbsp;
+                    <a class="pointer" data-bind="visible: groupsChanged() && ! $root.isLoadingRoles(), click: resetGroups">
+                      <i class="fa fa-undo"></i>
+                    </a>
+                    <a class="pointer" data-bind="visible: groupsChanged && ! $root.isLoadingRoles(), click: saveGroups">
+                      <i class="fa fa-save"></i>
+                    </a>
+                  </div>
+                </td>
+                <td>
+                </td>
+              </tr>
+              <tr>
+                <td colspan="2"></td>
+                <td colspan="4">
+                  <div data-bind="template: { name: 'privilege', foreach: $data.privileges }, visible: $data.showPrivileges">
+                  </div>
+                </td>
+              </tr>
+              <tr data-bind="visible: $data.showPrivileges">
+                <td colspan="2"></td>
+                <td colspan="4">
+                  <div class="acl-block acl-actions" data-bind="click: privilegesChanged().length == 0 ? addPrivilege : void(0), visible: $root.is_sentry_admin">
+                    <span class="pointer" data-bind="click: addPrivilege, visible: $data.showPrivileges" title="${ _('Add privilege') }"><i class="fa fa-plus"></i></span>
+                    <span class="pointer" data-bind="click: $root.list_sentry_privileges_by_role, visible: privilegesChanged().length > 0" title="${ _('Undo') }"> &nbsp; <i class="fa fa-undo"></i></span>
+                    <span class="pointer" data-bind="click: function() { deletePrivilegeModal($data) }, visible: privilegesChanged().length > 0" title="${ _('Save') }"> &nbsp; <i class="fa fa-save"></i></span>
+                  </div>
+                </td>
+              </tr>
+            </tbody>
+          </table>
+        </div>
+      </div>
+
+    </div> <!-- /span10 -->
+</div>
+
+
+
+<div id="createRoleModal" class="modal hide fade in" role="dialog">
+  <div class="modal-header">
+    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+    <h3 data-bind="visible: ! $root.role().isEditing()">${ _('Add or select a role') }</h3>
+    <h3 data-bind="visible: $root.role().isEditing()">${ _('Edit role') }</h3>
+  </div>
+  <div class="modal-body" data-bind="with: $root.role, visible: showCreateRole">
+
+    <div class="row-fluid">
+      <div class="span6">
+        <h4>${ _('Name') }</h4>
+        <input id="createRoleName" type="text" class="input-xlarge" data-bind="value: $data.name, visible: ! $data.isEditing()" placeholder="${ _('New or existing role name') }" style="width: 360px" />
+        <strong data-bind="text: $data.name, visible: $data.isEditing()"></strong>
+      </div>
+      <div class="span6">
+        <h4>${ _('Groups') }</h4>
+        <select data-bind="options: $root.selectableHadoopGroups, selectedOptions: groups, select2: { update: groups, type: 'group', placeholder: '${ _ko("Optional") }' }" size="5" multiple="true" style="width: 360px"></select>
+      </div>
+    </div>
+
+    <h4>${ _('Privileges') }</h4>
+    <div data-bind="template: { name: 'privilege', foreach: privileges }"></div>
+    <div class="acl-block acl-actions pointer" data-bind="click: addPrivilege">
+      <span class="pointer" title="${ _('Add privilege') }"><i class="fa fa-plus"></i></span>
+    </div>
+  </div>
+  <div class="modal-footer">
+    <button class="btn" data-dismiss="modal" aria-hidden="true">${ _('Cancel') }</button>
+    <button data-loading-text="${ _('Saving...') }" class="btn btn-primary disable-enter" data-bind="click: $root.role().create, visible: ! $root.role().isEditing()">${ _('Save') }</button>
+    <button data-loading-text="${ _('Saving...') }" class="btn btn-primary disable-enter" data-bind="click: $root.role().update, visible: $root.role().isEditing()">${ _('Update') }</button>
+  </div>
+</div>
+
+
+<div id="grantPrivilegeModal" class="modal hide fade in" role="dialog">
+  <div class="modal-header">
+    <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
+    <h3>${ _('Grant privilege') }</h3>
+  </div>
+  <div class="modal-body">
+
+    <!-- ko if: $root.grantToPrivilege() -->
+      <div data-bind="template: { name: 'privilege', data: $root.grantToPrivilege() }"></div>
+    <!-- /ko -->
+
+    <br/>
+    <span>${ _('To role') }&nbsp;&nbsp;</span>
+    <select data-bind="options: $root.selectableRoles(), value: $root.grantToPrivilegeRole, select2: { update: $root.grantToPrivilegeRole, placeholder: '${ _ko("Select a role") }', type: 'role' }" style="width: 360px"></select>
+    <br/>
+
+  </div>
+  <div class="modal-footer">
+    <button class="btn" data-dismiss="modal" aria-hidden="true">${ _('Cancel') }</button>
+    <button data-loading-text="${ _('Saving...') }" class="btn btn-primary disable-enter" data-bind="click: $root.grant_privilege">${ _('Grant') }</button>
+  </div>
+</div>
+
+
+<div id="deleteRoleModal" class="modal hide fade in" role="dialog">
+  <div class="modal-header">
+    <a href="#" class="close" data-dismiss="modal">&times;</a>
+    <h3>${ _('Do you really want to delete the selected role(s)?') }</h3>
+  </div>
+  <div class="modal-footer">
+    <button class="btn" data-dismiss="modal" aria-hidden="true">${ _('Cancel') }</button>
+    <button data-loading-text="${ _('Deleting...') }" class="btn btn-danger" data-bind="click: $root.deleteSelectedRoles">${ _('Yes') }</button>
+  </div>
+</div>
+
+
+<div id="deletePrivilegeModal" class="modal hide fade in" role="dialog">
+  <div class="modal-header">
+    <a href="#" class="close" data-dismiss="modal">&times;</a>
+    <h3>${ _('Confirm the deletion?') }</h3>
+  </div>
+  <div class="modal-body">
+    ${ _('Sentry will recursively delete the SERVER or DATABASE privileges you marked for deletion.') }
+  </div>
+  <div class="modal-footer">
+    <button class="btn" data-dismiss="modal" aria-hidden="true">${ _('Cancel') }</button>
+    <button data-loading-text="${ _('Deleting...') }" class="btn btn-danger" data-bind="click: function() { $root.role().savePrivileges($root.roleToUpdate()); }">${ _('Yes, delete') }</button>
+  </div>
+</div>
+
+
+<div id="bulkActionsModal" class="modal hide fade in" role="dialog">
+  <div class="modal-header">
+    <a href="#" class="close" data-dismiss="modal">&times;</a>
+    <h3>${ _('Select one operation') }</h3>
+  </div>
+  <div class="modal-body" style="overflow-x: hidden">
+
+    <div class="row-fluid">
+      <div class="span8">
+        <div class="row-fluid">
+          <div class="span4 center">
+            <div class="big-btn" data-bind="css: {'selected': $root.bulkAction() == 'add'}, click: function(){$root.bulkAction('add')}">
+              <i class="fa fa-plus"></i><br/><br/>
+              <span>${ _('Add current privileges to selection') }</span>
+            </div>
+          </div>
+          <div class="span4 center">
+            <div class="big-btn" data-bind="css: {'selected': $root.bulkAction() == 'sync'}, click: function(){$root.bulkAction('sync')}">
+              <i class="fa fa-random"></i><br/><br/>
+              <span>${ _('Replace selection with current privileges') }</span>
+            </div>
+          </div>
+          <div class="span4 center">
+            <div class="big-btn" data-bind="css: {'selected': $root.bulkAction() == 'delete'}, click: function(){$root.bulkAction('delete')}">
+              <i class="fa fa-eraser"></i><br/><br/>
+              <span>${ _('Delete all privileges of selection') }</span>
+            </div>
+          </div>
+        </div>
+      </div>
+      <div class="span4">
+        <h4>${ _('to apply to the selection') }</h4>
+        <ul class="unstyled modal-panel" data-bind="foreach: $root.assist.checkedItems">
+          <li data-bind="visible: path.indexOf('.') > -1" class="force-word-break">
+            <i class="fa fa-database muted"></i>
+            <span data-bind="text: path.split('.')[0]"></span>
+            <i class="fa fa-long-arrow-right muted"></i>
+            <i class="fa fa-table muted"></i>
+            <span data-bind="text: path.split('.')[1]"></span>
+            <span data-bind="visible: path.split('.')[2]">
+              <i class="fa fa-long-arrow-right muted"></i>
+              <i class="fa fa-columns muted"></i>
+              <span data-bind="text: path.split('.')[2]"></span>
+            </span>
+          </li>
+          <li data-bind="visible: path.indexOf('.') == -1" class="force-word-break">
+            <i class="fa fa-database muted"></i> <span data-bind="text: path.split('.')[0]"></span>
+          </li>
+        </ul>
+      </div>
+    </div>
+    <br/>
+    <div class="row-fluid" data-bind="visible: $root.bulkAction() != '' && $root.bulkAction() != 'delete'">
+      <div class="span12">
+
+        <h4>${ _('Privileges to apply') }</h4>
+
+        <div data-bind="visible: $root.assist.privileges().length == 0"><em class="muted">${ _('No privileges found for the selected item.')}</em></div>
+        <div data-bind="template: { name: 'role', foreach: $root.assist.roles }" class="modal-panel"></div>
+      </div>
+    </div>
+
+  </div>
+  <div class="modal-footer">
+    <button class="btn" data-dismiss="modal" aria-hidden="true">${ _('Cancel') }</button>
+    <button class="btn" data-bind="enable: $root.bulkAction(), css: {'btn-primary': $root.bulkAction() != 'delete', 'btn-danger': $root.bulkAction() == 'delete'}, click: $root.bulkPerfomAction">${ _('Confirm') }</button>
+  </div>
+</div>
+
+<div id="chooseFile" class="modal hide fade">
+  <div class="modal-header">
+      <a href="#" class="close" data-dismiss="modal">&times;</a>
+      <h3>${_('Choose a file')}</h3>
+  </div>
+  <div class="modal-body">
+      <div id="filechooser">
+      </div>
+  </div>
+  <div class="modal-footer">
+  </div>
+</div>
+
+
+<%def name="treeIcons()">
+  'fa-hdd-o': isServer(),
+  'fa-database-open': isDb() && (nodes().length > 0 || isLoaded()),
+  'fa-database': isDb() && nodes().length == 0,
+  'fa-table': isTable(),
+  'fa-columns': isColumn()
+</%def>
+
+<%def name="withPrivilegesPullRight()">
+  <div class="pull-right">
+    <i class="fa fa-shield" data-bind="visible: withPrivileges()" style="color: #338bb8" title="${ _('Has some privileges') }"></i>&nbsp;
+    <i class="fa fa-file-o muted" data-bind="click: $root.assist.showHdfs ,visible: isTable()"></i>
+  </div>
+</%def>
+
+
+${ tree.import_templates(itemClick='$root.assist.setPath', iconClick='$root.assist.togglePath', itemSelected='$root.assist.path() == path()', styleModifier='withPrivileges', iconModifier=treeIcons, anchorProperty='path', itemChecked='isChecked', styleModifierPullRight=withPrivilegesPullRight) }
+
+
+<script src="${ static('desktop/ext/js/knockout.min.js') }" type="text/javascript" charset="utf-8"></script>
+<script src="${ static('desktop/ext/js/knockout-mapping.min.js') }" type="text/javascript" charset="utf-8"></script>
+<script src="${ static('desktop/ext/js/routie-0.3.0.min.js') }" type="text/javascript" charset="utf-8"></script>
+
+<script src="${ static('desktop/js/ko.hue-bindings.js') }" type="text/javascript" charset="utf-8"></script>
+<script src="${ static('security/js/hive.ko.js') }" type="text/javascript" charset="utf-8"></script>
+
+<script src="${ static('desktop/ext/js/moment-with-locales.min.js') }" type="text/javascript" charset="utf-8"></script>
+<script src="${ static('desktop/js/jquery.hiveautocomplete.js') }" type="text/javascript" charset="utf-8"></script>
+<script src="${ static('desktop/js/jquery.filechooser.js') }" type="text/javascript" charset="utf-8"></script>
+
+
+<script type="text/javascript" charset="utf-8">
+
+    function deletePrivilegeModal(role) {
+      var cascadeDeletes = $.grep(role.privilegesChanged(), function(privilege) {
+          return privilege.status() == 'deleted' && (privilege.privilegeScope() == 'SERVER' || privilege.privilegeScope() == 'DATABASE'); }
+      );
+      if (cascadeDeletes.length > 0 ) {
+        viewModel.roleToUpdate(role);
+        $('#deletePrivilegeModal').modal('show');
+      } else {
+        viewModel.role().savePrivileges(role);
+      }
+    }
+
+    var viewModel = new HiveViewModel(${ initial | n,unicode });
+    ko.applyBindings(viewModel);
+
+    $(document).ready(function () {
+      var _initialPath = viewModel.getPathHash();
+      viewModel.init(_initialPath);
+      $("#path").val(_initialPath);
+
+      $(".help").popover({
+        'title': "${ _('Looking for edit permissions?') }",
+        'content': $("#help-content").html(),
+        'trigger': 'click',
+        'placement': 'left',
+        'html': true
+      });
+
+      function setPathFromAutocomplete(path){
+        if (path.lastIndexOf(".") == path.length -1){
+          path = path.substring(0, path.length - 1);
+        }
+        viewModel.assist.path(path);
+        viewModel.assist.updatePathProperty(viewModel.assist.growingTree(), path, "isExpanded", true);
+        viewModel.assist.fetchHivePath();
+      }
+
+      $("#path").jHueHiveAutocomplete({
+        skipColumns: true,
+        home: viewModel.assist.path(),
+        onPathChange: function (path) {
+          setPathFromAutocomplete(path);
+        },
+        onEnter: function (el) {
+          setPathFromAutocomplete(el.val());
+        },
+        smartTooltip: "${_('Did you know? You can use the tab key or CTRL + Space to autocomplete file and folder names')}"
+      });
+
+      function resizeComponents() {
+        $("#path").width($(".tree-toolbar").width() - 64);
+        $("#expandableTree").height($(window).height() - 260);
+        $(".acl-panel-content").height($(window).height() - 240);
+      }
+
+      resizeComponents();
+
+      $(document).on("rendered.tree", function() {
+        var _path = viewModel.assist.path();
+        if (_path[_path.length-1] == "/"){
+          _path = _path.substr(0, _path.length - 1);
+        }
+        if ($("a.anchor[href^='"+_path+"']").length > 0){
+          $("#expandableTree").animate({
+            scrollTop: ($("a.anchor[href^='"+_path+"']:first").position().top + $("#expandableTree").scrollTop() - $("#expandableTree").position().top - 4) + "px"
+          });
+        }
+      });
+
+      $(document).on("created.role", function(){
+        $("#createRoleModal").modal("hide");
+        $("#grantPrivilegeModal").modal("hide");
+        $("#deletePrivilegeModal").modal("hide");
+        viewModel.clearTempRoles();
+        window.setTimeout(function(){
+          viewModel.refreshExpandedRoles();
+        }, 500);
+      });
+
+      $(document).on("deleted.role", function(){
+        $("#deleteRoleModal").modal("hide");
+      });
+
+      $(document).on("changed.path", function(){
+        if ($("#path").val() != viewModel.assist.path()){
+          $("#path").val(viewModel.assist.path());
+        }
+      });
+
+      function showMainSection(mainSection) {
+        if ($("#" + mainSection).is(":hidden")) {
+          $(".mainSection").hide();
+          $("#" + mainSection).show();
+          highlightMainMenu(mainSection);
+          viewModel.updateSectionHash(mainSection);
+        }
+        logGA(mainSection);
+      }
+
+      function highlightMainMenu(mainSection) {
+        $(".nav.nav-list li").removeClass("active");
+        $("a[data-toggleSection='" + mainSection + "']").parent().addClass("active");
+      }
+
+      $("[data-toggleSection]").on("click", function(){
+        showMainSection($(this).attr("data-toggleSection"));
+      });
+
+      showMainSection(viewModel.getSectionHash());
+
+      $(document).on("show.mainSection", function(){
+        showMainSection(viewModel.getSectionHash());
+      });
+
+      $(document).on("show.role", function(e, role) {
+        if (typeof role != "undefined" && role.name != null){
+          $("#bulkActionsModal").modal("hide");
+          showMainSection("roles");
+          $("html, body").animate({
+            scrollTop: ($("a[href='" + role.name() + "']").position().top - 90) + "px"
+          });
+        }
+      });
+
+      var _resizeTimeout = -1;
+      $(window).resize(function(){
+        window.clearTimeout(_resizeTimeout);
+        _resizeTimeout = window.setTimeout(resizeComponents, 100);
+      });
+
+      window.onpopstate = function() {
+        viewModel.assist.path(viewModel.getPathHash());
+      };
+
+      $("#createRoleModal").modal({
+        show: false
+      });
+
+      $("#grantPrivilegeModal").modal({
+        show: false
+      });
+
+      $("#createRoleModal").on("hidden", function () {
+        $('#jHueHiveAutocomplete').hide();
+        viewModel.resetCreateRole();
+      });
+
+      $("#grantPrivilegeModal").on("hidden", function () {
+        viewModel.clearTempRoles();
+      });
+
+
+      $("#deleteRoleModal").modal({
+        show: false
+      });
+
+      $("#selectedGroup").select2("val", "");
+      $("#selectedGroup").change(function() {
+        viewModel.list_sentry_privileges_by_authorizable();
+        viewModel.list_sentry_roles_by_group();
+      });
+
+      $(document).on("added.bulk.privileges", function() {
+        $(document).trigger("info", "${ _('The current privileges have been successfully added to the checked items.') }");
+        $("#bulkActionsModal").modal("hide");
+      });
+
+      $(document).on("deleted.bulk.privileges", function() {
+        $(document).trigger("info", "${ _('All the privileges have been successfully removed from the checked items.') }");
+        $("#bulkActionsModal").modal("hide");
+      });
+
+      $(document).on("syncd.bulk.privileges", function() {
+        $(document).trigger("info", "${ _('All the privileges for the checked items have been replaced with the current selection.') }");
+        $("#bulkActionsModal").modal("hide");
+      });
+
+      $("#bulkActionsModal").modal({
+        show: false
+      });
+
+      $("#bulkActionsModal").on("hidden", function(){
+        viewModel.isApplyingBulk(false);
+      });
+
+      $(document).on("create.typeahead", function(){
+        $("#createRoleName").typeahead({
+            source: function (query) {
+              var _options = [];
+              viewModel.selectableRoles().forEach(function(item){
+                if (item.toLowerCase().indexOf(query.toLowerCase()) > -1){
+                  _options.push(item);
+                }
+              });
+              return _options;
+            },
+            'updater': function(item) {
+                return item;
+            }
+        });
+      });
+      $(document).on("destroy.typeahead", function(){
+        $('.typeahead').unbind();
+        $("ul.typeahead").hide();
+      });
+
+      $(document).trigger("create.typeahead");
+
+      $("#deletePrivilegeModal").modal({
+        show: false
+      });
+    });
+</script>
+
+${ commonfooter(messages) | n,unicode }

+ 2 - 0
apps/security/src/security/templates/layout.mako

@@ -64,6 +64,8 @@ def is_selected(section, matcher):
                 </a>
               </li>
               <li class="${is_selected(section, 'hive')}"><a href="${ url('security:hive') }">${_('Sentry Tables')}</a></li>
+              <li class="${is_selected(section, 'hive2')}"><a href="${ url('security:hive2') }">${_('Sentry Tables v2')}</a></li>
+              <li class="${is_selected(section, 'solr')}"><a href="${ url('security:hive') }">${_('Solr Collections')}</a></li>
               <li class="${is_selected(section, 'hdfs')}"><a href="${ url('security:hdfs') }">${_('File ACLs')}</a></li>
             </ul>
           </div>

+ 24 - 1
apps/security/src/security/urls.py

@@ -21,6 +21,8 @@ from django.conf.urls import patterns, url
 urlpatterns = patterns('security.views',
   url(r'^$', 'hive', name='index'),
   url(r'^hive$', 'hive', name='hive'),
+  url(r'^hive2$', 'hive2', name='hive2'),
+  url(r'^solr$', 'solr', name='solr'),
   url(r'^hdfs$', 'hdfs', name='hdfs'),
 )
 
@@ -35,7 +37,7 @@ urlpatterns += patterns('security.api.hdfs',
 )
 
 
-urlpatterns += patterns('security.api.hive',
+urlpatterns += patterns('security.api.hive2',
   url(r'^api/hive/fetch_hive_path', 'fetch_hive_path', name='fetch_hive_path'),
 
   url(r'^api/hive/list_sentry_roles_by_group', 'list_sentry_roles_by_group', name='list_sentry_roles_by_group'),
@@ -54,3 +56,24 @@ urlpatterns += patterns('security.api.hive',
   # Unused: API is for blind bulk operations
   url(r'^api/hive/rename_sentry_privilege', 'rename_sentry_privilege', name='rename_sentry_privilege'),
 )
+
+
+urlpatterns += patterns('security.api.hive2',
+  url(r'^api/hive2/fetch_hive_path', 'fetch_hive_path', name='fetch_hive_path'),
+
+  url(r'^api/hive2/list_sentry_roles_by_group', 'list_sentry_roles_by_group', name='list_sentry_roles_by_group'),
+  url(r'^api/hive2/list_sentry_privileges_by_role', 'list_sentry_privileges_by_role', name='list_sentry_privileges_by_role'),
+  url(r'^api/hive2/list_sentry_privileges_for_provider$', 'list_sentry_privileges_for_provider', name='list_sentry_privileges_for_provider'),
+  url(r'^api/hive2/list_sentry_privileges_by_authorizable', 'list_sentry_privileges_by_authorizable', name='list_sentry_privileges_by_authorizable'),
+  url(r'^api/hive2/create_sentry_role', 'create_sentry_role', name='create_sentry_role'),
+  url(r'^api/hive2/update_role_groups', 'update_role_groups', name='update_role_groups'),
+  url(r'^api/hive2/drop_sentry_role', 'drop_sentry_role', name='drop_sentry_role'),
+  url(r'^api/hive2/create_role$', 'create_role', name='create_role'),
+  url(r'^api/hive2/save_privileges$', 'save_privileges', name='save_privileges'),
+  url(r'^api/hive2/bulk_delete_privileges', 'bulk_delete_privileges', name='bulk_delete_privileges'),
+  url(r'^api/hive2/bulk_add_privileges', 'bulk_add_privileges', name='bulk_add_privileges'),
+  url(r'^api/hive2/grant_privilege', 'grant_privilege', name='grant_privilege'),
+
+  # Unused: API is for blind bulk operations
+  url(r'^api/hive2/rename_sentry_privilege', 'rename_sentry_privilege', name='rename_sentry_privilege'),
+)

+ 26 - 1
apps/security/src/security/views.py

@@ -26,7 +26,32 @@ def hive(request):
 
   return render("hive.mako", request, {
       'initial': json.dumps({
-          'user': request.user.username, 'sentry_provider': get_hive_sentry_provider(),
+          'user': request.user.username,
+          'sentry_provider': get_hive_sentry_provider(),
+          'is_sentry_admin': request.user.groups.filter(name__in=get_sentry_server_admin_groups()).exists()
+      }),
+      'has_impersonation_perm': _has_impersonation_perm(request.user),
+  })
+
+
+def hive2(request):
+
+  return render("hive2.mako", request, {
+      'initial': json.dumps({
+          'user': request.user.username,
+          'sentry_provider': get_hive_sentry_provider(),
+          'is_sentry_admin': request.user.groups.filter(name__in=get_sentry_server_admin_groups()).exists()
+      }),
+      'has_impersonation_perm': _has_impersonation_perm(request.user),
+  })
+
+
+def solr(request):
+
+  return render("hive.mako", request, {
+      'initial': json.dumps({
+          'user': request.user.username,
+          'sentry_provider': get_hive_sentry_provider(),
           'is_sentry_admin': request.user.groups.filter(name__in=get_sentry_server_admin_groups()).exists()
       }),
       'has_impersonation_perm': _has_impersonation_perm(request.user),

+ 3 - 1
desktop/core/src/desktop/templates/common_header.mako

@@ -564,7 +564,9 @@ from django.utils.translation import ugettext as _
          <li class="dropdown">
            <a title="${_('Hadoop Security')}" rel="navigator-tooltip" href="#" data-toggle="dropdown" class="dropdown-toggle">Security <b class="caret"></b></a>
            <ul role="menu" class="dropdown-menu">
-             <li><a href="${ url('security:hive') }">&nbsp;<i class="fa fa-database"></i>&nbsp;&nbsp;${_('Sentry Tables')}</a></li>
+             <li><a href="${ url('security:hive') }">&nbsp;<img src="/static/metastore/art/icon_metastore_48.png" class="app-icon"></img>&nbsp;&nbsp;${_('Sentry Tables')}</a></li>
+             <li><a href="${ url('security:hive2') }">&nbsp;<img src="/static/metastore/art/icon_metastore_48.png" class="app-icon"></img>&nbsp;&nbsp;${_('Sentry Tables v2')}</a></li>
+             <li><a href="${ url('security:solr') }">&nbsp;<i class="fa fa-database"></i>&nbsp;&nbsp;${_('Solr Collections')}</a></li>
              <li><a href="${ url('security:hdfs') }">&nbsp;<i class="fa fa-file"></i>&nbsp;&nbsp;${_('File ACLs')}</a></li>
            </ul>
          </li>

+ 1 - 1
desktop/libs/libsentry/src/libsentry/api.py

@@ -24,7 +24,7 @@ import time
 from django.utils.translation import ugettext as _
 
 from desktop.lib.exceptions_renderable import PopupException
-from libsentry.client import SentryClient
+from libsentry.client2 import SentryClient
 from libsentry.conf import HOSTNAME, PORT
 from libsentry.sentry_site import get_sentry_server_ha_enabled, get_sentry_server_ha_zookeeper_quorum, get_sentry_server_ha_zookeeper_namespace
 from libzookeeper.models import ZookeeperClient