aboutsummaryrefslogtreecommitdiff
path: root/auth
diff options
context:
space:
mode:
authorKun Zhang <zhangkun@google.com>2016-06-14 13:51:24 -0700
committerKun Zhang <zhangkun@google.com>2016-06-16 16:07:54 -0700
commitdef237d9606cc226f6b523f0818ae2ae664645cf (patch)
tree272f59fec38a4da12ed9717712e726786b33b8d0 /auth
parent63d82746615a5a168e8a6057ed8f61cf90648995 (diff)
downloadgrpc-grpc-java-def237d9606cc226f6b523f0818ae2ae664645cf.tar.gz
auth: MoreCallCredentials.from(Credentials).
It converts Google Auth Library Credentials to CallCredentials, and supersedes ClientAuthInterceptor, which is now deprecated. Also swaps out the ClientAuthInterceptor implementation. Caveat: This in fact changes ClientAuthInterceptor's behavior. Before this change, if multiple ClientAuthInterceptors were attached, their effects would be additive. After this change, only the last executed one would take effect, and it would also overwrite the CallCredentials set in CallOptions. We don't think it's an issue, since other languages also only allow one call credentials to be attached to an RPC.
Diffstat (limited to 'auth')
-rw-r--r--auth/build.gradle3
-rw-r--r--auth/src/main/java/io/grpc/auth/ClientAuthInterceptor.java104
-rw-r--r--auth/src/main/java/io/grpc/auth/GoogleAuthLibraryCallCredentials.java168
-rw-r--r--auth/src/main/java/io/grpc/auth/MoreCallCredentials.java53
-rw-r--r--auth/src/test/java/io/grpc/auth/ClientAuthInterceptorTests.java113
-rw-r--r--auth/src/test/java/io/grpc/auth/GoogleAuthLibraryCallCredentialsTests.java249
6 files changed, 488 insertions, 202 deletions
diff --git a/auth/build.gradle b/auth/build.gradle
index 30cd3226f..14af19ec5 100644
--- a/auth/build.gradle
+++ b/auth/build.gradle
@@ -5,7 +5,8 @@ plugins {
description = "gRpc: Auth"
dependencies {
compile project(':grpc-core'),
- libraries.oauth_client
+ libraries.google_auth_credentials
+ testCompile libraries.oauth_client
}
// Configure the animal sniffer plugin
diff --git a/auth/src/main/java/io/grpc/auth/ClientAuthInterceptor.java b/auth/src/main/java/io/grpc/auth/ClientAuthInterceptor.java
index e636b3996..aa2df86f8 100644
--- a/auth/src/main/java/io/grpc/auth/ClientAuthInterceptor.java
+++ b/auth/src/main/java/io/grpc/auth/ClientAuthInterceptor.java
@@ -38,17 +38,8 @@ import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptor;
-import io.grpc.ClientInterceptors.CheckedForwardingClientCall;
-import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
-import io.grpc.Status;
-import io.grpc.StatusException;
-import java.io.IOException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.List;
-import java.util.Map;
import java.util.concurrent.Executor;
/**
@@ -57,109 +48,22 @@ import java.util.concurrent.Executor;
*
* <p>Uses the new and simplified Google auth library:
* https://github.com/google/google-auth-library-java
+ *
+ * @deprecated use {@link GoogleAuthLibraryCallCredentials} instead.
*/
+@Deprecated
public final class ClientAuthInterceptor implements ClientInterceptor {
private final Credentials credentials;
- private Metadata cached;
- private Map<String, List<String>> lastMetadata;
-
public ClientAuthInterceptor(
Credentials credentials, @SuppressWarnings("unused") Executor executor) {
this.credentials = Preconditions.checkNotNull(credentials);
- // TODO(louiscryan): refresh token asynchronously with this executor.
}
@Override
public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
final MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, final Channel next) {
- // TODO(ejona86): If the call fails for Auth reasons, this does not properly propagate info that
- // would be in WWW-Authenticate, because it does not yet have access to the header.
- return new CheckedForwardingClientCall<ReqT, RespT>(next.newCall(method, callOptions)) {
- @Override
- protected void checkedStart(Listener<RespT> responseListener, Metadata headers)
- throws StatusException {
- Metadata cachedSaved;
- URI uri = serviceUri(next, method);
- synchronized (ClientAuthInterceptor.this) {
- // TODO(louiscryan): This is icky but the current auth library stores the same
- // metadata map until the next refresh cycle. This will be fixed once
- // https://github.com/google/google-auth-library-java/issues/3
- // is resolved.
- // getRequestMetadata() may return a different map based on the provided URI, i.e., for
- // JWT. However, today it does not cache JWT and so we won't bother tring to cache its
- // return value based on the URI.
- Map<String, List<String>> latestMetadata = getRequestMetadata(uri);
- if (lastMetadata == null || lastMetadata != latestMetadata) {
- lastMetadata = latestMetadata;
- cached = toHeaders(lastMetadata);
- }
- cachedSaved = cached;
- }
- headers.merge(cachedSaved);
- delegate().start(responseListener, headers);
- }
- };
- }
-
- /**
- * Generate a JWT-specific service URI. The URI is simply an identifier with enough information
- * for a service to know that the JWT was intended for it. The URI will commonly be verified with
- * a simple string equality check.
- */
- private URI serviceUri(Channel channel, MethodDescriptor<?, ?> method) throws StatusException {
- String authority = channel.authority();
- if (authority == null) {
- throw Status.UNAUTHENTICATED.withDescription("Channel has no authority").asException();
- }
- // Always use HTTPS, by definition.
- final String scheme = "https";
- final int defaultPort = 443;
- String path = "/" + MethodDescriptor.extractFullServiceName(method.getFullMethodName());
- URI uri;
- try {
- uri = new URI(scheme, authority, path, null, null);
- } catch (URISyntaxException e) {
- throw Status.UNAUTHENTICATED.withDescription("Unable to construct service URI for auth")
- .withCause(e).asException();
- }
- // The default port must not be present. Alternative ports should be present.
- if (uri.getPort() == defaultPort) {
- uri = removePort(uri);
- }
- return uri;
- }
-
- private URI removePort(URI uri) throws StatusException {
- try {
- return new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), -1 /* port */,
- uri.getPath(), uri.getQuery(), uri.getFragment());
- } catch (URISyntaxException e) {
- throw Status.UNAUTHENTICATED.withDescription(
- "Unable to construct service URI after removing port")
- .withCause(e).asException();
- }
- }
-
- private Map<String, List<String>> getRequestMetadata(URI uri) throws StatusException {
- try {
- return credentials.getRequestMetadata(uri);
- } catch (IOException e) {
- throw Status.UNAUTHENTICATED.withCause(e).asException();
- }
- }
-
- private static final Metadata toHeaders(Map<String, List<String>> metadata) {
- Metadata headers = new Metadata();
- if (metadata != null) {
- for (String key : metadata.keySet()) {
- Metadata.Key<String> headerKey = Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER);
- for (String value : metadata.get(key)) {
- headers.put(headerKey, value);
- }
- }
- }
- return headers;
+ return next.newCall(method, callOptions.withCredentials(MoreCallCredentials.from(credentials)));
}
}
diff --git a/auth/src/main/java/io/grpc/auth/GoogleAuthLibraryCallCredentials.java b/auth/src/main/java/io/grpc/auth/GoogleAuthLibraryCallCredentials.java
new file mode 100644
index 000000000..e5b5cd731
--- /dev/null
+++ b/auth/src/main/java/io/grpc/auth/GoogleAuthLibraryCallCredentials.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2016, Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.grpc.auth;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.auth.Credentials;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.io.BaseEncoding;
+
+import io.grpc.Attributes;
+import io.grpc.CallCredentials.MetadataApplier;
+import io.grpc.CallCredentials;
+import io.grpc.Metadata;
+import io.grpc.MethodDescriptor;
+import io.grpc.Status;
+import io.grpc.StatusException;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/**
+ * Wraps {@link Credentials} as a {@link CallCredentials}.
+ */
+final class GoogleAuthLibraryCallCredentials implements CallCredentials {
+
+ @VisibleForTesting
+ final Credentials creds;
+
+ private Metadata lastHeaders;
+ private Map<String, List<String>> lastMetadata;
+
+ public GoogleAuthLibraryCallCredentials(Credentials creds) {
+ this.creds = checkNotNull(creds, "creds");
+ }
+
+ @Override
+ public void applyRequestMetadata(MethodDescriptor<?, ?> method, Attributes attrs,
+ Executor appExecutor, final MetadataApplier applier) {
+ Metadata cachedSaved;
+ String authority = checkNotNull(attrs.get(ATTR_AUTHORITY), "authority");
+ final URI uri;
+ try {
+ uri = serviceUri(authority, method);
+ } catch (StatusException e) {
+ applier.fail(e.getStatus());
+ return;
+ }
+ appExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ // Credentials is expected to manage caching internally if the metadata is fetched over
+ // the network.
+ // TODO(zhangkun83): we don't know whether there is valid cache data. If there is, we
+ // would waste a context switch by always scheduling in executor. However, we have to
+ // do so because we can't risk blocking the network thread. This can be resolved after
+ // https://github.com/google/google-auth-library-java/issues/3 is resolved.
+ Map<String, List<String>> metadata = creds.getRequestMetadata(uri);
+ // Re-use the headers if getRequestMetadata() returns the same map. It may return a
+ // different map based on the provided URI, i.e., for JWT. However, today it does not
+ // cache JWT and so we won't bother tring to save its return value based on the URI.
+ Metadata headers;
+ synchronized (GoogleAuthLibraryCallCredentials.this) {
+ if (lastMetadata != metadata) {
+ lastMetadata = metadata;
+ lastHeaders = toHeaders(metadata);
+ }
+ headers = lastHeaders;
+ }
+ applier.apply(headers);
+ } catch (Throwable e) {
+ applier.fail(Status.UNAUTHENTICATED.withCause(e));
+ }
+ }
+ });
+ }
+
+ /**
+ * Generate a JWT-specific service URI. The URI is simply an identifier with enough information
+ * for a service to know that the JWT was intended for it. The URI will commonly be verified with
+ * a simple string equality check.
+ */
+ private static URI serviceUri(String authority, MethodDescriptor<?, ?> method)
+ throws StatusException {
+ if (authority == null) {
+ throw Status.UNAUTHENTICATED.withDescription("Channel has no authority").asException();
+ }
+ // Always use HTTPS, by definition.
+ final String scheme = "https";
+ final int defaultPort = 443;
+ String path = "/" + MethodDescriptor.extractFullServiceName(method.getFullMethodName());
+ URI uri;
+ try {
+ uri = new URI(scheme, authority, path, null, null);
+ } catch (URISyntaxException e) {
+ throw Status.UNAUTHENTICATED.withDescription("Unable to construct service URI for auth")
+ .withCause(e).asException();
+ }
+ // The default port must not be present. Alternative ports should be present.
+ if (uri.getPort() == defaultPort) {
+ uri = removePort(uri);
+ }
+ return uri;
+ }
+
+ private static URI removePort(URI uri) throws StatusException {
+ try {
+ return new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), -1 /* port */,
+ uri.getPath(), uri.getQuery(), uri.getFragment());
+ } catch (URISyntaxException e) {
+ throw Status.UNAUTHENTICATED.withDescription(
+ "Unable to construct service URI after removing port").withCause(e).asException();
+ }
+ }
+
+ private static Metadata toHeaders(Map<String, List<String>> metadata) {
+ Metadata headers = new Metadata();
+ if (metadata != null) {
+ for (String key : metadata.keySet()) {
+ if (key.endsWith("-bin")) {
+ Metadata.Key<byte[]> headerKey = Metadata.Key.of(key, Metadata.BINARY_BYTE_MARSHALLER);
+ for (String value : metadata.get(key)) {
+ headers.put(headerKey, BaseEncoding.base64().decode(value));
+ }
+ } else {
+ Metadata.Key<String> headerKey = Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER);
+ for (String value : metadata.get(key)) {
+ headers.put(headerKey, value);
+ }
+ }
+ }
+ }
+ return headers;
+ }
+}
diff --git a/auth/src/main/java/io/grpc/auth/MoreCallCredentials.java b/auth/src/main/java/io/grpc/auth/MoreCallCredentials.java
new file mode 100644
index 000000000..b8e8ca52a
--- /dev/null
+++ b/auth/src/main/java/io/grpc/auth/MoreCallCredentials.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2016, Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.grpc.auth;
+
+import com.google.auth.Credentials;
+
+import io.grpc.CallCredentials;
+import io.grpc.ExperimentalApi;
+
+/**
+ * A utility class that converts other types of credentials to {@link CallCredentials}.
+ */
+@ExperimentalApi("https//github.com/grpc/grpc-java/issues/1914")
+public final class MoreCallCredentials {
+ /**
+ * Converts a Google Auth Library {@link Credentials} to {@link CallCredentials}.
+ */
+ public static CallCredentials from(Credentials creds) {
+ return new GoogleAuthLibraryCallCredentials(creds);
+ }
+
+ private MoreCallCredentials() {
+ }
+}
diff --git a/auth/src/test/java/io/grpc/auth/ClientAuthInterceptorTests.java b/auth/src/test/java/io/grpc/auth/ClientAuthInterceptorTests.java
index 29fc20100..ce478a561 100644
--- a/auth/src/test/java/io/grpc/auth/ClientAuthInterceptorTests.java
+++ b/auth/src/test/java/io/grpc/auth/ClientAuthInterceptorTests.java
@@ -31,58 +31,37 @@
package io.grpc.auth;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.isA;
+import static org.junit.Assert.assertSame;
import static org.mockito.Matchers.same;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
import com.google.auth.Credentials;
-import com.google.auth.oauth2.AccessToken;
-import com.google.auth.oauth2.OAuth2Credentials;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.LinkedListMultimap;
-import com.google.common.collect.ListMultimap;
-import com.google.common.collect.Multimaps;
-import com.google.common.util.concurrent.MoreExecutors;
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
-import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.MethodDescriptor.Marshaller;
-import io.grpc.Status;
-import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
import org.mockito.Mock;
-import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
-import java.io.IOException;
-import java.net.URI;
-import java.util.Date;
import java.util.concurrent.Executor;
/**
* Tests for {@link ClientAuthInterceptor}.
*/
@RunWith(JUnit4.class)
+@Deprecated
public class ClientAuthInterceptorTests {
-
- private static final Metadata.Key<String> AUTHORIZATION = Metadata.Key.of("Authorization",
- Metadata.ASCII_STRING_MARSHALLER);
- private static final Metadata.Key<String> EXTRA_AUTHORIZATION = Metadata.Key.of(
- "Extra-Authorization", Metadata.ASCII_STRING_MARSHALLER);
-
- private final Executor executor = MoreExecutors.directExecutor();
+ @Mock
+ Executor executor;
@Mock
Credentials credentials;
@@ -96,13 +75,10 @@ public class ClientAuthInterceptorTests {
MethodDescriptor<String, Integer> descriptor;
@Mock
- ClientCall.Listener<Integer> listener;
-
- @Mock
Channel channel;
- @Mock
- ClientCall<String, Integer> call;
+ @Captor
+ ArgumentCaptor<CallOptions> callOptionsCaptor;
ClientAuthInterceptor interceptor;
@@ -112,82 +88,17 @@ public class ClientAuthInterceptorTests {
MockitoAnnotations.initMocks(this);
descriptor = MethodDescriptor.create(
MethodDescriptor.MethodType.UNKNOWN, "a.service/method", stringMarshaller, intMarshaller);
- when(channel.newCall(same(descriptor), any(CallOptions.class))).thenReturn(call);
- doReturn("localhost:443").when(channel).authority();
interceptor = new ClientAuthInterceptor(credentials, executor);
}
@Test
- public void testCopyCredentialToHeaders() throws IOException {
- ListMultimap<String, String> values = LinkedListMultimap.create();
- values.put("Authorization", "token1");
- values.put("Authorization", "token2");
- values.put("Extra-Authorization", "token3");
- values.put("Extra-Authorization", "token4");
- when(credentials.getRequestMetadata(any(URI.class))).thenReturn(Multimaps.asMap(values));
- ClientCall<String, Integer> interceptedCall =
- interceptor.interceptCall(descriptor, CallOptions.DEFAULT, channel);
- Metadata headers = new Metadata();
- interceptedCall.start(listener, headers);
- verify(call).start(listener, headers);
-
- Iterable<String> authorization = headers.getAll(AUTHORIZATION);
- Assert.assertArrayEquals(new String[]{"token1", "token2"},
- Iterables.toArray(authorization, String.class));
- Iterable<String> extraAuthorization = headers.getAll(EXTRA_AUTHORIZATION);
- Assert.assertArrayEquals(new String[]{"token3", "token4"},
- Iterables.toArray(extraAuthorization, String.class));
- }
-
- @Test
- public void testCredentialsThrows() throws IOException {
- when(credentials.getRequestMetadata(any(URI.class))).thenThrow(new IOException("Broken"));
+ public void callCredentialsSet() throws Exception {
ClientCall<String, Integer> interceptedCall =
interceptor.interceptCall(descriptor, CallOptions.DEFAULT, channel);
- Metadata headers = new Metadata();
- interceptedCall.start(listener, headers);
- ArgumentCaptor<Status> statusCaptor = ArgumentCaptor.forClass(Status.class);
- Mockito.verify(listener).onClose(statusCaptor.capture(), isA(Metadata.class));
- Assert.assertNull(headers.getAll(AUTHORIZATION));
- Mockito.verify(call, never()).start(listener, headers);
- Assert.assertEquals(Status.Code.UNAUTHENTICATED, statusCaptor.getValue().getCode());
- Assert.assertNotNull(statusCaptor.getValue().getCause());
- }
- @Test
- public void testWithOAuth2Credential() {
- final AccessToken token = new AccessToken("allyourbase", new Date(Long.MAX_VALUE));
- final OAuth2Credentials oAuth2Credentials = new OAuth2Credentials() {
- @Override
- public AccessToken refreshAccessToken() throws IOException {
- return token;
- }
- };
- interceptor = new ClientAuthInterceptor(oAuth2Credentials, executor);
- ClientCall<String, Integer> interceptedCall =
- interceptor.interceptCall(descriptor, CallOptions.DEFAULT, channel);
- Metadata headers = new Metadata();
- interceptedCall.start(listener, headers);
- verify(call).start(listener, headers);
- Iterable<String> authorization = headers.getAll(AUTHORIZATION);
- Assert.assertArrayEquals(new String[]{"Bearer allyourbase"},
- Iterables.toArray(authorization, String.class));
- }
-
- @Test
- public void verifyServiceUri() throws IOException {
- ClientCall<String, Integer> interceptedCall;
-
- doReturn("example.com:443").when(channel).authority();
- interceptedCall = interceptor.interceptCall(descriptor, CallOptions.DEFAULT, channel);
- interceptedCall.start(listener, new Metadata());
- verify(credentials).getRequestMetadata(URI.create("https://example.com/a.service"));
- interceptedCall.cancel("Cancel for test", null);
-
- doReturn("example.com:123").when(channel).authority();
- interceptedCall = interceptor.interceptCall(descriptor, CallOptions.DEFAULT, channel);
- interceptedCall.start(listener, new Metadata());
- verify(credentials).getRequestMetadata(URI.create("https://example.com:123/a.service"));
- interceptedCall.cancel("Cancel for test", null);
+ verify(channel).newCall(same(descriptor), callOptionsCaptor.capture());
+ GoogleAuthLibraryCallCredentials callCredentials =
+ (GoogleAuthLibraryCallCredentials) callOptionsCaptor.getValue().getCredentials();
+ assertSame(credentials, callCredentials.creds);
}
}
diff --git a/auth/src/test/java/io/grpc/auth/GoogleAuthLibraryCallCredentialsTests.java b/auth/src/test/java/io/grpc/auth/GoogleAuthLibraryCallCredentialsTests.java
new file mode 100644
index 000000000..d99812f3d
--- /dev/null
+++ b/auth/src/test/java/io/grpc/auth/GoogleAuthLibraryCallCredentialsTests.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright 2016, Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.grpc.auth;
+
+import static com.google.common.base.Charsets.US_ASCII;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.auth.Credentials;
+import com.google.auth.oauth2.AccessToken;
+import com.google.auth.oauth2.OAuth2Credentials;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Multimaps;
+
+import io.grpc.Attributes;
+import io.grpc.CallCredentials.MetadataApplier;
+import io.grpc.CallCredentials;
+import io.grpc.Metadata;
+import io.grpc.MethodDescriptor;
+import io.grpc.SecurityLevel;
+import io.grpc.Status;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.concurrent.Executor;
+
+/**
+ * Tests for {@link GoogleAuthLibraryCallCredentials}.
+ */
+@RunWith(JUnit4.class)
+public class GoogleAuthLibraryCallCredentialsTests {
+
+ private static final Metadata.Key<String> AUTHORIZATION = Metadata.Key.of("Authorization",
+ Metadata.ASCII_STRING_MARSHALLER);
+ private static final Metadata.Key<byte[]> EXTRA_AUTHORIZATION = Metadata.Key.of(
+ "Extra-Authorization-bin", Metadata.BINARY_BYTE_MARSHALLER);
+
+ @Mock
+ private MethodDescriptor.Marshaller<String> stringMarshaller;
+
+ @Mock
+ private MethodDescriptor.Marshaller<Integer> intMarshaller;
+
+ @Mock
+ private Credentials credentials;
+
+ @Mock
+ private MetadataApplier applier;
+
+ @Mock
+ private Executor executor;
+
+ @Captor
+ private ArgumentCaptor<Metadata> headersCaptor;
+
+ @Captor
+ private ArgumentCaptor<Status> statusCaptor;
+
+ private MethodDescriptor<String, Integer> method;
+ private URI expectedUri;
+
+ private final String authority = "testauthority";
+ private final Attributes attrs = Attributes.newBuilder()
+ .set(CallCredentials.ATTR_AUTHORITY, authority)
+ .set(CallCredentials.ATTR_SECURITY_LEVEL, SecurityLevel.PRIVACY_AND_INTEGRITY)
+ .build();
+
+ private ArrayList<Runnable> pendingRunnables = new ArrayList<Runnable>();
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ method = MethodDescriptor.create(
+ MethodDescriptor.MethodType.UNKNOWN, "a.service/method", stringMarshaller, intMarshaller);
+ expectedUri = new URI("https://testauthority/a.service");
+ doAnswer(new Answer<Void>() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ Runnable r = (Runnable) invocation.getArguments()[0];
+ pendingRunnables.add(r);
+ return null;
+ }
+ }).when(executor).execute(any(Runnable.class));
+ }
+
+ @Test
+ public void copyCredentialsToHeaders() throws Exception {
+ ListMultimap<String, String> values = LinkedListMultimap.create();
+ values.put("Authorization", "token1");
+ values.put("Authorization", "token2");
+ values.put("Extra-Authorization-bin", "dG9rZW4z"); // bytes "token3" in base64
+ values.put("Extra-Authorization-bin", "dG9rZW40"); // bytes "token4" in base64
+ when(credentials.getRequestMetadata(eq(expectedUri))).thenReturn(Multimaps.asMap(values));
+
+ GoogleAuthLibraryCallCredentials callCredentials =
+ new GoogleAuthLibraryCallCredentials(credentials);
+ callCredentials.applyRequestMetadata(method, attrs, executor, applier);
+ assertEquals(1, runPendingRunnables());
+
+ verify(credentials).getRequestMetadata(eq(expectedUri));
+ verify(applier).apply(headersCaptor.capture());
+ Metadata headers = headersCaptor.getValue();
+ Iterable<String> authorization = headers.getAll(AUTHORIZATION);
+ assertArrayEquals(new String[]{"token1", "token2"},
+ Iterables.toArray(authorization, String.class));
+ Iterable<byte[]> extraAuthorization = headers.getAll(EXTRA_AUTHORIZATION);
+ assertEquals(2, Iterables.size(extraAuthorization));
+ assertArrayEquals("token3".getBytes(US_ASCII), Iterables.get(extraAuthorization, 0));
+ assertArrayEquals("token4".getBytes(US_ASCII), Iterables.get(extraAuthorization, 1));
+ }
+
+ @Test
+ public void invalidBase64() throws Exception {
+ ListMultimap<String, String> values = LinkedListMultimap.create();
+ values.put("Extra-Authorization-bin", "dG9rZW4z1"); // invalid base64
+ when(credentials.getRequestMetadata(eq(expectedUri))).thenReturn(Multimaps.asMap(values));
+
+ GoogleAuthLibraryCallCredentials callCredentials =
+ new GoogleAuthLibraryCallCredentials(credentials);
+ callCredentials.applyRequestMetadata(method, attrs, executor, applier);
+ assertEquals(1, runPendingRunnables());
+
+ verify(credentials).getRequestMetadata(eq(expectedUri));
+ verify(applier).fail(statusCaptor.capture());
+ Status status = statusCaptor.getValue();
+ assertEquals(Status.Code.UNAUTHENTICATED, status.getCode());
+ assertEquals(IllegalArgumentException.class, status.getCause().getClass());
+ }
+
+ @Test
+ public void credentialsThrows() throws Exception {
+ IOException exception = new IOException("Broken");
+ when(credentials.getRequestMetadata(eq(expectedUri))).thenThrow(exception);
+
+ GoogleAuthLibraryCallCredentials callCredentials =
+ new GoogleAuthLibraryCallCredentials(credentials);
+ callCredentials.applyRequestMetadata(method, attrs, executor, applier);
+ assertEquals(1, runPendingRunnables());
+
+ verify(credentials).getRequestMetadata(eq(expectedUri));
+ verify(applier).fail(statusCaptor.capture());
+ Status status = statusCaptor.getValue();
+ assertEquals(Status.Code.UNAUTHENTICATED, status.getCode());
+ assertEquals(exception, status.getCause());
+ }
+
+ @Test
+ public void oauth2Credential() {
+ final AccessToken token = new AccessToken("allyourbase", new Date(Long.MAX_VALUE));
+ final OAuth2Credentials credentials = new OAuth2Credentials() {
+ @Override
+ public AccessToken refreshAccessToken() throws IOException {
+ return token;
+ }
+ };
+
+ GoogleAuthLibraryCallCredentials callCredentials =
+ new GoogleAuthLibraryCallCredentials(credentials);
+ callCredentials.applyRequestMetadata(method, attrs, executor, applier);
+ assertEquals(1, runPendingRunnables());
+
+ verify(applier).apply(headersCaptor.capture());
+ Metadata headers = headersCaptor.getValue();
+ Iterable<String> authorization = headers.getAll(AUTHORIZATION);
+ assertArrayEquals(new String[]{"Bearer allyourbase"},
+ Iterables.toArray(authorization, String.class));
+ }
+
+ @Test
+ public void serviceUri() throws Exception {
+ GoogleAuthLibraryCallCredentials callCredentials =
+ new GoogleAuthLibraryCallCredentials(credentials);
+ callCredentials.applyRequestMetadata(method,
+ Attributes.newBuilder()
+ .setAll(attrs)
+ .set(CallCredentials.ATTR_AUTHORITY, "example.com:443")
+ .build(),
+ executor, applier);
+ assertEquals(1, runPendingRunnables());
+ verify(credentials).getRequestMetadata(eq(new URI("https://example.com/a.service")));
+
+ callCredentials.applyRequestMetadata(method,
+ Attributes.newBuilder()
+ .setAll(attrs)
+ .set(CallCredentials.ATTR_AUTHORITY, "example.com:123")
+ .build(),
+ executor, applier);
+ assertEquals(1, runPendingRunnables());
+ verify(credentials).getRequestMetadata(eq(new URI("https://example.com:123/a.service")));
+ }
+
+ private int runPendingRunnables() {
+ ArrayList<Runnable> savedPendingRunnables = pendingRunnables;
+ pendingRunnables = new ArrayList<Runnable>();
+ for (Runnable r : savedPendingRunnables) {
+ r.run();
+ }
+ return savedPendingRunnables.size();
+ }
+}