Initial work for Manage Access dialog (#7483)

* Add width and height properties to checkbox component

* Initial work on manage access dialog

* Add missed change

* Add comments and clean up

* Initial work on manage access dialog

* Add missed change

* Add comments and clean up

* Add return type

* Address comments and use apiWrapper

* Fix compile error
This commit is contained in:
Charles Gagnon
2019-10-03 08:58:06 -07:00
committed by GitHub
parent 6582debd73
commit af24a9d002
13 changed files with 567 additions and 29 deletions

View File

@@ -30,8 +30,8 @@ export class ApiWrapper {
return azdata.dataprotocol.registerFileBrowserProvider(provider);
}
public createDialog(title: string): azdata.window.Dialog {
return azdata.window.createModelViewDialog(title);
public createDialog(title: string, dialogName?: string, isWide?: boolean): azdata.window.Dialog {
return azdata.window.createModelViewDialog(title, dialogName, isWide);
}
public openDialog(dialog: azdata.window.Dialog): void {

View File

@@ -101,7 +101,7 @@ export class AclEntryPermission {
* e.g. The following are all valid strings
* rwx
* ---
* -w-
* -w-
* @param permissionString The string representation of the permission
*/
function parseAclPermission(permissionString: string): AclEntryPermission {
@@ -113,10 +113,11 @@ function parseAclPermission(permissionString: string): AclEntryPermission {
}
/**
* A single ACL Entry. This consists of up to 4 values
* scope - The scope of the entry @see AclEntryScope
* type - The type of the entry @see AclEntryType
* name - The name of the user/group. Optional.
* A single ACL Permission entry
* scope - The scope of the entry @see AclEntryScope
* type - The type of the entry @see AclEntryType
* name - The name of the user/group used to set ACLs Optional.
* displayName - The name to display in the UI
* permission - The permission set for this ACL. @see AclPermission
*/
export class AclEntry {
@@ -124,6 +125,7 @@ export class AclEntry {
public readonly scope: AclEntryScope,
public readonly type: AclEntryType | AclPermissionType,
public readonly name: string,
public readonly displayName: string,
public readonly permission: AclEntryPermission,
) { }
@@ -135,12 +137,48 @@ export class AclEntry {
* Example strings :
* user:bob:rwx
* default:user:bob:rwx
* user::r-x
* default:group::r--
* user::r-x
* default:group::r--
*/
toString(): string {
return `${this.scope === AclEntryScope.default ? 'default:' : ''}${this.type}:${this.name}:${this.permission.toString()}`;
toAclString(): string {
return `${this.scope === AclEntryScope.default ? 'default:' : ''}${getAclEntryType(this.type)}:${this.name}:${this.permission.toString()}`;
}
/**
* Checks whether this and the specified AclEntry are equal. Two entries are considered equal
* if their scope, type and name are equal.
* @param other The other entry to compare against
*/
public isEqual(other: AclEntry): boolean {
if (!other) {
return false;
}
return this.scope === other.scope &&
this.type === other.type &&
this.name === other.name;
}
}
/**
* Maps the possible entry types into their corresponding values for using in an ACL string
* @param type The type to convert
*/
function getAclEntryType(type: AclEntryType | AclPermissionType): AclEntryType {
// We only need to map AclPermissionType - AclEntryType is already the
// correct values we're mapping to.
if (type in AclPermissionType) {
switch (type) {
case AclPermissionType.owner:
return AclEntryType.user;
case AclPermissionType.group:
return AclEntryType.group;
case AclPermissionType.other:
return AclEntryType.other;
default:
throw new Error(`Unknown AclPermissionType : ${type}`);
}
}
return <AclEntryType>type;
}
/**
@@ -156,7 +194,7 @@ export class AclEntry {
* default:other:r--
*
* So a valid ACL string might look like this
* user:bob:rwx,user::rwx,default::bob:rwx,group::r-x,default:other:r--
* user:bob:rwx,user::rwx,default::bob:rwx,group::r-x,default:other:r--
* @param aclString The string representation of the ACL
*/
export function parseAcl(aclString: string): AclEntry[] {
@@ -194,7 +232,7 @@ export function parseAclEntry(aclString: string): AclEntry {
}
const name = parts[i++];
const permission = parseAclPermission(parts[i++]);
return new AclEntry(scope, type, name, permission);
return new AclEntry(scope, type, name, name, permission);
}
/**
@@ -208,7 +246,7 @@ export function parseAclEntry(aclString: string): AclEntry {
* So an octal of 730 would map to :
* - The owner with rwx permissions
* - The group with -wx permissions
* - All others with --- permissions
* - All others with --- permissions
* @param octal The octal string to parse
*/
export function parseAclPermissionFromOctal(octal: string): { owner: AclEntryPermission, group: AclEntryPermission, other: AclEntryPermission } {

View File

@@ -0,0 +1,77 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as vscode from 'vscode';
import { IFileSource } from '../objectExplorerNodeProvider/fileSources';
import { IAclStatus, AclEntry, AclEntryScope, AclEntryType, AclEntryPermission } from './aclEntry';
/**
* Model for storing the state of a specified file/folder in HDFS
*/
export class HdfsModel {
private readonly _onAclStatusUpdated = new vscode.EventEmitter<IAclStatus>();
/**
* Event that's fired anytime changes are made by the model to the ACLStatus
*/
public onAclStatusUpdated = this._onAclStatusUpdated.event;
/**
* The ACL status of the file/folder
*/
public aclStatus: IAclStatus;
constructor(private fileSource: IFileSource, private path: string) {
this.refresh();
}
/**
* Refresh the ACL status with the current values on HDFS
*/
public async refresh(): Promise<void> {
this.aclStatus = await this.fileSource.getAclStatus(this.path);
this._onAclStatusUpdated.fire(this.aclStatus);
}
/**
* Creates a new ACL Entry and adds it to the list of current entries. Will do nothing
* if a duplicate entry (@see AclEntry.isEqual) exists
* @param name The name of the ACL Entry
* @param type The type of ACL to create
*/
public createAndAddAclEntry(name: string, type: AclEntryType): void {
if (!this.aclStatus) {
return;
}
const newEntry = new AclEntry(AclEntryScope.access, type, name, name, new AclEntryPermission(true, true, true));
// Don't add duplicates. This also checks the owner, group and other items
if ([this.aclStatus.owner, this.aclStatus.group, this.aclStatus.other].concat(this.aclStatus.entries).find(entry => entry.isEqual(newEntry))) {
return;
}
this.aclStatus.entries.push(newEntry);
this._onAclStatusUpdated.fire(this.aclStatus);
}
/**
* Deletes the specified entry from the list of registered
* @param entryToDelete The entry to delete
*/
public deleteAclEntry(entryToDelete: AclEntry): void {
this.aclStatus.entries = this.aclStatus.entries.filter(entry => !entry.isEqual(entryToDelete));
this._onAclStatusUpdated.fire(this.aclStatus);
}
/**
* Applies the changes made to this model to HDFS. Note that this will overwrite ALL permissions so any
* permissions that shouldn't change need to still exist and have the same values.
* @param recursive Whether to apply the changes recursively (to all sub-folders and files)
*/
public apply(recursive: boolean = false): Promise<void> {
// TODO Apply recursive
return this.fileSource.setAcl(this.path, this.aclStatus.owner, this.aclStatus.group, this.aclStatus.other, this.aclStatus.entries);
}
}

View File

@@ -0,0 +1,337 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as azdata from 'azdata';
import { HdfsModel } from '../hdfsModel';
import { IFileSource } from '../../objectExplorerNodeProvider/fileSources';
import { IAclStatus, AclEntry, AclEntryType } from '../../hdfs/aclEntry';
import { cssStyles } from './uiConstants';
import { ownersHeader, permissionsHeader, stickyHeader, readHeader, writeHeader, executeHeader, manageAccessTitle, allOthersHeader, addUserOrGroupHeader, enterNamePlaceholder, addLabel, accessHeader, defaultHeader, applyText, applyRecursivelyText, userLabel, groupLabel, errorApplyingAclChanges } from '../../localizedConstants';
import { HdfsError } from '../webhdfs';
import { ApiWrapper } from '../../apiWrapper';
const permissionsNameColumnWidth = 250;
const permissionsStickyColumnWidth = 50;
const permissionsReadColumnWidth = 50;
const permissionsWriteColumnWidth = 50;
const permissionsExecuteColumnWidth = 50;
const permissionsDeleteColumnWidth = 50;
const permissionsRowHeight = 75;
export class ManageAccessDialog {
private hdfsModel: HdfsModel;
private viewInitialized: boolean = false;
private modelBuilder: azdata.ModelBuilder;
private rootLoadingComponent: azdata.LoadingComponent;
private ownersPermissionsContainer: azdata.FlexContainer;
private othersPermissionsContainer: azdata.FlexContainer;
private namedUsersAndGroupsPermissionsContainer: azdata.FlexContainer;
private addUserOrGroupInput: azdata.InputBoxComponent;
private dialog: azdata.window.Dialog;
private addUserOrGroupSelectedType: AclEntryType;
constructor(private hdfsPath: string, private fileSource: IFileSource, private readonly apiWrapper: ApiWrapper) {
this.hdfsModel = new HdfsModel(this.fileSource, this.hdfsPath);
this.hdfsModel.onAclStatusUpdated(aclStatus => this.handleAclStatusUpdated(aclStatus));
}
public openDialog(): void {
if (!this.dialog) {
this.dialog = this.apiWrapper.createDialog(manageAccessTitle, 'HdfsManageAccess', true);
this.dialog.okButton.label = applyText;
const applyRecursivelyButton = azdata.window.createButton(applyRecursivelyText);
applyRecursivelyButton.onClick(async () => {
try {
await this.hdfsModel.apply(true);
} catch (err) {
this.apiWrapper.showErrorMessage(errorApplyingAclChanges(err instanceof HdfsError ? err.message : err));
}
});
this.dialog.customButtons = [applyRecursivelyButton];
this.dialog.registerCloseValidator(async (): Promise<boolean> => {
try {
await this.hdfsModel.apply();
return true;
} catch (err) {
this.apiWrapper.showErrorMessage(errorApplyingAclChanges(err instanceof HdfsError ? err.message : err));
}
return false;
});
const tab = azdata.window.createTab(manageAccessTitle);
tab.registerContent(async (modelView: azdata.ModelView) => {
this.modelBuilder = modelView.modelBuilder;
const rootContainer = modelView.modelBuilder.flexContainer()
.withLayout({ flexFlow: 'column', width: '100%', height: '100%' })
.component();
this.rootLoadingComponent = modelView.modelBuilder.loadingComponent().withItem(rootContainer).component();
const pathLabel = modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: this.hdfsPath }).component();
rootContainer.addItem(pathLabel, { flex: '0 0 auto' });
// =====================
// = Permissions Title =
// =====================
const permissionsTitle = modelView.modelBuilder.text()
.withProperties<azdata.TextComponentProperties>({ value: permissionsHeader, CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '10px' } })
.component();
rootContainer.addItem(permissionsTitle, { CSSStyles: { 'margin-top': '15px', 'padding-left': '10px', ...cssStyles.titleCss } });
// ==============================
// = Owners permissions section =
// ==============================
const ownersPermissionsHeaderRow = createPermissionsHeaderRow(modelView.modelBuilder, ownersHeader, true);
rootContainer.addItem(ownersPermissionsHeaderRow, { CSSStyles: { 'padding-left': '10px', 'box-sizing': 'border-box', 'user-select': 'text' } });
// Empty initially - this is going to eventually be populated with the owner/owning group permissions
this.ownersPermissionsContainer = modelView.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
rootContainer.addItem(this.ownersPermissionsContainer);
// ==============================
// = Others permissions section =
// ==============================
const othersPermissionsHeaderRow = modelView.modelBuilder.flexContainer().withLayout({ flexFlow: 'row' }).component();
const ownersHeaderCell = modelView.modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: allOthersHeader }).component();
othersPermissionsHeaderRow.addItem(ownersHeaderCell, { flex: '1 1 auto', CSSStyles: { 'width': `${permissionsNameColumnWidth}px`, 'min-width': `${permissionsNameColumnWidth}px`, ...cssStyles.tableHeaderCss } });
rootContainer.addItem(othersPermissionsHeaderRow, { CSSStyles: { 'padding-left': '10px', 'box-sizing': 'border-box', 'user-select': 'text' } });
// Empty initially - this is eventually going to be populated with the "Everyone" permissions
this.othersPermissionsContainer = modelView.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
rootContainer.addItem(this.othersPermissionsContainer);
// ===========================
// = Add User Or Group Input =
// ===========================
const addUserOrGroupTitle = modelView.modelBuilder.text()
.withProperties<azdata.TextComponentProperties>({ value: addUserOrGroupHeader, CSSStyles: { 'margin-block-start': '0px', 'margin-block-end': '10px' } })
.component();
rootContainer.addItem(addUserOrGroupTitle, { CSSStyles: { 'margin-top': '15px', 'padding-left': '10px', ...cssStyles.titleCss } });
const typeContainer = modelView.modelBuilder.flexContainer().withProperties({ flexFlow: 'row' }).component();
const aclEntryTypeGroup = 'aclEntryType';
const userTypeButton = this.createRadioButton(modelView.modelBuilder, userLabel, aclEntryTypeGroup, AclEntryType.user);
const groupTypeButton = this.createRadioButton(modelView.modelBuilder, groupLabel, aclEntryTypeGroup, AclEntryType.group);
userTypeButton.checked = true;
this.addUserOrGroupSelectedType = AclEntryType.user;
typeContainer.addItems([userTypeButton, groupTypeButton], { flex: '0 0 auto' });
rootContainer.addItem(typeContainer, { flex: '0 0 auto' });
const addUserOrGroupInputRow = modelView.modelBuilder.flexContainer().withLayout({ flexFlow: 'row' }).component();
this.addUserOrGroupInput = modelView.modelBuilder.inputBox().withProperties<azdata.InputBoxProperties>({ inputType: 'text', placeHolder: enterNamePlaceholder, width: 250 }).component();
const addUserOrGroupButton = modelView.modelBuilder.button().withProperties<azdata.ButtonProperties>({ label: addLabel, width: 75 }).component();
addUserOrGroupButton.onDidClick(() => {
this.hdfsModel.createAndAddAclEntry(this.addUserOrGroupInput.value, this.addUserOrGroupSelectedType);
this.addUserOrGroupInput.value = '';
});
addUserOrGroupButton.enabled = false; // Init to disabled since we don't have any name entered in yet
this.addUserOrGroupInput.onTextChanged(() => {
if (this.addUserOrGroupInput.value === '') {
addUserOrGroupButton.enabled = false;
} else {
addUserOrGroupButton.enabled = true;
}
});
addUserOrGroupInputRow.addItem(this.addUserOrGroupInput, { flex: '0 0 auto' });
addUserOrGroupInputRow.addItem(addUserOrGroupButton, { flex: '0 0 auto', CSSStyles: { 'margin-left': '20px' } });
rootContainer.addItem(addUserOrGroupInputRow);
// =================================================
// = Named Users and Groups permissions header row =
// =================================================
const namedUsersAndGroupsPermissionsHeaderRow = createPermissionsHeaderRow(modelView.modelBuilder, ownersHeader, false);
rootContainer.addItem(namedUsersAndGroupsPermissionsHeaderRow, { CSSStyles: { 'padding-left': '10px', 'box-sizing': 'border-box', 'user-select': 'text' } });
// Empty initially - this is eventually going to be populated with the ACL entries set for this path
this.namedUsersAndGroupsPermissionsContainer = modelView.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
rootContainer.addItem(this.namedUsersAndGroupsPermissionsContainer);
this.viewInitialized = true;
this.handleAclStatusUpdated(this.hdfsModel.aclStatus);
await modelView.initializeModel(this.rootLoadingComponent);
});
this.dialog.content = [tab];
}
azdata.window.openDialog(this.dialog);
}
private handleAclStatusUpdated(aclStatus: IAclStatus): void {
if (!aclStatus || !this.viewInitialized) {
return;
}
// Owners
const ownerPermissionsRow = this.createOwnerPermissionsRow(this.modelBuilder, aclStatus.stickyBit, aclStatus.owner);
const owningGroupPermissionsRow = this.createPermissionsRow(this.modelBuilder, aclStatus.group, false);
this.ownersPermissionsContainer.clearItems();
this.ownersPermissionsContainer.addItems([ownerPermissionsRow, owningGroupPermissionsRow], { CSSStyles: { 'border-bottom': cssStyles.tableBorder, 'border-top': cssStyles.tableBorder } });
// Others
const otherPermissionsRow = this.createPermissionsRow(this.modelBuilder, aclStatus.other, false);
this.othersPermissionsContainer.clearItems();
this.othersPermissionsContainer.addItem(otherPermissionsRow, { CSSStyles: { 'border-bottom': cssStyles.tableBorder, 'border-top': cssStyles.tableBorder } });
this.namedUsersAndGroupsPermissionsContainer.clearItems();
// Named users and groups
aclStatus.entries.forEach(entry => {
const namedEntryRow = this.createPermissionsRow(this.modelBuilder, entry, true);
this.namedUsersAndGroupsPermissionsContainer.addItem(namedEntryRow, { CSSStyles: { 'border-bottom': cssStyles.tableBorder, 'border-top': cssStyles.tableBorder } });
});
this.rootLoadingComponent.loading = false;
}
private createRadioButton(modelBuilder: azdata.ModelBuilder, label: string, name: string, aclEntryType: AclEntryType): azdata.RadioButtonComponent {
const button = modelBuilder.radioButton().withProperties<azdata.RadioButtonProperties>({ label: label, name: name }).component();
button.onDidClick(() => {
this.addUserOrGroupSelectedType = aclEntryType;
});
return button;
}
private createOwnerPermissionsRow(builder: azdata.ModelBuilder, sticky: boolean, entry: AclEntry): azdata.FlexContainer {
const row = this.createPermissionsRow(builder, entry, false);
const stickyCheckbox = builder.checkBox().withProperties({ checked: sticky, height: 25, width: 25 }).component();
const stickyContainer = builder.flexContainer().withLayout({ width: permissionsReadColumnWidth }).withItems([stickyCheckbox]).component();
// Insert after name item but before other checkboxes
row.insertItem(stickyContainer, 1, { flex: '0 0 auto' });
return row;
}
private createPermissionsRow(builder: azdata.ModelBuilder, entry: AclEntry, includeDelete: boolean): azdata.FlexContainer {
const rowContainer = builder.flexContainer().withLayout({ flexFlow: 'row', height: permissionsRowHeight }).component();
const nameCell = builder.text().withProperties({ value: entry.displayName }).component();
rowContainer.addItem(nameCell);
// Access - Read
const accessReadComponents = createCheckbox(builder, entry.permission.read, permissionsReadColumnWidth, permissionsRowHeight);
rowContainer.addItem(accessReadComponents.container, { flex: '0 0 auto' });
accessReadComponents.checkbox.onChanged(() => {
entry.permission.read = accessReadComponents.checkbox.checked;
});
// Access - Write
const accessWriteComponents = createCheckbox(builder, entry.permission.write, permissionsWriteColumnWidth, permissionsRowHeight);
rowContainer.addItem(accessWriteComponents.container, { flex: '0 0 auto' });
accessWriteComponents.checkbox.onChanged(() => {
entry.permission.write = accessWriteComponents.checkbox.checked;
});
// Access - Execute
const accessExecuteComponents = createCheckbox(builder, entry.permission.execute, permissionsExecuteColumnWidth, permissionsRowHeight);
rowContainer.addItem(accessExecuteComponents.container, { flex: '0 0 auto', CSSStyles: { 'border-right': cssStyles.tableBorder } });
accessExecuteComponents.checkbox.onChanged(() => {
entry.permission.execute = accessExecuteComponents.checkbox.checked;
});
// Default - Read
const defaultReadComponents = createCheckbox(builder, false, permissionsReadColumnWidth, permissionsRowHeight);
rowContainer.addItem(defaultReadComponents.container, { flex: '0 0 auto' });
defaultReadComponents.checkbox.onChanged(() => {
// entry.permission.read = defaultReadComponents.checkbox.checked; TODO hook up default logic
});
// Default - Write
const defaultWriteComponents = createCheckbox(builder, false, permissionsWriteColumnWidth, permissionsRowHeight);
rowContainer.addItem(defaultWriteComponents.container, { flex: '0 0 auto' });
accessReadComponents.checkbox.onChanged(() => {
// entry.permission.write = accessReadComponents.checkbox.checked; TODO hook up default logic
});
// Default - Execute
const defaultExecuteComponents = createCheckbox(builder, false, permissionsExecuteColumnWidth, permissionsRowHeight);
rowContainer.addItem(defaultExecuteComponents.container, { flex: '0 0 auto' });
accessReadComponents.checkbox.onChanged(() => {
// entry.permission.execute = accessReadComponents.checkbox.checked; TODO hook up default logic
});
const deleteContainer = builder.flexContainer().withLayout({ width: permissionsDeleteColumnWidth }).component();
if (includeDelete) {
const deleteButton = builder.button().withProperties<azdata.ButtonProperties>({ label: 'Delete' }).component();
deleteButton.onDidClick(() => { this.hdfsModel.deleteAclEntry(entry); });
deleteContainer.addItem(deleteButton, { flex: '0 0 auto' });
}
rowContainer.addItem(deleteContainer, { flex: '0 0 auto' });
return rowContainer;
}
}
/**
* Creates the header row for the permissions tables. This contains headers for the name, optional sticky and then read/write/execute for both
* access and default sections.
* @param modelBuilder The builder used to create the model components
* @param nameColumnText The text to display for the name column
* @param includeSticky Whether to include the sticky header
*/
function createPermissionsHeaderRow(modelBuilder: azdata.ModelBuilder, nameColumnText: string, includeSticky: boolean): azdata.FlexContainer {
const rowsContainer = modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component();
// Section Headers
const sectionHeaderContainer = modelBuilder.flexContainer().withLayout({ flexFlow: 'row', justifyContent: 'flex-end' }).component();
const accessSectionHeader = modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: accessHeader }).component();
sectionHeaderContainer.addItem(accessSectionHeader, { CSSStyles: { 'width': `${permissionsReadColumnWidth + permissionsWriteColumnWidth + permissionsExecuteColumnWidth}px`, 'min-width': `${permissionsReadColumnWidth + permissionsWriteColumnWidth + permissionsExecuteColumnWidth}px`, ...cssStyles.tableHeaderCss } });
const defaultSectionHeader = modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: defaultHeader }).component();
sectionHeaderContainer.addItem(defaultSectionHeader, { CSSStyles: { 'width': `${permissionsReadColumnWidth + permissionsWriteColumnWidth + permissionsExecuteColumnWidth}px`, 'min-width': `${permissionsReadColumnWidth + permissionsWriteColumnWidth + permissionsExecuteColumnWidth}px`, ...cssStyles.tableHeaderCss } });
rowsContainer.addItem(sectionHeaderContainer);
// Table headers
const headerRowContainer = modelBuilder.flexContainer().withLayout({ flexFlow: 'row' }).component();
const ownersCell = modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: nameColumnText }).component();
headerRowContainer.addItem(ownersCell, { flex: '1 1 auto', CSSStyles: { 'width': `${permissionsNameColumnWidth}px`, 'min-width': `${permissionsNameColumnWidth}px`, ...cssStyles.tableHeaderCss } });
if (includeSticky) {
const stickyCell = modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: stickyHeader }).component();
headerRowContainer.addItem(stickyCell, { CSSStyles: { 'width': `${permissionsStickyColumnWidth}px`, 'min-width': `${permissionsStickyColumnWidth}px`, ...cssStyles.tableHeaderCss } });
}
// Access Permissions Group
const accessReadCell = modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: readHeader }).component();
headerRowContainer.addItem(accessReadCell, { CSSStyles: { 'width': `${permissionsReadColumnWidth}px`, 'min-width': `${permissionsReadColumnWidth}px`, ...cssStyles.tableHeaderCss } });
const accessWriteCell = modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: writeHeader }).component();
headerRowContainer.addItem(accessWriteCell, { CSSStyles: { 'width': `${permissionsWriteColumnWidth}px`, 'min-width': `${permissionsWriteColumnWidth}px`, ...cssStyles.tableHeaderCss } });
const accessExecuteCell = modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: executeHeader }).component();
headerRowContainer.addItem(accessExecuteCell, { CSSStyles: { 'width': `${permissionsExecuteColumnWidth}px`, 'min-width': `${permissionsExecuteColumnWidth}px`, ...cssStyles.tableHeaderCss } });
// Default Permissions Group
const defaultReadCell = modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: readHeader }).component();
headerRowContainer.addItem(defaultReadCell, { CSSStyles: { 'width': `${permissionsReadColumnWidth}px`, 'min-width': `${permissionsReadColumnWidth}px`, ...cssStyles.tableHeaderCss } });
const defaultWriteCell = modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: writeHeader }).component();
headerRowContainer.addItem(defaultWriteCell, { CSSStyles: { 'width': `${permissionsWriteColumnWidth}px`, 'min-width': `${permissionsWriteColumnWidth}px`, ...cssStyles.tableHeaderCss } });
const defaultExecuteCell = modelBuilder.text().withProperties<azdata.TextComponentProperties>({ value: executeHeader }).component();
headerRowContainer.addItem(defaultExecuteCell, { CSSStyles: { 'width': `${permissionsExecuteColumnWidth}px`, 'min-width': `${permissionsExecuteColumnWidth}px`, ...cssStyles.tableHeaderCss } });
// Delete
const deleteCell = modelBuilder.text().component();
headerRowContainer.addItem(deleteCell, { CSSStyles: { 'width': `${permissionsDeleteColumnWidth}px`, 'min-width': `${permissionsDeleteColumnWidth}px` } });
rowsContainer.addItem(headerRowContainer);
return rowsContainer;
}
function createCheckbox(builder: azdata.ModelBuilder, checked: boolean, containerWidth: number, containerHeight: number): { container: azdata.FlexContainer, checkbox: azdata.CheckBoxComponent } {
const checkbox = builder.checkBox().withProperties({ checked: checked, height: 25, width: 25 }).component();
return {
container: builder.flexContainer()
.withLayout({ width: containerWidth, height: containerHeight })
.withItems([checkbox])
.component(),
checkbox: checkbox
};
}

View File

@@ -0,0 +1,10 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
export namespace cssStyles {
export const tableBorder = '1px solid #ccc';
export const titleCss = { 'font-size': '20px', 'font-weight': '600' };
export const tableHeaderCss = { 'font-weight': 'bold', 'text-transform': 'uppercase', 'font-size': '10px', 'user-select': 'text' };
}

View File

@@ -12,6 +12,7 @@ import * as nls from 'vscode-nls';
import * as auth from '../util/auth';
import { IHdfsOptions, IRequestParams } from '../objectExplorerNodeProvider/fileSources';
import { IAclStatus, AclEntry, parseAcl, AclPermissionType, parseAclPermissionFromOctal, AclEntryScope } from './aclEntry';
import { everyoneName } from '../localizedConstants';
const localize = nls.loadMessageBundle();
const ErrorMessageInvalidDataStructure = localize('webhdfs.invalidDataStructure', "Invalid Data Structure");
@@ -435,11 +436,11 @@ export class WebHDFS {
} else if (response.body.hasOwnProperty('AclStatus')) {
const permissions = parseAclPermissionFromOctal(response.body.AclStatus.permission);
const aclStatus: IAclStatus = {
owner: new AclEntry(AclEntryScope.access, AclPermissionType.owner, response.body.AclStatus.owner || '', permissions.owner),
group: new AclEntry(AclEntryScope.access, AclPermissionType.group, response.body.AclStatus.group || '', permissions.group),
other: new AclEntry(AclEntryScope.access, AclPermissionType.other, response.body.AclStatus.other || '', permissions.other),
owner: new AclEntry(AclEntryScope.access, AclPermissionType.owner, '', response.body.AclStatus.owner || '', permissions.owner),
group: new AclEntry(AclEntryScope.access, AclPermissionType.group, '', response.body.AclStatus.group || '', permissions.group),
other: new AclEntry(AclEntryScope.access, AclPermissionType.other, '', everyoneName, permissions.other),
stickyBit: !!response.body.AclStatus.stickyBit,
entries: (<any[]>response.body.AclStatus.entries).map(entry => parseAcl(entry)).reduce((acc, parsedEntries) => acc.concat(parsedEntries, []))
entries: (<any[]>response.body.AclStatus.entries).map(entry => parseAcl(entry)).reduce((acc, parsedEntries) => acc.concat(parsedEntries), [])
};
callback(undefined, aclStatus);
} else {
@@ -449,16 +450,22 @@ export class WebHDFS {
}
/**
* Set ACL for the given path
* Set ACL for the given path. The owner, group and other fields are required - other entries are optional.
* @param path The path to the file/folder to set the ACL on
* @param aclEntries The ACL entries to set
* @param ownerEntry The entry corresponding to the path owner
* @param groupEntry The entry corresponding to the path owning group
* @param otherEntry The entry corresponding to default permissions for all other users
* @param aclEntries The optional additional ACL entries to set
* @param callback Callback to handle the response
* @returns void
*/
public setAcl(path: string, aclEntries: AclEntry[], callback: (error: HdfsError) => void): void {
public setAcl(path: string, ownerEntry: AclEntry, groupEntry: AclEntry, otherEntry: AclEntry, aclEntries: AclEntry[], callback: (error: HdfsError) => void): void {
this.checkArgDefined('path', path);
this.checkArgDefined('ownerEntry', ownerEntry);
this.checkArgDefined('groupEntry', groupEntry);
this.checkArgDefined('otherEntry', otherEntry);
this.checkArgDefined('aclEntries', aclEntries);
const aclSpec = aclEntries.join(',');
const aclSpec = [ownerEntry, groupEntry, otherEntry].concat(aclEntries).map(entry => entry.toAclString()).join(',');
let endpoint = this.getOperationEndpoint('setacl', path, { aclspec: aclSpec });
this.sendRequest('PUT', endpoint, undefined, (error) => {
return callback && callback(error);

View File

@@ -3,14 +3,36 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as nls from 'vscode-nls';
const localize = nls.loadMessageBundle();
// HDFS Constants //////////////////////////////////////////////////////////
export const msgMissingNodeContext = localize('msgMissingNodeContext', 'Node Command called without any node passed');
// HDFS Manage Access Dialog Constants ////////////////////////////////////
export const manageAccessTitle = localize('mssql.manageAccessTitle', "Manage Access");
export const permissionsHeader = localize('mssql.permissionsTitle', "Permissions");
export const ownersHeader = localize('mssql.ownersHeader', "Owners");
export const allOthersHeader = localize('mssql.allOthersHeader', "All Others");
export const everyoneName = localize('mssql.everyone', "Everyone");
export const userLabel = localize('mssql.userLabel', "User");
export const groupLabel = localize('mssql.groupLabel', "Group");
export const accessHeader = localize('mssql.accessHeader', "Access");
export const defaultHeader = localize('mssql.defaultHeader', "Default");
export const stickyHeader = localize('mssql.stickyHeader', "Sticky");
export const readHeader = localize('mssql.readHeader', "Read");
export const writeHeader = localize('mssql.writeHeader', "Write");
export const executeHeader = localize('mssql.executeHeader', "Execute");
export const addUserOrGroupHeader = localize('mssql.addUserOrGroup', "Add User or Group");
export const enterNamePlaceholder = localize('mssql.enterNamePlaceholder', "Enter name");
export const addLabel = localize('mssql.addLabel', "Add");
export const namedUsersAndGroupsHeader = localize('mssql.namedUsersAndGroups', "Named Users and Groups");
export const applyText = localize('mssql.apply', "Apply");
export const applyRecursivelyText = localize('mssql.applyRecursively', "Apply Recursively");
export function errorApplyingAclChanges(errMsg: string): string { return localize('mssql.errorApplyingAclChanges', "Unexpected error occurred while applying changes : {0}", errMsg); }
// Spark Job Submission Constants //////////////////////////////////////////
export const sparkLocalFileDestinationHint = localize('sparkJobSubmission_LocalFileDestinationHint', 'Local file will be uploaded to HDFS. ');
export const sparkJobSubmissionEndMessage = localize('sparkJobSubmission_SubmissionEndMessage', '.......................... Submit Spark Job End ............................');

View File

@@ -13,7 +13,7 @@ import ContextProvider from './contextProvider';
import * as Utils from './utils';
import { AppContext } from './appContext';
import { ApiWrapper } from './apiWrapper';
import { UploadFilesCommand, MkDirCommand, SaveFileCommand, PreviewFileCommand, CopyPathCommand, DeleteFilesCommand } from './objectExplorerNodeProvider/hdfsCommands';
import { UploadFilesCommand, MkDirCommand, SaveFileCommand, PreviewFileCommand, CopyPathCommand, DeleteFilesCommand, ManageAccessCommand } from './objectExplorerNodeProvider/hdfsCommands';
import { IPrompter } from './prompts/question';
import CodeAdapter from './prompts/adapter';
import { IExtension } from './mssql';
@@ -99,6 +99,7 @@ function registerHdfsCommands(context: vscode.ExtensionContext, prompter: IPromp
context.subscriptions.push(new PreviewFileCommand(prompter, appContext));
context.subscriptions.push(new CopyPathCommand(appContext));
context.subscriptions.push(new DeleteFilesCommand(prompter, appContext));
context.subscriptions.push(new ManageAccessCommand(appContext));
}
function activateSparkFeatures(appContext: AppContext): void {

View File

@@ -65,8 +65,20 @@ export interface IFileSource {
readFileLines(path: string, maxLines: number): Promise<Buffer>;
writeFile(localFile: IFile, remoteDir: string): Promise<string>;
delete(path: string, recursive?: boolean): Promise<void>;
/**
* Get ACL status for given path
* @param path The path to the file/folder to get the status of
*/
getAclStatus(path: string): Promise<IAclStatus>;
setAcl(path: string, aclEntries: AclEntry[]): Promise<void>;
/**
* Sets the ACL status for given path
* @param path The path to the file/folder to set the ACL on
* @param ownerEntry The entry corresponding to the path owner
* @param groupEntry The entry corresponding to the path owning group
* @param otherEntry The entry corresponding to default permissions for all other users
* @param aclEntries The ACL entries to set
*/
setAcl(path: string, ownerEntry: AclEntry, groupEntry: AclEntry, otherEntry: AclEntry, aclEntries: AclEntry[]): Promise<void>;
exists(path: string): Promise<boolean>;
}
@@ -323,11 +335,14 @@ export class HdfsFileSource implements IFileSource {
/**
* Sets the ACL status for given path
* @param path The path to the file/folder to set the ACL on
* @param ownerEntry The entry corresponding to the path owner
* @param groupEntry The entry corresponding to the path owning group
* @param otherEntry The entry corresponding to default permissions for all other users
* @param aclEntries The ACL entries to set
*/
public setAcl(path: string, aclEntries: AclEntry[]): Promise<void> {
public setAcl(path: string, ownerEntry: AclEntry, groupEntry: AclEntry, otherEntry: AclEntry, aclEntries: AclEntry[]): Promise<void> {
return new Promise((resolve, reject) => {
this.client.setAcl(path, aclEntries, (error: HdfsError) => {
this.client.setAcl(path, ownerEntry, groupEntry, otherEntry, aclEntries, (error: HdfsError) => {
if (error) {
reject(error);
} else {

View File

@@ -21,6 +21,7 @@ import * as utils from '../utils';
import { AppContext } from '../appContext';
import { TreeNode } from './treeNodes';
import { MssqlObjectExplorerNodeProvider } from './objectExplorerNodeProvider';
import { ManageAccessDialog } from '../hdfs/ui/hdfsManageAccessDialog';
async function getSaveableUri(apiWrapper: ApiWrapper, fileName: string, isPreview?: boolean): Promise<vscode.Uri> {
let root = utils.getUserHome();
@@ -384,3 +385,24 @@ export class CopyPathCommand extends Command {
}
}
}
export class ManageAccessCommand extends Command {
constructor(appContext: AppContext) {
super('mssqlCluster.manageAccess', appContext);
}
async execute(context: ICommandViewContext | ICommandObjectExplorerContext, ...args: any[]): Promise<void> {
try {
let node = await getNode<HdfsFileSourceNode>(context, this.appContext);
if (node) {
new ManageAccessDialog(node.hdfsPath, node.fileSource, this.apiWrapper).openDialog();
} else {
this.apiWrapper.showErrorMessage(LocalizedConstants.msgMissingNodeContext);
}
} catch (err) {
this.apiWrapper.showErrorMessage(
localize('manageAccessError', "An unexpected error occurred while opening the Manage Access dialog: {0}", utils.getErrorMessage(err, true)));
}
}
}

View File

@@ -75,7 +75,7 @@ export class HdfsProvider implements vscode.TreeDataProvider<TreeNode>, ITreeCha
}
export abstract class HdfsFileSourceNode extends TreeNode {
constructor(protected context: TreeDataContext, protected _path: string, protected fileSource: IFileSource) {
constructor(protected context: TreeDataContext, protected _path: string, public readonly fileSource: IFileSource) {
super();
}