Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for generating SAS tokens at the account and Table service level. #21944

Merged
merged 25 commits into from
Jun 4, 2021
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e87fe2d
Added support for generating SAS tokens at the Account and Table Serv…
vcolin7 May 29, 2021
d4188f1
Added partition key and row key values for SAS generation.
vcolin7 May 29, 2021
0872d7e
Fixed CheckStyle issues.
vcolin7 May 29, 2021
5e401ed
Fixed SpotBugs issue.
vcolin7 May 29, 2021
aa9ec31
Removed more unused imports.
vcolin7 May 29, 2021
4144ec2
Renamed classes used for generating table-level SAS tokens. Made clie…
vcolin7 Jun 2, 2021
4e85675
Made client builders throw an IllegalStateException if more than one …
vcolin7 Jun 2, 2021
fa7eb98
Changed module-info.java to export the tables package to all other pa…
vcolin7 Jun 2, 2021
159c76a
Added tests for SAS models.
vcolin7 Jun 2, 2021
7578f7c
Added builder tests for when multiple forms of authentication are set.
vcolin7 Jun 2, 2021
a0dc7bc
Updated builders to throw when no endpoint or form of authentication …
vcolin7 Jun 2, 2021
385656a
Fixed CheckStyle issues.
vcolin7 Jun 2, 2021
62ec3ce
Fixed test name.
vcolin7 Jun 2, 2021
475bd88
Removed unnecessary exports for implementation packages in module-inf…
vcolin7 Jun 2, 2021
cd25daa
Applied PR feedback:
vcolin7 Jun 4, 2021
0d9820d
Added tests and renamed test classes to match clients and builders.
vcolin7 Jun 4, 2021
08e581d
Updated CHANGELOG and client builders' JavaDoc.
vcolin7 Jun 4, 2021
fc1cdf3
Applied APIView feedback.
vcolin7 Jun 4, 2021
893da5f
Updated CHANGELOG again.
vcolin7 Jun 4, 2021
d52aa73
Removed unused imports. Simplified SAS token comparison logic.
vcolin7 Jun 4, 2021
594bac2
Fixed SAS token generation at the table level. Re-ordered query param…
vcolin7 Jun 4, 2021
91786bb
Updated CHANGELOG.
vcolin7 Jun 4, 2021
44d20a2
Fixed test and CheckStyle issues.
vcolin7 Jun 4, 2021
096a492
Added @Immutable and @Fluent annotations where appropriate. Made more…
vcolin7 Jun 4, 2021
576b106
Added more @Immutable annotations.
vcolin7 Jun 4, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions sdk/tables/azure-data-tables/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -22,6 +36,7 @@
- `updateEntity(TableEntity entity, TableEntityUpdateMode updateMode,
boolean ifUnchanged)`
- `getEntity(String partitionKey, String rowKey, List<String> 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)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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<HttpPipelinePolicy> 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 a builder's 'endpoint()'"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
new IllegalStateException("An 'endpoint' is required to create a client. Use a builder's 'endpoint()'"
new IllegalStateException("An 'endpoint' is required to create a client. Use builder's 'endpoint()'"

+ " or 'connectionString()' methods to set this value."));
} else if (endpoint.contains(COSMOS_ENDPOINT_SUFFIX)) {
policies.add(new CosmosPatchTransformPolicy());
}

Expand All @@ -82,20 +87,22 @@ static HttpPipeline buildPipeline(
policies.add(retryPolicy);

policies.add(new AddDatePolicy());

HttpPipelinePolicy credentialPolicy;

if (azureNamedKeyCredential != null) {
credentialPolicy = new TableAzureNamedKeyCredentialPolicy(azureNamedKeyCredential);
} else if (azureSasCredential != null) {
credentialPolicy = new AzureSasCredentialPolicy(azureSasCredential, false);
} 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 "
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
new IllegalStateException("A form of authentication is required to create a client. Use a builder's "
new IllegalStateException("A form of authentication is required to create a client. Use 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);
Expand All @@ -121,17 +128,56 @@ static HttpPipeline buildNullClientPipeline() {
.build();
}

private static void validateSingleCredentialIsPresent(AzureNamedKeyCredential azureNamedKeyCredential,
AzureSasCredential azureSasCredential, String sasToken,
ClientLogger logger) {
List<Object> 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<Object> 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 + "."));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -195,6 +199,30 @@ public TableServiceVersion getServiceVersion() {
return TableServiceVersion.fromString(tablesImplementation.getVersion());
}

/**
* Generates a service SAS for the table using the specified {@link TableSasSignatureValues}.
*
* <p>Note : The client must be authenticated via {@link AzureNamedKeyCredential}
vcolin7 marked this conversation as resolved.
Show resolved Hide resolved
* <p>See {@link TableSasSignatureValues} for more information on how to construct a service SAS.</p>
*
* @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.
*
Expand Down Expand Up @@ -821,11 +849,11 @@ <T extends TableEntity> Mono<Response<T>> getEntityWithResponse(String partition
* {@link TableSignedIdentifier access policies}.
*/
@ServiceMethod(returns = ReturnType.COLLECTION)
public PagedFlux<TableSignedIdentifier> getAccessPolicy() {
return (PagedFlux<TableSignedIdentifier>) fluxContext(this::getAccessPolicy);
public PagedFlux<TableSignedIdentifier> listAccessPolicies() {
return (PagedFlux<TableSignedIdentifier>) fluxContext(this::listAccessPolicies);
}

PagedFlux<TableSignedIdentifier> getAccessPolicy(Context context) {
PagedFlux<TableSignedIdentifier> listAccessPolicies(Context context) {
context = context == null ? Context.NONE : context;

try {
Expand Down Expand Up @@ -870,8 +898,8 @@ private TableAccessPolicy toTableAccessPolicy(AccessPolicy accessPolicy) {
* @return An empty reactive result.
*/
@ServiceMethod(returns = ReturnType.SINGLE)
public Mono<Void> setAccessPolicy(List<TableSignedIdentifier> tableSignedIdentifiers) {
return this.setAccessPolicyWithResponse(tableSignedIdentifiers).flatMap(FluxUtil::toMono);
public Mono<Void> setAccessPolicies(List<TableSignedIdentifier> tableSignedIdentifiers) {
return this.setAccessPoliciesWithResponse(tableSignedIdentifiers).flatMap(FluxUtil::toMono);
}

/**
Expand All @@ -883,12 +911,12 @@ public Mono<Void> setAccessPolicy(List<TableSignedIdentifier> tableSignedIdentif
* @return A reactive result containing the HTTP response.
*/
@ServiceMethod(returns = ReturnType.SINGLE)
public Mono<Response<Void>> setAccessPolicyWithResponse(List<TableSignedIdentifier> tableSignedIdentifiers) {
return withContext(context -> this.setAccessPolicyWithResponse(tableSignedIdentifiers, context));
public Mono<Response<Void>> setAccessPoliciesWithResponse(List<TableSignedIdentifier> tableSignedIdentifiers) {
return withContext(context -> this.setAccessPoliciesWithResponse(tableSignedIdentifiers, context));
}

Mono<Response<Void>> setAccessPolicyWithResponse(List<TableSignedIdentifier> tableSignedIdentifiers,
Context context) {
Mono<Response<Void>> setAccessPoliciesWithResponse(List<TableSignedIdentifier> tableSignedIdentifiers,
Context context) {
context = context == null ? Context.NONE : context;

try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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<HttpResponse> 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<String, String> 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);
}
Expand All @@ -68,6 +71,7 @@ String generateAuthorizationHeader(URL requestUrl, Map<String, String> 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<String, String> headers) {
Expand All @@ -87,6 +91,7 @@ private String buildStringToSign(URL requestUrl, Map<String, String> 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<String, String> headers, String headerName) {
Expand All @@ -100,6 +105,7 @@ private String getStandardHeaderValue(Map<String, String> 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) {
Expand Down Expand Up @@ -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;
}
}
Loading