UNPKG

gethue

Version:

Hue is an Open source SQL Query Editor for Databases/Warehouses

772 lines (711 loc) 27.2 kB
// 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 $ from 'jquery'; import * as ko from 'knockout'; import koMapping from 'knockout.mapping'; import componentUtils from './componentUtils'; import I18n from '../../utils/i18n'; import UUID from '../../utils/string/UUID'; export const NAME = 'hue-sentry-privileges'; const TEMPLATE = ` <script type="text/html" id="sentry-privileges-component-role"> <div class="acl-block-title"> <i class="fa fa-cube muted"></i> <a class="pointer" data-bind="click: function(){ $parents[0].showRole($data); }"><span data-bind="text: name"></span></a> </div> <div data-bind="template: { name: 'sentry-privileges-component-privilege', foreach: privilegesForView }"></div> <div class="acl-block acl-actions"> <span class="pointer" data-bind="visible: privilegesForViewTo() < privileges().length, click: function(){ privilegesForViewTo(privilegesForViewTo() + 50) }" title="${I18n( 'Show 50 more...' )}"><i class="fa fa-ellipsis-h"></i></span> <span class="pointer" data-bind="click: addPrivilege, visible: $parents[0].isSentryAdmin && !$parents[0].readOnly" title="${I18n( 'Add privilege' )}"><i class="fa fa-plus"></i></span> <span class="pointer" data-bind="click: function() { $parents[0].listSentryPrivilegesByAuthorizable() }, visible: privilegesChanged().length > 0" title="${I18n( 'Undo' )}"> &nbsp; <i class="fa fa-undo"></i></span> <span class="pointer" data-bind="click: function() { $parents[0].deletePrivilegeModal($data) }, visible: privilegesChanged().length > 0" title="${I18n( 'Save' )}"> &nbsp; <i class="fa fa-save"></i></span> </div> </script> <script type="text/html" id="sentry-privileges-component-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() || $parents[1].isSentryAdmin) && !$parents[1].readOnly"> <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($parents[1].availableActions(privilegeScope())[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', apiHelperUser: '${ window.LOGGED_USERNAME }', apiHelperType: 'hive'" 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', valueUpdate: 'afterkeydown'" placeholder="URI"> <select data-bind="options: $parents[1].availableActions(privilegeScope()), value: $data.action, enable: (privilegeType() == 'db')" style="width: 100px; margin-bottom: 0"></select> <div class="margin-top-10"> <label class="checkbox pull-left"><input type="checkbox" data-bind="checked: grantOption"> ${I18n( 'With grant' )}</label> <div class="inline-block margin-left-10" style="margin-top: 6px"> <a class="pointer showAdvanced" data-bind="click: function(){ showAdvanced(true); }, visible: !showAdvanced()"><i class="fa fa-cog"></i> ${I18n( 'Show advanced' )}</a> <a class="pointer showAdvanced" data-bind="click: function(){ showAdvanced(false); }, visible: showAdvanced()"><i class="fa fa-cog"></i> ${I18n( 'Hide advanced' )}</a> </div> <div class="clearfix"></div> </div> <div class="acl-block-section" data-bind="visible: showAdvanced" style="margin-top: 0"> ${I18n( 'Server' )} <input type="text" data-bind="value: serverName" placeholder="serverName" style="margin-left: 6px"> </div> <!-- /ko --> <!-- ko ifnot: editing --> <div class="pull-right privilege-actions" data-bind="visible: (grantOption() || $parents[1].isSentryAdmin) && !$parents[1].readOnly"> <a title="${I18n( 'Edit this privilege' )}" class="pointer" style="margin-right: 4px" data-bind="visible: $parents[1].isSentryAdmin, click: function() { if (! editing()) { editing(true); }}"><i class="fa fa-pencil"></i></a> <a title="${I18n( 'Delete this privilege' )}" class="pointer" style="margin-right: 4px" data-bind="visible: $parents[1].isSentryAdmin, click: remove"><i class="fa fa-times"></i></a> </div> <span class="muted" data-bind="text: privilegeScope, attr: {title: moment(timestamp()).fromNow()}"></span> <!-- ko if: grantOption --> <i class="fa fa-unlock muted" title="${I18n('With grant option')}"></i> <!-- /ko --> <span data-bind="visible: metastorePath() != '' && privilegeType() == 'db'"> <a data-bind="hueLink: metastorePath()" class="muted" style="margin-left: 4px" title="${I18n( 'Open in Table Browser...' )}"><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(){ $parents[1].linkToBrowse(dbName()) }" title="${I18n( '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(){ $parents[1].linkToBrowse(dbName() + '.' + tableName()) }" title="${I18n( '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(){ $parents[1].linkToBrowse(dbName() + '.' + tableName() + '.' + columnName()) }" title="${I18n( '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="hueLink: '/filebrowser/view=/' + URI().split('/')[3]"><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> <!-- ko hueSpinner: { spin: isLoadingPrivileges, inline: true } --><!-- /ko --> <!-- ko if: roles().length --> <div data-bind="template: { name: 'sentry-privileges-component-role', foreach: roles }"></div> <!-- /ko --> <!-- ko ifnot: roles().length --> <em>${I18n('No privileges found for the selected object.')}</em> <!-- /ko --> <div id="sentryPrivilegesComponentDeletePrivilegeModal" class="modal hide fade in" role="dialog"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-label="${I18n( 'Close' )}"><span aria-hidden="true">&times;</span></button> <h2 class="modal-title">${I18n('Confirm the deletion?')}</h2> </div> <div class="modal-body"> ${I18n( '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">${I18n('Cancel')}</button> <button data-loading-text="${I18n( 'Deleting...' )}" class="btn btn-danger" data-bind="click: function() { roleToUpdate().savePrivileges(roleToUpdate()); }">${I18n( 'Yes, delete' )}</button> </div> </div> `; class Privilege { constructor(vm, privilege) { const self = this; self.id = ko.observable( typeof privilege.id != 'undefined' && privilege.id != null ? privilege.id : '' ); self.roleName = ko.observable( typeof privilege.roleName != 'undefined' && privilege.roleName != null ? privilege.roleName : '' ); self.status = ko.observable( typeof privilege.status != 'undefined' && privilege.status != null ? privilege.status : '' ); self.editing = ko.observable( typeof privilege.editing != 'undefined' && privilege.editing != null ? privilege.editing : false ); self.serverName = ko.observable( typeof privilege.serverName != 'undefined' && privilege.serverName != null ? privilege.serverName : '' ); self.serverName.subscribe(() => { if (self.status() === '') { self.status('modified'); } }); self.dbName = ko.observable( typeof privilege.dbName != 'undefined' && privilege.dbName != null ? privilege.dbName : '' ); self.dbName.subscribe(() => { if (self.status() === '') { self.status('modified'); } }); self.tableName = ko.observable( typeof privilege.tableName != 'undefined' && privilege.tableName != null ? privilege.tableName : '' ); self.tableName.subscribe(() => { if (self.status() === '') { self.status('modified'); } }); self.columnName = ko.observable( typeof privilege.columnName != 'undefined' && privilege.columnName != null ? privilege.columnName : '' ); self.columnName.subscribe(() => { if (self.status() === '') { self.status('modified'); } }); self.URI = ko.observable( typeof privilege.URI != 'undefined' && privilege.URI != null ? privilege.URI : '' ); self.URI.subscribe(() => { if (self.status() === '') { self.status('modified'); } }); self.action = ko.observable( typeof privilege.action != 'undefined' && privilege.action != null ? privilege.action : 'SELECT' ); self.action.subscribe(() => { if (self.status() === '') { self.status('modified'); } }); self.timestamp = ko.observable( typeof privilege.timestamp != 'undefined' && privilege.timestamp != null ? privilege.timestamp : 0 ); self.grantOption = ko.observable( typeof privilege.grantOption != 'undefined' && privilege.grantOption != null ? privilege.grantOption : false ); self.grantOption.subscribe(() => { if (self.status() === '') { self.status('modified'); } }); // UI self.privilegeType = ko.observable( typeof privilege.privilegeScope != 'undefined' && privilege.privilegeScope === 'URI' ? 'uri' : 'db' ); self.showAdvanced = ko.observable(false); self.path = ko.computed({ read: function () { if (self.columnName().length > 0) { return self.dbName() + '.' + self.tableName() + '.' + self.columnName(); } else if (self.tableName().length > 0) { return self.dbName() + '.' + self.tableName(); } else { return self.dbName(); } }, write: function (value) { const _parts = value.split('.'); this.dbName(_parts[0]); this.tableName(_parts.length > 1 ? _parts[1] : ''); this.columnName(_parts.length > 2 ? _parts[2] : ''); }, owner: self }); self.metastorePath = ko.computed(() => { if (self.columnName().length > 0) { return ( '/metastore/table/' + self.dbName() + '/' + self.tableName() + '#col=' + self.columnName() ); } else if (self.tableName().length > 0) { return '/metastore/table/' + self.dbName() + '/' + self.tableName(); } else if (self.dbName().length > 0) { return '/metastore/tables/' + self.dbName(); } else { return ''; } }); function getPrivilegeScope() { if (self.privilegeType() === 'uri') { return 'URI'; } else if (self.columnName().length > 0) { return 'COLUMN'; } else if (self.tableName().length > 0) { return 'TABLE'; } else if (self.dbName().length > 0) { return 'DATABASE'; } else { return 'SERVER'; } } self.privilegeScope = ko.observable( typeof privilege.privilegeScope != 'undefined' ? privilege.privilegeScope : getPrivilegeScope() ); self.privilegeType.subscribe(() => { self.privilegeScope(getPrivilegeScope()); }); self.columnName.subscribe(() => { self.privilegeScope(getPrivilegeScope()); }); self.tableName.subscribe(() => { self.privilegeScope(getPrivilegeScope()); }); self.dbName.subscribe(() => { self.privilegeScope(getPrivilegeScope()); }); self.remove = function (privilege) { if (privilege.status() === 'new') { privilege.status('alreadydeleted'); } else { privilege.status('deleted'); } }; } } class Role { constructor(vm, role) { const self = this; self.name = ko.observable( typeof role.name != 'undefined' && role.name != null ? role.name : '' ); self.name.subscribe(value => { let _found = false; vm.role().isEditing(false); ko.utils.arrayForEach(vm.roles(), role => { if (role.name() === value) { vm.role(role); _found = true; } }); if (_found) { vm.role().isEditing(true); vm.list_sentry_privileges_by_role(vm.role()); $(document).trigger('destroyTypeahead'); } }); self.selected = ko.observable(false); self.handleSelect = function () { self.selected(!self.selected()); }; self.groups = ko.observableArray(); self.originalGroups = ko.observableArray(); self.groups.extend({ rateLimit: 300 }); self.originalGroups.extend({ rateLimit: 300 }); $.each( typeof role.groups != 'undefined' && role.groups != null ? role.groups : [], (index, group) => { self.groups.push(group); self.originalGroups.push(group); } ); self.privileges = ko.observableArray(); // Not included in the API self.originalPrivileges = ko.observableArray(); // Not included in the API self.privilegesForViewTo = ko.observable(49); self.showPrivileges = ko.observable(false); self.showPrivileges.subscribe(value => { const _expanded = vm.expandedRoles(); if (value) { if (_expanded.indexOf(self.name()) === -1) { _expanded.push(self.name()); } } else if (_expanded.indexOf(self.name()) > -1) { _expanded.splice(_expanded.indexOf(self.name()), 1); } vm.expandedRoles(_expanded); }); self.showEditGroups = ko.observable(false); self.isEditing = ko.observable(false); self.isLoading = ko.observable(false); self.isValid = ko.computed(() => { return ( self.name().length > 0 && $.grep(self.privileges(), privilege => { return privilege.privilegeType() === 'uri' && privilege.URI() === ''; }).length === 0 ); }); self.privilegesChanged = ko.computed(() => { return $.grep(self.privileges(), privilege => { return ['new', 'deleted', 'modified'].indexOf(privilege.status()) !== -1; }); }); self.groupsChanged = ko.computed(() => { return !( $(self.groups()).not(self.originalGroups()).length === 0 && $(self.originalGroups()).not(self.groups()).length === 0 ); }); self.groupsChanged.extend({ rateLimit: 300 }); self.privilegesForView = ko.computed(() => { return self.privileges().slice(0, self.privilegesForViewTo()); }); self.reset = function () { self.name(''); self.groups.removeAll(); self.privileges.removeAll(); self.isEditing(false); }; self.addGroup = function () { self.groups.push(''); }; self.addPrivilege = function () { self.privileges.push( new Privilege(vm, { serverName: vm.server(), status: 'new', editing: true, dbName: vm.db(), tableName: vm.table(), columnName: vm.column() }) ); }; self.resetGroups = function () { self.groups.removeAll(); $.each(self.originalGroups(), (index, group) => { self.groups.push(group); }); }; self.saveGroups = function () { $('.jHueNotify').remove(); $.post( '/security/api/hive/update_role_groups', { role: koMapping.toJSON(self) }, data => { if (data.status === 0) { self.showEditGroups(false); self.originalGroups.removeAll(); $.each(self.groups(), (index, group) => { self.originalGroups.push(group); }); } else { $(document).trigger('error', data.message); } } ).fail(xhr => { $(document).trigger('error', xhr.responseText); }); }; self.create = function () { $('.jHueNotify').remove(); if (self.isValid()) { self.isLoading(true); $.post( '/security/api/hive/create_role', { role: koMapping.toJSON(self) }, data => { if (data.status === 0) { $(document).trigger('info', data.message); vm.showCreateRole(false); self.reset(); const role = new Role(vm, data.role); role.showPrivileges(true); vm.originalRoles.unshift(role); vm.listSentryPrivilegesByAuthorizable(); $(document).trigger('createdRole'); } else { $(document).trigger('error', data.message); } } ) .fail(xhr => { $(document).trigger('error', xhr.responseText); }) .always(() => { self.isLoading(false); }); } }; self.update = function () { $('.jHueNotify').remove(); if (self.isValid()) { self.isLoading(true); $.post( '/security/api/hive/save_privileges', { role: koMapping.toJSON(self) }, data => { if (data.status === 0) { $(document).trigger('info', data.message); vm.showCreateRole(false); vm.listSentryPrivilegesByAuthorizable(); $(document).trigger('createdRole'); } else { $(document).trigger('error', data.message); } } ) .fail(xhr => { $(document).trigger('error', xhr.responseText); }) .always(() => { self.isLoading(false); }); } }; self.remove = function (role) { $('.jHueNotify').remove(); self.isLoading(true); $.post( '/security/api/hive/drop_sentry_role', { roleName: role.name }, data => { if (data.status === 0) { vm.removeRole(role.name()); vm.listSentryPrivilegesByAuthorizable(); $(document).trigger('removedRole'); } else { $(document).trigger('error', data.message); } } ) .fail(xhr => { $(document).trigger('error', xhr.responseText); }) .always(() => { self.isLoading(false); }); }; self.savePrivileges = function (role) { $('.jHueNotify').remove(); $.post( '/security/api/hive/save_privileges', { role: koMapping.toJSON(role) }, data => { if (data.status === 0) { vm.listSentryPrivilegesByAuthorizable(); $(document).trigger('createdRole'); } else { $(document).trigger('error', data.message); } } ).fail(xhr => { $(document).trigger('error', xhr.responseText); }); }; } } const _create_authorizable_from_ko = (server, path) => { if (path != null) { const paths = path.split(/[.]/); return { server: server, db: paths[0] ? paths[0] : null, table: paths[1] ? paths[1] : null, column: paths[2] ? paths[2] : null }; } }; const _create_ko_privilege = privilege => new Privilege(self, { privilegeScope: privilege.scope, serverName: privilege.server, dbName: privilege.database, tableName: privilege.table, columnName: privilege.column, URI: privilege.URI, action: privilege.action, timestamp: privilege.timestamp, roleName: privilege.roleName, grantOption: privilege.grantOption, id: UUID() }); class SentryPrivilegesViewModel { constructor(params) { const self = this; self.server = ko.observable(params.server); self.path = ko.observable(params.path); self.isSentryAdmin = params.isSentryAdmin; self.readOnly = !!params.readOnly; self.apiVersion = params.apiVersion || 'v1'; self.apiType = params.apiType || 'hive'; self.showFilter = params.showFilter; self.enableCreateRole = params.enableCreateRole; self.path.subscribe(self.listSentryPrivilegesByAuthorizable); self.db = ko.computed(() => { const db = self.path().split(/[.]/)[0]; return db ? db : null; }); self.table = ko.computed(() => { const table = self.path().split(/[.]/)[1]; return table ? table : null; }); self.column = ko.computed(() => { const column = self.path().split(/[.]/)[2]; return column ? column : null; }); self.metastorePath = ko.computed(() => { if (self.column()) { return '/metastore/table/' + self.db() + '/' + self.table() + '#col=' + self.column(); } else if (self.table()) { return '/metastore/table/' + self.db() + '/' + self.table(); } else if (self.db()) { return '/metastore/tables/' + self.db(); } else { return '/metastore/databases'; } }); self.privileges = ko.observableArray(); self.roles = ko.observableArray(); self.roleToUpdate = ko.observable(); self.isLoadingPrivileges = ko.observable(false); self.availableActions = function (scope) { let actions = ['SELECT', 'INSERT', 'ALL']; const databaseActions = ['CREATE']; const tableActions = ['REFRESH', 'ALTER', 'DROP']; switch (scope) { case 'SERVER': actions = actions.concat(databaseActions).concat(tableActions); break; case 'DATABASE': actions = actions.concat(databaseActions); break; case 'TABLE': actions = actions.concat(tableActions); break; } return ko.observableArray(actions.sort()); }; self.deletePrivilegeModal = function (role) { const cascadeDeletes = role .privilegesChanged() .filter( privilege => privilege.status() === 'deleted' && (privilege.privilegeScope() === 'SERVER' || privilege.privilegeScope() === 'DATABASE') ); if (cascadeDeletes.length > 0) { self.roleToUpdate(role); $('#sentryPrivilegesComponentDeletePrivilegeModal').modal('show'); } else { role.savePrivileges(role); } }; self.listSentryPrivilegesByAuthorizable = function () { self.isLoadingPrivileges(true); $.ajax({ type: 'POST', url: '/security/api/hive/list_sentry_privileges_by_authorizable', data: { groupName: '', roleSet: koMapping.toJSON({ all: true, roles: [] }), authorizableHierarchy: koMapping.toJSON( _create_authorizable_from_ko(self.server(), self.path()) ) }, success: function (data) { if (data.status === 0) { self.roles([]); const _privileges = []; $.each(data.privileges, (index, item) => { if (typeof window.skipList === 'undefined' || window.skipList === false) { let _role = null; self.roles().forEach(role => { if (role.name() === item.roleName) { _role = role; } }); if (_role == null) { const _idx = self.roles.push(new Role(self, { name: item.roleName })); _role = self.roles()[_idx - 1]; } const privilege = _create_ko_privilege(item); const privilegeCopy = _create_ko_privilege(item); privilegeCopy.id(privilege.id()); _role.privileges.push(privilege); _role.originalPrivileges.push(privilegeCopy); _privileges.push(privilege); } }); if (typeof window.skipList === 'undefined' || window.skipList === false) { self.privileges(_privileges); } } else { $(document).trigger('error', data.message); } } }) .fail(xhr => { $(document).trigger('error', xhr.responseText); }) .always(() => { self.isLoadingPrivileges(false); }); }; self.linkToBrowse = function (path) { // TODO: implement smart linking }; self.listSentryPrivilegesByAuthorizable(); } } componentUtils.registerComponent(NAME, SentryPrivilegesViewModel, TEMPLATE);