diff --git a/extensions/mssql/package.json b/extensions/mssql/package.json index 08b993e092..dce0773297 100644 --- a/extensions/mssql/package.json +++ b/extensions/mssql/package.json @@ -276,6 +276,11 @@ "when": "nodeType=~/^mssqlCluster/ && nodeType != mssqlCluster:connection && nodeType != mssqlCluster:message && nodeType != mssqlCluster:hdfs", "group": "1mssqlCluster@3" }, + { + "command": "mssqlCluster.manageAccess", + "when": "nodeType=~/^mssqlCluster/ && nodeType != mssqlCluster:connection && nodeType != mssqlCluster:message", + "group": "1mssqlCluster@3" + }, { "command": "mssqlCluster.deleteFiles", "when": "nodeType=~/^mssqlCluster/ && nodeType != mssqlCluster:hdfs && nodeType != mssqlCluster:connection && viewItem != mssqlCluster:connection && nodeType != mssqlCluster:message && nodeSubType=~/^(?!:mount).*$/", diff --git a/extensions/mssql/src/hdfs/aclEntry.ts b/extensions/mssql/src/hdfs/aclEntry.ts index 86afff03df..a1322914d9 100644 --- a/extensions/mssql/src/hdfs/aclEntry.ts +++ b/extensions/mssql/src/hdfs/aclEntry.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { IconPathHelper, IconPath } from '../iconHelper'; +import { groupBy } from '../util/arrays'; /** * The permission status of an HDFS path - this consists of : @@ -32,7 +33,8 @@ export class PermissionStatus { * @see AclEntryPermission for more information on the permission digits */ public get permissionOctal(): string { - return `${this.stickyBit ? '1' : ''}${this.owner.permissionDigit}${this.group.permissionDigit}${this.other.permissionDigit}`; + // Always use the access scope for the permission octal - it doesn't have a concept of other scopes + return `${this.stickyBit ? '1' : ''}${this.owner.getPermissionDigit(AclEntryScope.access)}${this.group.getPermissionDigit(AclEntryScope.access)}${this.other.getPermissionDigit(AclEntryScope.access)}`; } } @@ -135,23 +137,62 @@ function parseAclPermission(permissionString: string): AclEntryPermission { * permission - The permission set for this ACL. @see AclPermission */ export class AclEntry { + private readonly permissions = new Map(); + constructor( - public readonly scope: AclEntryScope, public readonly type: AclType | PermissionType, public readonly name: string, public readonly displayName: string, - public readonly permission: AclEntryPermission, ) { } /** - * Gets the octal number representing the permission for this entry. This digit is a value - * between 0 and 7 inclusive, which is a bitwise OR the permission flags (r/w/x). + * Adds a new permission at the specified scope, overwriting the existing permission at that scope if it + * exists + * @param scope The scope to add the new permission at + * @param permission The permission to set */ - public get permissionDigit(): number { - return this.permission.permissionDigit; + public addPermission(scope: AclEntryScope, permission: AclEntryPermission): void { + this.permissions.set(scope, permission); } + /** - * Returns the string representation of the ACL Entry in the form [SCOPE:]TYPE:NAME:PERMISSION. + * Deletes the permission at the specified scope. + * @param scope The scope to delete the permission for + * @returns True if the entry was successfully deleted, false if not (it didn't exist) + */ + public removePermission(scope: AclEntryScope): boolean { + return this.permissions.delete(scope); + } + + /** + * Gets the permission at the specified scope if one exists + * @param scope The scope to retrieve the permission for + */ + public getPermission(scope: AclEntryScope): AclEntryPermission | undefined { + return this.permissions.get(scope); + } + + /** + * Gets the full list of permissions and their scopes for this entry + */ + public getAllPermissions(): { scope: AclEntryScope, permission: AclEntryPermission }[] { + return Array.from(this.permissions.entries()).map((entry: [AclEntryScope, AclEntryPermission]) => { + return { scope: entry[0], permission: entry[1] }; + }); + } + + /** + * Gets the octal number representing the permission for the specified scope of + * this entry. This will either be a number between 0 and 7 inclusive (which is + * a bitwise OR the permission flags rwx) or undefined if the scope doesn't exist + * for this entry. + */ + public getPermissionDigit(scope: AclEntryScope): number | undefined { + return this.permissions.has(scope) ? this.permissions.get(scope).permissionDigit : undefined; + } + + /** + * Returns the string representation of each ACL Entry in the form [SCOPE:]TYPE:NAME:PERMISSION. * Note that SCOPE is only displayed if it's default - access is implied if there is no scope * specified. * The name is optional and so may be empty. @@ -161,8 +202,10 @@ export class AclEntry { * user::r-x * default:group::r-- */ - toAclString(): string { - return `${this.scope === AclEntryScope.default ? 'default:' : ''}${getAclEntryType(this.type)}:${this.name}:${this.permission.toString()}`; + toAclStrings(): string[] { + return Array.from(this.permissions.entries()).map((entry: [AclEntryScope, AclEntryPermission]) => { + return `${entry[0] === AclEntryScope.default ? 'default:' : ''}${getAclEntryType(this.type)}:${this.name}:${entry[1].toString()}`; + }); } /** @@ -174,9 +217,22 @@ export class AclEntry { if (!other) { return false; } - return this.scope === other.scope && - this.type === other.type && - this.name === other.name; + return AclEntry.compare(this, other) === 0; + } + + /** + * Compares two AclEntry objects for ordering + * @param a The first AclEntry to compare + * @param b The second AclEntry to compare + */ + static compare(a: AclEntry, b: AclEntry): number { + if (a.name === b.name) { + if (a.type === b.type) { + return 0; + } + return a.type.localeCompare(b.type); + } + return a.name.localeCompare(b.name); } } @@ -218,11 +274,15 @@ function getAclEntryType(type: AclType | PermissionType): AclType { * 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[] { +export function parseAclList(aclString: string): AclEntry[] { + if (aclString === '') { + return []; + } + if (!/^(default:)?(user|group|mask|other):([A-Za-z_][A-Za-z0-9._-]*)?:([rwx-]{3})?(,(default:)?(user|group|mask|other):([A-Za-z_][A-Za-z0-9._-]*)?:([rwx-]{3})?)*$/.test(aclString)) { throw new Error(`Invalid ACL string ${aclString}. Expected to match ^(default:)?(user|group|mask|other):[[A-Za-z_][A-Za-z0-9._-]]*:([rwx-]{3})?(,(default:)?(user|group|mask|other):[[A-Za-z_][A-Za-z0-9._-]]*:([rwx-]{3})?)*$`); } - return aclString.split(',').map(aclEntryString => parseAclEntry(aclEntryString)); + return mergeAclEntries(aclString.split(',').map(aclEntryString => parseAclEntry(aclEntryString))); } /** @@ -253,7 +313,9 @@ export function parseAclEntry(aclString: string): AclEntry { } const name = parts[i++]; const permission = parseAclPermission(parts[i++]); - return new AclEntry(scope, type, name, name, permission); + const entry = new AclEntry(type, name, name); + entry.addPermission(scope, permission); + return entry; } /** @@ -303,3 +365,19 @@ export function getImageForType(type: AclType | PermissionType): IconPath { } return { dark: '', light: '' }; } + +/** + * Merges a list of AclEntry objects such that the resulting list contains only a single entry for each name/type pair with + * a separate permission for each separate AclEntry + * @param entries The set of AclEntries to merge + */ +function mergeAclEntries(entries: AclEntry[]): AclEntry[] { + const groupedEntries = groupBy(entries, (a, b) => AclEntry.compare(a, b)); // First group the entries together + return groupedEntries.map(entryGroup => { // Now make a single AclEntry for each group and add all the permissions from each group + const entry = new AclEntry(entryGroup[0].type, entryGroup[0].name, entryGroup[0].displayName); + entryGroup.forEach(e => { + e.getAllPermissions().forEach(sp => entry.addPermission(sp.scope, sp.permission)); + }); + return entry; + }); +} diff --git a/extensions/mssql/src/hdfs/hdfsModel.ts b/extensions/mssql/src/hdfs/hdfsModel.ts index dbac23e6ec..67ac0742e7 100644 --- a/extensions/mssql/src/hdfs/hdfsModel.ts +++ b/extensions/mssql/src/hdfs/hdfsModel.ts @@ -3,10 +3,14 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import * as azdata from 'azdata'; import * as vscode from 'vscode'; import { IFileSource } from '../objectExplorerNodeProvider/fileSources'; import { PermissionStatus, AclEntry, AclEntryScope, AclType, AclEntryPermission } from './aclEntry'; import { FileStatus } from './fileStatus'; +import * as nls from 'vscode-nls'; + +const localize = nls.loadMessageBundle(); /** * Model for storing the state of a specified file/folder in HDFS @@ -29,7 +33,7 @@ export class HdfsModel { */ public fileStatus: FileStatus; - constructor(private fileSource: IFileSource, private path: string) { + constructor(private readonly fileSource: IFileSource, private readonly path: string) { this.refresh(); } @@ -53,7 +57,8 @@ export class HdfsModel { if (!this.permissionStatus) { return; } - const newEntry = new AclEntry(AclEntryScope.access, type, name, name, new AclEntryPermission(true, true, true)); + const newEntry = new AclEntry(type, name, name); + newEntry.addPermission(AclEntryScope.access, new AclEntryPermission(true, true, true)); // Don't add duplicates. This also checks the owner, group and other items if ([this.permissionStatus.owner, this.permissionStatus.group, this.permissionStatus.other].concat(this.permissionStatus.aclEntries).find(entry => entry.isEqual(newEntry))) { return; @@ -78,10 +83,62 @@ export class HdfsModel { * 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 { - // TODO Apply recursive + public async apply(recursive: boolean = false): Promise { + await this.applyAclChanges(this.path); + if (recursive) { + azdata.tasks.startBackgroundOperation( + { + connection: undefined, + displayName: localize('mssql.recursivePermissionOpStarted', "Applying permission changes recursively under '{0}'", this.path), + description: '', + isCancelable: false, + operation: async op => { + await this.applyToChildrenRecursive(op, this.path); + op.updateStatus(azdata.TaskStatus.Succeeded, localize('mssql.recursivePermissionOpSucceeded', "Permission changes applied successfully.")); + } + } + ); + } + } + + /** + * Recursive call to apply the current set of changes to all children of this path (if any) + * @param op Background operation used to track status of the task + * @param path The path + */ + private async applyToChildrenRecursive(op: azdata.BackgroundOperation, path: string): Promise { + try { + op.updateStatus(azdata.TaskStatus.InProgress, localize('mssql.recursivePermissionOpProgress', "Applying permission changes to '{0}'.", path)); + const files = await this.fileSource.enumerateFiles(path, true); + // Apply changes to all children of this path and then recursively apply to children of any directories + await Promise.all( + [ + files.map(file => this.applyAclChanges(file.path)), + files.filter(f => f.isDirectory).map(d => this.applyToChildrenRecursive(op, d.path)) + ]); + } catch (error) { + const errMsg = localize('mssql.recursivePermissionOpError', "Error applying permission changes: {0}", (error instanceof Error ? error.message : error)); + vscode.window.showErrorMessage(errMsg); + op.updateStatus(azdata.TaskStatus.Failed, errMsg); + } + } + + /** + * Applies the current set of Permissions/ACLs to the specified path + * @param path The path to apply the changes to + */ + private async applyAclChanges(path: string): Promise { + // HDFS won't remove existing default ACLs even if you call setAcl with no default ACLs specified. You + // need to call removeDefaultAcl specifically to remove them. + if (!this.permissionStatus.owner.getPermission(AclEntryScope.default) && + !this.permissionStatus.group.getPermission(AclEntryScope.default) && + !this.permissionStatus.other.getPermission(AclEntryScope.default)) { + await this.fileSource.removeDefaultAcl(path); + } return Promise.all([ - this.fileSource.setAcl(this.path, this.permissionStatus.owner, this.permissionStatus.group, this.permissionStatus.other, this.permissionStatus.aclEntries), - this.fileSource.setPermission(this.path, this.permissionStatus)]); + this.fileSource.setAcl(path, this.permissionStatus.owner, this.permissionStatus.group, this.permissionStatus.other, this.permissionStatus.aclEntries), + this.fileSource.setPermission(path, this.permissionStatus)]); } } + + diff --git a/extensions/mssql/src/hdfs/ui/hdfsManageAccessDialog.ts b/extensions/mssql/src/hdfs/ui/hdfsManageAccessDialog.ts index e76d3e9319..af4946b073 100644 --- a/extensions/mssql/src/hdfs/ui/hdfsManageAccessDialog.ts +++ b/extensions/mssql/src/hdfs/ui/hdfsManageAccessDialog.ts @@ -6,7 +6,7 @@ import * as azdata from 'azdata'; import { HdfsModel } from '../hdfsModel'; import { IFileSource } from '../../objectExplorerNodeProvider/fileSources'; -import { PermissionStatus, AclEntry, AclType, getImageForType } from '../../hdfs/aclEntry'; +import { PermissionStatus, AclEntry, AclType, getImageForType, AclEntryScope, AclEntryPermission } from '../../hdfs/aclEntry'; import { cssStyles } from './uiConstants'; import * as loc from '../../localizedConstants'; import { HdfsError } from '../webhdfs'; @@ -16,7 +16,7 @@ import { HdfsFileType } from '../fileStatus'; const permissionsTypeIconColumnWidth = 35; const permissionsNameColumnWidth = 250; -const permissionsStickyColumnWidth = 50; +const permissionsInheritColumnWidth = 50; const permissionsReadColumnWidth = 50; const permissionsWriteColumnWidth = 50; const permissionsExecuteColumnWidth = 50; @@ -25,6 +25,15 @@ const permissionsDeleteColumnWidth = 50; const permissionsRowHeight = 35; const locationLabelHeight = 23; // Fits the text size without too much white space +const checkboxSize = 20; + + +type PermissionCheckboxesMapping = { + model: AclEntry, + access: { read: azdata.CheckBoxComponent, write: azdata.CheckBoxComponent, execute: azdata.CheckBoxComponent }, + default: { read: azdata.CheckBoxComponent, write: azdata.CheckBoxComponent, execute: azdata.CheckBoxComponent } +}; + export class ManageAccessDialog { private hdfsModel: HdfsModel; @@ -32,14 +41,16 @@ export class ManageAccessDialog { private modelBuilder: azdata.ModelBuilder; private rootLoadingComponent: azdata.LoadingComponent; - private ownersPermissionsContainer: azdata.FlexContainer; - private othersPermissionsContainer: azdata.FlexContainer; + private stickyCheckbox: azdata.CheckBoxComponent; + private inheritDefaultsCheckbox: azdata.CheckBoxComponent; + private posixPermissionsContainer: azdata.FlexContainer; private namedUsersAndGroupsPermissionsContainer: azdata.FlexContainer; private addUserOrGroupInput: azdata.InputBoxComponent; private dialog: azdata.window.Dialog; - + private applyRecursivelyButton: azdata.window.Button; private defaultSectionComponents: azdata.Component[] = []; - + private posixPermissionCheckboxesMapping: PermissionCheckboxesMapping[] = []; + private namedSectionInheritCheckboxes: azdata.CheckBoxComponent[] = []; private addUserOrGroupSelectedType: AclType; constructor(private hdfsPath: string, private fileSource: IFileSource, private readonly apiWrapper: ApiWrapper) { @@ -52,15 +63,16 @@ export class ManageAccessDialog { this.dialog = this.apiWrapper.createDialog(loc.manageAccessTitle, 'HdfsManageAccess', true); this.dialog.okButton.label = loc.applyText; - const applyRecursivelyButton = azdata.window.createButton(loc.applyRecursivelyText); - applyRecursivelyButton.onClick(async () => { + this.applyRecursivelyButton = azdata.window.createButton(loc.applyRecursivelyText); + this.applyRecursivelyButton.onClick(async () => { try { + azdata.window.closeDialog(this.dialog); await this.hdfsModel.apply(true); } catch (err) { this.apiWrapper.showErrorMessage(loc.errorApplyingAclChanges(err instanceof HdfsError ? err.message : err)); } }); - this.dialog.customButtons = [applyRecursivelyButton]; + this.dialog.customButtons = [this.applyRecursivelyButton]; this.dialog.registerCloseValidator(async (): Promise => { try { await this.hdfsModel.apply(); @@ -123,30 +135,16 @@ export class ManageAccessDialog { .component(); contentContainer.addItem(permissionsTitle, { CSSStyles: { 'margin-top': '15px', ...cssStyles.titleCss } }); - // ============================== - // = Owners permissions section = - // ============================== + // ============================= + // = POSIX permissions section = + // ============================= - const ownersPermissionsHeaderRow = this.createPermissionsHeaderRow(modelView.modelBuilder, loc.ownersHeader, true); - contentContainer.addItem(ownersPermissionsHeaderRow, { CSSStyles: { ...cssStyles.tableHeaderLayoutCss } }); + const posixPermissionsContainer = this.createPermissionsHeaderRow(modelView.modelBuilder, '', /*includeInherit*/false, /*includeStickyAndInherit*/true); + contentContainer.addItem(posixPermissionsContainer, { CSSStyles: { ...cssStyles.tableHeaderLayoutCss } }); - // Empty initially - this is going to eventually be populated with the owner/owning group permissions - this.ownersPermissionsContainer = modelView.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); - contentContainer.addItem(this.ownersPermissionsContainer, { flex: '0 0 auto', CSSStyles: { 'margin-bottom': '20px' } }); - - // ============================== - // = Others permissions section = - // ============================== - - const othersPermissionsHeaderRow = modelView.modelBuilder.flexContainer().withLayout({ flexFlow: 'row' }).component(); - const ownersHeaderCell = modelView.modelBuilder.text().withProperties({ value: loc.allOthersHeader }).component(); - othersPermissionsHeaderRow.addItem(ownersHeaderCell, { flex: '1 1 auto', CSSStyles: { 'width': `${permissionsNameColumnWidth}px`, 'min-width': `${permissionsNameColumnWidth}px`, ...cssStyles.tableHeaderCss } }); - - contentContainer.addItem(othersPermissionsHeaderRow, { CSSStyles: { ...cssStyles.tableHeaderLayoutCss } }); - - // Empty initially - this is eventually going to be populated with the "Everyone" permissions - this.othersPermissionsContainer = modelView.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); - contentContainer.addItem(this.othersPermissionsContainer, { flex: '0 0 auto', CSSStyles: { 'margin-bottom': '20px' } }); + // Empty initially - this is going to eventually be populated with the owner/owning/other group permissions + this.posixPermissionsContainer = modelView.modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); + contentContainer.addItem(this.posixPermissionsContainer, { flex: '0 0 auto', CSSStyles: { 'margin-bottom': '20px' } }); // =========================== // = Add User Or Group Input = @@ -165,7 +163,7 @@ export class ManageAccessDialog { this.addUserOrGroupSelectedType = AclType.user; typeContainer.addItems([userTypeButton, groupTypeButton], { flex: '0 0 auto' }); - contentContainer.addItem(typeContainer, { flex: '0 0 auto' }); + contentContainer.addItem(typeContainer, { flex: '0 0 auto', CSSStyles: { 'margin-bottom': '5px' } }); const addUserOrGroupInputRow = modelView.modelBuilder.flexContainer().withLayout({ flexFlow: 'row' }).component(); this.addUserOrGroupInput = modelView.modelBuilder.inputBox() @@ -203,7 +201,7 @@ export class ManageAccessDialog { // = Named Users and Groups permissions header row = // ================================================= - const namedUsersAndGroupsPermissionsHeaderRow = this.createPermissionsHeaderRow(modelView.modelBuilder, loc.namedUsersAndGroupsHeader, false); + const namedUsersAndGroupsPermissionsHeaderRow = this.createPermissionsHeaderRow(modelView.modelBuilder, loc.namedUsersAndGroupsHeader, /*includeInherit*/true, /*includeStickyAndInherit*/false); contentContainer.addItem(namedUsersAndGroupsPermissionsHeaderRow, { CSSStyles: { ...cssStyles.tableHeaderLayoutCss } }); // Empty initially - this is eventually going to be populated with the ACL entries set for this path @@ -219,6 +217,7 @@ export class ManageAccessDialog { this.dialog.content = [tab]; } + this.applyRecursivelyButton.hidden = true; // Always hide the button until we get the status back saying whether this is a directory or not azdata.window.openDialog(this.dialog); } @@ -227,24 +226,26 @@ export class ManageAccessDialog { return; } + this.stickyCheckbox.checked = permissionStatus.stickyBit; + this.inheritDefaultsCheckbox.checked = + !permissionStatus.owner.getPermission(AclEntryScope.default) && + !permissionStatus.group.getPermission(AclEntryScope.default) && + !permissionStatus.other.getPermission(AclEntryScope.default); // Update display status for headers for the Default section - you can't set Default ACLs for non-directories so we just hide that column this.defaultSectionComponents.forEach(component => component.display = this.hdfsModel.fileStatus.type === HdfsFileType.Directory ? '' : 'none'); - - // Owners - const ownerPermissionsRow = this.createOwnerPermissionsRow(this.modelBuilder, permissionStatus); - const owningGroupPermissionsRow = this.createPermissionsRow(this.modelBuilder, permissionStatus.group, /*includeDelete*/false); - this.ownersPermissionsContainer.clearItems(); - this.ownersPermissionsContainer.addItems([ownerPermissionsRow, owningGroupPermissionsRow], { CSSStyles: { 'border-bottom': cssStyles.tableBorderCss, 'border-top': cssStyles.tableBorderCss, 'margin-right': '14px' } }); - - // Others - const otherPermissionsRow = this.createPermissionsRow(this.modelBuilder, permissionStatus.other, false); - this.othersPermissionsContainer.clearItems(); - this.othersPermissionsContainer.addItem(otherPermissionsRow, { CSSStyles: { 'border-bottom': cssStyles.tableBorderCss, 'border-top': cssStyles.tableBorderCss, 'margin-right': '14px' } }); + this.applyRecursivelyButton.hidden = this.hdfsModel.fileStatus.type !== HdfsFileType.Directory; + this.inheritDefaultsCheckbox.display = this.hdfsModel.fileStatus.type === HdfsFileType.Directory ? '' : 'none'; + // POSIX permission owner/group/other + const ownerPermissionsRow = this.createPermissionsRow(this.modelBuilder, permissionStatus.owner, /*includeDelete*/false, /*includeInherit*/false); + const owningGroupPermissionsRow = this.createPermissionsRow(this.modelBuilder, permissionStatus.group, /*includeDelete*/false, /*includeInherit*/false); + const otherPermissionsRow = this.createPermissionsRow(this.modelBuilder, permissionStatus.other, /*includeDelete*/false, /*includeInherit*/false); + this.posixPermissionsContainer.clearItems(); + this.posixPermissionsContainer.addItems([ownerPermissionsRow, owningGroupPermissionsRow, otherPermissionsRow], { CSSStyles: { 'border-bottom': cssStyles.tableBorderCss, 'border-top': cssStyles.tableBorderCss, 'margin-right': '14px' } }); this.namedUsersAndGroupsPermissionsContainer.clearItems(); // Named users and groups permissionStatus.aclEntries.forEach(entry => { - const namedEntryRow = this.createPermissionsRow(this.modelBuilder, entry, true); + const namedEntryRow = this.createPermissionsRow(this.modelBuilder, entry, /*includeDelete*/true, /*includeInherit*/true); this.namedUsersAndGroupsPermissionsContainer.addItem(namedEntryRow, { CSSStyles: { 'border-bottom': cssStyles.tableBorderCss, 'border-top': cssStyles.tableBorderCss } }); }); @@ -259,18 +260,7 @@ export class ManageAccessDialog { return button; } - private createOwnerPermissionsRow(builder: azdata.ModelBuilder, permissionStatus: PermissionStatus): azdata.FlexContainer { - const row = this.createPermissionsRow(builder, permissionStatus.owner, false); - const stickyComponents = createCheckbox(builder, permissionStatus.stickyBit, permissionsReadColumnWidth, permissionsRowHeight); - stickyComponents.checkbox.onChanged(() => { - permissionStatus.stickyBit = stickyComponents.checkbox.checked; - }); - // Insert after name item but before other checkboxes - row.insertItem(stickyComponents.container, 2, { flex: '0 0 auto' }); - return row; - } - - private createPermissionsRow(builder: azdata.ModelBuilder, entry: AclEntry, includeDelete: boolean): azdata.FlexContainer { + private createPermissionsRow(builder: azdata.ModelBuilder, entry: AclEntry, includeDelete: boolean, includeInherit: boolean): azdata.FlexContainer { const rowContainer = builder.flexContainer().withLayout({ flexFlow: 'row', height: permissionsRowHeight }).component(); // Icon @@ -290,50 +280,92 @@ export class ManageAccessDialog { rowContainer.addItem(nameCell); // Access - Read - const accessReadComponents = createCheckbox(builder, entry.permission.read, permissionsReadColumnWidth, permissionsRowHeight); + const accessReadComponents = createCheckbox(builder, entry.getPermission(AclEntryScope.access).read, true, permissionsReadColumnWidth, permissionsRowHeight); rowContainer.addItem(accessReadComponents.container, { flex: '0 0 auto' }); accessReadComponents.checkbox.onChanged(() => { - entry.permission.read = accessReadComponents.checkbox.checked; + entry.getPermission(AclEntryScope.access).read = accessReadComponents.checkbox.checked; }); // Access - Write - const accessWriteComponents = createCheckbox(builder, entry.permission.write, permissionsWriteColumnWidth, permissionsRowHeight); + const accessWriteComponents = createCheckbox(builder, entry.getPermission(AclEntryScope.access).write, true, permissionsWriteColumnWidth, permissionsRowHeight); rowContainer.addItem(accessWriteComponents.container, { flex: '0 0 auto' }); accessWriteComponents.checkbox.onChanged(() => { - entry.permission.write = accessWriteComponents.checkbox.checked; + entry.getPermission(AclEntryScope.access).write = accessWriteComponents.checkbox.checked; }); // Access - Execute - const accessExecuteComponents = createCheckbox(builder, entry.permission.execute, permissionsExecuteColumnWidth, permissionsRowHeight); + const accessExecuteComponents = createCheckbox(builder, entry.getPermission(AclEntryScope.access).execute, true, permissionsExecuteColumnWidth, permissionsRowHeight); rowContainer.addItem(accessExecuteComponents.container, { flex: '0 0 auto', CSSStyles: { 'border-right': this.hdfsModel.fileStatus.type === HdfsFileType.Directory ? cssStyles.tableBorderCss : '' } }); accessExecuteComponents.checkbox.onChanged(() => { - entry.permission.execute = accessExecuteComponents.checkbox.checked; + entry.getPermission(AclEntryScope.access).execute = accessExecuteComponents.checkbox.checked; }); + const permissionsCheckboxesMapping: PermissionCheckboxesMapping = { + model: entry, + access: { read: accessReadComponents.checkbox, write: accessWriteComponents.checkbox, execute: accessExecuteComponents.checkbox }, + default: { read: undefined, write: undefined, execute: undefined } + }; + // Only directories can set ACL defaults so we hide the column for non-directories if (this.hdfsModel.fileStatus.type === HdfsFileType.Directory) { + + const defaultPermission = entry.getPermission(AclEntryScope.default); + + const defaultReadComponents = createCheckbox(builder, defaultPermission && defaultPermission.read, !!defaultPermission, permissionsReadColumnWidth, permissionsRowHeight); + const defaultWriteComponents = createCheckbox(builder, defaultPermission && defaultPermission.write, !!defaultPermission, permissionsWriteColumnWidth, permissionsRowHeight); + const defaultExecuteComponents = createCheckbox(builder, defaultPermission && defaultPermission.execute, !!defaultPermission, permissionsExecuteColumnWidth, permissionsRowHeight); + permissionsCheckboxesMapping.default = { read: defaultReadComponents.checkbox, write: defaultWriteComponents.checkbox, execute: defaultExecuteComponents.checkbox }; + + // Default - Inherit + if (includeInherit) { + const defaultInheritComponents = createCheckbox(builder, !defaultPermission, !this.inheritDefaultsCheckbox.checked, permissionsInheritColumnWidth, permissionsRowHeight); + defaultInheritComponents.checkbox.onChanged(() => { + defaultReadComponents.checkbox.enabled = !defaultInheritComponents.checkbox.checked; + defaultWriteComponents.checkbox.enabled = !defaultInheritComponents.checkbox.checked; + defaultExecuteComponents.checkbox.enabled = !defaultInheritComponents.checkbox.checked; + if (defaultInheritComponents.checkbox.checked) { + entry.removePermission(AclEntryScope.default); + defaultReadComponents.checkbox.checked = false; + defaultWriteComponents.checkbox.checked = false; + defaultExecuteComponents.checkbox.checked = false; + } else { + // Default to the access settings - this is what HDFS does if you don't + // specify the complete set of default ACLs for owner, owning group and other + const accessRead = accessReadComponents.checkbox.checked; + const accessWrite = accessWriteComponents.checkbox.checked; + const accessExecute = accessExecuteComponents.checkbox.checked; + defaultReadComponents.checkbox.checked = accessRead; + defaultWriteComponents.checkbox.checked = accessWrite; + defaultExecuteComponents.checkbox.checked = accessExecute; + entry.addPermission(AclEntryScope.default, + new AclEntryPermission(accessRead, accessWrite, accessExecute)); + } + }); + this.namedSectionInheritCheckboxes.push(defaultInheritComponents.checkbox); + rowContainer.addItem(defaultInheritComponents.container, { flex: '0 0 auto', CSSStyles: { 'border-right': cssStyles.tableBorderCss } }); + } + + // 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 + entry.getPermission(AclEntryScope.default).read = defaultReadComponents.checkbox.checked; }); // 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 + defaultWriteComponents.checkbox.onChanged(() => { + entry.getPermission(AclEntryScope.default).write = defaultWriteComponents.checkbox.checked; }); // 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 + defaultExecuteComponents.checkbox.onChanged(() => { + entry.getPermission(AclEntryScope.default).execute = defaultExecuteComponents.checkbox.checked; }); } + this.posixPermissionCheckboxesMapping.push(permissionsCheckboxesMapping); const deleteContainer = builder.flexContainer().withLayout({ width: permissionsDeleteColumnWidth, height: permissionsRowHeight }).component(); @@ -351,23 +383,74 @@ export class ManageAccessDialog { deleteButton.onDidClick(() => { this.hdfsModel.deleteAclEntry(entry); }); deleteContainer.addItem(deleteButton); } - rowContainer.addItem(deleteContainer, { flex: '0 0 auto', CSSStyles: { 'margin-top': '5px', 'margin-left': '5px' } }); + rowContainer.addItem(deleteContainer, { flex: '0 0 auto', CSSStyles: { 'margin-top': '7px', 'margin-left': '5px' } }); 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. + * Creates the header row for the permissions tables. This contains headers for the name and read/write/execute for the + * access section. If the path is for a directory then a default section is included for specifying default permissions. * @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 */ - private createPermissionsHeaderRow(modelBuilder: azdata.ModelBuilder, nameColumnText: string, includeSticky: boolean): azdata.FlexContainer { + private createPermissionsHeaderRow(modelBuilder: azdata.ModelBuilder, nameColumnText: string, includeInherit: boolean, includeStickyAndInherit: boolean): azdata.FlexContainer { const rowsContainer = modelBuilder.flexContainer().withLayout({ flexFlow: 'column' }).component(); // Section Headers const sectionHeaderContainer = modelBuilder.flexContainer().withLayout({ flexFlow: 'row', justifyContent: 'flex-end' }).component(); + + if (includeStickyAndInherit) { + this.inheritDefaultsCheckbox = modelBuilder.checkBox() + .withProperties({ + width: checkboxSize, + height: checkboxSize, + checked: false, // Will be set when we get the model update + label: loc.inheritDefaultsLabel + }) + .component(); + + this.inheritDefaultsCheckbox.onChanged(() => { + if (this.inheritDefaultsCheckbox.checked) { + this.namedSectionInheritCheckboxes.forEach(c => { + c.enabled = false; + c.checked = true; + }); + } else { + this.namedSectionInheritCheckboxes.forEach(c => { + c.enabled = true; + c.checked = false; + }); + } + // Go through each of the rows for owner/owning group/other and update + // their checkboxes based on the new value of the inherit checkbox + this.posixPermissionCheckboxesMapping.forEach(m => { + m.default.read.enabled = !this.inheritDefaultsCheckbox.checked; + m.default.write.enabled = !this.inheritDefaultsCheckbox.checked; + m.default.execute.enabled = !this.inheritDefaultsCheckbox.checked; + if (this.inheritDefaultsCheckbox.checked) { + m.model.removePermission(AclEntryScope.default); + m.default.read.checked = false; + m.default.write.checked = false; + m.default.execute.checked = false; + } else { + // Default to the access settings - this is what HDFS does if you don't + // specify the complete set of default ACLs for owner, owning group and other + const accessRead = m.access.read.checked; + const accessWrite = m.access.write.checked; + const accessExecute = m.access.execute.checked; + m.default.read.checked = accessRead; + m.default.write.checked = accessWrite; + m.default.execute.checked = accessExecute; + m.model.addPermission(AclEntryScope.default, new AclEntryPermission(accessRead, accessWrite, accessExecute)); + } + }); + }); + this.defaultSectionComponents.push(this.inheritDefaultsCheckbox); + sectionHeaderContainer.addItem(this.inheritDefaultsCheckbox); + } + + // Access const accessSectionHeader = modelBuilder.text() .withProperties({ value: loc.accessHeader, @@ -378,8 +461,9 @@ export class ManageAccessDialog { } }) .component(); - sectionHeaderContainer.addItem(accessSectionHeader, { flex: '0 0 auto' }); + + // Default const defaultSectionHeader = modelBuilder.text() .withProperties({ value: loc.defaultHeader, @@ -392,6 +476,7 @@ export class ManageAccessDialog { .component(); sectionHeaderContainer.addItem(defaultSectionHeader, { flex: '0 0 auto' }); this.defaultSectionComponents.push(defaultSectionHeader); + // Delete - just used as a spacer const deleteSectionHeader = modelBuilder.text().component(); sectionHeaderContainer.addItem(deleteSectionHeader, { CSSStyles: { 'width': `${permissionsDeleteColumnWidth}px`, 'min-width': `${permissionsDeleteColumnWidth}px` } }); @@ -401,17 +486,25 @@ export class ManageAccessDialog { // Table headers const headerRowContainer = modelBuilder.flexContainer().withLayout({ flexFlow: 'row' }).component(); - // Icon (spacer, no text) - const typeIconCell = modelBuilder.text().withProperties({ CSSStyles: { 'width': `${permissionsTypeIconColumnWidth}px`, 'min-width': `${permissionsTypeIconColumnWidth}px` } }).component(); - headerRowContainer.addItem(typeIconCell, { flex: '0 0 auto' }); + if (includeStickyAndInherit) { + // Sticky + this.stickyCheckbox = modelBuilder.checkBox() + .withProperties({ + width: checkboxSize, + height: checkboxSize, + checked: false, // Will be set when we get the model update + label: loc.stickyLabel + }) + .component(); + this.stickyCheckbox.onChanged(() => { + this.hdfsModel.permissionStatus.stickyBit = this.stickyCheckbox.checked; + }); + headerRowContainer.addItem(this.stickyCheckbox); + } // Name const nameCell = modelBuilder.text().withProperties({ value: nameColumnText }).component(); headerRowContainer.addItem(nameCell, { flex: '1 1 auto', CSSStyles: { 'width': `${permissionsNameColumnWidth}px`, 'min-width': `${permissionsNameColumnWidth}px`, ...cssStyles.tableHeaderCss } }); - if (includeSticky) { - const stickyCell = modelBuilder.text().withProperties({ value: loc.stickyHeader, CSSStyles: { ...cssStyles.permissionsTableHeaderCss } }).component(); - headerRowContainer.addItem(stickyCell, { CSSStyles: { 'width': `${permissionsStickyColumnWidth}px`, 'min-width': `${permissionsStickyColumnWidth}px` } }); - } // Access Permissions Group const accessReadCell = modelBuilder.text().withProperties({ value: loc.readHeader, CSSStyles: { ...cssStyles.permissionsTableHeaderCss } }).component(); @@ -422,6 +515,10 @@ export class ManageAccessDialog { headerRowContainer.addItem(accessExecuteCell, { CSSStyles: { 'width': `${permissionsExecuteColumnWidth}px`, 'min-width': `${permissionsExecuteColumnWidth}px`, 'margin-right': '5px' } }); // Default Permissions Group const defaultPermissionsHeadersContainer = modelBuilder.flexContainer().withLayout({ flexFlow: 'row' }).component(); + if (includeInherit) { + const inheritCell = modelBuilder.text().withProperties({ value: loc.inheritDefaultsLabel, CSSStyles: { ...cssStyles.permissionsTableHeaderCss } }).component(); + defaultPermissionsHeadersContainer.addItem(inheritCell, { CSSStyles: { 'width': `${permissionsInheritColumnWidth}px`, 'min-width': `${permissionsInheritColumnWidth}px` } }); + } const defaultReadCell = modelBuilder.text().withProperties({ value: loc.readHeader, CSSStyles: { ...cssStyles.permissionsTableHeaderCss } }).component(); defaultPermissionsHeadersContainer.addItem(defaultReadCell, { CSSStyles: { 'width': `${permissionsReadColumnWidth}px`, 'min-width': `${permissionsReadColumnWidth}px` } }); const defaultWriteCell = modelBuilder.text().withProperties({ value: loc.writeHeader, CSSStyles: { ...cssStyles.permissionsTableHeaderCss } }).component(); @@ -441,9 +538,9 @@ export class ManageAccessDialog { } } -function createCheckbox(builder: azdata.ModelBuilder, checked: boolean, containerWidth: number, containerHeight: number): { container: azdata.FlexContainer, checkbox: azdata.CheckBoxComponent } { +function createCheckbox(builder: azdata.ModelBuilder, checked: boolean, enabled: boolean, containerWidth: number, containerHeight: number): { container: azdata.FlexContainer, checkbox: azdata.CheckBoxComponent } { const checkbox = builder.checkBox() - .withProperties({ checked: checked, height: 20, width: 20 }).component(); + .withProperties({ checked: checked, enabled: enabled, height: checkboxSize, width: checkboxSize }).component(); const container = builder.flexContainer() .withLayout({ width: containerWidth, height: containerHeight }) .component(); diff --git a/extensions/mssql/src/hdfs/webhdfs.ts b/extensions/mssql/src/hdfs/webhdfs.ts index c70e642b6a..722c082946 100644 --- a/extensions/mssql/src/hdfs/webhdfs.ts +++ b/extensions/mssql/src/hdfs/webhdfs.ts @@ -11,9 +11,9 @@ import * as through from 'through2'; import * as nls from 'vscode-nls'; import * as auth from '../util/auth'; import { IHdfsOptions, IRequestParams } from '../objectExplorerNodeProvider/fileSources'; -import { PermissionStatus, AclEntry, parseAcl, PermissionType, parseAclPermissionFromOctal, AclEntryScope } from './aclEntry'; +import { PermissionStatus, AclEntry, parseAclList, PermissionType, parseAclPermissionFromOctal, AclEntryScope, AclType } from './aclEntry'; import { Mount } from './mount'; -import { everyoneName } from '../localizedConstants'; +import { everyoneName, ownerPostfix, owningGroupPostfix } from '../localizedConstants'; import { FileStatus, parseHdfsFileType } from './fileStatus'; const localize = nls.loadMessageBundle(); @@ -340,11 +340,11 @@ export class WebHDFS { } /** - * Read directory contents + * List the status of a path * * @returns void */ - public readdir(path: string, callback: (error: HdfsError, files: any[]) => void): void { + public listStatus(path: string, callback: (error: HdfsError, files: FileStatus[]) => void): void { this.checkArgDefined('path', path); let endpoint = this.getOperationEndpoint('liststatus', path); @@ -356,7 +356,21 @@ export class WebHDFS { callback(error, undefined); } else if (response.body.hasOwnProperty('FileStatuses') && response.body.FileStatuses.hasOwnProperty('FileStatus')) { - files = response.body.FileStatuses.FileStatus; + files = (response.body.FileStatuses.FileStatus).map(fs => { + return new FileStatus( + fs.accessTime || '', + fs.blockSize || '', + fs.group || '', + fs.length || '', + fs.modificationTime || '', + fs.owner || '', + fs.pathSuffix || '', + fs.permission || '', + fs.replication || '', + fs.snapshotEnabled || '', + parseHdfsFileType(fs.type) + ); + }); callback(undefined, files); } else { callback(new HdfsError(ErrorMessageInvalidDataStructure), undefined); @@ -401,26 +415,6 @@ export class WebHDFS { }); } - /** - * Get file status for given path - * @returns void - */ - public stat(path: string, callback: (error: HdfsError, fileStatus: any) => void): void { - this.checkArgDefined('path', path); - - let endpoint = this.getOperationEndpoint('getfilestatus', path); - this.sendRequest('GET', endpoint, undefined, (error, response) => { - if (!callback) { return; } - if (error) { - callback(error, undefined); - } else if (response.body.hasOwnProperty('FileStatus')) { - callback(undefined, response.body.FileStatus); - } else { - callback(new HdfsError(ErrorMessageInvalidDataStructure), undefined); - } - }); - } - public getFileStatus(path: string, callback: (error: HdfsError, fileStatus: FileStatus) => void): void { this.checkArgDefined('path', path); @@ -466,16 +460,45 @@ export class WebHDFS { callback(error, undefined); } else if (response.body.hasOwnProperty('AclStatus')) { const permissions = parseAclPermissionFromOctal(response.body.AclStatus.permission); + const ownerEntry = new AclEntry(PermissionType.owner, '', `${response.body.AclStatus.owner || ''}${ownerPostfix}`); + ownerEntry.addPermission(AclEntryScope.access, permissions.owner); + const groupEntry = new AclEntry(PermissionType.group, '', `${response.body.AclStatus.group || ''}${owningGroupPostfix}`); + groupEntry.addPermission(AclEntryScope.access, permissions.group); + const otherEntry = new AclEntry(PermissionType.other, '', everyoneName); + otherEntry.addPermission(AclEntryScope.access, permissions.other); + const parsedEntries = parseAclList((response.body.AclStatus.entries).join(',')); + + // First go through and apply any ACLs for the unnamed entries (which correspond to the permissions in + // the permission octal) + parsedEntries.filter(e => e.name === '').forEach(e => { + let targetEntry: AclEntry; + switch (e.type) { + case AclType.user: + targetEntry = ownerEntry; + break; + case AclType.group: + targetEntry = groupEntry; + break; + case AclType.other: + targetEntry = otherEntry; + break; + default: + // Unknown type - just ignore since we don't currently support the other types + return; + } + e.getAllPermissions().forEach( sp => { + targetEntry.addPermission(sp.scope, sp.permission); + }); + }); + const permissionStatus = new PermissionStatus( - new AclEntry(AclEntryScope.access, PermissionType.owner, '', response.body.AclStatus.owner || '', permissions.owner), - new AclEntry(AclEntryScope.access, PermissionType.group, '', response.body.AclStatus.group || '', permissions.group), - new AclEntry(AclEntryScope.access, PermissionType.other, '', everyoneName, permissions.other), + ownerEntry, + groupEntry, + otherEntry, !!response.body.AclStatus.stickyBit, - // We filter out empty names here since those are already added by the permission bits - WebHDFS creates an extra ACL - // entry for the owning group whenever another ACL is added. - (response.body.AclStatus.entries).map(entry => parseAcl(entry)) - .reduce((acc, parsedEntries) => acc.concat(parsedEntries), []) - .filter(e => e.name !== '')); + // We filter out empty names here since those have already been merged into the + // owner/owning group/other entries + parsedEntries.filter(e => e.name !== '')); callback(undefined, permissionStatus); } else { callback(new HdfsError(ErrorMessageInvalidDataStructure), undefined); @@ -491,7 +514,6 @@ export class WebHDFS { * @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, ownerEntry: AclEntry, groupEntry: AclEntry, otherEntry: AclEntry, aclEntries: AclEntry[], callback: (error: HdfsError) => void): void { this.checkArgDefined('path', path); @@ -499,7 +521,8 @@ export class WebHDFS { this.checkArgDefined('groupEntry', groupEntry); this.checkArgDefined('otherEntry', otherEntry); this.checkArgDefined('aclEntries', aclEntries); - const aclSpec = [ownerEntry, groupEntry, otherEntry].concat(aclEntries).map(entry => entry.toAclString()).join(','); + const concatEntries = [ownerEntry, groupEntry, otherEntry].concat(aclEntries); + const aclSpec = concatEntries.reduce((acc, entry) => acc.concat(entry.toAclStrings()), []).join(','); let endpoint = this.getOperationEndpoint('setacl', path, { aclspec: aclSpec }); this.sendRequest('PUT', endpoint, undefined, (error) => { return callback && callback(error); @@ -510,6 +533,7 @@ export class WebHDFS { * Sets the permission octal (sticky, owner, group & other) for a file/folder * @param path The path to the file/folder to set the permission of * @param permissionStatus The status containing the permission to set + * @param callback Callback to handle the response */ public setPermission(path: string, permissionStatus: PermissionStatus, callback: (error: HdfsError) => void): void { this.checkArgDefined('path', path); @@ -520,6 +544,19 @@ export class WebHDFS { }); } + /** + * Removes the default ACLs for the specified path + * @param path The path to remove the default ACLs for + * @param callback Callback to handle the response + */ + public removeDefaultAcl(path: string, callback: (error: HdfsError) => void): void { + this.checkArgDefined('path', path); + let endpoint = this.getOperationEndpoint('removedefaultacl', path); + this.sendRequest('PUT', endpoint, undefined, (error) => { + return callback && callback(error); + }); + } + /** * Get all mounts for a HDFS connection * @param callback Callback to handle the response @@ -550,7 +587,7 @@ export class WebHDFS { public exists(path: string, callback: (error: HdfsError, exists: boolean) => void): void { this.checkArgDefined('path', path); - this.stat(path, (error, fileStatus) => { + this.listStatus(path, (error, fileStatus) => { let exists = !fileStatus ? false : true; callback(error, exists); }); diff --git a/extensions/mssql/src/localizedConstants.ts b/extensions/mssql/src/localizedConstants.ts index 23c36c3934..bd98da8056 100644 --- a/extensions/mssql/src/localizedConstants.ts +++ b/extensions/mssql/src/localizedConstants.ts @@ -14,15 +14,16 @@ export const msgMissingNodeContext = localize('msgMissingNodeContext', 'Node Com export const manageAccessTitle = localize('mssql.manageAccessTitle', "Manage Access"); export const locationTitle = localize('mssql.locationTitle', "Location : "); 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 ownerPostfix = localize('mssql.ownerPostfix', " - Owner"); +export const owningGroupPostfix = localize('mssql.owningGroupPostfix', " - Owning Group"); +export const everyoneName = localize('mssql.everyone', "Everyone else"); 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 deleteTitle = localize('mssql.delete', "Delete"); -export const stickyHeader = localize('mssql.stickyHeader', "Sticky"); +export const stickyLabel = localize('mssql.stickyHeader', "Sticky"); +export const inheritDefaultsLabel = localize('mssql.inheritDefaultsLabel', "Inherit Defaults"); export const readHeader = localize('mssql.readHeader', "Read"); export const writeHeader = localize('mssql.writeHeader', "Write"); export const executeHeader = localize('mssql.executeHeader', "Execute"); diff --git a/extensions/mssql/src/objectExplorerNodeProvider/fileSources.ts b/extensions/mssql/src/objectExplorerNodeProvider/fileSources.ts index 4ab12349d1..2f61717d4e 100644 --- a/extensions/mssql/src/objectExplorerNodeProvider/fileSources.ts +++ b/extensions/mssql/src/objectExplorerNodeProvider/fileSources.ts @@ -17,7 +17,7 @@ import * as constants from '../constants'; import { WebHDFS, HdfsError } from '../hdfs/webhdfs'; import { AclEntry, PermissionStatus } from '../hdfs/aclEntry'; import { Mount, MountStatus } from '../hdfs/mount'; -import { FileStatus } from '../hdfs/fileStatus'; +import { FileStatus, HdfsFileType } from '../hdfs/fileStatus'; const localize = nls.loadMessageBundle(); @@ -87,6 +87,11 @@ export interface IFileSource { * @param aclEntries The ACL entries to set */ setAcl(path: string, ownerEntry: AclEntry, groupEntry: AclEntry, otherEntry: AclEntry, aclEntries: AclEntry[]): Promise; + /** + * Removes the default ACLs for the specified path + * @param path The path to remove the default ACLs for + */ + removeDefaultAcl(path: string): Promise; /** * Sets the permission octal (sticky, owner, group & other) for a file/folder * @param path The path to the file/folder to set the permission of @@ -120,11 +125,6 @@ export interface IRequestParams { headers?: {}; } -export interface IHdfsFileStatus { - type: 'FILE' | 'DIRECTORY'; - pathSuffix: string; -} - export class FileSourceFactory { private static _instance: FileSourceFactory; @@ -181,7 +181,7 @@ export class HdfsFileSource implements IFileSource { if (!this.mounts || refresh) { await this.loadMounts(); } - return this.readdir(path); + return this.listStatus(path); } private loadMounts(): Promise { @@ -196,16 +196,15 @@ export class HdfsFileSource implements IFileSource { }); } - private readdir(path: string): Promise { + private listStatus(path: string): Promise { return new Promise((resolve, reject) => { - this.client.readdir(path, (error, files) => { + this.client.listStatus(path, (error, fileStatuses) => { if (error) { reject(error); } else { - let hdfsFiles: IFile[] = files.map(fileStat => { - let hdfsFile = fileStat; - let file = new File(File.createPath(path, hdfsFile.pathSuffix), hdfsFile.type === 'DIRECTORY'); + let hdfsFiles: IFile[] = fileStatuses.map(fileStatus => { + let file = new File(File.createPath(path, fileStatus.pathSuffix), fileStatus.type === HdfsFileType.Directory); if (this.mounts && this.mounts.has(file.path)) { file.mountStatus = MountStatus.Mount; } @@ -403,6 +402,22 @@ export class HdfsFileSource implements IFileSource { }); } + /** + * Removes the default ACLs for the specified path + * @param path The path to remove the default ACLs for + */ + public removeDefaultAcl(path: string): Promise { + return new Promise((resolve, reject) => { + this.client.removeDefaultAcl(path, (error: HdfsError) => { + if (error) { + reject(error); + } else { + resolve(); + } + }); + }); + } + /** * Sets the permission octal (sticky, owner, group & other) for a file/folder * @param path The path to the file/folder to set the permission of diff --git a/extensions/mssql/src/util/arrays.ts b/extensions/mssql/src/util/arrays.ts index 7b19733ba9..e59e6305b2 100644 --- a/extensions/mssql/src/util/arrays.ts +++ b/extensions/mssql/src/util/arrays.ts @@ -19,4 +19,18 @@ export function equals(one: ReadonlyArray, other: ReadonlyArray, itemEq export function flatten(arr: ReadonlyArray[]): T[] { return ([] as T[]).concat.apply([], arr); -} \ No newline at end of file +} + +export function groupBy(data: ReadonlyArray, compare: (a: T, b: T) => number): T[][] { + const result: T[][] = []; + let currentGroup: T[] | undefined = undefined; + for (const element of data.slice(0).sort(compare)) { + if (!currentGroup || compare(currentGroup[0], element) !== 0) { + currentGroup = [element]; + result.push(currentGroup); + } else { + currentGroup.push(element); + } + } + return result; +} diff --git a/src/sql/workbench/browser/modelComponents/checkbox.component.ts b/src/sql/workbench/browser/modelComponents/checkbox.component.ts index c6e2ee1185..1ff8f3d1d9 100644 --- a/src/sql/workbench/browser/modelComponents/checkbox.component.ts +++ b/src/sql/workbench/browser/modelComponents/checkbox.component.ts @@ -20,7 +20,7 @@ import { IWorkbenchThemeService } from 'vs/workbench/services/themes/common/work @Component({ selector: 'modelview-checkbox', template: ` -
+
` }) export default class CheckBoxComponent extends ComponentBase implements IComponent, OnDestroy, AfterViewInit {