diff --git a/sdk/tables/azure-data-tables/CHANGELOG.md b/sdk/tables/azure-data-tables/CHANGELOG.md index 0412600a6812..f1815ce615e6 100644 --- a/sdk/tables/azure-data-tables/CHANGELOG.md +++ b/sdk/tables/azure-data-tables/CHANGELOG.md @@ -5,6 +5,20 @@ ### New Features - Introduced the `TableTransactionAction` class and the `TableTransactionActionType` enum. +- Added support for generating SAS tokens at the Account and Table Service level in all clients. +- Added the following methods to `TableClient`, `TableAsyncClient`: + - `listAccessPolicies()` + - `setAccessPolicies()` + - `setAccessPoliciesWithResponse()` + - `generateSasToken()` +- Added the following methods to `TableServiceClient`, `TableServiceAsyncClient`: + - `getProperties()` + - `getPropertiesWithResponse()` + - `setProperties()` + - `setPropertiesWithResponse()` + - `getStatistics()` + - `getStatisticsWithResponse()` + - `generateAccountSasToken()` ### Breaking Changes @@ -22,6 +36,7 @@ - `updateEntity(TableEntity entity, TableEntityUpdateMode updateMode, boolean ifUnchanged)` - `getEntity(String partitionKey, String rowKey, List select)` +- Client builders now also throw an `IllegalStateException` when calling `buildClient()` and `buildAsyncClient()` if multiple forms of authentication are provided, with the exception of `sasToken` + `connectionString`; or if `endpoint` and/or `sasToken` are set alongside a `connectionString` and the endpoint and/or SAS token in the latter are different than the former, respectively. ## 12.0.0-beta.7 (2021-05-15) diff --git a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/BuilderHelper.java b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/BuilderHelper.java index 7e6f350f11f3..788e5d1fb9fa 100644 --- a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/BuilderHelper.java +++ b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/BuilderHelper.java @@ -26,11 +26,14 @@ import com.azure.core.util.logging.ClientLogger; import com.azure.data.tables.implementation.CosmosPatchTransformPolicy; import com.azure.data.tables.implementation.NullHttpClient; +import com.azure.data.tables.implementation.StorageAuthenticationSettings; +import com.azure.data.tables.implementation.StorageConnectionString; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.StringJoiner; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -51,12 +54,14 @@ static HttpPipeline buildPipeline( retryPolicy = (retryPolicy == null) ? new RetryPolicy() : retryPolicy; logOptions = (logOptions == null) ? new HttpLogOptions() : logOptions; - validateSingleCredentialIsPresent(azureNamedKeyCredential, azureSasCredential, sasToken, logger); - // Closest to API goes first, closest to wire goes last. List policies = new ArrayList<>(); - if (endpoint.contains(COSMOS_ENDPOINT_SUFFIX)) { + if (endpoint == null) { + throw logger.logExceptionAsError( + new IllegalStateException("An 'endpoint' is required to create a client. Use builders' 'endpoint()' or" + + " 'connectionString()' methods to set this value.")); + } else if (endpoint.contains(COSMOS_ENDPOINT_SUFFIX)) { policies.add(new CosmosPatchTransformPolicy()); } @@ -82,7 +87,9 @@ static HttpPipeline buildPipeline( policies.add(retryPolicy); policies.add(new AddDatePolicy()); + HttpPipelinePolicy credentialPolicy; + if (azureNamedKeyCredential != null) { credentialPolicy = new TableAzureNamedKeyCredentialPolicy(azureNamedKeyCredential); } else if (azureSasCredential != null) { @@ -90,12 +97,12 @@ static HttpPipeline buildPipeline( } else if (sasToken != null) { credentialPolicy = new AzureSasCredentialPolicy(new AzureSasCredential(sasToken), false); } else { - credentialPolicy = null; + throw logger.logExceptionAsError( + new IllegalStateException("A form of authentication is required to create a client. Use a builder's " + + "'credential()', 'sasToken()' or 'connectionString()' methods to set a form of authentication.")); } - if (credentialPolicy != null) { - policies.add(credentialPolicy); - } + policies.add(credentialPolicy); // Add per retry additional policies. policies.addAll(perRetryAdditionalPolicies); @@ -121,17 +128,56 @@ static HttpPipeline buildNullClientPipeline() { .build(); } - private static void validateSingleCredentialIsPresent(AzureNamedKeyCredential azureNamedKeyCredential, - AzureSasCredential azureSasCredential, String sasToken, - ClientLogger logger) { - List usedCredentials = Stream.of(azureNamedKeyCredential, azureSasCredential, sasToken) - .filter(Objects::nonNull).collect(Collectors.toList()); + static void validateCredentials(AzureNamedKeyCredential azureNamedKeyCredential, + AzureSasCredential azureSasCredential, String sasToken, String connectionString, + ClientLogger logger) { + List usedCredentials = + Stream.of(azureNamedKeyCredential, azureSasCredential, sasToken, connectionString) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + // Only allow two forms of authentication when 'connectionString' and 'sasToken' are provided. Validate that + // both contain the same SAS settings. + if (usedCredentials.size() == 2 && connectionString != null && sasToken != null) { + StorageConnectionString storageConnectionString = + StorageConnectionString.create(connectionString, logger); + StorageAuthenticationSettings authSettings = storageConnectionString.getStorageAuthSettings(); + + if (authSettings.getType() == StorageAuthenticationSettings.Type.SAS_TOKEN) { + if (sasToken.equals(authSettings.getSasToken())) { + return; + } else { + throw logger.logExceptionAsError(new IllegalStateException("'connectionString' contains a SAS token" + + " with different settings than 'sasToken'.")); + } + } + + // If the 'connectionString' auth type is not SAS_TOKEN and a 'sasToken' was provided, then multiplte + // incompatible forms of authentication were specified in the client builder. + } + if (usedCredentials.size() > 1) { + StringJoiner usedCredentialsStringBuilder = new StringJoiner(", "); + + if (azureNamedKeyCredential != null) { + usedCredentialsStringBuilder.add("azureNamedKeyCredential"); + } + + if (azureSasCredential != null) { + usedCredentialsStringBuilder.add("azureSasCredential"); + } + + if (sasToken != null) { + usedCredentialsStringBuilder.add("sasToken"); + } + + if (connectionString != null) { + usedCredentialsStringBuilder.add("connectionString"); + } + throw logger.logExceptionAsError(new IllegalStateException( - "Only one credential should be used. Credentials present: " - + usedCredentials.stream().map(c -> c instanceof String ? "sasToken" : c.getClass().getName()) - .collect(Collectors.joining(",")) - )); + "Only one form of authentication should be used. The authentication forms present are: " + + usedCredentialsStringBuilder + ".")); } } } diff --git a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/TableAsyncClient.java b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/TableAsyncClient.java index fead6b55b6d7..33d3b6539779 100644 --- a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/TableAsyncClient.java +++ b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/TableAsyncClient.java @@ -5,6 +5,7 @@ import com.azure.core.annotation.ReturnType; import com.azure.core.annotation.ServiceClient; import com.azure.core.annotation.ServiceMethod; +import com.azure.core.credential.AzureNamedKeyCredential; import com.azure.core.http.HttpHeaders; import com.azure.core.http.HttpPipeline; import com.azure.core.http.HttpRequest; @@ -21,15 +22,12 @@ import com.azure.core.util.serializer.SerializerAdapter; import com.azure.data.tables.implementation.AzureTableImpl; import com.azure.data.tables.implementation.AzureTableImplBuilder; -import com.azure.data.tables.implementation.TransactionalBatchImpl; import com.azure.data.tables.implementation.ModelHelper; +import com.azure.data.tables.implementation.TableSasGenerator; +import com.azure.data.tables.implementation.TableSasUtils; import com.azure.data.tables.implementation.TableUtils; +import com.azure.data.tables.implementation.TransactionalBatchImpl; import com.azure.data.tables.implementation.models.AccessPolicy; -import com.azure.data.tables.implementation.models.TransactionalBatchChangeSet; -import com.azure.data.tables.implementation.models.TransactionalBatchAction; -import com.azure.data.tables.implementation.models.TransactionalBatchRequestBody; -import com.azure.data.tables.implementation.models.TransactionalBatchSubRequest; -import com.azure.data.tables.implementation.models.TransactionalBatchResponse; import com.azure.data.tables.implementation.models.OdataMetadataFormat; import com.azure.data.tables.implementation.models.QueryOptions; import com.azure.data.tables.implementation.models.ResponseFormat; @@ -38,7 +36,11 @@ import com.azure.data.tables.implementation.models.TableProperties; import com.azure.data.tables.implementation.models.TableResponseProperties; import com.azure.data.tables.implementation.models.TableServiceError; -import com.azure.data.tables.models.TableTransactionActionResponse; +import com.azure.data.tables.implementation.models.TransactionalBatchAction; +import com.azure.data.tables.implementation.models.TransactionalBatchChangeSet; +import com.azure.data.tables.implementation.models.TransactionalBatchRequestBody; +import com.azure.data.tables.implementation.models.TransactionalBatchResponse; +import com.azure.data.tables.implementation.models.TransactionalBatchSubRequest; import com.azure.data.tables.models.ListEntitiesOptions; import com.azure.data.tables.models.TableAccessPolicy; import com.azure.data.tables.models.TableEntity; @@ -47,8 +49,10 @@ import com.azure.data.tables.models.TableServiceException; import com.azure.data.tables.models.TableSignedIdentifier; import com.azure.data.tables.models.TableTransactionAction; +import com.azure.data.tables.models.TableTransactionActionResponse; import com.azure.data.tables.models.TableTransactionFailedException; import com.azure.data.tables.models.TableTransactionResult; +import com.azure.data.tables.sas.TableSasSignatureValues; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -195,6 +199,30 @@ public TableServiceVersion getServiceVersion() { return TableServiceVersion.fromString(tablesImplementation.getVersion()); } + /** + * Generates a service SAS for the table using the specified {@link TableSasSignatureValues}. + * + *

Note : The client must be authenticated via {@link AzureNamedKeyCredential} + *

See {@link TableSasSignatureValues} for more information on how to construct a service SAS.

+ * + * @param tableSasSignatureValues {@link TableSasSignatureValues} + * + * @return A {@code String} representing the SAS query parameters. + * + * @throws IllegalStateException If this {@link TableAsyncClient} is not authenticated with an + * {@link AzureNamedKeyCredential}. + */ + public String generateSas(TableSasSignatureValues tableSasSignatureValues) { + AzureNamedKeyCredential azureNamedKeyCredential = TableSasUtils.extractNamedKeyCredential(getHttpPipeline()); + + if (azureNamedKeyCredential == null) { + throw logger.logExceptionAsError(new IllegalStateException("Cannot generate a SAS token with a client that" + + " is not authenticated with an AzureNamedKeyCredential.")); + } + + return new TableSasGenerator(tableSasSignatureValues, getTableName(), azureNamedKeyCredential).getSas(); + } + /** * Creates the table within the Tables service. * @@ -821,11 +849,11 @@ Mono> getEntityWithResponse(String partition * {@link TableSignedIdentifier access policies}. */ @ServiceMethod(returns = ReturnType.COLLECTION) - public PagedFlux getAccessPolicy() { - return (PagedFlux) fluxContext(this::getAccessPolicy); + public PagedFlux listAccessPolicies() { + return (PagedFlux) fluxContext(this::listAccessPolicies); } - PagedFlux getAccessPolicy(Context context) { + PagedFlux listAccessPolicies(Context context) { context = context == null ? Context.NONE : context; try { @@ -870,8 +898,8 @@ private TableAccessPolicy toTableAccessPolicy(AccessPolicy accessPolicy) { * @return An empty reactive result. */ @ServiceMethod(returns = ReturnType.SINGLE) - public Mono setAccessPolicy(List tableSignedIdentifiers) { - return this.setAccessPolicyWithResponse(tableSignedIdentifiers).flatMap(FluxUtil::toMono); + public Mono setAccessPolicies(List tableSignedIdentifiers) { + return this.setAccessPoliciesWithResponse(tableSignedIdentifiers).flatMap(FluxUtil::toMono); } /** @@ -883,12 +911,12 @@ public Mono setAccessPolicy(List tableSignedIdentif * @return A reactive result containing the HTTP response. */ @ServiceMethod(returns = ReturnType.SINGLE) - public Mono> setAccessPolicyWithResponse(List tableSignedIdentifiers) { - return withContext(context -> this.setAccessPolicyWithResponse(tableSignedIdentifiers, context)); + public Mono> setAccessPoliciesWithResponse(List tableSignedIdentifiers) { + return withContext(context -> this.setAccessPoliciesWithResponse(tableSignedIdentifiers, context)); } - Mono> setAccessPolicyWithResponse(List tableSignedIdentifiers, - Context context) { + Mono> setAccessPoliciesWithResponse(List tableSignedIdentifiers, + Context context) { context = context == null ? Context.NONE : context; try { diff --git a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/TableAzureNamedKeyCredentialPolicy.java b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/TableAzureNamedKeyCredentialPolicy.java index 6bddd72130ef..0701a99db7e0 100644 --- a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/TableAzureNamedKeyCredentialPolicy.java +++ b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/TableAzureNamedKeyCredentialPolicy.java @@ -17,7 +17,7 @@ import java.util.Locale; import java.util.Map; -import static com.azure.data.tables.implementation.TableUtils.computeHMac256; +import static com.azure.data.tables.implementation.TableSasUtils.computeHmac256; import static com.azure.data.tables.implementation.TableUtils.parseQueryStringSplitValues; /** @@ -41,24 +41,27 @@ public TableAzureNamedKeyCredentialPolicy(AzureNamedKeyCredential credential) { * * @param context The context of the request. * @param next The next policy in the pipeline. + * * @return A reactive result containing the HTTP response. */ public Mono process(HttpPipelineCallContext context, HttpPipelineNextPolicy next) { String authorizationValue = generateAuthorizationHeader(context.getHttpRequest().getUrl(), context.getHttpRequest().getHeaders().toMap()); context.getHttpRequest().setHeader("Authorization", authorizationValue); + return next.process(); } /** * Generates the Auth Headers * - * @param requestUrl the URL which the request is going to - * @param headers the headers of the request - * @return the auth header + * @param requestUrl The URL which the request is going to. + * @param headers The headers of the request. + * + * @return The auth header */ String generateAuthorizationHeader(URL requestUrl, Map headers) { - String signature = computeHMac256(credential.getAzureNamedKey().getKey(), buildStringToSign(requestUrl, + String signature = computeHmac256(credential.getAzureNamedKey().getKey(), buildStringToSign(requestUrl, headers)); return String.format(AUTHORIZATION_HEADER_FORMAT, credential.getAzureNamedKey().getName(), signature); } @@ -68,6 +71,7 @@ String generateAuthorizationHeader(URL requestUrl, Map headers) * * @param requestUrl The Url which the request is going to. * @param headers The headers of the request. + * * @return A string to sign for the request. */ private String buildStringToSign(URL requestUrl, Map headers) { @@ -87,6 +91,7 @@ private String buildStringToSign(URL requestUrl, Map headers) { * * @param headers A map of the headers which the request has. * @param headerName The name of the header to get the standard header for. + * * @return The standard header for the given name. */ private String getStandardHeaderValue(Map headers, String headerName) { @@ -100,6 +105,7 @@ private String getStandardHeaderValue(Map headers, String header * Returns the canonicalized resource needed for a request. * * @param requestUrl The url of the request. + * * @return The string that is the canonicalized resource. */ private String getCanonicalizedResource(URL requestUrl) { @@ -133,4 +139,13 @@ private String getCanonicalizedResource(URL requestUrl) { return canonicalizedResource.toString(); } + + /** + * Get the {@link AzureNamedKeyCredential} linked to the policy. + * + * @return The {@link AzureNamedKeyCredential}. + */ + public AzureNamedKeyCredential getCredential() { + return credential; + } } diff --git a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/TableClient.java b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/TableClient.java index f811b2ee05e4..ecd189746b23 100644 --- a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/TableClient.java +++ b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/TableClient.java @@ -5,10 +5,10 @@ import com.azure.core.annotation.ReturnType; import com.azure.core.annotation.ServiceClient; import com.azure.core.annotation.ServiceMethod; +import com.azure.core.credential.AzureNamedKeyCredential; import com.azure.core.http.rest.PagedIterable; import com.azure.core.http.rest.Response; import com.azure.core.util.Context; -import com.azure.data.tables.models.TableTransactionActionResponse; import com.azure.data.tables.models.ListEntitiesOptions; import com.azure.data.tables.models.TableEntity; import com.azure.data.tables.models.TableEntityUpdateMode; @@ -16,8 +16,10 @@ import com.azure.data.tables.models.TableServiceException; import com.azure.data.tables.models.TableSignedIdentifier; import com.azure.data.tables.models.TableTransactionAction; +import com.azure.data.tables.models.TableTransactionActionResponse; import com.azure.data.tables.models.TableTransactionFailedException; import com.azure.data.tables.models.TableTransactionResult; +import com.azure.data.tables.sas.TableSasSignatureValues; import java.time.Duration; import java.util.List; @@ -80,6 +82,23 @@ public TableServiceVersion getServiceVersion() { return this.client.getServiceVersion(); } + /** + * Generates a service SAS for the table using the specified {@link TableSasSignatureValues}. + * + *

Note : The client must be authenticated via {@link AzureNamedKeyCredential} + *

See {@link TableSasSignatureValues} for more information on how to construct a service SAS.

+ * + * @param tableSasSignatureValues {@link TableSasSignatureValues} + * + * @return A {@code String} representing the SAS query parameters. + * + * @throws IllegalStateException If this {@link TableClient} is not authenticated with an + * {@link AzureNamedKeyCredential}. + */ + public String generateSas(TableSasSignatureValues tableSasSignatureValues) { + return client.generateSas(tableSasSignatureValues); + } + /** * Creates the table within the Tables service. * @@ -396,8 +415,8 @@ public Response getEntityWithResponse(String partitionKey, String r * {@link TableSignedIdentifier access policies}. */ @ServiceMethod(returns = ReturnType.COLLECTION) - public PagedIterable getAccessPolicy() { - return new PagedIterable<>(client.getAccessPolicy()); + public PagedIterable listAccessPolicies() { + return new PagedIterable<>(client.listAccessPolicies()); } /** @@ -411,8 +430,8 @@ public PagedIterable getAccessPolicy() { * {@link TableSignedIdentifier access policies}. */ @ServiceMethod(returns = ReturnType.COLLECTION) - public PagedIterable getAccessPolicy(Duration timeout, Context context) { - return new PagedIterable<>(applyOptionalTimeout(client.getAccessPolicy(context), timeout)); + public PagedIterable listAccessPolicies(Duration timeout, Context context) { + return new PagedIterable<>(applyOptionalTimeout(client.listAccessPolicies(context), timeout)); } /** @@ -421,8 +440,8 @@ public PagedIterable getAccessPolicy(Duration timeout, Co * @param tableSignedIdentifiers The {@link TableSignedIdentifier access policies} for the table. */ @ServiceMethod(returns = ReturnType.SINGLE) - public void setAccessPolicy(List tableSignedIdentifiers) { - client.setAccessPolicy(tableSignedIdentifiers).block(); + public void setAccessPolicies(List tableSignedIdentifiers) { + client.setAccessPolicies(tableSignedIdentifiers).block(); } /** @@ -436,9 +455,9 @@ public void setAccessPolicy(List tableSignedIdentifiers) * @return The HTTP response. */ @ServiceMethod(returns = ReturnType.SINGLE) - public Response setAccessPolicyWithResponse(List tableSignedIdentifiers, - Duration timeout, Context context) { - return blockWithOptionalTimeout(client.setAccessPolicyWithResponse(tableSignedIdentifiers, context), timeout); + public Response setAccessPoliciesWithResponse(List tableSignedIdentifiers, + Duration timeout, Context context) { + return blockWithOptionalTimeout(client.setAccessPoliciesWithResponse(tableSignedIdentifiers, context), timeout); } /** diff --git a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/TableClientBuilder.java b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/TableClientBuilder.java index cda5f3e71965..b21877a735a6 100644 --- a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/TableClientBuilder.java +++ b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/TableClientBuilder.java @@ -27,6 +27,8 @@ import java.util.ArrayList; import java.util.List; +import static com.azure.data.tables.BuilderHelper.validateCredentials; + /** * This class provides a fluent builder API to help aid the configuration and instantiation of {@link TableClient} and * {@link TableAsyncClient} objects. Call {@link #buildClient()} or {@link #buildAsyncClient()}, respectively, to @@ -43,6 +45,7 @@ public final class TableClientBuilder { private String tableName; private Configuration configuration; private HttpClient httpClient; + private String connectionString; private String endpoint; private HttpLogOptions httpLogOptions; private ClientOptions clientOptions; @@ -65,8 +68,13 @@ public TableClientBuilder() { * * @return A {@link TableClient} created from the configurations in this builder. * - * @throws IllegalArgumentException If {@code tableName} is {@code null} or empty. - * @throws IllegalStateException If multiple credentials have been specified. + * @throws NullPointerException If {@code endpoint} or {@code tableName} are {@code null}. + * @throws IllegalArgumentException If {@code endpoint} is malformed or empty or if {@code tableName} is empty. + * @throws IllegalStateException If no form of authentication or {@code endpoint} have been specified or if + * multiple forms of authentication are provided, with the exception of {@code sasToken} + + * {@code connectionString}. Also thrown if {@code endpoint} and/or {@code sasToken} are set alongside a + * {@code connectionString} and the endpoint and/or SAS token in the latter are different than the former, + * respectively. */ public TableClient buildClient() { return new TableClient(buildAsyncClient()); @@ -77,12 +85,63 @@ public TableClient buildClient() { * * @return A {@link TableAsyncClient} created from the configurations in this builder. * - * @throws IllegalArgumentException If {@code tableName} is {@code null} or empty. - * @throws IllegalStateException If multiple credentials have been specified. + * @throws NullPointerException If {@code endpoint} or {@code tableName} are {@code null}. + * @throws IllegalArgumentException If {@code endpoint} is malformed or empty or if {@code tableName} is empty. + * @throws IllegalStateException If no form of authentication or {@code endpoint} have been specified or if + * multiple forms of authentication are provided, with the exception of {@code sasToken} + + * {@code connectionString}. Also thrown if {@code endpoint} and/or {@code sasToken} are set alongside a + * {@code connectionString} and the endpoint and/or SAS token in the latter are different than the former, + * respectively. */ public TableAsyncClient buildAsyncClient() { TableServiceVersion serviceVersion = version != null ? version : TableServiceVersion.getLatest(); + validateCredentials(azureNamedKeyCredential, azureSasCredential, sasToken, connectionString, logger); + + // If 'connectionString' was provided, extract the endpoint and sasToken. + if (connectionString != null) { + StorageConnectionString storageConnectionString = StorageConnectionString.create(connectionString, logger); + StorageEndpoint storageConnectionStringTableEndpoint = storageConnectionString.getTableEndpoint(); + + if (storageConnectionStringTableEndpoint == null + || storageConnectionStringTableEndpoint.getPrimaryUri() == null) { + + throw logger.logExceptionAsError(new IllegalArgumentException( + "'connectionString' is missing the required settings to derive a Tables endpoint.")); + } + + String connectionStringEndpoint = storageConnectionStringTableEndpoint.getPrimaryUri(); + + // If no 'endpoint' was provided, use the one in the 'connectionString'. Else, verify they are the same. + if (endpoint == null) { + endpoint = connectionStringEndpoint; + } else { + if (endpoint.endsWith("/")) { + endpoint = endpoint.substring(0, endpoint.length() - 1); + } + + if (connectionStringEndpoint.endsWith("/")) { + connectionStringEndpoint = + connectionStringEndpoint.substring(0, connectionStringEndpoint.length() - 1); + } + + if (!endpoint.equals(connectionStringEndpoint)) { + throw logger.logExceptionAsError(new IllegalStateException( + "'endpoint' points to a different tables endpoint than 'connectionString'.")); + } + } + + StorageAuthenticationSettings authSettings = storageConnectionString.getStorageAuthSettings(); + + if (authSettings.getType() == StorageAuthenticationSettings.Type.ACCOUNT_NAME_KEY) { + azureNamedKeyCredential = (azureNamedKeyCredential != null) ? azureNamedKeyCredential + : new AzureNamedKeyCredential(authSettings.getAccount().getName(), + authSettings.getAccount().getAccessKey()); + } else if (authSettings.getType() == StorageAuthenticationSettings.Type.SAS_TOKEN) { + sasToken = (sasToken != null) ? sasToken : authSettings.getSasToken(); + } + } + HttpPipeline pipeline = (httpPipeline != null) ? httpPipeline : BuilderHelper.buildPipeline( azureNamedKeyCredential, azureSasCredential, sasToken, endpoint, retryPolicy, httpLogOptions, clientOptions, httpClient, perCallPolicies, perRetryPolicies, configuration, logger); @@ -98,6 +157,7 @@ public TableAsyncClient buildAsyncClient() { * * @return The updated {@link TableClientBuilder}. * + * @throws NullPointerException If {@code connectionString} is {@code null}. * @throws IllegalArgumentException If {@code connectionString} isn't a valid connection string. */ public TableClientBuilder connectionString(String connectionString) { @@ -105,25 +165,9 @@ public TableClientBuilder connectionString(String connectionString) { throw logger.logExceptionAsError(new NullPointerException("'connectionString' cannot be null.")); } - StorageConnectionString storageConnectionString = StorageConnectionString.create(connectionString, logger); - StorageEndpoint endpoint = storageConnectionString.getTableEndpoint(); - - if (endpoint == null || endpoint.getPrimaryUri() == null) { - throw logger.logExceptionAsError( - new IllegalArgumentException( - "'connectionString' missing required settings to derive tables service endpoint.")); - } - - this.endpoint(endpoint.getPrimaryUri()); - - StorageAuthenticationSettings authSettings = storageConnectionString.getStorageAuthSettings(); + StorageConnectionString.create(connectionString, logger); - if (authSettings.getType() == StorageAuthenticationSettings.Type.ACCOUNT_NAME_KEY) { - this.credential(new AzureNamedKeyCredential(authSettings.getAccount().getName(), - authSettings.getAccount().getAccessKey())); - } else if (authSettings.getType() == StorageAuthenticationSettings.Type.SAS_TOKEN) { - this.sasToken(authSettings.getSasToken()); - } + this.connectionString = connectionString; return this; } @@ -135,6 +179,7 @@ public TableClientBuilder connectionString(String connectionString) { * * @return The updated {@link TableClientBuilder}. * + * @throws NullPointerException If {@code endpoint} is {@code null}. * @throws IllegalArgumentException If {@code endpoint} isn't a valid URL. */ public TableClientBuilder endpoint(String endpoint) { @@ -184,14 +229,15 @@ public TableClientBuilder configuration(Configuration configuration) { } /** - * Sets the SAS token used to authorize requests sent to the service. + * Sets the SAS token used to authorize requests sent to the service. Setting this is mutually exclusive with + * {@code credential(AzureSasCredential)} or {@code credential(AzureNamedKeyCredential)}. * * @param sasToken The SAS token to use for authenticating requests. * * @return The updated {@link TableClientBuilder}. * - * @throws IllegalArgumentException If {@code sasToken} is empty. * @throws NullPointerException If {@code sasToken} is {@code null}. + * @throws IllegalArgumentException If {@code sasToken} is empty. */ public TableClientBuilder sasToken(String sasToken) { if (sasToken == null) { @@ -199,17 +245,17 @@ public TableClientBuilder sasToken(String sasToken) { } if (sasToken.isEmpty()) { - throw logger.logExceptionAsError(new IllegalArgumentException("'sasToken' cannot be null or empty.")); + throw logger.logExceptionAsError(new IllegalArgumentException("'sasToken' cannot be empty.")); } this.sasToken = sasToken; - this.azureNamedKeyCredential = null; return this; } /** - * Sets the {@link AzureSasCredential} used to authorize requests sent to the service. + * Sets the {@link AzureSasCredential} used to authorize requests sent to the service. Setting this is mutually + * exclusive with {@code credential(AzureNamedKeyCredential)} or {@code sasToken(String)}. * * @param credential {@link AzureSasCredential} used to authorize requests sent to the service. * @@ -228,7 +274,8 @@ public TableClientBuilder credential(AzureSasCredential credential) { } /** - * Sets the {@link AzureNamedKeyCredential} used to authorize requests sent to the service. + * Sets the {@link AzureNamedKeyCredential} used to authorize requests sent to the service. Setting this is mutually + * exclusive with using {@code credential(AzureSasCredential)} or {@code sasToken(String)}. * * @param credential {@link AzureNamedKeyCredential} used to authorize requests sent to the service. * @@ -242,7 +289,6 @@ public TableClientBuilder credential(AzureNamedKeyCredential credential) { } this.azureNamedKeyCredential = credential; - this.sasToken = null; return this; } diff --git a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/TableServiceAsyncClient.java b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/TableServiceAsyncClient.java index 0eb9d2ff0e6d..180a167db98a 100644 --- a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/TableServiceAsyncClient.java +++ b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/TableServiceAsyncClient.java @@ -5,6 +5,7 @@ import com.azure.core.annotation.ReturnType; import com.azure.core.annotation.ServiceClient; import com.azure.core.annotation.ServiceMethod; +import com.azure.core.credential.AzureNamedKeyCredential; import com.azure.core.http.HttpHeaders; import com.azure.core.http.HttpPipeline; import com.azure.core.http.HttpRequest; @@ -21,6 +22,8 @@ import com.azure.data.tables.implementation.AzureTableImpl; import com.azure.data.tables.implementation.AzureTableImplBuilder; import com.azure.data.tables.implementation.ModelHelper; +import com.azure.data.tables.implementation.TableAccountSasGenerator; +import com.azure.data.tables.implementation.TableSasUtils; import com.azure.data.tables.implementation.TableUtils; import com.azure.data.tables.implementation.models.CorsRule; import com.azure.data.tables.implementation.models.GeoReplication; @@ -45,6 +48,7 @@ import com.azure.data.tables.models.TableServiceProperties; import com.azure.data.tables.models.TableServiceRetentionPolicy; import com.azure.data.tables.models.TableServiceStatistics; +import com.azure.data.tables.sas.TableAccountSasSignatureValues; import reactor.core.publisher.Mono; import java.net.URI; @@ -80,7 +84,7 @@ public final class TableServiceAsyncClient { this.accountName = uri.getHost().split("\\.", 2)[0]; logger.verbose("Table Service URI: {}", uri); - } catch (IllegalArgumentException ex) { + } catch (NullPointerException | IllegalArgumentException ex) { throw logger.logExceptionAsError(ex); } @@ -147,6 +151,31 @@ public TableServiceVersion getServiceVersion() { return TableServiceVersion.fromString(implementation.getVersion()); } + /** + * Generates an account SAS for the Azure Storage account using the specified + * {@link TableAccountSasSignatureValues}. + * + *

Note : The client must be authenticated via {@link AzureNamedKeyCredential}. + *

See {@link TableAccountSasSignatureValues} for more information on how to construct an account SAS.

+ * + * @param tableAccountSasSignatureValues {@link TableAccountSasSignatureValues}. + * + * @return A {@link String} representing the SAS query parameters. + * + * @throws IllegalStateException If this {@link TableClient} is not authenticated with an + * {@link AzureNamedKeyCredential}. + */ + public String generateAccountSas(TableAccountSasSignatureValues tableAccountSasSignatureValues) { + AzureNamedKeyCredential azureNamedKeyCredential = TableSasUtils.extractNamedKeyCredential(getHttpPipeline()); + + if (azureNamedKeyCredential == null) { + throw logger.logExceptionAsError(new IllegalStateException("Cannot generate a SAS token with a client that" + + " is not authenticated with an AzureNamedKeyCredential.")); + } + + return new TableAccountSasGenerator(tableAccountSasSignatureValues, azureNamedKeyCredential).getSas(); + } + /** * Gets a {@link TableAsyncClient} instance for the provided table in the account. * diff --git a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/TableServiceClient.java b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/TableServiceClient.java index e7e85e270cce..f7bc25e59e7e 100644 --- a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/TableServiceClient.java +++ b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/TableServiceClient.java @@ -5,6 +5,7 @@ import com.azure.core.annotation.ReturnType; import com.azure.core.annotation.ServiceClient; import com.azure.core.annotation.ServiceMethod; +import com.azure.core.credential.AzureNamedKeyCredential; import com.azure.core.http.HttpResponse; import com.azure.core.http.rest.PagedIterable; import com.azure.core.http.rest.Response; @@ -18,6 +19,7 @@ import com.azure.data.tables.models.TableServiceException; import com.azure.data.tables.models.TableServiceProperties; import com.azure.data.tables.models.TableServiceStatistics; +import com.azure.data.tables.sas.TableAccountSasSignatureValues; import reactor.core.publisher.Mono; import java.time.Duration; @@ -71,6 +73,24 @@ public TableServiceVersion getServiceVersion() { return client.getServiceVersion(); } + /** + * Generates an account SAS for the Azure Storage account using the specified + * {@link TableAccountSasSignatureValues}. + * + *

Note : The client must be authenticated via {@link AzureNamedKeyCredential}. + *

See {@link TableAccountSasSignatureValues} for more information on how to construct an account SAS.

+ * + * @param tableAccountSasSignatureValues {@link TableAccountSasSignatureValues}. + * + * @return A {@link String} representing the SAS query parameters. + * + * @throws IllegalStateException If this {@link TableClient} is not authenticated with an + * {@link AzureNamedKeyCredential}. + */ + public String generateAccountSas(TableAccountSasSignatureValues tableAccountSasSignatureValues) { + return client.generateAccountSas(tableAccountSasSignatureValues); + } + /** * Gets a {@link TableClient} instance for the provided table in the account. * diff --git a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/TableServiceClientBuilder.java b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/TableServiceClientBuilder.java index c5a246b0addd..d139a0f88b63 100644 --- a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/TableServiceClientBuilder.java +++ b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/TableServiceClientBuilder.java @@ -26,6 +26,8 @@ import java.util.ArrayList; import java.util.List; +import static com.azure.data.tables.BuilderHelper.validateCredentials; + /** * This class provides a fluent builder API to help aid the configuration and instantiation of * {@link TableServiceClient} and {@link TableServiceAsyncClient} objects. Call {@link #buildClient()} or @@ -38,6 +40,7 @@ public final class TableServiceClientBuilder { private final List perCallPolicies = new ArrayList<>(); private final List perRetryPolicies = new ArrayList<>(); private Configuration configuration; + private String connectionString; private String endpoint; private HttpClient httpClient; private HttpLogOptions httpLogOptions; @@ -61,7 +64,13 @@ public TableServiceClientBuilder() { * * @return A {@link TableServiceClient} created from the configurations in this builder. * - * @throws IllegalStateException If multiple credentials have been specified. + * @throws NullPointerException If {@code endpoint} is {@code null}. + * @throws IllegalArgumentException If {@code endpoint} is malformed or empty. + * @throws IllegalStateException If no form of authentication or {@code endpoint} have been specified or if + * multiple forms of authentication are provided, with the exception of {@code sasToken} + + * {@code connectionString}. Also thrown if {@code endpoint} and/or {@code sasToken} are set alongside a + * {@code connectionString} and the endpoint and/or SAS token in the latter are different than the former, + * respectively. */ public TableServiceClient buildClient() { return new TableServiceClient(buildAsyncClient()); @@ -72,11 +81,63 @@ public TableServiceClient buildClient() { * * @return A {@link TableServiceAsyncClient} created from the configurations in this builder. * - * @throws IllegalStateException If multiple credentials have been specified. + * @throws NullPointerException If {@code endpoint} is {@code null}. + * @throws IllegalArgumentException If {@code endpoint} is malformed or empty. + * @throws IllegalStateException If no form of authentication or {@code endpoint} have been specified or if + * multiple forms of authentication are provided, with the exception of {@code sasToken} + + * {@code connectionString}. Also thrown if {@code endpoint} and/or {@code sasToken} are set alongside a + * {@code connectionString} and the endpoint and/or SAS token in the latter are different than the former, + * respectively. */ public TableServiceAsyncClient buildAsyncClient() { TableServiceVersion serviceVersion = version != null ? version : TableServiceVersion.getLatest(); + validateCredentials(azureNamedKeyCredential, azureSasCredential, sasToken, connectionString, logger); + + // If 'connectionString' was provided, extract the endpoint and sasToken. + if (connectionString != null) { + StorageConnectionString storageConnectionString = StorageConnectionString.create(connectionString, logger); + StorageEndpoint storageConnectionStringTableEndpoint = storageConnectionString.getTableEndpoint(); + + if (storageConnectionStringTableEndpoint == null + || storageConnectionStringTableEndpoint.getPrimaryUri() == null) { + + throw logger.logExceptionAsError(new IllegalArgumentException( + "'connectionString' is missing the required settings to derive a Tables endpoint.")); + } + + String connectionStringEndpoint = storageConnectionStringTableEndpoint.getPrimaryUri(); + + // If no 'endpoint' was provided, use the one in the 'connectionString'. Else, verify they are the same. + if (endpoint == null) { + endpoint = connectionStringEndpoint; + } else { + if (endpoint.endsWith("/")) { + endpoint = endpoint.substring(0, endpoint.length() - 1); + } + + if (connectionStringEndpoint.endsWith("/")) { + connectionStringEndpoint = + connectionStringEndpoint.substring(0, connectionStringEndpoint.length() - 1); + } + + if (!endpoint.equals(connectionStringEndpoint)) { + throw logger.logExceptionAsError(new IllegalStateException( + "'endpoint' points to a different tables endpoint than 'connectionString'.")); + } + } + + StorageAuthenticationSettings authSettings = storageConnectionString.getStorageAuthSettings(); + + if (authSettings.getType() == StorageAuthenticationSettings.Type.ACCOUNT_NAME_KEY) { + azureNamedKeyCredential = (azureNamedKeyCredential != null) ? azureNamedKeyCredential + : new AzureNamedKeyCredential(authSettings.getAccount().getName(), + authSettings.getAccount().getAccessKey()); + } else if (authSettings.getType() == StorageAuthenticationSettings.Type.SAS_TOKEN) { + sasToken = (sasToken != null) ? sasToken : authSettings.getSasToken(); + } + } + HttpPipeline pipeline = (httpPipeline != null) ? httpPipeline : BuilderHelper.buildPipeline( azureNamedKeyCredential, azureSasCredential, sasToken, endpoint, retryPolicy, httpLogOptions, clientOptions, httpClient, perCallPolicies, perRetryPolicies, configuration, logger); @@ -91,6 +152,7 @@ public TableServiceAsyncClient buildAsyncClient() { * * @return The updated {@link TableServiceClientBuilder}. * + * @throws NullPointerException If {@code connectionString} is {@code null}. * @throws IllegalArgumentException If {@code connectionString} isn't a valid connection string. */ public TableServiceClientBuilder connectionString(String connectionString) { @@ -98,25 +160,9 @@ public TableServiceClientBuilder connectionString(String connectionString) { throw logger.logExceptionAsError(new NullPointerException("'connectionString' cannot be null.")); } - StorageConnectionString storageConnectionString = StorageConnectionString.create(connectionString, logger); - StorageEndpoint endpoint = storageConnectionString.getTableEndpoint(); - - if (endpoint == null || endpoint.getPrimaryUri() == null) { - throw logger.logExceptionAsError( - new IllegalArgumentException( - "'connectionString' missing required settings to derive tables service endpoint.")); - } - - this.endpoint(endpoint.getPrimaryUri()); + StorageConnectionString.create(connectionString, logger); - StorageAuthenticationSettings authSettings = storageConnectionString.getStorageAuthSettings(); - - if (authSettings.getType() == StorageAuthenticationSettings.Type.ACCOUNT_NAME_KEY) { - this.credential(new AzureNamedKeyCredential(authSettings.getAccount().getName(), - authSettings.getAccount().getAccessKey())); - } else if (authSettings.getType() == StorageAuthenticationSettings.Type.SAS_TOKEN) { - this.sasToken(authSettings.getSasToken()); - } + this.connectionString = connectionString; return this; } @@ -177,13 +223,15 @@ public TableServiceClientBuilder configuration(Configuration configuration) { } /** - * Sets the SAS token used to authorize requests sent to the service. + * Sets the SAS token used to authorize requests sent to the service. Setting this is mutually exclusive with + * {@code credential(AzureSasCredential)} or {@code credential(AzureNamedKeyCredential)}. * * @param sasToken The SAS token to use for authenticating requests. * * @return The updated {@link TableServiceClientBuilder}. * - * @throws NullPointerException if {@code sasToken} is {@code null}. + * @throws NullPointerException If {@code sasToken} is {@code null}. + * @throws IllegalArgumentException If {@code sasToken} is empty. */ public TableServiceClientBuilder sasToken(String sasToken) { if (sasToken == null) { @@ -191,23 +239,23 @@ public TableServiceClientBuilder sasToken(String sasToken) { } if (sasToken.isEmpty()) { - throw logger.logExceptionAsError(new IllegalArgumentException("'sasToken' cannot be null or empty.")); + throw logger.logExceptionAsError(new IllegalArgumentException("'sasToken' cannot be empty.")); } this.sasToken = sasToken; - this.azureNamedKeyCredential = null; return this; } /** - * Sets the {@link AzureSasCredential} used to authorize requests sent to the service. + * Sets the {@link AzureSasCredential} used to authorize requests sent to the service. Setting this is mutually + * exclusive with {@code credential(AzureNamedKeyCredential)} or {@code sasToken(String)}. * * @param credential {@link AzureSasCredential} used to authorize requests sent to the service. * * @return The updated {@link TableServiceClientBuilder}. * - * @throws NullPointerException if {@code credential} is {@code null}. + * @throws NullPointerException If {@code credential} is {@code null}. */ public TableServiceClientBuilder credential(AzureSasCredential credential) { if (credential == null) { @@ -220,13 +268,14 @@ public TableServiceClientBuilder credential(AzureSasCredential credential) { } /** - * Sets the {@link AzureNamedKeyCredential} used to authorize requests sent to the service. + * Sets the {@link AzureNamedKeyCredential} used to authorize requests sent to the service. Setting this is mutually + * exclusive with using {@code credential(AzureSasCredential)} or {@code sasToken(String)}. * * @param credential {@link AzureNamedKeyCredential} used to authorize requests sent to the service. * * @return The updated {@link TableServiceClientBuilder}. * - * @throws NullPointerException if {@code credential} is {@code null}. + * @throws NullPointerException If {@code credential} is {@code null}. */ public TableServiceClientBuilder credential(AzureNamedKeyCredential credential) { if (credential == null) { @@ -234,7 +283,6 @@ public TableServiceClientBuilder credential(AzureNamedKeyCredential credential) } this.azureNamedKeyCredential = credential; - this.sasToken = null; return this; } @@ -279,7 +327,7 @@ public TableServiceClientBuilder httpLogOptions(HttpLogOptions logOptions) { * * @return The updated {@link TableServiceClientBuilder}. * - * @throws NullPointerException if {@code pipelinePolicy} is {@code null}. + * @throws NullPointerException If {@code pipelinePolicy} is {@code null}. */ public TableServiceClientBuilder addPolicy(HttpPipelinePolicy pipelinePolicy) { if (pipelinePolicy == null) { diff --git a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/implementation/StorageConstants.java b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/implementation/StorageConstants.java index 9ea549548453..f8c4a7b321a7 100644 --- a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/implementation/StorageConstants.java +++ b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/implementation/StorageConstants.java @@ -3,6 +3,8 @@ package com.azure.data.tables.implementation; +import com.azure.data.tables.sas.TableSasProtocol; + import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.Locale; @@ -36,12 +38,12 @@ private StorageConstants() { public static final long TB = 1024L * GB; /** - * Represents the value for {@link SasProtocol#HTTPS_ONLY}. + * Represents the value for {@link TableSasProtocol#HTTPS_ONLY}. */ public static final String HTTPS = "https"; /** - * Represents the value for {@link SasProtocol#HTTPS_HTTP}. + * Represents the value for {@link TableSasProtocol#HTTPS_HTTP}. */ public static final String HTTPS_HTTP = "https,http"; @@ -327,6 +329,11 @@ private UrlConstants() { */ public static final String SAS_CONTENT_TYPE = "rsct"; + /** + * The SAS table name parameter. + */ + public static final String SAS_TABLE_NAME = "tn"; + /** * The SAS signed object id parameter for user delegation SAS. */ @@ -381,5 +388,25 @@ private UrlConstants() { * The SAS queue constant. */ public static final String SAS_QUEUE_CONSTANT = "q"; + + /** + * The SAS table start partition key. + */ + public static final String SAS_TABLE_START_PARTITION_KEY = "spk"; + + /** + * The SAS table start row key. + */ + public static final String SAS_TABLE_START_ROW_KEY = "srk"; + + /** + * The SAS table end partition key. + */ + public static final String SAS_TABLE_END_PARTITION_KEY = "epk"; + + /** + * The SAS table end row key. + */ + public static final String SAS_TABLE_END_ROW_KEY = "erk"; } } diff --git a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/implementation/TableAccountSasGenerator.java b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/implementation/TableAccountSasGenerator.java new file mode 100644 index 000000000000..941fbe7361f9 --- /dev/null +++ b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/implementation/TableAccountSasGenerator.java @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.data.tables.implementation; + +import com.azure.core.credential.AzureNamedKeyCredential; +import com.azure.core.util.CoreUtils; +import com.azure.core.util.logging.ClientLogger; +import com.azure.data.tables.sas.TableAccountSasPermission; +import com.azure.data.tables.sas.TableAccountSasSignatureValues; +import com.azure.data.tables.sas.TableSasIpRange; +import com.azure.data.tables.sas.TableSasProtocol; + +import java.time.OffsetDateTime; +import java.util.Objects; + +import static com.azure.data.tables.implementation.TableSasUtils.computeHmac256; +import static com.azure.data.tables.implementation.TableSasUtils.formatQueryParameterDate; +import static com.azure.data.tables.implementation.TableSasUtils.tryAppendQueryParameter; + +/** + * A class containing utility methods for generating SAS tokens for the Azure Storage accounts. + */ +public class TableAccountSasGenerator { + private final ClientLogger logger = new ClientLogger(TableAccountSasGenerator.class); + private final OffsetDateTime expiryTime; + private final OffsetDateTime startTime; + private final String permissions; + private final String resourceTypes; + private final String services; + private final String sas; + private final TableSasProtocol protocol; + private final TableSasIpRange sasIpRange; + private String version; + + /** + * Creates a new {@link TableAccountSasGenerator} which will generate an account-level SAS signed with an + * {@link AzureNamedKeyCredential}. + * + * @param sasValues The {@link TableAccountSasSignatureValues account signature values}. + * @param azureNamedKeyCredential An {@link AzureNamedKeyCredential} whose key will be used to sign the SAS. + */ + public TableAccountSasGenerator(TableAccountSasSignatureValues sasValues, + AzureNamedKeyCredential azureNamedKeyCredential) { + Objects.requireNonNull(sasValues, "'sasValues' cannot be null."); + Objects.requireNonNull(azureNamedKeyCredential, "'azureNamedKeyCredential' cannot be null."); + Objects.requireNonNull(sasValues.getServices(), "'services' in 'sasValues' cannot be null."); + Objects.requireNonNull(sasValues.getResourceTypes(), "'resourceTypes' in 'sasValues' cannot be null."); + Objects.requireNonNull(sasValues.getExpiryTime(), "'expiryTime' in 'sasValues' cannot be null."); + Objects.requireNonNull(sasValues.getPermissions(), "'permissions' in 'sasValues' cannot be null."); + + this.version = sasValues.getVersion(); + this.protocol = sasValues.getProtocol(); + this.startTime = sasValues.getStartTime(); + this.expiryTime = sasValues.getExpiryTime(); + this.permissions = sasValues.getPermissions(); + this.sasIpRange = sasValues.getSasIpRange(); + this.services = sasValues.getServices(); + this.resourceTypes = sasValues.getResourceTypes(); + + if (CoreUtils.isNullOrEmpty(version)) { + version = StorageConstants.HeaderConstants.TARGET_STORAGE_VERSION; + } + + String stringToSign = stringToSign(azureNamedKeyCredential); + + // Signature is generated on the un-url-encoded values. + String signature = computeHmac256(azureNamedKeyCredential.getAzureNamedKey().getKey(), stringToSign); + + this.sas = encode(signature); + } + + /** + * Get the SAS produced by this {@link TableAccountSasGenerator}. + * + * @return The SAS produced by this {@link TableAccountSasGenerator}. + */ + public String getSas() { + return sas; + } + + private String stringToSign(final AzureNamedKeyCredential azureNamedKeyCredential) { + return String.join("\n", + azureNamedKeyCredential.getAzureNamedKey().getName(), + TableAccountSasPermission.parse(this.permissions).toString(), // guarantees ordering + this.services, + resourceTypes, + this.startTime == null ? "" : StorageConstants.ISO_8601_UTC_DATE_FORMATTER.format(this.startTime), + StorageConstants.ISO_8601_UTC_DATE_FORMATTER.format(this.expiryTime), + this.sasIpRange == null ? "" : this.sasIpRange.toString(), + this.protocol == null ? "" : this.protocol.toString(), + this.version, + "" // Account SAS requires an additional newline character + ); + } + + private String encode(String signature) { + /* + We should be url-encoding each key and each value, but because we know all the keys and values will encode to + themselves, we cheat except for the signature value. + */ + StringBuilder sb = new StringBuilder(); + + tryAppendQueryParameter(sb, StorageConstants.UrlConstants.SAS_SERVICE_VERSION, this.version); + tryAppendQueryParameter(sb, StorageConstants.UrlConstants.SAS_SERVICES, this.services); + tryAppendQueryParameter(sb, StorageConstants.UrlConstants.SAS_RESOURCES_TYPES, this.resourceTypes); + tryAppendQueryParameter(sb, StorageConstants.UrlConstants.SAS_START_TIME, + formatQueryParameterDate(this.startTime)); + tryAppendQueryParameter(sb, StorageConstants.UrlConstants.SAS_EXPIRY_TIME, + formatQueryParameterDate(this.expiryTime)); + tryAppendQueryParameter(sb, StorageConstants.UrlConstants.SAS_SIGNED_PERMISSIONS, this.permissions); + tryAppendQueryParameter(sb, StorageConstants.UrlConstants.SAS_IP_RANGE, this.sasIpRange); + tryAppendQueryParameter(sb, StorageConstants.UrlConstants.SAS_PROTOCOL, this.protocol); + tryAppendQueryParameter(sb, StorageConstants.UrlConstants.SAS_SIGNATURE, signature); + + return sb.toString(); + } +} diff --git a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/implementation/TableSasGenerator.java b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/implementation/TableSasGenerator.java new file mode 100644 index 000000000000..343fa061fbc1 --- /dev/null +++ b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/implementation/TableSasGenerator.java @@ -0,0 +1,178 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.data.tables.implementation; + +import com.azure.core.credential.AzureNamedKeyCredential; +import com.azure.core.util.logging.ClientLogger; +import com.azure.data.tables.TableServiceVersion; +import com.azure.data.tables.sas.TableSasIpRange; +import com.azure.data.tables.sas.TableSasPermission; +import com.azure.data.tables.sas.TableSasProtocol; +import com.azure.data.tables.sas.TableSasSignatureValues; + +import java.time.OffsetDateTime; +import java.util.Locale; +import java.util.Objects; + +import static com.azure.data.tables.implementation.TableSasUtils.computeHmac256; +import static com.azure.data.tables.implementation.TableSasUtils.formatQueryParameterDate; +import static com.azure.data.tables.implementation.TableSasUtils.tryAppendQueryParameter; + +/** + * A class containing utility methods for generating SAS tokens for the Azure Data Tables service. + */ +public class TableSasGenerator { + private final ClientLogger logger = new ClientLogger(TableSasGenerator.class); + private final OffsetDateTime expiryTime; + private final OffsetDateTime startTime; + private final String endPartitionKey; + private final String endRowKey; + private final String identifier; + private final String sas; + private final String startPartitionKey; + private final String startRowKey; + private final String tableName; + private final TableSasProtocol protocol; + private final TableSasIpRange sasIpRange; + private String permissions; + private String version; + + /** + * Creates a new {@link TableSasGenerator} which will generate an table-level SAS signed with an + * {@link AzureNamedKeyCredential}. + * + * @param sasValues The {@link TableSasSignatureValues} to generate the SAS token with. + * @param tableName The table name. + * @param azureNamedKeyCredential An {@link AzureNamedKeyCredential} whose key will be used to sign the SAS. + */ + public TableSasGenerator(TableSasSignatureValues sasValues, String tableName, + AzureNamedKeyCredential azureNamedKeyCredential) { + Objects.requireNonNull(sasValues, "'sasValues' cannot be null."); + Objects.requireNonNull(azureNamedKeyCredential, "'azureNamedKeyCredential' cannot be null."); + + this.version = sasValues.getVersion(); + this.protocol = sasValues.getProtocol(); + this.startTime = sasValues.getStartTime(); + this.expiryTime = sasValues.getExpiryTime(); + this.permissions = sasValues.getPermissions(); + this.sasIpRange = sasValues.getSasIpRange(); + this.tableName = tableName; + this.identifier = sasValues.getIdentifier(); + this.startPartitionKey = sasValues.getStartPartitionKey(); + this.startRowKey = sasValues.getStartRowKey(); + this.endPartitionKey = sasValues.getEndPartitionKey(); + this.endRowKey = sasValues.getEndRowKey(); + + validateState(); + + // Signature is generated on the un-url-encoded values. + String canonicalName = getCanonicalName(azureNamedKeyCredential.getAzureNamedKey().getName()); + String stringToSign = stringToSign(canonicalName); + String signature = computeHmac256(azureNamedKeyCredential.getAzureNamedKey().getKey(), stringToSign); + + this.sas = encode(signature); + } + + /** + * Get the SAS produced by this {@link TableSasGenerator}. + * + * @return The SAS produced by this {@link TableSasGenerator}. + */ + public String getSas() { + return sas; + } + + private String encode(String signature) { + /* + * We should be url-encoding each key and each value, but because we know all the keys and values will encode to + * themselves, we cheat except for the signature value. + */ + StringBuilder sb = new StringBuilder(); + + tryAppendQueryParameter(sb, StorageConstants.UrlConstants.SAS_SERVICE_VERSION, this.version); + tryAppendQueryParameter(sb, StorageConstants.UrlConstants.SAS_START_TIME, + formatQueryParameterDate(this.startTime)); + tryAppendQueryParameter(sb, StorageConstants.UrlConstants.SAS_EXPIRY_TIME, + formatQueryParameterDate(this.expiryTime)); + tryAppendQueryParameter(sb, StorageConstants.UrlConstants.SAS_TABLE_NAME, tableName); + tryAppendQueryParameter(sb, StorageConstants.UrlConstants.SAS_SIGNED_PERMISSIONS, this.permissions); + tryAppendQueryParameter(sb, StorageConstants.UrlConstants.SAS_TABLE_START_PARTITION_KEY, startPartitionKey); + tryAppendQueryParameter(sb, StorageConstants.UrlConstants.SAS_TABLE_START_ROW_KEY, startRowKey); + tryAppendQueryParameter(sb, StorageConstants.UrlConstants.SAS_TABLE_END_PARTITION_KEY, endPartitionKey); + tryAppendQueryParameter(sb, StorageConstants.UrlConstants.SAS_TABLE_END_ROW_KEY, endRowKey); + tryAppendQueryParameter(sb, StorageConstants.UrlConstants.SAS_IP_RANGE, this.sasIpRange); + tryAppendQueryParameter(sb, StorageConstants.UrlConstants.SAS_PROTOCOL, this.protocol); + tryAppendQueryParameter(sb, StorageConstants.UrlConstants.SAS_SIGNED_IDENTIFIER, this.identifier); + tryAppendQueryParameter(sb, StorageConstants.UrlConstants.SAS_SIGNATURE, signature); + + return sb.toString(); + } + + /** + * Ensures that the builder's properties are in a consistent state. + * + * 1. If there is no version, use latest. + * 2. If there is no identifier set, ensure expiryTime and permissions are set. + * 4. Re-parse permissions depending on what the resource is. If it is an unrecognised resource, do nothing. + */ + private void validateState() { + if (version == null) { + version = TableServiceVersion.getLatest().getVersion(); + } + + if (identifier == null) { + if (expiryTime == null || permissions == null) { + throw logger.logExceptionAsError(new IllegalStateException("If identifier is not set, expiry time " + + "and permissions must be set")); + } + } + + if (permissions != null) { + if (tableName != null) { + permissions = TableSasPermission.parse(permissions).toString(); + } else { + // We won't re-parse the permissions if we don't know the type. + logger.info("Not re-parsing permissions. Resource type is not table."); + } + } + + if ((startPartitionKey != null && startRowKey == null) || (startPartitionKey == null && startRowKey != null)) { + throw logger.logExceptionAsError(new IllegalStateException("'startPartitionKey' and 'startRowKey' must " + + "either be both provided or both null. One cannot be provided without the other.")); + } + + if ((endPartitionKey != null && endRowKey == null) || (endPartitionKey == null && endRowKey != null)) { + throw logger.logExceptionAsError(new IllegalStateException("'endPartitionKey' and 'endRowKey' must either " + + "be both provided or both null. One cannot be provided without the other.")); + } + } + + /** + * Computes the canonical name for a table resource for SAS signing. + * + * @param account Account of the storage account. + * + * @return Canonical name as a string. + */ + private String getCanonicalName(String account) { + // Table: "/table/account/tablename" + return String.join("", new String[]{"/table/", account, "/", tableName}); + } + + private String stringToSign(String canonicalName) { + return String.join("\n", + this.permissions == null ? "" : this.permissions, + this.startTime == null ? "" : StorageConstants.ISO_8601_UTC_DATE_FORMATTER.format(this.startTime), + this.expiryTime == null ? "" : StorageConstants.ISO_8601_UTC_DATE_FORMATTER.format(this.expiryTime), + canonicalName.toLowerCase(Locale.ROOT), + this.identifier == null ? "" : this.identifier, + this.sasIpRange == null ? "" : this.sasIpRange.toString(), + this.protocol == null ? "" : protocol.toString(), + this.version == null ? "" : this.version, + this.startPartitionKey == null ? "" : this.startPartitionKey, + this.startRowKey == null ? "" : this.startRowKey, + this.endPartitionKey == null ? "" : this.endPartitionKey, + this.endRowKey == null ? "" : this.endRowKey + ); + } +} diff --git a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/implementation/TableSasUtils.java b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/implementation/TableSasUtils.java new file mode 100644 index 000000000000..751a1637d0aa --- /dev/null +++ b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/implementation/TableSasUtils.java @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.data.tables.implementation; + +import com.azure.core.credential.AzureNamedKeyCredential; +import com.azure.core.http.HttpPipeline; +import com.azure.data.tables.TableAzureNamedKeyCredentialPolicy; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.time.OffsetDateTime; +import java.util.Base64; + +/** + * This class provides helper methods used when generating SAS. + */ +public class TableSasUtils { + private static final String STRING_TO_SIGN_LOG_INFO_MESSAGE = "The string to sign computed by the SDK is: {}{}"; + private static final String STRING_TO_SIGN_LOG_WARNING_MESSAGE = "Please remember to disable '{}' before going " + + "to production as this string can potentially contain PII."; + + /** + * Shared helper method to append a SAS query parameter. + * + * @param sb The {@link StringBuilder} to append to. + * @param param The {@link String} parameter to append. + * @param value The value of the parameter to append. + */ + public static void tryAppendQueryParameter(StringBuilder sb, String param, Object value) { + if (value != null) { + if (sb.length() != 0) { + sb.append('&'); + } + + sb.append(TableUtils.urlEncode(param)).append('=').append(TableUtils.urlEncode(value.toString())); + } + } + + /** + * Formats date time SAS query parameters. + * + * @param dateTime The SAS date time. + * @return A String representing the SAS date time. + */ + public static String formatQueryParameterDate(OffsetDateTime dateTime) { + if (dateTime == null) { + return null; + } else { + return StorageConstants.ISO_8601_UTC_DATE_FORMATTER.format(dateTime); + } + } + + /** + * Extracts the {@link AzureNamedKeyCredential} from a {@link HttpPipeline} + * + * @param pipeline An {@link HttpPipeline} to extract an {@link AzureNamedKeyCredential} from. + * + * @return The extracted {@link AzureNamedKeyCredential}. + */ + public static AzureNamedKeyCredential extractNamedKeyCredential(HttpPipeline pipeline) { + for (int i = 0; i < pipeline.getPolicyCount(); i++) { + if (pipeline.getPolicy(i) instanceof TableAzureNamedKeyCredentialPolicy) { + TableAzureNamedKeyCredentialPolicy policy = (TableAzureNamedKeyCredentialPolicy) pipeline.getPolicy(i); + + return policy.getCredential(); + } + } + + return null; + } + + /** + * Computes a signature for the specified string using the HMAC-SHA256 algorithm. + * + * @param base64Key Base64 encoded key used to sign the string + * @param stringToSign UTF-8 encoded string to sign + * + * @return the HMAC-SHA256 encoded signature + * + * @throws RuntimeException If the HMAC-SHA256 algorithm isn't support, if the key isn't a valid Base64 encoded + * string, or the UTF-8 charset isn't supported. + */ + public static String computeHmac256(final String base64Key, final String stringToSign) { + try { + byte[] key = Base64.getDecoder().decode(base64Key); + Mac hmacSHA256 = Mac.getInstance("HmacSHA256"); + hmacSHA256.init(new SecretKeySpec(key, "HmacSHA256")); + byte[] utf8Bytes = stringToSign.getBytes(StandardCharsets.UTF_8); + + return Base64.getEncoder().encodeToString(hmacSHA256.doFinal(utf8Bytes)); + } catch (NoSuchAlgorithmException | InvalidKeyException ex) { + throw new RuntimeException(ex); + } + } +} diff --git a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/implementation/TableUtils.java b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/implementation/TableUtils.java index 4b062111ed5c..b5d7b550b5e1 100644 --- a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/implementation/TableUtils.java +++ b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/implementation/TableUtils.java @@ -17,15 +17,10 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; -import java.nio.charset.StandardCharsets; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; +import java.net.URLEncoder; import java.time.Duration; -import java.util.Base64; import java.util.Locale; import java.util.Map; import java.util.TreeMap; @@ -177,30 +172,6 @@ public static Mono> swallowExce return monoError(logger, httpResponseException); } - /** - * Computes a signature for the specified string using the HMAC-SHA256 algorithm. - * - * @param base64Key Base64 encoded key used to sign the string - * @param stringToSign UTF-8 encoded string to sign - * - * @return the HMAC-SHA256 encoded signature - * - * @throws RuntimeException If the HMAC-SHA256 algorithm isn't support, if the key isn't a valid Base64 encoded - * string, or the UTF-8 charset isn't supported. - */ - public static String computeHMac256(final String base64Key, final String stringToSign) { - try { - byte[] key = Base64.getDecoder().decode(base64Key); - Mac hmacSHA256 = Mac.getInstance("HmacSHA256"); - hmacSHA256.init(new SecretKeySpec(key, "HmacSHA256")); - byte[] utf8Bytes = stringToSign.getBytes(StandardCharsets.UTF_8); - - return Base64.getEncoder().encodeToString(hmacSHA256.doFinal(utf8Bytes)); - } catch (NoSuchAlgorithmException | InvalidKeyException ex) { - throw new RuntimeException(ex); - } - } - /** * Parses the query string into a key-value pair map that maintains key, query parameter key, order. The value is * stored as a parsed array (ex. key=[val1, val2, val3] instead of key=val1,val2,val3). @@ -294,4 +265,57 @@ private static String decode(final String stringToDecode) { throw new RuntimeException(ex); } } + + /** + * Performs a safe encoding of the specified string, taking care to insert %20 for each space character instead of + * inserting the {@code +} character. + * + * @param stringToEncode String value to encode + * @return the encoded string value + * @throws RuntimeException If the UTF-8 charset isn't supported + */ + public static String urlEncode(final String stringToEncode) { + if (stringToEncode == null) { + return null; + } + + if (stringToEncode.length() == 0) { + return ""; + } + + if (stringToEncode.contains(" ")) { + StringBuilder outBuilder = new StringBuilder(); + + int startDex = 0; + for (int m = 0; m < stringToEncode.length(); m++) { + if (stringToEncode.charAt(m) == ' ') { + if (m > startDex) { + outBuilder.append(encode(stringToEncode.substring(startDex, m))); + } + + outBuilder.append("%20"); + startDex = m + 1; + } + } + + if (startDex != stringToEncode.length()) { + outBuilder.append(encode(stringToEncode.substring(startDex))); + } + + return outBuilder.toString(); + } else { + return encode(stringToEncode); + } + } + + /* + * Helper method to reduce duplicate calls of URLEncoder.encode + */ + private static String encode(final String stringToEncode) { + try { + return URLEncoder.encode(stringToEncode, UTF8_CHARSET); + } catch (UnsupportedEncodingException ex) { + throw new RuntimeException(ex); + } + } } diff --git a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/models/TableItem.java b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/models/TableItem.java index 5fdcd8bd2388..d240add3d4ef 100644 --- a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/models/TableItem.java +++ b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/models/TableItem.java @@ -2,12 +2,14 @@ // Licensed under the MIT License. package com.azure.data.tables.models; +import com.azure.core.annotation.Immutable; import com.azure.data.tables.implementation.ModelHelper; import com.azure.data.tables.implementation.models.TableResponseProperties; /** * A table within a storage or CosmosDB table API account. */ +@Immutable public final class TableItem { private final String name; private final String odataType; diff --git a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/models/TableServiceError.java b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/models/TableServiceError.java index 8dd84cbd9e29..930d88497fe1 100644 --- a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/models/TableServiceError.java +++ b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/models/TableServiceError.java @@ -2,9 +2,12 @@ // Licensed under the MIT License. package com.azure.data.tables.models; +import com.azure.core.annotation.Immutable; + /** * A class that represents an error occurred in a Tables operation. */ +@Immutable public final class TableServiceError { /* * The service error code. diff --git a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/models/TableServiceException.java b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/models/TableServiceException.java index f8e7e6e3ae66..7b2c565ac207 100644 --- a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/models/TableServiceException.java +++ b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/models/TableServiceException.java @@ -2,12 +2,14 @@ // Licensed under the MIT License. package com.azure.data.tables.models; +import com.azure.core.annotation.Immutable; import com.azure.core.exception.HttpResponseException; import com.azure.core.http.HttpResponse; /** * Exception thrown for an invalid response with {@link TableServiceError} information. */ +@Immutable public class TableServiceException extends HttpResponseException { /** * Initializes a new instance of the {@link TableServiceException} class. diff --git a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/models/TableTransactionAction.java b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/models/TableTransactionAction.java index 05c7db6654aa..5e5f35823179 100644 --- a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/models/TableTransactionAction.java +++ b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/models/TableTransactionAction.java @@ -2,10 +2,13 @@ // Licensed under the MIT License. package com.azure.data.tables.models; +import com.azure.core.annotation.Immutable; + /** * Defines an action to be included as part of a transactional batch operation. */ -public class TableTransactionAction { +@Immutable +public final class TableTransactionAction { private final TableTransactionActionType actionType; private final TableEntity entity; private final boolean ifUnchanged; diff --git a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/models/TableTransactionFailedException.java b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/models/TableTransactionFailedException.java index 06d45cbd1d94..6d6e8f3d2df7 100644 --- a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/models/TableTransactionFailedException.java +++ b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/models/TableTransactionFailedException.java @@ -2,6 +2,7 @@ // Licensed under the MIT License. package com.azure.data.tables.models; +import com.azure.core.annotation.Immutable; import com.azure.core.http.HttpResponse; import com.azure.core.util.Context; import com.azure.data.tables.TableAsyncClient; @@ -13,7 +14,8 @@ /** * Exception thrown for an invalid response on a transactional operation with {@link TableServiceError} information. */ -public class TableTransactionFailedException extends TableServiceException { +@Immutable +public final class TableTransactionFailedException extends TableServiceException { private final Integer failedTransactionActionIndex; /** diff --git a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/models/TableTransactionResult.java b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/models/TableTransactionResult.java index ee5aeb2c1f6c..ed0225d0833e 100644 --- a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/models/TableTransactionResult.java +++ b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/models/TableTransactionResult.java @@ -2,6 +2,7 @@ // Licensed under the MIT License. package com.azure.data.tables.models; +import com.azure.core.annotation.Immutable; import com.azure.core.util.Context; import com.azure.data.tables.TableAsyncClient; import com.azure.data.tables.TableClient; @@ -16,7 +17,8 @@ * {@link TableClient#submitTransactionWithResponse(List, Duration, Context)}, * {@link TableAsyncClient#submitTransaction(List)} or {@link TableAsyncClient#submitTransactionWithResponse(List)}. */ -public class TableTransactionResult { +@Immutable +public final class TableTransactionResult { private final List transactionActionResponses; private final Map lookupMap; diff --git a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/sas/TableAccountSasPermission.java b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/sas/TableAccountSasPermission.java new file mode 100644 index 000000000000..5569ca24fd7c --- /dev/null +++ b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/sas/TableAccountSasPermission.java @@ -0,0 +1,405 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.data.tables.sas; + +import com.azure.core.annotation.Fluent; +import com.azure.data.tables.implementation.StorageConstants; + +import java.util.Locale; + +/** + * This is a helper class to construct a string representing the permissions granted by an Account SAS. 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 {@code toString()} and set as the permissions field on an + * {@link TableAccountSasSignatureValues} 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. + *

+ * + * @see TableAccountSasSignatureValues + * @see Create account SAS + */ +@Fluent +public final class TableAccountSasPermission { + private boolean readPermission; + private boolean addPermission; + private boolean createPermission; + private boolean writePermission; + private boolean deletePermission; + private boolean deleteVersionPermission; + private boolean listPermission; + private boolean updatePermission; + private boolean processMessagesPermission; + private boolean tagsPermission; + private boolean filterTagsPermission; + + /** + * Creates an {@link TableAccountSasPermission} from the specified permissions string. This method will throw an + * {@link IllegalArgumentException} if it encounters a character that does not correspond to a valid permission. + * + * @param permissionsString A {@code String} which represents the {@link TableAccountSasPermission account permissions}. + * + * @return An {@link TableAccountSasPermission} object generated from the given {@link String}. + * + * @throws IllegalArgumentException If {@code permString} contains a character other than r, w, d, x, l, a, c, u, p, + * t or f. + */ + public static TableAccountSasPermission parse(String permissionsString) { + TableAccountSasPermission permissions = new TableAccountSasPermission(); + + for (int i = 0; i < permissionsString.length(); i++) { + char c = permissionsString.charAt(i); + switch (c) { + case 'r': + permissions.readPermission = true; + break; + case 'w': + permissions.writePermission = true; + break; + case 'd': + permissions.deletePermission = true; + break; + case 'x': + permissions.deleteVersionPermission = true; + break; + case 'l': + permissions.listPermission = true; + break; + case 'a': + permissions.addPermission = true; + break; + case 'c': + permissions.createPermission = true; + break; + case 'u': + permissions.updatePermission = true; + break; + case 'p': + permissions.processMessagesPermission = true; + break; + case 't': + permissions.tagsPermission = true; + break; + case 'f': + permissions.filterTagsPermission = true; + break; + default: + throw new IllegalArgumentException( + String.format(Locale.ROOT, StorageConstants.ENUM_COULD_NOT_BE_PARSED_INVALID_VALUE, + "Permissions", permissionsString, c)); + } + } + + return permissions; + } + + /** + * Gets the read permission status. Valid for all signed resources types (Service, Container, and Object). Permits + * read permissions to the specified resource type. + * + * @return The read permission status. + */ + public boolean hasReadPermission() { + return readPermission; + } + + /** + * Sets the read permission status. Valid for all signed resources types (Service, Container, and Object). Permits + * read permissions to the specified resource type. + * + * @param hasReadPermission The permission status to set. + * + * @return The updated {@link TableAccountSasPermission} object. + */ + public TableAccountSasPermission setReadPermission(boolean hasReadPermission) { + this.readPermission = hasReadPermission; + + return this; + } + + /** + * Gets the add permission status. Valid for the following Object resource types only: queue messages, table + * entities, and append blobs. + * + * @return The add permission status. + */ + public boolean hasAddPermission() { + return addPermission; + } + + /** + * Sets the add permission status. Valid for the following Object resource types only: queue messages, table + * entities, and append blobs. + * + * @param hasAddPermission The permission status to set. + * + * @return The updated {@link TableAccountSasPermission} object. + */ + public TableAccountSasPermission setAddPermission(boolean hasAddPermission) { + this.addPermission = hasAddPermission; + + return this; + } + + /** + * Gets the create permission status. Valid for the following Object resource types only: blobs and files. Users can + * create new blobs or files, but may not overwrite existing blobs or files. + * + * @return The create permission status. + */ + public boolean hasCreatePermission() { + return createPermission; + } + + /** + * Sets the create permission status. Valid for the following Object resource types only: blobs and files. Users can + * create new blobs or files, but may not overwrite existing blobs or files. + * + * @param hasCreatePermission The permission status to set. + * + * @return The updated {@link TableAccountSasPermission} object. + */ + public TableAccountSasPermission setCreatePermission(boolean hasCreatePermission) { + this.createPermission = hasCreatePermission; + + return this; + } + + /** + * Gets the write permission status. Valid for all signed resources types (Service, Container, and Object). Permits + * write permissions to the specified resource type. + * + * @return The write permission status. + */ + public boolean hasWritePermission() { + return writePermission; + } + + /** + * Sets the write permission status. Valid for all signed resources types (Service, Container, and Object). Permits + * write permissions to the specified resource type. + * + * @param hasWritePermission The permission status to set. + * + * @return The updated {@link TableAccountSasPermission} object. + */ + public TableAccountSasPermission setWritePermission(boolean hasWritePermission) { + this.writePermission = hasWritePermission; + + return this; + } + + /** + * Gets the delete permission status. Valid for Container and Object resource types, except for queue messages. + * + * @return The delete permission status. + */ + public boolean hasDeletePermission() { + return deletePermission; + } + + /** + * Sets the delete permission status. Valid for Container and Object resource types, except for queue messages. + * + * @param hasDeletePermission The permission status to set. + * + * @return The updated {@link TableAccountSasPermission} object. + */ + public TableAccountSasPermission setDeletePermission(boolean hasDeletePermission) { + this.deletePermission = hasDeletePermission; + + return this; + } + + /** + * Gets the delete version permission status. Used to delete a blob version + * + * @return The delete version permission status. + */ + public boolean hasDeleteVersionPermission() { + return deleteVersionPermission; + } + + /** + * Sets the delete version permission status. Used to delete a blob version + * + * @param hasDeleteVersionPermission The permission status to set. + * + * @return The updated {@link TableAccountSasPermission} object. + */ + public TableAccountSasPermission setDeleteVersionPermission(boolean hasDeleteVersionPermission) { + this.deleteVersionPermission = hasDeleteVersionPermission; + + return this; + } + + /** + * Gets the list permission status. Valid for Service and Container resource types only. + * + * @return The list permission status. + */ + public boolean hasListPermission() { + return listPermission; + } + + /** + * Sets the list permission status. Valid for Service and Container resource types only. + * + * @param hasListPermission The permission status to set. + * + * @return The updated {@link TableAccountSasPermission} object. + */ + public TableAccountSasPermission setListPermission(boolean hasListPermission) { + this.listPermission = hasListPermission; + + return this; + } + + /** + * Gets the update permission status. Valid for the following Object resource types only: queue messages and table + * entities. + * + * @return The update permission status. + */ + public boolean hasUpdatePermission() { + return updatePermission; + } + + /** + * Sets the update permission status. Valid for the following Object resource types only: queue messages and table + * entities. + * + * @param hasUpdatePermission The permission status to set. + * + * @return The updated {@link TableAccountSasPermission} object. + */ + public TableAccountSasPermission setUpdatePermission(boolean hasUpdatePermission) { + this.updatePermission = hasUpdatePermission; + + return this; + } + + /** + * Gets the process messages permission. Valid for the following Object resource type only: queue messages. + * + * @return The process messages permission status. + */ + public boolean hasProcessMessages() { + return processMessagesPermission; + } + + /** + * Sets the process messages permission. Valid for the following Object resource type only: queue messages. + * + * @param hasProcessMessagesPermission The permission status to set. + * + * @return The updated {@link TableAccountSasPermission} object. + */ + public TableAccountSasPermission setProcessMessages(boolean hasProcessMessagesPermission) { + this.processMessagesPermission = hasProcessMessagesPermission; + + return this; + } + + /** + * @return The tags permission status. Used to read or write the tags on a blob. + */ + public boolean hasTagsPermission() { + return tagsPermission; + } + + /** + * Sets the tags permission status. + * + * @param tagsPermission The permission status to set. Used to read or write the tags on a blob. + * + * @return The updated {@link TableAccountSasPermission} object. + */ + public TableAccountSasPermission setTagsPermission(boolean tagsPermission) { + this.tagsPermission = tagsPermission; + + return this; + } + + + /** + * @return The filter tags permission status. Used to filter blobs by their tags. + */ + public boolean hasFilterTagsPermission() { + return filterTagsPermission; + } + + /** + * Sets the filter tags permission status. Used to filter blobs by their tags. + * + * @param filterTagsPermission The permission status to set. + * + * @return The updated {@link TableAccountSasPermission} object. + */ + public TableAccountSasPermission setFilterTagsPermission(boolean filterTagsPermission) { + this.filterTagsPermission = filterTagsPermission; + + return this; + } + + /** + * Converts the given permissions to a {@link String}. Using this method will guarantee the permissions are in an + * order accepted by the service. If all permissions are set to false, an empty string is returned from this method. + * + * @return A {@link String} which represents the {@link TableAccountSasPermission}. + */ + @Override + public String toString() { + // The order of the characters should be as specified here to ensure correctness: + // https://docs.microsoft.com/rest/api/storageservices/constructing-an-account-sas + final StringBuilder builder = new StringBuilder(); + + if (this.readPermission) { + builder.append('r'); + } + + if (this.writePermission) { + builder.append('w'); + } + + if (this.deletePermission) { + builder.append('d'); + } + + if (this.deleteVersionPermission) { + builder.append('x'); + } + + if (this.listPermission) { + builder.append('l'); + } + + if (this.addPermission) { + builder.append('a'); + } + + if (this.createPermission) { + builder.append('c'); + } + + if (this.updatePermission) { + builder.append('u'); + } + + if (this.processMessagesPermission) { + builder.append('p'); + } + + if (this.tagsPermission) { + builder.append('t'); + } + + if (this.filterTagsPermission) { + builder.append('f'); + } + + return builder.toString(); + } +} diff --git a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/sas/TableAccountSasResourceType.java b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/sas/TableAccountSasResourceType.java new file mode 100644 index 000000000000..a0d3d6bc2082 --- /dev/null +++ b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/sas/TableAccountSasResourceType.java @@ -0,0 +1,158 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.data.tables.sas; + +import com.azure.core.annotation.Fluent; +import com.azure.data.tables.implementation.StorageConstants; + +import java.util.Locale; + +/** + * This is a helper class to construct a string representing the resources accessible by an Account SAS. Setting a value + * to true means that any SAS which uses these permissions will grant access to that resource type. Once all the values + * are set, this should be serialized with {@code toString()} and set as the resources field on an + * {@link TableAccountSasSignatureValues} object. It is possible to construct the resources string without this class, + * but the order of the resources is particular and this class guarantees correctness. + */ +@Fluent +public final class TableAccountSasResourceType { + private boolean service; + private boolean container; + private boolean object; + + /** + * Creates an {@link TableAccountSasResourceType} from the specified resource types string. This method will throw an + * {@link IllegalArgumentException} if it encounters a character that does not correspond to a valid resource type. + * + * @param resourceTypesString A {@code String} which represents the + * {@link TableAccountSasResourceType account resource types}. + * + * @return A {@link TableAccountSasResourceType} generated from the given {@link String}. + * + * @throws IllegalArgumentException If {@code resourceTypesString} contains a character other than s, c, or o. + */ + public static TableAccountSasResourceType parse(String resourceTypesString) { + TableAccountSasResourceType resourceType = new TableAccountSasResourceType(); + + for (int i = 0; i < resourceTypesString.length(); i++) { + char c = resourceTypesString.charAt(i); + + switch (c) { + case 's': + resourceType.service = true; + break; + case 'c': + resourceType.container = true; + break; + case 'o': + resourceType.object = true; + break; + default: + throw new IllegalArgumentException( + String.format(Locale.ROOT, StorageConstants.ENUM_COULD_NOT_BE_PARSED_INVALID_VALUE, + "Resource Types", resourceTypesString, c)); + } + } + + return resourceType; + } + + /** + * Get the access status for service level APIs. + * + * @return The access status for service level APIs. + */ + public boolean isService() { + return service; + } + + /** + * Sets the access status for service level APIs. + * + * @param service The access status to set. + * + * @return The updated {@link TableAccountSasResourceType} object. + */ + public TableAccountSasResourceType setService(boolean service) { + this.service = service; + + return this; + } + + /** + * Gets the access status for container level APIs, this grants access to Blob Containers, Tables, Queues, and + * File Shares. + * + * @return The access status for container level APIs, this grants access to Blob Containers, Tables, Queues, and + * File Shares. + */ + public boolean isContainer() { + return container; + } + + /** + * Sets the access status for container level APIs, this grants access to Blob Containers, Tables, Queues, and File + * Shares. + * + * @param container The access status to set. + * + * @return The updated {@link TableAccountSasResourceType} object. + */ + public TableAccountSasResourceType setContainer(boolean container) { + this.container = container; + + return this; + } + + /** + * Get the access status for object level APIs, this grants access to Blobs, Table Entities, Queue Messages, Files. + * + * @return The access status for object level APIs, this grants access to Blobs, Table Entities, Queue Messages, + * Files. + */ + public boolean isObject() { + return object; + } + + /** + * Sets the access status for object level APIs, this grants access to Blobs, Table Entities, Queue Messages, + * Files. + * + * @param object The access status to set. + * + * @return The updated {@link TableAccountSasResourceType} object. + */ + public TableAccountSasResourceType setObject(boolean object) { + this.object = object; + + return this; + } + + /** + * Converts the given resource types to a {@link String}. Using this method will guarantee the resource types are in + * an order accepted by the service. If all resource types are set to false, an empty string is returned from this + * method. + * + * @return A {@code String} which represents the {@link TableAccountSasResourceType account resource types}. + */ + @Override + public String toString() { + // The order of the characters should be as specified here to ensure correctness: + // https://docs.microsoft.com/rest/api/storageservices/constructing-an-account-sas + StringBuilder builder = new StringBuilder(); + + if (this.service) { + builder.append('s'); + } + + if (this.container) { + builder.append('c'); + } + + if (this.object) { + builder.append('o'); + } + + return builder.toString(); + } +} diff --git a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/sas/TableAccountSasService.java b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/sas/TableAccountSasService.java new file mode 100644 index 000000000000..e6af4c1854b5 --- /dev/null +++ b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/sas/TableAccountSasService.java @@ -0,0 +1,169 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.data.tables.sas; + +import com.azure.core.annotation.Fluent; +import com.azure.data.tables.implementation.StorageConstants; + +import java.util.Locale; + +/** + * This is a helper class to construct a string representing the services accessible by an Account SAS. Setting a value + * to true means that any SAS which uses these permissions will grant access to that service. Once all the values are + * set, this should be serialized with {@code toString()} and set as the services field on an + * {@link TableAccountSasSignatureValues} object. It is possible to construct the services string without this class, but + * the order of the services is particular and this class guarantees correctness. + */ +@Fluent +public final class TableAccountSasService { + private boolean blob; + private boolean file; + private boolean queue; + private boolean table; + + /** + * Creates an {@link TableAccountSasService} from the specified services string. This method will throw an + * {@link IllegalArgumentException} if it encounters a character that does not correspond to a valid service. + * + * @param servicesString A {@link String} which represents the {@link TableAccountSasService account services}. + * + * @return A {@link TableAccountSasService} generated from the given {@link String}. + * + * @throws IllegalArgumentException If {@code servicesString} contains a character other than b, f, q, or t. + */ + public static TableAccountSasService parse(String servicesString) { + TableAccountSasService services = new TableAccountSasService(); + + for (int i = 0; i < servicesString.length(); i++) { + char c = servicesString.charAt(i); + switch (c) { + case 'b': + services.blob = true; + break; + case 'f': + services.file = true; + break; + case 'q': + services.queue = true; + break; + case 't': + services.table = true; + break; + default: + throw new IllegalArgumentException( + String.format(Locale.ROOT, StorageConstants.ENUM_COULD_NOT_BE_PARSED_INVALID_VALUE, "Services", + servicesString, c)); + } + } + + return services; + } + + /** + * @return The access status for blob resources. + */ + public boolean hasBlobAccess() { + return blob; + } + + /** + * Sets the access status for blob resources. + * + * @param blob The access status to set. + * + * @return The updated {@link TableAccountSasService} object. + */ + public TableAccountSasService setBlobAccess(boolean blob) { + this.blob = blob; + + return this; + } + + /** + * @return The access status for file resources. + */ + public boolean hasFileAccess() { + return file; + } + + /** + * Sets the access status for file resources. + * + * @param file The access status to set. + * + * @return The updated {@link TableAccountSasService} object. + */ + public TableAccountSasService setFileAccess(boolean file) { + this.file = file; + + return this; + } + + /** + * @return The access status for queue resources. + */ + public boolean hasQueueAccess() { + return queue; + } + + /** + * Sets the access status for queue resources. + * + * @param queue The access status to set. + * + * @return The updated {@link TableAccountSasService} object. + */ + public TableAccountSasService setQueueAccess(boolean queue) { + this.queue = queue; + + return this; + } + + /** + * @return The access status for table resources. + */ + public boolean hasTableAccess() { + return table; + } + + /** + * Sets the access status for table resources. + * + * @param table The access status to set. + * + * @return The updated {@link TableAccountSasService} object. + */ + public TableAccountSasService setTableAccess(boolean table) { + this.table = table; + + return this; + } + + /** + * Converts the given services to a {@link String}. Using this method will guarantee the services are in an order + * accepted by the service. If all services are set to false, an empty string is returned from this method. + * + * @return A {@link String} which represents the {@link TableAccountSasService account services}. + */ + @Override + public String toString() { + // The order of the characters should be as specified here to ensure correctness: + // https://docs.microsoft.com/rest/api/storageservices/constructing-an-account-sas + StringBuilder value = new StringBuilder(); + + if (this.blob) { + value.append('b'); + } + if (this.queue) { + value.append('q'); + } + if (this.table) { + value.append('t'); + } + if (this.file) { + value.append('f'); + } + + return value.toString(); + } +} diff --git a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/sas/TableAccountSasSignatureValues.java b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/sas/TableAccountSasSignatureValues.java new file mode 100644 index 000000000000..cef83db5cb9d --- /dev/null +++ b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/sas/TableAccountSasSignatureValues.java @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.data.tables.sas; + +import com.azure.core.annotation.Fluent; + +import java.time.OffsetDateTime; +import java.util.Objects; + +/** + * Used to initialize parameters for a Shared Access Signature (SAS) for an Azure Storage account. Once all the values + * here are set, use the {@code generateAccountSas()} method on the desired service client to obtain a + * representation of the SAS which can then be applied to a new client using the {@code sasToken(String)} method on + * the desired client builder. + * + * @see Storage SAS overview + * @see Create an account SAS + */ +@Fluent +public final class TableAccountSasSignatureValues { + private final OffsetDateTime expiryTime; + private final String permissions; + private final String services; + private final String resourceTypes; + private String version; + private TableSasProtocol protocol; + private OffsetDateTime startTime; + private TableSasIpRange sasIpRange; + + /** + * Initializes a new {@link TableAccountSasSignatureValues} object. + * + * @param expiryTime The time after which the SAS will no longer work. + * @param permissions {@link TableAccountSasPermission account permissions} allowed by the SAS. + * @param services {@link TableAccountSasService account services} targeted by the SAS. + * @param resourceTypes {@link TableAccountSasResourceType account resource types} targeted by the SAS. + */ + public TableAccountSasSignatureValues(OffsetDateTime expiryTime, TableAccountSasPermission permissions, + TableAccountSasService services, TableAccountSasResourceType resourceTypes) { + + Objects.requireNonNull(expiryTime, "'expiryTime' cannot be null"); + Objects.requireNonNull(services, "'services' cannot be null"); + Objects.requireNonNull(permissions, "'permissions' cannot be null"); + Objects.requireNonNull(resourceTypes, "'resourceTypes' cannot be null"); + + this.expiryTime = expiryTime; + this.services = services.toString(); + this.resourceTypes = resourceTypes.toString(); + this.permissions = permissions.toString(); + } + + /** + * Get The time after which the SAS will no longer work. + * + * @return The time after which the SAS will no longer work. + */ + public OffsetDateTime getExpiryTime() { + return expiryTime; + } + + /** + * Gets the operations the SAS user may perform. Please refer to {@link TableAccountSasPermission} to help determine + * which permissions are allowed. + * + * @return The operations the SAS user may perform. + */ + public String getPermissions() { + return permissions; + } + + /** + * Get the services accessible with this SAS. Please refer to {@link TableAccountSasService} to help determine which + * services are accessible. + * + * @return The services accessible with this SAS. + */ + public String getServices() { + return services; + } + + /** + * Get the resource types accessible with this SAS. Please refer to {@link TableAccountSasResourceType} to help determine + * the resource types that are accessible. + * + * @return The resource types accessible with this SAS. + */ + public String getResourceTypes() { + return resourceTypes; + } + + /** + * Get the service version that is targeted, if {@code null} or empty the latest service version targeted by the + * library will be used. + * + * @return The service version that is targeted. + */ + public String getVersion() { + return version; + } + + /** + * Sets the service version that is targeted. Leave this {@code null} or empty to target the version used by the + * library. + * + * @param version The target version to set. + * + * @return The updated {@link TableAccountSasSignatureValues} object. + */ + public TableAccountSasSignatureValues setVersion(String version) { + this.version = version; + + return this; + } + + /** + * Get the {@link TableSasProtocol} which determines the HTTP protocol that will be used. + * + * @return The {@link TableSasProtocol}. + */ + public TableSasProtocol getProtocol() { + return protocol; + } + + /** + * Sets the {@link TableSasProtocol} which determines the HTTP protocol that will be used. + * + * @param protocol The {@link TableSasProtocol} to set. + * + * @return The updated {@link TableAccountSasSignatureValues} object. + */ + public TableAccountSasSignatureValues setProtocol(TableSasProtocol protocol) { + this.protocol = protocol; + + return this; + } + + /** + * Get when the SAS will take effect. + * + * @return When the SAS will take effect. + */ + public OffsetDateTime getStartTime() { + return startTime; + } + + /** + * Sets when the SAS will take effect. + * + * @param startTime The start time to set. + * + * @return The updated {@link TableAccountSasSignatureValues} object. + */ + public TableAccountSasSignatureValues setStartTime(OffsetDateTime startTime) { + this.startTime = startTime; + + return this; + } + + /** + * Get the {@link TableSasIpRange} which determines the IP ranges that are allowed to use the SAS. + * + * @return The {@link TableSasIpRange}. + */ + public TableSasIpRange getSasIpRange() { + return sasIpRange; + } + + /** + * Sets the {@link TableSasIpRange} which determines the IP ranges that are allowed to use the SAS. + * + * @param sasIpRange The {@link TableSasIpRange allowed IP range} to set. + * + * @return The updated {@link TableAccountSasSignatureValues} object. + * + * @see Specifying + * IP Address or IP range + */ + public TableAccountSasSignatureValues setSasIpRange(TableSasIpRange sasIpRange) { + this.sasIpRange = sasIpRange; + + return this; + } +} diff --git a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/sas/TableSasIpRange.java b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/sas/TableSasIpRange.java new file mode 100644 index 000000000000..d60ee0ed7f26 --- /dev/null +++ b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/sas/TableSasIpRange.java @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.data.tables.sas; + +import com.azure.core.annotation.Fluent; + +/** + * This type specifies a continuous range of IP addresses. It is used to limit permissions on SAS tokens. Null may be + * set if it is not desired to confine the sas permissions to an IP range. + */ +@Fluent +public final class TableSasIpRange { + private String ipMin; + private String ipMax; + + /** + * Creates a {@link TableSasIpRange} from the specified string. + * + * @param rangeStr The {@link String} representation of the {@link TableSasIpRange}. + * @return The {@link TableSasIpRange} generated from the {@link String}. + */ + public static TableSasIpRange parse(String rangeStr) { + String[] addrs = rangeStr.split("-"); + + TableSasIpRange range = new TableSasIpRange().setIpMin(addrs[0]); + + if (addrs.length > 1) { + range.setIpMax(addrs[1]); + } + + return range; + } + + /** + * @return The minimum IP address of the range. + */ + public String getIpMin() { + return ipMin; + } + + /** + * Sets the minimum IP address of the range. + * + * @param ipMin IP address to set as the minimum. + * @return The updated {@link TableSasIpRange} object. + */ + public TableSasIpRange setIpMin(String ipMin) { + this.ipMin = ipMin; + + return this; + } + + /** + * @return The maximum IP address of the range. + */ + public String getIpMax() { + return ipMax; + } + + /** + * Sets the maximum IP address of the range. + * + * @param ipMax IP address to set as the maximum. + * @return The updated {@link TableSasIpRange} object. + */ + public TableSasIpRange setIpMax(String ipMax) { + this.ipMax = ipMax; + + return this; + } + + /** + * Output the single IP address or range of IP addresses formatted as a {@link String}. If {@code minIpRange} is set + * to {@code null}, an empty string is returned from this method. Otherwise, if {@code maxIpRange} is set + * to {@code null}, then this method returns the value of {@code minIpRange}. + * + * @return The single IP address or range of IP addresses formatted as a {@link String}. + */ + @Override + public String toString() { + if (this.ipMin == null) { + return ""; + } else if (this.ipMax == null) { + return this.ipMin; + } else { + return this.ipMin + "-" + this.ipMax; + } + } +} diff --git a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/sas/TableSasPermission.java b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/sas/TableSasPermission.java new file mode 100644 index 000000000000..0b6f67210258 --- /dev/null +++ b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/sas/TableSasPermission.java @@ -0,0 +1,205 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.data.tables.sas; + +import com.azure.core.annotation.Fluent; +import com.azure.data.tables.implementation.StorageConstants; + +import java.util.Locale; + +/** + * Constructs a string representing the permissions granted by an Azure Service SAS to a table. 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 {@link #toString() toString} and set as the permissions field on + * {@link TableSasSignatureValues#setPermissions(TableSasPermission)} TableSasSignatureValues}. + * + *

+ * It is possible to construct the permissions string without this class, but the order of the permissions is + * particular and this class guarantees correctness. + *

+ * + * @see + * Permissions for a table + * @see TableSasSignatureValues + */ +@Fluent +public final class TableSasPermission { + private boolean readPermission; + private boolean addPermission; + private boolean updatePermission; + private boolean deletePermission; + + /** + * Creates a {@link TableSasPermission} from the specified permissions string. This method will throw an + * {@link IllegalArgumentException} if it encounters a character that does not correspond to a valid permission. + * + * @param permString A {@link String} which represents the {@link TableSasPermission}. + * + * @return A {@link TableSasPermission} generated from the given {@link String}. + * + * @throws IllegalArgumentException If {@code permString} contains a character other than r, a, u, or d. + */ + public static TableSasPermission parse(String permString) { + TableSasPermission permissions = new TableSasPermission(); + + for (int i = 0; i < permString.length(); i++) { + char c = permString.charAt(i); + switch (c) { + case 'r': + permissions.readPermission = true; + + break; + case 'a': + permissions.addPermission = true; + + break; + case 'u': + permissions.updatePermission = true; + + break; + case 'd': + permissions.deletePermission = true; + + break; + default: + throw new IllegalArgumentException( + String.format(Locale.ROOT, StorageConstants.ENUM_COULD_NOT_BE_PARSED_INVALID_VALUE, + "Permissions", permString, c)); + } + } + + return permissions; + } + + /** + * Gets the read permissions status. + * + * @return {@code true} if the SAS has permission to get entities and query entities. {@code false}, otherwise. + */ + public boolean hasReadPermission() { + return readPermission; + } + + /** + * Sets the read permission status. + * + * @param hasReadPermission {@code true} if the SAS has permission to get entities and query entities. + * {@code false}, otherwise + * + * @return The updated TableSasPermission object. + */ + public TableSasPermission setReadPermission(boolean hasReadPermission) { + this.readPermission = hasReadPermission; + + return this; + } + + /** + * Gets the add permission status. + * + * @return {@code true} if the SAS has permission to add entities to the table. {@code false}, otherwise. + */ + public boolean hasAddPermission() { + return addPermission; + } + + /** + * Sets the add permission status. + * + * @param hasAddPermission {@code true} if the SAS has permission to add entities to the table. {@code false}, + * otherwise. + * + *

+ * Note: The {@code add} and {@code update} permissions are required for upsert operations. + *

+ * + * @return The updated {@link TableSasPermission} object. + */ + public TableSasPermission setAddPermission(boolean hasAddPermission) { + this.addPermission = hasAddPermission; + + return this; + } + + /** + * Gets the update permission status. + * + * @return {@code true} if the SAS has permission to update entities in the table. {@code false}, otherwise. + */ + public boolean hasUpdatePermission() { + return updatePermission; + } + + /** + * Sets the update permission status. + * + *

+ * Note: The {@code add} and {@code update} permissions are required for upsert operations. + *

+ * + * @param hasUpdatePermission {@code true} if the SAS has permission to update entities in the table. {@code false}, + * otherwise. + * + * @return The updated {@link TableSasPermission} object. + */ + public TableSasPermission setUpdatePermission(boolean hasUpdatePermission) { + this.updatePermission = hasUpdatePermission; + + return this; + } + + /** + * Gets the delete permission status. + * + * @return {@code true} if the SAS has permission to delete entities from the table. {@code false}, otherwise. + */ + public boolean hasDeletePermission() { + return deletePermission; + } + + /** + * Sets the process permission status. + * + * @param hasDeletePermission {@code true} if the SAS has permission to delete entities from the table. + * {@code false}, otherwise. + * + * @return The updated {@link TableSasPermission} object. + */ + public TableSasPermission setDeletePermission(boolean hasDeletePermission) { + this.deletePermission = hasDeletePermission; + + return this; + } + + /** + * Converts the given permissions to a {@link String}. Using this method will guarantee the permissions are in an + * order accepted by the service. If all permissions are set to false, an empty string is returned from this method. + * + * @return A {@link String} which represents the {@link TableSasPermission}. + */ + @Override + public String toString() { + // The order of the characters should be as specified here to ensure correctness: + // https://docs.microsoft.com/rest/api/storageservices/constructing-a-service-sas + + final StringBuilder builder = new StringBuilder(); + + if (this.readPermission) { + builder.append('r'); + } + + if (this.addPermission) { + builder.append('a'); + } + + if (this.updatePermission) { + builder.append('u'); + } + + if (this.deletePermission) { + builder.append('d'); + } + + return builder.toString(); + } +} diff --git a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/implementation/SasProtocol.java b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/sas/TableSasProtocol.java similarity index 74% rename from sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/implementation/SasProtocol.java rename to sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/sas/TableSasProtocol.java index 36463e7180b8..ca3b747a5d58 100644 --- a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/implementation/SasProtocol.java +++ b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/sas/TableSasProtocol.java @@ -1,13 +1,16 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.data.tables.implementation; + +package com.azure.data.tables.sas; + +import com.azure.data.tables.implementation.StorageConstants; import java.util.Locale; /** * Specifies the set of possible permissions for Shared Access Signature protocol. */ -public enum SasProtocol { +public enum TableSasProtocol { /** * Permission to use SAS only through https granted. */ @@ -20,23 +23,23 @@ public enum SasProtocol { private final String protocols; - SasProtocol(String p) { + TableSasProtocol(String p) { this.protocols = p; } /** - * Parses a {@code String} into a {@link SasProtocol} value if possible. + * Parses a {@code String} into a {@link TableSasProtocol} value if possible. * * @param str The value to try to parse. * * @return A {@code SasProtocol} value that represents the string if possible. * @throws IllegalArgumentException If {@code str} doesn't equal "https" or "https,http". */ - public static SasProtocol parse(String str) { + public static TableSasProtocol parse(String str) { if (str.equals(StorageConstants.HTTPS)) { - return SasProtocol.HTTPS_ONLY; + return TableSasProtocol.HTTPS_ONLY; } else if (str.equals(StorageConstants.HTTPS_HTTP)) { - return SasProtocol.HTTPS_HTTP; + return TableSasProtocol.HTTPS_HTTP; } throw new IllegalArgumentException(String.format(Locale.ROOT, diff --git a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/sas/TableSasSignatureValues.java b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/sas/TableSasSignatureValues.java new file mode 100644 index 000000000000..4d736c67a080 --- /dev/null +++ b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/sas/TableSasSignatureValues.java @@ -0,0 +1,315 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.data.tables.sas; + +import com.azure.core.annotation.Fluent; + +import java.time.OffsetDateTime; +import java.util.Objects; + +/** + * Used to initialize parameters for a Shared Access Signature (SAS) for the Azure Table Storage service. Once all the + * values here are set, use the {@code generateSas()} method on the desired Table client to obtain a representation + * of the SAS which can then be applied to a new client using the {@code sasToken(String)} method on the desired + * client builder. + * + * @see Storage SAS overview + * @see Constructing a Service SAS + */ +@Fluent +public final class TableSasSignatureValues { + private String version; + private TableSasProtocol protocol; + private OffsetDateTime startTime; + private OffsetDateTime expiryTime; + private String permissions; + private TableSasIpRange sasIpRange; + private String identifier; + private String startPartitionKey; + private String startRowKey; + private String endPartitionKey; + private String endRowKey; + + /** + * Creates an object with the specified expiry time and permissions. + * + * @param expiryTime The time after which the SAS will no longer work. + * @param permissions {@link TableSasPermission table permissions} allowed by the SAS. + */ + public TableSasSignatureValues(OffsetDateTime expiryTime, TableSasPermission permissions) { + Objects.requireNonNull(expiryTime, "'expiryTime' cannot be null"); + Objects.requireNonNull(permissions, "'permissions' cannot be null"); + + this.expiryTime = expiryTime; + this.permissions = permissions.toString(); + } + + /** + * Creates an object with the specified identifier. + * + * @param identifier Name of the access policy. + */ + public TableSasSignatureValues(String identifier) { + Objects.requireNonNull(identifier, "'identifier' cannot be null"); + + this.identifier = identifier; + } + + /** + * @return The version of the service this SAS will target. If not specified, it will default to the version + * targeted by the library. + */ + public String getVersion() { + return version; + } + + /** + * Sets the version of the service this SAS will target. If not specified, it will default to the version targeted + * by the library. + * + * @param version Version to target + * + * @return The updated {@link TableSasSignatureValues} object. + */ + public TableSasSignatureValues setVersion(String version) { + this.version = version; + + return this; + } + + /** + * @return The {@link TableSasProtocol} which determines the protocols allowed by the SAS. + */ + public TableSasProtocol getProtocol() { + return protocol; + } + + /** + * Sets the {@link TableSasProtocol} which determines the protocols allowed by the SAS. + * + * @param protocol Protocol for the SAS + * + * @return The updated {@link TableSasSignatureValues} object. + */ + public TableSasSignatureValues setProtocol(TableSasProtocol protocol) { + this.protocol = protocol; + + return this; + } + + /** + * @return When the SAS will take effect. + */ + public OffsetDateTime getStartTime() { + return startTime; + } + + /** + * Sets when the SAS will take effect. + * + * @param startTime When the SAS takes effect + * + * @return The updated {@link TableSasSignatureValues} object. + */ + public TableSasSignatureValues setStartTime(OffsetDateTime startTime) { + this.startTime = startTime; + + return this; + } + + /** + * @return The time after which the SAS will no longer work. + */ + public OffsetDateTime getExpiryTime() { + return expiryTime; + } + + /** + * Sets the time after which the SAS will no longer work. + * + * @param expiryTime When the SAS will no longer work + * + * @return The updated {@link TableSasSignatureValues} object. + */ + public TableSasSignatureValues setExpiryTime(OffsetDateTime expiryTime) { + this.expiryTime = expiryTime; + + return this; + } + + /** + * @return The permissions string allowed by the SAS. Please refer to {@link TableSasPermission} for help + * determining the permissions allowed. + */ + public String getPermissions() { + return permissions; + } + + /** + * Sets the permissions string allowed by the SAS. Please refer to {@link TableSasPermission} for help constructing + * the permissions string. + * + * @param permissions Permissions for the SAS + * + * @return The updated {@link TableSasSignatureValues} object. + * + * @throws NullPointerException if {@code permissions} is null. + */ + public TableSasSignatureValues setPermissions(TableSasPermission permissions) { + Objects.requireNonNull(permissions, "'permissions' cannot be null"); + + this.permissions = permissions.toString(); + + return this; + } + + /** + * @return The {@link TableSasIpRange} which determines the IP ranges that are allowed to use the SAS. + */ + public TableSasIpRange getSasIpRange() { + return sasIpRange; + } + + /** + * Sets the {@link TableSasIpRange} which determines the IP ranges that are allowed to use the SAS. + * + * @param sasIpRange Allowed IP range to set + * + * @return The updated {@link TableSasSignatureValues} object. + * + * @see Specifying + * IP Address or IP range + */ + public TableSasSignatureValues setSasIpRange(TableSasIpRange sasIpRange) { + this.sasIpRange = sasIpRange; + + return this; + } + + /** + * @return The name of the access policy on the table this SAS references if any. Please see + * here + * for more information. + */ + public String getIdentifier() { + return identifier; + } + + /** + * Sets the name of the access policy on the table this SAS references if any. Please see + * here + * for more information. + * + * @param identifier Name of the access policy + * + * @return The updated {@link TableSasSignatureValues} object. + */ + public TableSasSignatureValues setIdentifier(String identifier) { + this.identifier = identifier; + + return this; + } + + /** + * Get the minimum partition key accessible with this shared access signature. Key values are inclusive. If omitted, + * there is no lower bound on the table entities that can be accessed. If provided, it must accompany a start row + * key that can be set via {@code setStartRowKey()}. + * + * @return The start partition key. + */ + public String getStartPartitionKey() { + return this.startPartitionKey; + } + + /** + * Set the minimum partition key accessible with this shared access signature. Key values are inclusive. If omitted, + * there is no lower bound on the table entities that can be accessed. If provided, it must accompany a start row + * key that can be set via {@code setStartRowKey()}. + * + * @param startPartitionKey The start partition key to set. + * + * @return The updated {@link TableSasSignatureValues} object. + */ + public TableSasSignatureValues setStartPartitionKey(String startPartitionKey) { + this.startPartitionKey = startPartitionKey; + return this; + } + + /** + * Get the minimum row key accessible with this shared access signature. Key values are inclusive. If omitted, there + * is no lower bound on the table entities that can be accessed. If provided, it must accompany a start row key + * that can be set via {@code setStartPartitionKey()}. + * + * @return The start row key. + */ + public String getStartRowKey() { + return this.startRowKey; + } + + /** + * Set the minimum row key accessible with this shared access signature. Key values are inclusive. If omitted, there + * is no lower bound on the table entities that can be accessed. If provided, it must accompany a start row key + * that can be set via {@code setStartPartitionKey()}. + * + * @param startRowKey The start row key to set. + * + * @return The updated {@link TableSasSignatureValues} object. + */ + public TableSasSignatureValues setStartRowKey(String startRowKey) { + this.startRowKey = startRowKey; + + return this; + } + + /** + * Get the maximum partition key accessible with this shared access signature. Key values are inclusive. If omitted, + * there is no upper bound on the table entities that can be accessed. If provided, it must accompany an ending row + * key that can be set via {@code setEndRowKey()}. + * + * @return The end partition key. + */ + public String getEndPartitionKey() { + return this.endPartitionKey; + } + + /** + * Set the maximum partition key accessible with this shared access signature. Key values are inclusive. If omitted, + * there is no upper bound on the table entities that can be accessed. If provided, it must accompany an ending row + * key that can be set via {@code setEndRowKey()}. + * + * @param endPartitionKey The end partition key to set. + * + * @return The updated {@link TableSasSignatureValues} object. + */ + public TableSasSignatureValues setEndPartitionKey(String endPartitionKey) { + this.endPartitionKey = endPartitionKey; + + return this; + } + + /** + * Get the maximum row key accessible with this shared access signature. Key values are inclusive. If omitted, there + * is no upper bound on the table entities that can be accessed. If provided, it must accompany an ending row key + * that can be set via {@code setEndPartitionKey()}. + * + * @return The end row key. + */ + public String getEndRowKey() { + return this.endRowKey; + } + + /** + * Set the maximum row key accessible with this shared access signature. Key values are inclusive. If omitted, there + * is no upper bound on the table entities that can be accessed. If provided, it must accompany an ending row key + * that can be set via {@code setEndPartitionKey()}. + * + * @param endRowKey The end row key to set. + * + * @return The updated {@link TableSasSignatureValues} object. + */ + public TableSasSignatureValues setEndRowKey(String endRowKey) { + this.endRowKey = endRowKey; + + return this; + } +} diff --git a/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/sas/package-info.java b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/sas/package-info.java new file mode 100644 index 000000000000..bc32f4bf2928 --- /dev/null +++ b/sdk/tables/azure-data-tables/src/main/java/com/azure/data/tables/sas/package-info.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Package containing SAS (shared access signature) classes used by Azure Data Tables. + */ +package com.azure.data.tables.sas; diff --git a/sdk/tables/azure-data-tables/src/main/java/module-info.java b/sdk/tables/azure-data-tables/src/main/java/module-info.java index 1b8c7dec7b82..a4df97c48c88 100644 --- a/sdk/tables/azure-data-tables/src/main/java/module-info.java +++ b/sdk/tables/azure-data-tables/src/main/java/module-info.java @@ -7,13 +7,12 @@ // public API surface area exports com.azure.data.tables; exports com.azure.data.tables.models; - - exports com.azure.data.tables.implementation to com.azure.core; - exports com.azure.data.tables.implementation.models to com.azure.core; + exports com.azure.data.tables.sas; // exporting some packages specifically for Jackson opens com.azure.data.tables to com.fasterxml.jackson.databind, com.azure.core; opens com.azure.data.tables.implementation to com.fasterxml.jackson.databind, com.azure.core; opens com.azure.data.tables.implementation.models to com.fasterxml.jackson.databind, com.azure.core; opens com.azure.data.tables.models to com.fasterxml.jackson.databind, com.azure.core; + opens com.azure.data.tables.sas to com.fasterxml.jackson.databind, com.azure.core; } diff --git a/sdk/tables/azure-data-tables/src/test/java/com/azure/data/tables/SasModelsTest.java b/sdk/tables/azure-data-tables/src/test/java/com/azure/data/tables/SasModelsTest.java new file mode 100644 index 000000000000..8be6348d27e2 --- /dev/null +++ b/sdk/tables/azure-data-tables/src/test/java/com/azure/data/tables/SasModelsTest.java @@ -0,0 +1,329 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.data.tables; + +import com.azure.data.tables.sas.TableAccountSasPermission; +import com.azure.data.tables.sas.TableAccountSasResourceType; +import com.azure.data.tables.sas.TableAccountSasService; +import com.azure.data.tables.sas.TableAccountSasSignatureValues; +import com.azure.data.tables.sas.TableSasIpRange; +import com.azure.data.tables.sas.TableSasPermission; +import com.azure.data.tables.sas.TableSasProtocol; +import com.azure.data.tables.sas.TableSasSignatureValues; +import org.junit.jupiter.api.Test; + +import java.time.OffsetDateTime; +import java.time.ZoneOffset; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class SasModelsTest { + @Test + public void createTableAccountSasSignatureValuesWithMinimumValues() { + OffsetDateTime expiryTime = OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); + TableAccountSasPermission permissions = TableAccountSasPermission.parse("l"); + TableAccountSasService services = TableAccountSasService.parse("t"); + TableAccountSasResourceType resourceTypes = TableAccountSasResourceType.parse("o"); + + OffsetDateTime startTime = OffsetDateTime.of(2015, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); + TableSasIpRange ipRange = TableSasIpRange.parse("a-b"); + TableSasProtocol protocol = TableSasProtocol.HTTPS_ONLY; + + TableAccountSasSignatureValues sasSignatureValues = + new TableAccountSasSignatureValues(expiryTime, permissions, services, resourceTypes) + .setStartTime(startTime) + .setSasIpRange(ipRange) + .setProtocol(protocol); + + assertEquals(expiryTime, sasSignatureValues.getExpiryTime()); + assertEquals(permissions.toString(), sasSignatureValues.getPermissions()); + assertEquals(services.toString(), sasSignatureValues.getServices()); + assertEquals(resourceTypes.toString(), sasSignatureValues.getResourceTypes()); + assertEquals(startTime, sasSignatureValues.getStartTime()); + assertEquals(ipRange, sasSignatureValues.getSasIpRange()); + assertEquals(protocol, sasSignatureValues.getProtocol()); + } + + @Test + public void createTableAccountSasSignatureValuesWithNullRequiredValue() { + OffsetDateTime expiryTime = OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); + TableAccountSasPermission permissions = TableAccountSasPermission.parse("l"); // List permission + TableAccountSasService services = TableAccountSasService.parse("t"); // Tables service + TableAccountSasResourceType resourceTypes = TableAccountSasResourceType.parse("o"); // Object resource + + assertThrows(NullPointerException.class, + () -> new TableAccountSasSignatureValues(null, permissions, services, resourceTypes)); + assertThrows(NullPointerException.class, + () -> new TableAccountSasSignatureValues(expiryTime, null, services, resourceTypes)); + assertThrows(NullPointerException.class, + () -> new TableAccountSasSignatureValues(expiryTime, permissions, null, resourceTypes)); + assertThrows(NullPointerException.class, + () -> new TableAccountSasSignatureValues(expiryTime, permissions, services, null)); + } + + @Test + public void tableAccountSasPermissionToString() { + assertEquals("rwdxlacuptf", new TableAccountSasPermission() + .setReadPermission(true) + .setWritePermission(true) + .setDeletePermission(true) + .setListPermission(true) + .setAddPermission(true) + .setCreatePermission(true) + .setUpdatePermission(true) + .setProcessMessages(true) + .setDeleteVersionPermission(true) + .setTagsPermission(true) + .setFilterTagsPermission(true) + .toString()); + assertEquals("r", new TableAccountSasPermission().setReadPermission(true).toString()); + assertEquals("w", new TableAccountSasPermission().setWritePermission(true).toString()); + assertEquals("d", new TableAccountSasPermission().setDeletePermission(true).toString()); + assertEquals("l", new TableAccountSasPermission().setListPermission(true).toString()); + assertEquals("a", new TableAccountSasPermission().setAddPermission(true).toString()); + assertEquals("c", new TableAccountSasPermission().setCreatePermission(true).toString()); + assertEquals("u", new TableAccountSasPermission().setUpdatePermission(true).toString()); + assertEquals("p", new TableAccountSasPermission().setProcessMessages(true).toString()); + assertEquals("x", new TableAccountSasPermission().setDeleteVersionPermission(true).toString()); + assertEquals("t", new TableAccountSasPermission().setTagsPermission(true).toString()); + assertEquals("f", new TableAccountSasPermission().setFilterTagsPermission(true).toString()); + } + + @Test + public void tableAccountSasPermissionParse() { + TableAccountSasPermission tableAccountSasPermission = TableAccountSasPermission.parse("rwdxlacuptf"); + + assertTrue(tableAccountSasPermission.hasReadPermission()); + assertTrue(tableAccountSasPermission.hasWritePermission()); + assertTrue(tableAccountSasPermission.hasDeletePermission()); + assertTrue(tableAccountSasPermission.hasListPermission()); + assertTrue(tableAccountSasPermission.hasAddPermission()); + assertTrue(tableAccountSasPermission.hasCreatePermission()); + assertTrue(tableAccountSasPermission.hasUpdatePermission()); + assertTrue(tableAccountSasPermission.hasProcessMessages()); + assertTrue(tableAccountSasPermission.hasDeleteVersionPermission()); + assertTrue(tableAccountSasPermission.hasTagsPermission()); + assertTrue(tableAccountSasPermission.hasFilterTagsPermission()); + + tableAccountSasPermission = TableAccountSasPermission.parse("lwfrutpcaxd"); + + assertTrue(tableAccountSasPermission.hasReadPermission()); + assertTrue(tableAccountSasPermission.hasWritePermission()); + assertTrue(tableAccountSasPermission.hasDeletePermission()); + assertTrue(tableAccountSasPermission.hasListPermission()); + assertTrue(tableAccountSasPermission.hasAddPermission()); + assertTrue(tableAccountSasPermission.hasCreatePermission()); + assertTrue(tableAccountSasPermission.hasUpdatePermission()); + assertTrue(tableAccountSasPermission.hasProcessMessages()); + assertTrue(tableAccountSasPermission.hasDeleteVersionPermission()); + assertTrue(tableAccountSasPermission.hasTagsPermission()); + assertTrue(tableAccountSasPermission.hasFilterTagsPermission()); + + assertTrue(TableAccountSasPermission.parse("r").hasReadPermission()); + assertTrue(TableAccountSasPermission.parse("w").hasWritePermission()); + assertTrue(TableAccountSasPermission.parse("d").hasDeletePermission()); + assertTrue(TableAccountSasPermission.parse("l").hasListPermission()); + assertTrue(TableAccountSasPermission.parse("a").hasAddPermission()); + assertTrue(TableAccountSasPermission.parse("c").hasCreatePermission()); + assertTrue(TableAccountSasPermission.parse("u").hasUpdatePermission()); + assertTrue(TableAccountSasPermission.parse("p").hasProcessMessages()); + assertTrue(TableAccountSasPermission.parse("x").hasDeleteVersionPermission()); + assertTrue(TableAccountSasPermission.parse("t").hasTagsPermission()); + assertTrue(TableAccountSasPermission.parse("f").hasFilterTagsPermission()); + } + + @Test + public void tableAccountSasPermissionParseIllegalString() { + assertThrows(IllegalArgumentException.class, () -> TableAccountSasPermission.parse("rwaq")); + } + + @Test + public void tableAccountSasResourceTypeToString() { + assertEquals("sco", new TableAccountSasResourceType() + .setService(true) + .setContainer(true) + .setObject(true) + .toString()); + + assertEquals("s", new TableAccountSasResourceType().setService(true).toString()); + assertEquals("c", new TableAccountSasResourceType().setContainer(true).toString()); + assertEquals("o", new TableAccountSasResourceType().setObject(true).toString()); + } + + @Test + public void tableAccountSasResourceTypeParse() { + TableAccountSasResourceType tableAccountSasResourceType = TableAccountSasResourceType.parse("sco"); + + assertTrue(tableAccountSasResourceType.isService()); + assertTrue(tableAccountSasResourceType.isContainer()); + assertTrue(tableAccountSasResourceType.isObject()); + + assertTrue(TableAccountSasResourceType.parse("s").isService()); + assertTrue(TableAccountSasResourceType.parse("c").isContainer()); + assertTrue(TableAccountSasResourceType.parse("o").isObject()); + } + + @Test + public void tableAccountSasResourceTypeParseIllegalString() { + assertThrows(IllegalArgumentException.class, () -> TableAccountSasResourceType.parse("scq")); + } + + @Test + public void tableAccountSasServiceToString() { + assertEquals("bqtf", new TableAccountSasService() + .setBlobAccess(true) + .setQueueAccess(true) + .setTableAccess(true) + .setFileAccess(true) + .toString()); + + assertEquals("b", new TableAccountSasService().setBlobAccess(true).toString()); + assertEquals("q", new TableAccountSasService().setQueueAccess(true).toString()); + assertEquals("t", new TableAccountSasService().setTableAccess(true).toString()); + assertEquals("f", new TableAccountSasService().setFileAccess(true).toString()); + } + + @Test + public void tableAccountSasServiceParse() { + TableAccountSasService tableAccountSasService = TableAccountSasService.parse("bqtf"); + + assertTrue(tableAccountSasService.hasBlobAccess()); + assertTrue(tableAccountSasService.hasQueueAccess()); + assertTrue(tableAccountSasService.hasTableAccess()); + assertTrue(tableAccountSasService.hasFileAccess()); + + assertTrue(TableAccountSasService.parse("b").hasBlobAccess()); + assertTrue(TableAccountSasService.parse("q").hasQueueAccess()); + assertTrue(TableAccountSasService.parse("t").hasTableAccess()); + assertTrue(TableAccountSasService.parse("f").hasFileAccess()); + } + + @Test + public void tableAccountSasServiceParseIllegalString() { + assertThrows(IllegalArgumentException.class, () -> TableAccountSasService.parse("bqta")); + } + + @Test + public void tableSasIpRangeToString() { + assertEquals("a-b", new TableSasIpRange() + .setIpMin("a") + .setIpMax("b") + .toString()); + + assertEquals("a", new TableSasIpRange().setIpMin("a").toString()); + assertEquals("", new TableSasIpRange().setIpMax("b").toString()); + } + + @Test + public void tableSasIpRangeParse() { + TableSasIpRange tableSasIpRange = TableSasIpRange.parse("a-b"); + + assertEquals("a", tableSasIpRange.getIpMin()); + assertEquals("b", tableSasIpRange.getIpMax()); + + tableSasIpRange = TableSasIpRange.parse("a"); + + assertEquals("a", tableSasIpRange.getIpMin()); + assertNull(tableSasIpRange.getIpMax()); + + tableSasIpRange = TableSasIpRange.parse(""); + + assertEquals("", tableSasIpRange.getIpMin()); + assertNull(tableSasIpRange.getIpMax()); + } + + @Test + public void tableSasProtocolParse() { + assertEquals(TableSasProtocol.HTTPS_ONLY, TableSasProtocol.parse("https")); + assertEquals(TableSasProtocol.HTTPS_HTTP, TableSasProtocol.parse("https,http")); + } + + @Test + public void createTableSasSignatureValuesWithMinimumValues() { + OffsetDateTime expiryTime = OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); + TableSasPermission permissions = TableSasPermission.parse("r"); + + OffsetDateTime startTime = OffsetDateTime.of(2015, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); + TableSasIpRange ipRange = TableSasIpRange.parse("a-b"); + TableSasProtocol protocol = TableSasProtocol.HTTPS_ONLY; + String startPartitionKey = "startPartitionKey"; + String startRowKey = "startRowKey"; + String endPartitionKey = "endPartitionKey"; + String endRowKey = "endRowKey"; + + TableSasSignatureValues sasSignatureValues = + new TableSasSignatureValues(expiryTime, permissions) + .setStartTime(startTime) + .setSasIpRange(ipRange) + .setProtocol(protocol) + .setStartPartitionKey(startPartitionKey) + .setStartRowKey(startRowKey) + .setEndPartitionKey(endPartitionKey) + .setEndRowKey(endRowKey); + + assertEquals(expiryTime, sasSignatureValues.getExpiryTime()); + assertEquals(permissions.toString(), sasSignatureValues.getPermissions()); + assertEquals(startTime, sasSignatureValues.getStartTime()); + assertEquals(ipRange, sasSignatureValues.getSasIpRange()); + assertEquals(protocol, sasSignatureValues.getProtocol()); + assertEquals(startPartitionKey, sasSignatureValues.getStartPartitionKey()); + assertEquals(startRowKey, sasSignatureValues.getStartRowKey()); + assertEquals(endPartitionKey, sasSignatureValues.getEndPartitionKey()); + assertEquals(endRowKey, sasSignatureValues.getEndRowKey()); + } + + @Test + public void createTableSasSignatureValuesWithNullRequiredValue() { + OffsetDateTime expiryTime = OffsetDateTime.of(2017, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); + TableSasPermission permissions = TableSasPermission.parse("r"); + + assertThrows(NullPointerException.class, + () -> new TableSasSignatureValues(null, permissions)); + assertThrows(NullPointerException.class, + () -> new TableSasSignatureValues(expiryTime, null)); + } + + @Test + public void tableSasPermissionToString() { + assertEquals("raud", new TableSasPermission() + .setReadPermission(true) + .setAddPermission(true) + .setUpdatePermission(true) + .setDeletePermission(true) + .toString()); + assertEquals("r", new TableSasPermission().setReadPermission(true).toString()); + assertEquals("a", new TableSasPermission().setAddPermission(true).toString()); + assertEquals("u", new TableSasPermission().setUpdatePermission(true).toString()); + assertEquals("d", new TableSasPermission().setDeletePermission(true).toString()); + } + + @Test + public void tableSasPermissionParse() { + TableSasPermission tableSasPermission = TableSasPermission.parse("raud"); + + assertTrue(tableSasPermission.hasReadPermission()); + assertTrue(tableSasPermission.hasAddPermission()); + assertTrue(tableSasPermission.hasUpdatePermission()); + assertTrue(tableSasPermission.hasDeletePermission()); + + tableSasPermission = TableSasPermission.parse("urda"); + + assertTrue(tableSasPermission.hasReadPermission()); + assertTrue(tableSasPermission.hasAddPermission()); + assertTrue(tableSasPermission.hasUpdatePermission()); + assertTrue(tableSasPermission.hasDeletePermission()); + + assertTrue(TableSasPermission.parse("r").hasReadPermission()); + assertTrue(TableSasPermission.parse("a").hasAddPermission()); + assertTrue(TableSasPermission.parse("u").hasUpdatePermission()); + assertTrue(TableSasPermission.parse("d").hasDeletePermission()); + } + + @Test + public void tableSasPermissionParseIllegalString() { + assertThrows(IllegalArgumentException.class, () -> TableSasPermission.parse("raup")); + } +} diff --git a/sdk/tables/azure-data-tables/src/test/java/com/azure/data/tables/TablesAsyncClientTest.java b/sdk/tables/azure-data-tables/src/test/java/com/azure/data/tables/TableAsyncClientTest.java similarity index 88% rename from sdk/tables/azure-data-tables/src/test/java/com/azure/data/tables/TablesAsyncClientTest.java rename to sdk/tables/azure-data-tables/src/test/java/com/azure/data/tables/TableAsyncClientTest.java index 640ed3fce28d..aea0ed4f5502 100644 --- a/sdk/tables/azure-data-tables/src/test/java/com/azure/data/tables/TablesAsyncClientTest.java +++ b/sdk/tables/azure-data-tables/src/test/java/com/azure/data/tables/TableAsyncClientTest.java @@ -12,14 +12,18 @@ import com.azure.core.http.rest.Response; import com.azure.core.test.TestBase; import com.azure.core.test.utils.TestResourceNamer; -import com.azure.data.tables.models.TableTransactionActionResponse; import com.azure.data.tables.models.ListEntitiesOptions; import com.azure.data.tables.models.TableEntity; import com.azure.data.tables.models.TableEntityUpdateMode; import com.azure.data.tables.models.TableTransactionAction; +import com.azure.data.tables.models.TableTransactionActionResponse; import com.azure.data.tables.models.TableTransactionActionType; import com.azure.data.tables.models.TableTransactionFailedException; import com.azure.data.tables.models.TableTransactionResult; +import com.azure.data.tables.sas.TableSasIpRange; +import com.azure.data.tables.sas.TableSasPermission; +import com.azure.data.tables.sas.TableSasProtocol; +import com.azure.data.tables.sas.TableSasSignatureValues; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; @@ -29,6 +33,7 @@ import java.time.Duration; import java.time.OffsetDateTime; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Date; import java.util.List; @@ -43,7 +48,7 @@ /** * Tests {@link TableAsyncClient}. */ -public class TablesAsyncClientTest extends TestBase { +public class TableAsyncClientTest extends TestBase { private static final Duration TIMEOUT = Duration.ofSeconds(100); private TableAsyncClient tableClient; @@ -72,13 +77,16 @@ protected void beforeTest() { if (interceptorManager.isPlaybackMode()) { playbackClient = interceptorManager.getPlaybackClient(); + builder.httpClient(playbackClient); } else { builder.httpClient(HttpClient.createDefault()); if (!interceptorManager.isLiveMode()) { recordPolicy = interceptorManager.getRecordPolicy(); + builder.addPolicy(recordPolicy); } + builder.addPolicy(new RetryPolicy(new ExponentialBackoff(6, Duration.ofMillis(1500), Duration.ofSeconds(100)))); } @@ -884,4 +892,121 @@ void submitTransactionAsyncWithDifferentPartitionKeys() { && e.getMessage().contains("rowKey='" + rowKeyValue2)) .verify(); } + + @Test + @Tag("SAS") + public void generateSasTokenWithMinimumParameters() { + final OffsetDateTime expiryTime = OffsetDateTime.of(2021, 12, 12, 0, 0, 0, 0, ZoneOffset.UTC); + final TableSasPermission permissions = TableSasPermission.parse("r"); + final TableSasProtocol protocol = TableSasProtocol.HTTPS_ONLY; + + final TableSasSignatureValues sasSignatureValues = + new TableSasSignatureValues(expiryTime, permissions) + .setProtocol(protocol) + .setVersion(TableServiceVersion.V2019_02_02.getVersion()); + + final String sas = tableClient.generateSas(sasSignatureValues); + + assertTrue( + sas.startsWith( + "sv=2019-02-02" + + "&se=2021-12-12T00%3A00%3A00Z" + + "&tn=" + tableClient.getTableName() + + "&sp=r" + + "&spr=https" + + "&sig=" + ) + ); + } + + @Test + @Tag("SAS") + public void generateSasTokenWithAllParameters() { + final OffsetDateTime expiryTime = OffsetDateTime.of(2021, 12, 12, 0, 0, 0, 0, ZoneOffset.UTC); + final TableSasPermission permissions = TableSasPermission.parse("raud"); + final TableSasProtocol protocol = TableSasProtocol.HTTPS_HTTP; + + final OffsetDateTime startTime = OffsetDateTime.of(2015, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); + final TableSasIpRange ipRange = TableSasIpRange.parse("a-b"); + final String startPartitionKey = "startPartitionKey"; + final String startRowKey = "startRowKey"; + final String endPartitionKey = "endPartitionKey"; + final String endRowKey = "endRowKey"; + + final TableSasSignatureValues sasSignatureValues = + new TableSasSignatureValues(expiryTime, permissions) + .setProtocol(protocol) + .setVersion(TableServiceVersion.V2019_02_02.getVersion()) + .setStartTime(startTime) + .setSasIpRange(ipRange) + .setStartPartitionKey(startPartitionKey) + .setStartRowKey(startRowKey) + .setEndPartitionKey(endPartitionKey) + .setEndRowKey(endRowKey); + + final String sas = tableClient.generateSas(sasSignatureValues); + + assertTrue( + sas.startsWith( + "sv=2019-02-02" + + "&st=2015-01-01T00%3A00%3A00Z" + + "&se=2021-12-12T00%3A00%3A00Z" + + "&tn=" + tableClient.getTableName() + + "&sp=raud" + + "&spk=startPartitionKey" + + "&srk=startRowKey" + + "&epk=endPartitionKey" + + "&erk=endRowKey" + + "&sip=a-b" + + "&spr=https%2Chttp" + + "&sig=" + ) + ); + } + + @Test + @Tag("SAS") + public void canUseSasTokenToCreateValidTableClient() { + final OffsetDateTime expiryTime = OffsetDateTime.of(2021, 12, 12, 0, 0, 0, 0, ZoneOffset.UTC); + final TableSasPermission permissions = TableSasPermission.parse("a"); + final TableSasProtocol protocol = TableSasProtocol.HTTPS_HTTP; + + final TableSasSignatureValues sasSignatureValues = + new TableSasSignatureValues(expiryTime, permissions) + .setProtocol(protocol) + .setVersion(TableServiceVersion.V2019_02_02.getVersion()); + + final String sas = tableClient.generateSas(sasSignatureValues); + + final TableClientBuilder tableClientBuilder = new TableClientBuilder() + .httpLogOptions(new HttpLogOptions().setLogLevel(HttpLogDetailLevel.BODY_AND_HEADERS)) + .endpoint(tableClient.getTableEndpoint()) + .sasToken(sas) + .tableName(tableClient.getTableName()); + + if (interceptorManager.isPlaybackMode()) { + tableClientBuilder.httpClient(playbackClient); + } else { + tableClientBuilder.httpClient(HttpClient.createDefault()); + + if (!interceptorManager.isLiveMode()) { + tableClientBuilder.addPolicy(recordPolicy); + } + + tableClientBuilder.addPolicy(new RetryPolicy(new ExponentialBackoff(6, Duration.ofMillis(1500), + Duration.ofSeconds(100)))); + } + + // Create a new client authenticated with the SAS token. + final TableAsyncClient tableAsyncClient = tableClientBuilder.buildAsyncClient(); + final String partitionKeyValue = testResourceNamer.randomName("partitionKey", 20); + final String rowKeyValue = testResourceNamer.randomName("rowKey", 20); + final TableEntity entity = new TableEntity(partitionKeyValue, rowKeyValue); + final int expectedStatusCode = 204; + + StepVerifier.create(tableAsyncClient.createEntityWithResponse(entity)) + .assertNext(response -> assertEquals(expectedStatusCode, response.getStatusCode())) + .expectComplete() + .verify(); + } } diff --git a/sdk/tables/azure-data-tables/src/test/java/com/azure/data/tables/TablesClientBuilderTest.java b/sdk/tables/azure-data-tables/src/test/java/com/azure/data/tables/TableClientBuilderTest.java similarity index 64% rename from sdk/tables/azure-data-tables/src/test/java/com/azure/data/tables/TablesClientBuilderTest.java rename to sdk/tables/azure-data-tables/src/test/java/com/azure/data/tables/TableClientBuilderTest.java index 9104f15fc517..50dbd29adbcf 100644 --- a/sdk/tables/azure-data-tables/src/test/java/com/azure/data/tables/TablesClientBuilderTest.java +++ b/sdk/tables/azure-data-tables/src/test/java/com/azure/data/tables/TableClientBuilderTest.java @@ -21,12 +21,13 @@ import java.security.SecureRandom; import java.util.Collections; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; -public class TablesClientBuilderTest { +public class TableClientBuilderTest { private String tableName; private String connectionString; private TableServiceVersion serviceVersion; @@ -192,4 +193,81 @@ public void addPerCallPolicy() { assertTrue(perCallPolicyPosition < retryPolicyPosition); assertTrue(retryPolicyPosition < perRetryPolicyPosition); } + + @Test + public void multipleFormsOfAuthenticationPresent() { + assertThrows(IllegalStateException.class, () -> new TableClientBuilder() + .sasToken("sasToken") + .credential(new AzureNamedKeyCredential("name", "key")) + .tableName("myTable") + .endpoint("https://myaccount.table.core.windows.net") + .buildAsyncClient()); + + assertThrows(IllegalStateException.class, () -> new TableClientBuilder() + .sasToken("sasToken") + .credential(new AzureSasCredential("sasToken")) + .tableName("myTable") + .endpoint("https://myaccount.table.core.windows.net") + .buildAsyncClient()); + + assertThrows(IllegalStateException.class, () -> new TableClientBuilder() + .credential(new AzureNamedKeyCredential("name", "key")) + .credential(new AzureSasCredential("sasToken")) + .tableName("myTable") + .endpoint("https://myaccount.table.core.windows.net") + .buildAsyncClient()); + + assertThrows(IllegalStateException.class, () -> new TableClientBuilder() + .sasToken("sasToken") + .credential(new AzureNamedKeyCredential("name", "key")) + .credential(new AzureSasCredential("sasToken")) + .tableName("myTable") + .endpoint("https://myaccount.table.core.windows.net") + .buildAsyncClient()); + } + + @Test + public void buildWithSameSasTokenInConnectionStringDoesNotThrow() { + assertDoesNotThrow(() -> new TableClientBuilder() + .sasToken("sv=2020-02-10&ss=t&srt=o&sp=rwdlacu&se=2021-06-04T04:45:57Z&st=2021-06-03T20:45:57Z&spr=https&sig=someSignature") + .connectionString("TableEndpoint=https://myaccount.table.core.windows.net/;SharedAccessSignature=sv=2020-02-10&ss=t&srt=o&sp=rwdlacu&se=2021-06-04T04:45:57Z&st=2021-06-03T20:45:57Z&spr=https&sig=someSignature") + .tableName("myTable") + .buildAsyncClient()); + } + + @Test + public void buildWithDifferentSasTokenInConnectionStringThrows() { + assertThrows(IllegalStateException.class, () -> new TableClientBuilder() + .sasToken("sv=2020-02-10&ss=t&srt=o&sp=rwd&se=2021-06-04T04:45:57Z&st=2021-06-03T20:45:57Z&spr=https&sig=someSignature") + .connectionString("TableEndpoint=https://myaccount.table.core.windows.net/;SharedAccessSignature=sv=2020-02-10&ss=t&srt=o&sp=rwdlacu&se=2021-06-04T04:45:57Z&st=2021-06-03T20:45:57Z&spr=https&sig=anotherSignature") + .tableName("myTable") + .buildAsyncClient()); + } + + @Test + public void buildWithSameEndpointInConnectionStringDoesNotThrow() { + assertDoesNotThrow(() -> new TableClientBuilder() + .endpoint("https://myaccount.table.core.windows.net/") + .connectionString("TableEndpoint=https://myaccount.table.core.windows.net/;SharedAccessSignature=sv=2020-02-10&ss=t&srt=o&sp=rwdlacu&se=2021-06-04T04:45:57Z&st=2021-06-03T20:45:57Z&spr=https&sig=someSignature") + .tableName("myTable") + .buildAsyncClient()); + } + + @Test + public void buildWithSameEndpointInConnectionStringWithTrailingSlashDoesNotThrow() { + assertDoesNotThrow(() -> new TableClientBuilder() + .endpoint("https://myaccount.table.core.windows.net") + .connectionString("TableEndpoint=https://myaccount.table.core.windows.net/;SharedAccessSignature=sv=2020-02-10&ss=t&srt=o&sp=rwdlacu&se=2021-06-04T04:45:57Z&st=2021-06-03T20:45:57Z&spr=https&sig=someSignature") + .tableName("myTable") + .buildAsyncClient()); + } + + @Test + public void buildWithDifferentEndpointInConnectionStringThrows() { + assertThrows(IllegalStateException.class, () -> new TableClientBuilder() + .endpoint("https://myotheraccount.table.core.windows.net/") + .connectionString("TableEndpoint=https://myaccount.table.core.windows.net/;SharedAccessSignature=sv=2020-02-10&ss=t&srt=o&sp=rwdlacu&se=2021-06-04T04:45:57Z&st=2021-06-03T20:45:57Z&spr=https&sig=someSignature") + .tableName("myTable") + .buildAsyncClient()); + } } diff --git a/sdk/tables/azure-data-tables/src/test/java/com/azure/data/tables/TableServiceAsyncClientTest.java b/sdk/tables/azure-data-tables/src/test/java/com/azure/data/tables/TableServiceAsyncClientTest.java index 4e95402734c2..b94b25f1f855 100644 --- a/sdk/tables/azure-data-tables/src/test/java/com/azure/data/tables/TableServiceAsyncClientTest.java +++ b/sdk/tables/azure-data-tables/src/test/java/com/azure/data/tables/TableServiceAsyncClientTest.java @@ -7,10 +7,18 @@ import com.azure.core.http.policy.ExponentialBackoff; import com.azure.core.http.policy.HttpLogDetailLevel; import com.azure.core.http.policy.HttpLogOptions; +import com.azure.core.http.policy.HttpPipelinePolicy; import com.azure.core.http.policy.RetryPolicy; import com.azure.core.test.TestBase; import com.azure.data.tables.models.ListTablesOptions; +import com.azure.data.tables.models.TableEntity; import com.azure.data.tables.models.TableServiceException; +import com.azure.data.tables.sas.TableAccountSasPermission; +import com.azure.data.tables.sas.TableAccountSasResourceType; +import com.azure.data.tables.sas.TableAccountSasService; +import com.azure.data.tables.sas.TableAccountSasSignatureValues; +import com.azure.data.tables.sas.TableSasIpRange; +import com.azure.data.tables.sas.TableSasProtocol; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; @@ -19,10 +27,13 @@ import reactor.test.StepVerifier; import java.time.Duration; +import java.time.OffsetDateTime; +import java.time.ZoneOffset; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; /** * Tests methods for {@link TableServiceAsyncClient}. @@ -30,6 +41,8 @@ public class TableServiceAsyncClientTest extends TestBase { private static final Duration TIMEOUT = Duration.ofSeconds(100); private TableServiceAsyncClient serviceClient; + private HttpPipelinePolicy recordPolicy; + private HttpClient playbackClient; @BeforeAll static void beforeAll() { @@ -49,12 +62,18 @@ protected void beforeTest() { .httpLogOptions(new HttpLogOptions().setLogLevel(HttpLogDetailLevel.BODY_AND_HEADERS)); if (interceptorManager.isPlaybackMode()) { - builder.httpClient(interceptorManager.getPlaybackClient()); + playbackClient = interceptorManager.getPlaybackClient(); + + builder.httpClient(playbackClient); } else { builder.httpClient(HttpClient.createDefault()); + if (!interceptorManager.isLiveMode()) { - builder.addPolicy(interceptorManager.getRecordPolicy()); + recordPolicy = interceptorManager.getRecordPolicy(); + + builder.addPolicy(recordPolicy); } + builder.addPolicy(new RetryPolicy(new ExponentialBackoff(6, Duration.ofMillis(1500), Duration.ofSeconds(100)))); } @@ -277,6 +296,123 @@ void serviceGetTableClientAsync() { TableAsyncClient tableClient = serviceClient.getTableClient(tableName); // Act & Assert - TablesAsyncClientTest.getEntityWithResponseAsyncImpl(tableClient, this.testResourceNamer); + TableAsyncClientTest.getEntityWithResponseAsyncImpl(tableClient, this.testResourceNamer); + } + + @Test + @Tag("SAS") + public void generateAccountSasTokenWithMinimumParameters() { + final OffsetDateTime expiryTime = OffsetDateTime.of(2021, 12, 12, 0, 0, 0, 0, ZoneOffset.UTC); + final TableAccountSasPermission permissions = TableAccountSasPermission.parse("r"); + final TableAccountSasService services = new TableAccountSasService().setTableAccess(true); + final TableAccountSasResourceType resourceTypes = new TableAccountSasResourceType().setObject(true); + final TableSasProtocol protocol = TableSasProtocol.HTTPS_ONLY; + + final TableAccountSasSignatureValues sasSignatureValues = + new TableAccountSasSignatureValues(expiryTime, permissions, services, resourceTypes) + .setProtocol(protocol) + .setVersion(TableServiceVersion.V2019_02_02.getVersion()); + + final String sas = serviceClient.generateAccountSas(sasSignatureValues); + + assertTrue( + sas.startsWith( + "sv=2019-02-02" + + "&ss=t" + + "&srt=o" + + "&se=2021-12-12T00%3A00%3A00Z" + + "&sp=r" + + "&spr=https" + + "&sig=" + ) + ); + } + + @Test + @Tag("SAS") + public void generateAccountSasTokenWithAllParameters() { + final OffsetDateTime expiryTime = OffsetDateTime.of(2021, 12, 12, 0, 0, 0, 0, ZoneOffset.UTC); + final TableAccountSasPermission permissions = TableAccountSasPermission.parse("rdau"); + final TableAccountSasService services = new TableAccountSasService().setTableAccess(true); + final TableAccountSasResourceType resourceTypes = new TableAccountSasResourceType().setObject(true); + final TableSasProtocol protocol = TableSasProtocol.HTTPS_HTTP; + + final OffsetDateTime startTime = OffsetDateTime.of(2015, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC); + final TableSasIpRange ipRange = TableSasIpRange.parse("a-b"); + + final TableAccountSasSignatureValues sasSignatureValues = + new TableAccountSasSignatureValues(expiryTime, permissions, services, resourceTypes) + .setProtocol(protocol) + .setVersion(TableServiceVersion.V2019_02_02.getVersion()) + .setStartTime(startTime) + .setSasIpRange(ipRange); + + final String sas = serviceClient.generateAccountSas(sasSignatureValues); + + assertTrue( + sas.startsWith( + "sv=2019-02-02" + + "&ss=t" + + "&srt=o" + + "&st=2015-01-01T00%3A00%3A00Z" + + "&se=2021-12-12T00%3A00%3A00Z" + + "&sp=rdau" + + "&sip=a-b" + + "&spr=https%2Chttp" + + "&sig=" + ) + ); + } + + @Test + @Tag("SAS") + public void canUseSasTokenToCreateValidTableClient() { + final OffsetDateTime expiryTime = OffsetDateTime.of(2021, 12, 12, 0, 0, 0, 0, ZoneOffset.UTC); + final TableAccountSasPermission permissions = TableAccountSasPermission.parse("a"); + final TableAccountSasService services = new TableAccountSasService().setTableAccess(true); + final TableAccountSasResourceType resourceTypes = new TableAccountSasResourceType().setObject(true); + final TableSasProtocol protocol = TableSasProtocol.HTTPS_ONLY; + + final TableAccountSasSignatureValues sasSignatureValues = + new TableAccountSasSignatureValues(expiryTime, permissions, services, resourceTypes) + .setProtocol(protocol) + .setVersion(TableServiceVersion.V2019_02_02.getVersion()); + + final String sas = serviceClient.generateAccountSas(sasSignatureValues); + final String tableName = testResourceNamer.randomName("test", 20); + + serviceClient.createTable(tableName).block(TIMEOUT); + + final TableClientBuilder tableClientBuilder = new TableClientBuilder() + .httpLogOptions(new HttpLogOptions().setLogLevel(HttpLogDetailLevel.BODY_AND_HEADERS)) + .endpoint(serviceClient.getServiceEndpoint()) + .sasToken(sas) + .tableName(tableName); + + if (interceptorManager.isPlaybackMode()) { + tableClientBuilder.httpClient(playbackClient); + } else { + tableClientBuilder.httpClient(HttpClient.createDefault()); + + if (!interceptorManager.isLiveMode()) { + tableClientBuilder.addPolicy(recordPolicy); + } + + tableClientBuilder.addPolicy(new RetryPolicy(new ExponentialBackoff(6, Duration.ofMillis(1500), + Duration.ofSeconds(100)))); + } + + // Create a new client authenticated with the SAS token. + final TableAsyncClient tableAsyncClient = tableClientBuilder.buildAsyncClient(); + final String partitionKeyValue = testResourceNamer.randomName("partitionKey", 20); + final String rowKeyValue = testResourceNamer.randomName("rowKey", 20); + final TableEntity entity = new TableEntity(partitionKeyValue, rowKeyValue); + final int expectedStatusCode = 204; + + //Act & Assert + StepVerifier.create(tableAsyncClient.createEntityWithResponse(entity)) + .assertNext(response -> assertEquals(expectedStatusCode, response.getStatusCode())) + .expectComplete() + .verify(); } } diff --git a/sdk/tables/azure-data-tables/src/test/java/com/azure/data/tables/TableServiceClientBuilderTest.java b/sdk/tables/azure-data-tables/src/test/java/com/azure/data/tables/TableServiceClientBuilderTest.java index c41a9e9fbd85..8667309445a8 100644 --- a/sdk/tables/azure-data-tables/src/test/java/com/azure/data/tables/TableServiceClientBuilderTest.java +++ b/sdk/tables/azure-data-tables/src/test/java/com/azure/data/tables/TableServiceClientBuilderTest.java @@ -21,6 +21,7 @@ import java.security.SecureRandom; import java.util.Collections; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -183,4 +184,72 @@ public void addPerCallPolicy() { assertTrue(perCallPolicyPosition < retryPolicyPosition); assertTrue(retryPolicyPosition < perRetryPolicyPosition); } + + @Test + public void multipleFormsOfAuthenticationPresent() { + assertThrows(IllegalStateException.class, () -> new TableServiceClientBuilder() + .sasToken("sasToken") + .credential(new AzureNamedKeyCredential("name", "key")) + .endpoint("https://myaccount.table.core.windows.net") + .buildAsyncClient()); + + assertThrows(IllegalStateException.class, () -> new TableServiceClientBuilder() + .sasToken("sasToken") + .credential(new AzureSasCredential("sasToken")) + .endpoint("https://myaccount.table.core.windows.net") + .buildAsyncClient()); + + assertThrows(IllegalStateException.class, () -> new TableServiceClientBuilder() + .credential(new AzureNamedKeyCredential("name", "key")) + .credential(new AzureSasCredential("sasToken")) + .endpoint("https://myaccount.table.core.windows.net") + .buildAsyncClient()); + + assertThrows(IllegalStateException.class, () -> new TableServiceClientBuilder() + .sasToken("sasToken") + .credential(new AzureNamedKeyCredential("name", "key")) + .credential(new AzureSasCredential("sasToken")) + .endpoint("https://myaccount.table.core.windows.net") + .buildAsyncClient()); + } + + @Test + public void buildWithSameSasTokenInConnectionStringDoesNotThrow() { + assertDoesNotThrow(() -> new TableServiceClientBuilder() + .sasToken("sv=2020-02-10&ss=t&srt=o&sp=rwdlacu&se=2021-06-04T04:45:57Z&st=2021-06-03T20:45:57Z&spr=https&sig=someSignature") + .connectionString("TableEndpoint=https://myaccount.table.core.windows.net/;SharedAccessSignature=sv=2020-02-10&ss=t&srt=o&sp=rwdlacu&se=2021-06-04T04:45:57Z&st=2021-06-03T20:45:57Z&spr=https&sig=someSignature") + .buildAsyncClient()); + } + + @Test + public void buildWithDifferentSasTokenInConnectionStringThrows() { + assertThrows(IllegalStateException.class, () -> new TableServiceClientBuilder() + .sasToken("sv=2020-02-10&ss=t&srt=o&sp=rwd&se=2021-06-04T04:45:57Z&st=2021-06-03T20:45:57Z&spr=https&sig=someSignature") + .connectionString("TableEndpoint=https://myaccount.table.core.windows.net/;SharedAccessSignature=sv=2020-02-10&ss=t&srt=o&sp=rwdlacu&se=2021-06-04T04:45:57Z&st=2021-06-03T20:45:57Z&spr=https&sig=anotherSignature") + .buildAsyncClient()); + } + + @Test + public void buildWithSameEndpointInConnectionStringDoesNotThrow() { + assertDoesNotThrow(() -> new TableServiceClientBuilder() + .endpoint("https://myaccount.table.core.windows.net/") + .connectionString("TableEndpoint=https://myaccount.table.core.windows.net/;SharedAccessSignature=sv=2020-02-10&ss=t&srt=o&sp=rwdlacu&se=2021-06-04T04:45:57Z&st=2021-06-03T20:45:57Z&spr=https&sig=someSignature") + .buildAsyncClient()); + } + + @Test + public void buildWithSameEndpointInConnectionStringWithTrailingSlashDoesNotThrow() { + assertDoesNotThrow(() -> new TableServiceClientBuilder() + .endpoint("https://myaccount.table.core.windows.net") + .connectionString("TableEndpoint=https://myaccount.table.core.windows.net/;SharedAccessSignature=sv=2020-02-10&ss=t&srt=o&sp=rwdlacu&se=2021-06-04T04:45:57Z&st=2021-06-03T20:45:57Z&spr=https&sig=someSignature") + .buildAsyncClient()); + } + + @Test + public void buildWithDifferentEndpointInConnectionStringThrows() { + assertThrows(IllegalStateException.class, () -> new TableServiceClientBuilder() + .endpoint("https://myotheraccount.table.core.windows.net/") + .connectionString("TableEndpoint=https://myaccount.table.core.windows.net/;SharedAccessSignature=sv=2020-02-10&ss=t&srt=o&sp=rwdlacu&se=2021-06-04T04:45:57Z&st=2021-06-03T20:45:57Z&spr=https&sig=someSignature") + .buildAsyncClient()); + } } diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.canUseSasTokenToCreateValidTableClient.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.canUseSasTokenToCreateValidTableClient.json new file mode 100644 index 000000000000..f4a1368377be --- /dev/null +++ b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.canUseSasTokenToCreateValidTableClient.json @@ -0,0 +1,55 @@ +{ + "networkCallRecords" : [ { + "Method" : "POST", + "Uri" : "https://REDACTED.table.core.windows.net/Tables", + "Headers" : { + "x-ms-version" : "2019-02-02", + "User-Agent" : "azsdk-java-azure-data-tables/12.0.0-beta.8 (11.0.6; Windows 10; 10.0)", + "x-ms-client-request-id" : "e56a3194-48cd-45f2-ace7-b1dfe471f364", + "Content-Type" : "application/json;odata=nometadata" + }, + "Response" : { + "content-length" : "0", + "x-ms-version" : "2019-02-02", + "Server" : "Windows-Azure-Table/1.0 Microsoft-HTTPAPI/2.0", + "X-Content-Type-Options" : "nosniff", + "retry-after" : "0", + "StatusCode" : "204", + "Date" : "Fri, 04 Jun 2021 20:21:58 GMT", + "Cache-Control" : "no-cache", + "DataServiceId" : "https://tablesstoragetests.table.core.windows.net/Tables('tablename40793703')", + "x-ms-request-id" : "01f46ed0-a002-0035-257f-597034000000", + "x-ms-client-request-id" : "e56a3194-48cd-45f2-ace7-b1dfe471f364", + "Preference-Applied" : "return-no-content", + "Location" : "https://tablesstoragetests.table.core.windows.net/Tables('tablename40793703')" + }, + "Exception" : null + }, { + "Method" : "POST", + "Uri" : "https://REDACTED.table.core.windows.net/tablename40793703/tablename40793703?sv=2019-02-02&se=2021-12-12T00%3A00%3A00Z&tn=tablename40793703&sp=a&spr=https%2Chttp&sig=REDACTED", + "Headers" : { + "x-ms-version" : "2019-02-02", + "User-Agent" : "azsdk-java-azure-data-tables/12.0.0-beta.8 (11.0.6; Windows 10; 10.0)", + "x-ms-client-request-id" : "587dac9f-2e50-47b0-9554-cc449d2b94ab", + "Content-Type" : "application/json;odata=nometadata" + }, + "Response" : { + "content-length" : "0", + "x-ms-version" : "2019-02-02", + "Server" : "Windows-Azure-Table/1.0 Microsoft-HTTPAPI/2.0", + "X-Content-Type-Options" : "nosniff", + "retry-after" : "0", + "StatusCode" : "204", + "Date" : "Fri, 04 Jun 2021 20:21:58 GMT", + "Cache-Control" : "no-cache", + "DataServiceId" : "https://tablesstoragetests.table.core.windows.net/tablename40793703(PartitionKey='partitionkey915773',RowKey='rowkey25200da12')", + "eTag" : "W/datetime'2021-06-04T20%3A21%3A58.6283474Z'", + "x-ms-request-id" : "01f46efe-a002-0035-4f7f-597034000000", + "x-ms-client-request-id" : "587dac9f-2e50-47b0-9554-cc449d2b94ab", + "Preference-Applied" : "return-no-content", + "Location" : "https://tablesstoragetests.table.core.windows.net/tablename40793703(PartitionKey='partitionkey915773',RowKey='rowkey25200da12')" + }, + "Exception" : null + } ], + "variables" : [ "tablename40793703", "partitionkey915773", "rowkey25200da12" ] +} \ No newline at end of file diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.createEntityAsync.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.createEntityAsync.json similarity index 100% rename from sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.createEntityAsync.json rename to sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.createEntityAsync.json diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.createEntitySubclassAsync.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.createEntitySubclassAsync.json similarity index 100% rename from sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.createEntitySubclassAsync.json rename to sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.createEntitySubclassAsync.json diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.createEntityWithAllSupportedDataTypesAsync.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.createEntityWithAllSupportedDataTypesAsync.json similarity index 100% rename from sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.createEntityWithAllSupportedDataTypesAsync.json rename to sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.createEntityWithAllSupportedDataTypesAsync.json diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.createEntityWithResponseAsync.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.createEntityWithResponseAsync.json similarity index 100% rename from sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.createEntityWithResponseAsync.json rename to sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.createEntityWithResponseAsync.json diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.createTableAsync.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.createTableAsync.json similarity index 100% rename from sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.createTableAsync.json rename to sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.createTableAsync.json diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.createTableWithResponseAsync.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.createTableWithResponseAsync.json similarity index 100% rename from sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.createTableWithResponseAsync.json rename to sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.createTableWithResponseAsync.json diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.deleteEntityAsync.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.deleteEntityAsync.json similarity index 100% rename from sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.deleteEntityAsync.json rename to sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.deleteEntityAsync.json diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.deleteEntityWithResponseAsync.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.deleteEntityWithResponseAsync.json similarity index 100% rename from sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.deleteEntityWithResponseAsync.json rename to sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.deleteEntityWithResponseAsync.json diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.deleteEntityWithResponseMatchETagAsync.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.deleteEntityWithResponseMatchETagAsync.json similarity index 100% rename from sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.deleteEntityWithResponseMatchETagAsync.json rename to sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.deleteEntityWithResponseMatchETagAsync.json diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.deleteNonExistingEntityAsync.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.deleteNonExistingEntityAsync.json similarity index 100% rename from sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.deleteNonExistingEntityAsync.json rename to sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.deleteNonExistingEntityAsync.json diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.deleteNonExistingEntityWithResponseAsync.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.deleteNonExistingEntityWithResponseAsync.json similarity index 100% rename from sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.deleteNonExistingEntityWithResponseAsync.json rename to sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.deleteNonExistingEntityWithResponseAsync.json diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.deleteNonExistingTableAsync.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.deleteNonExistingTableAsync.json similarity index 100% rename from sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.deleteNonExistingTableAsync.json rename to sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.deleteNonExistingTableAsync.json diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.deleteNonExistingTableWithResponseAsync.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.deleteNonExistingTableWithResponseAsync.json similarity index 100% rename from sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.deleteNonExistingTableWithResponseAsync.json rename to sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.deleteNonExistingTableWithResponseAsync.json diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.deleteTableAsync.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.deleteTableAsync.json similarity index 100% rename from sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.deleteTableAsync.json rename to sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.deleteTableAsync.json diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.deleteTableWithResponseAsync.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.deleteTableWithResponseAsync.json similarity index 100% rename from sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.deleteTableWithResponseAsync.json rename to sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.deleteTableWithResponseAsync.json diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.generateSasTokenWithAllParameters.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.generateSasTokenWithAllParameters.json new file mode 100644 index 000000000000..c84249d7bdd6 --- /dev/null +++ b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.generateSasTokenWithAllParameters.json @@ -0,0 +1,29 @@ +{ + "networkCallRecords" : [ { + "Method" : "POST", + "Uri" : "https://REDACTED.table.core.windows.net/Tables", + "Headers" : { + "x-ms-version" : "2019-02-02", + "User-Agent" : "azsdk-java-azure-data-tables/12.0.0-beta.8 (11.0.6; Windows 10; 10.0)", + "x-ms-client-request-id" : "93a2a3c9-4159-40a8-9247-748b1f0b66f4", + "Content-Type" : "application/json;odata=nometadata" + }, + "Response" : { + "content-length" : "0", + "x-ms-version" : "2019-02-02", + "Server" : "Windows-Azure-Table/1.0 Microsoft-HTTPAPI/2.0", + "X-Content-Type-Options" : "nosniff", + "retry-after" : "0", + "StatusCode" : "204", + "Date" : "Fri, 04 Jun 2021 20:21:57 GMT", + "Cache-Control" : "no-cache", + "DataServiceId" : "https://tablesstoragetests.table.core.windows.net/Tables('tablename17721fc9')", + "x-ms-request-id" : "7b55d062-1002-0005-137f-592a1e000000", + "x-ms-client-request-id" : "93a2a3c9-4159-40a8-9247-748b1f0b66f4", + "Preference-Applied" : "return-no-content", + "Location" : "https://tablesstoragetests.table.core.windows.net/Tables('tablename17721fc9')" + }, + "Exception" : null + } ], + "variables" : [ "tablename17721fc9" ] +} \ No newline at end of file diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.generateSasTokenWithMinimumParameters.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.generateSasTokenWithMinimumParameters.json new file mode 100644 index 000000000000..85584ab2fdb1 --- /dev/null +++ b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.generateSasTokenWithMinimumParameters.json @@ -0,0 +1,29 @@ +{ + "networkCallRecords" : [ { + "Method" : "POST", + "Uri" : "https://REDACTED.table.core.windows.net/Tables", + "Headers" : { + "x-ms-version" : "2019-02-02", + "User-Agent" : "azsdk-java-azure-data-tables/12.0.0-beta.8 (11.0.6; Windows 10; 10.0)", + "x-ms-client-request-id" : "943ad224-303a-4763-8e83-a818550503c7", + "Content-Type" : "application/json;odata=nometadata" + }, + "Response" : { + "content-length" : "0", + "x-ms-version" : "2019-02-02", + "Server" : "Windows-Azure-Table/1.0 Microsoft-HTTPAPI/2.0", + "X-Content-Type-Options" : "nosniff", + "retry-after" : "0", + "StatusCode" : "204", + "Date" : "Fri, 04 Jun 2021 20:21:56 GMT", + "Cache-Control" : "no-cache", + "DataServiceId" : "https://tablesstoragetests.table.core.windows.net/Tables('tablename69053ff7')", + "x-ms-request-id" : "1ab91518-c002-0025-707f-5946d2000000", + "x-ms-client-request-id" : "943ad224-303a-4763-8e83-a818550503c7", + "Preference-Applied" : "return-no-content", + "Location" : "https://tablesstoragetests.table.core.windows.net/Tables('tablename69053ff7')" + }, + "Exception" : null + } ], + "variables" : [ "tablename69053ff7" ] +} \ No newline at end of file diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.getEntityWithResponseAsync.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.getEntityWithResponseAsync.json similarity index 100% rename from sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.getEntityWithResponseAsync.json rename to sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.getEntityWithResponseAsync.json diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.getEntityWithResponseSubclassAsync.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.getEntityWithResponseSubclassAsync.json similarity index 100% rename from sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.getEntityWithResponseSubclassAsync.json rename to sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.getEntityWithResponseSubclassAsync.json diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.getEntityWithResponseWithSelectAsync.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.getEntityWithResponseWithSelectAsync.json similarity index 100% rename from sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.getEntityWithResponseWithSelectAsync.json rename to sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.getEntityWithResponseWithSelectAsync.json diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.listEntitiesAsync.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.listEntitiesAsync.json similarity index 100% rename from sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.listEntitiesAsync.json rename to sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.listEntitiesAsync.json diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.listEntitiesSubclassAsync.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.listEntitiesSubclassAsync.json similarity index 100% rename from sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.listEntitiesSubclassAsync.json rename to sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.listEntitiesSubclassAsync.json diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.listEntitiesWithFilterAsync.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.listEntitiesWithFilterAsync.json similarity index 100% rename from sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.listEntitiesWithFilterAsync.json rename to sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.listEntitiesWithFilterAsync.json diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.listEntitiesWithSelectAsync.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.listEntitiesWithSelectAsync.json similarity index 100% rename from sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.listEntitiesWithSelectAsync.json rename to sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.listEntitiesWithSelectAsync.json diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.listEntitiesWithTopAsync.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.listEntitiesWithTopAsync.json similarity index 100% rename from sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.listEntitiesWithTopAsync.json rename to sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.listEntitiesWithTopAsync.json diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.submitTransactionAsync.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.submitTransactionAsync.json similarity index 100% rename from sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.submitTransactionAsync.json rename to sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.submitTransactionAsync.json diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.submitTransactionAsyncAllActions.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.submitTransactionAsyncAllActions.json similarity index 100% rename from sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.submitTransactionAsyncAllActions.json rename to sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.submitTransactionAsyncAllActions.json diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.submitTransactionAsyncWithDifferentPartitionKeys.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.submitTransactionAsyncWithDifferentPartitionKeys.json similarity index 100% rename from sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.submitTransactionAsyncWithDifferentPartitionKeys.json rename to sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.submitTransactionAsyncWithDifferentPartitionKeys.json diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.submitTransactionAsyncWithFailingAction.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.submitTransactionAsyncWithFailingAction.json similarity index 100% rename from sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.submitTransactionAsyncWithFailingAction.json rename to sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.submitTransactionAsyncWithFailingAction.json diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.submitTransactionAsyncWithSameRowKeys.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.submitTransactionAsyncWithSameRowKeys.json similarity index 100% rename from sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.submitTransactionAsyncWithSameRowKeys.json rename to sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.submitTransactionAsyncWithSameRowKeys.json diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.updateEntityWithResponseMergeAsync.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.updateEntityWithResponseMergeAsync.json similarity index 100% rename from sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.updateEntityWithResponseMergeAsync.json rename to sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.updateEntityWithResponseMergeAsync.json diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.updateEntityWithResponseReplaceAsync.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.updateEntityWithResponseReplaceAsync.json similarity index 100% rename from sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.updateEntityWithResponseReplaceAsync.json rename to sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.updateEntityWithResponseReplaceAsync.json diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.updateEntityWithResponseSubclassAsync.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.updateEntityWithResponseSubclassAsync.json similarity index 100% rename from sdk/tables/azure-data-tables/src/test/resources/session-records/TablesAsyncClientTest.updateEntityWithResponseSubclassAsync.json rename to sdk/tables/azure-data-tables/src/test/resources/session-records/TableAsyncClientTest.updateEntityWithResponseSubclassAsync.json diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TableServiceAsyncClientTest.canUseSasTokenToCreateValidTableClient.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableServiceAsyncClientTest.canUseSasTokenToCreateValidTableClient.json new file mode 100644 index 000000000000..7f30fd87b782 --- /dev/null +++ b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableServiceAsyncClientTest.canUseSasTokenToCreateValidTableClient.json @@ -0,0 +1,55 @@ +{ + "networkCallRecords" : [ { + "Method" : "POST", + "Uri" : "https://REDACTED.table.core.windows.net/Tables", + "Headers" : { + "x-ms-version" : "2019-02-02", + "User-Agent" : "azsdk-java-azure-data-tables/12.0.0-beta.8 (11.0.6; Windows 10; 10.0)", + "x-ms-client-request-id" : "ebc98fd2-eee5-4da3-a077-62459e7024b6", + "Content-Type" : "application/json;odata=nometadata" + }, + "Response" : { + "content-length" : "0", + "x-ms-version" : "2019-02-02", + "Server" : "Windows-Azure-Table/1.0 Microsoft-HTTPAPI/2.0", + "X-Content-Type-Options" : "nosniff", + "retry-after" : "0", + "StatusCode" : "204", + "Date" : "Fri, 04 Jun 2021 20:20:48 GMT", + "Cache-Control" : "no-cache", + "DataServiceId" : "https://tablesstoragetests.table.core.windows.net/Tables('test859634c577')", + "x-ms-request-id" : "0ea574e2-e002-001b-4b7f-59f0f3000000", + "x-ms-client-request-id" : "ebc98fd2-eee5-4da3-a077-62459e7024b6", + "Preference-Applied" : "return-no-content", + "Location" : "https://tablesstoragetests.table.core.windows.net/Tables('test859634c577')" + }, + "Exception" : null + }, { + "Method" : "POST", + "Uri" : "https://REDACTED.table.core.windows.net/test859634c577?sv=2019-02-02&ss=t&srt=o&se=2021-12-12T00%3A00%3A00Z&sp=a&spr=https&sig=REDACTED", + "Headers" : { + "x-ms-version" : "2019-02-02", + "User-Agent" : "azsdk-java-azure-data-tables/12.0.0-beta.8 (11.0.6; Windows 10; 10.0)", + "x-ms-client-request-id" : "b2b12824-3264-46b5-a8ac-b546ea263bc6", + "Content-Type" : "application/json;odata=nometadata" + }, + "Response" : { + "content-length" : "0", + "x-ms-version" : "2019-02-02", + "Server" : "Windows-Azure-Table/1.0 Microsoft-HTTPAPI/2.0", + "X-Content-Type-Options" : "nosniff", + "retry-after" : "0", + "StatusCode" : "204", + "Date" : "Fri, 04 Jun 2021 20:20:49 GMT", + "Cache-Control" : "no-cache", + "DataServiceId" : "https://tablesstoragetests.table.core.windows.net/test859634c577(PartitionKey='partitionkey55134c',RowKey='rowkey71408f997')", + "eTag" : "W/datetime'2021-06-04T20%3A20%3A49.7021587Z'", + "x-ms-request-id" : "0ea5751a-e002-001b-017f-59f0f3000000", + "x-ms-client-request-id" : "b2b12824-3264-46b5-a8ac-b546ea263bc6", + "Preference-Applied" : "return-no-content", + "Location" : "https://tablesstoragetests.table.core.windows.net/test859634c577(PartitionKey='partitionkey55134c',RowKey='rowkey71408f997')" + }, + "Exception" : null + } ], + "variables" : [ "test859634c577", "partitionkey55134c", "rowkey71408f997" ] +} \ No newline at end of file diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TableServiceAsyncClientTest.generateAccountSasTokenWithAllParameters.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableServiceAsyncClientTest.generateAccountSasTokenWithAllParameters.json new file mode 100644 index 000000000000..ba5f37f8f855 --- /dev/null +++ b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableServiceAsyncClientTest.generateAccountSasTokenWithAllParameters.json @@ -0,0 +1,4 @@ +{ + "networkCallRecords" : [ ], + "variables" : [ ] +} \ No newline at end of file diff --git a/sdk/tables/azure-data-tables/src/test/resources/session-records/TableServiceAsyncClientTest.generateAccountSasTokenWithMinimumParameters.json b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableServiceAsyncClientTest.generateAccountSasTokenWithMinimumParameters.json new file mode 100644 index 000000000000..ba5f37f8f855 --- /dev/null +++ b/sdk/tables/azure-data-tables/src/test/resources/session-records/TableServiceAsyncClientTest.generateAccountSasTokenWithMinimumParameters.json @@ -0,0 +1,4 @@ +{ + "networkCallRecords" : [ ], + "variables" : [ ] +} \ No newline at end of file