diff --git a/build.gradle b/build.gradle index d6416b31b..9109966b4 100644 --- a/build.gradle +++ b/build.gradle @@ -564,14 +564,18 @@ task deployCloudSchedulerAndQueue { def env = environment if (!prodOrSandboxEnv) { exec { + workingDir "${rootDir}/release/builder/" commandLine 'go', 'run', - "${rootDir}/release/builder/deployCloudSchedulerAndQueue.go", + "./deployCloudSchedulerAndQueue.go", + "${rootDir}/core/src/main/java/google/registry/config/files/nomulus-config-${env}.yaml", "${rootDir}/core/src/main/java/google/registry/env/${env}/default/WEB-INF/cloud-scheduler-tasks.xml", "domain-registry-${env}" } exec { + workingDir "${rootDir}/release/builder/" commandLine 'go', 'run', - "${rootDir}/release/builder/deployCloudSchedulerAndQueue.go", + "./deployCloudSchedulerAndQueue.go", + "${rootDir}/core/src/main/java/google/registry/config/files/nomulus-config-${env}.yaml", "${rootDir}/core/src/main/java/google/registry/env/common/default/WEB-INF/cloud-tasks-queue.xml", "domain-registry-${env}" } diff --git a/core/src/main/java/google/registry/batch/CloudTasksUtils.java b/core/src/main/java/google/registry/batch/CloudTasksUtils.java index 55305032f..cf9e23439 100644 --- a/core/src/main/java/google/registry/batch/CloudTasksUtils.java +++ b/core/src/main/java/google/registry/batch/CloudTasksUtils.java @@ -20,8 +20,6 @@ import static google.registry.tools.ServiceConnection.getServer; import static java.util.concurrent.TimeUnit.SECONDS; import com.google.api.gax.rpc.ApiException; -import com.google.cloud.tasks.v2.AppEngineHttpRequest; -import com.google.cloud.tasks.v2.AppEngineRouting; import com.google.cloud.tasks.v2.CloudTasksClient; import com.google.cloud.tasks.v2.HttpMethod; import com.google.cloud.tasks.v2.HttpRequest; @@ -39,10 +37,12 @@ import com.google.common.net.MediaType; import com.google.common.net.UrlEscapers; import com.google.protobuf.ByteString; import com.google.protobuf.util.Timestamps; +import google.registry.config.CredentialModule.ApplicationDefaultCredential; import google.registry.config.RegistryConfig.Config; import google.registry.request.Action.Service; import google.registry.util.Clock; import google.registry.util.CollectionUtils; +import google.registry.util.GoogleCredentialsBundle; import google.registry.util.Retrier; import java.io.Serializable; import java.nio.charset.StandardCharsets; @@ -52,7 +52,6 @@ import java.util.Random; import java.util.function.BiConsumer; import java.util.function.Consumer; import java.util.function.Supplier; -import javax.annotation.Nullable; import javax.inject.Inject; import org.joda.time.Duration; @@ -67,9 +66,8 @@ public class CloudTasksUtils implements Serializable { private final Clock clock; private final String projectId; private final String locationId; - // defaultServiceAccount and iapClientId are nullable because Optional isn't serializable - @Nullable private final String defaultServiceAccount; - @Nullable private final String iapClientId; + private final String oauthClientId; + private final GoogleCredentialsBundle credential; private final SerializableCloudTasksClient client; @Inject @@ -78,15 +76,15 @@ public class CloudTasksUtils implements Serializable { Clock clock, @Config("projectId") String projectId, @Config("locationId") String locationId, - @Config("defaultServiceAccount") Optional defaultServiceAccount, - @Config("iapClientId") Optional iapClientId, + @Config("oauthClientId") String oauthClientId, + @ApplicationDefaultCredential GoogleCredentialsBundle credential, SerializableCloudTasksClient client) { this.retrier = retrier; this.clock = clock; this.projectId = projectId; this.locationId = locationId; - this.defaultServiceAccount = defaultServiceAccount.orElse(null); - this.iapClientId = iapClientId.orElse(null); + this.oauthClientId = oauthClientId; + this.credential = credential; this.client = client; } @@ -124,7 +122,7 @@ public class CloudTasksUtils implements Serializable { * * @return the resulting path (unchanged for POST requests, with params added for GET requests) */ - private String processRequestParameters( + private static String processRequestParameters( String path, HttpMethod method, Multimap params, @@ -152,43 +150,20 @@ public class CloudTasksUtils implements Serializable { return path; } - /** - * Creates a {@link Task} that does not use AppEngine for submission. - * - *

This uses the standard Cloud Tasks auth format to create and send an OIDC ID token set to - * the default service account. That account must have permission to submit tasks to Cloud Tasks. - */ - private Task createNonAppEngineTask( - String path, HttpMethod method, Service service, Multimap params) { - HttpRequest.Builder requestBuilder = HttpRequest.newBuilder().setHttpMethod(method); - path = - processRequestParameters( - path, method, params, requestBuilder::putHeaders, requestBuilder::setBody); - OidcToken.Builder oidcTokenBuilder = - OidcToken.newBuilder().setServiceAccountEmail(defaultServiceAccount); - // If the service is using IAP, add that as the audience for the token so the request can be - // appropriately authed. Otherwise, use the project name. - if (iapClientId != null) { - oidcTokenBuilder.setAudience(iapClientId); - } else { - oidcTokenBuilder.setAudience(projectId); - } - requestBuilder.setOidcToken(oidcTokenBuilder.build()); - String totalPath = String.format("%s%s", getServer(service), path); - requestBuilder.setUrl(totalPath); - return Task.newBuilder().setHttpRequest(requestBuilder.build()).build(); - } - /** * Create a {@link Task} to be enqueued. * + *

This uses the standard Cloud Tasks auth format to create and send an OIDC ID token with the + * default service account as the principal. That account must have permission to submit tasks to + * Cloud Tasks. + * * @param path the relative URI (staring with a slash and ending without one). * @param method the HTTP method to be used for the request, only GET and POST are supported. * @param service the App Engine service to route the request to. Note that with App Engine Task * Queue API if no service is specified, the service which enqueues the task will be used to * process the task. Cloud Tasks API does not support this feature so the service will always * needs to be explicitly specified. - * @param params a multi-map of URL query parameters. Duplicate keys are saved as is, and it is up + * @param params a multimap of URL query parameters. Duplicate keys are saved as is, and it is up * to the server to process the duplicate keys. * @return the enqueued task. * @see provideServiceAccountEmails(RegistryConfigSettings config) { - return ImmutableList.copyOf(config.gcpProject.serviceAccountEmails); - } - - @Provides - @Config("defaultServiceAccount") - public static Optional provideDefaultServiceAccount(RegistryConfigSettings config) { - return Optional.ofNullable(config.gcpProject.defaultServiceAccount); - } /** * The filename of the logo to be displayed in the header of the registrar console. @@ -257,7 +248,7 @@ public final class RegistryConfig { @Provides @Config("databaseRetention") public static Duration provideDatabaseRetention() { - return RegistryConfig.getDatabaseRetention(); + return getDatabaseRetention(); } /** @@ -304,7 +295,7 @@ public final class RegistryConfig { * The maximum number of domain and host updates to batch together to send to * PublishDnsUpdatesAction, to avoid exceeding HTTP request timeout limits. * - * @see google.registry.dns.ReadDnsRefreshRequestsAction + * @see ReadDnsRefreshRequestsAction */ @Provides @Config("dnsTldUpdateBatchSize") @@ -1144,7 +1135,7 @@ public final class RegistryConfig { @Provides @Config("availableOauthScopes") public static ImmutableSet provideAvailableOauthScopes(RegistryConfigSettings config) { - return ImmutableSet.copyOf(config.oAuth.availableOauthScopes); + return ImmutableSet.copyOf(config.auth.availableOauthScopes); } /** @@ -1157,27 +1148,38 @@ public final class RegistryConfig { * API, which requires at least one of: * *

*/ @Provides @Config("requiredOauthScopes") public static ImmutableSet provideRequiredOauthScopes(RegistryConfigSettings config) { - return ImmutableSet.copyOf(config.oAuth.requiredOauthScopes); + return ImmutableSet.copyOf(config.auth.requiredOauthScopes); + } + + /** + * Provides service account email addresses allowed to authenticate with the app at {@link + * google.registry.request.auth.AuthSettings.AuthLevel#APP} level. + */ + @Provides + @Config("allowedServiceAccountEmails") + public static ImmutableSet provideAllowedServiceAccountEmails( + RegistryConfigSettings config) { + return ImmutableSet.copyOf(config.auth.allowedServiceAccountEmails); } /** Provides the allowed OAuth client IDs (could be multibinding). */ @Provides @Config("allowedOauthClientIds") public static ImmutableSet provideAllowedOauthClientIds(RegistryConfigSettings config) { - return ImmutableSet.copyOf(config.oAuth.allowedOauthClientIds); + return ImmutableSet.copyOf(config.auth.allowedOauthClientIds); } @Provides - @Config("iapClientId") - public static Optional provideIapClientId(RegistryConfigSettings config) { - return Optional.ofNullable(config.oAuth.iapClientId); + @Config("oauthClientId") + public static String provideOauthClientId(RegistryConfigSettings config) { + return config.auth.oauthClientId; } /** @@ -1253,7 +1255,7 @@ public final class RegistryConfig { toImmutableSortedMap( naturalOrder(), e -> - e.getKey().equals("START_OF_TIME") + "START_OF_TIME".equals(e.getKey()) ? START_OF_TIME : DateTime.parse(e.getKey()), Entry::getValue)); @@ -1374,6 +1376,12 @@ public final class RegistryConfig { public static String providePackageDomainLimitUpgradeEmailBody(RegistryConfigSettings config) { return config.packageMonitoring.packageDomainLimitUpgradeEmailBody; } + + private static String formatComments(String text) { + return Splitter.on('\n').omitEmptyStrings().trimResults().splitToList(text).stream() + .map(s -> "# " + s) + .collect(Collectors.joining("\n")); + } } /** Returns the App Engine project ID, which is based off the environment name. */ @@ -1539,9 +1547,9 @@ public final class RegistryConfig { * one single INSERT statement which can dramatically increase speed in situations with many * inserts. * - *

Hibernate docs, i.e. - * https://docs.jboss.org/hibernate/orm/5.6/userguide/html_single/Hibernate_User_Guide.html, - * recommend between 10 and 50. + *

Hibernate + * User Guide recommends between 10 and 50. */ public static int getHibernateJdbcBatchSize() { return CONFIG_SETTINGS.get().hibernate.jdbcBatchSize; @@ -1578,11 +1586,7 @@ public final class RegistryConfig { public static final Supplier CONFIG_SETTINGS = memoize(RegistryConfig::getConfigSettings); - private static String formatComments(String text) { - return Splitter.on('\n').omitEmptyStrings().trimResults().splitToList(text).stream() - .map(s -> "# " + s) - .collect(Collectors.joining("\n")); - } + private static InternetAddress parseEmailAddress(String email) { try { diff --git a/core/src/main/java/google/registry/config/RegistryConfigSettings.java b/core/src/main/java/google/registry/config/RegistryConfigSettings.java index cd8d9aa9e..42af240ba 100644 --- a/core/src/main/java/google/registry/config/RegistryConfigSettings.java +++ b/core/src/main/java/google/registry/config/RegistryConfigSettings.java @@ -23,7 +23,7 @@ public class RegistryConfigSettings { public GcpProject gcpProject; public GSuite gSuite; - public OAuth oAuth; + public Auth auth; public CredentialOAuth credentialOAuth; public RegistryPolicy registryPolicy; public Hibernate hibernate; @@ -54,16 +54,15 @@ public class RegistryConfigSettings { public String backendServiceUrl; public String toolsServiceUrl; public String pubapiServiceUrl; - public List serviceAccountEmails; - public String defaultServiceAccount; } - /** Configuration options for OAuth settings for authenticating users. */ - public static class OAuth { + /** Configuration options for authenticating users. */ + public static class Auth { public List availableOauthScopes; public List requiredOauthScopes; public List allowedOauthClientIds; - public String iapClientId; + public List allowedServiceAccountEmails; + public String oauthClientId; } /** Configuration options for accessing Google APIs. */ diff --git a/core/src/main/java/google/registry/config/files/default-config.yaml b/core/src/main/java/google/registry/config/files/default-config.yaml index f41f4fe30..70d5a7d19 100644 --- a/core/src/main/java/google/registry/config/files/default-config.yaml +++ b/core/src/main/java/google/registry/config/files/default-config.yaml @@ -18,18 +18,10 @@ gcpProject: # whether to use local/test credentials when connecting to the servers isLocal: true # URLs of the services for the project. - defaultServiceUrl: https://localhost - backendServiceUrl: https://localhost - toolsServiceUrl: https://localhost - pubapiServiceUrl: https://localhost - # Service accounts eligible for authorization (e.g. default service account, - # account used by Cloud Scheduler) to send authenticated requests. - serviceAccountEmails: - - default-service-account-email@email.com - - cloud-scheduler-email@email.com - # The default service account with which the service is running. For example, - # on GAE this would be {project-id}@appspot.gserviceaccount.com - defaultServiceAccount: null + defaultServiceUrl: https://default.example.com + backendServiceUrl: https://backend.example.com + toolsServiceUrl: https://tools.example.com + pubapiServiceUrl: https://pubapi.example.com gSuite: # Publicly accessible domain name of the running G Suite instance. @@ -295,24 +287,41 @@ caching: # long duration is acceptable because claims lists don't change frequently. claimsListCachingSeconds: 21600 # six hours -oAuth: +# Note: Only allowedServiceAccountEmails and oauthClientId should be configured. +# Other fields are related to OAuth-based authentication and will be removed. +auth: + # Deprecated: Use OIDC-based auth instead. This field is for OAuth-based auth. # OAuth scopes to detect on access tokens. Superset of requiredOauthScopes. availableOauthScopes: - https://www.googleapis.com/auth/userinfo.email + # Deprecated: Use OIDC-based auth instead. This field is for OAuth-based auth. # OAuth scopes required for authenticating. Subset of availableOauthScopes. requiredOauthScopes: - https://www.googleapis.com/auth/userinfo.email + # Deprecated: Use OIDC-based auth instead. This field is for OAuth-based auth. # OAuth client IDs that are allowed to authenticate and communicate with - # backend services, e. g. nomulus tool, EPP proxy, etc. The client_id value - # used in registryTool.clientId field for associated tooling should be included - # in this list. Client IDs are typically of the format + # backend services, e.g. nomulus tool, EPP proxy, etc. The value in + # registryTool.clientId field should be included in this list. Client IDs are + # typically of the format # numbers-alphanumerics.apps.googleusercontent.com allowedOauthClientIds: [] - # GCP Identity-Aware Proxy client ID, if set up (note: this requires manual setup - # of User objects in the database for Nomulus tool users) - iapClientId: null + + # Service accounts (e.g. default service account, account used by Cloud + # Scheduler) allowed to send authenticated requests. + allowedServiceAccountEmails: + - default-service-account-email@email.com + - cloud-scheduler-email@email.com + + # OAuth 2.0 client ID that will be used as the audience in OIDC ID tokens sent + # from clients (e.g. proxy, nomulus tool, cloud tasks) for authentication. The + # same ID is the only one accepted by the regular OIDC or IAP authentication + # mechanisms. In most cases we should use the client ID created for IAP here, + # as it allows requests bearing a token with this audience to be accepted by + # both IAP or regular OIDC. The clientId value in proxy config file should be + # the same as this one. + oauthClientId: iap-oauth-clientid credentialOAuth: # OAuth scopes required for accessing Google APIs using the default diff --git a/core/src/main/java/google/registry/cron/TldFanoutAction.java b/core/src/main/java/google/registry/cron/TldFanoutAction.java index 73733b955..6073cf74f 100644 --- a/core/src/main/java/google/registry/cron/TldFanoutAction.java +++ b/core/src/main/java/google/registry/cron/TldFanoutAction.java @@ -140,25 +140,13 @@ public final class TldFanoutAction implements Runnable { for (String tld : tlds) { Task task = createTask(tld, flowThruParams); Task createdTask = cloudTasksUtils.enqueue(queue, task); - if (createdTask.hasAppEngineHttpRequest()) { - outputPayload.append( - String.format( - "- Task: '%s', tld: '%s', endpoint: '%s'\n", - createdTask.getName(), - tld, - createdTask.getAppEngineHttpRequest().getRelativeUri())); - logger.atInfo().log( - "Task: '%s', tld: '%s', endpoint: '%s'.", - createdTask.getName(), tld, createdTask.getAppEngineHttpRequest().getRelativeUri()); - } else { - outputPayload.append( - String.format( - "- Task: '%s', tld: '%s', endpoint: '%s'\n", - createdTask.getName(), tld, createdTask.getHttpRequest().getUrl())); - logger.atInfo().log( - "Task: '%s', tld: '%s', endpoint: '%s'.", - createdTask.getName(), tld, createdTask.getHttpRequest().getUrl()); - } + outputPayload.append( + String.format( + "- Task: '%s', tld: '%s', endpoint: '%s'\n", + createdTask.getName(), tld, createdTask.getHttpRequest().getUrl())); + logger.atInfo().log( + "Task: '%s', tld: '%s', endpoint: '%s'.", + createdTask.getName(), tld, createdTask.getHttpRequest().getUrl()); } response.setContentType(PLAIN_TEXT_UTF_8); response.setPayload(outputPayload.toString()); diff --git a/core/src/main/java/google/registry/request/auth/AuthModule.java b/core/src/main/java/google/registry/request/auth/AuthModule.java index 08586b04c..b416e5b00 100644 --- a/core/src/main/java/google/registry/request/auth/AuthModule.java +++ b/core/src/main/java/google/registry/request/auth/AuthModule.java @@ -44,7 +44,7 @@ public class AuthModule { // See: https://cloud.google.com/iap/docs/signed-headers-howto#verifying_the_jwt_payload private static final String IAP_AUDIENCE_FORMAT = "/projects/%d/apps/%s"; private static final String IAP_ISSUER_URL = "https://cloud.google.com/iap"; - private static final String SA_ISSUER_URL = "https://accounts.google.com"; + private static final String REGULAR_ISSUER_URL = "https://accounts.google.com"; /** Provides the custom authentication mechanisms (including OAuth and OIDC). */ @Provides @@ -82,8 +82,8 @@ public class AuthModule { @Provides @RegularOidc @Singleton - TokenVerifier provideRegularTokenVerifier(@Config("projectId") String projectId) { - return TokenVerifier.newBuilder().setAudience(projectId).setIssuer(SA_ISSUER_URL).build(); + TokenVerifier provideRegularTokenVerifier(@Config("oauthClientId") String clientId) { + return TokenVerifier.newBuilder().setAudience(clientId).setIssuer(REGULAR_ISSUER_URL).build(); } @Provides diff --git a/core/src/main/java/google/registry/request/auth/OAuthAuthenticationMechanism.java b/core/src/main/java/google/registry/request/auth/OAuthAuthenticationMechanism.java index 933cdabea..a27dcf79d 100644 --- a/core/src/main/java/google/registry/request/auth/OAuthAuthenticationMechanism.java +++ b/core/src/main/java/google/registry/request/auth/OAuthAuthenticationMechanism.java @@ -32,7 +32,7 @@ import javax.servlet.http.HttpServletRequest; /** * OAuth authentication mechanism, using the OAuthService interface. * - * Only OAuth version 2 is supported. + *

Only OAuth version 2 is supported. */ public class OAuthAuthenticationMechanism implements AuthenticationMechanism { diff --git a/core/src/main/java/google/registry/request/auth/OidcTokenAuthenticationMechanism.java b/core/src/main/java/google/registry/request/auth/OidcTokenAuthenticationMechanism.java index de6fa70cc..9efaa96cc 100644 --- a/core/src/main/java/google/registry/request/auth/OidcTokenAuthenticationMechanism.java +++ b/core/src/main/java/google/registry/request/auth/OidcTokenAuthenticationMechanism.java @@ -19,7 +19,7 @@ import static google.registry.request.auth.AuthSettings.AuthLevel.APP; import com.google.api.client.json.webtoken.JsonWebSignature; import com.google.auth.oauth2.TokenVerifier; import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import com.google.common.flogger.FluentLogger; import google.registry.config.RegistryConfig.Config; import google.registry.config.RegistryEnvironment; @@ -57,10 +57,10 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication protected final TokenExtractor tokenExtractor; - private final ImmutableList serviceAccountEmails; + private final ImmutableSet serviceAccountEmails; protected OidcTokenAuthenticationMechanism( - ImmutableList serviceAccountEmails, + ImmutableSet serviceAccountEmails, TokenVerifier tokenVerifier, TokenExtractor tokenExtractor) { this.serviceAccountEmails = serviceAccountEmails; @@ -83,7 +83,11 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication try { token = tokenVerifier.verify(rawIdToken); } catch (Exception e) { - logger.atInfo().withCause(e).log("Error when verifying access token"); + logger.atInfo().withCause(e).log( + "Failed OIDC verification attempt:\n%s", + RegistryEnvironment.get().equals(RegistryEnvironment.PRODUCTION) + ? "Raw token redacted in prod" + : rawIdToken); return AuthResult.NOT_AUTHENTICATED; } String email = (String) token.getPayload().get("email"); @@ -95,6 +99,7 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication if (maybeUser.isPresent()) { return AuthResult.create(AuthLevel.USER, UserAuthInfo.create(maybeUser.get())); } + // TODO: implement caching so we don't have to look up the database for every request. logger.atInfo().log("No end user found for email address %s", email); if (serviceAccountEmails.stream().anyMatch(e -> e.equals(email))) { return AuthResult.create(APP); @@ -136,7 +141,7 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication @Inject protected IapOidcAuthenticationMechanism( - @Config("serviceAccountEmails") ImmutableList serviceAccountEmails, + @Config("allowedServiceAccountEmails") ImmutableSet serviceAccountEmails, @IapOidc TokenVerifier tokenVerifier, @IapOidc TokenExtractor tokenExtractor) { super(serviceAccountEmails, tokenVerifier, tokenExtractor); @@ -164,7 +169,7 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication @Inject protected RegularOidcAuthenticationMechanism( - @Config("serviceAccountEmails") ImmutableList serviceAccountEmails, + @Config("allowedServiceAccountEmails") ImmutableSet serviceAccountEmails, @RegularOidc TokenVerifier tokenVerifier, @RegularOidc TokenExtractor tokenExtractor) { super(serviceAccountEmails, tokenVerifier, tokenExtractor); diff --git a/core/src/main/java/google/registry/tools/RequestFactoryModule.java b/core/src/main/java/google/registry/tools/RequestFactoryModule.java index cf6020584..6fa0d6eac 100644 --- a/core/src/main/java/google/registry/tools/RequestFactoryModule.java +++ b/core/src/main/java/google/registry/tools/RequestFactoryModule.java @@ -14,23 +14,17 @@ package google.registry.tools; -import com.google.api.client.http.GenericUrl; -import com.google.api.client.http.HttpRequest; +import static com.google.common.net.HttpHeaders.PROXY_AUTHORIZATION; + import com.google.api.client.http.HttpRequestFactory; -import com.google.api.client.http.HttpResponse; -import com.google.api.client.http.UrlEncodedContent; import com.google.api.client.http.javanet.NetHttpTransport; -import com.google.api.client.util.GenericData; -import com.google.auth.oauth2.UserCredentials; import dagger.Module; import dagger.Provides; import google.registry.config.CredentialModule.ApplicationDefaultCredential; import google.registry.config.RegistryConfig; import google.registry.config.RegistryConfig.Config; import google.registry.util.GoogleCredentialsBundle; -import java.io.IOException; -import java.net.URI; -import java.util.Optional; +import google.registry.util.OidcTokenUtils; /** * Module for providing the HttpRequestFactory. @@ -39,25 +33,16 @@ import java.util.Optional; * connections in that they don't require OAuth2 credentials, but instead require a special cookie. */ @Module -class RequestFactoryModule { +final class RequestFactoryModule { static final int REQUEST_TIMEOUT_MS = 10 * 60 * 1000; - /** - * Server to use if we want to manually request an IAP ID token - * - *

If we need to have an IAP-enabled audience, we can use the existing refresh token and the - * IAP client ID audience to request an IAP-enabled ID token. This token is read and used by - * {@link IapHeaderAuthenticationMechanismMechanism}, and it requires that the user have a {@link - * google.registry.model.console.User} object present in the database. - */ - private static final GenericUrl TOKEN_SERVER_URL = - new GenericUrl(URI.create("https://oauth2.googleapis.com/token")); + private RequestFactoryModule() {} @Provides static HttpRequestFactory provideHttpRequestFactory( @ApplicationDefaultCredential GoogleCredentialsBundle credentialsBundle, - @Config("iapClientId") Optional iapClientId) { + @Config("oauthClientId") String oauthClientId) { if (RegistryConfig.areServersLocal()) { return new NetHttpTransport() .createRequestFactory( @@ -71,14 +56,13 @@ class RequestFactoryModule { request -> { // Use the standard credential initializer to set the Authorization header credentialsBundle.getHttpRequestInitializer().initialize(request); - // If using IAP, use the refresh token to acquire an IAP-enabled ID token and use - // that for authentication. - if (iapClientId.isPresent()) { - String idToken = getIdToken(credentialsBundle, iapClientId.get()); - // Set the Proxy-Authentication header so that IAP can read from it, see - // https://cloud.google.com/iap/docs/authentication-howto#authenticating_from_proxy-authorization_header - request.getHeaders().set("Proxy-Authorization", "Bearer " + idToken); - } + // Set OIDC token as the alternative bearer token. + request + .getHeaders() + .set( + PROXY_AUTHORIZATION, + "Bearer " + + OidcTokenUtils.createOidcToken(credentialsBundle, oauthClientId)); // GAE request times out after 10 min, so here we set the timeout to 10 min. This is // needed to support some nomulus commands like updating premium lists that take // a lot of time to complete. @@ -89,32 +73,4 @@ class RequestFactoryModule { }); } } - - /** - * Uses the saved desktop-app refresh token to acquire an IAP ID token. - * - *

This is lifted mostly from the Google Auth Library's {@link UserCredentials} - * "doRefreshAccessToken" method (which is private and thus inaccessible) while adding in the - * audience of the IAP client ID. That addition of the audience is what allows us to satisfy IAP - * auth. See - * https://cloud.google.com/iap/docs/authentication-howto#authenticating_from_a_desktop_app for - * more details. - */ - private static String getIdToken(GoogleCredentialsBundle credentialsBundle, String iapClientId) - throws IOException { - UserCredentials credentials = (UserCredentials) credentialsBundle.getGoogleCredentials(); - GenericData tokenRequest = new GenericData(); - tokenRequest.set("client_id", credentials.getClientId()); - tokenRequest.set("client_secret", credentials.getClientSecret()); - tokenRequest.set("refresh_token", credentials.getRefreshToken()); - tokenRequest.set("audience", iapClientId); - tokenRequest.set("grant_type", "refresh_token"); - UrlEncodedContent content = new UrlEncodedContent(tokenRequest); - - HttpRequestFactory requestFactory = credentialsBundle.getHttpTransport().createRequestFactory(); - HttpRequest request = requestFactory.buildPostRequest(TOKEN_SERVER_URL, content); - request.setParser(credentialsBundle.getJsonFactory().createJsonObjectParser()); - HttpResponse response = request.execute(); - return response.parseAs(GenericData.class).get("id_token").toString(); - } } diff --git a/core/src/test/java/google/registry/batch/AsyncTaskEnqueuerTest.java b/core/src/test/java/google/registry/batch/AsyncTaskEnqueuerTest.java index 05db49c01..d75a16319 100644 --- a/core/src/test/java/google/registry/batch/AsyncTaskEnqueuerTest.java +++ b/core/src/test/java/google/registry/batch/AsyncTaskEnqueuerTest.java @@ -73,7 +73,7 @@ public class AsyncTaskEnqueuerTest { cloudTasksHelper.assertTasksEnqueued( QUEUE_ASYNC_ACTIONS, new CloudTasksHelper.TaskMatcher() - .url(ResaveEntityAction.PATH) + .path(ResaveEntityAction.PATH) .method(HttpMethod.POST) .service("backend") .header("content-type", "application/x-www-form-urlencoded") @@ -93,7 +93,7 @@ public class AsyncTaskEnqueuerTest { cloudTasksHelper.assertTasksEnqueued( QUEUE_ASYNC_ACTIONS, new TaskMatcher() - .url(ResaveEntityAction.PATH) + .path(ResaveEntityAction.PATH) .method(HttpMethod.POST) .service("backend") .header("content-type", "application/x-www-form-urlencoded") diff --git a/core/src/test/java/google/registry/batch/CloudTasksUtilsTest.java b/core/src/test/java/google/registry/batch/CloudTasksUtilsTest.java index ef0c06070..f99fcbe27 100644 --- a/core/src/test/java/google/registry/batch/CloudTasksUtilsTest.java +++ b/core/src/test/java/google/registry/batch/CloudTasksUtilsTest.java @@ -30,6 +30,7 @@ import com.google.common.collect.ImmutableMultimap; import com.google.common.collect.LinkedListMultimap; import google.registry.batch.CloudTasksUtils.SerializableCloudTasksClient; import google.registry.request.Action.Service; +import google.registry.testing.CloudTasksHelper.FakeGoogleCredentialsBundle; import google.registry.testing.FakeClock; import google.registry.testing.FakeSleeper; import google.registry.util.Retrier; @@ -47,14 +48,14 @@ public class CloudTasksUtilsTest { private final LinkedListMultimap params = LinkedListMultimap.create(); private final SerializableCloudTasksClient mockClient = mock(SerializableCloudTasksClient.class); private final FakeClock clock = new FakeClock(DateTime.parse("2021-11-08")); - private CloudTasksUtils cloudTasksUtils = + private final CloudTasksUtils cloudTasksUtils = new CloudTasksUtils( new Retrier(new FakeSleeper(clock), 1), clock, "project", "location", - Optional.empty(), - Optional.empty(), + "clientId", + FakeGoogleCredentialsBundle.create(), mockClient); @BeforeEach @@ -66,208 +67,6 @@ public class CloudTasksUtilsTest { .thenAnswer(invocation -> invocation.getArgument(3)); } - @Test - void testSuccess_createGetTasks() { - Task task = cloudTasksUtils.createGetTask("/the/path", Service.BACKEND, params); - assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.GET); - assertThat(task.getAppEngineHttpRequest().getRelativeUri()) - .isEqualTo("/the/path?key1=val1&key2=val2&key1=val3"); - assertThat(task.getAppEngineHttpRequest().getAppEngineRouting().getService()) - .isEqualTo(Service.BACKEND.toString()); - assertThat(task.getScheduleTime().getSeconds()).isEqualTo(0); - } - - @Test - void testSuccess_createPostTasks() { - Task task = cloudTasksUtils.createPostTask("/the/path", Service.BACKEND, params); - assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.POST); - assertThat(task.getAppEngineHttpRequest().getRelativeUri()).isEqualTo("/the/path"); - assertThat(task.getAppEngineHttpRequest().getAppEngineRouting().getService()) - .isEqualTo(Service.BACKEND.toString()); - assertThat(task.getAppEngineHttpRequest().getHeadersMap().get("Content-Type")) - .isEqualTo("application/x-www-form-urlencoded"); - assertThat(task.getAppEngineHttpRequest().getBody().toString(StandardCharsets.UTF_8)) - .isEqualTo("key1=val1&key2=val2&key1=val3"); - assertThat(task.getScheduleTime().getSeconds()).isEqualTo(0); - } - - @Test - void testSuccess_createGetTasks_withNullParams() { - Task task = cloudTasksUtils.createGetTask("/the/path", Service.BACKEND, null); - assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.GET); - assertThat(task.getAppEngineHttpRequest().getRelativeUri()).isEqualTo("/the/path"); - assertThat(task.getAppEngineHttpRequest().getAppEngineRouting().getService()) - .isEqualTo(Service.BACKEND.toString()); - assertThat(task.getScheduleTime().getSeconds()).isEqualTo(0); - } - - @Test - void testSuccess_createPostTasks_withNullParams() { - Task task = cloudTasksUtils.createPostTask("/the/path", Service.BACKEND, null); - assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.POST); - assertThat(task.getAppEngineHttpRequest().getRelativeUri()).isEqualTo("/the/path"); - assertThat(task.getAppEngineHttpRequest().getAppEngineRouting().getService()) - .isEqualTo(Service.BACKEND.toString()); - assertThat(task.getAppEngineHttpRequest().getBody().toString(StandardCharsets.UTF_8)).isEmpty(); - assertThat(task.getScheduleTime().getSeconds()).isEqualTo(0); - } - - @Test - void testSuccess_createGetTasks_withEmptyParams() { - Task task = cloudTasksUtils.createGetTask("/the/path", Service.BACKEND, ImmutableMultimap.of()); - assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.GET); - assertThat(task.getAppEngineHttpRequest().getRelativeUri()).isEqualTo("/the/path"); - assertThat(task.getAppEngineHttpRequest().getAppEngineRouting().getService()) - .isEqualTo(Service.BACKEND.toString()); - assertThat(task.getScheduleTime().getSeconds()).isEqualTo(0); - } - - @Test - void testSuccess_createPostTasks_withEmptyParams() { - Task task = - cloudTasksUtils.createPostTask("/the/path", Service.BACKEND, ImmutableMultimap.of()); - assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.POST); - assertThat(task.getAppEngineHttpRequest().getRelativeUri()).isEqualTo("/the/path"); - assertThat(task.getAppEngineHttpRequest().getAppEngineRouting().getService()) - .isEqualTo(Service.BACKEND.toString()); - assertThat(task.getAppEngineHttpRequest().getBody().toString(StandardCharsets.UTF_8)).isEmpty(); - assertThat(task.getScheduleTime().getSeconds()).isEqualTo(0); - } - - @SuppressWarnings("ProtoTimestampGetSecondsGetNano") - @Test - void testSuccess_createGetTasks_withJitterSeconds() { - Task task = - cloudTasksUtils.createGetTaskWithJitter( - "/the/path", Service.BACKEND, params, Optional.of(100)); - assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.GET); - assertThat(task.getAppEngineHttpRequest().getRelativeUri()) - .isEqualTo("/the/path?key1=val1&key2=val2&key1=val3"); - assertThat(task.getAppEngineHttpRequest().getAppEngineRouting().getService()) - .isEqualTo(Service.BACKEND.toString()); - - Instant scheduleTime = Instant.ofEpochSecond(task.getScheduleTime().getSeconds()); - Instant lowerBoundTime = Instant.ofEpochMilli(clock.nowUtc().getMillis()); - Instant upperBound = Instant.ofEpochMilli(clock.nowUtc().plusSeconds(100).getMillis()); - - assertThat(scheduleTime.isBefore(lowerBoundTime)).isFalse(); - assertThat(upperBound.isBefore(scheduleTime)).isFalse(); - } - - @SuppressWarnings("ProtoTimestampGetSecondsGetNano") - @Test - void testSuccess_createPostTasks_withJitterSeconds() { - Task task = - cloudTasksUtils.createPostTaskWithJitter( - "/the/path", Service.BACKEND, params, Optional.of(1)); - assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.POST); - assertThat(task.getAppEngineHttpRequest().getRelativeUri()).isEqualTo("/the/path"); - assertThat(task.getAppEngineHttpRequest().getAppEngineRouting().getService()) - .isEqualTo(Service.BACKEND.toString()); - assertThat(task.getAppEngineHttpRequest().getHeadersMap().get("Content-Type")) - .isEqualTo("application/x-www-form-urlencoded"); - assertThat(task.getAppEngineHttpRequest().getBody().toString(StandardCharsets.UTF_8)) - .isEqualTo("key1=val1&key2=val2&key1=val3"); - assertThat(task.getScheduleTime().getSeconds()).isNotEqualTo(0); - - Instant scheduleTime = Instant.ofEpochSecond(task.getScheduleTime().getSeconds()); - Instant lowerBoundTime = Instant.ofEpochMilli(clock.nowUtc().getMillis()); - Instant upperBound = Instant.ofEpochMilli(clock.nowUtc().plusSeconds(1).getMillis()); - - assertThat(scheduleTime.isBefore(lowerBoundTime)).isFalse(); - assertThat(upperBound.isBefore(scheduleTime)).isFalse(); - } - - @Test - void testSuccess_createPostTasks_withEmptyJitterSeconds() { - Task task = - cloudTasksUtils.createPostTaskWithJitter( - "/the/path", Service.BACKEND, params, Optional.empty()); - assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.POST); - assertThat(task.getAppEngineHttpRequest().getRelativeUri()).isEqualTo("/the/path"); - assertThat(task.getAppEngineHttpRequest().getAppEngineRouting().getService()) - .isEqualTo(Service.BACKEND.toString()); - assertThat(task.getAppEngineHttpRequest().getHeadersMap().get("Content-Type")) - .isEqualTo("application/x-www-form-urlencoded"); - assertThat(task.getAppEngineHttpRequest().getBody().toString(StandardCharsets.UTF_8)) - .isEqualTo("key1=val1&key2=val2&key1=val3"); - assertThat(task.getScheduleTime().getSeconds()).isEqualTo(0); - } - - @Test - void testSuccess_createGetTasks_withEmptyJitterSeconds() { - Task task = - cloudTasksUtils.createGetTaskWithJitter( - "/the/path", Service.BACKEND, params, Optional.empty()); - assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.GET); - assertThat(task.getAppEngineHttpRequest().getRelativeUri()) - .isEqualTo("/the/path?key1=val1&key2=val2&key1=val3"); - assertThat(task.getAppEngineHttpRequest().getAppEngineRouting().getService()) - .isEqualTo(Service.BACKEND.toString()); - assertThat(task.getScheduleTime().getSeconds()).isEqualTo(0); - } - - @Test - void testSuccess_createPostTasks_withZeroJitterSeconds() { - Task task = - cloudTasksUtils.createPostTaskWithJitter( - "/the/path", Service.BACKEND, params, Optional.of(0)); - assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.POST); - assertThat(task.getAppEngineHttpRequest().getRelativeUri()).isEqualTo("/the/path"); - assertThat(task.getAppEngineHttpRequest().getAppEngineRouting().getService()) - .isEqualTo(Service.BACKEND.toString()); - assertThat(task.getAppEngineHttpRequest().getHeadersMap().get("Content-Type")) - .isEqualTo("application/x-www-form-urlencoded"); - assertThat(task.getAppEngineHttpRequest().getBody().toString(StandardCharsets.UTF_8)) - .isEqualTo("key1=val1&key2=val2&key1=val3"); - assertThat(task.getScheduleTime().getSeconds()).isEqualTo(0); - } - - @Test - void testSuccess_createGetTasks_withZeroJitterSeconds() { - Task task = - cloudTasksUtils.createGetTaskWithJitter( - "/the/path", Service.BACKEND, params, Optional.of(0)); - assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.GET); - assertThat(task.getAppEngineHttpRequest().getRelativeUri()) - .isEqualTo("/the/path?key1=val1&key2=val2&key1=val3"); - assertThat(task.getAppEngineHttpRequest().getAppEngineRouting().getService()) - .isEqualTo(Service.BACKEND.toString()); - assertThat(task.getScheduleTime().getSeconds()).isEqualTo(0); - } - - @Test - void testSuccess_createGetTasks_withDelay() { - Task task = - cloudTasksUtils.createGetTaskWithDelay( - "/the/path", Service.BACKEND, params, Duration.standardMinutes(10)); - assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.GET); - assertThat(task.getAppEngineHttpRequest().getRelativeUri()) - .isEqualTo("/the/path?key1=val1&key2=val2&key1=val3"); - assertThat(task.getAppEngineHttpRequest().getAppEngineRouting().getService()) - .isEqualTo(Service.BACKEND.toString()); - assertThat(Instant.ofEpochSecond(task.getScheduleTime().getSeconds())) - .isEqualTo(Instant.ofEpochMilli(clock.nowUtc().plusMinutes(10).getMillis())); - } - - @Test - void testSuccess_createPostTasks_withDelay() { - Task task = - cloudTasksUtils.createPostTaskWithDelay( - "/the/path", Service.BACKEND, params, Duration.standardMinutes(10)); - assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.POST); - assertThat(task.getAppEngineHttpRequest().getRelativeUri()).isEqualTo("/the/path"); - assertThat(task.getAppEngineHttpRequest().getAppEngineRouting().getService()) - .isEqualTo(Service.BACKEND.toString()); - assertThat(task.getAppEngineHttpRequest().getHeadersMap().get("Content-Type")) - .isEqualTo("application/x-www-form-urlencoded"); - assertThat(task.getAppEngineHttpRequest().getBody().toString(StandardCharsets.UTF_8)) - .isEqualTo("key1=val1&key2=val2&key1=val3"); - assertThat(task.getScheduleTime().getSeconds()).isNotEqualTo(0); - assertThat(Instant.ofEpochSecond(task.getScheduleTime().getSeconds())) - .isEqualTo(Instant.ofEpochMilli(clock.nowUtc().plusMinutes(10).getMillis())); - } - @Test void testFailure_createGetTasks_withNegativeDelay() { IllegalArgumentException thrown = @@ -290,34 +89,6 @@ public class CloudTasksUtilsTest { assertThat(thrown).hasMessageThat().isEqualTo("Negative duration is not supported."); } - @Test - void testSuccess_createPostTasks_withZeroDelay() { - Task task = - cloudTasksUtils.createPostTaskWithDelay( - "/the/path", Service.BACKEND, params, Duration.ZERO); - assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.POST); - assertThat(task.getAppEngineHttpRequest().getRelativeUri()).isEqualTo("/the/path"); - assertThat(task.getAppEngineHttpRequest().getAppEngineRouting().getService()) - .isEqualTo(Service.BACKEND.toString()); - assertThat(task.getAppEngineHttpRequest().getHeadersMap().get("Content-Type")) - .isEqualTo("application/x-www-form-urlencoded"); - assertThat(task.getAppEngineHttpRequest().getBody().toString(StandardCharsets.UTF_8)) - .isEqualTo("key1=val1&key2=val2&key1=val3"); - assertThat(task.getScheduleTime().getSeconds()).isEqualTo(0); - } - - @Test - void testSuccess_createGetTasks_withZeroDelay() { - Task task = - cloudTasksUtils.createGetTaskWithDelay("/the/path", Service.BACKEND, params, Duration.ZERO); - assertThat(task.getAppEngineHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.GET); - assertThat(task.getAppEngineHttpRequest().getRelativeUri()) - .isEqualTo("/the/path?key1=val1&key2=val2&key1=val3"); - assertThat(task.getAppEngineHttpRequest().getAppEngineRouting().getService()) - .isEqualTo(Service.BACKEND.toString()); - assertThat(task.getScheduleTime().getSeconds()).isEqualTo(0); - } - @Test void testFailure_illegalPath() { assertThrows( @@ -357,22 +128,20 @@ public class CloudTasksUtilsTest { } @Test - void testSuccess_nonAppEngine_createGetTasks() { - createOidcTasksUtils(); + void testSuccess_createGetTasks() { Task task = cloudTasksUtils.createGetTask("/the/path", Service.BACKEND, params); assertThat(task.getHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.GET); assertThat(task.getHttpRequest().getUrl()) - .isEqualTo("https://localhost/the/path?key1=val1&key2=val2&key1=val3"); + .isEqualTo("https://backend.example.com/the/path?key1=val1&key2=val2&key1=val3"); verifyOidcToken(task); assertThat(task.getScheduleTime().getSeconds()).isEqualTo(0); } @Test - void testSuccess_nonAppEngine_createPostTasks() { - createOidcTasksUtils(); + void testSuccess_createPostTasks() { Task task = cloudTasksUtils.createPostTask("/the/path", Service.BACKEND, params); assertThat(task.getHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.POST); - assertThat(task.getHttpRequest().getUrl()).isEqualTo("https://localhost/the/path"); + assertThat(task.getHttpRequest().getUrl()).isEqualTo("https://backend.example.com/the/path"); assertThat(task.getHttpRequest().getHeadersMap().get("Content-Type")) .isEqualTo("application/x-www-form-urlencoded"); assertThat(task.getHttpRequest().getBody().toString(StandardCharsets.UTF_8)) @@ -382,43 +151,39 @@ public class CloudTasksUtilsTest { } @Test - void testSuccess_nonAppEngine_createGetTasks_withNullParams() { - createOidcTasksUtils(); + void testSuccess_createGetTasks_withNullParams() { Task task = cloudTasksUtils.createGetTask("/the/path", Service.BACKEND, null); assertThat(task.getHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.GET); - assertThat(task.getHttpRequest().getUrl()).isEqualTo("https://localhost/the/path"); + assertThat(task.getHttpRequest().getUrl()).isEqualTo("https://backend.example.com/the/path"); verifyOidcToken(task); assertThat(task.getScheduleTime().getSeconds()).isEqualTo(0); } @Test - void testSuccess_nonAppEngine_createPostTasks_withNullParams() { - createOidcTasksUtils(); + void testSuccess_createPostTasks_withNullParams() { Task task = cloudTasksUtils.createPostTask("/the/path", Service.BACKEND, null); assertThat(task.getHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.POST); - assertThat(task.getHttpRequest().getUrl()).isEqualTo("https://localhost/the/path"); + assertThat(task.getHttpRequest().getUrl()).isEqualTo("https://backend.example.com/the/path"); assertThat(task.getHttpRequest().getBody().toString(StandardCharsets.UTF_8)).isEmpty(); verifyOidcToken(task); assertThat(task.getScheduleTime().getSeconds()).isEqualTo(0); } @Test - void testSuccess_nonAppEngine_createGetTasks_withEmptyParams() { - createOidcTasksUtils(); + void testSuccess_createGetTasks_withEmptyParams() { Task task = cloudTasksUtils.createGetTask("/the/path", Service.BACKEND, ImmutableMultimap.of()); assertThat(task.getHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.GET); - assertThat(task.getHttpRequest().getUrl()).isEqualTo("https://localhost/the/path"); + assertThat(task.getHttpRequest().getUrl()).isEqualTo("https://backend.example.com/the/path"); verifyOidcToken(task); assertThat(task.getScheduleTime().getSeconds()).isEqualTo(0); } @Test - void testSuccess_nonAppEngine_createPostTasks_withEmptyParams() { - createOidcTasksUtils(); + void testSuccess_createPostTasks_withEmptyParams() { Task task = cloudTasksUtils.createPostTask("/the/path", Service.BACKEND, ImmutableMultimap.of()); assertThat(task.getHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.POST); - assertThat(task.getHttpRequest().getUrl()).isEqualTo("https://localhost/the/path"); + assertThat(task.getHttpRequest().getUrl()).isEqualTo("https://backend.example.com/the/path"); assertThat(task.getHttpRequest().getBody().toString(StandardCharsets.UTF_8)).isEmpty(); verifyOidcToken(task); assertThat(task.getScheduleTime().getSeconds()).isEqualTo(0); @@ -426,14 +191,13 @@ public class CloudTasksUtilsTest { @SuppressWarnings("ProtoTimestampGetSecondsGetNano") @Test - void testSuccess_nonAppEngine_createGetTasks_withJitterSeconds() { - createOidcTasksUtils(); + void testSuccess_createGetTasks_withJitterSeconds() { Task task = cloudTasksUtils.createGetTaskWithJitter( "/the/path", Service.BACKEND, params, Optional.of(100)); assertThat(task.getHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.GET); assertThat(task.getHttpRequest().getUrl()) - .isEqualTo("https://localhost/the/path?key1=val1&key2=val2&key1=val3"); + .isEqualTo("https://backend.example.com/the/path?key1=val1&key2=val2&key1=val3"); verifyOidcToken(task); Instant scheduleTime = Instant.ofEpochSecond(task.getScheduleTime().getSeconds()); @@ -446,13 +210,12 @@ public class CloudTasksUtilsTest { @SuppressWarnings("ProtoTimestampGetSecondsGetNano") @Test - void testSuccess_nonAppEngine_createPostTasks_withJitterSeconds() { - createOidcTasksUtils(); + void testSuccess_createPostTasks_withJitterSeconds() { Task task = cloudTasksUtils.createPostTaskWithJitter( "/the/path", Service.BACKEND, params, Optional.of(1)); assertThat(task.getHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.POST); - assertThat(task.getHttpRequest().getUrl()).isEqualTo("https://localhost/the/path"); + assertThat(task.getHttpRequest().getUrl()).isEqualTo("https://backend.example.com/the/path"); assertThat(task.getHttpRequest().getHeadersMap().get("Content-Type")) .isEqualTo("application/x-www-form-urlencoded"); assertThat(task.getHttpRequest().getBody().toString(StandardCharsets.UTF_8)) @@ -469,13 +232,12 @@ public class CloudTasksUtilsTest { } @Test - void testSuccess_nonAppEngine_createPostTasks_withEmptyJitterSeconds() { - createOidcTasksUtils(); + void testSuccess_createPostTasks_withEmptyJitterSeconds() { Task task = cloudTasksUtils.createPostTaskWithJitter( "/the/path", Service.BACKEND, params, Optional.empty()); assertThat(task.getHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.POST); - assertThat(task.getHttpRequest().getUrl()).isEqualTo("https://localhost/the/path"); + assertThat(task.getHttpRequest().getUrl()).isEqualTo("https://backend.example.com/the/path"); assertThat(task.getHttpRequest().getHeadersMap().get("Content-Type")) .isEqualTo("application/x-www-form-urlencoded"); assertThat(task.getHttpRequest().getBody().toString(StandardCharsets.UTF_8)) @@ -485,26 +247,24 @@ public class CloudTasksUtilsTest { } @Test - void testSuccess_nonAppEngine_createGetTasks_withEmptyJitterSeconds() { - createOidcTasksUtils(); + void testSuccess_createGetTasks_withEmptyJitterSeconds() { Task task = cloudTasksUtils.createGetTaskWithJitter( "/the/path", Service.BACKEND, params, Optional.empty()); assertThat(task.getHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.GET); assertThat(task.getHttpRequest().getUrl()) - .isEqualTo("https://localhost/the/path?key1=val1&key2=val2&key1=val3"); + .isEqualTo("https://backend.example.com/the/path?key1=val1&key2=val2&key1=val3"); verifyOidcToken(task); assertThat(task.getScheduleTime().getSeconds()).isEqualTo(0); } @Test - void testSuccess_nonAppEngine_createPostTasks_withZeroJitterSeconds() { - createOidcTasksUtils(); + void testSuccess_createPostTasks_withZeroJitterSeconds() { Task task = cloudTasksUtils.createPostTaskWithJitter( "/the/path", Service.BACKEND, params, Optional.of(0)); assertThat(task.getHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.POST); - assertThat(task.getHttpRequest().getUrl()).isEqualTo("https://localhost/the/path"); + assertThat(task.getHttpRequest().getUrl()).isEqualTo("https://backend.example.com/the/path"); assertThat(task.getHttpRequest().getHeadersMap().get("Content-Type")) .isEqualTo("application/x-www-form-urlencoded"); assertThat(task.getHttpRequest().getBody().toString(StandardCharsets.UTF_8)) @@ -514,40 +274,37 @@ public class CloudTasksUtilsTest { } @Test - void testSuccess_nonAppEngine_createGetTasks_withZeroJitterSeconds() { - createOidcTasksUtils(); + void testSuccess_createGetTasks_withZeroJitterSeconds() { Task task = cloudTasksUtils.createGetTaskWithJitter( "/the/path", Service.BACKEND, params, Optional.of(0)); assertThat(task.getHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.GET); assertThat(task.getHttpRequest().getUrl()) - .isEqualTo("https://localhost/the/path?key1=val1&key2=val2&key1=val3"); + .isEqualTo("https://backend.example.com/the/path?key1=val1&key2=val2&key1=val3"); verifyOidcToken(task); assertThat(task.getScheduleTime().getSeconds()).isEqualTo(0); } @Test - void testSuccess_nonAppEngine_createGetTasks_withDelay() { - createOidcTasksUtils(); + void testSuccess_createGetTasks_withDelay() { Task task = cloudTasksUtils.createGetTaskWithDelay( "/the/path", Service.BACKEND, params, Duration.standardMinutes(10)); assertThat(task.getHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.GET); assertThat(task.getHttpRequest().getUrl()) - .isEqualTo("https://localhost/the/path?key1=val1&key2=val2&key1=val3"); + .isEqualTo("https://backend.example.com/the/path?key1=val1&key2=val2&key1=val3"); verifyOidcToken(task); assertThat(Instant.ofEpochSecond(task.getScheduleTime().getSeconds())) .isEqualTo(Instant.ofEpochMilli(clock.nowUtc().plusMinutes(10).getMillis())); } @Test - void testSuccess_nonAppEngine_createPostTasks_withDelay() { - createOidcTasksUtils(); + void testSuccess_createPostTasks_withDelay() { Task task = cloudTasksUtils.createPostTaskWithDelay( "/the/path", Service.BACKEND, params, Duration.standardMinutes(10)); assertThat(task.getHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.POST); - assertThat(task.getHttpRequest().getUrl()).isEqualTo("https://localhost/the/path"); + assertThat(task.getHttpRequest().getUrl()).isEqualTo("https://backend.example.com/the/path"); assertThat(task.getHttpRequest().getHeadersMap().get("Content-Type")) .isEqualTo("application/x-www-form-urlencoded"); assertThat(task.getHttpRequest().getBody().toString(StandardCharsets.UTF_8)) @@ -559,13 +316,12 @@ public class CloudTasksUtilsTest { } @Test - void testSuccess_nonAppEngine_createPostTasks_withZeroDelay() { - createOidcTasksUtils(); + void testSuccess_createPostTasks_withZeroDelay() { Task task = cloudTasksUtils.createPostTaskWithDelay( "/the/path", Service.BACKEND, params, Duration.ZERO); assertThat(task.getHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.POST); - assertThat(task.getHttpRequest().getUrl()).isEqualTo("https://localhost/the/path"); + assertThat(task.getHttpRequest().getUrl()).isEqualTo("https://backend.example.com/the/path"); assertThat(task.getHttpRequest().getHeadersMap().get("Content-Type")) .isEqualTo("application/x-www-form-urlencoded"); assertThat(task.getHttpRequest().getBody().toString(StandardCharsets.UTF_8)) @@ -575,35 +331,22 @@ public class CloudTasksUtilsTest { } @Test - void testSuccess_nonAppEngine_createGetTasks_withZeroDelay() { - createOidcTasksUtils(); + void testSuccess_createGetTasks_withZeroDelay() { Task task = cloudTasksUtils.createGetTaskWithDelay("/the/path", Service.BACKEND, params, Duration.ZERO); assertThat(task.getHttpRequest().getHttpMethod()).isEqualTo(HttpMethod.GET); assertThat(task.getHttpRequest().getUrl()) - .isEqualTo("https://localhost/the/path?key1=val1&key2=val2&key1=val3"); + .isEqualTo("https://backend.example.com/the/path?key1=val1&key2=val2&key1=val3"); verifyOidcToken(task); assertThat(task.getScheduleTime().getSeconds()).isEqualTo(0); } - private void createOidcTasksUtils() { - cloudTasksUtils = - new CloudTasksUtils( - new Retrier(new FakeSleeper(clock), 1), - clock, - "project", - "location", - Optional.of("defaultServiceAccount"), - Optional.of("iapClientId"), - mockClient); - } - - private void verifyOidcToken(Task task) { + private static void verifyOidcToken(Task task) { assertThat(task.getHttpRequest().getOidcToken()) .isEqualTo( OidcToken.newBuilder() - .setServiceAccountEmail("defaultServiceAccount") - .setAudience("iapClientId") + .setServiceAccountEmail("service@account.com") + .setAudience("clientId") .build()); } } diff --git a/core/src/test/java/google/registry/batch/RelockDomainActionTest.java b/core/src/test/java/google/registry/batch/RelockDomainActionTest.java index 4921d2c1b..a8b4637f6 100644 --- a/core/src/test/java/google/registry/batch/RelockDomainActionTest.java +++ b/core/src/test/java/google/registry/batch/RelockDomainActionTest.java @@ -305,7 +305,7 @@ public class RelockDomainActionTest { cloudTasksHelper.assertTasksEnqueued( QUEUE_ASYNC_ACTIONS, new TaskMatcher() - .url(RelockDomainAction.PATH) + .path(RelockDomainAction.PATH) .method(HttpMethod.POST) .param( RelockDomainAction.OLD_UNLOCK_REVISION_ID_PARAM, diff --git a/core/src/test/java/google/registry/batch/ResaveEntityActionTest.java b/core/src/test/java/google/registry/batch/ResaveEntityActionTest.java index 1adc87121..48b2d5b2c 100644 --- a/core/src/test/java/google/registry/batch/ResaveEntityActionTest.java +++ b/core/src/test/java/google/registry/batch/ResaveEntityActionTest.java @@ -136,7 +136,7 @@ public class ResaveEntityActionTest { cloudTasksHelper.assertTasksEnqueued( QUEUE_ASYNC_ACTIONS, new TaskMatcher() - .url(ResaveEntityAction.PATH) + .path(ResaveEntityAction.PATH) .method(HttpMethod.POST) .service("backend") .header("content-type", "application/x-www-form-urlencoded") diff --git a/core/src/test/java/google/registry/beam/rde/RdePipelineTest.java b/core/src/test/java/google/registry/beam/rde/RdePipelineTest.java index 14561d12e..cf6a4568c 100644 --- a/core/src/test/java/google/registry/beam/rde/RdePipelineTest.java +++ b/core/src/test/java/google/registry/beam/rde/RdePipelineTest.java @@ -464,7 +464,7 @@ public class RdePipelineTest { cloudTasksHelper.assertTasksEnqueued( "brda", new TaskMatcher() - .url("/_dr/task/brdaCopy") + .path("/_dr/task/brdaCopy") .service("backend") .param("tld", "soy") .param("watermark", now.toString()) @@ -472,7 +472,7 @@ public class RdePipelineTest { cloudTasksHelper.assertTasksEnqueued( "rde-upload", new TaskMatcher() - .url("/_dr/task/rdeUpload") + .path("/_dr/task/rdeUpload") .service("backend") .param("tld", "soy") .param("prefix", "rde-job/")); diff --git a/core/src/test/java/google/registry/cron/TldFanoutActionTest.java b/core/src/test/java/google/registry/cron/TldFanoutActionTest.java index 7ae5b92ba..42195dfdc 100644 --- a/core/src/test/java/google/registry/cron/TldFanoutActionTest.java +++ b/core/src/test/java/google/registry/cron/TldFanoutActionTest.java @@ -91,14 +91,14 @@ class TldFanoutActionTest { .map( namespace -> new TaskMatcher() - .url(ENDPOINT) + .path(ENDPOINT) .header("content-type", "application/x-www-form-urlencoded") .param("tld", namespace)) .collect(toImmutableList())); } private void assertTaskWithoutTld() { - cloudTasksHelper.assertTasksEnqueued(QUEUE, new TaskMatcher().url(ENDPOINT)); + cloudTasksHelper.assertTasksEnqueued(QUEUE, new TaskMatcher().path(ENDPOINT)); } @Test @@ -211,7 +211,7 @@ class TldFanoutActionTest { void testSuccess_additionalArgsFlowThroughToPostParams() { run(getParamsMap("forEachTestTld", "", "newkey", "newval")); cloudTasksHelper.assertTasksEnqueued( - QUEUE, new TaskMatcher().url("/the/servlet").param("newkey", "newval")); + QUEUE, new TaskMatcher().path("/the/servlet").param("newkey", "newval")); } @Test @@ -224,9 +224,9 @@ class TldFanoutActionTest { String expectedResponse = String.format( "OK: Launched the following 3 tasks in queue the-queue\n" - + "- Task: '%s', tld: 'com', endpoint: '/the/servlet'\n" - + "- Task: '%s', tld: 'net', endpoint: '/the/servlet'\n" - + "- Task: '%s', tld: 'org', endpoint: '/the/servlet'\n", + + "- Task: '%s', tld: 'com', endpoint: 'https://backend.example.com/the/servlet'\n" + + "- Task: '%s', tld: 'net', endpoint: 'https://backend.example.com/the/servlet'\n" + + "- Task: '%s', tld: 'org', endpoint: 'https://backend.example.com/the/servlet'\n", taskList.get(0).getName(), taskList.get(1).getName(), taskList.get(2).getName()); assertThat(response.getPayload()).isEqualTo(expectedResponse); } @@ -241,7 +241,7 @@ class TldFanoutActionTest { String expectedResponse = String.format( "OK: Launched the following 1 tasks in queue the-queue\n" - + "- Task: '%s', tld: '', endpoint: '/the/servlet'\n", + + "- Task: '%s', tld: '', endpoint: 'https://backend.example.com/the/servlet'\n", taskList.get(0).getName()); assertThat(response.getPayload()).isEqualTo(expectedResponse); } diff --git a/core/src/test/java/google/registry/dns/PublishDnsUpdatesActionTest.java b/core/src/test/java/google/registry/dns/PublishDnsUpdatesActionTest.java index 571fd0a09..ac7865e70 100644 --- a/core/src/test/java/google/registry/dns/PublishDnsUpdatesActionTest.java +++ b/core/src/test/java/google/registry/dns/PublishDnsUpdatesActionTest.java @@ -294,7 +294,7 @@ public class PublishDnsUpdatesActionTest { cloudTasksHelper.assertTasksEnqueued( DNS_PUBLISH_PUSH_QUEUE_NAME, new TaskMatcher() - .url(PublishDnsUpdatesAction.PATH) + .path(PublishDnsUpdatesAction.PATH) .param(PARAM_TLD, "xn--q9jyb4c") .param(PARAM_DNS_WRITER, "correctWriter") .param(PARAM_LOCK_INDEX, "1") @@ -305,7 +305,7 @@ public class PublishDnsUpdatesActionTest { .param(PARAM_HOSTS, "") .header("content-type", "application/x-www-form-urlencoded"), new TaskMatcher() - .url(PublishDnsUpdatesAction.PATH) + .path(PublishDnsUpdatesAction.PATH) .param(PARAM_TLD, "xn--q9jyb4c") .param(PARAM_DNS_WRITER, "correctWriter") .param(PARAM_LOCK_INDEX, "1") @@ -333,7 +333,7 @@ public class PublishDnsUpdatesActionTest { cloudTasksHelper.assertTasksEnqueued( DNS_PUBLISH_PUSH_QUEUE_NAME, new TaskMatcher() - .url(PublishDnsUpdatesAction.PATH) + .path(PublishDnsUpdatesAction.PATH) .param(PARAM_TLD, "xn--q9jyb4c") .param(PARAM_DNS_WRITER, "correctWriter") .param(PARAM_LOCK_INDEX, "1") @@ -344,7 +344,7 @@ public class PublishDnsUpdatesActionTest { .param(PARAM_HOSTS, "") .header("content-type", "application/x-www-form-urlencoded"), new TaskMatcher() - .url(PublishDnsUpdatesAction.PATH) + .path(PublishDnsUpdatesAction.PATH) .param(PARAM_TLD, "xn--q9jyb4c") .param(PARAM_DNS_WRITER, "correctWriter") .param(PARAM_LOCK_INDEX, "1") @@ -370,7 +370,7 @@ public class PublishDnsUpdatesActionTest { cloudTasksHelper.assertTasksEnqueued( DNS_PUBLISH_PUSH_QUEUE_NAME, new TaskMatcher() - .url(PublishDnsUpdatesAction.PATH) + .path(PublishDnsUpdatesAction.PATH) .param(PARAM_TLD, "xn--q9jyb4c") .param(PARAM_DNS_WRITER, "correctWriter") .param(PARAM_LOCK_INDEX, "1") @@ -381,7 +381,7 @@ public class PublishDnsUpdatesActionTest { .param(PARAM_HOSTS, "") .header("content-type", "application/x-www-form-urlencoded"), new TaskMatcher() - .url(PublishDnsUpdatesAction.PATH) + .path(PublishDnsUpdatesAction.PATH) .param(PARAM_TLD, "xn--q9jyb4c") .param(PARAM_DNS_WRITER, "correctWriter") .param(PARAM_LOCK_INDEX, "1") diff --git a/core/src/test/java/google/registry/dns/ReadDnsRefreshRequestsActionTest.java b/core/src/test/java/google/registry/dns/ReadDnsRefreshRequestsActionTest.java index af698e5d0..0f06a78eb 100644 --- a/core/src/test/java/google/registry/dns/ReadDnsRefreshRequestsActionTest.java +++ b/core/src/test/java/google/registry/dns/ReadDnsRefreshRequestsActionTest.java @@ -225,7 +225,7 @@ public class ReadDnsRefreshRequestsActionTest { cloudTasksHelper.assertTasksEnqueued( "dns-publish", new TaskMatcher() - .url("/_dr/task/publishDnsUpdates") + .path("/_dr/task/publishDnsUpdates") .service("BACKEND") .param("tld", "tld") .param("dnsWriter", "FooWriter") @@ -236,7 +236,7 @@ public class ReadDnsRefreshRequestsActionTest { .param("domains", "domain.tld,future.tld") .param("hosts", "ns1.domain.tld"), new TaskMatcher() - .url("/_dr/task/publishDnsUpdates") + .path("/_dr/task/publishDnsUpdates") .service("BACKEND") .param("tld", "tld") .param("dnsWriter", "BarWriter") diff --git a/core/src/test/java/google/registry/flows/domain/DomainDeleteFlowTest.java b/core/src/test/java/google/registry/flows/domain/DomainDeleteFlowTest.java index 3d40bb4f2..e931e045f 100644 --- a/core/src/test/java/google/registry/flows/domain/DomainDeleteFlowTest.java +++ b/core/src/test/java/google/registry/flows/domain/DomainDeleteFlowTest.java @@ -304,7 +304,7 @@ class DomainDeleteFlowTest extends ResourceFlowTestCase { cloudTasksHelper.assertTasksEnqueued( QUEUE_HOST_RENAME, new TaskMatcher() - .url(RefreshDnsOnHostRenameAction.PATH) + .path(RefreshDnsOnHostRenameAction.PATH) .method(HttpMethod.POST) .service("backend") .param(PARAM_HOST_KEY, renamedHost.createVKey().stringify())); diff --git a/core/src/test/java/google/registry/rde/RdeUploadActionTest.java b/core/src/test/java/google/registry/rde/RdeUploadActionTest.java index 4b572d4ef..4b98203a8 100644 --- a/core/src/test/java/google/registry/rde/RdeUploadActionTest.java +++ b/core/src/test/java/google/registry/rde/RdeUploadActionTest.java @@ -227,7 +227,7 @@ public class RdeUploadActionTest { action, Tld.get("lol"), standardSeconds(23), CursorType.RDE_UPLOAD, standardDays(1)); cloudTasksHelper.assertTasksEnqueued( "rde-report", - new TaskMatcher().url(RdeReportAction.PATH).param(RequestParameters.PARAM_TLD, "lol")); + new TaskMatcher().path(RdeReportAction.PATH).param(RequestParameters.PARAM_TLD, "lol")); verifyNoMoreInteractions(runner); } @@ -244,7 +244,7 @@ public class RdeUploadActionTest { cloudTasksHelper.assertTasksEnqueued( "rde-report", new TaskMatcher() - .url(RdeReportAction.PATH) + .path(RdeReportAction.PATH) .param(RequestParameters.PARAM_TLD, "lol") .param(RdeModule.PARAM_PREFIX, "job-name/")); verifyNoMoreInteractions(runner); diff --git a/core/src/test/java/google/registry/reporting/billing/GenerateInvoicesActionTest.java b/core/src/test/java/google/registry/reporting/billing/GenerateInvoicesActionTest.java index ea89ca014..a947e85c4 100644 --- a/core/src/test/java/google/registry/reporting/billing/GenerateInvoicesActionTest.java +++ b/core/src/test/java/google/registry/reporting/billing/GenerateInvoicesActionTest.java @@ -74,7 +74,7 @@ class GenerateInvoicesActionTest extends BeamActionTestBase { cloudTasksHelper.assertTasksEnqueued( "beam-reporting", new TaskMatcher() - .url("/_dr/task/publishInvoices") + .path("/_dr/task/publishInvoices") .method(HttpMethod.POST) .param("jobId", "jobid") .param("yearMonth", "2017-10") diff --git a/core/src/test/java/google/registry/reporting/billing/PublishInvoicesActionTest.java b/core/src/test/java/google/registry/reporting/billing/PublishInvoicesActionTest.java index d70ff12db..d8bafefc7 100644 --- a/core/src/test/java/google/registry/reporting/billing/PublishInvoicesActionTest.java +++ b/core/src/test/java/google/registry/reporting/billing/PublishInvoicesActionTest.java @@ -83,7 +83,7 @@ class PublishInvoicesActionTest { verify(emailUtils).emailOverallInvoice(); TaskMatcher matcher = new TaskMatcher() - .url("/_dr/task/copyDetailReports") + .path("/_dr/task/copyDetailReports") .method(HttpMethod.POST) .param("yearMonth", "2017-10"); cloudTasksHelper.assertTasksEnqueued("retryable-cron-tasks", matcher); diff --git a/core/src/test/java/google/registry/reporting/icann/IcannReportingStagingActionTest.java b/core/src/test/java/google/registry/reporting/icann/IcannReportingStagingActionTest.java index 11048842f..06b22702e 100644 --- a/core/src/test/java/google/registry/reporting/icann/IcannReportingStagingActionTest.java +++ b/core/src/test/java/google/registry/reporting/icann/IcannReportingStagingActionTest.java @@ -78,7 +78,7 @@ class IcannReportingStagingActionTest { cloudTasksHelper.assertTasksEnqueued( "retryable-cron-tasks", new TaskMatcher() - .url("/_dr/task/icannReportingUpload") + .path("/_dr/task/icannReportingUpload") .method(HttpMethod.POST) .scheduleTime(clock.nowUtc().plus(Duration.standardMinutes(2)))); } diff --git a/core/src/test/java/google/registry/reporting/spec11/GenerateSpec11ReportActionTest.java b/core/src/test/java/google/registry/reporting/spec11/GenerateSpec11ReportActionTest.java index 993a96333..ca534566f 100644 --- a/core/src/test/java/google/registry/reporting/spec11/GenerateSpec11ReportActionTest.java +++ b/core/src/test/java/google/registry/reporting/spec11/GenerateSpec11ReportActionTest.java @@ -86,7 +86,7 @@ class GenerateSpec11ReportActionTest extends BeamActionTestBase { cloudTasksHelper.assertTasksEnqueued( "beam-reporting", new TaskMatcher() - .url("/_dr/task/publishSpec11") + .path("/_dr/task/publishSpec11") .method(HttpMethod.POST) .param("jobId", "jobid") .param("date", "2018-06-11") diff --git a/core/src/test/java/google/registry/request/auth/OidcTokenAuthenticationMechanismTest.java b/core/src/test/java/google/registry/request/auth/OidcTokenAuthenticationMechanismTest.java index f2d2a65c7..f600135aa 100644 --- a/core/src/test/java/google/registry/request/auth/OidcTokenAuthenticationMechanismTest.java +++ b/core/src/test/java/google/registry/request/auth/OidcTokenAuthenticationMechanismTest.java @@ -29,7 +29,7 @@ import com.google.api.client.json.webtoken.JsonWebSignature; import com.google.api.client.json.webtoken.JsonWebSignature.Header; import com.google.auth.oauth2.TokenVerifier; import com.google.auth.oauth2.TokenVerifier.VerificationException; -import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSet; import dagger.Component; import dagger.Module; import dagger.Provides; @@ -54,8 +54,8 @@ public class OidcTokenAuthenticationMechanismTest { private static final String rawToken = "this-token"; private static final String email = "user@email.test"; private static final String gaiaId = "gaia-id"; - private static final ImmutableList serviceAccounts = - ImmutableList.of("service@email.test", "email@service.goog"); + private static final ImmutableSet serviceAccounts = + ImmutableSet.of("service@email.test", "email@service.goog"); private final Payload payload = new Payload(); private final User user = @@ -222,9 +222,16 @@ public class OidcTokenAuthenticationMechanismTest { @Provides @Singleton - @Config("serviceAccountEmails") - ImmutableList provideServiceAccountEmails() { + @Config("allowedServiceAccountEmails") + ImmutableSet provideAllowedServiceAccountEmails() { return serviceAccounts; } + + @Provides + @Singleton + @Config("oauthClientId") + String provideOauthClientId() { + return "client-id"; + } } } diff --git a/core/src/test/java/google/registry/testing/CloudTasksHelper.java b/core/src/test/java/google/registry/testing/CloudTasksHelper.java index 50f2e8b43..51c05dd0a 100644 --- a/core/src/test/java/google/registry/testing/CloudTasksHelper.java +++ b/core/src/test/java/google/registry/testing/CloudTasksHelper.java @@ -24,7 +24,9 @@ import static google.registry.util.DiffUtils.prettyPrintEntityDeepDiff; import static java.util.Arrays.asList; import static java.util.stream.Collectors.joining; +import com.google.auth.oauth2.GoogleCredentials; import com.google.cloud.tasks.v2.HttpMethod; +import com.google.cloud.tasks.v2.HttpRequest; import com.google.cloud.tasks.v2.Task; import com.google.common.base.Ascii; import com.google.common.base.Joiner; @@ -45,6 +47,7 @@ import dagger.Module; import dagger.Provides; import google.registry.batch.CloudTasksUtils; import google.registry.model.ImmutableObject; +import google.registry.util.GoogleCredentialsBundle; import google.registry.util.Retrier; import java.io.Serializable; import java.net.URI; @@ -58,12 +61,13 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; import java.util.Objects; -import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Function; import java.util.function.Predicate; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.annotation.Nonnull; import javax.inject.Singleton; import org.joda.time.DateTime; @@ -104,8 +108,8 @@ public class CloudTasksHelper implements Serializable { clock, PROJECT_ID, LOCATION_ID, - Optional.empty(), - Optional.empty(), + "client.id", + FakeGoogleCredentialsBundle.create(), new FakeCloudTasksClient()); testTasks.put(instanceId, Multimaps.synchronizedListMultimap(LinkedListMultimap.create())); } @@ -219,18 +223,35 @@ public class CloudTasksHelper implements Serializable { } } + public static class FakeGoogleCredentialsBundle extends GoogleCredentialsBundle { + + private static final long serialVersionUID = -3251343247195058893L; + + private static final FakeGoogleCredentialsBundle INSTANCE = new FakeGoogleCredentialsBundle(); + + public static GoogleCredentialsBundle create() { + return INSTANCE; + } + + private FakeGoogleCredentialsBundle() { + super(new GoogleCredentials(null)); + } + + @Override + public String serviceAccount() { + return "service@account.com"; + } + } + /** An adapter to clean up a {@link Task} for ease of matching. */ private static class MatchableTask extends ImmutableObject { + private static final Pattern HOSTNAME_PATTERN = + Pattern.compile("(?<=https://)[a-z]+(?=\\.example\\.com)"); String taskName; String service; - // App Engine TaskOption methods default to "POST". This isn't obvious from the code, so we - // default it to POST here so that we don't accidentally create an entry with a GET method when - // converting to CloudTasksUtils, which requires that the method be specified explicitly. - // Should we ever actually want to do a GET, we'll likewise have to set this explicitly for the - // tests. - HttpMethod method = HttpMethod.POST; - String url; + HttpMethod method; + String path; Timestamp scheduleTime; Multimap headers = ArrayListMultimap.create(); Multimap params = ArrayListMultimap.create(); @@ -238,24 +259,24 @@ public class CloudTasksHelper implements Serializable { MatchableTask() {} MatchableTask(Task task) { + HttpRequest request = task.getHttpRequest(); + taskName = task.getName(); + String url = request.getUrl(); + // URI class provides helper method to extract query parameters. URI uri; try { - // Construct a fake full URI for parsing purpose. The relative URI must start with a slash. - uri = - new URI( - String.format( - "https://nomulus.foo%s", task.getAppEngineHttpRequest().getRelativeUri())); + uri = new URI(url); } catch (URISyntaxException e) { throw new IllegalArgumentException(e); } - taskName = task.getName(); - service = - Ascii.toLowerCase(task.getAppEngineHttpRequest().getAppEngineRouting().getService()); - method = task.getAppEngineHttpRequest().getHttpMethod(); - url = uri.getPath(); + Matcher hostnameMatcher = HOSTNAME_PATTERN.matcher(url); + assertThat(hostnameMatcher.find()).isTrue(); + service = Ascii.toLowerCase(hostnameMatcher.group()); + path = url.substring(String.format("https://%s.example.com", service).length()); + method = request.getHttpMethod(); scheduleTime = task.getScheduleTime(); ImmutableMultimap.Builder headerBuilder = new ImmutableMultimap.Builder<>(); - task.getAppEngineHttpRequest() + request .getHeadersMap() .forEach( (key, value) -> { @@ -269,14 +290,13 @@ public class CloudTasksHelper implements Serializable { // where parameters are not properly URL-encoded); it always does a best-effort parse. if (method == HttpMethod.GET && uri.getQuery() != null) { paramBuilder.putAll(UriParameters.parse(uri.getQuery())); - } else if (method == HttpMethod.POST && !task.getAppEngineHttpRequest().getBody().isEmpty()) { + } else if (method == HttpMethod.POST && !request.getBody().isEmpty()) { assertThat( headers.containsEntry( Ascii.toLowerCase(HttpHeaders.CONTENT_TYPE), MediaType.FORM_DATA.toString())) .isTrue(); paramBuilder.putAll( - UriParameters.parse( - task.getAppEngineHttpRequest().getBody().toString(StandardCharsets.UTF_8))); + UriParameters.parse(request.getBody().toString(StandardCharsets.UTF_8))); } params = paramBuilder.build(); } @@ -286,7 +306,7 @@ public class CloudTasksHelper implements Serializable { builder.put("taskName", taskName); builder.put("method", method); builder.put("service", service); - builder.put("url", url); + builder.put("path", path); builder.put("headers", headers); builder.put("params", params); builder.put("scheduleTime", scheduleTime); @@ -310,8 +330,8 @@ public class CloudTasksHelper implements Serializable { return this; } - public TaskMatcher url(String url) { - expected.url = url; + public TaskMatcher path(String path) { + expected.path = path; return this; } @@ -371,7 +391,7 @@ public class CloudTasksHelper implements Serializable { public boolean test(@Nonnull Task task) { MatchableTask actual = new MatchableTask(task); return (expected.taskName == null || Objects.equals(expected.taskName, actual.taskName)) - && (expected.url == null || Objects.equals(expected.url, actual.url)) + && (expected.path == null || Objects.equals(expected.path, actual.path)) && (expected.method == null || Objects.equals(expected.method, actual.method)) && (expected.service == null || Objects.equals(expected.service, actual.service)) && (expected.scheduleTime == null diff --git a/core/src/test/java/google/registry/tmch/NordnUploadActionTest.java b/core/src/test/java/google/registry/tmch/NordnUploadActionTest.java index 65a36434c..c671b64ab 100644 --- a/core/src/test/java/google/registry/tmch/NordnUploadActionTest.java +++ b/core/src/test/java/google/registry/tmch/NordnUploadActionTest.java @@ -252,7 +252,7 @@ class NordnUploadActionTest { cloudTasksHelper.assertTasksEnqueued( NordnVerifyAction.QUEUE, new TaskMatcher() - .url(NordnVerifyAction.PATH) + .path(NordnVerifyAction.PATH) .param(NordnVerifyAction.NORDN_URL_PARAM, LOCATION_URL) .param(RequestParameters.PARAM_TLD, "tld") .header(CONTENT_TYPE, FORM_DATA.toString())); diff --git a/core/src/test/java/google/registry/tools/DomainLockUtilsTest.java b/core/src/test/java/google/registry/tools/DomainLockUtilsTest.java index 079119c91..1ecce093b 100644 --- a/core/src/test/java/google/registry/tools/DomainLockUtilsTest.java +++ b/core/src/test/java/google/registry/tools/DomainLockUtilsTest.java @@ -259,7 +259,7 @@ public final class DomainLockUtilsTest { cloudTasksHelper.assertTasksEnqueued( QUEUE_ASYNC_ACTIONS, new TaskMatcher() - .url(RelockDomainAction.PATH) + .path(RelockDomainAction.PATH) .method(HttpMethod.POST) .service("backend") .param( @@ -481,7 +481,7 @@ public final class DomainLockUtilsTest { cloudTasksHelper.assertTasksEnqueued( QUEUE_ASYNC_ACTIONS, new CloudTasksHelper.TaskMatcher() - .url(RelockDomainAction.PATH) + .path(RelockDomainAction.PATH) .method(HttpMethod.POST) .service("backend") .param( diff --git a/core/src/test/java/google/registry/tools/GcpProjectConnectionTest.java b/core/src/test/java/google/registry/tools/GcpProjectConnectionTest.java index e5224a51d..d73130b0e 100644 --- a/core/src/test/java/google/registry/tools/GcpProjectConnectionTest.java +++ b/core/src/test/java/google/registry/tools/GcpProjectConnectionTest.java @@ -73,7 +73,7 @@ final class GcpProjectConnectionTest { ByteArrayOutputStream output = new ByteArrayOutputStream(); getStreamingContent().writeTo(output); output.close(); - return new String(output.toByteArray(), UTF_8); + return output.toString(UTF_8); } } @@ -97,7 +97,7 @@ final class GcpProjectConnectionTest { .isEqualTo("MyContent"); assertThat(httpTransport.method).isEqualTo("GET"); assertThat(httpTransport.url) - .isEqualTo("https://localhost/my/path?query&key1=value1&key2=value2"); + .isEqualTo("https://tools.example.com/my/path?query&key1=value1&key2=value2"); assertThat(lowLevelHttpRequest.headers).containsEntry("Cache-Control", "no-cache"); assertThat(lowLevelHttpRequest.headers).containsEntry("x-requested-with", "RegistryTool"); } @@ -113,7 +113,7 @@ final class GcpProjectConnectionTest { .isEqualTo("MyContent"); assertThat(httpTransport.method).isEqualTo("POST"); assertThat(httpTransport.url) - .isEqualTo("https://localhost/my/path?query&key1=value1&key2=value2"); + .isEqualTo("https://tools.example.com/my/path?query&key1=value1&key2=value2"); assertThat(lowLevelHttpRequest.getContentType()).isEqualTo("text/plain; charset=utf-8"); assertThat(lowLevelHttpRequest.getContentString()).isEqualTo("some data"); assertThat(lowLevelHttpRequest.headers).containsEntry("Cache-Control", "no-cache"); @@ -130,7 +130,7 @@ final class GcpProjectConnectionTest { "/my/path?query", ImmutableMap.of("string", "value1", "bool", true))) .containsExactly("key", "value"); assertThat(httpTransport.method).isEqualTo("POST"); - assertThat(httpTransport.url).isEqualTo("https://localhost/my/path?query"); + assertThat(httpTransport.url).isEqualTo("https://tools.example.com/my/path?query"); assertThat(lowLevelHttpRequest.getContentType()).isEqualTo("application/json; charset=utf-8"); assertThat(lowLevelHttpRequest.getContentString()) .isEqualTo("{\"string\":\"value1\",\"bool\":true}"); diff --git a/core/src/test/java/google/registry/tools/GenerateEscrowDepositCommandTest.java b/core/src/test/java/google/registry/tools/GenerateEscrowDepositCommandTest.java index 5cfc001cc..a668b972b 100644 --- a/core/src/test/java/google/registry/tools/GenerateEscrowDepositCommandTest.java +++ b/core/src/test/java/google/registry/tools/GenerateEscrowDepositCommandTest.java @@ -187,7 +187,7 @@ public class GenerateEscrowDepositCommandTest cloudTasksHelper.assertTasksEnqueued( "rde-report", new TaskMatcher() - .url("/_dr/task/rdeStaging") + .path("/_dr/task/rdeStaging") .param("mode", "THIN") .param("lenient", "true") .param("watermarks", "2017-01-01T00:00:00.000Z") @@ -204,7 +204,7 @@ public class GenerateEscrowDepositCommandTest cloudTasksHelper.assertTasksEnqueued( "rde-report", new TaskMatcher() - .url("/_dr/task/rdeStaging") + .path("/_dr/task/rdeStaging") .param("mode", "THIN") .param("lenient", "false") .param("watermarks", "2017-01-01T00:00:00.000Z") @@ -221,7 +221,7 @@ public class GenerateEscrowDepositCommandTest cloudTasksHelper.assertTasksEnqueued( "rde-report", new TaskMatcher() - .url("/_dr/task/rdeStaging") + .path("/_dr/task/rdeStaging") .param("lenient", "false") .param("mode", "THIN") .param("watermarks", "2017-01-01T00:00:00.000Z") @@ -237,7 +237,7 @@ public class GenerateEscrowDepositCommandTest cloudTasksHelper.assertTasksEnqueued( "rde-report", new TaskMatcher() - .url("/_dr/task/rdeStaging") + .path("/_dr/task/rdeStaging") .param("mode", "FULL") .param("lenient", "false") .param("watermarks", "2017-01-01T00:00:00.000Z") @@ -259,7 +259,7 @@ public class GenerateEscrowDepositCommandTest cloudTasksHelper.assertTasksEnqueued( "rde-report", new TaskMatcher() - .url("/_dr/task/rdeStaging") + .path("/_dr/task/rdeStaging") .param("mode", "THIN") .param("lenient", "false") .param("watermarks", "2017-01-01T00:00:00.000Z,2017-01-02T00:00:00.000Z") diff --git a/core/src/test/java/google/registry/tools/RequestFactoryModuleTest.java b/core/src/test/java/google/registry/tools/RequestFactoryModuleTest.java index d423aebc3..d9fc2bd50 100644 --- a/core/src/test/java/google/registry/tools/RequestFactoryModuleTest.java +++ b/core/src/test/java/google/registry/tools/RequestFactoryModuleTest.java @@ -35,7 +35,6 @@ import com.google.auth.oauth2.UserCredentials; import google.registry.config.RegistryConfig; import google.registry.testing.SystemPropertyExtension; import google.registry.util.GoogleCredentialsBundle; -import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -65,7 +64,7 @@ public class RequestFactoryModuleTest { RegistryConfig.CONFIG_SETTINGS.get().gcpProject.isLocal = true; try { HttpRequestFactory factory = - RequestFactoryModule.provideHttpRequestFactory(credentialsBundle, Optional.empty()); + RequestFactoryModule.provideHttpRequestFactory(credentialsBundle, "client-id"); HttpRequestInitializer initializer = factory.getInitializer(); assertThat(initializer).isNotNull(); HttpRequest request = factory.buildGetRequest(new GenericUrl("http://localhost")); @@ -79,29 +78,7 @@ public class RequestFactoryModuleTest { @Test void test_provideHttpRequestFactory_remote() throws Exception { when(credentialsBundle.getHttpRequestInitializer()).thenReturn(httpRequestInitializer); - // Make sure that example.com creates a request factory with the UNITTEST client id but no - boolean origIsLocal = RegistryConfig.CONFIG_SETTINGS.get().gcpProject.isLocal; - RegistryConfig.CONFIG_SETTINGS.get().gcpProject.isLocal = false; - try { - HttpRequestFactory factory = - RequestFactoryModule.provideHttpRequestFactory(credentialsBundle, Optional.empty()); - HttpRequestInitializer initializer = factory.getInitializer(); - assertThat(initializer).isNotNull(); - // HttpRequestFactory#buildGetRequest() calls initialize() once. - HttpRequest request = factory.buildGetRequest(new GenericUrl("http://localhost")); - verify(httpRequestInitializer).initialize(request); - assertThat(request.getConnectTimeout()).isEqualTo(REQUEST_TIMEOUT_MS); - assertThat(request.getReadTimeout()).isEqualTo(REQUEST_TIMEOUT_MS); - verifyNoMoreInteractions(httpRequestInitializer); - } finally { - RegistryConfig.CONFIG_SETTINGS.get().gcpProject.isLocal = origIsLocal; - } - } - - @Test - void test_provideHttpRequestFactory_remote_withIap() throws Exception { - when(credentialsBundle.getHttpRequestInitializer()).thenReturn(httpRequestInitializer); - // Mock the request/response to/from the IAP server requesting an ID token + // Mock the request/response to/from the OIDC server requesting an ID token UserCredentials mockUserCredentials = mock(UserCredentials.class); when(credentialsBundle.getGoogleCredentials()).thenReturn(mockUserCredentials); HttpTransport mockTransport = mock(HttpTransport.class); @@ -114,17 +91,16 @@ public class RequestFactoryModuleTest { HttpResponse mockResponse = mock(HttpResponse.class); when(mockPostRequest.execute()).thenReturn(mockResponse); GenericData genericDataResponse = new GenericData(); - genericDataResponse.set("id_token", "iapIdToken"); + genericDataResponse.set("id_token", "oidc.token"); when(mockResponse.parseAs(GenericData.class)).thenReturn(genericDataResponse); boolean origIsLocal = RegistryConfig.CONFIG_SETTINGS.get().gcpProject.isLocal; RegistryConfig.CONFIG_SETTINGS.get().gcpProject.isLocal = false; try { HttpRequestFactory factory = - RequestFactoryModule.provideHttpRequestFactory( - credentialsBundle, Optional.of("iapClientId")); + RequestFactoryModule.provideHttpRequestFactory(credentialsBundle, "clientId"); HttpRequest request = factory.buildGetRequest(new GenericUrl("http://localhost")); - assertThat(request.getHeaders().get("Proxy-Authorization")).isEqualTo("Bearer iapIdToken"); + assertThat(request.getHeaders().get("Proxy-Authorization")).isEqualTo("Bearer oidc.token"); assertThat(request.getConnectTimeout()).isEqualTo(REQUEST_TIMEOUT_MS); assertThat(request.getReadTimeout()).isEqualTo(REQUEST_TIMEOUT_MS); verify(httpRequestInitializer).initialize(request); diff --git a/core/src/test/java/google/registry/tools/ServiceConnectionTest.java b/core/src/test/java/google/registry/tools/ServiceConnectionTest.java index e465de813..c33cd40f6 100644 --- a/core/src/test/java/google/registry/tools/ServiceConnectionTest.java +++ b/core/src/test/java/google/registry/tools/ServiceConnectionTest.java @@ -26,13 +26,13 @@ public class ServiceConnectionTest { void testServerUrl_notCanary() { ServiceConnection connection = new ServiceConnection().withService(DEFAULT, false); String serverUrl = connection.getServer().toString(); - assertThat(serverUrl).isEqualTo("https://localhost"); // See default-config.yaml + assertThat(serverUrl).isEqualTo("https://default.example.com"); // See default-config.yaml } @Test void testServerUrl_canary() { ServiceConnection connection = new ServiceConnection().withService(DEFAULT, true); String serverUrl = connection.getServer().toString(); - assertThat(serverUrl).isEqualTo("https://nomulus-dot-localhost"); + assertThat(serverUrl).isEqualTo("https://nomulus-dot-default.example.com"); } } diff --git a/core/src/test/java/google/registry/ui/server/registrar/RegistrarSettingsActionTest.java b/core/src/test/java/google/registry/ui/server/registrar/RegistrarSettingsActionTest.java index d5190da2f..d59bfcf50 100644 --- a/core/src/test/java/google/registry/ui/server/registrar/RegistrarSettingsActionTest.java +++ b/core/src/test/java/google/registry/ui/server/registrar/RegistrarSettingsActionTest.java @@ -70,7 +70,7 @@ class RegistrarSettingsActionTest extends RegistrarSettingsActionTestCase { cloudTasksHelper.assertTasksEnqueued( "sheet", new TaskMatcher() - .url(SyncRegistrarsSheetAction.PATH) + .path(SyncRegistrarsSheetAction.PATH) .service("Backend") .method(HttpMethod.GET)); assertMetric(CLIENT_ID, "update", "[OWNER]", "SUCCESS"); diff --git a/proxy/src/main/java/google/registry/proxy/EppProtocolModule.java b/proxy/src/main/java/google/registry/proxy/EppProtocolModule.java index dbfa0f19c..833548114 100644 --- a/proxy/src/main/java/google/registry/proxy/EppProtocolModule.java +++ b/proxy/src/main/java/google/registry/proxy/EppProtocolModule.java @@ -16,7 +16,6 @@ package google.registry.proxy; import static google.registry.util.ResourceUtils.readResourceBytes; -import com.google.auth.oauth2.GoogleCredentials; import com.google.common.collect.ImmutableList; import dagger.Module; import dagger.Provides; @@ -44,7 +43,6 @@ import io.netty.handler.timeout.ReadTimeoutHandler; import java.io.IOException; import java.security.PrivateKey; import java.security.cert.X509Certificate; -import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.ScheduledExecutorService; import java.util.function.Supplier; @@ -116,7 +114,7 @@ public final class EppProtocolModule { config.epp.headerLengthBytes, // Adjustment applied to the header field value in order to obtain message length. -config.epp.headerLengthBytes, - // Initial bytes to strip (i. e. strip the length header). + // Initial bytes to strip (i.e. strip the length header). config.epp.headerLengthBytes); } @@ -149,18 +147,12 @@ public final class EppProtocolModule { @Provides static EppServiceHandler provideEppServiceHandler( - Supplier refreshedCredentialsSupplier, - @Named("iapClientId") Optional iapClientId, + @Named("idToken") Supplier idTokenSupplier, @Named("hello") byte[] helloBytes, FrontendMetrics metrics, ProxyConfig config) { return new EppServiceHandler( - config.epp.relayHost, - config.epp.relayPath, - refreshedCredentialsSupplier, - iapClientId, - helloBytes, - metrics); + config.epp.relayHost, config.epp.relayPath, idTokenSupplier, helloBytes, metrics); } @Singleton diff --git a/proxy/src/main/java/google/registry/proxy/ProxyConfig.java b/proxy/src/main/java/google/registry/proxy/ProxyConfig.java index 597407d03..c9c41818d 100644 --- a/proxy/src/main/java/google/registry/proxy/ProxyConfig.java +++ b/proxy/src/main/java/google/registry/proxy/ProxyConfig.java @@ -40,7 +40,7 @@ public class ProxyConfig { private static final String CUSTOM_CONFIG_FORMATTER = "config/proxy-config-%s.yaml"; public String projectId; - public String iapClientId; + public String oauthClientId; public List gcpScopes; public int serverCertificateCacheSeconds; public Gcs gcs; diff --git a/proxy/src/main/java/google/registry/proxy/ProxyModule.java b/proxy/src/main/java/google/registry/proxy/ProxyModule.java index 455a957f7..623d622db 100644 --- a/proxy/src/main/java/google/registry/proxy/ProxyModule.java +++ b/proxy/src/main/java/google/registry/proxy/ProxyModule.java @@ -28,6 +28,7 @@ import com.google.cloud.storage.BlobId; import com.google.cloud.storage.Storage; import com.google.cloud.storage.StorageException; import com.google.cloud.storage.StorageOptions; +import com.google.common.base.Suppliers; import com.google.monitoring.metrics.MetricReporter; import dagger.Component; import dagger.Module; @@ -45,6 +46,7 @@ import google.registry.proxy.handler.ProxyProtocolHandler; import google.registry.util.Clock; import google.registry.util.GoogleCredentialsBundle; import google.registry.util.JdkLoggerConfig; +import google.registry.util.OidcTokenUtils; import google.registry.util.SystemClock; import io.netty.handler.logging.LogLevel; import io.netty.handler.logging.LoggingHandler; @@ -59,6 +61,7 @@ import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; import java.util.function.Supplier; import java.util.logging.ConsoleHandler; import java.util.logging.Handler; @@ -126,7 +129,8 @@ public class ProxyModule { // corresponds to Level.FINE (JUL log level). It uses a JUL logger with the name // "io.netty.handler.logging.LoggingHandler" to actually process the logs. This JUL logger is // set to Level.FINE if the --log parameter is passed, so that it does not filter out logs - // that the LoggingHandler writes. Otherwise the logs are silently ignored because the default + // that the LoggingHandler writes. Otherwise, the logs are silently ignored because the + // default // JUL logger level is Level.INFO. JdkLoggerConfig.getConfig(LoggingHandler.class).setLevel(Level.FINE); // Log source IP information if --log parameter is passed. This is considered PII and should @@ -158,10 +162,10 @@ public class ProxyModule { } @Provides - @Named("iapClientId") + @Named("oauthClientId") @Singleton - Optional provideIapClientId(ProxyConfig config) { - return Optional.ofNullable(config.iapClientId); + String provideClientId(ProxyConfig config) { + return config.oauthClientId; } @Provides @@ -242,6 +246,23 @@ public class ProxyModule { }; } + /** + * Provides an OIDC token with the given OAuth client ID as audience used for authentication. + * + *

The token is cached for 1 hour to reduce repeated calls to the metadata server. + * + * @see ID token + * lifetime + */ + @Singleton + @Provides + @Named("idToken") + static Supplier provideOidcToken( + GoogleCredentialsBundle credentialsBundle, @Named("oauthClientId") String clientId) { + return Suppliers.memoizeWithExpiration( + () -> OidcTokenUtils.createOidcToken(credentialsBundle, clientId), 1, TimeUnit.HOURS); + } + @Singleton @Provides static CloudKMS provideCloudKms(GoogleCredentialsBundle credentialsBundle, ProxyConfig config) { diff --git a/proxy/src/main/java/google/registry/proxy/WhoisProtocolModule.java b/proxy/src/main/java/google/registry/proxy/WhoisProtocolModule.java index b93568017..108d1ef49 100644 --- a/proxy/src/main/java/google/registry/proxy/WhoisProtocolModule.java +++ b/proxy/src/main/java/google/registry/proxy/WhoisProtocolModule.java @@ -14,7 +14,6 @@ package google.registry.proxy; -import com.google.auth.oauth2.GoogleCredentials; import com.google.common.collect.ImmutableList; import dagger.Module; import dagger.Provides; @@ -35,7 +34,6 @@ import google.registry.util.Clock; import io.netty.channel.ChannelHandler; import io.netty.handler.codec.LineBasedFrameDecoder; import io.netty.handler.timeout.ReadTimeoutHandler; -import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.ScheduledExecutorService; import java.util.function.Supplier; @@ -46,7 +44,9 @@ import javax.inject.Singleton; /** A module that provides the {@link FrontendProtocol} used for whois protocol. */ @Module -public class WhoisProtocolModule { +public final class WhoisProtocolModule { + + private WhoisProtocolModule() {} /** Dagger qualifier to provide whois protocol related handlers and other bindings. */ @Qualifier @@ -93,15 +93,10 @@ public class WhoisProtocolModule { @Provides static WhoisServiceHandler provideWhoisServiceHandler( ProxyConfig config, - Supplier refreshedCredentialsSupplier, - @Named("iapClientId") Optional iapClientId, + @Named("idToken") Supplier idTokenSupplier, FrontendMetrics metrics) { return new WhoisServiceHandler( - config.whois.relayHost, - config.whois.relayPath, - refreshedCredentialsSupplier, - iapClientId, - metrics); + config.whois.relayHost, config.whois.relayPath, idTokenSupplier, metrics); } @Provides diff --git a/proxy/src/main/java/google/registry/proxy/config/default-config.yaml b/proxy/src/main/java/google/registry/proxy/config/default-config.yaml index 88db88581..3ded21bfd 100644 --- a/proxy/src/main/java/google/registry/proxy/config/default-config.yaml +++ b/proxy/src/main/java/google/registry/proxy/config/default-config.yaml @@ -8,14 +8,17 @@ # GCP project ID projectId: your-gcp-project-id -# IAP client ID, if IAP is enabled for this project -iapClientId: null +# OAuth client ID set as the audience of the OIDC token. This value must be the +# same as the auth.oauthClientId value in Nomulus config file, which usually is +# the IAP client ID, to allow the request to access IAP protected endpoints. +# Regular OIDC authentication mechanism also checks for this audience. +oauthClientId: iap-client-id # OAuth scope that the GoogleCredential will be constructed with. This list # should include all service scopes that the proxy depends on. gcpScopes: # The default OAuth scope granted to GCE instances. Local development instance - # needs this scope to mimic running on GCE. Currently it is used to access + # needs this scope to mimic running on GCE. Currently, it is used to access # Cloud KMS and Stackdriver Monitoring APIs. - https://www.googleapis.com/auth/cloud-platform @@ -25,8 +28,8 @@ gcpScopes: # Server certificate is cached for 30 minutes. # -# Encrypted server server certificate and private keys are stored on GCS. They -# are cached and shared for all connections for 30 minutes. We not not cache +# Encrypted server certificate and private keys are stored on GCS. They +# are cached and shared for all connections for 30 minutes. We do not cache # the certificate indefinitely because if we upload a new one to GCS, all # existing instances need to be killed if they cache the old one indefinitely. serverCertificateCacheSeconds: 1800 @@ -59,8 +62,8 @@ epp: # The first 4 bytes in a message is the total length of message, in bytes. # # We accept a message up to 1 GB, which should be plentiful, if not over the - # top. In fact we should probably limit this to a more reasonable number, as a - # 1 GB message will likely cause the proxy to go out of memory. + # top. In fact, we should probably limit this to a more reasonable number, as + # a 1 GB message will likely cause the proxy to go out of memory. # # See also: RFC 5734 4 Data Unit Format # (https://tools.ietf.org/html/rfc5734#section-4). @@ -76,7 +79,7 @@ epp: # Time after which an idle connection will be closed. # # The RFC gives registry discretionary power to set a timeout period. 1 hr - # should be reasonable enough for any registrar to login and submit their + # should be reasonable enough for any registrar to log in and submit their # request. readTimeoutSeconds: 3600 @@ -91,7 +94,7 @@ epp: # Default quota for any userId not matched in customQuota. defaultQuota: - # List of identifiers, e. g. IP address, certificate hash. + # List of identifiers, e.g. IP address, certificate hash. # # userId for defaultQuota should always be an empty list. Any value # in the list will be discarded. @@ -129,7 +132,7 @@ whois: # (http://www.freesoft.org/CIE/RFC/1035/9.htm). maxMessageLengthBytes: 512 - # Whois protocol is transient, the client should not establish a long lasting + # Whois protocol is transient, the client should not establish a long-lasting # idle connection. readTimeoutSeconds: 60 @@ -144,7 +147,7 @@ whois: # Default quota for any userId not matched in customQuota. defaultQuota: - # List of identifiers, e. g. IP address, certificate hash. + # List of identifiers, e.g. IP address, certificate hash. # # userId for defaultQuota should always be an empty list. userId: [] diff --git a/proxy/src/main/java/google/registry/proxy/handler/EppServiceHandler.java b/proxy/src/main/java/google/registry/proxy/handler/EppServiceHandler.java index af0384073..fb0233dcf 100644 --- a/proxy/src/main/java/google/registry/proxy/handler/EppServiceHandler.java +++ b/proxy/src/main/java/google/registry/proxy/handler/EppServiceHandler.java @@ -20,7 +20,6 @@ import static google.registry.networking.handler.SslServerInitializer.CLIENT_CER import static google.registry.proxy.handler.ProxyProtocolHandler.REMOTE_ADDRESS_KEY; import static google.registry.util.X509Utils.getCertificateHash; -import com.google.auth.oauth2.GoogleCredentials; import com.google.common.flogger.FluentLogger; import google.registry.proxy.metric.FrontendMetrics; import google.registry.util.ProxyHttpHeaders; @@ -37,7 +36,6 @@ import io.netty.handler.ssl.SslHandshakeCompletionEvent; import io.netty.util.AttributeKey; import io.netty.util.concurrent.Promise; import java.security.cert.X509Certificate; -import java.util.Optional; import java.util.function.Supplier; /** Handler that processes EPP protocol logic. */ @@ -62,12 +60,11 @@ public class EppServiceHandler extends HttpsRelayServiceHandler { public EppServiceHandler( String relayHost, String relayPath, - Supplier refreshedCredentialsSupplier, - Optional iapClientId, + Supplier idTokenSupplier, byte[] helloBytes, FrontendMetrics metrics) { - super(relayHost, relayPath, refreshedCredentialsSupplier, iapClientId, metrics); - this.helloBytes = helloBytes; + super(relayHost, relayPath, idTokenSupplier, metrics); + this.helloBytes = helloBytes.clone(); } /** @@ -91,6 +88,7 @@ public class EppServiceHandler extends HttpsRelayServiceHandler { */ @Override public void channelActive(ChannelHandlerContext ctx) throws Exception { + @SuppressWarnings("unused") Promise unusedPromise = ctx.channel() .attr(CLIENT_CERTIFICATE_PROMISE_KEY) @@ -110,6 +108,7 @@ public class EppServiceHandler extends HttpsRelayServiceHandler { logger.atWarning().withCause(promise.cause()).log( "Cannot finish handshake for channel %s, remote IP %s", ctx.channel(), ctx.channel().attr(REMOTE_ADDRESS_KEY).get()); + @SuppressWarnings("unused") ChannelFuture unusedFuture = ctx.close(); } }); @@ -136,7 +135,7 @@ public class EppServiceHandler extends HttpsRelayServiceHandler { checkArgument(msg instanceof HttpResponse); HttpResponse response = (HttpResponse) msg; String sessionAliveValue = response.headers().get(ProxyHttpHeaders.EPP_SESSION); - if (sessionAliveValue != null && sessionAliveValue.equals("close")) { + if ("close".equals(sessionAliveValue)) { promise.addListener(ChannelFutureListener.CLOSE); } super.write(ctx, msg, promise); diff --git a/proxy/src/main/java/google/registry/proxy/handler/HttpsRelayServiceHandler.java b/proxy/src/main/java/google/registry/proxy/handler/HttpsRelayServiceHandler.java index 8aa39f9f9..d64af4121 100644 --- a/proxy/src/main/java/google/registry/proxy/handler/HttpsRelayServiceHandler.java +++ b/proxy/src/main/java/google/registry/proxy/handler/HttpsRelayServiceHandler.java @@ -16,12 +16,7 @@ package google.registry.proxy.handler; import static java.nio.charset.StandardCharsets.UTF_8; -import com.google.auth.oauth2.GoogleCredentials; -import com.google.auth.oauth2.IdToken; -import com.google.auth.oauth2.IdTokenProvider; -import com.google.auth.oauth2.IdTokenProvider.Option; import com.google.common.base.Throwables; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.flogger.FluentLogger; import google.registry.proxy.metric.FrontendMetrics; @@ -42,11 +37,9 @@ import io.netty.handler.codec.http.cookie.ClientCookieDecoder; import io.netty.handler.codec.http.cookie.ClientCookieEncoder; import io.netty.handler.codec.http.cookie.Cookie; import io.netty.handler.timeout.ReadTimeoutException; -import java.io.IOException; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.function.Supplier; import javax.net.ssl.SSLHandshakeException; @@ -63,8 +56,8 @@ import javax.net.ssl.SSLHandshakeException; * of the next outbound handler in the channel pipeline, which eventually writes the response bytes * to the remote peer of this channel. * - *

This handler is session aware and will store all the session cookies that the are contained in - * the HTTP response headers, which are added back to headers of subsequent HTTP requests. + *

This handler is session-aware and will store all the session cookies that are contained in the + * HTTP response headers, which are added back to headers of subsequent HTTP requests. */ public abstract class HttpsRelayServiceHandler extends ByteToMessageCodec { @@ -79,21 +72,18 @@ public abstract class HttpsRelayServiceHandler extends ByteToMessageCodec cookieStore = new LinkedHashMap<>(); private final String relayHost; private final String relayPath; - private final Supplier refreshedCredentialsSupplier; - private final Optional iapClientId; + private final Supplier idTokenSupplier; protected final FrontendMetrics metrics; HttpsRelayServiceHandler( String relayHost, String relayPath, - Supplier refreshedCredentialsSupplier, - Optional iapClientId, + Supplier idTokenSupplier, FrontendMetrics metrics) { this.relayHost = relayHost; this.relayPath = relayPath; - this.refreshedCredentialsSupplier = refreshedCredentialsSupplier; - this.iapClientId = iapClientId; + this.idTokenSupplier = idTokenSupplier; this.metrics = metrics; } @@ -108,30 +98,12 @@ public abstract class HttpsRelayServiceHandler extends ByteToMessageCodec refreshedCredentialsSupplier, - Optional iapClientId, + Supplier idTokenSupplier, FrontendMetrics metrics) { - super(relayHost, relayPath, refreshedCredentialsSupplier, iapClientId, metrics); + super(relayHost, relayPath, idTokenSupplier, metrics); } @Override diff --git a/proxy/src/test/java/google/registry/proxy/EppProtocolModuleTest.java b/proxy/src/test/java/google/registry/proxy/EppProtocolModuleTest.java index 14b75e875..b2e157b15 100644 --- a/proxy/src/test/java/google/registry/proxy/EppProtocolModuleTest.java +++ b/proxy/src/test/java/google/registry/proxy/EppProtocolModuleTest.java @@ -24,7 +24,6 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import com.google.common.base.Throwables; import google.registry.proxy.handler.HttpsRelayServiceHandler.NonOkHttpResponseException; -import google.registry.testing.FakeClock; import google.registry.util.SelfSignedCaCertificate; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; @@ -90,7 +89,7 @@ class EppProtocolModuleTest extends ProtocolModuleTest { } /** Get a {@link ByteBuf} that represents the raw epp request with the given content. */ - private ByteBuf getByteBufFromContent(byte[] content) { + private static ByteBuf getByteBufFromContent(byte[] content) { ByteBuf buffer = Unpooled.buffer(); buffer.writeInt(content.length + HEADER_LENGTH); buffer.writeBytes(content); @@ -102,18 +101,17 @@ class EppProtocolModuleTest extends ProtocolModuleTest { new String(content, UTF_8), PROXY_CONFIG.epp.relayHost, PROXY_CONFIG.epp.relayPath, - TestModule.provideFakeCredentials().get(), + TestModule.provideFakeIdToken().get(), getCertificateHash(certificate), CLIENT_ADDRESS, - TestModule.provideIapClientId(), cookies); } - private FullHttpResponse makeEppHttpResponse(byte[] content, Cookie... cookies) { + private static FullHttpResponse makeEppHttpResponse(byte[] content, Cookie... cookies) { return makeEppHttpResponse(content, HttpResponseStatus.OK, cookies); } - private FullHttpResponse makeEppHttpResponse( + private static FullHttpResponse makeEppHttpResponse( byte[] content, HttpResponseStatus status, Cookie... cookies) { return TestUtils.makeEppHttpResponse(new String(content, UTF_8), status, cookies); } @@ -121,7 +119,7 @@ class EppProtocolModuleTest extends ProtocolModuleTest { @BeforeEach @Override void beforeEach() throws Exception { - testComponent = makeTestComponent(new FakeClock()); + testComponent = makeTestComponent(); certificate = SelfSignedCaCertificate.create().cert(); initializeChannel( ch -> { @@ -129,6 +127,7 @@ class EppProtocolModuleTest extends ProtocolModuleTest { ch.attr(CLIENT_CERTIFICATE_PROMISE_KEY).set(ch.eventLoop().newPromise()); addAllTestableHandlers(ch); }); + @SuppressWarnings("unused") Promise unusedPromise = channel.attr(CLIENT_CERTIFICATE_PROMISE_KEY).get().setSuccess(certificate); } diff --git a/proxy/src/test/java/google/registry/proxy/ProtocolModuleTest.java b/proxy/src/test/java/google/registry/proxy/ProtocolModuleTest.java index c3724db36..de79804b2 100644 --- a/proxy/src/test/java/google/registry/proxy/ProtocolModuleTest.java +++ b/proxy/src/test/java/google/registry/proxy/ProtocolModuleTest.java @@ -17,14 +17,7 @@ package google.registry.proxy; import static com.google.common.collect.ImmutableList.toImmutableList; import static google.registry.proxy.ProxyConfig.Environment.LOCAL; import static google.registry.proxy.ProxyConfig.getProxyConfig; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; -import com.google.auth.oauth2.AccessToken; -import com.google.auth.oauth2.ComputeEngineCredentials; -import com.google.auth.oauth2.GoogleCredentials; -import com.google.auth.oauth2.IdToken; -import com.google.auth.oauth2.IdTokenProvider.Option; import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -59,9 +52,7 @@ import io.netty.channel.embedded.EmbeddedChannel; import io.netty.handler.logging.LoggingHandler; import io.netty.handler.ssl.SslProvider; import io.netty.handler.timeout.ReadTimeoutHandler; -import java.io.IOException; import java.time.Duration; -import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -80,14 +71,14 @@ import org.junit.jupiter.api.BeforeEach; * correctly performed by various handlers attached to its pipeline. Non-business essential handlers * should be excluded. * - *

Subclass should implement an no-arg constructor that calls constructors of this class, + *

Subclass should implement a no-arg constructor that calls constructors of this class, * providing the method reference of the {@link TestComponent} method to call to obtain the list of * {@link ChannelHandler} providers for the {@link Protocol} to test, and optionally a set of {@link * ChannelHandler} classes to exclude from testing. */ public abstract class ProtocolModuleTest { - static final ProxyConfig PROXY_CONFIG = getProxyConfig(Environment.LOCAL); + static final ProxyConfig PROXY_CONFIG = getProxyConfig(LOCAL); TestComponent testComponent; @@ -112,7 +103,7 @@ public abstract class ProtocolModuleTest { FullHttpResponseRelayHandler.class, // This handler is tested in its own unit tests. It is installed in web whois redirect // protocols. The end-to-end tests for the rest of the handlers in its pipeline need to - // be able to emit incoming requests out of the channel for assertions. Therefore this + // be able to emit incoming requests out of the channel for assertions. Therefore, this // handler is removed from the pipeline. WebWhoisRedirectHandler.class, // The rest are not part of business logic and do not need to be tested, obviously. @@ -150,7 +141,7 @@ public abstract class ProtocolModuleTest { this(handlerProvidersMethod, DEFAULT_EXCLUDED_HANDLERS); } - /** Excludes handler providers that are not of interested for testing. */ + /** Excludes handler providers that are not of interest for testing. */ private ImmutableList> excludeHandlerProvidersForTesting( ImmutableList> handlerProviders) { return handlerProviders.stream() @@ -177,7 +168,7 @@ public abstract class ProtocolModuleTest { } } - static TestComponent makeTestComponent(FakeClock fakeClock) { + static TestComponent makeTestComponent() { return DaggerProtocolModuleTest_TestComponent.builder() .testModule(new TestModule(new FakeClock())) .build(); @@ -185,7 +176,7 @@ public abstract class ProtocolModuleTest { @BeforeEach void beforeEach() throws Exception { - testComponent = makeTestComponent(new FakeClock()); + testComponent = makeTestComponent(); initializeChannel(this::addAllTestableHandlers); } @@ -244,10 +235,11 @@ public abstract class ProtocolModuleTest { this.fakeClock = fakeClock; } + @Singleton @Provides - @Named("iapClientId") - public static Optional provideIapClientId() { - return Optional.of("iapClientId"); + @Named("clientId") + public static String provideClientId() { + return "clientId"; } @Singleton @@ -264,19 +256,9 @@ public abstract class ProtocolModuleTest { @Singleton @Provides - static Supplier provideFakeCredentials() { - ComputeEngineCredentials mockCredentials = mock(ComputeEngineCredentials.class); - when(mockCredentials.getAccessToken()).thenReturn(new AccessToken("fake.test.token", null)); - IdToken mockIdToken = mock(IdToken.class); - when(mockIdToken.getTokenValue()).thenReturn("fake.test.id.token"); - try { - when(mockCredentials.idTokenWithAudience( - "iapClientId", ImmutableList.of(Option.FORMAT_FULL))) - .thenReturn(mockIdToken); - } catch (IOException e) { - throw new RuntimeException(e); - } - return Suppliers.ofInstance(mockCredentials); + @Named("idToken") + static Supplier provideFakeIdToken() { + return Suppliers.ofInstance("fake.test.id.token"); } @Singleton @@ -306,7 +288,7 @@ public abstract class ProtocolModuleTest { @Singleton @Provides static Environment provideEnvironment() { - return Environment.LOCAL; + return LOCAL; } @Singleton diff --git a/proxy/src/test/java/google/registry/proxy/TestUtils.java b/proxy/src/test/java/google/registry/proxy/TestUtils.java index 363d84b03..ba185cc2c 100644 --- a/proxy/src/test/java/google/registry/proxy/TestUtils.java +++ b/proxy/src/test/java/google/registry/proxy/TestUtils.java @@ -17,10 +17,6 @@ package google.registry.proxy; import static com.google.common.truth.Truth.assertThat; import static java.nio.charset.StandardCharsets.US_ASCII; -import com.google.auth.oauth2.GoogleCredentials; -import com.google.auth.oauth2.IdTokenProvider; -import com.google.auth.oauth2.IdTokenProvider.Option; -import com.google.common.collect.ImmutableList; import google.registry.util.ProxyHttpHeaders; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; @@ -39,10 +35,11 @@ import io.netty.handler.codec.http.cookie.ClientCookieEncoder; import io.netty.handler.codec.http.cookie.Cookie; import io.netty.handler.codec.http.cookie.ServerCookieEncoder; import java.io.IOException; -import java.util.Optional; /** Utility class for various helper methods used in testing. */ -public class TestUtils { +public final class TestUtils { + + private TestUtils() {} public static FullHttpRequest makeHttpPostRequest(String content, String host, String path) { ByteBuf buf = Unpooled.wrappedBuffer(content.getBytes(US_ASCII)); @@ -77,19 +74,13 @@ public class TestUtils { } public static FullHttpRequest makeWhoisHttpRequest( - String content, - String host, - String path, - GoogleCredentials credentials, - Optional iapClientId) - throws IOException { + String content, String host, String path, String idToken) throws IOException { FullHttpRequest request = makeHttpPostRequest(content, host, path); request .headers() - .set("authorization", "Bearer " + credentials.getAccessToken().getTokenValue()) + .set("authorization", "Bearer " + idToken) .set(HttpHeaderNames.CONTENT_TYPE, "text/plain") .set("accept", "text/plain"); - maybeSetProxyAuthForIap(request, credentials, iapClientId); return request; } @@ -97,21 +88,19 @@ public class TestUtils { String content, String host, String path, - GoogleCredentials credentials, + String idToken, String sslClientCertificateHash, String clientAddress, - Optional iapClientId, Cookie... cookies) throws IOException { FullHttpRequest request = makeHttpPostRequest(content, host, path); request .headers() - .set("authorization", "Bearer " + credentials.getAccessToken().getTokenValue()) + .set("authorization", "Bearer " + idToken) .set(HttpHeaderNames.CONTENT_TYPE, "application/epp+xml") .set("accept", "application/epp+xml") .set(ProxyHttpHeaders.CERTIFICATE_HASH, sslClientCertificateHash) .set(ProxyHttpHeaders.IP_ADDRESS, clientAddress); - maybeSetProxyAuthForIap(request, credentials, iapClientId); if (cookies.length != 0) { request.headers().set("cookie", ClientCookieEncoder.STRICT.encode(cookies)); } @@ -140,7 +129,7 @@ public class TestUtils { *

This method is needed because an HTTP message decoded and aggregated from inbound {@link * ByteBuf} is of a different class than the one written to the outbound {@link ByteBuf}, and The * {@link ByteBuf} implementations that hold the content of the HTTP messages are different, even - * though the actual content, headers, etc are the same. + * though the actual content, headers, etc. are the same. * *

This method is not type-safe, msg1 & msg2 can be a request and a response, respectively. Do * not use this method directly. @@ -161,16 +150,4 @@ public class TestUtils { public static void assertHttpRequestEquivalent(HttpRequest req1, HttpRequest req2) { assertHttpMessageEquivalent(req1, req2); } - - private static void maybeSetProxyAuthForIap( - FullHttpRequest request, GoogleCredentials credentials, Optional iapClientId) - throws IOException { - if (iapClientId.isPresent()) { - String idTokenValue = - ((IdTokenProvider) credentials) - .idTokenWithAudience(iapClientId.get(), ImmutableList.of(Option.FORMAT_FULL)) - .getTokenValue(); - request.headers().set("proxy-authorization", "Bearer " + idTokenValue); - } - } } diff --git a/proxy/src/test/java/google/registry/proxy/WhoisProtocolModuleTest.java b/proxy/src/test/java/google/registry/proxy/WhoisProtocolModuleTest.java index 37742d4e3..4eb49a363 100644 --- a/proxy/src/test/java/google/registry/proxy/WhoisProtocolModuleTest.java +++ b/proxy/src/test/java/google/registry/proxy/WhoisProtocolModuleTest.java @@ -53,8 +53,7 @@ class WhoisProtocolModuleTest extends ProtocolModuleTest { "test.tld", PROXY_CONFIG.whois.relayHost, PROXY_CONFIG.whois.relayPath, - TestModule.provideFakeCredentials().get(), - TestModule.provideIapClientId()); + TestModule.provideFakeIdToken().get()); assertThat(actualRequest).isEqualTo(expectedRequest); assertThat(channel.isActive()).isTrue(); // Nothing more to read. @@ -89,8 +88,7 @@ class WhoisProtocolModuleTest extends ProtocolModuleTest { "test1.tld", PROXY_CONFIG.whois.relayHost, PROXY_CONFIG.whois.relayPath, - TestModule.provideFakeCredentials().get(), - TestModule.provideIapClientId()); + TestModule.provideFakeIdToken().get()); assertThat(actualRequest1).isEqualTo(expectedRequest1); // No more message at this point. assertThat((Object) channel.readInbound()).isNull(); @@ -104,8 +102,7 @@ class WhoisProtocolModuleTest extends ProtocolModuleTest { "test2.tld", PROXY_CONFIG.whois.relayHost, PROXY_CONFIG.whois.relayPath, - TestModule.provideFakeCredentials().get(), - TestModule.provideIapClientId()); + TestModule.provideFakeIdToken().get()); assertThat(actualRequest2).isEqualTo(expectedRequest2); // The third message is not complete yet. assertThat(channel.isActive()).isTrue(); diff --git a/proxy/src/test/java/google/registry/proxy/handler/EppServiceHandlerTest.java b/proxy/src/test/java/google/registry/proxy/handler/EppServiceHandlerTest.java index a5d8c7454..94130e757 100644 --- a/proxy/src/test/java/google/registry/proxy/handler/EppServiceHandlerTest.java +++ b/proxy/src/test/java/google/registry/proxy/handler/EppServiceHandlerTest.java @@ -25,14 +25,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; -import com.google.auth.oauth2.AccessToken; -import com.google.auth.oauth2.ComputeEngineCredentials; -import com.google.auth.oauth2.IdToken; -import com.google.auth.oauth2.IdTokenProvider.Option; import com.google.common.base.Throwables; -import com.google.common.collect.ImmutableList; import google.registry.proxy.TestUtils; import google.registry.proxy.handler.HttpsRelayServiceHandler.NonOkHttpResponseException; import google.registry.proxy.metric.FrontendMetrics; @@ -52,7 +46,6 @@ import io.netty.handler.codec.http.cookie.DefaultCookie; import io.netty.util.concurrent.Promise; import java.io.IOException; import java.security.cert.X509Certificate; -import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -69,27 +62,18 @@ class EppServiceHandlerTest { private static final String RELAY_PATH = "/epp"; private static final String CLIENT_ADDRESS = "epp.client.tld"; private static final String PROTOCOL = "epp"; - private static final String IAP_CLIENT_ID = "iapClientId"; - - private static final ComputeEngineCredentials mockCredentials = - mock(ComputeEngineCredentials.class); + private static final String ID_TOKEN = "fake.id.token"; private X509Certificate clientCertificate; private final FrontendMetrics metrics = mock(FrontendMetrics.class); private final EppServiceHandler eppServiceHandler = - new EppServiceHandler( - RELAY_HOST, - RELAY_PATH, - () -> mockCredentials, - Optional.of(IAP_CLIENT_ID), - HELLO.getBytes(UTF_8), - metrics); + new EppServiceHandler(RELAY_HOST, RELAY_PATH, () -> ID_TOKEN, HELLO.getBytes(UTF_8), metrics); private EmbeddedChannel channel; - private void setHandshakeSuccess(EmbeddedChannel channel, X509Certificate certificate) { + private static void setHandshakeSuccess(EmbeddedChannel channel, X509Certificate certificate) { @SuppressWarnings("unused") Promise unusedPromise = channel.attr(CLIENT_CERTIFICATE_PROMISE_KEY).get().setSuccess(certificate); @@ -99,7 +83,8 @@ class EppServiceHandlerTest { setHandshakeSuccess(channel, clientCertificate); } - private void setHandshakeFailure(EmbeddedChannel channel) { + private static void setHandshakeFailure(EmbeddedChannel channel) { + @SuppressWarnings("unused") Promise unusedPromise = channel .attr(CLIENT_CERTIFICATE_PROMISE_KEY) @@ -116,25 +101,19 @@ class EppServiceHandlerTest { content, RELAY_HOST, RELAY_PATH, - mockCredentials, + ID_TOKEN, getCertificateHash(clientCertificate), CLIENT_ADDRESS, - Optional.of(IAP_CLIENT_ID), cookies); } @BeforeEach void beforeEach() throws Exception { - when(mockCredentials.getAccessToken()).thenReturn(new AccessToken("this.access.token", null)); - IdToken mockIdToken = mock(IdToken.class); - when(mockIdToken.getTokenValue()).thenReturn("fake.test.id.token"); - when(mockCredentials.idTokenWithAudience(IAP_CLIENT_ID, ImmutableList.of(Option.FORMAT_FULL))) - .thenReturn(mockIdToken); clientCertificate = SelfSignedCaCertificate.create().cert(); channel = setUpNewChannel(eppServiceHandler); } - private EmbeddedChannel setUpNewChannel(EppServiceHandler handler) { + private static EmbeddedChannel setUpNewChannel(EppServiceHandler handler) { return new EmbeddedChannel( DefaultChannelId.newInstance(), new ChannelInitializer() { @@ -165,12 +144,7 @@ class EppServiceHandlerTest { // Set up the second channel. EppServiceHandler eppServiceHandler2 = new EppServiceHandler( - RELAY_HOST, - RELAY_PATH, - () -> mockCredentials, - Optional.empty(), - HELLO.getBytes(UTF_8), - metrics); + RELAY_HOST, RELAY_PATH, () -> ID_TOKEN, HELLO.getBytes(UTF_8), metrics); EmbeddedChannel channel2 = setUpNewChannel(eppServiceHandler2); setHandshakeSuccess(channel2, clientCertificate); @@ -190,12 +164,7 @@ class EppServiceHandlerTest { // Set up the second channel. EppServiceHandler eppServiceHandler2 = new EppServiceHandler( - RELAY_HOST, - RELAY_PATH, - () -> mockCredentials, - Optional.empty(), - HELLO.getBytes(UTF_8), - metrics); + RELAY_HOST, RELAY_PATH, () -> ID_TOKEN, HELLO.getBytes(UTF_8), metrics); EmbeddedChannel channel2 = setUpNewChannel(eppServiceHandler2); X509Certificate clientCertificate2 = SelfSignedCaCertificate.create().cert(); setHandshakeSuccess(channel2, clientCertificate2); @@ -358,38 +327,4 @@ class EppServiceHandlerTest { assertThat((Object) channel.readOutbound()).isNull(); assertThat(channel.isActive()).isTrue(); } - - @Test - void testSuccess_withoutIapClientId() throws Exception { - // Without an IAP client ID configured, we shouldn't include the proxy-authorization header - EppServiceHandler nonIapServiceHandler = - new EppServiceHandler( - RELAY_HOST, - RELAY_PATH, - () -> mockCredentials, - Optional.empty(), - HELLO.getBytes(UTF_8), - metrics); - channel = setUpNewChannel(nonIapServiceHandler); - - setHandshakeSuccess(); - // First inbound message is hello. - channel.readInbound(); - String content = "stuff"; - channel.writeInbound(Unpooled.wrappedBuffer(content.getBytes(UTF_8))); - FullHttpRequest request = channel.readInbound(); - assertThat(request) - .isEqualTo( - TestUtils.makeEppHttpRequest( - content, - RELAY_HOST, - RELAY_PATH, - mockCredentials, - getCertificateHash(clientCertificate), - CLIENT_ADDRESS, - Optional.empty())); - // Nothing further to pass to the next handler. - assertThat((Object) channel.readInbound()).isNull(); - assertThat(channel.isActive()).isTrue(); - } } diff --git a/proxy/src/test/java/google/registry/proxy/handler/WhoisServiceHandlerTest.java b/proxy/src/test/java/google/registry/proxy/handler/WhoisServiceHandlerTest.java index 07d420daf..bd8d9200d 100644 --- a/proxy/src/test/java/google/registry/proxy/handler/WhoisServiceHandlerTest.java +++ b/proxy/src/test/java/google/registry/proxy/handler/WhoisServiceHandlerTest.java @@ -22,14 +22,8 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; -import static org.mockito.Mockito.when; -import com.google.auth.oauth2.AccessToken; -import com.google.auth.oauth2.ComputeEngineCredentials; -import com.google.auth.oauth2.IdToken; -import com.google.auth.oauth2.IdTokenProvider.Option; import com.google.common.base.Throwables; -import com.google.common.collect.ImmutableList; import google.registry.proxy.handler.HttpsRelayServiceHandler.NonOkHttpResponseException; import google.registry.proxy.metric.FrontendMetrics; import io.netty.buffer.ByteBuf; @@ -40,7 +34,6 @@ import io.netty.handler.codec.EncoderException; import io.netty.handler.codec.http.FullHttpRequest; import io.netty.handler.codec.http.FullHttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; -import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -52,24 +45,16 @@ class WhoisServiceHandlerTest { private static final String QUERY_CONTENT = "test.tld"; private static final String PROTOCOL = "whois"; private static final String CLIENT_HASH = "none"; - private static final String IAP_CLIENT_ID = "iapClientId"; + private static final String ID_TOKEN = "fake.id.token"; - private static final ComputeEngineCredentials mockCredentials = - mock(ComputeEngineCredentials.class); private final FrontendMetrics metrics = mock(FrontendMetrics.class); private final WhoisServiceHandler whoisServiceHandler = - new WhoisServiceHandler( - RELAY_HOST, RELAY_PATH, () -> mockCredentials, Optional.of(IAP_CLIENT_ID), metrics); + new WhoisServiceHandler(RELAY_HOST, RELAY_PATH, () -> ID_TOKEN, metrics); private EmbeddedChannel channel; @BeforeEach void beforeEach() throws Exception { - when(mockCredentials.getAccessToken()).thenReturn(new AccessToken("this.access.token", null)); - IdToken mockIdToken = mock(IdToken.class); - when(mockIdToken.getTokenValue()).thenReturn("fake.test.id.token"); - when(mockCredentials.idTokenWithAudience(IAP_CLIENT_ID, ImmutableList.of(Option.FORMAT_FULL))) - .thenReturn(mockIdToken); // Need to reset metrics for each test method, since they are static fields on the class and // shared between each run. channel = new EmbeddedChannel(whoisServiceHandler); @@ -89,8 +74,7 @@ class WhoisServiceHandlerTest { // Setup second channel. WhoisServiceHandler whoisServiceHandler2 = - new WhoisServiceHandler( - RELAY_HOST, RELAY_PATH, () -> mockCredentials, Optional.empty(), metrics); + new WhoisServiceHandler(RELAY_HOST, RELAY_PATH, () -> ID_TOKEN, metrics); EmbeddedChannel channel2 = // We need a new channel id so that it has a different hash code. // This only is needed for EmbeddedChannel because it has a dummy hash code implementation. @@ -104,8 +88,7 @@ class WhoisServiceHandlerTest { void testSuccess_fireInboundHttpRequest() throws Exception { ByteBuf inputBuffer = Unpooled.wrappedBuffer(QUERY_CONTENT.getBytes(US_ASCII)); FullHttpRequest expectedRequest = - makeWhoisHttpRequest( - QUERY_CONTENT, RELAY_HOST, RELAY_PATH, mockCredentials, Optional.of(IAP_CLIENT_ID)); + makeWhoisHttpRequest(QUERY_CONTENT, RELAY_HOST, RELAY_PATH, ID_TOKEN); // Input data passed to next handler assertThat(channel.writeInbound(inputBuffer)).isTrue(); FullHttpRequest inputRequest = channel.readInbound(); @@ -128,27 +111,6 @@ class WhoisServiceHandlerTest { assertThat(channel.isActive()).isFalse(); } - @Test - void testSuccess_withoutIapClientId() throws Exception { - // Without an IAP client ID configured, we shouldn't include the proxy-authorization header - WhoisServiceHandler nonIapHandler = - new WhoisServiceHandler( - RELAY_HOST, RELAY_PATH, () -> mockCredentials, Optional.empty(), metrics); - channel = new EmbeddedChannel(nonIapHandler); - - ByteBuf inputBuffer = Unpooled.wrappedBuffer(QUERY_CONTENT.getBytes(US_ASCII)); - FullHttpRequest expectedRequest = - makeWhoisHttpRequest( - QUERY_CONTENT, RELAY_HOST, RELAY_PATH, mockCredentials, Optional.empty()); - // Input data passed to next handler - assertThat(channel.writeInbound(inputBuffer)).isTrue(); - FullHttpRequest inputRequest = channel.readInbound(); - assertThat(inputRequest).isEqualTo(expectedRequest); - // The channel is still open, and nothing else is to be read from it. - assertThat((Object) channel.readInbound()).isNull(); - assertThat(channel.isActive()).isTrue(); - } - @Test void testFailure_OutboundHttpResponseNotOK() { String outputString = "line1\r\nline2\r\n"; diff --git a/release/builder/Dockerfile b/release/builder/Dockerfile index 1f6ef503a..4e2ea9f6f 100644 --- a/release/builder/Dockerfile +++ b/release/builder/Dockerfile @@ -23,8 +23,9 @@ FROM golang:1.19 as deployCloudSchedulerAndQueueBuilder WORKDIR /usr/src/deployCloudSchedulerAndQueue -RUN go mod init deployCloudSchedulerAndQueue -COPY *.go ./ +COPY deployCloudSchedulerAndQueue.go ./ +COPY go.sum ./ +COPY go.mod ./ RUN go build -o /deployCloudSchedulerAndQueue FROM marketplace.gcr.io/google/debian10 diff --git a/release/builder/deployCloudSchedulerAndQueue.go b/release/builder/deployCloudSchedulerAndQueue.go index 9e4302536..c386db26b 100644 --- a/release/builder/deployCloudSchedulerAndQueue.go +++ b/release/builder/deployCloudSchedulerAndQueue.go @@ -26,10 +26,14 @@ import ( "os" "os/exec" "strings" + + "gopkg.in/yaml.v3" ) var projectName string +var clientId string + const GcpLocation = "us-central1" type SyncManager[T Task | Queue] interface { @@ -74,6 +78,12 @@ type TasksSyncManager struct { ServiceAccountEmail string } +type YamlEntries struct { + Auth struct { + OauthClientId string `yaml:"oauthClientId"` + } `yaml:"auth"` +} + type XmlEntries struct { XMLName xml.Name `xml:"entries"` Tasks []Task `xml:"task"` @@ -180,7 +190,7 @@ func (manager TasksSyncManager) getArgs(task Task, operationType string) []strin "--description", description, "--http-method", "get", "--oidc-service-account-email", getCloudSchedulerServiceAccountEmail(), - "--oidc-token-audience", projectName, + "--oidc-token-audience", clientId, } } @@ -301,21 +311,37 @@ func getExistingEntries(cmd *exec.Cmd) ExistingEntries { } func main() { - if len(os.Args) < 3 || os.Args[1] == "" || os.Args[2] == "" { - panic("Error - Invalid Parameters.\nRequired params: 1 - config file path; 2 - project name;") + if len(os.Args) < 4 || os.Args[1] == "" || os.Args[2] == "" || os.Args[3] == "" { + panic("Error - Invalid Parameters.\n" + + "Required params: 1 - Nomulu config YAML path; 2 - config XML path; 3 - project name;") } - // Config file path - configFileLocation := os.Args[1] + // Nomulus YAML config file path, used to extract OAuth client ID. + nomulusConfigFileLocation := os.Args[1] + // XML config file path + configFileLocation := os.Args[2] // Project name where to submit the tasks - projectName = os.Args[2] + projectName = os.Args[3] - log.Default().Println("Filepath " + configFileLocation) + log.Default().Println("YAML Filepath " + nomulusConfigFileLocation) + yamlFile, err := os.Open(nomulusConfigFileLocation) + if err != nil { + panic(err) + } + defer yamlFile.Close() + byteValue, _ := io.ReadAll(yamlFile) + var yamlEntries YamlEntries + if err := yaml.Unmarshal(byteValue, &yamlEntries); err != nil { + panic("Failed to parse YAML file entries: " + err.Error()) + } + clientId = yamlEntries.Auth.OauthClientId + + log.Default().Println("XML Filepath " + configFileLocation) xmlFile, err := os.Open(configFileLocation) if err != nil { panic(err) } defer xmlFile.Close() - byteValue, _ := io.ReadAll(xmlFile) + byteValue, _ = io.ReadAll(xmlFile) var xmlEntries XmlEntries if err := xml.Unmarshal(byteValue, &xmlEntries); err != nil { panic("Failed to parse xml file entries: " + err.Error()) diff --git a/release/builder/go.mod b/release/builder/go.mod new file mode 100644 index 000000000..7955d8092 --- /dev/null +++ b/release/builder/go.mod @@ -0,0 +1,5 @@ +module nomulus/release/builder + +go 1.21 + +require gopkg.in/yaml.v3 v3.0.1 diff --git a/release/builder/go.sum b/release/builder/go.sum new file mode 100644 index 000000000..a62c313c5 --- /dev/null +++ b/release/builder/go.sum @@ -0,0 +1,4 @@ +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/release/cloudbuild-deploy.yaml b/release/cloudbuild-deploy.yaml index 165aecf15..fdc290104 100644 --- a/release/cloudbuild-deploy.yaml +++ b/release/cloudbuild-deploy.yaml @@ -43,8 +43,9 @@ steps: fi gsutil cp gs://$PROJECT_ID-deploy/${TAG_NAME}/${_ENV}.tar . tar -xvf ${_ENV}.tar - deployCloudSchedulerAndQueue default/WEB-INF/cloud-scheduler-tasks.xml $project_id - deployCloudSchedulerAndQueue default/WEB-INF/cloud-tasks-queue.xml $project_id + unzip default/WEB-INF/lib/core.jar + deployCloudSchedulerAndQueue google/registry/config/files/nomulus-config-${_ENV}.yaml default/WEB-INF/cloud-scheduler-tasks.xml $project_id + deployCloudSchedulerAndQueue google/registry/config/files/nomulus-config-${_ENV}.yaml default/WEB-INF/cloud-tasks-queue.xml $project_id # Deploy the GAE config files. # First authorize the gcloud tool to use the credential json file, then # download and unzip the tarball that contains the relevant config files diff --git a/util/src/main/java/google/registry/util/GoogleCredentialsBundle.java b/util/src/main/java/google/registry/util/GoogleCredentialsBundle.java index b0d374100..014d9cead 100644 --- a/util/src/main/java/google/registry/util/GoogleCredentialsBundle.java +++ b/util/src/main/java/google/registry/util/GoogleCredentialsBundle.java @@ -20,6 +20,7 @@ import com.google.api.client.googleapis.util.Utils; import com.google.api.client.http.HttpRequestInitializer; import com.google.api.client.http.HttpTransport; import com.google.api.client.json.JsonFactory; +import com.google.auth.ServiceAccountSigner; import com.google.auth.http.HttpCredentialsAdapter; import com.google.auth.oauth2.GoogleCredentials; import java.io.Serializable; @@ -31,12 +32,13 @@ import java.io.Serializable; */ public class GoogleCredentialsBundle implements Serializable { + private static final long serialVersionUID = -7184513645423688942L; private static final HttpTransport HTTP_TRANSPORT = Utils.getDefaultTransport(); private static final JsonFactory JSON_FACTORY = Utils.getDefaultJsonFactory(); - private GoogleCredentials googleCredentials; + private final GoogleCredentials googleCredentials; - private GoogleCredentialsBundle(GoogleCredentials googleCredentials) { + protected GoogleCredentialsBundle(GoogleCredentials googleCredentials) { checkNotNull(googleCredentials); this.googleCredentials = googleCredentials; } @@ -46,6 +48,21 @@ public class GoogleCredentialsBundle implements Serializable { return new GoogleCredentialsBundle(credentials); } + /** + * Returns the service account email address of the underlying {@link} GoogleCredentials, if + * possible. + */ + public String serviceAccount() { + if (googleCredentials instanceof ServiceAccountSigner) { + return ((ServiceAccountSigner) googleCredentials).getAccount(); + } else { + throw new RuntimeException( + String.format( + "%s is a %s, not a service account.", + googleCredentials, googleCredentials.getClass().getSimpleName())); + } + } + /** Returns the same {@link GoogleCredentials} used to create this object. */ public GoogleCredentials getGoogleCredentials() { return googleCredentials; diff --git a/util/src/main/java/google/registry/util/OidcTokenUtils.java b/util/src/main/java/google/registry/util/OidcTokenUtils.java new file mode 100644 index 000000000..df0fe8cae --- /dev/null +++ b/util/src/main/java/google/registry/util/OidcTokenUtils.java @@ -0,0 +1,98 @@ +// Copyright 2023 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.util; + +import static com.google.api.client.googleapis.auth.oauth2.GoogleOAuthConstants.TOKEN_SERVER_URL; + +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpRequest; +import com.google.api.client.http.HttpRequestFactory; +import com.google.api.client.http.HttpResponse; +import com.google.api.client.http.UrlEncodedContent; +import com.google.api.client.util.GenericData; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.auth.oauth2.IdToken; +import com.google.auth.oauth2.IdTokenProvider; +import com.google.auth.oauth2.IdTokenProvider.Option; +import com.google.auth.oauth2.UserCredentials; +import com.google.common.collect.ImmutableList; +import com.google.common.flogger.FluentLogger; +import java.io.IOException; +import java.net.URI; +import java.security.GeneralSecurityException; + +public final class OidcTokenUtils { + + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + private OidcTokenUtils() {} + + public static String createOidcToken(GoogleCredentialsBundle credentialsBundle, String clientId) { + GoogleCredentials credentials = credentialsBundle.getGoogleCredentials(); + if (credentials instanceof UserCredentials) { + try { + return getIdTokenForUserCredential(credentialsBundle, clientId); + } catch (Exception e) { + logger.atSevere().withCause(e).log( + "Cannot generate OIDC token for credential %s", credentials); + throw new RuntimeException("Cannot create OIDC token", e); + } + } else { + IdTokenProvider idTokenProvider = (IdTokenProvider) credentials; + // Note: we use Option.FORMAT_FULL to make sure the JWT we receive contains the email + // address (as is required by IAP) + try { + IdToken idToken = + idTokenProvider.idTokenWithAudience(clientId, ImmutableList.of(Option.FORMAT_FULL)); + return idToken.getTokenValue(); + } catch (IOException e) { + logger.atSevere().withCause(e).log( + "Cannot generate OIDC token for credential %s", credentials); + throw new RuntimeException("Cannot create OIDC token", e); + } + } + } + + /** + * Uses the saved desktop-app refresh token to acquire a token with the given audience. + * + *

This is lifted mostly from the Google Auth Library's {@link UserCredentials} + * "doRefreshAccessToken" method (which is private and thus inaccessible) while adding in the + * audience of the IAP client ID. The "idTokenWithAudience" method of that class does not support + * setting custom audience, paradoxically. + * + * @see + * Authenticating from a desktop app + */ + private static String getIdTokenForUserCredential( + GoogleCredentialsBundle credentialsBundle, String audience) + throws GeneralSecurityException, IOException { + UserCredentials credentials = (UserCredentials) credentialsBundle.getGoogleCredentials(); + GenericData tokenRequest = new GenericData(); + tokenRequest.set("client_id", credentials.getClientId()); + tokenRequest.set("client_secret", credentials.getClientSecret()); + tokenRequest.set("refresh_token", credentials.getRefreshToken()); + tokenRequest.set("audience", audience); + tokenRequest.set("grant_type", "refresh_token"); + UrlEncodedContent content = new UrlEncodedContent(tokenRequest); + + HttpRequestFactory requestFactory = credentialsBundle.getHttpTransport().createRequestFactory(); + HttpRequest request = + requestFactory.buildPostRequest(new GenericUrl(URI.create(TOKEN_SERVER_URL)), content); + request.setParser(credentialsBundle.getJsonFactory().createJsonObjectParser()); + HttpResponse response = request.execute(); + return response.parseAs(GenericData.class).get("id_token").toString(); + } +}