Use self-managed credential in remote api installer

RemoteApiOption has a package-private method that takes a Stream representing the content of a JSON and use a GoogleCredential created from it as its credential. This CL uses reflection to change the access modifier of that method in order to supply a credential stream that is self-managed. This is obviously not ideal and prone to breakage in case the getGoogleCredentialStream method is changed. Unfortunately upstream is not willing to make it public citing the reason that GoogleCredential.fromStream() (which getGoogleCredentialStream uses) is a @Beta annotated function (see https://groups.google.com[]forum/#!searchin/domain-registry-eng/remoteapioptions%7Csort:date/domain-registry-eng/Flsah6skszQ/CySZv2XEBwAJ). However this function is introduced 5 years ago as a public function (b857184bfa). I think at this point it is safe to assume that it is part of the widely used APIs and will not change without sufficient notice.

Note here that RemoteApiOptions creates its own copy of GoogleCredential to be used to call App Engine APIs locally, whereas communications to Nomulus endpoints use the Credential provided in AuthModule. Even though both credentials are created from the same client id, client secret and refresh token (the three elements needed to construct a GoogleCredential this way, see https://github.com/googleapis/google-api-java-client/blob/master/google-api-client/src/main/java/com/google/api/client/googleapis/auth/oauth2/GoogleCredential.java#L842), their refreshes cycles are independent of each other. I verified that refreshing one of the credential does not invalidate the access token of the other credential, as long as it is not expired yet.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=224156131
This commit is contained in:
jianglai 2018-12-05 08:10:29 -08:00
parent aeedc427ad
commit 6352b8a01a
13 changed files with 229 additions and 74 deletions

View file

@ -117,6 +117,7 @@ dependencies {
maybeRuntime 'com.google.auth:google-auth-library-oauth2-http:0.7.1' maybeRuntime 'com.google.auth:google-auth-library-oauth2-http:0.7.1'
maybeRuntime 'com.google.auto:auto-common:0.8' maybeRuntime 'com.google.auto:auto-common:0.8'
maybeRuntime 'com.google.auto.factory:auto-factory:1.0-beta3' maybeRuntime 'com.google.auto.factory:auto-factory:1.0-beta3'
compile 'com.google.code.gson:gson:2.8.5'
compile 'com.google.auto.value:auto-value-annotations:1.6.2' compile 'com.google.auto.value:auto-value-annotations:1.6.2'
maybeRuntime 'com.google.cloud.bigdataoss:gcsio:1.4.5' maybeRuntime 'com.google.cloud.bigdataoss:gcsio:1.4.5'
maybeRuntime 'com.google.cloud.bigdataoss:util:1.4.5' maybeRuntime 'com.google.cloud.bigdataoss:util:1.4.5'

View file

@ -1229,6 +1229,14 @@ public final class RegistryConfig {
return ImmutableList.copyOf(config.credentialOAuth.delegatedCredentialOauthScopes); return ImmutableList.copyOf(config.credentialOAuth.delegatedCredentialOauthScopes);
} }
/** Provides the OAuth scopes required for credentials created locally for the nomulus tool. */
@Provides
@Config("localCredentialOauthScopes")
public static ImmutableList<String> provideLocalCredentialOauthScopes(
RegistryConfigSettings config) {
return ImmutableList.copyOf(config.credentialOAuth.localCredentialOauthScopes);
}
/** Provides the OAuth scopes required for access to App Engine Admin API. */ /** Provides the OAuth scopes required for access to App Engine Admin API. */
@Provides @Provides
@Config("appEngineAdminApiCredentialOauthScopes") @Config("appEngineAdminApiCredentialOauthScopes")

View file

@ -58,6 +58,7 @@ public class RegistryConfigSettings {
public static class CredentialOAuth { public static class CredentialOAuth {
public List<String> defaultCredentialOauthScopes; public List<String> defaultCredentialOauthScopes;
public List<String> delegatedCredentialOauthScopes; public List<String> delegatedCredentialOauthScopes;
public List<String> localCredentialOauthScopes;
public List<String> appEngineAdminApiCredentialOauthScopes; public List<String> appEngineAdminApiCredentialOauthScopes;
} }

View file

@ -283,6 +283,12 @@ credentialOAuth:
- https://www.googleapis.com/auth/admin.directory.group - https://www.googleapis.com/auth/admin.directory.group
# View and manage group settings in Group Settings API. # View and manage group settings in Group Settings API.
- https://www.googleapis.com/auth/apps.groups.settings - https://www.googleapis.com/auth/apps.groups.settings
# OAuth scopes required to create a credential locally in for the nomulus tool.
localCredentialOauthScopes:
# Call App Engine APIs.
- https://www.googleapis.com/auth/appengine.apis
# View your email address.
- https://www.googleapis.com/auth/userinfo.email
# OAuth scopes required for accessing App Engine Admin API using the # OAuth scopes required for accessing App Engine Admin API using the
# AppIdentityCredential. # AppIdentityCredential.
appEngineAdminApiCredentialOauthScopes: appEngineAdminApiCredentialOauthScopes:

View file

@ -59,6 +59,7 @@ def domain_registry_repositories(
omit_com_google_auto_factory = False, omit_com_google_auto_factory = False,
omit_com_google_auto_service = False, omit_com_google_auto_service = False,
omit_com_google_auto_value = False, omit_com_google_auto_value = False,
omit_com_google_code_gson = False,
omit_com_google_cloud_bigdataoss_gcsio = False, omit_com_google_cloud_bigdataoss_gcsio = False,
omit_com_google_cloud_bigdataoss_util = False, omit_com_google_cloud_bigdataoss_util = False,
omit_com_google_code_findbugs_jsr305 = False, omit_com_google_code_findbugs_jsr305 = False,
@ -242,6 +243,8 @@ def domain_registry_repositories(
com_google_auto_service() com_google_auto_service()
if not omit_com_google_auto_value: if not omit_com_google_auto_value:
com_google_auto_value() com_google_auto_value()
if not omit_com_google_code_gson:
com_google_code_gson()
if not omit_com_google_cloud_bigdataoss_gcsio: if not omit_com_google_cloud_bigdataoss_gcsio:
com_google_cloud_bigdataoss_gcsio() com_google_cloud_bigdataoss_gcsio()
if not omit_com_google_cloud_bigdataoss_util: if not omit_com_google_cloud_bigdataoss_util:
@ -1115,6 +1118,17 @@ def com_google_auto_value():
]), ]),
) )
def com_google_code_gson():
java_import_external(
name = "com_google_code_gson",
licenses = ["notice"], # Apache 2.0
jar_sha256 = "233a0149fc365c9f6edbd683cfe266b19bdc773be98eabdaf6b3c924b48e7d81",
jar_urls = [
"http://repo1.maven.org/maven2/com/google/code/gson/gson/2.8.5/gson-2.8.5.jar",
"http://maven.ibiblio.org/maven2/com/google/code/gson/gson/2.8.5/gson-2.8.5.jar",
],
)
def com_google_cloud_bigdataoss_gcsio(): def com_google_cloud_bigdataoss_gcsio():
java_import_external( java_import_external(
name = "com_google_cloud_bigdataoss_gcsio", name = "com_google_cloud_bigdataoss_gcsio",

View file

@ -26,11 +26,15 @@ import com.google.api.client.json.JsonFactory;
import com.google.api.client.util.store.AbstractDataStoreFactory; import com.google.api.client.util.store.AbstractDataStoreFactory;
import com.google.api.client.util.store.FileDataStoreFactory; import com.google.api.client.util.store.FileDataStoreFactory;
import com.google.common.base.Joiner; import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet; import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Ordering; import com.google.common.collect.Ordering;
import com.google.gson.Gson;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
import google.registry.config.RegistryConfig.Config; import google.registry.config.RegistryConfig.Config;
import java.io.ByteArrayInputStream;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@ -51,9 +55,8 @@ public class AuthModule {
new File(System.getProperty("user.home"), ".config/nomulus/credentials"); new File(System.getProperty("user.home"), ".config/nomulus/credentials");
@Provides @Provides
public Credential provideCredential( public static Credential provideCredential(
GoogleAuthorizationCodeFlow flow, GoogleAuthorizationCodeFlow flow, @ClientScopeQualifier String clientScopeQualifier) {
@ClientScopeQualifier String clientScopeQualifier) {
try { try {
// Try to load the credentials, throw an exception if we fail. // Try to load the credentials, throw an exception if we fail.
Credential credential = flow.loadCredential(clientScopeQualifier); Credential credential = flow.loadCredential(clientScopeQualifier);
@ -67,10 +70,10 @@ public class AuthModule {
} }
@Provides @Provides
GoogleAuthorizationCodeFlow provideAuthorizationCodeFlow( public static GoogleAuthorizationCodeFlow provideAuthorizationCodeFlow(
JsonFactory jsonFactory, JsonFactory jsonFactory,
GoogleClientSecrets clientSecrets, GoogleClientSecrets clientSecrets,
@Config("requiredOauthScopes") ImmutableSet<String> requiredOauthScopes, @Config("localCredentialOauthScopes") ImmutableList<String> requiredOauthScopes,
AbstractDataStoreFactory dataStoreFactory) { AbstractDataStoreFactory dataStoreFactory) {
try { try {
return new GoogleAuthorizationCodeFlow.Builder( return new GoogleAuthorizationCodeFlow.Builder(
@ -83,17 +86,17 @@ public class AuthModule {
} }
@Provides @Provides
AuthorizationCodeInstalledApp provideAuthorizationCodeInstalledApp( public static AuthorizationCodeInstalledApp provideAuthorizationCodeInstalledApp(
GoogleAuthorizationCodeFlow flow) { GoogleAuthorizationCodeFlow flow) {
return new AuthorizationCodeInstalledApp(flow, new LocalServerReceiver()); return new AuthorizationCodeInstalledApp(flow, new LocalServerReceiver());
} }
@Provides @Provides
GoogleClientSecrets provideClientSecrets( public static GoogleClientSecrets provideClientSecrets(
@Config("clientSecretFilename") String clientSecretFilename, JsonFactory jsonFactory) { @Config("clientSecretFilename") String clientSecretFilename, JsonFactory jsonFactory) {
try { try {
// Load the client secrets file. // Load the client secrets file.
InputStream secretResourceStream = getClass().getResourceAsStream(clientSecretFilename); InputStream secretResourceStream = AuthModule.class.getResourceAsStream(clientSecretFilename);
if (secretResourceStream == null) { if (secretResourceStream == null) {
throw new RuntimeException("No client secret file found: " + clientSecretFilename); throw new RuntimeException("No client secret file found: " + clientSecretFilename);
} }
@ -105,19 +108,39 @@ public class AuthModule {
} }
@Provides @Provides
@OAuthClientId String provideClientId(GoogleClientSecrets clientSecrets) { @LocalCredentialStream
public static Supplier<InputStream> provideLocalCredentialStream(
GoogleClientSecrets clientSecrets, Credential credential) {
String json =
new Gson()
.toJson(
ImmutableMap.<String, String>builder()
.put("type", "authorized_user")
.put("client_id", clientSecrets.getDetails().getClientId())
.put("client_secret", clientSecrets.getDetails().getClientSecret())
.put("refresh_token", credential.getRefreshToken())
.build());
// A supplier is provided so that each binding gets a fresh stream, to avoid contention.
return () -> new ByteArrayInputStream(json.getBytes(UTF_8));
}
@Provides
@OAuthClientId
static String provideClientId(GoogleClientSecrets clientSecrets) {
return clientSecrets.getDetails().getClientId(); return clientSecrets.getDetails().getClientId();
} }
@Provides @Provides
@ClientScopeQualifier String provideClientScopeQualifier( @ClientScopeQualifier
@OAuthClientId String clientId, @Config("requiredOauthScopes") ImmutableSet<String> scopes) { static String provideClientScopeQualifier(
@OAuthClientId String clientId,
@Config("localCredentialOauthScopes") ImmutableList<String> scopes) {
return clientId + " " + Joiner.on(" ").join(Ordering.natural().sortedCopy(scopes)); return clientId + " " + Joiner.on(" ").join(Ordering.natural().sortedCopy(scopes));
} }
@Provides @Provides
@Singleton @Singleton
public AbstractDataStoreFactory provideDataStoreFactory() { public static AbstractDataStoreFactory provideDataStoreFactory() {
try { try {
return new FileDataStoreFactory(DATA_STORE_DIR); return new FileDataStoreFactory(DATA_STORE_DIR);
} catch (IOException ex) { } catch (IOException ex) {
@ -125,31 +148,26 @@ public class AuthModule {
} }
} }
/** Wrapper class to hold the login() function. */ /** Raised when we need a user login. */
public static class Authorizer { static class LoginRequiredException extends RuntimeException {
/** Initiate the login flow. */ LoginRequiredException() {}
public static void login(
GoogleAuthorizationCodeFlow flow,
@ClientScopeQualifier String clientScopeQualifier) throws IOException {
new AuthorizationCodeInstalledApp(flow, new LocalServerReceiver())
.authorize(clientScopeQualifier);
}
} }
/** Raised when we need a user login. */ /** Dagger qualifier for the JSON stream used to create the local credential. */
public static class LoginRequiredException extends RuntimeException { @Qualifier
public LoginRequiredException() {} @Documented
} @Retention(RetentionPolicy.RUNTIME)
@interface LocalCredentialStream {}
/** Dagger qualifier for the credential qualifier consisting of client and scopes. */ /** Dagger qualifier for the credential qualifier consisting of client and scopes. */
@Qualifier @Qualifier
@Documented @Documented
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface ClientScopeQualifier {} @interface ClientScopeQualifier {}
/** Dagger qualifier for the OAuth2 client id. */ /** Dagger qualifier for the OAuth2 client id. */
@Qualifier @Qualifier
@Documented @Documented
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface OAuthClientId {} @interface OAuthClientId {}
} }

View file

@ -83,6 +83,7 @@ java_library(
"@com_google_appengine_remote_api//:link", "@com_google_appengine_remote_api//:link",
"@com_google_auto_value", "@com_google_auto_value",
"@com_google_code_findbugs_jsr305", "@com_google_code_findbugs_jsr305",
"@com_google_code_gson",
"@com_google_dagger", "@com_google_dagger",
"@com_google_flogger", "@com_google_flogger",
"@com_google_flogger_system_backend", "@com_google_flogger_system_backend",

View file

@ -197,7 +197,8 @@ final class RegistryCli implements AutoCloseable, CommandRunner {
// Use dev credentials for localhost. // Use dev credentials for localhost.
options.useDevelopmentServerCredential(); options.useDevelopmentServerCredential();
} else { } else {
options.useApplicationDefaultCredential(); RemoteApiOptionsUtil.useGoogleCredentialStream(options, component
.googleCredentialStream().get());
} }
installer.install(options); installer.install(options);
} }

View file

@ -14,6 +14,7 @@
package google.registry.tools; package google.registry.tools;
import com.google.common.base.Supplier;
import dagger.Component; import dagger.Component;
import google.registry.bigquery.BigqueryModule; import google.registry.bigquery.BigqueryModule;
import google.registry.config.CredentialModule; import google.registry.config.CredentialModule;
@ -31,10 +32,12 @@ import google.registry.request.Modules.Jackson2Module;
import google.registry.request.Modules.URLFetchServiceModule; import google.registry.request.Modules.URLFetchServiceModule;
import google.registry.request.Modules.UrlFetchTransportModule; import google.registry.request.Modules.UrlFetchTransportModule;
import google.registry.request.Modules.UserServiceModule; import google.registry.request.Modules.UserServiceModule;
import google.registry.tools.AuthModule.LocalCredentialStream;
import google.registry.util.AppEngineServiceUtilsImpl.AppEngineServiceUtilsModule; import google.registry.util.AppEngineServiceUtilsImpl.AppEngineServiceUtilsModule;
import google.registry.util.SystemClock.SystemClockModule; import google.registry.util.SystemClock.SystemClockModule;
import google.registry.util.SystemSleeper.SystemSleeperModule; import google.registry.util.SystemSleeper.SystemSleeperModule;
import google.registry.whois.WhoisModule; import google.registry.whois.WhoisModule;
import java.io.InputStream;
import javax.inject.Singleton; import javax.inject.Singleton;
/** /**
@ -110,4 +113,7 @@ interface RegistryToolComponent {
void inject(WhoisQueryCommand command); void inject(WhoisQueryCommand command);
AppEngineConnection appEngineConnection(); AppEngineConnection appEngineConnection();
@LocalCredentialStream
Supplier<InputStream> googleCredentialStream();
} }

View file

@ -0,0 +1,43 @@
// Copyright 2018 The Nomulus Authors. All Rights Reserved.
//
// Licensed 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 google.registry.tools;
import static com.google.common.base.Preconditions.checkState;
import com.google.appengine.tools.remoteapi.RemoteApiOptions;
import java.io.InputStream;
import java.lang.reflect.Method;
/**
* Provides a method to access {@link RemoteApiOptions#useGoogleCredentialStream(InputStream)},
* which is a package private method.
*
* <p>This is obviously a hack, but until that method is exposed, we have to do this to set up the
* {@link RemoteApiOptions} with a JSON representing a user credential.
*/
public class RemoteApiOptionsUtil {
static RemoteApiOptions useGoogleCredentialStream(RemoteApiOptions options, InputStream stream)
throws Exception {
Method method =
options.getClass().getDeclaredMethod("useGoogleCredentialStream", InputStream.class);
checkState(
!method.isAccessible(),
"RemoteApiOptoins#useGoogleCredentialStream(InputStream) is accessible."
+ " Stop using RemoteApiOptionsUtil.");
method.setAccessible(true);
method.invoke(options, stream);
return options;
}
}

View file

@ -5,7 +5,7 @@
"auth_uri":"https://accounts.google.com/o/oauth2/auth", "auth_uri":"https://accounts.google.com/o/oauth2/auth",
"token_uri":"https://accounts.google.com/o/oauth2/token", "token_uri":"https://accounts.google.com/o/oauth2/token",
"auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs", "auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs",
"client_secret":"TBj4EcP5c0609ojiy2DIG6wE", "client_secret":"UNITTEST-CLIENT-SECRET",
"redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"] "redirect_uris":["urn:ietf:wg:oauth:2.0:oob","http://localhost"]
} }
} }

View file

@ -16,37 +16,63 @@ package google.registry.tools;
import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.JUnitBackports.assertThrows; import static google.registry.testing.JUnitBackports.assertThrows;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import com.google.api.client.auth.oauth2.ClientParametersAuthentication;
import com.google.api.client.auth.oauth2.Credential; import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.auth.oauth2.StoredCredential; import com.google.api.client.auth.oauth2.StoredCredential;
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets; import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
import com.google.api.client.http.GenericUrl;
import com.google.api.client.http.HttpRequest; import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.jackson2.JacksonFactory; import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.util.store.AbstractDataStoreFactory; import com.google.api.client.util.store.AbstractDataStoreFactory;
import com.google.api.client.util.store.DataStore; import com.google.api.client.util.store.DataStore;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableList;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Serializable; import java.io.Serializable;
import java.util.Map;
import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.junit.runners.JUnit4; import org.junit.runners.JUnit4;
/** Unit tests for {@link AuthModule}. */
@RunWith(JUnit4.class) @RunWith(JUnit4.class)
public class AuthModuleTest { public class AuthModuleTest {
private static final String TEST_CLIENT_SECRET_FILENAME = private static final String TEST_CLIENT_SECRET_FILENAME =
"/google/registry/tools/resources/client_secret_UNITTEST.json"; "/google/registry/tools/resources/client_secret_UNITTEST.json";
private static final Credential FAKE_CREDENTIAL = new Credential( private static final String CLIENT_ID = "UNITTEST-CLIENT-ID";
new Credential.AccessMethod() { private static final String CLIENT_SECRET = "UNITTEST-CLIENT-SECRET";
@Override private static final String ACCESS_TOKEN = "FakeAccessToken";
public void intercept(HttpRequest request, String accessToken) {} private static final String REFRESH_TOKEN = "FakeReFreshToken";
@Override private final Credential fakeCredential =
public String getAccessTokenFromRequest(HttpRequest request) { new Credential.Builder(
return "MockAccessToken"; new Credential.AccessMethod() {
} @Override
}); public void intercept(HttpRequest request, String accessToken) {}
@Override
public String getAccessTokenFromRequest(HttpRequest request) {
return ACCESS_TOKEN;
}
})
// We need to set the following fields because they are checked when
// Credential#setRefreshToken is called. However they are not actually persisted in the
// DataStore and not actually used in tests.
.setJsonFactory(new JacksonFactory())
.setTransport(new NetHttpTransport())
.setTokenServerUrl(new GenericUrl("https://accounts.google.com/o/oauth2/token"))
.setClientAuthentication(new ClientParametersAuthentication(CLIENT_ID, CLIENT_SECRET))
.build();
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
DataStore<StoredCredential> dataStore = mock(DataStore.class); DataStore<StoredCredential> dataStore = mock(DataStore.class);
@ -59,11 +85,18 @@ public class AuthModuleTest {
return result; return result;
} }
} }
@Before
public void setUp() throws Exception {
fakeCredential.setRefreshToken(REFRESH_TOKEN);
when(dataStore.get(CLIENT_ID + " scope1"))
.thenReturn(new StoredCredential(fakeCredential));
}
@Test @Test
public void test_clientScopeQualifier() { public void test_clientScopeQualifier() {
AuthModule authModule = new AuthModule();
String simpleQualifier = String simpleQualifier =
authModule.provideClientScopeQualifier("client-id", ImmutableSet.of("foo", "bar")); AuthModule.provideClientScopeQualifier("client-id", ImmutableList.of("foo", "bar"));
// If we change the way we encode client id and scopes, this assertion will break. That's // If we change the way we encode client id and scopes, this assertion will break. That's
// probably ok and you can just change the text. The things you have to be aware of are: // probably ok and you can just change the text. The things you have to be aware of are:
@ -73,59 +106,81 @@ public class AuthModuleTest {
assertThat(simpleQualifier).isEqualTo("client-id bar foo"); assertThat(simpleQualifier).isEqualTo("client-id bar foo");
// Verify order independence. // Verify order independence.
assertThat(simpleQualifier).isEqualTo( assertThat(simpleQualifier)
authModule.provideClientScopeQualifier("client-id", ImmutableSet.of("bar", "foo"))); .isEqualTo(
AuthModule.provideClientScopeQualifier("client-id", ImmutableList.of("bar", "foo")));
// Verify changing client id produces a different value. // Verify changing client id produces a different value.
assertThat(simpleQualifier).isNotEqualTo( assertThat(simpleQualifier)
authModule.provideClientScopeQualifier("new-client", ImmutableSet.of("bar", "foo"))); .isNotEqualTo(
AuthModule.provideClientScopeQualifier("new-client", ImmutableList.of("bar", "foo")));
// Verify that adding/deleting/modifying scopes produces a different value. // Verify that adding/deleting/modifying scopes produces a different value.
assertThat(simpleQualifier).isNotEqualTo( assertThat(simpleQualifier)
authModule.provideClientScopeQualifier("client id", ImmutableSet.of("bar", "foo", "baz"))); .isNotEqualTo(
assertThat(simpleQualifier).isNotEqualTo( AuthModule.provideClientScopeQualifier(
authModule.provideClientScopeQualifier("client id", ImmutableSet.of("barx", "foo"))); "client id", ImmutableList.of("bar", "foo", "baz")));
assertThat(simpleQualifier).isNotEqualTo( assertThat(simpleQualifier)
authModule.provideClientScopeQualifier("client id", ImmutableSet.of("bar", "foox"))); .isNotEqualTo(
assertThat(simpleQualifier).isNotEqualTo( AuthModule.provideClientScopeQualifier("client id", ImmutableList.of("barx", "foo")));
authModule.provideClientScopeQualifier("client id", ImmutableSet.of("bar"))); assertThat(simpleQualifier)
.isNotEqualTo(
AuthModule.provideClientScopeQualifier("client id", ImmutableList.of("bar", "foox")));
assertThat(simpleQualifier)
.isNotEqualTo(AuthModule.provideClientScopeQualifier("client id", ImmutableList.of("bar")));
// Verify that delimiting works. // Verify that delimiting works.
assertThat(simpleQualifier).isNotEqualTo( assertThat(simpleQualifier)
authModule.provideClientScopeQualifier("client-id", ImmutableSet.of("barf", "oo"))); .isNotEqualTo(
assertThat(simpleQualifier).isNotEqualTo( AuthModule.provideClientScopeQualifier("client-id", ImmutableList.of("barf", "oo")));
authModule.provideClientScopeQualifier("client-idb", ImmutableSet.of("ar", "foo"))); assertThat(simpleQualifier)
.isNotEqualTo(
AuthModule.provideClientScopeQualifier("client-idb", ImmutableList.of("ar", "foo")));
} }
private Credential getCredential() { private Credential getCredential() {
// Reconstruct the entire dependency graph, injecting FakeDatastoreFactory and credential // Reconstruct the entire dependency graph, injecting FakeDatastoreFactory and credential
// parameters. // parameters.
AuthModule authModule = new AuthModule();
JacksonFactory jsonFactory = new JacksonFactory(); JacksonFactory jsonFactory = new JacksonFactory();
GoogleClientSecrets clientSecrets = GoogleClientSecrets clientSecrets = getSecrets();
authModule.provideClientSecrets(TEST_CLIENT_SECRET_FILENAME, jsonFactory); ImmutableList<String> scopes = ImmutableList.of("scope1");
ImmutableSet<String> scopes = ImmutableSet.of("scope1"); return AuthModule.provideCredential(
return authModule.provideCredential( AuthModule.provideAuthorizationCodeFlow(
authModule.provideAuthorizationCodeFlow(
jsonFactory, clientSecrets, scopes, new FakeDataStoreFactory()), jsonFactory, clientSecrets, scopes, new FakeDataStoreFactory()),
authModule.provideClientScopeQualifier(authModule.provideClientId(clientSecrets), scopes)); AuthModule.provideClientScopeQualifier(AuthModule.provideClientId(clientSecrets), scopes));
}
private GoogleClientSecrets getSecrets() {
return AuthModule.provideClientSecrets(TEST_CLIENT_SECRET_FILENAME, new JacksonFactory());
} }
@Test @Test
public void test_provideCredential() throws Exception { public void test_provideLocalCredentialStream() {
when(dataStore.get("UNITTEST-CLIENT-ID scope1")).thenReturn( InputStream jsonStream =
new StoredCredential(FAKE_CREDENTIAL)); AuthModule.provideLocalCredentialStream(getSecrets(), getCredential()).get();
Map<String, String> jsonMap =
new Gson()
.fromJson(
new InputStreamReader(jsonStream, UTF_8),
new TypeToken<Map<String, String>>() {}.getType());
assertThat(jsonMap.get("type")).isEqualTo("authorized_user");
assertThat(jsonMap.get("client_secret")).isEqualTo(CLIENT_SECRET);
assertThat(jsonMap.get("client_id")).isEqualTo(CLIENT_ID);
assertThat(jsonMap.get("refresh_token")).isEqualTo(REFRESH_TOKEN);
}
@Test
public void test_provideCredential() {
Credential cred = getCredential(); Credential cred = getCredential();
assertThat(cred.getAccessToken()).isEqualTo(FAKE_CREDENTIAL.getAccessToken()); assertThat(cred.getAccessToken()).isEqualTo(fakeCredential.getAccessToken());
assertThat(cred.getRefreshToken()).isEqualTo(FAKE_CREDENTIAL.getRefreshToken()); assertThat(cred.getRefreshToken()).isEqualTo(fakeCredential.getRefreshToken());
assertThat(cred.getExpirationTimeMilliseconds()).isEqualTo( assertThat(cred.getExpirationTimeMilliseconds()).isEqualTo(
FAKE_CREDENTIAL.getExpirationTimeMilliseconds()); fakeCredential.getExpirationTimeMilliseconds());
} }
@Test @Test
public void test_provideCredential_notStored() { public void test_provideCredential_notStored() throws IOException {
// Doing this without the mock setup should cause us to throw an exception because the when(dataStore.get(CLIENT_ID + " scope1")).thenReturn(null);
// credential has not been stored.
assertThrows(AuthModule.LoginRequiredException.class, this::getCredential); assertThrows(AuthModule.LoginRequiredException.class, this::getCredential);
} }
} }

View file

@ -45,6 +45,7 @@ java_library(
"@com_google_appengine_remote_api", "@com_google_appengine_remote_api",
"@com_google_auto_value", "@com_google_auto_value",
"@com_google_code_findbugs_jsr305", "@com_google_code_findbugs_jsr305",
"@com_google_code_gson",
"@com_google_guava", "@com_google_guava",
"@com_google_http_client", "@com_google_http_client",
"@com_google_http_client_jackson2", "@com_google_http_client_jackson2",