Use local GoogleCredential in the nomulus tool

The scenarios in which the credential is used are:

1) Calls to Nomulus GAE HTTP endpoints.
2) Calls to Google APIs within the tool.
3) Calls to GAE APIs within the tool.

From now on the tool should not depend on ADCs created from gcloud any more (expect for beam pipeline deployments which need some more investigation as the dependency on ADC is not apparent). Using the nomulus tool requires running "nomulus login" first, but only once.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=224165735
This commit is contained in:
jianglai 2018-12-05 09:20:26 -08:00
parent 5f9aad90fc
commit fdda03eb53
12 changed files with 143 additions and 165 deletions

View file

@ -1237,14 +1237,6 @@ public final class RegistryConfig {
return ImmutableList.copyOf(config.credentialOAuth.localCredentialOauthScopes);
}
/** Provides the OAuth scopes required for access to App Engine Admin API. */
@Provides
@Config("appEngineAdminApiCredentialOauthScopes")
public static ImmutableList<String> provideAppEngineAdminApiCredentialOauthScopes(
RegistryConfigSettings config) {
return ImmutableList.copyOf(config.credentialOAuth.appEngineAdminApiCredentialOauthScopes);
}
/**
* Returns the help path for the RDAP terms of service.
*

View file

@ -59,7 +59,6 @@ public class RegistryConfigSettings {
public List<String> defaultCredentialOauthScopes;
public List<String> delegatedCredentialOauthScopes;
public List<String> localCredentialOauthScopes;
public List<String> appEngineAdminApiCredentialOauthScopes;
}
/** Configuration options for the G Suite account used by Nomulus. */

View file

@ -285,17 +285,15 @@ credentialOAuth:
- 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.
# View and manage data in all Google Cloud APIs.
- https://www.googleapis.com/auth/cloud-platform
# Call App Engine APIs locally.
- 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
# AppIdentityCredential.
appEngineAdminApiCredentialOauthScopes:
# View and manage your applications deployed on Google App Engine
- https://www.googleapis.com/auth/appengine.admin
icannReporting:
# URL we PUT monthly ICANN transactions reports to.
icannTransactionsReportingUploadUrl: https://ry-api.icann.org/report/registrar-transactions

View file

@ -44,6 +44,7 @@ icannReporting:
oAuth:
allowedOauthClientIds:
- placeholder.apps.googleusercontent.com
- placeholder-for-proxy
rde:
reportUrlPrefix: https://ry-api.icann.org/report/registry-escrow-report
@ -69,3 +70,7 @@ keyring:
activeKeyring: KMS
kms:
projectId: placeholder
registryTool:
clientId: placeholder.apps.googleusercontent.com
clientSecret: placeholder

View file

@ -14,13 +14,13 @@
package google.registry.tools;
import com.google.api.client.googleapis.extensions.appengine.auth.oauth2.AppIdentityCredential;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.googleapis.util.Utils;
import com.google.api.services.appengine.v1.Appengine;
import dagger.Module;
import dagger.Provides;
import google.registry.config.CredentialModule.AppEngineAdminApiCredential;
import google.registry.config.RegistryConfig.Config;
import google.registry.tools.AuthModule.LocalCredential;
import javax.inject.Singleton;
/** Module providing the instance of {@link Appengine} to access App Engine Admin Api. */
@ -30,10 +30,9 @@ public abstract class AppEngineAdminApiModule {
@Provides
@Singleton
public static Appengine provideAppengine(
@AppEngineAdminApiCredential AppIdentityCredential appIdentityCredential,
@Config("projectId") String projectId) {
@LocalCredential GoogleCredential credential, @Config("projectId") String projectId) {
return new Appengine.Builder(
Utils.getDefaultTransport(), Utils.getDefaultJsonFactory(), appIdentityCredential)
Utils.getDefaultTransport(), Utils.getDefaultJsonFactory(), credential)
.setApplicationName(projectId)
.build();
}

View file

@ -22,6 +22,7 @@ import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver;
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow;
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets.Details;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.util.store.AbstractDataStoreFactory;
@ -32,8 +33,10 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Ordering;
import com.google.gson.Gson;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import google.registry.config.CredentialModule.DefaultCredential;
import google.registry.config.RegistryConfig.Config;
import java.io.ByteArrayInputStream;
import java.io.File;
@ -54,8 +57,17 @@ public class AuthModule {
private static final File DATA_STORE_DIR =
new File(System.getProperty("user.home"), ".config/nomulus/credentials");
@Module
abstract static class LocalCredentialModule {
@Binds
@DefaultCredential
abstract GoogleCredential provideLocalCredentialAsDefaultCredential(
@LocalCredential GoogleCredential credential);
}
@Provides
public static Credential provideCredential(
@StoredCredential
static Credential provideCredential(
GoogleAuthorizationCodeFlow flow, @ClientScopeQualifier String clientScopeQualifier) {
try {
// Try to load the credentials, throw an exception if we fail.
@ -69,6 +81,17 @@ public class AuthModule {
}
}
@Provides
@LocalCredential
public static GoogleCredential provideLocalCredential(
@LocalCredentialStream Supplier<InputStream> credentialStream) {
try {
return GoogleCredential.fromStream(credentialStream.get());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Provides
public static GoogleAuthorizationCodeFlow provideAuthorizationCodeFlow(
JsonFactory jsonFactory,
@ -111,7 +134,7 @@ public class AuthModule {
@Provides
@LocalCredentialStream
public static Supplier<InputStream> provideLocalCredentialStream(
GoogleClientSecrets clientSecrets, Credential credential) {
GoogleClientSecrets clientSecrets, @StoredCredential Credential credential) {
String json =
new Gson()
.toJson(
@ -154,6 +177,24 @@ public class AuthModule {
LoginRequiredException() {}
}
/**
* Dagger qualifier for the {@link Credential} constructed from the data stored on disk.
*
* <p>This {@link Credential} should not be used in another module, hence the private qualifier.
* It's only use is to build a {@link GoogleCredential}, which is used in injection sites
* elsewhere.
*/
@Qualifier
@Documented
@Retention(RetentionPolicy.RUNTIME)
private @interface StoredCredential {}
/** Dagger qualifier for the local credential used in the nomulus tool. */
@Qualifier
@Documented
@Retention(RetentionPolicy.RUNTIME)
@interface LocalCredential {}
/** Dagger qualifier for the JSON stream used to create the local credential. */
@Qualifier
@Documented

View file

@ -1,73 +0,0 @@
// Copyright 2017 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 com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.javanet.NetHttpTransport;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
import google.registry.config.RegistryConfig;
import javax.inject.Named;
import javax.inject.Provider;
/**
* Module for providing the default HttpRequestFactory.
*
*
* <p>This module provides a standard NetHttpTransport-based HttpRequestFactory binding.
* The binding is qualified with the name named "default" and is not consumed directly. The
* RequestFactoryModule module binds the "default" HttpRequestFactory to the unqualified
* HttpRequestFactory, allowing users to override the actual, unqualified HttpRequestFactory
* binding by replacing RequestFactoryfModule with their own module, optionally providing
* the "default" factory in some circumstances.
*
* <p>Localhost connections go to the App Engine dev server. The dev server differs from most HTTP
* connections in that they don't require OAuth2 credentials, but instead require a special cookie.
*/
@Module
class DefaultRequestFactoryModule {
@Provides
@Named("default")
public HttpRequestFactory provideHttpRequestFactory(
Provider<Credential> credentialProvider) {
if (RegistryConfig.areServersLocal()) {
return new NetHttpTransport()
.createRequestFactory(
request -> request
.getHeaders()
.setCookie("dev_appserver_login=test@example.com:true:1858047912411"));
} else {
return new NetHttpTransport().createRequestFactory(credentialProvider.get());
}
}
/**
* Module for providing HttpRequestFactory.
*
* <p>Localhost connections go to the App Engine dev server. The dev server differs from most HTTP
* connections in that it doesn't require OAuth2 credentials, but instead requires a special
* cookie.
*/
@Module
abstract static class RequestFactoryModule {
@Binds
public abstract HttpRequestFactory provideHttpRequestFactory(
@Named("default") HttpRequestFactory requestFactory);
}
}

View file

@ -14,9 +14,6 @@
package google.registry.tools;
import com.google.appengine.tools.remoteapi.RemoteApiInstaller;
import com.google.appengine.tools.remoteapi.RemoteApiOptions;
import static com.google.common.base.Preconditions.checkState;
import static google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.tools.Injector.injectReflectively;
@ -26,10 +23,14 @@ import com.beust.jcommander.Parameter;
import com.beust.jcommander.ParameterException;
import com.beust.jcommander.Parameters;
import com.beust.jcommander.ParametersDelegate;
import com.google.appengine.tools.remoteapi.RemoteApiInstaller;
import com.google.appengine.tools.remoteapi.RemoteApiOptions;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import google.registry.config.RegistryConfig;
import google.registry.model.ofy.ObjectifyService;
import google.registry.tools.AuthModule.LoginRequiredException;
import google.registry.tools.params.ParameterFactory;
import java.net.URL;
import java.security.Security;
@ -55,7 +56,6 @@ final class RegistryCli implements AutoCloseable, CommandRunner {
description = "Returns all command names.")
private boolean showAllCommands;
// Do not make this final - compile-time constant inlining may interfere with JCommander.
@ParametersDelegate
private LoggingParameters loggingParams = new LoggingParameters();
@ -80,8 +80,7 @@ final class RegistryCli implements AutoCloseable, CommandRunner {
Security.addProvider(new BouncyCastleProvider());
component = DaggerRegistryToolComponent.builder()
.build();
component = DaggerRegistryToolComponent.create();
}
// The <? extends Class<? extends Command>> wildcard looks a little funny, but is needed so that
@ -156,18 +155,32 @@ final class RegistryCli implements AutoCloseable, CommandRunner {
try {
runCommand(command);
} catch (AuthModule.LoginRequiredException ex) {
} catch (RuntimeException ex) {
if (Throwables.getRootCause(ex) instanceof LoginRequiredException) {
System.err.println("===================================================================");
System.err.println("You must login using 'nomulus login' prior to running this command.");
System.err.println("===================================================================");
} else {
throw ex;
}
}
}
@Override
public void close() {
if (installer != null) {
try {
installer.uninstall();
installer = null;
} catch (IllegalArgumentException e) {
// There is no point throwing the error if the API is already uninstalled, which is most
// likely caused by something wrong when installing the API. That something (e. g. no
// credential found) must have already thrown an error message earlier (e. g. must run
// "nomulus login" first). This error message here is non-actionable.
if (!e.getMessage().equals("remote API is already uninstalled")) {
throw e;
}
}
}
}
@ -197,8 +210,8 @@ final class RegistryCli implements AutoCloseable, CommandRunner {
// Use dev credentials for localhost.
options.useDevelopmentServerCredential();
} else {
RemoteApiOptionsUtil.useGoogleCredentialStream(options, component
.googleCredentialStream().get());
RemoteApiOptionsUtil.useGoogleCredentialStream(
options, component.googleCredentialStream().get());
}
installer.install(options);
}

View file

@ -17,7 +17,6 @@ package google.registry.tools;
import com.google.common.base.Supplier;
import dagger.Component;
import google.registry.bigquery.BigqueryModule;
import google.registry.config.CredentialModule;
import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.dns.writer.VoidDnsWriterModule;
import google.registry.dns.writer.clouddns.CloudDnsWriterModule;
@ -32,6 +31,7 @@ import google.registry.request.Modules.Jackson2Module;
import google.registry.request.Modules.URLFetchServiceModule;
import google.registry.request.Modules.UrlFetchTransportModule;
import google.registry.request.Modules.UserServiceModule;
import google.registry.tools.AuthModule.LocalCredentialModule;
import google.registry.tools.AuthModule.LocalCredentialStream;
import google.registry.util.AppEngineServiceUtilsImpl.AppEngineServiceUtilsModule;
import google.registry.util.SystemClock.SystemClockModule;
@ -51,22 +51,20 @@ import javax.inject.Singleton;
modules = {
AppEngineAdminApiModule.class,
AppEngineServiceUtilsModule.class,
// TODO(b/36866706): Find a way to replace this with a command-line friendly version
AuthModule.class,
BigqueryModule.class,
ConfigModule.class,
CredentialModule.class,
CloudDnsWriterModule.class,
DatastoreServiceModule.class,
DummyKeyringModule.class,
CloudDnsWriterModule.class,
DefaultRequestFactoryModule.class,
DefaultRequestFactoryModule.RequestFactoryModule.class,
DnsUpdateWriterModule.class,
Jackson2Module.class,
KeyModule.class,
KeyringModule.class,
KmsModule.class,
LocalCredentialModule.class,
RdeModule.class,
RequestFactoryModule.class,
SystemClockModule.class,
SystemSleeperModule.class,
URLFetchServiceModule.class,
@ -117,3 +115,4 @@ interface RegistryToolComponent {
@LocalCredentialStream
Supplier<InputStream> googleCredentialStream();
}

View file

@ -0,0 +1,48 @@
// Copyright 2017 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 com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.javanet.NetHttpTransport;
import dagger.Module;
import dagger.Provides;
import google.registry.config.RegistryConfig;
import google.registry.tools.AuthModule.LocalCredential;
/**
* Module for providing the HttpRequestFactory.
*
* <p>Localhost connections go to the App Engine dev server. The dev server differs from most HTTP
* connections in that they don't require OAuth2 credentials, but instead require a special cookie.
*/
@Module
class RequestFactoryModule {
@Provides
static HttpRequestFactory provideHttpRequestFactory(
@LocalCredential GoogleCredential credential) {
if (RegistryConfig.areServersLocal()) {
return new NetHttpTransport()
.createRequestFactory(
request ->
request
.getHeaders()
.setCookie("dev_appserver_login=test@example.com:true:1858047912411"));
} else {
return new NetHttpTransport().createRequestFactory(credential);
}
}
}

View file

@ -1,32 +0,0 @@
// Copyright 2017 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;
/** Static methods to get the current user id. */
class UserIdProvider {
static String getTestUserId() {
return "test@example.com"; // Predefined default user for the development server.
}
/** Pick up the username from an appropriate source. */
static String getProdUserId() {
// TODO(b/28219927): fix tool authentication to use actual user credentials.
// For the time being, use the empty string so that for testing, requests without credentials
// can still pass the server-side XSRF token check (which will represent no user as "").
return "";
}
}

View file

@ -15,9 +15,9 @@
package google.registry.tools;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.mock;
import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.http.HttpRequestFactory;
import com.google.api.client.http.HttpRequestInitializer;
import google.registry.config.RegistryConfig;
@ -29,23 +29,12 @@ import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public class DefaultRequestFactoryModuleTest {
public class RequestFactoryModuleTest {
private static final Credential FAKE_CREDENTIAL = new Credential(
new Credential.AccessMethod() {
@Override
public void intercept(HttpRequest request, String accessToken) {}
@Override
public String getAccessTokenFromRequest(HttpRequest request) {
return "MockAccessToken";
}
});
private final GoogleCredential googleCredential = mock(GoogleCredential.class);
@Rule public final SystemPropertyRule systemPropertyRule = new SystemPropertyRule();
DefaultRequestFactoryModule module = new DefaultRequestFactoryModule();
@Before
public void setUp() {
RegistryToolEnvironment.UNITTEST.setup(systemPropertyRule);
@ -55,17 +44,17 @@ public class DefaultRequestFactoryModuleTest {
public void test_provideHttpRequestFactory_localhost() {
// Make sure that localhost creates a request factory with an initializer.
RegistryConfig.CONFIG_SETTINGS.get().appEngine.isLocal = true;
HttpRequestFactory factory = module.provideHttpRequestFactory(() -> FAKE_CREDENTIAL);
HttpRequestFactory factory = RequestFactoryModule.provideHttpRequestFactory(googleCredential);
HttpRequestInitializer initializer = factory.getInitializer();
assertThat(initializer).isNotNull();
assertThat(initializer).isNotSameAs(FAKE_CREDENTIAL);
assertThat(initializer).isNotSameAs(googleCredential);
}
@Test
public void test_provideHttpRequestFactory_remote() {
// Make sure that example.com creates a request factory with the UNITTEST client id but no
RegistryConfig.CONFIG_SETTINGS.get().appEngine.isLocal = false;
HttpRequestFactory factory = module.provideHttpRequestFactory(() -> FAKE_CREDENTIAL);
assertThat(factory.getInitializer()).isSameAs(FAKE_CREDENTIAL);
HttpRequestFactory factory = RequestFactoryModule.provideHttpRequestFactory(googleCredential);
assertThat(factory.getInitializer()).isSameAs(googleCredential);
}
}