From 2e8784e9124f23c0ad84be3f33ebf49c56cee803 Mon Sep 17 00:00:00 2001 From: Lin Jian Date: Thu, 24 Sep 2020 18:42:28 +0800 Subject: [PATCH] [storage][stg74] Delegation SAS v2 and Directory SAS (#11395) * move under sas/ * wip * wip * test wip * autofill directoryDepth --- .../review/storage-file-datalake.api.md | 19 +- .../storage-file-datalake/src/index.ts | 18 +- .../src/{ => sas}/AccountSASPermissions.ts | 0 .../src/{ => sas}/AccountSASResourceTypes.ts | 0 .../src/{ => sas}/AccountSASServices.ts | 0 .../{ => sas}/AccountSASSignatureValues.ts | 6 +- .../src/{ => sas}/DataLakeSASPermissions.ts | 56 +++ .../{ => sas}/DataLakeSASSignatureValues.ts | 198 +++++++++- .../src/sas/DirectorySASPermissions.ts | 191 ++++++++++ .../src/{ => sas}/FileSystemSASPermissions.ts | 56 +++ .../src/{ => sas}/SASQueryParameters.ts | 73 +++- .../src/{ => sas}/SasIPRange.ts | 0 .../test/node/sas.spec.ts | 354 +++++++++++++++++- .../storage-file-datalake/test/utils/index.ts | 17 + 14 files changed, 950 insertions(+), 38 deletions(-) rename sdk/storage/storage-file-datalake/src/{ => sas}/AccountSASPermissions.ts (100%) rename sdk/storage/storage-file-datalake/src/{ => sas}/AccountSASResourceTypes.ts (100%) rename sdk/storage/storage-file-datalake/src/{ => sas}/AccountSASServices.ts (100%) rename sdk/storage/storage-file-datalake/src/{ => sas}/AccountSASSignatureValues.ts (96%) rename sdk/storage/storage-file-datalake/src/{ => sas}/DataLakeSASPermissions.ts (73%) rename sdk/storage/storage-file-datalake/src/{ => sas}/DataLakeSASSignatureValues.ts (75%) create mode 100644 sdk/storage/storage-file-datalake/src/sas/DirectorySASPermissions.ts rename sdk/storage/storage-file-datalake/src/{ => sas}/FileSystemSASPermissions.ts (75%) rename sdk/storage/storage-file-datalake/src/{ => sas}/SASQueryParameters.ts (82%) rename sdk/storage/storage-file-datalake/src/{ => sas}/SasIPRange.ts (100%) diff --git a/sdk/storage/storage-file-datalake/review/storage-file-datalake.api.md b/sdk/storage/storage-file-datalake/review/storage-file-datalake.api.md index 997898bf1077..53b8bdb12319 100644 --- a/sdk/storage/storage-file-datalake/review/storage-file-datalake.api.md +++ b/sdk/storage/storage-file-datalake/review/storage-file-datalake.api.md @@ -78,8 +78,12 @@ export class AccountSASPermissions { add: boolean; create: boolean; delete: boolean; + execute: boolean; list: boolean; + move: boolean; + ownership: boolean; static parse(permissions: string): AccountSASPermissions; + permission: boolean; process: boolean; read: boolean; toString(): string; @@ -259,7 +263,11 @@ export class DataLakeSASPermissions { add: boolean; create: boolean; delete: boolean; + execute: boolean; + move: boolean; + ownership: boolean; static parse(permissions: string): DataLakeSASPermissions; + permission: boolean; read: boolean; toString(): string; write: boolean; @@ -267,20 +275,25 @@ export class DataLakeSASPermissions { // @public export interface DataLakeSASSignatureValues { + authorizedUserObjectId?: string; cacheControl?: string; contentDisposition?: string; contentEncoding?: string; contentLanguage?: string; contentType?: string; + correlationId?: string; + directoryDepth?: number; expiresOn?: Date; fileSystemName: string; identifier?: string; ipRange?: SasIPRange; + isDirectory?: boolean; pathName?: string; permissions?: DataLakeSASPermissions; protocol?: SASProtocol; snapshotTime?: string; startsOn?: Date; + unauthorizedUserObjectId?: string; version?: string; } @@ -725,8 +738,12 @@ export class FileSystemSASPermissions { add: boolean; create: boolean; delete: boolean; + execute: boolean; list: boolean; + move: boolean; + ownership: boolean; static parse(permissions: string): FileSystemSASPermissions; + permission: boolean; read: boolean; toString(): string; write: boolean; @@ -1545,7 +1562,7 @@ export enum SASProtocol { // @public export class SASQueryParameters { - constructor(version: string, signature: string, permissions?: string, services?: string, resourceTypes?: string, protocol?: SASProtocol, startsOn?: Date, expiresOn?: Date, ipRange?: SasIPRange, identifier?: string, resource?: string, cacheControl?: string, contentDisposition?: string, contentEncoding?: string, contentLanguage?: string, contentType?: string, userDelegationKey?: UserDelegationKey); + constructor(version: string, signature: string, permissions?: string, services?: string, resourceTypes?: string, protocol?: SASProtocol, startsOn?: Date, expiresOn?: Date, ipRange?: SasIPRange, identifier?: string, resource?: string, cacheControl?: string, contentDisposition?: string, contentEncoding?: string, contentLanguage?: string, contentType?: string, userDelegationKey?: UserDelegationKey, directoryDepth?: number, authorizedUserObjectId?: string, unauthorizedUserObjectId?: string, correlationId?: string); readonly cacheControl?: string; readonly contentDisposition?: string; readonly contentEncoding?: string; diff --git a/sdk/storage/storage-file-datalake/src/index.ts b/sdk/storage/storage-file-datalake/src/index.ts index da80b3cb1d3c..07ebfc43e6d4 100644 --- a/sdk/storage/storage-file-datalake/src/index.ts +++ b/sdk/storage/storage-file-datalake/src/index.ts @@ -5,13 +5,13 @@ export * from "./DataLakeServiceClient"; export * from "./DataLakeFileSystemClient"; export * from "./clients"; export * from "./DataLakeLeaseClient"; -export * from "./AccountSASPermissions"; -export * from "./AccountSASResourceTypes"; -export * from "./AccountSASServices"; -export * from "./AccountSASSignatureValues"; -export * from "./DataLakeSASPermissions"; -export * from "./DataLakeSASSignatureValues"; -export * from "./FileSystemSASPermissions"; +export * from "./sas/AccountSASPermissions"; +export * from "./sas/AccountSASResourceTypes"; +export * from "./sas/AccountSASServices"; +export * from "./sas/AccountSASSignatureValues"; +export * from "./sas/DataLakeSASPermissions"; +export * from "./sas/DataLakeSASSignatureValues"; +export * from "./sas/FileSystemSASPermissions"; export * from "./StorageBrowserPolicyFactory"; export * from "./credentials/AnonymousCredential"; export * from "./credentials/Credential"; @@ -21,10 +21,10 @@ export * from "./policies/AnonymousCredentialPolicy"; export * from "./policies/CredentialPolicy"; export * from "./StorageRetryPolicyFactory"; export * from "./policies/StorageSharedKeyCredentialPolicy"; -export * from "./SASQueryParameters"; +export * from "./sas/SASQueryParameters"; export * from "./models"; export { CommonOptions } from "./StorageClient"; -export { SasIPRange } from "./SasIPRange"; +export { SasIPRange } from "./sas/SasIPRange"; export { ToBlobEndpointHostMappings, ToDfsEndpointHostMappings } from "./utils/constants"; export { RestError } from "@azure/core-http"; export { logger } from "./log"; diff --git a/sdk/storage/storage-file-datalake/src/AccountSASPermissions.ts b/sdk/storage/storage-file-datalake/src/sas/AccountSASPermissions.ts similarity index 100% rename from sdk/storage/storage-file-datalake/src/AccountSASPermissions.ts rename to sdk/storage/storage-file-datalake/src/sas/AccountSASPermissions.ts diff --git a/sdk/storage/storage-file-datalake/src/AccountSASResourceTypes.ts b/sdk/storage/storage-file-datalake/src/sas/AccountSASResourceTypes.ts similarity index 100% rename from sdk/storage/storage-file-datalake/src/AccountSASResourceTypes.ts rename to sdk/storage/storage-file-datalake/src/sas/AccountSASResourceTypes.ts diff --git a/sdk/storage/storage-file-datalake/src/AccountSASServices.ts b/sdk/storage/storage-file-datalake/src/sas/AccountSASServices.ts similarity index 100% rename from sdk/storage/storage-file-datalake/src/AccountSASServices.ts rename to sdk/storage/storage-file-datalake/src/sas/AccountSASServices.ts diff --git a/sdk/storage/storage-file-datalake/src/AccountSASSignatureValues.ts b/sdk/storage/storage-file-datalake/src/sas/AccountSASSignatureValues.ts similarity index 96% rename from sdk/storage/storage-file-datalake/src/AccountSASSignatureValues.ts rename to sdk/storage/storage-file-datalake/src/sas/AccountSASSignatureValues.ts index f0cd84e68f3e..e74aed7efb52 100644 --- a/sdk/storage/storage-file-datalake/src/AccountSASSignatureValues.ts +++ b/sdk/storage/storage-file-datalake/src/sas/AccountSASSignatureValues.ts @@ -4,11 +4,11 @@ import { AccountSASPermissions } from "./AccountSASPermissions"; import { AccountSASResourceTypes } from "./AccountSASResourceTypes"; import { AccountSASServices } from "./AccountSASServices"; -import { StorageSharedKeyCredential } from "./credentials/StorageSharedKeyCredential"; +import { StorageSharedKeyCredential } from "../credentials/StorageSharedKeyCredential"; import { SasIPRange, ipRangeToString } from "./SasIPRange"; import { SASProtocol, SASQueryParameters } from "./SASQueryParameters"; -import { SERVICE_VERSION } from "./utils/constants"; -import { truncatedISO8061Date } from "./utils/utils.common"; +import { SERVICE_VERSION } from "../utils/constants"; +import { truncatedISO8061Date } from "../utils/utils.common"; /** * ONLY AVAILABLE IN NODE.JS RUNTIME. diff --git a/sdk/storage/storage-file-datalake/src/DataLakeSASPermissions.ts b/sdk/storage/storage-file-datalake/src/sas/DataLakeSASPermissions.ts similarity index 73% rename from sdk/storage/storage-file-datalake/src/DataLakeSASPermissions.ts rename to sdk/storage/storage-file-datalake/src/sas/DataLakeSASPermissions.ts index 2d59ae67829b..fbb7a25b3775 100644 --- a/sdk/storage/storage-file-datalake/src/DataLakeSASPermissions.ts +++ b/sdk/storage/storage-file-datalake/src/sas/DataLakeSASPermissions.ts @@ -43,6 +43,18 @@ export class DataLakeSASPermissions { case "d": blobSASPermissions.delete = true; break; + case "m": + blobSASPermissions.move = true; + break; + case "e": + blobSASPermissions.execute = true; + break; + case "o": + blobSASPermissions.ownership = true; + break; + case "p": + blobSASPermissions.permission = true; + break; default: throw new RangeError(`Invalid permission: ${char}`); } @@ -91,6 +103,38 @@ export class DataLakeSASPermissions { */ public delete: boolean = false; + /** + * Specifies Move access granted. + * + * @type {boolean} + * @memberof DirectorySASPermissions + */ + public move: boolean = false; + + /** + * Specifies Execute access granted. + * + * @type {boolean} + * @memberof DirectorySASPermissions + */ + public execute: boolean = false; + + /** + * Specifies Ownership access granted. + * + * @type {boolean} + * @memberof DirectorySASPermissions + */ + public ownership: boolean = false; + + /** + * Specifies Permission access granted. + * + * @type {boolean} + * @memberof DirectorySASPermissions + */ + public permission: boolean = false; + /** * Converts the given permissions to a string. Using this method will guarantee the permissions are in an * order accepted by the service. @@ -115,6 +159,18 @@ export class DataLakeSASPermissions { if (this.delete) { permissions.push("d"); } + if (this.move) { + permissions.push("m"); + } + if (this.execute) { + permissions.push("e"); + } + if (this.ownership) { + permissions.push("o"); + } + if (this.permission) { + permissions.push("p"); + } return permissions.join(""); } } diff --git a/sdk/storage/storage-file-datalake/src/DataLakeSASSignatureValues.ts b/sdk/storage/storage-file-datalake/src/sas/DataLakeSASSignatureValues.ts similarity index 75% rename from sdk/storage/storage-file-datalake/src/DataLakeSASSignatureValues.ts rename to sdk/storage/storage-file-datalake/src/sas/DataLakeSASSignatureValues.ts index 1d59a70dcf00..b3858df08803 100644 --- a/sdk/storage/storage-file-datalake/src/DataLakeSASSignatureValues.ts +++ b/sdk/storage/storage-file-datalake/src/sas/DataLakeSASSignatureValues.ts @@ -1,14 +1,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { StorageSharedKeyCredential } from "./credentials/StorageSharedKeyCredential"; -import { UserDelegationKeyCredential } from "./credentials/UserDelegationKeyCredential"; +import { StorageSharedKeyCredential } from "../credentials/StorageSharedKeyCredential"; +import { UserDelegationKeyCredential } from "../credentials/UserDelegationKeyCredential"; import { DataLakeSASPermissions } from "./DataLakeSASPermissions"; import { FileSystemSASPermissions } from "./FileSystemSASPermissions"; -import { UserDelegationKey } from "./models"; +import { UserDelegationKey } from "../models"; import { ipRangeToString, SasIPRange } from "./SasIPRange"; import { SASProtocol, SASQueryParameters } from "./SASQueryParameters"; -import { SERVICE_VERSION } from "./utils/constants"; -import { truncatedISO8061Date } from "./utils/utils.common"; +import { SERVICE_VERSION } from "../utils/constants"; +import { truncatedISO8061Date } from "../utils/utils.common"; +import { DirectorySASPermissions } from "./DirectorySASPermissions"; /** * ONLY AVAILABLE IN NODE.JS RUNTIME. @@ -86,11 +87,63 @@ export interface DataLakeSASSignatureValues { */ pathName?: string; + /** + * Optional. Beginning in version 2020-02-10, this value defines whether or not the {@link pathName} is a directory. + * If this value is set to true, the Path is a Directory for a Directory SAS. If set to false or default, the Path + * is a File Path for a File Path SAS. + * + * @type {boolean} + * @memberof DataLakeSASSignatureValues + */ + isDirectory?: boolean; + + /** + * Optional. Beginning in version 2020-02-10, indicate the depth of the directory + * specified in the canonicalizedresource field of the string-to-sign. The depth of the directory is the number of directories + * beneath the root folder. + * + * @type {number} + * @memberof DataLakeSASSignatureValues + */ + directoryDepth?: number; + + /** + * Optional. Beginning in version 2020-02-10, specifies the Authorized AAD Object Id in GUID format. The AAD Object ID of a user + * authorized by the owner of the User Delegation Key to perform the action granted by the SAS. The Azure Storage service will + * ensure that the owner of the user delegation key has the required permissions before granting access but no additional permission + * check for the user specified in this value will be performed. This cannot be used in conjuction with {@link unauthorizedUserObjectId}. + * This is only used for User Delegation SAS. + * + * @type {string} + * @memberof DataLakeSASSignatureValues + */ + authorizedUserObjectId?: string; + + /** + * Optional. Beginning in version 2020-02-10, specifies the Unauthorized AAD Object Id in GUID format. The AAD Object Id of a user that is assumed + * to be unauthorized by the owner of the User Delegation Key. The Azure Storage Service will perform an additional POSIX ACL check to determine + * if the user is authorized to perform the requested operation. This cannot be used in conjuction with {@link authorizedUserObjectId}. + * This is only used for User Delegation SAS. + * + * @type {string} + * @memberof DataLakeSASSignatureValues + */ + unauthorizedUserObjectId?: string; + + /** + * Optional. Beginning in version 2020-02-10, this is a GUID value that will be logged in the storage diagnostic logs and can be used to + * correlate SAS generation with storage resource access. This is only used for User Delegation SAS. + * + * @type {string} + * @memberof DataLakeSASSignatureValues + */ + correlationId?: string; + /** * Optional. Snapshot timestamp string the SAS user may access. Only supported from API version 2018-11-09. * * @type {string} - * @memberof IBlobSASSignatureValues + * @memberof DataLakeSASSignatureValues */ snapshotTime?: string; @@ -342,6 +395,11 @@ function generateBlobSASQueryParameters20150405( throw RangeError("'version' must be >= '2018-11-09' when provided 'snapshotTime'."); } + dataLakeSASSignatureValues = SASSignatureValuesSanityCheckAndAutofill( + dataLakeSASSignatureValues, + version + ); + // Calling parse and toString guarantees the proper ordering and throws on invalid characters. if (dataLakeSASSignatureValues.permissions) { if (dataLakeSASSignatureValues.pathName) { @@ -449,15 +507,27 @@ function generateBlobSASQueryParameters20181109( throw RangeError("Must provide 'blobName' when provided 'snapshotTime'."); } + dataLakeSASSignatureValues = SASSignatureValuesSanityCheckAndAutofill( + dataLakeSASSignatureValues, + version + ); + // Calling parse and toString guarantees the proper ordering and throws on invalid characters. if (dataLakeSASSignatureValues.permissions) { if (dataLakeSASSignatureValues.pathName) { - verifiedPermissions = DataLakeSASPermissions.parse( - dataLakeSASSignatureValues.permissions.toString() - ).toString(); - resource = "b"; - if (dataLakeSASSignatureValues.snapshotTime) { - resource = "bs"; + if (dataLakeSASSignatureValues.isDirectory) { + verifiedPermissions = DirectorySASPermissions.parse( + dataLakeSASSignatureValues.permissions.toString() + ).toString(); + resource = "d"; + } else { + verifiedPermissions = DataLakeSASPermissions.parse( + dataLakeSASSignatureValues.permissions.toString() + ).toString(); + resource = "b"; + if (dataLakeSASSignatureValues.snapshotTime) { + resource = "bs"; + } } } else { verifiedPermissions = FileSystemSASPermissions.parse( @@ -513,7 +583,9 @@ function generateBlobSASQueryParameters20181109( dataLakeSASSignatureValues.contentDisposition, dataLakeSASSignatureValues.contentEncoding, dataLakeSASSignatureValues.contentLanguage, - dataLakeSASSignatureValues.contentType + dataLakeSASSignatureValues.contentType, + undefined, + dataLakeSASSignatureValues.directoryDepth ); } @@ -555,15 +627,27 @@ function generateBlobSASQueryParametersUDK20181109( throw RangeError("Must provide 'blobName' when provided 'snapshotTime'."); } + dataLakeSASSignatureValues = SASSignatureValuesSanityCheckAndAutofill( + dataLakeSASSignatureValues, + version + ); + // Calling parse and toString guarantees the proper ordering and throws on invalid characters. if (dataLakeSASSignatureValues.permissions) { if (dataLakeSASSignatureValues.pathName) { - verifiedPermissions = DataLakeSASPermissions.parse( - dataLakeSASSignatureValues.permissions.toString() - ).toString(); - resource = "b"; - if (dataLakeSASSignatureValues.snapshotTime) { - resource = "bs"; + if (dataLakeSASSignatureValues.isDirectory) { + verifiedPermissions = DirectorySASPermissions.parse( + dataLakeSASSignatureValues.permissions.toString() + ).toString(); + resource = "d"; + } else { + verifiedPermissions = DataLakeSASPermissions.parse( + dataLakeSASSignatureValues.permissions.toString() + ).toString(); + resource = "b"; + if (dataLakeSASSignatureValues.snapshotTime) { + resource = "bs"; + } } } else { verifiedPermissions = FileSystemSASPermissions.parse( @@ -596,6 +680,9 @@ function generateBlobSASQueryParametersUDK20181109( : "", userDelegationKeyCredential.userDelegationKey.signedService, userDelegationKeyCredential.userDelegationKey.signedVersion, + dataLakeSASSignatureValues.authorizedUserObjectId, + dataLakeSASSignatureValues.unauthorizedUserObjectId, + dataLakeSASSignatureValues.correlationId, dataLakeSASSignatureValues.ipRange ? ipRangeToString(dataLakeSASSignatureValues.ipRange) : "", dataLakeSASSignatureValues.protocol ? dataLakeSASSignatureValues.protocol : "", version, @@ -627,7 +714,11 @@ function generateBlobSASQueryParametersUDK20181109( dataLakeSASSignatureValues.contentEncoding, dataLakeSASSignatureValues.contentLanguage, dataLakeSASSignatureValues.contentType, - userDelegationKeyCredential.userDelegationKey + userDelegationKeyCredential.userDelegationKey, + dataLakeSASSignatureValues.directoryDepth, + dataLakeSASSignatureValues.authorizedUserObjectId, + dataLakeSASSignatureValues.unauthorizedUserObjectId, + dataLakeSASSignatureValues.correlationId ); } @@ -640,3 +731,70 @@ function getCanonicalName(accountName: string, containerName: string, blobName?: } return elements.join(""); } + +function SASSignatureValuesSanityCheckAndAutofill( + dataLakeSASSignatureValues: DataLakeSASSignatureValues, + version: string +): DataLakeSASSignatureValues { + if ( + version < "2020-02-10" && + (dataLakeSASSignatureValues.isDirectory || dataLakeSASSignatureValues.directoryDepth) + ) { + throw RangeError("'version' must be >= '2020-02-10' to support directory SAS."); + } + if (dataLakeSASSignatureValues.isDirectory && dataLakeSASSignatureValues.pathName === undefined) { + throw RangeError("Must provide 'pathName' when 'isDirectory' is true."); + } + if ( + dataLakeSASSignatureValues.directoryDepth !== undefined && + (!Number.isInteger(dataLakeSASSignatureValues.directoryDepth) || + dataLakeSASSignatureValues.directoryDepth < 0) + ) { + throw RangeError("'directoryDepth' must be a non-negative interger."); + } + if ( + dataLakeSASSignatureValues.isDirectory && + dataLakeSASSignatureValues.directoryDepth === undefined + ) { + // calculate directoryDepth from pathName + if (dataLakeSASSignatureValues.pathName === "/") { + dataLakeSASSignatureValues.directoryDepth = 0; + } else { + dataLakeSASSignatureValues.directoryDepth = dataLakeSASSignatureValues.pathName + ?.split("/") + .filter((x) => x !== "").length; + } + } + + if ( + version < "2020-02-10" && + dataLakeSASSignatureValues.permissions && + (dataLakeSASSignatureValues.permissions.move || + dataLakeSASSignatureValues.permissions.execute || + dataLakeSASSignatureValues.permissions.ownership || + dataLakeSASSignatureValues.permissions.permission) + ) { + throw RangeError("'version' must be >= '2020-02-10' when providing m, e, o or p permission."); + } + + if ( + version < "2020-02-10" && + (dataLakeSASSignatureValues.authorizedUserObjectId || + dataLakeSASSignatureValues.unauthorizedUserObjectId || + dataLakeSASSignatureValues.correlationId) + ) { + throw RangeError( + "'version' must be >= '2020-02-10' when providing 'authorizedUserObjectId', 'unauthorizedUserObjectId' or 'correlationId'." + ); + } + if ( + dataLakeSASSignatureValues.authorizedUserObjectId && + dataLakeSASSignatureValues.unauthorizedUserObjectId + ) { + throw RangeError( + "'authorizedUserObjectId' or 'unauthorizedUserObjectId' shouldn't be specified at the same time." + ); + } + + return dataLakeSASSignatureValues; +} diff --git a/sdk/storage/storage-file-datalake/src/sas/DirectorySASPermissions.ts b/sdk/storage/storage-file-datalake/src/sas/DirectorySASPermissions.ts new file mode 100644 index 000000000000..0bfcca7126c6 --- /dev/null +++ b/sdk/storage/storage-file-datalake/src/sas/DirectorySASPermissions.ts @@ -0,0 +1,191 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * This is a helper class to construct a string representing the permissions granted by a ServiceSAS to a directory. + * Setting a value to true means that any SAS which uses these permissions will grant permissions for that operation. + * Once all the values are set, this should be serialized with toString and set as the permissions field on a + * {@link DataLakeSASSignatureValues} object. It is possible to construct the permissions string without this class, but + * the order of the permissions is particular and this class guarantees correctness. + * + * @export + * @class DirectorySASPermissions + */ +export class DirectorySASPermissions { + /** + * Creates an {@link DirectorySASPermissions} from the specified permissions string. This method will throw an + * Error if it encounters a character that does not correspond to a valid permission. + * + * @static + * @param {string} permissions + * @returns {DirectorySASPermissions} + * @memberof DirectorySASPermissions + */ + public static parse(permissions: string) { + const directorySASPermissions = new DirectorySASPermissions(); + + for (const char of permissions) { + switch (char) { + case "r": + directorySASPermissions.read = true; + break; + case "a": + directorySASPermissions.add = true; + break; + case "c": + directorySASPermissions.create = true; + break; + case "w": + directorySASPermissions.write = true; + break; + case "d": + directorySASPermissions.delete = true; + break; + case "l": + directorySASPermissions.list = true; + break; + case "m": + directorySASPermissions.move = true; + break; + case "e": + directorySASPermissions.execute = true; + break; + case "o": + directorySASPermissions.ownership = true; + break; + case "p": + directorySASPermissions.permission = true; + break; + default: + throw new RangeError(`Invalid permission ${char}`); + } + } + + return directorySASPermissions; + } + + /** + * Specifies Read access granted. + * + * @type {boolean} + * @memberof DirectorySASPermissions + */ + public read: boolean = false; + + /** + * Specifies Add access granted. + * + * @type {boolean} + * @memberof DirectorySASPermissions + */ + public add: boolean = false; + + /** + * Specifies Create access granted. + * + * @type {boolean} + * @memberof DirectorySASPermissions + */ + public create: boolean = false; + + /** + * Specifies Write access granted. + * + * @type {boolean} + * @memberof DirectorySASPermissions + */ + public write: boolean = false; + + /** + * Specifies Delete access granted. + * + * @type {boolean} + * @memberof DirectorySASPermissions + */ + public delete: boolean = false; + + /** + * Specifies List access granted. + * + * @type {boolean} + * @memberof DirectorySASPermissions + */ + public list: boolean = false; + + /** + * Specifies Move access granted. + * + * @type {boolean} + * @memberof DirectorySASPermissions + */ + public move: boolean = false; + + /** + * Specifies Execute access granted. + * + * @type {boolean} + * @memberof DirectorySASPermissions + */ + public execute: boolean = false; + + /** + * Specifies Ownership access granted. + * + * @type {boolean} + * @memberof DirectorySASPermissions + */ + public ownership: boolean = false; + + /** + * Specifies Permission access granted. + * + * @type {boolean} + * @memberof DirectorySASPermissions + */ + public permission: boolean = false; + + /** + * Converts the given permissions to a string. Using this method will guarantee the permissions are in an + * order accepted by the service. + * + * The order of the characters should be as specified here to ensure correctness. + * @see https://docs.microsoft.com/en-us/rest/api/storageservices/constructing-a-service-sas + * + * @returns {string} + * @memberof DirectorySASPermissions + */ + public toString(): string { + const permissions: string[] = []; + if (this.read) { + permissions.push("r"); + } + if (this.add) { + permissions.push("a"); + } + if (this.create) { + permissions.push("c"); + } + if (this.write) { + permissions.push("w"); + } + if (this.delete) { + permissions.push("d"); + } + if (this.list) { + permissions.push("l"); + } + if (this.move) { + permissions.push("m"); + } + if (this.execute) { + permissions.push("e"); + } + if (this.ownership) { + permissions.push("o"); + } + if (this.permission) { + permissions.push("p"); + } + return permissions.join(""); + } +} diff --git a/sdk/storage/storage-file-datalake/src/FileSystemSASPermissions.ts b/sdk/storage/storage-file-datalake/src/sas/FileSystemSASPermissions.ts similarity index 75% rename from sdk/storage/storage-file-datalake/src/FileSystemSASPermissions.ts rename to sdk/storage/storage-file-datalake/src/sas/FileSystemSASPermissions.ts index 319e31ef213a..44fc86deafa5 100644 --- a/sdk/storage/storage-file-datalake/src/FileSystemSASPermissions.ts +++ b/sdk/storage/storage-file-datalake/src/sas/FileSystemSASPermissions.ts @@ -44,6 +44,18 @@ export class FileSystemSASPermissions { case "l": containerSASPermissions.list = true; break; + case "m": + containerSASPermissions.move = true; + break; + case "e": + containerSASPermissions.execute = true; + break; + case "o": + containerSASPermissions.ownership = true; + break; + case "p": + containerSASPermissions.permission = true; + break; default: throw new RangeError(`Invalid permission ${char}`); } @@ -100,6 +112,38 @@ export class FileSystemSASPermissions { */ public list: boolean = false; + /** + * Specifies Move access granted. + * + * @type {boolean} + * @memberof DirectorySASPermissions + */ + public move: boolean = false; + + /** + * Specifies Execute access granted. + * + * @type {boolean} + * @memberof DirectorySASPermissions + */ + public execute: boolean = false; + + /** + * Specifies Ownership access granted. + * + * @type {boolean} + * @memberof DirectorySASPermissions + */ + public ownership: boolean = false; + + /** + * Specifies Permission access granted. + * + * @type {boolean} + * @memberof DirectorySASPermissions + */ + public permission: boolean = false; + /** * Converts the given permissions to a string. Using this method will guarantee the permissions are in an * order accepted by the service. @@ -130,6 +174,18 @@ export class FileSystemSASPermissions { if (this.list) { permissions.push("l"); } + if (this.move) { + permissions.push("m"); + } + if (this.execute) { + permissions.push("e"); + } + if (this.ownership) { + permissions.push("o"); + } + if (this.permission) { + permissions.push("p"); + } return permissions.join(""); } } diff --git a/sdk/storage/storage-file-datalake/src/SASQueryParameters.ts b/sdk/storage/storage-file-datalake/src/sas/SASQueryParameters.ts similarity index 82% rename from sdk/storage/storage-file-datalake/src/SASQueryParameters.ts rename to sdk/storage/storage-file-datalake/src/sas/SASQueryParameters.ts index fbce2ebe2441..30df58d80009 100644 --- a/sdk/storage/storage-file-datalake/src/SASQueryParameters.ts +++ b/sdk/storage/storage-file-datalake/src/sas/SASQueryParameters.ts @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { UserDelegationKey } from "./models"; +import { UserDelegationKey } from "../models"; import { ipRangeToString, SasIPRange } from "./SasIPRange"; -import { truncatedISO8061Date } from "./utils/utils.common"; +import { truncatedISO8061Date } from "../utils/utils.common"; /** * Protocols for generated SAS. @@ -230,6 +230,47 @@ export class SASQueryParameters { */ private readonly signedVersion?: string; + /** + * Indicate the depth of the directory specified in the canonicalizedresource field of the string-to-sign. + * The depth of the directory is the number of directories beneath the root folder. + * + * @private + * @type {number} + * @memberof SASQueryParameters + */ + private readonly directoryDepth?: number; + + /** + * Authorized AAD Object Id in GUID format. The AAD Object ID of a user authorized by the owner of the User Delegation Key + * to perform the action granted by the SAS. The Azure Storage service will ensure that the owner of the user delegation key + * has the required permissions before granting access but no additional permission check for the user specified in + * this value will be performed. This cannot be used in conjuction with {@link signedUnauthorizedUserObjectId}. + * + * @private + * @type {string} + * @memberof SASQueryParameters + */ + private readonly authorizedUserObjectId?: string; + + /** + * Unauthorized AAD Object Id in GUID format. The AAD Object Id of a user that is assumed to be unauthorized by the owner of the User Delegation Key. + * The Azure Storage Service will perform an additional POSIX ACL check to determine if the user is authorized to perform the requested operation. + * This cannot be used in conjuction with {@link signedAuthorizedUserObjectId}. + * + * @private + * @type {string} + * @memberof SASQueryParameters + */ + private readonly unauthorizedUserObjectId?: string; + + /** + * A GUID value that will be logged in the storage diagnostic logs and can be used to correlate SAS generation with storage resource access. + * + * @type {string} + * @memberof SASQueryParameters + */ + private readonly correlationId?: string; + /** * Optional. IP range allowed for this SAS. * @@ -286,7 +327,11 @@ export class SASQueryParameters { contentEncoding?: string, contentLanguage?: string, contentType?: string, - userDelegationKey?: UserDelegationKey + userDelegationKey?: UserDelegationKey, + directoryDepth?: number, + authorizedUserObjectId?: string, + unauthorizedUserObjectId?: string, + correlationId?: string ) { this.version = version; this.services = services; @@ -304,6 +349,10 @@ export class SASQueryParameters { this.contentEncoding = contentEncoding; this.contentLanguage = contentLanguage; this.contentType = contentType; + this.directoryDepth = directoryDepth; + this.authorizedUserObjectId = authorizedUserObjectId; + this.unauthorizedUserObjectId = unauthorizedUserObjectId; + this.correlationId = correlationId; if (userDelegationKey) { this.signedOid = userDelegationKey.signedObjectId; @@ -344,7 +393,11 @@ export class SASQueryParameters { "rscd", "rsce", "rscl", - "rsct" + "rsct", + "sdd", + "saoid", + "suoid", + "scid" ]; const queries: string[] = []; @@ -436,6 +489,18 @@ export class SASQueryParameters { case "rsct": this.tryAppendQueryParameter(queries, param, this.contentType); break; + case "sdd": + this.tryAppendQueryParameter(queries, param, this.directoryDepth?.toString()); + break; + case "saoid": + this.tryAppendQueryParameter(queries, param, this.authorizedUserObjectId); + break; + case "suoid": + this.tryAppendQueryParameter(queries, param, this.unauthorizedUserObjectId); + break; + case "scid": + this.tryAppendQueryParameter(queries, param, this.correlationId); + break; } } return queries.join("&"); diff --git a/sdk/storage/storage-file-datalake/src/SasIPRange.ts b/sdk/storage/storage-file-datalake/src/sas/SasIPRange.ts similarity index 100% rename from sdk/storage/storage-file-datalake/src/SasIPRange.ts rename to sdk/storage/storage-file-datalake/src/sas/SasIPRange.ts diff --git a/sdk/storage/storage-file-datalake/test/node/sas.spec.ts b/sdk/storage/storage-file-datalake/test/node/sas.spec.ts index 045a8c00fd55..a9a755834001 100644 --- a/sdk/storage/storage-file-datalake/test/node/sas.spec.ts +++ b/sdk/storage/storage-file-datalake/test/node/sas.spec.ts @@ -1,3 +1,4 @@ +import { UserDelegationKey } from "@azure/storage-blob"; import { record, Recorder } from "@azure/test-utils-recorder"; import * as assert from "assert"; @@ -6,6 +7,7 @@ import { AccountSASResourceTypes, AccountSASServices, AnonymousCredential, + DataLakeDirectoryClient, DataLakeFileSystemClient, DataLakeSASPermissions, DataLakeServiceClient, @@ -13,16 +15,22 @@ import { generateAccountSASQueryParameters, generateDataLakeSASQueryParameters, newPipeline, + PathAccessControlItem, + PathPermissions, StorageSharedKeyCredential } from "../../src"; import { DataLakeFileClient } from "../../src/"; -import { SASProtocol } from "../../src/SASQueryParameters"; +import { SASProtocol } from "../../src/sas/SASQueryParameters"; import { getDataLakeServiceClient, + // getDataLakeServiceClientWithDefualtCredential, getTokenDataLakeServiceClient, recorderEnvSetup } from "../utils"; +import { setLogLevel } from "@azure/logger"; +setLogLevel("info"); + describe("Shared Access Signature (SAS) generation Node.js only", () => { let recorder: Recorder; let serviceClient: DataLakeServiceClient; @@ -622,3 +630,347 @@ describe("Shared Access Signature (SAS) generation Node.js only", () => { await fileSystemClient.delete(); }); }); + +describe("Shared Access Signature (SAS) generation Node.js only for permissions m, e, o, p", () => { + let recorder: Recorder; + let serviceClient: DataLakeServiceClient; + let fileSystemClient: DataLakeFileSystemClient; + let directoryClient: DataLakeDirectoryClient; + let fileClient: DataLakeFileClient; + let sharedKeyCredential: StorageSharedKeyCredential; + let now: Date; + let tmr: Date; + + const permissions: PathPermissions = { + extendedAcls: false, + stickyBit: true, + owner: { + read: true, + write: true, + execute: false + }, + group: { + read: true, + write: false, + execute: true + }, + other: { + read: false, + write: true, + execute: false + } + }; + + // const acl: PathAccessControlItem[] = [ + // { + // accessControlType: "user", + // entityId: "", + // defaultScope: false, + // permissions: { + // read: true, + // write: true, + // execute: true + // } + // }, + // { + // accessControlType: "group", + // entityId: "", + // defaultScope: false, + // permissions: { + // read: true, + // write: false, + // execute: true + // } + // }, + // { + // accessControlType: "other", + // entityId: "", + // defaultScope: false, + // permissions: { + // read: false, + // write: true, + // execute: false + // } + // } + // ]; + + beforeEach(async function() { + recorder = record(this, recorderEnvSetup); + serviceClient = getDataLakeServiceClient(); + + const fileSystemName = recorder.getUniqueName("filesystem"); + fileSystemClient = serviceClient.getFileSystemClient(fileSystemName); + await fileSystemClient.create(); + + const directoryName = recorder.getUniqueName("directory"); + directoryClient = fileSystemClient.getDirectoryClient(directoryName); + await directoryClient.create(); + + const fileName = recorder.getUniqueName("file"); + fileClient = directoryClient.getFileClient(fileName); + await fileClient.create(); + + now = recorder.newDate("now"); + now.setMinutes(now.getMinutes() - 10); // Skip clock skew with server + tmr = recorder.newDate("tmr"); + tmr.setDate(tmr.getDate() + 10); + + // By default, credential is always the last element of pipeline factories + const factories = (serviceClient as any).pipeline.factories; + sharedKeyCredential = factories[factories.length - 1]; + }); + + afterEach(async function() { + await fileSystemClient.delete(); + await recorder.stop(); + }); + + it("generateDataLakeSASQueryParameters for directory should work for permissions m, e, o, p", async () => { + const directorySAS = generateDataLakeSASQueryParameters( + { + fileSystemName: fileSystemClient.name, + pathName: directoryClient.name, + isDirectory: true, + directoryDepth: 1, + expiresOn: tmr, + ipRange: { start: "0.0.0.0", end: "255.255.255.255" }, + permissions: FileSystemSASPermissions.parse("racwdlmeop"), + protocol: SASProtocol.HttpsAndHttp, + startsOn: now, + version: "2020-02-10" + }, + sharedKeyCredential as StorageSharedKeyCredential + ); + const sasURL = `${directoryClient.url}?${directorySAS}`; + const directoryClientwithSAS = new DataLakeDirectoryClient( + sasURL, + newPipeline(new AnonymousCredential()) + ); + + // Does not work yet. + // m + // const newFileName = recorder.getUniqueName("newfile"); + // await directoryClientwithSAS.move(newFileName); + + // o + // const guid = "ca761232ed4211cebacd00aa0057b223"; + // await directoryClientwithSAS.setAccessControl(acl, { owner: guid }); + + // e + await directoryClientwithSAS.getAccessControl(); + + // p + await directoryClientwithSAS.setPermissions(permissions); + }); + + it("generateDataLakeSASQueryParameters for file should work for permissions m, e, o, p", async () => { + const fileSAS = generateDataLakeSASQueryParameters( + { + fileSystemName: fileSystemClient.name, + pathName: fileClient.name, + expiresOn: tmr, + ipRange: { start: "0.0.0.0", end: "255.255.255.255" }, + permissions: DataLakeSASPermissions.parse("racwdmeop"), + protocol: SASProtocol.HttpsAndHttp, + startsOn: now, + version: "2020-02-10" + }, + sharedKeyCredential as StorageSharedKeyCredential + ); + const sasURL = `${fileClient.url}?${fileSAS}`; + const fileClientWithSAS = new DataLakeFileClient( + sasURL, + newPipeline(new AnonymousCredential()) + ); + + // Does not work yet. + // m + // const newFileName = recorder.getUniqueName("newfile"); + // await fileClientWithSAS.move(newFileName); + + // o + // const guid = "ca761232ed4211cebacd00aa0057b223"; + // await fileClientWithSAS.setAccessControl(acl, { owner: guid }); + + // e + await fileClientWithSAS.getAccessControl(); + + // p + await fileClientWithSAS.setPermissions(permissions); + }); + + it("generateDataLakeSASQueryParameters for filesystem should work for permissions m, e, o, p", async () => { + const fileSystemSAS = generateDataLakeSASQueryParameters( + { + fileSystemName: fileSystemClient.name, + expiresOn: tmr, + ipRange: { start: "0.0.0.0", end: "255.255.255.255" }, + permissions: FileSystemSASPermissions.parse("racwdlmeop"), + protocol: SASProtocol.HttpsAndHttp, + startsOn: now, + version: "2020-02-10" + }, + sharedKeyCredential as StorageSharedKeyCredential + ); + const sasURL = `${directoryClient.url}?${fileSystemSAS}`; + const directoryClientwithSAS = new DataLakeDirectoryClient( + sasURL, + newPipeline(new AnonymousCredential()) + ); + + // Does not work yet. + // m + // const newFileName = recorder.getUniqueName("newfile"); + // await directoryClientwithSAS.move(newFileName); + + // o + // const guid = "ca761232ed4211cebacd00aa0057b223"; + // await directoryClientwithSAS.setAccessControl(acl, { owner: guid }); + + // e + await directoryClientwithSAS.getAccessControl(); + + // p + await directoryClientwithSAS.setPermissions(permissions); + }); +}); + +describe("Shared Access Signature (SAS) generation Node.js only for delegation SAS", () => { + let recorder: Recorder; + let serviceClient: DataLakeServiceClient; + let fileSystemClient: DataLakeFileSystemClient; + let directoryClient: DataLakeDirectoryClient; + let fileClient: DataLakeFileClient; + let userDelegationKey: UserDelegationKey; + let now: Date; + let tmr: Date; + let accountName: string; + + const permissions: PathPermissions = { + extendedAcls: false, + stickyBit: true, + owner: { + read: true, + write: true, + execute: false + }, + group: { + read: true, + write: false, + execute: true + }, + other: { + read: false, + write: true, + execute: false + } + }; + + beforeEach(async function() { + recorder = record(this, recorderEnvSetup); + accountName = process.env["DFS_ACCOUNT_NAME"] || ""; + try { + serviceClient = getTokenDataLakeServiceClient(); + } catch (err) { + this.skip(); + } + + now = recorder.newDate("now"); + now.setHours(now.getHours() - 1); + tmr = recorder.newDate("tmr"); + tmr.setDate(tmr.getDate() + 5); + userDelegationKey = await serviceClient.getUserDelegationKey(now, tmr); + + const fileSystemName = recorder.getUniqueName("filesystem"); + fileSystemClient = serviceClient.getFileSystemClient(fileSystemName); + await fileSystemClient.create(); + + const directoryName = recorder.getUniqueName("directory"); + directoryClient = fileSystemClient.getDirectoryClient(directoryName); + await directoryClient.create(); + + const fileName = recorder.getUniqueName("file"); + fileClient = directoryClient.getFileClient(fileName); + await fileClient.create(); + }); + + afterEach(async function() { + if (fileSystemClient) { + await fileSystemClient.delete(); + } + await recorder.stop(); + }); + + it.only("GenerateUserDelegationSAS for directory should work for permissions m, e, o, p", async () => { + const fileSystemSAS = generateDataLakeSASQueryParameters( + { + fileSystemName: fileSystemClient.name, + expiresOn: tmr, + permissions: FileSystemSASPermissions.parse("racwdlmeop") + }, + userDelegationKey, + accountName + ); + + const sasURL = `${directoryClient.url}?${fileSystemSAS}`; + const directoryClientwithSAS = new DataLakeDirectoryClient(sasURL); + + // Does not work yet. + // // m + // const newFileName = recorder.getUniqueName("newfile"); + // await directoryClientwithSAS.move(newFileName); + + // // o + // const guid = "ca761232ed4211cebacd00aa0057b223"; + // await directoryClientwithSAS.setAccessControl([], { owner: guid }); + + // e + await directoryClientwithSAS.getAccessControl(); + + // p + await directoryClientwithSAS.setPermissions(permissions); + }); + + it("GenerateUserDelegationSAS should work with unauthorizedUserObjectId", async () => { + const guid = "b77d5205-ddb5-42e1-80ee-26c74a5e9333"; + const fileSystemSAS = generateDataLakeSASQueryParameters( + { + fileSystemName: fileSystemClient.name, + expiresOn: tmr, + permissions: FileSystemSASPermissions.parse("racwdlmeop"), + unauthorizedUserObjectId: guid + }, + userDelegationKey, + accountName + ); + + // const sasURL = `${fileClient.url}?${fileSystemSAS}`; + // const fileClientWithSAS = new DataLakeDirectoryClient(sasURL); + + // try { + // await fileClientWithSAS.exists(); + // } catch (err) { + // assert.deepStrictEqual(err.details.errorCode, "AuthorizationPermissionMismatch"); + // } + + const acl: PathAccessControlItem[] = [ + { + accessControlType: "user", + entityId: guid, + defaultScope: false, + permissions: { + read: true, + write: true, + execute: true + } + } + ]; + await directoryClient.setAccessControl(acl); + + console.log(await directoryClient.getAccessControl()); + const newfileClient = directoryClient.getFileClient(recorder.getUniqueName("newfile")); + const SASURL = `${newfileClient.url}?${fileSystemSAS}`; + const newFileClientWithSAS = new DataLakeFileClient(SASURL); + await newFileClientWithSAS.createIfNotExists(); + }); +}); diff --git a/sdk/storage/storage-file-datalake/test/utils/index.ts b/sdk/storage/storage-file-datalake/test/utils/index.ts index e60468825990..ef3bd396e2f3 100644 --- a/sdk/storage/storage-file-datalake/test/utils/index.ts +++ b/sdk/storage/storage-file-datalake/test/utils/index.ts @@ -4,6 +4,7 @@ import { randomBytes } from "crypto"; import * as dotenv from "dotenv"; import * as fs from "fs"; import * as path from "path"; +import { DefaultAzureCredential } from "@azure/identity"; import { StorageSharedKeyCredential } from "../../src/credentials/StorageSharedKeyCredential"; import { DataLakeServiceClient } from "../../src/DataLakeServiceClient"; @@ -98,6 +99,22 @@ export function getDataLakeServiceClient( return getGenericDataLakeServiceClient("DFS_", undefined, pipelineOptions); } +export function getDataLakeServiceClientWithDefualtCredential( + accountType: string = "DFS_", + pipelineOptions: StoragePipelineOptions = {}, + accountNameSuffix: string = "" +): DataLakeServiceClient { + const accountNameEnvVar = `${accountType}ACCOUNT_NAME`; + let accountName = process.env[accountNameEnvVar]; + + const credential = new DefaultAzureCredential(); + const pipeline = newPipeline(credential, { + ...pipelineOptions + }); + const dfsPrimaryURL = `https://${accountName}${accountNameSuffix}.dfs.core.windows.net/`; + return new DataLakeServiceClient(dfsPrimaryURL, pipeline); +} + export function getAlternateDataLakeServiceClient(): DataLakeServiceClient { return getGenericDataLakeServiceClient("SECONDARY_", "-secondary"); }