gethue
Version:
Hue is an Open source SQL Query Editor for Databases/Warehouses
772 lines (711 loc) • 27.2 kB
JavaScript
// 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'
)}"> <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'
)}"> <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">×</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);