[JCLOUDS-1428] Support for SAS token based Authentication for Azure Blob Storage (#1270)

This commit is contained in:
Aliaksandra Kharushka 2019-02-27 12:20:22 +01:00 committed by Ignasi Barrera
parent b3aa23bb05
commit 0ce926108e
5 changed files with 240 additions and 31 deletions

View File

@ -21,10 +21,13 @@ import static com.google.common.io.ByteStreams.readBytes;
import static org.jclouds.crypto.Macs.asByteProcessor;
import static org.jclouds.util.Patterns.NEWLINE_PATTERN;
import static org.jclouds.util.Strings2.toInputStream;
import org.jclouds.http.Uris.UriBuilder;
import java.util.Collection;
import java.util.Map;
import java.util.Map.Entry;
import org.jclouds.http.Uris;
import java.net.URI;
import javax.annotation.Resource;
import javax.inject.Inject;
@ -67,12 +70,15 @@ import com.google.common.net.HttpHeaders;
@Singleton
public class SharedKeyLiteAuthentication implements HttpRequestFilter {
private static final Collection<String> FIRST_HEADERS_TO_SIGN = ImmutableList.of(HttpHeaders.DATE);
private final SignatureWire signatureWire;
private final Supplier<Credentials> creds;
private final Provider<String> timeStampProvider;
private final Crypto crypto;
private final String credential;
private final String identity;
private final HttpUtils utils;
private final URI storageUrl;
private final boolean isSAS;
@Resource
@Named(Constants.LOGGER_SIGNATURE)
@ -81,27 +87,73 @@ public class SharedKeyLiteAuthentication implements HttpRequestFilter {
@Inject
public SharedKeyLiteAuthentication(SignatureWire signatureWire,
@org.jclouds.location.Provider Supplier<Credentials> creds, @TimeStamp Provider<String> timeStampProvider,
Crypto crypto, HttpUtils utils) {
Crypto crypto, HttpUtils utils, @Named("sasAuth") boolean sasAuthentication) {
this.crypto = crypto;
this.utils = utils;
this.signatureWire = signatureWire;
this.storageUrl = URI.create("https://" + creds.get().identity + ".blob.core.windows.net/");
this.creds = creds;
this.identity = creds.get().identity;
this.credential = creds.get().credential;
this.timeStampProvider = timeStampProvider;
this.isSAS = sasAuthentication;
}
/**
* this is an updated filter method, which decides whether the SAS or SharedKeyLite
* is used and applies the right filtering.
*/
public HttpRequest filter(HttpRequest request) throws HttpException {
request = replaceDateHeader(request);
String signature = calculateSignature(createStringToSign(request));
request = replaceAuthorizationHeader(request, signature);
request = this.isSAS ? filterSAS(request, this.credential) : filterKey(request);
utils.logRequest(signatureLog, request, "<<");
return request;
}
/**
* this filter method is applied only for the cases with SAS Authentication.
*/
public HttpRequest filterSAS(HttpRequest request, String credential) throws HttpException, IllegalArgumentException {
URI requestUri = request.getEndpoint();
String formattedCredential = credential.startsWith("?") ? credential.substring(1) : credential;
String initialQuery = requestUri.getQuery();
String finalQuery = initialQuery == null ? formattedCredential : initialQuery + "&" + formattedCredential;
String[] parametersArray = cutUri(requestUri);
String containerName = parametersArray[1];
UriBuilder endpoint = Uris.uriBuilder(storageUrl).appendPath(containerName);
if (parametersArray.length == 3) {
endpoint.appendPath(parametersArray[2]).query(finalQuery);
} else {
endpoint.query("restype=container&" + finalQuery);
}
return removeAuthorizationHeader(
replaceDateHeader(request.toBuilder()
.endpoint(endpoint.build())
.build()));
}
/**
* this is a 'standard' filter method, applied when SharedKeyLite authentication is used.
*/
public HttpRequest filterKey(HttpRequest request) throws HttpException {
request = replaceDateHeader(request);
String signature = calculateSignature(createStringToSign(request));
return replaceAuthorizationHeader(request, signature);
}
HttpRequest replaceAuthorizationHeader(HttpRequest request, String signature) {
return request.toBuilder()
.replaceHeader(HttpHeaders.AUTHORIZATION, "SharedKeyLite " + creds.get().identity + ":" + signature)
.build();
}
/**
* this method removes Authorisation header, since it is not needed for SAS Authentication
*/
HttpRequest removeAuthorizationHeader(HttpRequest request) {
return request.toBuilder()
.removeHeader(HttpHeaders.AUTHORIZATION)
.build();
}
HttpRequest replaceDateHeader(HttpRequest request) {
Builder<String, String> builder = ImmutableMap.builder();
@ -110,6 +162,18 @@ public class SharedKeyLiteAuthentication implements HttpRequestFilter {
request = request.toBuilder().replaceHeaders(Multimaps.forMap(builder.build())).build();
return request;
}
/**
* this is the method to parse container name and blob name from the HttpRequest.
*/
public String[] cutUri(URI uri) throws IllegalArgumentException {
String path = uri.getPath();
String[] result = path.split("/");
if (result.length < 2) {
throw new IllegalArgumentException("there is neither ContainerName nor BlobName in the URI path");
}
return result;
}
public String createStringToSign(HttpRequest request) {
utils.logRequest(signatureLog, request, ">>");

View File

@ -23,6 +23,7 @@ import java.util.Date;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import javax.inject.Singleton;
import javax.inject.Named;
import org.jclouds.azure.storage.filters.SharedKeyLiteAuthentication;
import org.jclouds.blobstore.BlobRequestSigner;
@ -35,11 +36,11 @@ import org.jclouds.http.HttpRequest;
import org.jclouds.http.Uris;
import org.jclouds.http.options.GetOptions;
import org.jclouds.javax.annotation.Nullable;
import com.google.common.base.Supplier;
import com.google.common.net.HttpHeaders;
import com.google.inject.Provider;
@Singleton
public class AzureBlobRequestSigner implements BlobRequestSigner {
private static final int DEFAULT_EXPIRY_SECONDS = 15 * 60;
@ -51,19 +52,23 @@ public class AzureBlobRequestSigner implements BlobRequestSigner {
private final Provider<String> timeStampProvider;
private final DateService dateService;
private final SharedKeyLiteAuthentication auth;
private final String credential;
private final boolean isSAS;
@Inject
public AzureBlobRequestSigner(
BlobToHttpGetOptions blob2HttpGetOptions, @TimeStamp Provider<String> timeStampProvider,
DateService dateService, SharedKeyLiteAuthentication auth,
@org.jclouds.location.Provider Supplier<Credentials> creds)
@org.jclouds.location.Provider Supplier<Credentials> creds, @Named("sasAuth") boolean sasAuthentication)
throws SecurityException, NoSuchMethodException {
this.identity = creds.get().identity;
this.credential = creds.get().credential;
this.storageUrl = URI.create("https://" + creds.get().identity + ".blob.core.windows.net/");
this.blob2HttpGetOptions = checkNotNull(blob2HttpGetOptions, "blob2HttpGetOptions");
this.timeStampProvider = checkNotNull(timeStampProvider, "timeStampProvider");
this.dateService = checkNotNull(dateService, "dateService");
this.auth = auth;
this.isSAS = sasAuthentication;
}
@Override
@ -107,12 +112,14 @@ public class AzureBlobRequestSigner implements BlobRequestSigner {
return sign("GET", container, name, blob2HttpGetOptions.apply(checkNotNull(options, "options")),
DEFAULT_EXPIRY_SECONDS, null, null);
}
private HttpRequest sign(String method, String container, String name, @Nullable GetOptions options, long expires, @Nullable Long contentLength, @Nullable String contentType) {
/**
* method to sign HttpRequest when SharedKey Authentication is used
*/
private HttpRequest signKey(String method, String container, String name, @Nullable GetOptions options, long expires, @Nullable Long contentLength, @Nullable String contentType) {
checkNotNull(method, "method");
checkNotNull(container, "container");
checkNotNull(name, "name");
String nowString = timeStampProvider.get();
Date now = dateService.rfc1123DateParse(nowString);
Date expiration = new Date(now.getTime() + TimeUnit.SECONDS.toMillis(expires));
@ -125,7 +132,6 @@ public class AzureBlobRequestSigner implements BlobRequestSigner {
} else {
signedPermission = "r";
}
HttpRequest.Builder request = HttpRequest.builder()
.method(method)
.endpoint(Uris.uriBuilder(storageUrl).appendPath(container).appendPath(name).build())
@ -134,23 +140,7 @@ public class AzureBlobRequestSigner implements BlobRequestSigner {
.addQueryParam("se", iso8601)
.addQueryParam("sr", "b") // blob resource
.addQueryParam("sp", signedPermission); // permission
if (contentLength != null) {
request.replaceHeader(HttpHeaders.CONTENT_LENGTH, contentLength.toString());
}
if (contentType != null) {
request.replaceHeader("x-ms-blob-content-type", contentType);
}
if (options != null) {
request.headers(options.buildRequestHeaders());
}
if (method.equals("PUT")) {
request.replaceHeader("x-ms-blob-type", "BlockBlob");
}
request = setHeaders(request, method, options, contentLength, contentType);
String stringToSign =
signedPermission + "\n" + // signedpermission
"\n" + // signedstart
@ -165,9 +155,50 @@ public class AzureBlobRequestSigner implements BlobRequestSigner {
"\n" + // rsce
"\n" + // rscl
""; // rsct
String signature = auth.calculateSignature(stringToSign);
request.addQueryParam("sig", signature);
return request.build();
}
private HttpRequest.Builder setHeaders(HttpRequest.Builder request, String method, @Nullable GetOptions options, @Nullable Long contentLength, @Nullable String contentType){
if (contentLength != null) {
request.replaceHeader(HttpHeaders.CONTENT_LENGTH, contentLength.toString());
}
if (contentType != null) {
request.replaceHeader("x-ms-blob-content-type", contentType);
}
if (options != null) {
request.headers(options.buildRequestHeaders());
}
if (method.equals("PUT")) {
request.replaceHeader("x-ms-blob-type", "BlockBlob");
}
return request;
}
/**
* method, compatible with SAS Authentication
*/
private HttpRequest signSAS(String method, String container, String name, @Nullable GetOptions options, long expires, @Nullable Long contentLength, @Nullable String contentType) {
checkNotNull(method, "method");
checkNotNull(container, "container");
checkNotNull(name, "name");
String nowString = timeStampProvider.get();
HttpRequest.Builder request = HttpRequest.builder()
.method(method)
.endpoint(Uris.uriBuilder(storageUrl).appendPath(container).appendPath(name).query(this.credential).build())
.replaceHeader(HttpHeaders.DATE, nowString);
request = setHeaders(request, method, options, contentLength, contentType);
return request.build();
}
/**
* modified sign() method, which acts depending on the Auth input.
*/
public HttpRequest sign(String method, String container, String name, @Nullable GetOptions options, long expires, @Nullable Long contentLength, @Nullable String contentType) {
if (isSAS) {
return signSAS(method, container, name, options, expires, contentLength, contentType);
}
return signKey(method, container, name, options, expires, contentLength, contentType);
}
}

View File

@ -18,7 +18,15 @@ package org.jclouds.azureblob.config;
import static org.jclouds.Constants.PROPERTY_SESSION_INTERVAL;
import static com.google.common.base.Predicates.in;
import static com.google.common.collect.Iterables.all;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import java.util.concurrent.TimeUnit;
import java.util.Map;
import java.util.List;
import javax.inject.Named;
@ -36,6 +44,7 @@ import org.jclouds.json.config.GsonModule.DateAdapter;
import org.jclouds.json.config.GsonModule.Iso8601DateAdapter;
import org.jclouds.rest.ConfiguresHttpApi;
import org.jclouds.rest.config.HttpApiModule;
import org.jclouds.domain.Credentials;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
@ -63,6 +72,23 @@ public class AzureBlobHttpApiModule extends HttpApiModule<AzureBlobClient> {
protected String provideTimeStamp(@TimeStamp Supplier<String> cache) {
return cache.get();
}
/**
* checks which Authentication type is used
*/
@Named("sasAuth")
@Provides
protected boolean authSAS(@org.jclouds.location.Provider Supplier<Credentials> creds) {
String credential = creds.get().credential;
String formattedCredential = credential.startsWith("?") ? credential.substring(1) : credential;
List<String> required = ImmutableList.of("sv", "se", "sig", "sp");
try {
Map<String, String> tokens = Splitter.on('&').withKeyValueSeparator('=').split(formattedCredential);
return all(required, in(tokens.keySet()));
} catch (Exception ex) {
return false;
}
}
/**
* borrowing concurrency code to ensure that caching takes place properly

View File

@ -42,6 +42,8 @@ public class SharedKeyLiteAuthenticationTest {
private static final String ACCOUNT = "foo";
private Injector injector;
private SharedKeyLiteAuthentication filter;
private SharedKeyLiteAuthentication filterSAS;
private SharedKeyLiteAuthentication filterSASQuestionMark;
@DataProvider(parallel = true)
public Object[][] dataProvider() {
@ -52,6 +54,19 @@ public class SharedKeyLiteAuthenticationTest {
+ ".blob.core.windows.net/movies/MOV1.avi?comp=blocklist&timeout=120").build() },
{ HttpRequest.builder().method(HttpMethod.GET).endpoint("http://" + ACCOUNT + ".blob.core.windows.net/movies/MOV1.avi").build() } };
}
@DataProvider(name = "auth-sas-data", parallel = true)
public Object[][] requests(){
return new Object[][]{
{ HttpRequest.builder().method(HttpMethod.PUT).endpoint("https://" + ACCOUNT
+ ".blob.core.windows.net/movies/MOV1.avi?comp=block&blockid=BlockId1&timeout=60").build(), filterSAS, "https://foo.blob.core.windows.net/movies/MOV1.avi?comp=block&blockid=BlockId1&timeout=60&sv=2018-03-28&ss=b&srt=sco&sp=rwdlac&se=2019-02-13T17%3A18%3A22Z&st=2019-02-13T09%3A18%3A22Z&spr=https&sig=sMnaKSD94CzEPeGnWauTT0wBNIn%2B4ySkZO5PEAW7zs%3D"},
{ HttpRequest.builder().method(HttpMethod.PUT).endpoint("https://" + ACCOUNT
+ ".blob.core.windows.net/movies/MOV1.avi?comp=blocklist&timeout=120").build(), filterSAS, "https://foo.blob.core.windows.net/movies/MOV1.avi?comp=blocklist&timeout=120&sv=2018-03-28&ss=b&srt=sco&sp=rwdlac&se=2019-02-13T17%3A18%3A22Z&st=2019-02-13T09%3A18%3A22Z&spr=https&sig=sMnaKSD94CzEPeGnWauTT0wBNIn%2B4ySkZO5PEAW7zs%3D" },
{ HttpRequest.builder().method(HttpMethod.GET).endpoint("https://" + ACCOUNT
+ ".blob.core.windows.net/movies/MOV1.avi").build(), filterSAS, "https://foo.blob.core.windows.net/movies/MOV1.avi?sv=2018-03-28&ss=b&srt=sco&sp=rwdlac&se=2019-02-13T17%3A18%3A22Z&st=2019-02-13T09%3A18%3A22Z&spr=https&sig=sMnaKSD94CzEPeGnWauTT0wBNIn%2B4ySkZO5PEAW7zs%3D" },
{ HttpRequest.builder().method(HttpMethod.GET).endpoint("https://" + ACCOUNT
+ ".blob.core.windows.net/movies/MOV1.avi").build(), filterSASQuestionMark, "https://foo.blob.core.windows.net/movies/MOV1.avi?sv=2018-03-28&ss=b&srt=sco&sp=rwdlac&se=2019-02-13T17%3A18%3A22Z&st=2019-02-13T09%3A18%3A22Z&spr=https&sig=sMnaKSD94CzEPeGnWauTT0wBNIn%2B4ySkZO5PEAW7zs%3D" } };
}
/**
* NOTE this test is dependent on how frequently the timestamp updates. At
@ -74,6 +89,15 @@ public class SharedKeyLiteAuthenticationTest {
System.out.printf("%s: %d iterations before the timestamp updated %n", Thread.currentThread().getName(),
iterations);
}
/**
* this test is similar to testIdempotent; it checks whether request is properly filtered when it comes to SAS Authentication
*/
@Test(dataProvider = "auth-sas-data")
void testFilter(HttpRequest request, SharedKeyLiteAuthentication filter, String expected) {
request = filter.filter(request);
assertEquals(request.getEndpoint().toString(), expected);
}
@Test
void testAclQueryStringRoot() {
@ -127,5 +151,19 @@ public class SharedKeyLiteAuthenticationTest {
.modules(ImmutableSet.<Module> of(new MockModule(), new NullLoggingModule()))
.buildInjector();
filter = injector.getInstance(SharedKeyLiteAuthentication.class);
injector = ContextBuilder
.newBuilder("azureblob")
.endpoint("https://${jclouds.identity}.blob.core.windows.net")
.credentials(ACCOUNT, "sv=2018-03-28&ss=b&srt=sco&sp=rwdlac&se=2019-02-13T17:18:22Z&st=2019-02-13T09:18:22Z&spr=https&sig=sMnaKSD94CzEPeGnWauTT0wBNIn%2B4ySkZO5PEAW7zs%3D")
.modules(ImmutableSet.<Module> of(new MockModule(), new NullLoggingModule()))
.buildInjector();
filterSAS = injector.getInstance(SharedKeyLiteAuthentication.class);
injector = ContextBuilder
.newBuilder("azureblob")
.endpoint("https://${jclouds.identity}.blob.core.windows.net")
.credentials(ACCOUNT, "?sv=2018-03-28&ss=b&srt=sco&sp=rwdlac&se=2019-02-13T17:18:22Z&st=2019-02-13T09:18:22Z&spr=https&sig=sMnaKSD94CzEPeGnWauTT0wBNIn%2B4ySkZO5PEAW7zs%3D")
.modules(ImmutableSet.<Module> of(new MockModule(), new NullLoggingModule()))
.buildInjector();
filterSASQuestionMark = injector.getInstance(SharedKeyLiteAuthentication.class);
}
}

View File

@ -0,0 +1,50 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jclouds.azureblob.config;
import static org.testng.Assert.assertEquals;
import org.testng.annotations.Test;
import com.google.common.base.Suppliers;
import org.jclouds.domain.Credentials;
import org.testng.annotations.DataProvider;
@Test(groups = "unit", testName = "AzureBlobHttpApiModuleTest")
public class AzureBlobHttpApiModuleTest {
@DataProvider(name = "auth-sas-tokens")
public static Object[][] tokens() {
return new Object[][]{
{false, "sv=2018-03-28&se=2019-02-14T11:12:13Z"},
{false, "sv=2018-03-28&se=2019-02-14T11:12:13Z&sp=abc&st=2019-01-20T11:12:13Z"},
{false, "u2iAP01ARTewyK/MhOM1d1ASPpjqclkldsdkljfas2kfjkh895ssfslkjpXKfhg=="},
{false, "sadf;gjkhflgjkhfdlkfdljghskldjghlfdghw4986754ltjkghdlfkjghst;lyho56[09y7poinh"},
{false, "a=apple&b=banana&c=cucumber&d=diet"},
{false, "sva=swajak&sta=stancyja&spa=spakoj&sea=mora&sig=podpis"},
{true, "sv=2018-03-28&ss=b&srt=sco&sp=r&se=2019-02-13T17:03:09Z&st=2019-02-13T09:03:09Z&spr=https&sig=wNkWK%2GURTjHWhtqG6Q2Gu%2Qu%3FPukW6N4%2FIH4Mr%2F%2FO42M%3D"},
{true, "sp=rl&st=2019-02-14T08:50:26Z&se=2019-02-15T08:50:26Z&sv=2018-03-28&sig=Ukow8%2GtpQpAiVZBLcWp1%2RSpFq928MAqzp%2BdrdregaB6%3D&sr=b"},
{false, ""}
};
}
@Test(dataProvider = "auth-sas-tokens")
void testAuthSasNonSufficientParametersSvSe(boolean expected, String credential){
AzureBlobHttpApiModule module = new AzureBlobHttpApiModule();
Credentials creds = new Credentials("identity", credential);
assertEquals(module.authSAS(Suppliers.ofInstance(creds)), expected);
}
}