diff --git a/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/SwiftApiMetadata.java b/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/SwiftApiMetadata.java index 8d77bb058e..478bd81a10 100644 --- a/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/SwiftApiMetadata.java +++ b/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/SwiftApiMetadata.java @@ -23,13 +23,12 @@ import static org.jclouds.reflect.Reflection2.typeToken; import java.net.URI; import java.util.Properties; -import org.jclouds.openstack.keystone.v2_0.config.AuthenticationApiModule; import org.jclouds.openstack.keystone.v2_0.config.CredentialTypes; -import org.jclouds.openstack.keystone.v2_0.config.KeystoneAuthenticationModule; import org.jclouds.openstack.keystone.v2_0.config.KeystoneAuthenticationModule.RegionModule; import org.jclouds.openstack.swift.v1.blobstore.RegionScopedBlobStoreContext; import org.jclouds.openstack.swift.v1.blobstore.config.SignUsingTemporaryUrls; import org.jclouds.openstack.swift.v1.blobstore.config.SwiftBlobStoreContextModule; +import org.jclouds.openstack.swift.v1.config.SwiftAuthenticationModule; import org.jclouds.openstack.swift.v1.config.SwiftHttpApiModule; import org.jclouds.openstack.swift.v1.config.SwiftTypeAdapters; import org.jclouds.openstack.v2_0.ServiceType; @@ -38,9 +37,6 @@ import org.jclouds.rest.internal.BaseHttpApiMetadata; import com.google.common.collect.ImmutableSet; import com.google.inject.Module; -/** - * Implementation of {@link ApiMetadata} for the Swift API. - */ public class SwiftApiMetadata extends BaseHttpApiMetadata { @Override @@ -59,6 +55,7 @@ public class SwiftApiMetadata extends BaseHttpApiMetadata { public static Properties defaultProperties() { Properties properties = BaseHttpApiMetadata.defaultProperties(); properties.setProperty(SERVICE_TYPE, ServiceType.OBJECT_STORE); + // Can alternatively be set to "tempAuthCredentials" properties.setProperty(CREDENTIAL_TYPE, CredentialTypes.PASSWORD_CREDENTIALS); return properties; } @@ -72,13 +69,12 @@ public class SwiftApiMetadata extends BaseHttpApiMetadata { .credentialName("${password}") .documentation(URI.create("http://docs.openstack.org/api/openstack-object-storage/1.0/content/ch_object-storage-dev-overview.html")) .version("1") - .endpointName("Keystone base url ending in /v2.0/") + .endpointName("Keystone base url ending in /v2.0/ or TempAuth url ending in auth/v1.0/") .defaultEndpoint("http://localhost:5000/v2.0/") .defaultProperties(SwiftApiMetadata.defaultProperties()) .view(typeToken(RegionScopedBlobStoreContext.class)) .defaultModules(ImmutableSet.>builder() - .add(AuthenticationApiModule.class) - .add(KeystoneAuthenticationModule.class) + .add(SwiftAuthenticationModule.class) .add(RegionModule.class) .add(SwiftTypeAdapters.class) .add(SwiftHttpApiModule.class) diff --git a/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/config/SwiftAuthenticationModule.java b/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/config/SwiftAuthenticationModule.java new file mode 100644 index 0000000000..9b6ae403d2 --- /dev/null +++ b/apis/openstack-swift/src/main/java/org/jclouds/openstack/swift/v1/config/SwiftAuthenticationModule.java @@ -0,0 +1,160 @@ +/* + * 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.openstack.swift.v1.config; + +import static org.jclouds.http.HttpUtils.releasePayload; +import static org.jclouds.http.Uris.uriBuilder; +import static org.jclouds.openstack.v2_0.ServiceType.OBJECT_STORE; +import static org.jclouds.openstack.v2_0.reference.AuthHeaders.AUTH_TOKEN; +import static org.jclouds.rest.config.BinderUtils.bindHttpApi; + +import java.io.Closeable; +import java.net.URI; +import java.util.Date; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.HeaderParam; + +import org.jclouds.ContextBuilder; +import org.jclouds.domain.Credentials; +import org.jclouds.http.HttpRequest; +import org.jclouds.http.HttpResponse; +import org.jclouds.openstack.keystone.v2_0.AuthenticationApi; +import org.jclouds.openstack.keystone.v2_0.config.KeystoneAuthenticationModule; +import org.jclouds.openstack.keystone.v2_0.domain.Access; +import org.jclouds.openstack.keystone.v2_0.domain.Endpoint; +import org.jclouds.openstack.keystone.v2_0.domain.Service; +import org.jclouds.openstack.keystone.v2_0.domain.Token; +import org.jclouds.openstack.keystone.v2_0.domain.User; +import org.jclouds.rest.AuthorizationException; +import org.jclouds.rest.InvocationContext; +import org.jclouds.rest.annotations.ApiVersion; +import org.jclouds.rest.annotations.ResponseParser; +import org.jclouds.rest.annotations.VirtualHost; + +import com.google.common.base.Function; +import com.google.common.collect.ImmutableMap; +import com.google.inject.Injector; +import com.google.inject.name.Named; + +/** + * When {@link org.jclouds.openstack.keystone.v2_0.config.KeystoneProperties#CREDENTIAL_TYPE} is set to {@code + * tempAuthCredentials}, do not use Keystone. Instead, bridge TempAuth to Keystone by faking a service catalog out of + * the storage url. The {@link ContextBuilder#endpoint(String) endpoint} must be set to the TempAuth url, usually ending + * in {@code auth/v1.0/}. + */ +public final class SwiftAuthenticationModule extends KeystoneAuthenticationModule { + private static final String STORAGE_USER = "X-Storage-User"; + private static final String STORAGE_PASS = "X-Storage-Pass"; + private static final String STORAGE_URL = "X-Storage-Url"; + + @Override + protected void configure() { + super.configure(); + bindHttpApi(binder(), AuthenticationApi.class); + bindHttpApi(binder(), TempAuthApi.class); + } + + @Override protected Map> authenticationMethods(Injector i) { + return ImmutableMap.>builder() + .putAll(super.authenticationMethods(i)) + .put("tempAuthCredentials", i.getInstance(TempAuth.class)).build(); + } + + static final class TempAuth implements Function { + private final TempAuthApi delegate; + + @Inject TempAuth(TempAuthApi delegate) { + this.delegate = delegate; + } + + @Override public Access apply(Credentials input) { + return delegate.auth(input.identity, input.credential); + } + } + + @VirtualHost + interface TempAuthApi extends Closeable { + + @Named("TempAuth") + @GET + @Consumes + @ResponseParser(AdaptTempAuthResponseToAccess.class) + Access auth(@HeaderParam(STORAGE_USER) String user, @HeaderParam(STORAGE_PASS) String key); + } + + static final class AdaptTempAuthResponseToAccess + implements Function, InvocationContext { + + private final String apiVersion; + + private String host; + private String username; + + @Inject AdaptTempAuthResponseToAccess(@ApiVersion String apiVersion) { + this.apiVersion = apiVersion; + } + + @Override public Access apply(HttpResponse from) { + releasePayload(from); + URI storageUrl = null; + String authToken = null; + for (Map.Entry entry : from.getHeaders().entries()) { + String header = entry.getKey(); + if (header.equalsIgnoreCase(STORAGE_URL)) { + storageUrl = getURI(entry.getValue()); + } else if (header.equalsIgnoreCase(AUTH_TOKEN)) { + authToken = entry.getKey(); + } + } + if (storageUrl == null || authToken == null) { + throw new AuthorizationException("Invalid headers in TempAuth response " + from); + } + // For portability with keystone, based on common knowledge that these tokens tend to expire in 24 hours + // http://docs.openstack.org/api/openstack-object-storage/1.0/content/authentication-object-dev-guide.html + Date expires = new Date(System.currentTimeMillis() + TimeUnit.HOURS.toMillis(24)); + return Access.builder() + .user(User.builder().id(username).name(username).build()) + .token(Token.builder().id(authToken).expires(expires).build()) + .service(Service.builder().name("Object Storage").type(OBJECT_STORE) + .endpoint(Endpoint.builder().publicURL(storageUrl).id(apiVersion).region(storageUrl.getHost()).build()) + .build()).build(); + } + + // TODO: find the swift configuration or bug related to returning localhost + private URI getURI(String headerValue) { + if (headerValue == null) + return null; + URI toReturn = URI.create(headerValue); + if (!"127.0.0.1".equals(toReturn.getHost())) + return toReturn; + return uriBuilder(toReturn).host(host).build(); + } + + @Override + public AdaptTempAuthResponseToAccess setContext(HttpRequest request) { + String host = request.getEndpoint().getHost(); + this.host = host; + this.username = request.getFirstHeaderOrNull(STORAGE_USER); + return this; + } + } +} diff --git a/apis/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/TempAuthMockTest.java b/apis/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/TempAuthMockTest.java new file mode 100644 index 0000000000..140db7d46e --- /dev/null +++ b/apis/openstack-swift/src/test/java/org/jclouds/openstack/swift/v1/TempAuthMockTest.java @@ -0,0 +1,97 @@ +/* + * 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.openstack.swift.v1; + +import static com.google.common.util.concurrent.MoreExecutors.sameThreadExecutor; +import static javax.ws.rs.core.MediaType.APPLICATION_JSON; +import static org.jclouds.openstack.keystone.v2_0.config.KeystoneProperties.CREDENTIAL_TYPE; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + +import java.io.IOException; +import java.util.Properties; + +import org.jclouds.ContextBuilder; +import org.jclouds.concurrent.config.ExecutorServiceModule; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; + +import com.google.common.collect.ImmutableSet; +import com.squareup.okhttp.mockwebserver.MockResponse; +import com.squareup.okhttp.mockwebserver.MockWebServer; +import com.squareup.okhttp.mockwebserver.RecordedRequest; + +@Test(groups = "unit", testName = "TempAuthMockTest", singleThreaded = true) +public class TempAuthMockTest { + + private MockWebServer swiftServer; + private MockWebServer tempAuthServer; + + + public void testGenerateJWTRequest() throws Exception { + tempAuthServer.enqueue(new MockResponse().setResponseCode(204) + .addHeader("X-Auth-Token", "token") + .addHeader("X-Storage-Url", "http://127.0.0.1:" + swiftServer.getPort())); + + swiftServer.enqueue(new MockResponse().setBody("[{\"name\":\"test_container_1\",\"count\":2,\"bytes\":78}]")); + + SwiftApi api = api("http://127.0.0.1:" + tempAuthServer.getPort()); + + // Region name is derived from the swift server host. + assertEquals(api.getConfiguredRegions(), ImmutableSet.of("127.0.0.1")); + + assertTrue(api.getContainerApi("127.0.0.1").list().iterator().hasNext()); + + RecordedRequest auth = tempAuthServer.takeRequest(); + assertEquals(auth.getMethod(), "GET"); + assertEquals(auth.getHeader("X-Storage-User"), "user"); + assertEquals(auth.getHeader("X-Storage-Pass"), "password"); + + // list request went to the destination specified in X-Storage-Url. + RecordedRequest listContainers = swiftServer.takeRequest(); + assertEquals(listContainers.getMethod(), "GET"); + assertEquals(listContainers.getPath(), "/"); + assertEquals(listContainers.getHeader("Accept"), APPLICATION_JSON); + } + + private SwiftApi api(String authUrl) throws IOException { + Properties overrides = new Properties(); + overrides.setProperty(CREDENTIAL_TYPE, "tempAuthCredentials"); + return ContextBuilder.newBuilder(new SwiftApiMetadata()) + .credentials("user", "password") + .endpoint(authUrl) + .overrides(overrides) + .modules(ImmutableSet.of(new ExecutorServiceModule(sameThreadExecutor()))) + .buildApi(SwiftApi.class); + } + + @BeforeMethod + public void start() throws IOException { + tempAuthServer = new MockWebServer(); + tempAuthServer.play(); + + swiftServer = new MockWebServer(); + swiftServer.play(); + } + + @AfterMethod(alwaysRun = true) + public void stop() throws IOException { + tempAuthServer.shutdown(); + swiftServer.shutdown(); + } +}