From 1ec9438d210a125ec9b265c121a9b70c217e5922 Mon Sep 17 00:00:00 2001 From: Gauri Prasad <51212198+gapra-msft@users.noreply.github.com> Date: Fri, 12 Jul 2019 11:57:48 -0700 Subject: [PATCH] Storage SAS implementation (#4404) * SAS implementation * Fixed some minor formatting issues * Fixed checkstyle problems and test issue --- .../azure/storage/blob/BlobAsyncClient.java | 259 ++++ .../com/azure/storage/blob/BlobClient.java | 207 ++++ .../com/azure/storage/blob/Constants.java | 15 + .../storage/blob/ContainerAsyncClient.java | 231 ++++ .../azure/storage/blob/ContainerClient.java | 188 +++ .../storage/blob/SASQueryParameters.java | 4 +- .../blob/ServiceSASSignatureValues.java | 182 +-- .../storage/blob/StorageAsyncClient.java | 70 ++ .../com/azure/storage/blob/StorageClient.java | 49 + .../java/com/azure/storage/blob/Utility.java | 23 + .../credentials/SASTokenCredential.java | 96 +- .../policy/SharedKeyCredentialPolicy.java | 9 + .../com/azure/storage/blob/SASTest.groovy | 1079 +++++++++++++++++ 13 files changed, 2315 insertions(+), 97 deletions(-) create mode 100644 storage/client/blob/src/test/java/com/azure/storage/blob/SASTest.groovy diff --git a/storage/client/blob/src/main/java/com/azure/storage/blob/BlobAsyncClient.java b/storage/client/blob/src/main/java/com/azure/storage/blob/BlobAsyncClient.java index 7ec72ecf30b53..5042f9e782af5 100644 --- a/storage/client/blob/src/main/java/com/azure/storage/blob/BlobAsyncClient.java +++ b/storage/client/blob/src/main/java/com/azure/storage/blob/BlobAsyncClient.java @@ -21,6 +21,8 @@ import com.azure.storage.blob.models.ModifiedAccessConditions; import com.azure.storage.blob.models.ReliableDownloadOptions; import com.azure.storage.blob.models.StorageAccountInfo; +import com.azure.storage.blob.models.UserDelegationKey; +import com.azure.storage.common.credentials.SharedKeyCredential; import io.netty.buffer.ByteBuf; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -35,6 +37,7 @@ import java.nio.channels.AsynchronousFileChannel; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; +import java.time.OffsetDateTime; import java.util.ArrayList; import java.util.List; @@ -774,4 +777,260 @@ public Mono> getAccountInfo() { .getAccountInfo() .map(rb -> new SimpleResponse<>(rb, new StorageAccountInfo(rb.deserializedHeaders()))); } + + /** + * Generates a user delegation SAS with the specified parameters + * + * @param userDelegationKey + * The {@code UserDelegationKey} user delegation key for the SAS + * @param accountName + * The {@code String} account name for the SAS + * @param permissions + * The {@code ContainerSASPermissions} permission for the SAS + * @param expiryTime + * The {@code OffsetDateTime} expiry time for the SAS + * + * @return + * A string that represents the SAS token + */ + public String generateUserDelegationSAS(UserDelegationKey userDelegationKey, String accountName, + BlobSASPermission permissions, OffsetDateTime expiryTime) { + return this.generateUserDelegationSAS(userDelegationKey, accountName, permissions, expiryTime, null /* + startTime */, null /* version */, null /*sasProtocol */, null /* ipRange */, null /* cacheControl */, null + /*contentDisposition */, null /* contentEncoding */, null /* contentLanguage */, null /* contentType */); + } + + /** + * Generates a user delegation SAS token with the specified parameters + * + * @param userDelegationKey + * The {@code UserDelegationKey} user delegation key for the SAS + * @param accountName + * The {@code String} account name for the SAS + * @param permissions + * The {@code ContainerSASPermissions} permission for the SAS + * @param expiryTime + * The {@code OffsetDateTime} expiry time for the SAS + * @param startTime + * An optional {@code OffsetDateTime} start time for the SAS + * @param version + * An optional {@code String} version for the SAS + * @param sasProtocol + * An optional {@code SASProtocol} protocol for the SAS + * @param ipRange + * An optional {@code IPRange} ip address range for the SAS + * + * @return + * A string that represents the SAS token + */ + public String generateUserDelegationSAS(UserDelegationKey userDelegationKey, String accountName, + BlobSASPermission permissions, OffsetDateTime expiryTime, OffsetDateTime startTime, String version, + SASProtocol sasProtocol, IPRange ipRange) { + return this.generateUserDelegationSAS(userDelegationKey, accountName, permissions, expiryTime, startTime, + version, sasProtocol, ipRange, null /* cacheControl */, null /* contentDisposition */, null /* + contentEncoding */, null /* contentLanguage */, null /* contentType */); + } + + /** + * Generates a user delegation SAS token with the specified parameters + * + * @param userDelegationKey + * The {@code UserDelegationKey} user delegation key for the SAS + * @param accountName + * The {@code String} account name for the SAS + * @param permissions + * The {@code ContainerSASPermissions} permission for the SAS + * @param expiryTime + * The {@code OffsetDateTime} expiry time for the SAS + * @param startTime + * An optional {@code OffsetDateTime} start time for the SAS + * @param version + * An optional {@code String} version for the SAS + * @param sasProtocol + * An optional {@code SASProtocol} protocol for the SAS + * @param ipRange + * An optional {@code IPRange} ip address range for the SAS + * @param cacheControl + * An optional {@code String} cache-control header for the SAS. + * @param contentDisposition + * An optional {@code String} content-disposition header for the SAS. + * @param contentEncoding + * An optional {@code String} content-encoding header for the SAS. + * @param contentLanguage + * An optional {@code String} content-language header for the SAS. + * @param contentType + * An optional {@code String} content-type header for the SAS. + * + * @return + * A string that represents the SAS token + */ + public String generateUserDelegationSAS(UserDelegationKey userDelegationKey, String accountName, + BlobSASPermission permissions, OffsetDateTime expiryTime, OffsetDateTime startTime, String version, + SASProtocol sasProtocol, IPRange ipRange, String cacheControl, String contentDisposition, + String contentEncoding, String contentLanguage, String contentType) { + + ServiceSASSignatureValues serviceSASSignatureValues = new ServiceSASSignatureValues(version, sasProtocol, + startTime, expiryTime, permissions == null ? null : permissions.toString(), ipRange, null /* identifier*/, + cacheControl, contentDisposition, contentEncoding, contentLanguage, contentType); + + ServiceSASSignatureValues values = configureServiceSASSignatureValues(serviceSASSignatureValues, accountName); + + SASQueryParameters sasQueryParameters = values.generateSASQueryParameters(userDelegationKey); + + return sasQueryParameters.encode(); + } + + /** + * Generates a SAS token with the specified parameters + * + * @param permissions + * The {@code ContainerSASPermissions} permission for the SAS + * @param expiryTime + * The {@code OffsetDateTime} expiry time for the SAS + * + * @return + * A string that represents the SAS token + */ + public String generateSAS(BlobSASPermission permissions, OffsetDateTime expiryTime) { + return this.generateSAS(null, permissions, expiryTime, null /* startTime */, /* identifier */ null /* + version */, null /* sasProtocol */, null /* ipRange */, null /* cacheControl */, null /* contentLanguage*/, + null /* contentEncoding */, null /* contentLanguage */, null /* contentType */); + } + + /** + * Generates a SAS token with the specified parameters + * + * @param identifier + * The {@code String} name of the access policy on the container this SAS references if any + * + * @return + * A string that represents the SAS token + */ + public String generateSAS(String identifier) { + return this.generateSAS(identifier, null /* permissions */, null /* expiryTime */, null /* startTime */, + null /* version */, null /* sasProtocol */, null /* ipRange */, null /* cacheControl */, null /* + contentLanguage*/, null /* contentEncoding */, null /* contentLanguage */, null /* contentType */); + } + + /** + * Generates a SAS token with the specified parameters + * + * @param identifier + * The {@code String} name of the access policy on the container this SAS references if any + * @param permissions + * The {@code ContainerSASPermissions} permission for the SAS + * @param expiryTime + * The {@code OffsetDateTime} expiry time for the SAS + * @param startTime + * An optional {@code OffsetDateTime} start time for the SAS + * @param version + * An optional {@code String} version for the SAS + * @param sasProtocol + * An optional {@code SASProtocol} protocol for the SAS + * @param ipRange + * An optional {@code IPRange} ip address range for the SAS + * + * @return + * A string that represents the SAS token + */ + public String generateSAS(String identifier, BlobSASPermission permissions, OffsetDateTime expiryTime, + OffsetDateTime startTime, String version, SASProtocol sasProtocol, IPRange ipRange) { + return this.generateSAS(identifier, permissions, expiryTime, startTime, version, sasProtocol, ipRange, null + /* cacheControl */, null /* contentLanguage*/, null /* contentEncoding */, null /* contentLanguage */, + null /* contentType */); + } + + /** + * Generates a SAS token with the specified parameters + * + * @param identifier + * The {@code String} name of the access policy on the container this SAS references if any + * @param permissions + * The {@code ContainerSASPermissions} permission for the SAS + * @param expiryTime + * The {@code OffsetDateTime} expiry time for the SAS + * @param startTime + * An optional {@code OffsetDateTime} start time for the SAS + * @param version + * An optional {@code String} version for the SAS + * @param sasProtocol + * An optional {@code SASProtocol} protocol for the SAS + * @param ipRange + * An optional {@code IPRange} ip address range for the SAS + * @param cacheControl + * An optional {@code String} cache-control header for the SAS. + * @param contentDisposition + * An optional {@code String} content-disposition header for the SAS. + * @param contentEncoding + * An optional {@code String} content-encoding header for the SAS. + * @param contentLanguage + * An optional {@code String} content-language header for the SAS. + * @param contentType + * An optional {@code String} content-type header for the SAS. + * + * @return + * A string that represents the SAS token + */ + public String generateSAS(String identifier, BlobSASPermission permissions, OffsetDateTime expiryTime, + OffsetDateTime startTime, String version, SASProtocol sasProtocol, IPRange ipRange, String cacheControl, + String contentDisposition, String contentEncoding, String contentLanguage, String contentType) { + + ServiceSASSignatureValues serviceSASSignatureValues = new ServiceSASSignatureValues(version, sasProtocol, + startTime, expiryTime, permissions == null ? null : permissions.toString(), ipRange, identifier, + cacheControl, contentDisposition, contentEncoding, contentLanguage, contentType); + + SharedKeyCredential sharedKeyCredential = + Utility.getSharedKeyCredential(this.blobAsyncRawClient.azureBlobStorage.httpPipeline()); + + Utility.assertNotNull("sharedKeyCredential", sharedKeyCredential); + + ServiceSASSignatureValues values = configureServiceSASSignatureValues(serviceSASSignatureValues, + sharedKeyCredential.accountName()); + + SASQueryParameters sasQueryParameters = values.generateSASQueryParameters(sharedKeyCredential); + + return sasQueryParameters.encode(); + } + + /** + * Sets serviceSASSignatureValues parameters dependent on the current blob type + */ + ServiceSASSignatureValues configureServiceSASSignatureValues(ServiceSASSignatureValues serviceSASSignatureValues, + String accountName) { + + // Set canonical name + serviceSASSignatureValues.canonicalName(this.blobAsyncRawClient.azureBlobStorage.url(), accountName); + + // Set snapshotId + serviceSASSignatureValues.snapshotId(getSnapshotId()); + + // Set resource + if (isSnapshot()) { + serviceSASSignatureValues.resource(Constants.UrlConstants.SAS_BLOB_SNAPSHOT_CONSTANT); + } else { + serviceSASSignatureValues.resource(Constants.UrlConstants.SAS_BLOB_CONSTANT); + } + + return serviceSASSignatureValues; + } + + /** + * Gets the snapshotId for a blob resource + * + * @return + * A string that represents the snapshotId of the snapshot blob + */ + public String getSnapshotId() { + return this.blobAsyncRawClient.snapshot; + } + + /** + * Determines if a blob is a snapshot + * + * @return + * A boolean that indicates if a blob is a snapshot + */ + public boolean isSnapshot() { + return this.blobAsyncRawClient.snapshot != null; + } } diff --git a/storage/client/blob/src/main/java/com/azure/storage/blob/BlobClient.java b/storage/client/blob/src/main/java/com/azure/storage/blob/BlobClient.java index f47c3cf6d7830..21a2212713f5c 100644 --- a/storage/client/blob/src/main/java/com/azure/storage/blob/BlobClient.java +++ b/storage/client/blob/src/main/java/com/azure/storage/blob/BlobClient.java @@ -16,6 +16,7 @@ import com.azure.storage.blob.models.ModifiedAccessConditions; import com.azure.storage.blob.models.ReliableDownloadOptions; import com.azure.storage.blob.models.StorageAccountInfo; +import com.azure.storage.blob.models.UserDelegationKey; import reactor.core.publisher.Mono; import java.io.IOException; @@ -23,6 +24,7 @@ import java.io.UncheckedIOException; import java.net.URL; import java.time.Duration; +import java.time.OffsetDateTime; /** * Client to a blob of any type: block, append, or page. It may only be instantiated through a {@link BlobClientBuilder} or via @@ -800,4 +802,209 @@ public Response getAccountInfo(Duration timeout) { return Utility.blockWithOptionalTimeout(response, timeout); } + + /** + * Generates a user delegation SAS token with the specified parameters + * + * @param userDelegationKey + * The {@code UserDelegationKey} user delegation key for the SAS + * @param accountName + * The {@code String} account name for the SAS + * @param permissions + * The {@code ContainerSASPermissions} permission for the SAS + * @param expiryTime + * The {@code OffsetDateTime} expiry time for the SAS + * + * @return + * A string that represents the SAS token + */ + public String generateUserDelegationSAS(UserDelegationKey userDelegationKey, String accountName, + BlobSASPermission permissions, OffsetDateTime expiryTime) { + return this.blobAsyncClient.generateUserDelegationSAS(userDelegationKey, accountName, permissions, expiryTime); + } + + /** + * Generates a user delegation SAS token with the specified parameters + * + * @param userDelegationKey + * The {@code UserDelegationKey} user delegation key for the SAS + * @param accountName + * The {@code String} account name for the SAS + * @param permissions + * The {@code ContainerSASPermissions} permission for the SAS + * @param expiryTime + * The {@code OffsetDateTime} expiry time for the SAS + * @param startTime + * An optional {@code OffsetDateTime} start time for the SAS + * @param version + * An optional {@code String} version for the SAS + * @param sasProtocol + * An optional {@code SASProtocol} protocol for the SAS + * @param ipRange + * An optional {@code IPRange} ip address range for the SAS + * + * @return + * A string that represents the SAS token + */ + public String generateUserDelegationSAS(UserDelegationKey userDelegationKey, String accountName, + BlobSASPermission permissions, OffsetDateTime expiryTime, OffsetDateTime startTime, String version, + SASProtocol sasProtocol, IPRange ipRange) { + return this.blobAsyncClient.generateUserDelegationSAS(userDelegationKey, accountName, permissions, expiryTime, + startTime, version, sasProtocol, ipRange); + } + + /** + * Generates a user delegation SAS token with the specified parameters + * + * @param userDelegationKey + * The {@code UserDelegationKey} user delegation key for the SAS + * @param accountName + * The {@code String} account name for the SAS + * @param permissions + * The {@code ContainerSASPermissions} permission for the SAS + * @param expiryTime + * The {@code OffsetDateTime} expiry time for the SAS + * @param startTime + * An optional {@code OffsetDateTime} start time for the SAS + * @param version + * An optional {@code String} version for the SAS + * @param sasProtocol + * An optional {@code SASProtocol} protocol for the SAS + * @param ipRange + * An optional {@code IPRange} ip address range for the SAS + * @param cacheControl + * An optional {@code String} cache-control header for the SAS. + * @param contentDisposition + * An optional {@code String} content-disposition header for the SAS. + * @param contentEncoding + * An optional {@code String} content-encoding header for the SAS. + * @param contentLanguage + * An optional {@code String} content-language header for the SAS. + * @param contentType + * An optional {@code String} content-type header for the SAS. + * + * @return + * A string that represents the SAS token + */ + public String generateUserDelegationSAS(UserDelegationKey userDelegationKey, String accountName, + BlobSASPermission permissions, OffsetDateTime expiryTime, OffsetDateTime startTime, String version, + SASProtocol sasProtocol, IPRange ipRange, String cacheControl, String contentDisposition, + String contentEncoding, String contentLanguage, String contentType) { + return this.blobAsyncClient.generateUserDelegationSAS(userDelegationKey, accountName, permissions, expiryTime, + startTime, version, sasProtocol, ipRange, cacheControl, contentDisposition, contentEncoding, + contentLanguage, contentType); + } + + /** + * Generates a SAS token with the specified parameters + * + * @param expiryTime + * The {@code OffsetDateTime} expiry time for the SAS + * @param permissions + * The {@code ContainerSASPermissions} permission for the SAS + * + * @return + * A string that represents the SAS token + */ + public String generateSAS(OffsetDateTime expiryTime, BlobSASPermission permissions) { + return this.blobAsyncClient.generateSAS(permissions, expiryTime); + } + + /** + * Generates a SAS token with the specified parameters + * + * @param identifier + * The {@code String} name of the access policy on the container this SAS references if any + * + * @return + * A string that represents the SAS token + */ + public String generateSAS(String identifier) { + return this.blobAsyncClient.generateSAS(identifier); + } + + /** + * Generates a SAS token with the specified parameters + * + * @param identifier + * The {@code String} name of the access policy on the container this SAS references if any + * @param permissions + * The {@code ContainerSASPermissions} permission for the SAS + * @param expiryTime + * The {@code OffsetDateTime} expiry time for the SAS + * @param startTime + * An optional {@code OffsetDateTime} start time for the SAS + * @param version + * An optional {@code String} version for the SAS + * @param sasProtocol + * An optional {@code SASProtocol} protocol for the SAS + * @param ipRange + * An optional {@code IPRange} ip address range for the SAS + * + * @return + * A string that represents the SAS token + */ + public String generateSAS(String identifier, BlobSASPermission permissions, OffsetDateTime expiryTime, + OffsetDateTime startTime, String version, SASProtocol sasProtocol, IPRange ipRange) { + return this.blobAsyncClient.generateSAS(identifier, permissions, expiryTime, startTime, version, sasProtocol, + ipRange); + } + + /** + * Generates a SAS token with the specified parameters + * + * @param identifier + * The {@code String} name of the access policy on the container this SAS references if any + * @param permissions + * The {@code ContainerSASPermissions} permission for the SAS + * @param expiryTime + * The {@code OffsetDateTime} expiry time for the SAS + * @param startTime + * An optional {@code OffsetDateTime} start time for the SAS + * @param version + * An optional {@code String} version for the SAS + * @param sasProtocol + * An optional {@code SASProtocol} protocol for the SAS + * @param ipRange + * An optional {@code IPRange} ip address range for the SAS + * @param cacheControl + * An optional {@code String} cache-control header for the SAS. + * @param contentDisposition + * An optional {@code String} content-disposition header for the SAS. + * @param contentEncoding + * An optional {@code String} content-encoding header for the SAS. + * @param contentLanguage + * An optional {@code String} content-language header for the SAS. + * @param contentType + * An optional {@code String} content-type header for the SAS. + * + * @return + * A string that represents the SAS token + */ + public String generateSAS(String identifier, BlobSASPermission permissions, OffsetDateTime expiryTime, + OffsetDateTime startTime, String version, SASProtocol sasProtocol, IPRange ipRange, String cacheControl, + String contentDisposition, String contentEncoding, String contentLanguage, String contentType) { + return this.blobAsyncClient.generateSAS(identifier, permissions, expiryTime, startTime, version, sasProtocol, + ipRange, cacheControl, contentDisposition, contentEncoding, contentLanguage, contentType); + } + + /** + * Gets the snapshotId for a blob resource + * + * @return + * A string that represents the snapshotId of the snapshot blob + */ + public String getSnapshotId() { + return this.blobAsyncClient.getSnapshotId(); + } + + /** + * Determines if a blob is a snapshot + * + * @return + * A boolean that indicates if a blob is a snapshot + */ + public boolean isSnapshot() { + return this.blobAsyncClient.isSnapshot(); + } } diff --git a/storage/client/blob/src/main/java/com/azure/storage/blob/Constants.java b/storage/client/blob/src/main/java/com/azure/storage/blob/Constants.java index 653625b572c15..fa812df4b531b 100644 --- a/storage/client/blob/src/main/java/com/azure/storage/blob/Constants.java +++ b/storage/client/blob/src/main/java/com/azure/storage/blob/Constants.java @@ -288,6 +288,21 @@ static final class UrlConstants { */ public static final String SAS_SIGNED_KEY_VERSION = "skv"; + /** + * The SAS blob constant. + */ + public static final String SAS_BLOB_CONSTANT = "b"; + + /** + * The SAS blob snapshot constant. + */ + public static final String SAS_BLOB_SNAPSHOT_CONSTANT = "bs"; + + /** + * The SAS blob snapshot constant. + */ + public static final String SAS_CONTAINER_CONSTANT = "c"; + private UrlConstants() { // Private to prevent construction. } diff --git a/storage/client/blob/src/main/java/com/azure/storage/blob/ContainerAsyncClient.java b/storage/client/blob/src/main/java/com/azure/storage/blob/ContainerAsyncClient.java index 406a7440b0c5b..f979f084ac151 100644 --- a/storage/client/blob/src/main/java/com/azure/storage/blob/ContainerAsyncClient.java +++ b/storage/client/blob/src/main/java/com/azure/storage/blob/ContainerAsyncClient.java @@ -24,12 +24,15 @@ import com.azure.storage.blob.models.PublicAccessType; import com.azure.storage.blob.models.SignedIdentifier; import com.azure.storage.blob.models.StorageAccountInfo; +import com.azure.storage.blob.models.UserDelegationKey; +import com.azure.storage.common.credentials.SharedKeyCredential; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.net.MalformedURLException; import java.net.URL; import java.time.Duration; +import java.time.OffsetDateTime; import java.util.List; /** @@ -878,4 +881,232 @@ public Mono> getAccountInfo() { .getAccountInfo() .map(rb -> new SimpleResponse<>(rb, new StorageAccountInfo(rb.deserializedHeaders()))); } + + /** + * Generates a user delegation SAS with the specified parameters + * + * @param userDelegationKey + * The {@code UserDelegationKey} user delegation key for the SAS + * @param accountName + * The {@code String} account name for the SAS + * @param permissions + * The {@code ContainerSASPermissions} permission for the SAS + * @param expiryTime + * The {@code OffsetDateTime} expiry time for the SAS + * + * @return + * A string that represents the SAS token + */ + public String generateUserDelegationSAS(UserDelegationKey userDelegationKey, String accountName, + ContainerSASPermission permissions, OffsetDateTime expiryTime) { + return this.generateUserDelegationSAS(userDelegationKey, accountName, permissions, expiryTime, null /* + startTime */, null /* version */, null /* sasProtocol */, null /* ipRange */, null /* cacheControl */, null + /* contentDisposition */, null /* contentEncoding */, null /* contentLanguage */, null /* contentType */); + } + + /** + * Generates a user delegation SAS token with the specified parameters + * + * @param userDelegationKey + * The {@code UserDelegationKey} user delegation key for the SAS + * @param accountName + * The {@code String} account name for the SAS + * @param permissions + * The {@code ContainerSASPermissions} permission for the SAS + * @param expiryTime + * The {@code OffsetDateTime} expiry time for the SAS + * @param startTime + * An optional {@code OffsetDateTime} start time for the SAS + * @param version + * An optional {@code String} version for the SAS + * @param sasProtocol + * An optional {@code SASProtocol} protocol for the SAS + * @param ipRange + * An optional {@code IPRange} ip address range for the SAS + * + * @return + * A string that represents the SAS token + */ + public String generateUserDelegationSAS(UserDelegationKey userDelegationKey, String accountName, + ContainerSASPermission permissions, OffsetDateTime expiryTime, OffsetDateTime startTime, String version, + SASProtocol sasProtocol, IPRange ipRange) { + return this.generateUserDelegationSAS(userDelegationKey, accountName, permissions, expiryTime, startTime, + version, sasProtocol, ipRange, null /* cacheControl */, null /* contentDisposition */, null /* + contentEncoding */, null /* contentLanguage */, null /* contentType */); + } + + /** + * Generates a user delegation SAS token with the specified parameters + * + * @param userDelegationKey + * The {@code UserDelegationKey} user delegation key for the SAS + * @param accountName + * The {@code String} account name for the SAS + * @param permissions + * The {@code ContainerSASPermissions} permission for the SAS + * @param expiryTime + * The {@code OffsetDateTime} expiry time for the SAS + * @param startTime + * An optional {@code OffsetDateTime} start time for the SAS + * @param version + * An optional {@code String} version for the SAS + * @param sasProtocol + * An optional {@code SASProtocol} protocol for the SAS + * @param ipRange + * An optional {@code IPRange} ip address range for the SAS + * @param cacheControl + * An optional {@code String} cache-control header for the SAS. + * @param contentDisposition + * An optional {@code String} content-disposition header for the SAS. + * @param contentEncoding + * An optional {@code String} content-encoding header for the SAS. + * @param contentLanguage + * An optional {@code String} content-language header for the SAS. + * @param contentType + * An optional {@code String} content-type header for the SAS. + * + * @return + * A string that represents the SAS token + */ + public String generateUserDelegationSAS(UserDelegationKey userDelegationKey, String accountName, + ContainerSASPermission permissions, OffsetDateTime expiryTime, OffsetDateTime startTime, String version, + SASProtocol sasProtocol, IPRange ipRange, String cacheControl, String contentDisposition, + String contentEncoding, String contentLanguage, String contentType) { + ServiceSASSignatureValues serviceSASSignatureValues = new ServiceSASSignatureValues(version, sasProtocol, + startTime, expiryTime, permissions == null ? null : permissions.toString(), ipRange, null /* identifier*/, + cacheControl, contentDisposition, contentEncoding, contentLanguage, contentType); + + ServiceSASSignatureValues values = configureServiceSASSignatureValues(serviceSASSignatureValues, accountName); + + SASQueryParameters sasQueryParameters = values.generateSASQueryParameters(userDelegationKey); + + return sasQueryParameters.encode(); + } + + /** + * Generates a SAS token with the specified parameters + * + * @param permissions + * The {@code ContainerSASPermissions} permission for the SAS + * @param expiryTime + * The {@code OffsetDateTime} expiry time for the SAS + * + * @return + * A string that represents the SAS token + */ + public String generateSAS(ContainerSASPermission permissions, OffsetDateTime expiryTime) { + return this.generateSAS(null, permissions, /* identifier */ expiryTime, null /* startTime */, null /* version + */, null /* sasProtocol */, null /* ipRange */, null /* cacheControl */, null /* contentDisposition */, + null /* contentEncoding */, null /* contentLanguage */, null /*contentType*/); + } + + /** + * Generates a SAS token with the specified parameters + * + * @param identifier + * The {@code String} name of the access policy on the container this SAS references if any + * + * @return + * A string that represents the SAS token + */ + public String generateSAS(String identifier) { + return this.generateSAS(identifier, null /* permissions*/, null /* expiryTime */, null /* startTime */, null + /* version */, null /* sasProtocol */, null /* ipRange */, null /* cacheControl */, null /* + contentDisposition */, null /* contentEncoding */, null /* contentLanguage */, null /*contentType*/); + } + + /** + * Generates a SAS token with the specified parameters + * + * @param identifier + * The {@code String} name of the access policy on the container this SAS references if any + * @param permissions + * The {@code ContainerSASPermissions} permission for the SAS + * @param expiryTime + * The {@code OffsetDateTime} expiry time for the SAS + * @param startTime + * An optional {@code OffsetDateTime} start time for the SAS + * @param version + * An optional {@code String} version for the SAS + * @param sasProtocol + * An optional {@code SASProtocol} protocol for the SAS + * @param ipRange + * An optional {@code IPRange} ip address range for the SAS + * + * @return + * A string that represents the SAS token + */ + public String generateSAS(String identifier, ContainerSASPermission permissions, OffsetDateTime expiryTime, + OffsetDateTime startTime, + String version, SASProtocol sasProtocol, IPRange ipRange) { + return this.generateSAS(identifier, permissions, expiryTime, startTime, version, sasProtocol, ipRange, null + /* cacheControl */, null /* contentDisposition */, null /* contentEncoding */, null /* contentLanguage */, + null /*contentType*/); + } + + /** + * Generates a SAS token with the specified parameters + * + * @param identifier + * The {@code String} name of the access policy on the container this SAS references if any + * @param permissions + * The {@code ContainerSASPermissions} permission for the SAS + * @param expiryTime + * The {@code OffsetDateTime} expiry time for the SAS + * @param startTime + * An optional {@code OffsetDateTime} start time for the SAS + * @param version + * An optional {@code String} version for the SAS + * @param sasProtocol + * An optional {@code SASProtocol} protocol for the SAS + * @param ipRange + * An optional {@code IPRange} ip address range for the SAS + * @param cacheControl + * An optional {@code String} cache-control header for the SAS. + * @param contentDisposition + * An optional {@code String} content-disposition header for the SAS. + * @param contentEncoding + * An optional {@code String} content-encoding header for the SAS. + * @param contentLanguage + * An optional {@code String} content-language header for the SAS. + * @param contentType + * An optional {@code String} content-type header for the SAS. + * + * @return + * A string that represents the SAS token + */ + public String generateSAS(String identifier, ContainerSASPermission permissions, OffsetDateTime expiryTime, + OffsetDateTime startTime, String version, SASProtocol sasProtocol, IPRange ipRange, String cacheControl, + String contentDisposition, String contentEncoding, String contentLanguage, String contentType) { + ServiceSASSignatureValues serviceSASSignatureValues = new ServiceSASSignatureValues(version, sasProtocol, + startTime, expiryTime, permissions == null ? null : permissions.toString(), ipRange, identifier, + cacheControl, contentDisposition, contentEncoding, contentLanguage, contentType); + + SharedKeyCredential sharedKeyCredential = + Utility.getSharedKeyCredential(this.containerAsyncRawClient.azureBlobStorage.httpPipeline()); + + Utility.assertNotNull("sharedKeyCredential", sharedKeyCredential); + + ServiceSASSignatureValues values = configureServiceSASSignatureValues(serviceSASSignatureValues, + sharedKeyCredential.accountName()); + + SASQueryParameters sasQueryParameters = values.generateSASQueryParameters(sharedKeyCredential); + + return sasQueryParameters.encode(); + } + + /** + * Sets serviceSASSignatureValues parameters dependent on the current blob type + */ + private ServiceSASSignatureValues configureServiceSASSignatureValues(ServiceSASSignatureValues serviceSASSignatureValues, String accountName) { + // Set canonical name + serviceSASSignatureValues.canonicalName(this.containerAsyncRawClient.azureBlobStorage.url(), accountName); + + // Set snapshotId to null + serviceSASSignatureValues.snapshotId(null); + + // Set resource + serviceSASSignatureValues.resource(Constants.UrlConstants.SAS_CONTAINER_CONSTANT); + return serviceSASSignatureValues; + } } diff --git a/storage/client/blob/src/main/java/com/azure/storage/blob/ContainerClient.java b/storage/client/blob/src/main/java/com/azure/storage/blob/ContainerClient.java index b8c1f9d8bd09e..c9733fd3a7f61 100644 --- a/storage/client/blob/src/main/java/com/azure/storage/blob/ContainerClient.java +++ b/storage/client/blob/src/main/java/com/azure/storage/blob/ContainerClient.java @@ -15,11 +15,13 @@ import com.azure.storage.blob.models.PublicAccessType; import com.azure.storage.blob.models.SignedIdentifier; import com.azure.storage.blob.models.StorageAccountInfo; +import com.azure.storage.blob.models.UserDelegationKey; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.net.URL; import java.time.Duration; +import java.time.OffsetDateTime; import java.util.List; /** @@ -738,4 +740,190 @@ public Response getAccountInfo(Duration timeout) { return Utility.blockWithOptionalTimeout(response, timeout); } + + /** + * Generates a user delegation SAS token with the specified parameters + * + * @param userDelegationKey + * The {@code UserDelegationKey} user delegation key for the SAS + * @param accountName + * The {@code String} account name for the SAS + * @param permissions + * The {@code ContainerSASPermissions} permission for the SAS + * @param expiryTime + * The {@code OffsetDateTime} expiry time for the SAS + * + * @return + * A string that represents the SAS token + */ + public String generateUserDelegationSAS(UserDelegationKey userDelegationKey, String accountName, + ContainerSASPermission permissions, OffsetDateTime expiryTime) { + return this.containerAsyncClient.generateUserDelegationSAS(userDelegationKey, accountName, permissions, + expiryTime); + } + + /** + * Generates a user delegation SAS token with the specified parameters + * + * @param userDelegationKey + * The {@code UserDelegationKey} user delegation key for the SAS + * @param accountName + * The {@code String} account name for the SAS + * @param permissions + * The {@code ContainerSASPermissions} permission for the SAS + * @param expiryTime + * The {@code OffsetDateTime} expiry time for the SAS + * @param startTime + * An optional {@code OffsetDateTime} start time for the SAS + * @param version + * An optional {@code String} version for the SAS + * @param sasProtocol + * An optional {@code SASProtocol} protocol for the SAS + * @param ipRange + * An optional {@code IPRange} ip address range for the SAS + * + * @return + * A string that represents the SAS token + */ + public String generateUserDelegationSAS(UserDelegationKey userDelegationKey, String accountName, + ContainerSASPermission permissions, OffsetDateTime expiryTime, OffsetDateTime startTime, String version, + SASProtocol sasProtocol, IPRange ipRange) { + return this.containerAsyncClient.generateUserDelegationSAS(userDelegationKey, accountName, permissions, + expiryTime, startTime, version, sasProtocol, ipRange); + } + + /** + * Generates a user delegation SAS token with the specified parameters + * + * @param userDelegationKey + * The {@code UserDelegationKey} user delegation key for the SAS + * @param accountName + * The {@code String} account name for the SAS + * @param permissions + * The {@code ContainerSASPermissions} permission for the SAS + * @param expiryTime + * The {@code OffsetDateTime} expiry time for the SAS + * @param startTime + * An optional {@code OffsetDateTime} start time for the SAS + * @param version + * An optional {@code String} version for the SAS + * @param sasProtocol + * An optional {@code SASProtocol} protocol for the SAS + * @param ipRange + * An optional {@code IPRange} ip address range for the SAS + * @param cacheControl + * An optional {@code String} cache-control header for the SAS. + * @param contentDisposition + * An optional {@code String} content-disposition header for the SAS. + * @param contentEncoding + * An optional {@code String} content-encoding header for the SAS. + * @param contentLanguage + * An optional {@code String} content-language header for the SAS. + * @param contentType + * An optional {@code String} content-type header for the SAS. + * + * @return + * A string that represents the SAS token + */ + public String generateUserDelegationSAS(UserDelegationKey userDelegationKey, String accountName, + ContainerSASPermission permissions, OffsetDateTime expiryTime, OffsetDateTime startTime, String version, + SASProtocol sasProtocol, IPRange ipRange, String cacheControl, String contentDisposition, + String contentEncoding, String contentLanguage, String contentType) { + return this.containerAsyncClient.generateUserDelegationSAS(userDelegationKey, accountName, permissions, + expiryTime, startTime, version, sasProtocol, ipRange, cacheControl, contentDisposition, contentEncoding, + contentLanguage, contentType); + } + + /** + * Generates a SAS token with the specified parameters + * + * @param permissions + * The {@code ContainerSASPermissions} permission for the SAS + * @param expiryTime + * The {@code OffsetDateTime} expiry time for the SAS + * + * @return + * A string that represents the SAS token + */ + public String generateSAS(ContainerSASPermission permissions, OffsetDateTime expiryTime) { + return this.containerAsyncClient.generateSAS(permissions, expiryTime); + } + + /** + * Generates a SAS token with the specified parameters + * + * @param identifier + * The {@code String} name of the access policy on the container this SAS references if any + * + * @return + * A string that represents the SAS token + */ + public String generateSAS(String identifier) { + return this.containerAsyncClient.generateSAS(identifier); + } + + /** + * Generates a SAS token with the specified parameters + * + * @param identifier + * The {@code String} name of the access policy on the container this SAS references if any + * @param permissions + * The {@code ContainerSASPermissions} permission for the SAS + * @param expiryTime + * The {@code OffsetDateTime} expiry time for the SAS + * @param startTime + * An optional {@code OffsetDateTime} start time for the SAS + * @param version + * An optional {@code String} version for the SAS + * @param sasProtocol + * An optional {@code SASProtocol} protocol for the SAS + * @param ipRange + * An optional {@code IPRange} ip address range for the SAS + * + * @return + * A string that represents the SAS token + */ + public String generateSAS(String identifier, ContainerSASPermission permissions, OffsetDateTime expiryTime, + OffsetDateTime startTime, String version, SASProtocol sasProtocol, IPRange ipRange) { + return this.containerAsyncClient.generateSAS(identifier, permissions, expiryTime, startTime, version, + sasProtocol, ipRange); + } + + /** + * Generates a SAS token with the specified parameters + * + * @param identifier + * The {@code String} name of the access policy on the container this SAS references if any + * @param permissions + * The {@code ContainerSASPermissions} permission for the SAS + * @param expiryTime + * The {@code OffsetDateTime} expiry time for the SAS + * @param startTime + * An optional {@code OffsetDateTime} start time for the SAS + * @param version + * An optional {@code String} version for the SAS + * @param sasProtocol + * An optional {@code SASProtocol} protocol for the SAS + * @param ipRange + * An optional {@code IPRange} ip address range for the SAS + * @param cacheControl + * An optional {@code String} cache-control header for the SAS. + * @param contentDisposition + * An optional {@code String} content-disposition header for the SAS. + * @param contentEncoding + * An optional {@code String} content-encoding header for the SAS. + * @param contentLanguage + * An optional {@code String} content-language header for the SAS. + * @param contentType + * An optional {@code String} content-type header for the SAS. + * + * @return + * A string that represents the SAS token + */ + public String generateSAS(String identifier, ContainerSASPermission permissions, OffsetDateTime expiryTime, + OffsetDateTime startTime, String version, SASProtocol sasProtocol, IPRange ipRange, String cacheControl, + String contentDisposition, String contentEncoding, String contentLanguage, String contentType) { + return this.containerAsyncClient.generateSAS(identifier, permissions, expiryTime, startTime, version, + sasProtocol, ipRange, cacheControl, contentDisposition, contentEncoding, contentLanguage, contentType); + } } diff --git a/storage/client/blob/src/main/java/com/azure/storage/blob/SASQueryParameters.java b/storage/client/blob/src/main/java/com/azure/storage/blob/SASQueryParameters.java index b6afd82addcab..cd918e636bbd4 100644 --- a/storage/client/blob/src/main/java/com/azure/storage/blob/SASQueryParameters.java +++ b/storage/client/blob/src/main/java/com/azure/storage/blob/SASQueryParameters.java @@ -333,9 +333,7 @@ UserDelegationKey userDelegationKey() { private void tryAppendQueryParameter(StringBuilder sb, String param, Object value) { if (value != null) { - if (sb.length() == 0) { - sb.append('?'); - } else { + if (sb.length() != 0) { sb.append('&'); } sb.append(safeURLEncode(param)).append('=').append(safeURLEncode(value.toString())); diff --git a/storage/client/blob/src/main/java/com/azure/storage/blob/ServiceSASSignatureValues.java b/storage/client/blob/src/main/java/com/azure/storage/blob/ServiceSASSignatureValues.java index 7450ae699f5a4..114c28eb71209 100644 --- a/storage/client/blob/src/main/java/com/azure/storage/blob/ServiceSASSignatureValues.java +++ b/storage/client/blob/src/main/java/com/azure/storage/blob/ServiceSASSignatureValues.java @@ -6,6 +6,8 @@ import com.azure.storage.blob.models.UserDelegationKey; import com.azure.storage.common.credentials.SharedKeyCredential; +import java.net.MalformedURLException; +import java.net.URL; import java.security.InvalidKeyException; import java.time.OffsetDateTime; @@ -40,9 +42,9 @@ final class ServiceSASSignatureValues { private IPRange ipRange; - private String containerName; + private String canonicalName; - private String blobName; + private String resource; private String snapshotId; @@ -64,6 +66,43 @@ final class ServiceSASSignatureValues { ServiceSASSignatureValues() { } + /** + * Creates an object with the specified expiry time and permissions + * @param expiryTime + * @param permissions + */ + ServiceSASSignatureValues(OffsetDateTime expiryTime, String permissions) { + this.expiryTime = expiryTime; + this.permissions = permissions; + } + + /** + * Creates an object with the specified identifier + * @param identifier + */ + ServiceSASSignatureValues(String identifier) { + this.identifier = identifier; + } + + ServiceSASSignatureValues(String version, SASProtocol sasProtocol, OffsetDateTime startTime, + OffsetDateTime expiryTime, String permission, IPRange ipRange, String identifier, String cacheControl, + String contentDisposition, String contentEncoding, String contentLanguage, String contentType) { + if (version != null) { + this.version = version; + } + this.protocol = sasProtocol; + this.startTime = startTime; + this.expiryTime = expiryTime; + this.permissions = permission; + this.ipRange = ipRange; + this.identifier = identifier; + this.cacheControl = cacheControl; + this.contentDisposition = contentDisposition; + this.contentEncoding = contentEncoding; + this.contentLanguage = contentLanguage; + this.contentType = contentType; + } + /** * The version of the service this SAS will target. If not specified, it will default to the version targeted by the * library. @@ -159,32 +198,51 @@ public ServiceSASSignatureValues ipRange(IPRange ipRange) { } /** - * The name of the container the SAS user may access. + * The resource the SAS user may access. */ - public String containerName() { - return containerName; + public String resource() { + return resource; } /** - * The name of the container the SAS user may access. + * The resource the SAS user may access. */ - public ServiceSASSignatureValues containerName(String containerName) { - this.containerName = containerName; + public ServiceSASSignatureValues resource(String resource) { + this.resource = resource; return this; } /** - * The name of the blob the SAS user may access. + * The canonical name of the object the SAS user may access. */ - public String blobName() { - return blobName; + public String canonicalName() { + return canonicalName; } /** - * The name of the blob the SAS user may access. + * The canonical name of the object the SAS user may access. */ - public ServiceSASSignatureValues blobName(String blobName) { - this.blobName = blobName; + public ServiceSASSignatureValues canonicalName(String canonicalName) { + this.canonicalName = canonicalName; + return this; + } + + /** + * The canonical name of the object the SAS user may access. + * @throws RuntimeException If urlString is a malformed URL. + */ + public ServiceSASSignatureValues canonicalName(String urlString, String accountName) { + URL url = null; + try { + url = new URL(urlString); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + + StringBuilder canonicalName = new StringBuilder("/blob"); + canonicalName.append('/').append(accountName).append(url.getPath()); + this.canonicalName = canonicalName.toString(); + return this; } @@ -192,7 +250,7 @@ public ServiceSASSignatureValues blobName(String blobName) { * The specific snapshot the SAS user may access. */ public String snapshotId() { - return snapshotId; + return this.snapshotId; } /** @@ -309,13 +367,10 @@ public ServiceSASSignatureValues contentType(String contentType) { */ public SASQueryParameters generateSASQueryParameters(SharedKeyCredential sharedKeyCredentials) { Utility.assertNotNull("sharedKeyCredentials", sharedKeyCredentials); - assertGenerateOK(); - - String resource = getResource(); - String verifiedPermissions = getVerifiedPermissions(); + assertGenerateOK(false); // Signature is generated on the un-url-encoded values. - final String stringToSign = stringToSign(verifiedPermissions, resource, sharedKeyCredentials); + final String stringToSign = stringToSign(); String signature = null; try { @@ -336,22 +391,15 @@ public SASQueryParameters generateSASQueryParameters(SharedKeyCredential sharedK * @param delegationKey * A {@link UserDelegationKey} object used to sign the SAS values. * - * @param accountName - * Name of the account holding the resource this SAS is authorizing. - * * @return {@link SASQueryParameters} * @throws Error If the accountKey is not a valid Base64-encoded string. */ - public SASQueryParameters generateSASQueryParameters(UserDelegationKey delegationKey, String accountName) { + public SASQueryParameters generateSASQueryParameters(UserDelegationKey delegationKey) { Utility.assertNotNull("delegationKey", delegationKey); - Utility.assertNotNull("accountName", accountName); - assertGenerateOK(); - - String resource = getResource(); - String verifiedPermissions = getVerifiedPermissions(); + assertGenerateOK(true); // Signature is generated on the un-url-encoded values. - final String stringToSign = stringToSign(verifiedPermissions, resource, delegationKey, accountName); + final String stringToSign = stringToSign(delegationKey); String signature = null; try { @@ -369,70 +417,39 @@ public SASQueryParameters generateSASQueryParameters(UserDelegationKey delegatio /** * Common assertions for generateSASQueryParameters overloads. */ - private void assertGenerateOK() { + private void assertGenerateOK(boolean usingUserDelegation) { Utility.assertNotNull("version", this.version); - Utility.assertNotNull("containerName", this.containerName); - if (blobName == null && snapshotId != null) { - throw new IllegalArgumentException("Cannot set a snapshotId without a blobName."); - } - } + Utility.assertNotNull("canonicalName", this.canonicalName); - /** - * Gets the resource string for SAS tokens based on object state. - */ - private String getResource() { - String resource = "c"; - if (!Utility.isNullOrEmpty(this.blobName)) { - resource = snapshotId != null && !snapshotId.isEmpty() ? "bs" : "b"; - } - - return resource; - } - - /** - * Gets the verified permissions string for SAS tokens based on object state. - */ - private String getVerifiedPermissions() { - String verifiedPermissions = null; - // Calling parse and toString guarantees the proper ordering and throws on invalid characters. - if (Utility.isNullOrEmpty(this.blobName)) { - if (this.permissions != null) { - verifiedPermissions = ContainerSASPermission.parse(this.permissions).toString(); + // Ensure either (expiryTime and permissions) or (identifier) is set + if (this.expiryTime == null || this.permissions == null) { + // Identifier is not required if user delegation is being used + if (!usingUserDelegation) { + Utility.assertNotNull("identifier", this.identifier); } } else { - if (this.permissions != null) { - verifiedPermissions = BlobSASPermission.parse(this.permissions).toString(); - } + Utility.assertNotNull("expiryTime", this.expiryTime); + Utility.assertNotNull("permissions", this.permissions); } - return verifiedPermissions; - } - - private String getCanonicalName(String accountName) { - // Container: "/blob/account/containername" - // Blob: "/blob/account/containername/blobname" - StringBuilder canonicalName = new StringBuilder("/blob"); - canonicalName.append('/').append(accountName).append('/').append(this.containerName); - - if (!Utility.isNullOrEmpty(this.blobName)) { - canonicalName.append("/").append(this.blobName); + if (this.resource != null && this.resource.equals(Constants.UrlConstants.SAS_CONTAINER_CONSTANT)) { + if (this.snapshotId != null) { + throw new IllegalArgumentException("Cannot set a snapshotId without resource being a blob."); + } } - - return canonicalName.toString(); } - private String stringToSign(final String verifiedPermissions, final String resource, - final SharedKeyCredential sharedKeyCredentials) { + private String stringToSign() { return String.join("\n", - verifiedPermissions == null ? "" : verifiedPermissions, + this.permissions == null ? "" : this.permissions, this.startTime == null ? "" : Utility.ISO_8601_UTC_DATE_FORMATTER.format(this.startTime), this.expiryTime == null ? "" : Utility.ISO_8601_UTC_DATE_FORMATTER.format(this.expiryTime), - getCanonicalName(sharedKeyCredentials.accountName()), + this.canonicalName == null ? "" : this.canonicalName, this.identifier == null ? "" : this.identifier, this.ipRange == null ? (new IPRange()).toString() : this.ipRange.toString(), this.protocol == null ? "" : protocol.toString(), this.version == null ? "" : this.version, - resource == null ? "" : resource, + this.resource == null ? "" : this.resource, this.snapshotId == null ? "" : this.snapshotId, this.cacheControl == null ? "" : this.cacheControl, this.contentDisposition == null ? "" : this.contentDisposition, @@ -442,13 +459,12 @@ private String stringToSign(final String verifiedPermissions, final String resou ); } - private String stringToSign(final String verifiedPermissions, final String resource, - final UserDelegationKey key, final String accountName) { + private String stringToSign(final UserDelegationKey key) { return String.join("\n", - verifiedPermissions == null ? "" : verifiedPermissions, + this.permissions == null ? "" : this.permissions, this.startTime == null ? "" : Utility.ISO_8601_UTC_DATE_FORMATTER.format(this.startTime), this.expiryTime == null ? "" : Utility.ISO_8601_UTC_DATE_FORMATTER.format(this.expiryTime), - getCanonicalName(accountName), + this.canonicalName == null ? "" : this.canonicalName, key.signedOid() == null ? "" : key.signedOid(), key.signedTid() == null ? "" : key.signedTid(), key.signedStart() == null ? "" : Utility.ISO_8601_UTC_DATE_FORMATTER.format(key.signedStart()), @@ -458,7 +474,7 @@ private String stringToSign(final String verifiedPermissions, final String resou this.ipRange == null ? new IPRange().toString() : this.ipRange.toString(), this.protocol == null ? "" : this.protocol.toString(), this.version == null ? "" : this.version, - resource == null ? "" : resource, + this.resource == null ? "" : this.resource, this.snapshotId == null ? "" : this.snapshotId, this.cacheControl == null ? "" : this.cacheControl, this.contentDisposition == null ? "" : this.contentDisposition, diff --git a/storage/client/blob/src/main/java/com/azure/storage/blob/StorageAsyncClient.java b/storage/client/blob/src/main/java/com/azure/storage/blob/StorageAsyncClient.java index d5b0aecc3a3f1..c6c48d7e5770d 100644 --- a/storage/client/blob/src/main/java/com/azure/storage/blob/StorageAsyncClient.java +++ b/storage/client/blob/src/main/java/com/azure/storage/blob/StorageAsyncClient.java @@ -18,6 +18,7 @@ import com.azure.storage.blob.models.StorageServiceProperties; import com.azure.storage.blob.models.StorageServiceStats; import com.azure.storage.blob.models.UserDelegationKey; +import com.azure.storage.common.credentials.SharedKeyCredential; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -233,4 +234,73 @@ public Mono> getAccountInfo() { .getAccountInfo() .map(rb -> new SimpleResponse<>(rb, new StorageAccountInfo(rb.deserializedHeaders()))); } + + /** + * Generates an account SAS token with the specified parameters + * + * @param accountSASService + * The {@code AccountSASService} services for the account SAS + * @param accountSASResourceType + * An optional {@code AccountSASResourceType} resources for the account SAS + * @param accountSASPermission + * The {@code AccountSASPermission} permission for the account SAS + * @param expiryTime + * The {@code OffsetDateTime} expiry time for the account SAS + * + * @return + * A string that represents the SAS token + */ + public String generateAccountSAS(AccountSASService accountSASService, AccountSASResourceType accountSASResourceType, + AccountSASPermission accountSASPermission, OffsetDateTime expiryTime) { + return this.generateAccountSAS(accountSASService, accountSASResourceType, accountSASPermission, expiryTime, + null /* startTime */, null /* version */, null /* ipRange */, null /* sasProtocol */); + } + + /** + * Generates an account SAS token with the specified parameters + * + * @param accountSASService + * The {@code AccountSASService} services for the account SAS + * @param accountSASResourceType + * An optional {@code AccountSASResourceType} resources for the account SAS + * @param accountSASPermission + * The {@code AccountSASPermission} permission for the account SAS + * @param expiryTime + * The {@code OffsetDateTime} expiry time for the account SAS + * @param startTime + * The {@code OffsetDateTime} start time for the account SAS + * @param version + * The {@code String} version for the account SAS + * @param ipRange + * An optional {@code IPRange} ip address range for the SAS + * @param sasProtocol + * An optional {@code SASProtocol} protocol for the SAS + * + * @return + * A string that represents the SAS token + */ + public String generateAccountSAS(AccountSASService accountSASService, AccountSASResourceType accountSASResourceType, + AccountSASPermission accountSASPermission, OffsetDateTime expiryTime, OffsetDateTime startTime, String version, IPRange ipRange, + SASProtocol sasProtocol) { + + AccountSASSignatureValues accountSASSignatureValues = new AccountSASSignatureValues(); + accountSASSignatureValues.services(accountSASService == null ? null : accountSASService.toString()); + accountSASSignatureValues.resourceTypes(accountSASResourceType == null ? null : accountSASResourceType.toString()); + accountSASSignatureValues.permissions(accountSASPermission == null ? null : accountSASPermission.toString()); + accountSASSignatureValues.expiryTime(expiryTime); + accountSASSignatureValues.startTime(startTime); + + if (version != null) { + accountSASSignatureValues.version(version); + } + + accountSASSignatureValues.ipRange(ipRange); + accountSASSignatureValues.protocol(sasProtocol); + + SharedKeyCredential sharedKeyCredential = Utility.getSharedKeyCredential(this.storageAsyncRawClient.azureBlobStorage.httpPipeline()); + + SASQueryParameters sasQueryParameters = accountSASSignatureValues.generateSASQueryParameters(sharedKeyCredential); + + return sasQueryParameters.encode(); + } } diff --git a/storage/client/blob/src/main/java/com/azure/storage/blob/StorageClient.java b/storage/client/blob/src/main/java/com/azure/storage/blob/StorageClient.java index f18cf0640fbd9..c895cdda58bbc 100644 --- a/storage/client/blob/src/main/java/com/azure/storage/blob/StorageClient.java +++ b/storage/client/blob/src/main/java/com/azure/storage/blob/StorageClient.java @@ -290,4 +290,53 @@ public Response getAccountInfo(Duration timeout) { return Utility.blockWithOptionalTimeout(response, timeout); } + + /** + * Generates an account SAS token with the specified parameters + * + * @param accountSASService + * The {@code AccountSASService} services for the account SAS + * @param accountSASResourceType + * An optional {@code AccountSASResourceType} resources for the account SAS + * @param accountSASPermission + * The {@code AccountSASPermission} permission for the account SAS + * @param expiryTime + * The {@code OffsetDateTime} expiry time for the account SAS + * + * @return + * A string that represents the SAS token + */ + public String generateAccountSAS(AccountSASService accountSASService, AccountSASResourceType accountSASResourceType, + AccountSASPermission accountSASPermission, OffsetDateTime expiryTime) { + return this.storageAsyncClient.generateAccountSAS(accountSASService, accountSASResourceType, accountSASPermission, expiryTime); + } + + /** + * Generates an account SAS token with the specified parameters + * + * @param accountSASService + * The {@code AccountSASService} services for the account SAS + * @param accountSASResourceType + * An optional {@code AccountSASResourceType} resources for the account SAS + * @param accountSASPermission + * The {@code AccountSASPermission} permission for the account SAS + * @param expiryTime + * The {@code OffsetDateTime} expiry time for the account SAS + * @param startTime + * The {@code OffsetDateTime} start time for the account SAS + * @param version + * The {@code String} version for the account SAS + * @param ipRange + * An optional {@code IPRange} ip address range for the SAS + * @param sasProtocol + * An optional {@code SASProtocol} protocol for the SAS + * + * @return + * A string that represents the SAS token + */ + public String generateAccountSAS(AccountSASService accountSASService, AccountSASResourceType accountSASResourceType, + AccountSASPermission accountSASPermission, OffsetDateTime expiryTime, OffsetDateTime startTime, String version, IPRange ipRange, + SASProtocol sasProtocol) { + return this.storageAsyncClient.generateAccountSAS(accountSASService, accountSASResourceType, accountSASPermission, expiryTime, startTime, version, ipRange, sasProtocol); + } } diff --git a/storage/client/blob/src/main/java/com/azure/storage/blob/Utility.java b/storage/client/blob/src/main/java/com/azure/storage/blob/Utility.java index 3b5d073c4023e..252a5e83bbaf2 100644 --- a/storage/client/blob/src/main/java/com/azure/storage/blob/Utility.java +++ b/storage/client/blob/src/main/java/com/azure/storage/blob/Utility.java @@ -5,9 +5,13 @@ import com.azure.core.http.HttpHeader; import com.azure.core.http.HttpHeaders; +import com.azure.core.http.HttpPipeline; +import com.azure.core.http.policy.HttpPipelinePolicy; import com.azure.core.implementation.http.UrlBuilder; import com.azure.storage.blob.models.StorageErrorException; import com.azure.storage.blob.models.UserDelegationKey; +import com.azure.storage.common.credentials.SharedKeyCredential; +import com.azure.storage.common.policy.SharedKeyCredentialPolicy; import reactor.core.publisher.Mono; import reactor.util.annotation.Nullable; @@ -387,4 +391,23 @@ static T blockWithOptionalTimeout(Mono response, @Nullable Duration timeo return response.block(timeout); } } + + /** + * Gets the SharedKeyCredential from the HttpPipeline + * + * @param httpPipeline + * The {@code HttpPipeline} httpPipeline from which a sharedKeyCredential will be extracted + * + * @return The {@code SharedKeyCredential} sharedKeyCredential in the httpPipeline + */ + static SharedKeyCredential getSharedKeyCredential(HttpPipeline httpPipeline) { + for (int i = 0; i < httpPipeline.getPolicyCount(); i++) { + HttpPipelinePolicy httpPipelinePolicy = httpPipeline.getPolicy(i); + if (httpPipelinePolicy instanceof SharedKeyCredentialPolicy) { + SharedKeyCredentialPolicy sharedKeyCredentialPolicy = (SharedKeyCredentialPolicy) httpPipelinePolicy; + return sharedKeyCredentialPolicy.sharedKeyCredential(); + } + } + return null; + } } diff --git a/storage/client/blob/src/main/java/com/azure/storage/common/credentials/SASTokenCredential.java b/storage/client/blob/src/main/java/com/azure/storage/common/credentials/SASTokenCredential.java index fe93273e3f64e..4dce1c831ce21 100644 --- a/storage/client/blob/src/main/java/com/azure/storage/common/credentials/SASTokenCredential.java +++ b/storage/client/blob/src/main/java/com/azure/storage/common/credentials/SASTokenCredential.java @@ -18,12 +18,27 @@ public final class SASTokenCredential { private static final String SIGNED_PERMISSIONS = "sp"; private static final String SIGNED_EXPIRY = "se"; private static final String SIGNATURE = "sig"; + private static final String SIGNED_RESOURCE = "sr"; // Optional SAS token pieces private static final String SIGNED_START = "st"; private static final String SIGNED_PROTOCOL = "spr"; private static final String SIGNED_IP = "sip"; + private static final String CACHE_CONTROL = "rscc"; + private static final String CONTENT_DISPOSITION = "rscd"; + private static final String CONTENT_ENCODING = "rsce"; + private static final String CONTENT_LANGUAGE = "rscl"; + private static final String CONTENT_TYPE = "rsct"; + + // Possible User Delegation Key pieces + private static final String SIGNED_KEY_O_ID = "skoid"; + private static final String SIGNED_KEY_T_ID = "sktid"; + private static final String SIGNED_KEY_START = "skt"; + private static final String SIGNED_KEY_EXPIRY = "ske"; + private static final String SIGNED_KEY_SERVICE = "sks"; + private static final String SIGNED_KEY_VERSION = "skv"; + private final String sasToken; /** @@ -57,20 +72,34 @@ public static SASTokenCredential fromQuery(String query) { queryParams.put(key, queryParam); } - if (queryParams.size() < 6 - || !queryParams.containsKey(SIGNED_VERSION) - || !queryParams.containsKey(SIGNED_SERVICES) - || !queryParams.containsKey(SIGNED_RESOURCE_TYPES) - || !queryParams.containsKey(SIGNED_PERMISSIONS) - || !queryParams.containsKey(SIGNED_EXPIRY) - || !queryParams.containsKey(SIGNATURE)) { + /* Because ServiceSAS only requires expiry and permissions, both of which could be on the container + acl, the only guaranteed indication of a SAS is the signature. We'll let the service validate + the other query parameters. */ + if (!queryParams.containsKey(SIGNATURE)) { return null; } - StringBuilder sasTokenBuilder = new StringBuilder(queryParams.get(SIGNED_VERSION)) - .append("&").append(queryParams.get(SIGNED_SERVICES)) - .append("&").append(queryParams.get(SIGNED_RESOURCE_TYPES)) - .append("&").append(queryParams.get(SIGNED_PERMISSIONS)); + StringBuilder sasTokenBuilder = new StringBuilder(); + + if (queryParams.containsKey(SIGNED_VERSION)) { + sasTokenBuilder.append(queryParams.get(SIGNED_VERSION)); + } + + if (queryParams.containsKey(SIGNED_SERVICES)) { + sasTokenBuilder.append("&").append(queryParams.get(SIGNED_SERVICES)); + } + + if (queryParams.containsKey(SIGNED_RESOURCE_TYPES)) { + sasTokenBuilder.append("&").append(queryParams.get(SIGNED_RESOURCE_TYPES)); + } + + if (queryParams.containsKey(SIGNED_PERMISSIONS)) { + sasTokenBuilder.append("&").append(queryParams.get(SIGNED_PERMISSIONS)); + } + + if (queryParams.containsKey(SIGNED_RESOURCE)) { + sasTokenBuilder.append("&").append(queryParams.get(SIGNED_RESOURCE)); + } // SIGNED_START is optional if (queryParams.containsKey(SIGNED_START)) { @@ -89,6 +118,51 @@ public static SASTokenCredential fromQuery(String query) { sasTokenBuilder.append("&").append(queryParams.get(SIGNED_PROTOCOL)); } + if (queryParams.containsKey(CACHE_CONTROL)) { + sasTokenBuilder.append("&").append(queryParams.get(CACHE_CONTROL)); + } + + if (queryParams.containsKey(CONTENT_DISPOSITION)) { + sasTokenBuilder.append("&").append(queryParams.get(CONTENT_DISPOSITION)); + } + + if (queryParams.containsKey(CONTENT_ENCODING)) { + sasTokenBuilder.append("&").append(queryParams.get(CONTENT_ENCODING)); + } + + if (queryParams.containsKey(CONTENT_LANGUAGE)) { + sasTokenBuilder.append("&").append(queryParams.get(CONTENT_LANGUAGE)); + } + + if (queryParams.containsKey(CONTENT_TYPE)) { + sasTokenBuilder.append("&").append(queryParams.get(CONTENT_TYPE)); + } + + // User Delegation Key Parameters + if (queryParams.containsKey(SIGNED_KEY_O_ID)) { + sasTokenBuilder.append("&").append(queryParams.get(SIGNED_KEY_O_ID)); + } + + if (queryParams.containsKey(SIGNED_KEY_T_ID)) { + sasTokenBuilder.append("&").append(queryParams.get(SIGNED_KEY_T_ID)); + } + + if (queryParams.containsKey(SIGNED_KEY_START)) { + sasTokenBuilder.append("&").append(queryParams.get(SIGNED_KEY_START)); + } + + if (queryParams.containsKey(SIGNED_KEY_EXPIRY)) { + sasTokenBuilder.append("&").append(queryParams.get(SIGNED_KEY_EXPIRY)); + } + + if (queryParams.containsKey(SIGNED_KEY_SERVICE)) { + sasTokenBuilder.append("&").append(queryParams.get(SIGNED_KEY_SERVICE)); + } + + if (queryParams.containsKey(SIGNED_KEY_VERSION)) { + sasTokenBuilder.append("&").append(queryParams.get(SIGNED_KEY_VERSION)); + } + sasTokenBuilder.append("&").append(queryParams.get(SIGNATURE)); return new SASTokenCredential(sasTokenBuilder.toString()); diff --git a/storage/client/blob/src/main/java/com/azure/storage/common/policy/SharedKeyCredentialPolicy.java b/storage/client/blob/src/main/java/com/azure/storage/common/policy/SharedKeyCredentialPolicy.java index 8ee1284591dd2..743eae8f262e4 100644 --- a/storage/client/blob/src/main/java/com/azure/storage/common/policy/SharedKeyCredentialPolicy.java +++ b/storage/client/blob/src/main/java/com/azure/storage/common/policy/SharedKeyCredentialPolicy.java @@ -24,6 +24,15 @@ public SharedKeyCredentialPolicy(SharedKeyCredential credential) { this.credential = credential; } + /** + * Gets the shared key credential linked to the policy. + * @return + * The {@link SharedKeyCredential} linked to the policy. + */ + public SharedKeyCredential sharedKeyCredential() { + return this.credential; + } + @Override public Mono process(HttpPipelineCallContext context, HttpPipelineNextPolicy next) { String authorizationValue = credential.generateAuthorizationHeader(context.httpRequest().url(), diff --git a/storage/client/blob/src/test/java/com/azure/storage/blob/SASTest.groovy b/storage/client/blob/src/test/java/com/azure/storage/blob/SASTest.groovy new file mode 100644 index 0000000000000..1ebf9860f2ac3 --- /dev/null +++ b/storage/client/blob/src/test/java/com/azure/storage/blob/SASTest.groovy @@ -0,0 +1,1079 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.storage.blob + +import com.azure.storage.blob.models.AccessPolicy +import com.azure.storage.blob.models.BlobRange +import com.azure.storage.blob.models.SignedIdentifier +import com.azure.storage.blob.models.StorageErrorCode +import com.azure.storage.blob.models.UserDelegationKey +import com.azure.storage.common.credentials.SASTokenCredential +import com.azure.storage.common.credentials.SharedKeyCredential +import spock.lang.Unroll + +import java.time.LocalDateTime +import java.time.OffsetDateTime +import java.time.ZoneOffset + +class SASTest extends APISpec { + + def "responseError"() { + when: + cu.listBlobsFlat() + + then: + def e = thrown(StorageException) + e.errorCode() == StorageErrorCode.INVALID_QUERY_PARAMETER_VALUE + e.statusCode() == 400 + e.message().contains("Value for one of the query parameters specified in the request URI is invalid.") + e.getMessage().contains("