From 25ce398a44babec06b3faadfd12c0208765c345b Mon Sep 17 00:00:00 2001 From: adriancole Date: Mon, 21 Jan 2013 10:07:44 -0800 Subject: [PATCH] added AWS temporary security credentials and integrated with FormSigner --- .../aws/domain/TemporaryCredentials.java | 177 ++++++++++++++++++ .../org/jclouds/aws/filters/FormSigner.java | 9 + .../aws/xml/TemporaryCredentialsHandler.java | 83 ++++++++ .../jclouds/aws/filters/FormSignerTest.java | 84 +++++---- .../org/jclouds/aws/util/AWSUtilsTest.java | 3 +- .../xml/TemporaryCredentialsHandlerTest.java | 61 ++++++ common/aws/src/test/resources/credentials.xml | 6 + 7 files changed, 389 insertions(+), 34 deletions(-) create mode 100644 common/aws/src/main/java/org/jclouds/aws/domain/TemporaryCredentials.java create mode 100644 common/aws/src/main/java/org/jclouds/aws/xml/TemporaryCredentialsHandler.java create mode 100644 common/aws/src/test/java/org/jclouds/aws/xml/TemporaryCredentialsHandlerTest.java create mode 100644 common/aws/src/test/resources/credentials.xml diff --git a/common/aws/src/main/java/org/jclouds/aws/domain/TemporaryCredentials.java b/common/aws/src/main/java/org/jclouds/aws/domain/TemporaryCredentials.java new file mode 100644 index 0000000000..73ff43b149 --- /dev/null +++ b/common/aws/src/main/java/org/jclouds/aws/domain/TemporaryCredentials.java @@ -0,0 +1,177 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.aws.domain; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.Date; + +import org.jclouds.domain.Credentials; + +import com.google.common.base.Objects; + +/** + * AWS credentials for API authentication. + * + * @see + * + * @author Adrian Cole + */ +public final class TemporaryCredentials extends Credentials { + public static Builder builder() { + return new Builder(); + } + + public Builder toBuilder() { + return builder().from(this); + } + + public final static class Builder extends Credentials.Builder { + private String accessKeyId; + private String secretAccessKey; + private String sessionToken; + private Date expiration; + + @Override + public Builder identity(String identity) { + return accessKeyId(identity); + } + + @Override + public Builder credential(String credential) { + return secretAccessKey(credential); + } + + /** + * @see TemporaryCredentials#getAccessKeyId() + */ + public Builder accessKeyId(String accessKeyId) { + this.accessKeyId = accessKeyId; + return this; + } + + /** + * @see TemporaryCredentials#getSecretAccessKey() + */ + public Builder secretAccessKey(String secretAccessKey) { + this.secretAccessKey = secretAccessKey; + return this; + } + + /** + * @see TemporaryCredentials#getSessionToken() + */ + public Builder sessionToken(String sessionToken) { + this.sessionToken = sessionToken; + return this; + } + + /** + * @see TemporaryCredentials#getExpiration() + */ + public Builder expiration(Date expiration) { + this.expiration = expiration; + return this; + } + + public TemporaryCredentials build() { + return new TemporaryCredentials(accessKeyId, secretAccessKey, sessionToken, expiration); + } + + public Builder from(TemporaryCredentials in) { + return this.accessKeyId(in.identity).secretAccessKey(in.credential).sessionToken(in.sessionToken) + .expiration(in.expiration); + } + } + + private final String sessionToken; + private final Date expiration; + + private TemporaryCredentials(String accessKeyId, String secretAccessKey, String sessionToken, Date expiration) { + super(checkNotNull(accessKeyId, "accessKeyId"), checkNotNull(secretAccessKey, "secretAccessKey for %s", + accessKeyId)); + this.sessionToken = checkNotNull(sessionToken, "sessionToken for %s", accessKeyId); + this.expiration = checkNotNull(expiration, "expiration for %s", accessKeyId); + } + + /** + * AccessKeyId ID that identifies the temporary credentials. + */ + public String getAccessKeyId() { + return identity; + } + + /** + * The Secret Access Key to sign requests. + */ + public String getSecretAccessKey() { + return credential; + } + + /** + * The security token that users must pass to the service API to use the + * temporary credentials. + */ + public String getSessionToken() { + return sessionToken; + } + + /** + * The date on which these credentials expire. + */ + public Date getExpiration() { + return expiration; + } + + /** + * {@inheritDoc} + */ + @Override + public int hashCode() { + return Objects.hashCode(identity, credential, sessionToken, expiration); + } + + /** + * {@inheritDoc} + */ + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + TemporaryCredentials other = (TemporaryCredentials) obj; + return Objects.equal(this.identity, other.identity) && Objects.equal(this.credential, other.credential) + && Objects.equal(this.sessionToken, other.sessionToken) && Objects.equal(this.expiration, other.expiration); + } + + /** + * {@inheritDoc} + */ + @Override + public String toString() { + return Objects.toStringHelper(this).add("accessKeyId", identity).add("sessionToken", sessionToken) + .add("expiration", expiration).toString(); + } + +} diff --git a/common/aws/src/main/java/org/jclouds/aws/filters/FormSigner.java b/common/aws/src/main/java/org/jclouds/aws/filters/FormSigner.java index f957b9b6a6..76e9b716ce 100644 --- a/common/aws/src/main/java/org/jclouds/aws/filters/FormSigner.java +++ b/common/aws/src/main/java/org/jclouds/aws/filters/FormSigner.java @@ -47,6 +47,7 @@ import javax.inject.Singleton; import javax.ws.rs.core.HttpHeaders; import org.jclouds.Constants; +import org.jclouds.aws.domain.TemporaryCredentials; import org.jclouds.crypto.Crypto; import org.jclouds.date.TimeStamp; import org.jclouds.domain.Credentials; @@ -114,9 +115,17 @@ public class FormSigner implements HttpRequestFilter, RequestSigner { String signature = sign(stringToSign); addSignature(decodedParams, signature); request = setPayload(request, decodedParams); + Credentials current = creds.get(); + if (current instanceof TemporaryCredentials) { + request = replaceSecurityTokenHeader(request, TemporaryCredentials.class.cast(current)); + } utils.logRequest(signatureLog, request, "<<"); return request; } + + HttpRequest replaceSecurityTokenHeader(HttpRequest request, TemporaryCredentials current) { + return request.toBuilder().replaceHeader("SecurityToken", current.getSessionToken()).build(); + } HttpRequest setPayload(HttpRequest request, Multimap decodedParams) { String queryLine = buildQueryLine(decodedParams); diff --git a/common/aws/src/main/java/org/jclouds/aws/xml/TemporaryCredentialsHandler.java b/common/aws/src/main/java/org/jclouds/aws/xml/TemporaryCredentialsHandler.java new file mode 100644 index 0000000000..65f9e7eb4e --- /dev/null +++ b/common/aws/src/main/java/org/jclouds/aws/xml/TemporaryCredentialsHandler.java @@ -0,0 +1,83 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.aws.xml; + +import javax.inject.Inject; + +import org.jclouds.aws.domain.TemporaryCredentials; +import org.jclouds.date.DateService; +import org.jclouds.http.functions.ParseSax; +import org.jclouds.util.SaxUtils; + +/** + * @see + * + * @author Adrian Cole + */ +public class TemporaryCredentialsHandler extends ParseSax.HandlerForGeneratedRequestWithResult { + private final DateService dateService; + + @Inject + protected TemporaryCredentialsHandler(DateService dateService) { + this.dateService = dateService; + } + + private StringBuilder currentText = new StringBuilder(); + private TemporaryCredentials.Builder builder = TemporaryCredentials.builder(); + + /** + * {@inheritDoc} + */ + @Override + public TemporaryCredentials getResult() { + try { + return builder.build(); + } finally { + builder = TemporaryCredentials.builder(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void endElement(String uri, String name, String qName) { + if (qName.equals("AccessKeyId")) { + builder.accessKeyId(SaxUtils.currentOrNull(currentText)); + } else if (qName.equals("SecretAccessKey")) { + builder.secretAccessKey(SaxUtils.currentOrNull(currentText)); + } else if (qName.equals("SessionToken")) { + builder.sessionToken(SaxUtils.currentOrNull(currentText)); + } else if (qName.equals("Expiration")) { + builder.expiration(dateService.iso8601DateParse(SaxUtils.currentOrNull(currentText))); + } + currentText = new StringBuilder(); + } + + /** + * {@inheritDoc} + */ + @Override + public void characters(char ch[], int start, int length) { + currentText.append(ch, start, length); + } + +} diff --git a/common/aws/src/test/java/org/jclouds/aws/filters/FormSignerTest.java b/common/aws/src/test/java/org/jclouds/aws/filters/FormSignerTest.java index 6f7d551470..75a620e694 100644 --- a/common/aws/src/test/java/org/jclouds/aws/filters/FormSignerTest.java +++ b/common/aws/src/test/java/org/jclouds/aws/filters/FormSignerTest.java @@ -17,13 +17,16 @@ * under the License. */ package org.jclouds.aws.filters; + import static org.jclouds.aws.reference.AWSConstants.PROPERTY_HEADER_TAG; import static org.testng.Assert.assertEquals; import javax.ws.rs.core.HttpHeaders; import org.jclouds.ContextBuilder; +import org.jclouds.aws.xml.TemporaryCredentialsHandlerTest; import org.jclouds.date.TimeStamp; +import org.jclouds.domain.Credentials; import org.jclouds.http.HttpRequest; import org.jclouds.http.IntegrationTestAsyncClient; import org.jclouds.http.IntegrationTestClient; @@ -33,6 +36,7 @@ import org.jclouds.rest.RequestSigner; import org.jclouds.rest.internal.BaseRestApiTest.MockModule; import org.testng.annotations.Test; +import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMultimap; import com.google.inject.AbstractModule; @@ -45,49 +49,63 @@ import com.google.inject.name.Names; * * @author Adrian Cole */ -// NOTE:without testName, this will not call @Before* and fail w/NPE during surefire -@Test(groups = "unit", testName = "FormSignerTest") +// NOTE:without testName, this will not call @Before* and fail w/NPE during +// surefire +@Test(groups = "unit", singleThreaded = true, testName = "FormSignerTest") public class FormSignerTest { - public static final Injector INJECTOR = ContextBuilder - .newBuilder( - AnonymousProviderMetadata.forClientMappedToAsyncClientOnEndpoint(IntegrationTestClient.class, IntegrationTestAsyncClient.class, - "http://localhost")) - .credentials("identity", "credential") - .apiVersion("apiVersion") - .modules(ImmutableList. of(new MockModule(), new NullLoggingModule(), - new AbstractModule() { - @Override - protected void configure() { - bind(RequestSigner.class).to(FormSigner.class); - bind(String.class).annotatedWith(Names.named(PROPERTY_HEADER_TAG)).toInstance("amz"); - bind(String.class).annotatedWith(TimeStamp.class).toInstance("2009-11-08T15:54:08.897Z"); - } + public static Injector injector(Credentials creds) { + return ContextBuilder + .newBuilder( + AnonymousProviderMetadata.forClientMappedToAsyncClientOnEndpoint(IntegrationTestClient.class, + IntegrationTestAsyncClient.class, "http://localhost")) + .credentialsSupplier(Suppliers. ofInstance(creds)).apiVersion("apiVersion") + .modules(ImmutableList. of(new MockModule(), new NullLoggingModule(), new AbstractModule() { + @Override + protected void configure() { + bind(RequestSigner.class).to(FormSigner.class); + bind(String.class).annotatedWith(Names.named(PROPERTY_HEADER_TAG)).toInstance("amz"); + bind(String.class).annotatedWith(TimeStamp.class).toInstance("2009-11-08T15:54:08.897Z"); + } - })).buildInjector(); - FormSigner filter = INJECTOR.getInstance(FormSigner.class); + })).buildInjector(); + } + + public static FormSigner filter(Credentials creds) { + return injector(creds).getInstance(FormSigner.class); + } + + public static FormSigner staticCredentialsFilter = filter(new Credentials("identity", "credential")); + + HttpRequest request = HttpRequest.builder().method("GET") + .endpoint("http://localhost") + .addHeader(HttpHeaders.HOST, "localhost") + .addFormParam("Action", "DescribeImages") + .addFormParam("ImageId.1", "ami-2bb65342").build(); + + @Test + void testAddsSecurityToken() { + HttpRequest filtered = filter(new TemporaryCredentialsHandlerTest().expected()).filter(request); + assertEquals( + filtered.getPayload().getRawContent(), + "Action=DescribeImages&ImageId.1=ami-2bb65342&Signature=waV%2B%2BIdRwHRlnK2126CqgHHd4FZb%2B5wAeRueidjFc/M%3D&SignatureMethod=HmacSHA256&SignatureVersion=2&Timestamp=2009-11-08T15%3A54%3A08.897Z&Version=apiVersion&AWSAccessKeyId=AKIAIOSFODNN7EXAMPLE"); + assertEquals(filtered.getFirstHeaderOrNull("SecurityToken"), "AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT"); + } @Test void testBuildCanonicalizedStringSetsVersion() { - - assertEquals( - filter.filter( - HttpRequest.builder() - .method("GET") - .endpoint("http://localhost") - .addHeader(HttpHeaders.HOST, "localhost") - .payload("Action=DescribeImages&ImageId.1=ami-2bb65342").build()) - .getPayload().getRawContent(), - "Action=DescribeImages&ImageId.1=ami-2bb65342&Signature=ugnt4m2eHE7Ka/vXTr9EhKZq7bhxOfvW0y4pAEqF97w%3D&SignatureMethod=HmacSHA256&SignatureVersion=2&Timestamp=2009-11-08T15%3A54%3A08.897Z&Version=apiVersion&AWSAccessKeyId=identity"); + HttpRequest filtered = staticCredentialsFilter.filter(request); + assertEquals(filtered.getPayload().getRawContent(), + "Action=DescribeImages&ImageId.1=ami-2bb65342&Signature=ugnt4m2eHE7Ka/vXTr9EhKZq7bhxOfvW0y4pAEqF97w%3D&SignatureMethod=HmacSHA256&SignatureVersion=2&Timestamp=2009-11-08T15%3A54%3A08.897Z&Version=apiVersion&AWSAccessKeyId=identity"); } @Test void testBuildCanonicalizedString() { assertEquals( - filter.buildCanonicalizedString(new ImmutableMultimap.Builder().put("AWSAccessKeyId", - "foo").put("Action", "DescribeImages").put("Expires", "2008-02-10T12:00:00Z").put("ImageId.1", - "ami-2bb65342").put("SignatureMethod", "HmacSHA256").put("SignatureVersion", "2").put( - "Version", "2010-06-15").build()), - "AWSAccessKeyId=foo&Action=DescribeImages&Expires=2008-02-10T12%3A00%3A00Z&ImageId.1=ami-2bb65342&SignatureMethod=HmacSHA256&SignatureVersion=2&Version=2010-06-15"); + staticCredentialsFilter.buildCanonicalizedString(new ImmutableMultimap.Builder() + .put("AWSAccessKeyId", "foo").put("Action", "DescribeImages").put("Expires", "2008-02-10T12:00:00Z") + .put("ImageId.1", "ami-2bb65342").put("SignatureMethod", "HmacSHA256").put("SignatureVersion", "2") + .put("Version", "2010-06-15").build()), + "AWSAccessKeyId=foo&Action=DescribeImages&Expires=2008-02-10T12%3A00%3A00Z&ImageId.1=ami-2bb65342&SignatureMethod=HmacSHA256&SignatureVersion=2&Version=2010-06-15"); } } diff --git a/common/aws/src/test/java/org/jclouds/aws/util/AWSUtilsTest.java b/common/aws/src/test/java/org/jclouds/aws/util/AWSUtilsTest.java index fa9b565981..70c5c16f5d 100644 --- a/common/aws/src/test/java/org/jclouds/aws/util/AWSUtilsTest.java +++ b/common/aws/src/test/java/org/jclouds/aws/util/AWSUtilsTest.java @@ -29,6 +29,7 @@ import java.io.InputStream; import org.jclouds.aws.domain.AWSError; import org.jclouds.aws.filters.FormSignerTest; +import org.jclouds.domain.Credentials; import org.jclouds.http.HttpCommand; import org.jclouds.http.HttpRequest; import org.jclouds.http.HttpResponse; @@ -49,7 +50,7 @@ public class AWSUtilsTest { @BeforeTest protected void setUpInjector() throws IOException { - utils = FormSignerTest.INJECTOR.getInstance(AWSUtils.class); + utils = FormSignerTest.injector(new Credentials("identity", "credential")).getInstance(AWSUtils.class); command = createMock(HttpCommand.class); expect(command.getCurrentRequest()).andReturn(createMock(HttpRequest.class)).atLeastOnce(); diff --git a/common/aws/src/test/java/org/jclouds/aws/xml/TemporaryCredentialsHandlerTest.java b/common/aws/src/test/java/org/jclouds/aws/xml/TemporaryCredentialsHandlerTest.java new file mode 100644 index 0000000000..5c11266516 --- /dev/null +++ b/common/aws/src/test/java/org/jclouds/aws/xml/TemporaryCredentialsHandlerTest.java @@ -0,0 +1,61 @@ +/** + * Licensed to jclouds, Inc. (jclouds) under one or more + * contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. jclouds 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.aws.xml; + +import static org.testng.Assert.assertEquals; + +import java.io.InputStream; + +import org.jclouds.aws.domain.TemporaryCredentials; +import org.jclouds.aws.xml.TemporaryCredentialsHandler; +import org.jclouds.date.internal.SimpleDateFormatDateService; +import org.jclouds.http.functions.BaseHandlerTest; +import org.testng.annotations.Test; + +/** + * @author Adrian Cole + */ +// NOTE:without testName, this will not call @Before* and fail w/NPE during surefire +@Test(groups = "unit", testName = "TemporaryCredentialsHandlerTest") +public class TemporaryCredentialsHandlerTest extends BaseHandlerTest { + + public void test() { + InputStream is = getClass().getResourceAsStream("/credentials.xml"); + + TemporaryCredentials expected = expected(); + + TemporaryCredentialsHandler handler = injector.getInstance(TemporaryCredentialsHandler.class); + TemporaryCredentials result = factory.create(handler).parse(is); + + assertEquals(result, expected); + assertEquals(result.getAccessKeyId(), expected.getAccessKeyId()); + assertEquals(result.getSecretAccessKey(), expected.getSecretAccessKey()); + assertEquals(result.getSessionToken(), expected.getSessionToken()); + assertEquals(result.getExpiration(), expected.getExpiration()); + } + + public TemporaryCredentials expected() { + return TemporaryCredentials.builder() + .accessKeyId("AKIAIOSFODNN7EXAMPLE") + .secretAccessKey("wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY") + .sessionToken("AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT") + .expiration(new SimpleDateFormatDateService().iso8601DateParse("2011-07-11T19:55:29.611Z")).build(); + } + +} diff --git a/common/aws/src/test/resources/credentials.xml b/common/aws/src/test/resources/credentials.xml new file mode 100644 index 0000000000..615ba138af --- /dev/null +++ b/common/aws/src/test/resources/credentials.xml @@ -0,0 +1,6 @@ + + AQoEXAMPLEH4aoAH0gNCAPyJxz4BlCFFxWNE1OPTgk5TthT + wJalrXUtnFEMI/K7MDENG/bPxRfiCYzEXAMPLEKEY + 2011-07-11T19:55:29.611Z + AKIAIOSFODNN7EXAMPLE + \ No newline at end of file