mirror of
https://github.com/google/nomulus
synced 2026-05-19 22:31:47 +00:00
Compare commits
7 Commits
nomulus-20
...
nomulus-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04c6652793 | ||
|
|
5658fbe8bd | ||
|
|
a4540a847a | ||
|
|
fdfbb9572d | ||
|
|
cf1a148208 | ||
|
|
6b54b69163 | ||
|
|
a839ec434e |
@@ -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}"
|
||||
}
|
||||
|
||||
@@ -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<String> defaultServiceAccount,
|
||||
@Config("iapClientId") Optional<String> 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;
|
||||
}
|
||||
|
||||
@@ -94,10 +92,7 @@ public class CloudTasksUtils implements Serializable {
|
||||
return retrier.callWithRetry(
|
||||
() -> {
|
||||
logger.atInfo().log(
|
||||
"Enqueuing queue='%s' endpoint='%s' service='%s'",
|
||||
queue,
|
||||
task.getAppEngineHttpRequest().getRelativeUri(),
|
||||
task.getAppEngineHttpRequest().getAppEngineRouting().getService());
|
||||
"Enqueuing queue='%s' endpoint='%s'", queue, task.getHttpRequest().getUrl());
|
||||
return client.enqueue(projectId, locationId, queue, task);
|
||||
},
|
||||
ApiException.class);
|
||||
@@ -124,7 +119,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<String, String> params,
|
||||
@@ -152,43 +147,17 @@ public class CloudTasksUtils implements Serializable {
|
||||
return path;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a {@link Task} that does not use AppEngine for submission.
|
||||
*
|
||||
* <p>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<String, String> 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.
|
||||
*
|
||||
* <p>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 service the App Engine service to route the request to.
|
||||
* @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 <a
|
||||
@@ -204,21 +173,18 @@ public class CloudTasksUtils implements Serializable {
|
||||
method.equals(HttpMethod.GET) || method.equals(HttpMethod.POST),
|
||||
"HTTP method %s is used. Only GET and POST are allowed.",
|
||||
method);
|
||||
// If the default service account is configured, send a standard non-AppEngine HTTP request
|
||||
if (defaultServiceAccount != null) {
|
||||
return createNonAppEngineTask(path, method, service, params);
|
||||
} else {
|
||||
AppEngineHttpRequest.Builder requestBuilder =
|
||||
AppEngineHttpRequest.newBuilder()
|
||||
.setHttpMethod(method)
|
||||
.setAppEngineRouting(
|
||||
AppEngineRouting.newBuilder().setService(service.toString()).build());
|
||||
path =
|
||||
processRequestParameters(
|
||||
path, method, params, requestBuilder::putHeaders, requestBuilder::setBody);
|
||||
requestBuilder.setRelativeUri(path);
|
||||
return Task.newBuilder().setAppEngineHttpRequest(requestBuilder.build()).build();
|
||||
}
|
||||
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder().setHttpMethod(method);
|
||||
path =
|
||||
processRequestParameters(
|
||||
path, method, params, requestBuilder::putHeaders, requestBuilder::setBody);
|
||||
OidcToken.Builder oidcTokenBuilder =
|
||||
OidcToken.newBuilder()
|
||||
.setServiceAccountEmail(credential.serviceAccount())
|
||||
.setAudience(oauthClientId);
|
||||
requestBuilder.setOidcToken(oidcTokenBuilder.build());
|
||||
String totalPath = String.format("%s%s", getServer(service), path);
|
||||
requestBuilder.setUrl(totalPath);
|
||||
return Task.newBuilder().setHttpRequest(requestBuilder.build()).build();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -226,11 +192,8 @@ public class CloudTasksUtils implements Serializable {
|
||||
*
|
||||
* @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 service the App Engine service to route the request to.
|
||||
* @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.
|
||||
* @param jitterSeconds the number of seconds that a task is randomly delayed up to.
|
||||
* @return the enqueued task.
|
||||
@@ -260,11 +223,8 @@ public class CloudTasksUtils implements Serializable {
|
||||
*
|
||||
* @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 service the App Engine service to route the request to.
|
||||
* @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.
|
||||
* @param delay the amount of time that a task needs to delayed for.
|
||||
* @return the enqueued task.
|
||||
@@ -330,6 +290,9 @@ public class CloudTasksUtils implements Serializable {
|
||||
}
|
||||
|
||||
public abstract static class SerializableCloudTasksClient implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 7872861868968535498L;
|
||||
|
||||
public abstract Task enqueue(String projectId, String locationId, String queueName, Task task);
|
||||
}
|
||||
|
||||
|
||||
@@ -102,6 +102,8 @@ public final class RegistryConfig {
|
||||
@Module
|
||||
public static final class ConfigModule {
|
||||
|
||||
private ConfigModule() {}
|
||||
|
||||
@Provides
|
||||
@Config("projectId")
|
||||
public static String provideProjectId(RegistryConfigSettings config) {
|
||||
@@ -120,17 +122,6 @@ public final class RegistryConfig {
|
||||
return config.gcpProject.locationId;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Config("serviceAccountEmails")
|
||||
public static ImmutableList<String> provideServiceAccountEmails(RegistryConfigSettings config) {
|
||||
return ImmutableList.copyOf(config.gcpProject.serviceAccountEmails);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Config("defaultServiceAccount")
|
||||
public static Optional<String> 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<String> 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:
|
||||
*
|
||||
* <ul>
|
||||
* <li>https://www.googleapis.com/auth/appengine.apis
|
||||
* <li>https://www.googleapis.com/auth/cloud-platform
|
||||
* <li>{@code https://www.googleapis.com/auth/appengine.apis}
|
||||
* <li>{@code https://www.googleapis.com/auth/cloud-platform}
|
||||
* </ul>
|
||||
*/
|
||||
@Provides
|
||||
@Config("requiredOauthScopes")
|
||||
public static ImmutableSet<String> 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<String> provideAllowedServiceAccountEmails(
|
||||
RegistryConfigSettings config) {
|
||||
return ImmutableSet.copyOf(config.auth.allowedServiceAccountEmails);
|
||||
}
|
||||
|
||||
/** Provides the allowed OAuth client IDs (could be multibinding). */
|
||||
@Provides
|
||||
@Config("allowedOauthClientIds")
|
||||
public static ImmutableSet<String> provideAllowedOauthClientIds(RegistryConfigSettings config) {
|
||||
return ImmutableSet.copyOf(config.oAuth.allowedOauthClientIds);
|
||||
return ImmutableSet.copyOf(config.auth.allowedOauthClientIds);
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Config("iapClientId")
|
||||
public static Optional<String> 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.
|
||||
*
|
||||
* <p>Hibernate docs, i.e.
|
||||
* https://docs.jboss.org/hibernate/orm/5.6/userguide/html_single/Hibernate_User_Guide.html,
|
||||
* recommend between 10 and 50.
|
||||
* <p><a
|
||||
* href="https://docs.jboss.org/hibernate/orm/5.6/userguide/html_single/Hibernate_User_Guide.html">Hibernate
|
||||
* User Guide</a> recommends between 10 and 50.
|
||||
*/
|
||||
public static int getHibernateJdbcBatchSize() {
|
||||
return CONFIG_SETTINGS.get().hibernate.jdbcBatchSize;
|
||||
@@ -1568,6 +1576,11 @@ public final class RegistryConfig {
|
||||
return Duration.standardDays(CONFIG_SETTINGS.get().registryPolicy.contactAutomaticTransferDays);
|
||||
}
|
||||
|
||||
/** A discount for all sunrise domain creates, between 0.0 (no discount) and 1.0 (free). */
|
||||
public static double getSunriseDomainCreateDiscount() {
|
||||
return CONFIG_SETTINGS.get().registryPolicy.sunriseDomainCreateDiscount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Memoizes loading of the {@link RegistryConfigSettings} POJO.
|
||||
*
|
||||
@@ -1578,11 +1591,7 @@ public final class RegistryConfig {
|
||||
public static final Supplier<RegistryConfigSettings> 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 {
|
||||
|
||||
@@ -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<String> 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<String> availableOauthScopes;
|
||||
public List<String> requiredOauthScopes;
|
||||
public List<String> allowedOauthClientIds;
|
||||
public String iapClientId;
|
||||
public List<String> allowedServiceAccountEmails;
|
||||
public String oauthClientId;
|
||||
}
|
||||
|
||||
/** Configuration options for accessing Google APIs. */
|
||||
@@ -108,6 +107,7 @@ public class RegistryConfigSettings {
|
||||
public String registryName;
|
||||
public List<String> spec11WebResources;
|
||||
public boolean requireSslCertificates;
|
||||
public double sunriseDomainCreateDiscount;
|
||||
}
|
||||
|
||||
/** Configuration for Hibernate. */
|
||||
|
||||
@@ -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.
|
||||
@@ -188,6 +180,11 @@ registryPolicy:
|
||||
# should generally be true for production environments, for added security.
|
||||
requireSslCertificates: true
|
||||
|
||||
# A fractional discount, if any, to be provided to all sunrise domain creates.
|
||||
# 0 means no discount will be applied, and 1 means that all sunrise creates
|
||||
# will be free.
|
||||
sunriseDomainCreateDiscount: 0.15
|
||||
|
||||
hibernate:
|
||||
# Make 'SERIALIZABLE' the default isolation level to ensure correctness.
|
||||
#
|
||||
@@ -295,24 +292,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
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -337,7 +337,8 @@ public final class DomainCreateFlow implements TransactionalFlow {
|
||||
Optional<FeeCreateCommandExtension> feeCreate =
|
||||
eppInput.getSingleExtension(FeeCreateCommandExtension.class);
|
||||
FeesAndCredits feesAndCredits =
|
||||
pricingLogic.getCreatePrice(tld, targetId, now, years, isAnchorTenant, allocationToken);
|
||||
pricingLogic.getCreatePrice(
|
||||
tld, targetId, now, years, isAnchorTenant, isSunriseCreate, allocationToken);
|
||||
validateFeeChallenge(feeCreate, feesAndCredits, defaultTokenUsed);
|
||||
Optional<SecDnsCreateExtension> secDnsCreate =
|
||||
validateSecDnsExtension(eppInput.getSingleExtension(SecDnsCreateExtension.class));
|
||||
|
||||
@@ -690,6 +690,7 @@ public class DomainFlowUtils {
|
||||
now,
|
||||
years,
|
||||
isAnchorTenant(domainName, allocationToken, Optional.empty()),
|
||||
isSunrise,
|
||||
allocationToken)
|
||||
.getFees();
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import static google.registry.util.DomainNameUtils.getTldFromDomainName;
|
||||
import static google.registry.util.PreconditionsUtils.checkArgumentPresent;
|
||||
|
||||
import com.google.common.net.InternetDomainName;
|
||||
import google.registry.config.RegistryConfig;
|
||||
import google.registry.flows.EppException;
|
||||
import google.registry.flows.EppException.CommandUseErrorException;
|
||||
import google.registry.flows.custom.DomainPricingCustomLogic;
|
||||
@@ -72,26 +73,33 @@ public final class DomainPricingLogic {
|
||||
DateTime dateTime,
|
||||
int years,
|
||||
boolean isAnchorTenant,
|
||||
boolean isSunriseCreate,
|
||||
Optional<AllocationToken> allocationToken)
|
||||
throws EppException {
|
||||
CurrencyUnit currency = tld.getCurrency();
|
||||
|
||||
BaseFee createFeeOrCredit;
|
||||
BaseFee createFee;
|
||||
// Domain create cost is always zero for anchor tenants
|
||||
if (isAnchorTenant) {
|
||||
createFeeOrCredit = Fee.create(zeroInCurrency(currency), FeeType.CREATE, false);
|
||||
createFee = Fee.create(zeroInCurrency(currency), FeeType.CREATE, false);
|
||||
} else {
|
||||
DomainPrices domainPrices = getPricesForDomainName(domainName, dateTime);
|
||||
Money domainCreateCost =
|
||||
getDomainCreateCostWithDiscount(domainPrices, years, allocationToken);
|
||||
createFeeOrCredit =
|
||||
// Apply a sunrise discount if configured and applicable
|
||||
if (isSunriseCreate) {
|
||||
domainCreateCost =
|
||||
domainCreateCost.multipliedBy(
|
||||
1.0d - RegistryConfig.getSunriseDomainCreateDiscount(), RoundingMode.HALF_EVEN);
|
||||
}
|
||||
createFee =
|
||||
Fee.create(domainCreateCost.getAmount(), FeeType.CREATE, domainPrices.isPremium());
|
||||
}
|
||||
|
||||
// Create fees for the cost and the EAP fee, if any.
|
||||
Fee eapFee = tld.getEapFeeFor(dateTime);
|
||||
FeesAndCredits.Builder feesBuilder =
|
||||
new FeesAndCredits.Builder().setCurrency(currency).addFeeOrCredit(createFeeOrCredit);
|
||||
new FeesAndCredits.Builder().setCurrency(currency).addFeeOrCredit(createFee);
|
||||
// Don't charge anchor tenants EAP fees.
|
||||
if (!isAnchorTenant && !eapFee.hasZeroCost()) {
|
||||
feesBuilder.addFeeOrCredit(eapFee);
|
||||
|
||||
@@ -475,6 +475,9 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
/** An allowlist of hosts allowed to be used on domains on this TLD (ignored if empty). */
|
||||
@Nullable Set<String> allowedFullyQualifiedHostNames;
|
||||
|
||||
@Column(nullable = false)
|
||||
boolean breakglassMode = false;
|
||||
|
||||
/**
|
||||
* References to allocation tokens that can be used on the TLD if no other token is passed in on a
|
||||
* domain create.
|
||||
@@ -701,6 +704,10 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
return nullToEmptyImmutableCopy(idnTables);
|
||||
}
|
||||
|
||||
public boolean getBreakglassMode() {
|
||||
return breakglassMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Builder asBuilder() {
|
||||
return new Builder(clone(this));
|
||||
@@ -1004,6 +1011,11 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder setBreakglassMode(boolean breakglassMode) {
|
||||
getInstance().breakglassMode = breakglassMode;
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Tld build() {
|
||||
final Tld instance = getInstance();
|
||||
|
||||
@@ -367,7 +367,7 @@ final class RdapDataStructures {
|
||||
PENDING_RESTORE("pending restore"),
|
||||
REDEMPTION_PERIOD("redemption period"),
|
||||
RENEW_PERIOD("renew period"),
|
||||
SERVER_DELETE_PROHIBITED("server deleted prohibited"),
|
||||
SERVER_DELETE_PROHIBITED("server delete prohibited"),
|
||||
SERVER_RENEW_PROHIBITED("server renew prohibited"),
|
||||
SERVER_TRANSFER_PROHIBITED("server transfer prohibited"),
|
||||
SERVER_UPDATE_PROHIBITED("server update prohibited"),
|
||||
|
||||
@@ -54,12 +54,11 @@ public class RdapIcannStandardInformation {
|
||||
private static final Notice INACCURACY_COMPLAINT_FORM_NOTICE =
|
||||
Notice.builder()
|
||||
.setTitle("RDDS Inaccuracy Complaint Form")
|
||||
.setDescription(
|
||||
"URL of the ICANN RDDS Inaccuracy Complaint Form: https://www.icann.org/wicf")
|
||||
.setDescription("URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf")
|
||||
.addLink(
|
||||
Link.builder()
|
||||
.setRel("alternate")
|
||||
.setHref("https://www.icann.org/wicf")
|
||||
.setHref("https://icann.org/wicf")
|
||||
.setType("text/html")
|
||||
.build())
|
||||
.build();
|
||||
|
||||
@@ -1000,7 +1000,7 @@ public class RdapJsonFormatter {
|
||||
// Gustavo Lozano of ICANN, the one we should use is an embedded array of street address lines
|
||||
// if there is more than one line:
|
||||
//
|
||||
// RFC7095 provides two examples of structured addresses, and one of the examples shows a
|
||||
// RFC 7095 provides two examples of structured addresses, and one of the examples shows a
|
||||
// street JSON element that contains several data elements. The example showing (see below)
|
||||
// several data elements is the expected output when two or more <contact:street> elements
|
||||
// exists in the contact object.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -32,7 +32,7 @@ import javax.servlet.http.HttpServletRequest;
|
||||
/**
|
||||
* OAuth authentication mechanism, using the OAuthService interface.
|
||||
*
|
||||
* Only OAuth version 2 is supported.
|
||||
* <p>Only OAuth version 2 is supported.
|
||||
*/
|
||||
public class OAuthAuthenticationMechanism implements AuthenticationMechanism {
|
||||
|
||||
|
||||
@@ -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<String> serviceAccountEmails;
|
||||
private final ImmutableSet<String> serviceAccountEmails;
|
||||
|
||||
protected OidcTokenAuthenticationMechanism(
|
||||
ImmutableList<String> serviceAccountEmails,
|
||||
ImmutableSet<String> 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<String> serviceAccountEmails,
|
||||
@Config("allowedServiceAccountEmails") ImmutableSet<String> 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<String> serviceAccountEmails,
|
||||
@Config("allowedServiceAccountEmails") ImmutableSet<String> serviceAccountEmails,
|
||||
@RegularOidc TokenVerifier tokenVerifier,
|
||||
@RegularOidc TokenExtractor tokenExtractor) {
|
||||
super(serviceAccountEmails, tokenVerifier, tokenExtractor);
|
||||
|
||||
@@ -75,6 +75,11 @@ class CurlCommand implements CommandWithConnection {
|
||||
required = true)
|
||||
private Service service;
|
||||
|
||||
@Parameter(
|
||||
names = {"--canary"},
|
||||
description = "If set, use the canary end-point; otherwise use the regular end-point.")
|
||||
private Boolean canary = Boolean.FALSE;
|
||||
|
||||
@Override
|
||||
public void setConnection(ServiceConnection connection) {
|
||||
this.connection = connection;
|
||||
@@ -90,7 +95,7 @@ class CurlCommand implements CommandWithConnection {
|
||||
throw new IllegalArgumentException("You may not specify a body for a get method.");
|
||||
}
|
||||
|
||||
ServiceConnection connectionToService = connection.withService(service);
|
||||
ServiceConnection connectionToService = connection.withService(service, canary);
|
||||
String response =
|
||||
(method == Method.GET)
|
||||
? connectionToService.sendGetRequest(path, ImmutableMap.<String, String>of())
|
||||
|
||||
@@ -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
|
||||
*
|
||||
* <p>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<String> 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.
|
||||
*
|
||||
* <p>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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
package google.registry.tools;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkNotNull;
|
||||
import static com.google.common.base.Strings.isNullOrEmpty;
|
||||
import static com.google.common.base.Verify.verify;
|
||||
import static com.google.common.net.HttpHeaders.X_REQUESTED_WITH;
|
||||
import static com.google.common.net.MediaType.JSON_UTF_8;
|
||||
import static google.registry.security.JsonHttp.JSON_SAFETY_PREFIX;
|
||||
@@ -26,6 +28,7 @@ import com.google.api.client.http.HttpHeaders;
|
||||
import com.google.api.client.http.HttpRequest;
|
||||
import com.google.api.client.http.HttpRequestFactory;
|
||||
import com.google.api.client.http.HttpResponse;
|
||||
import com.google.common.annotations.VisibleForTesting;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.io.CharStreams;
|
||||
@@ -36,6 +39,7 @@ import google.registry.config.RegistryConfig;
|
||||
import google.registry.request.Action.Service;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.util.Map;
|
||||
import javax.annotation.Nullable;
|
||||
@@ -55,20 +59,23 @@ public class ServiceConnection {
|
||||
|
||||
@Inject HttpRequestFactory requestFactory;
|
||||
private final Service service;
|
||||
private final boolean useCanary;
|
||||
|
||||
@Inject
|
||||
ServiceConnection() {
|
||||
service = Service.TOOLS;
|
||||
useCanary = false;
|
||||
}
|
||||
|
||||
private ServiceConnection(Service service, HttpRequestFactory requestFactory) {
|
||||
private ServiceConnection(Service service, HttpRequestFactory requestFactory, boolean useCanary) {
|
||||
this.service = service;
|
||||
this.requestFactory = requestFactory;
|
||||
this.useCanary = useCanary;
|
||||
}
|
||||
|
||||
/** Returns a copy of this connection that talks to a different service. */
|
||||
public ServiceConnection withService(Service service) {
|
||||
return new ServiceConnection(service, requestFactory);
|
||||
/** Returns a copy of this connection that talks to a different service endpoint. */
|
||||
public ServiceConnection withService(Service service, boolean isCanary) {
|
||||
return new ServiceConnection(service, requestFactory, isCanary);
|
||||
}
|
||||
|
||||
/** Returns the contents of the title tag in the given HTML, or null if not found. */
|
||||
@@ -85,7 +92,7 @@ public class ServiceConnection {
|
||||
private String internalSend(
|
||||
String endpoint, Map<String, ?> params, MediaType contentType, @Nullable byte[] payload)
|
||||
throws IOException {
|
||||
GenericUrl url = new GenericUrl(String.format("%s%s", getServer(service), endpoint));
|
||||
GenericUrl url = new GenericUrl(String.format("%s%s", getServer(), endpoint));
|
||||
url.putAll(params);
|
||||
HttpRequest request =
|
||||
(payload != null)
|
||||
@@ -120,6 +127,20 @@ public class ServiceConnection {
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
URL getServer() {
|
||||
URL url = getServer(service);
|
||||
if (useCanary) {
|
||||
verify(!isNullOrEmpty(url.getHost()), "Null host in url");
|
||||
try {
|
||||
return new URL(url.getProtocol(), "nomulus-dot-" + url.getHost(), url.getFile());
|
||||
} catch (MalformedURLException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
public String sendPostRequest(
|
||||
String endpoint, Map<String, ?> params, MediaType contentType, byte[] payload)
|
||||
throws IOException {
|
||||
|
||||
@@ -15,20 +15,25 @@
|
||||
package google.registry.tools.server;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
import static com.google.common.collect.ImmutableList.toImmutableList;
|
||||
import static com.google.common.collect.Iterables.getLast;
|
||||
import static google.registry.dns.DnsUtils.requestDomainDnsRefresh;
|
||||
import static google.registry.model.tld.Tlds.assertTldsExist;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.request.RequestParameters.PARAM_TLDS;
|
||||
import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.flogger.FluentLogger;
|
||||
import google.registry.request.Action;
|
||||
import google.registry.request.Parameter;
|
||||
import google.registry.request.Response;
|
||||
import google.registry.request.auth.Auth;
|
||||
import google.registry.util.Clock;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import javax.inject.Inject;
|
||||
import org.apache.arrow.util.VisibleForTesting;
|
||||
import org.apache.http.HttpStatus;
|
||||
import org.joda.time.Duration;
|
||||
|
||||
@@ -43,10 +48,8 @@ import org.joda.time.Duration;
|
||||
* run internally, or by pretending to be internal by setting the X-AppEngine-QueueName header,
|
||||
* which only admin users can do.
|
||||
*
|
||||
* <p>You must pass in a number of {@code smearMinutes} as a URL parameter so that the DNS queue
|
||||
* doesn't get overloaded. A rough rule of thumb for Cloud DNS is 1 minute per every 1,000 domains.
|
||||
* This smears the updates out over the next N minutes. For small TLDs consisting of fewer than
|
||||
* 1,000 domains, passing in 1 is fine (which will execute all the updates immediately).
|
||||
* <p>You may pass in a {@code batchSize} for the batched read of domains from the database. This is
|
||||
* recommended to be somewhere between 200 and 500. The default value is 250.
|
||||
*/
|
||||
@Action(
|
||||
service = Action.Service.TOOLS,
|
||||
@@ -56,47 +59,78 @@ public class RefreshDnsForAllDomainsAction implements Runnable {
|
||||
|
||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||
|
||||
@Inject Response response;
|
||||
private static final int DEFAULT_BATCH_SIZE = 250;
|
||||
|
||||
private final Response response;
|
||||
private final ImmutableSet<String> tlds;
|
||||
|
||||
// Recommended value for batch size is between 200 and 500
|
||||
private final int batchSize;
|
||||
private final Random random;
|
||||
|
||||
@Inject
|
||||
@Parameter(PARAM_TLDS)
|
||||
ImmutableSet<String> tlds;
|
||||
|
||||
@Inject
|
||||
@Parameter("smearMinutes")
|
||||
int smearMinutes;
|
||||
|
||||
@Inject Clock clock;
|
||||
@Inject Random random;
|
||||
|
||||
@Inject
|
||||
RefreshDnsForAllDomainsAction() {}
|
||||
RefreshDnsForAllDomainsAction(
|
||||
Response response,
|
||||
@Parameter(PARAM_TLDS) ImmutableSet<String> tlds,
|
||||
@Parameter("batchSize") Optional<Integer> batchSize,
|
||||
Random random) {
|
||||
this.response = response;
|
||||
this.tlds = tlds;
|
||||
this.batchSize = batchSize.orElse(DEFAULT_BATCH_SIZE);
|
||||
this.random = random;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
assertTldsExist(tlds);
|
||||
checkArgument(smearMinutes > 0, "Must specify a positive number of smear minutes");
|
||||
tm().transact(
|
||||
() ->
|
||||
tm().query(
|
||||
"SELECT domainName FROM Domain "
|
||||
+ "WHERE tld IN (:tlds) "
|
||||
+ "AND deletionTime > :now",
|
||||
String.class)
|
||||
.setParameter("tlds", tlds)
|
||||
.setParameter("now", clock.nowUtc())
|
||||
.getResultStream()
|
||||
.forEach(
|
||||
domainName -> {
|
||||
try {
|
||||
// Smear the task execution time over the next N minutes.
|
||||
requestDomainDnsRefresh(
|
||||
domainName, Duration.standardMinutes(random.nextInt(smearMinutes)));
|
||||
} catch (Throwable t) {
|
||||
logger.atSevere().withCause(t).log(
|
||||
"Error while enqueuing DNS refresh for domain '%s'.", domainName);
|
||||
response.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
}));
|
||||
checkArgument(batchSize > 0, "Must specify a positive number for batch size");
|
||||
int smearMinutes = tm().transact(this::calculateSmearMinutes);
|
||||
ImmutableList<String> previousBatch = ImmutableList.of("");
|
||||
do {
|
||||
String lastInPreviousBatch = getLast(previousBatch);
|
||||
previousBatch = tm().transact(() -> refreshBatch(lastInPreviousBatch, smearMinutes));
|
||||
} while (previousBatch.size() == batchSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the number of smear minutes to enqueue refreshes so that the DNS queue does not get
|
||||
* overloaded.
|
||||
*/
|
||||
private int calculateSmearMinutes() {
|
||||
Long activeDomains =
|
||||
tm().query(
|
||||
"SELECT COUNT(*) FROM Domain WHERE tld IN (:tlds) AND deletionTime = :endOfTime",
|
||||
Long.class)
|
||||
.setParameter("tlds", tlds)
|
||||
.setParameter("endOfTime", END_OF_TIME)
|
||||
.getSingleResult();
|
||||
return Math.max(activeDomains.intValue() / 1000, 1);
|
||||
}
|
||||
|
||||
private ImmutableList<String> getBatch(String lastInPreviousBatch) {
|
||||
return tm().query(
|
||||
"SELECT domainName FROM Domain WHERE tld IN (:tlds) AND"
|
||||
+ " deletionTime = :endOfTime AND domainName >"
|
||||
+ " :lastInPreviousBatch ORDER BY domainName ASC",
|
||||
String.class)
|
||||
.setParameter("tlds", tlds)
|
||||
.setParameter("endOfTime", END_OF_TIME)
|
||||
.setParameter("lastInPreviousBatch", lastInPreviousBatch)
|
||||
.setMaxResults(batchSize)
|
||||
.getResultStream()
|
||||
.collect(toImmutableList());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
ImmutableList<String> refreshBatch(String lastInPreviousBatch, int smearMinutes) {
|
||||
ImmutableList<String> domainBatch = getBatch(lastInPreviousBatch);
|
||||
try {
|
||||
// Smear the task execution time over the next N minutes.
|
||||
requestDomainDnsRefresh(domainBatch, Duration.standardMinutes(random.nextInt(smearMinutes)));
|
||||
} catch (Throwable t) {
|
||||
logger.atSevere().withCause(t).log("Error while enqueuing DNS refresh batch");
|
||||
response.setStatus(HttpStatus.SC_OK);
|
||||
}
|
||||
return domainBatch;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ package google.registry.tools.server;
|
||||
|
||||
import static com.google.common.base.Strings.emptyToNull;
|
||||
import static google.registry.request.RequestParameters.extractIntParameter;
|
||||
import static google.registry.request.RequestParameters.extractOptionalIntParameter;
|
||||
import static google.registry.request.RequestParameters.extractOptionalParameter;
|
||||
import static google.registry.request.RequestParameters.extractRequiredParameter;
|
||||
|
||||
@@ -76,8 +77,8 @@ public class ToolsServerModule {
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Parameter("smearMinutes")
|
||||
static int provideSmearMinutes(HttpServletRequest req) {
|
||||
return extractIntParameter(req, "smearMinutes");
|
||||
@Parameter("batchSize")
|
||||
static Optional<Integer> provideBatchSize(HttpServletRequest req) {
|
||||
return extractOptionalIntParameter(req, "batchSize");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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<String, String> 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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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/"));
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -276,16 +276,24 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
|
||||
|
||||
boolean isAnchorTenant = expectedBillingFlags.contains(ANCHOR_TENANT);
|
||||
// Set up the creation cost.
|
||||
BigDecimal createCost =
|
||||
isDomainPremium(getUniqueIdFromCommand(), clock.nowUtc())
|
||||
? BigDecimal.valueOf(200)
|
||||
: BigDecimal.valueOf(26);
|
||||
if (isAnchorTenant) {
|
||||
createCost = BigDecimal.ZERO;
|
||||
}
|
||||
if (expectedBillingFlags.contains(SUNRISE)) {
|
||||
createCost =
|
||||
createCost.multiply(
|
||||
BigDecimal.valueOf(1 - RegistryConfig.getSunriseDomainCreateDiscount()));
|
||||
}
|
||||
FeesAndCredits feesAndCredits =
|
||||
new FeesAndCredits.Builder()
|
||||
.setCurrency(USD)
|
||||
.addFeeOrCredit(
|
||||
Fee.create(
|
||||
isAnchorTenant
|
||||
? BigDecimal.valueOf(0)
|
||||
: isDomainPremium(getUniqueIdFromCommand(), clock.nowUtc())
|
||||
? BigDecimal.valueOf(200)
|
||||
: BigDecimal.valueOf(26),
|
||||
createCost,
|
||||
FeeType.CREATE,
|
||||
isDomainPremium(getUniqueIdFromCommand(), clock.nowUtc())))
|
||||
.build();
|
||||
|
||||
@@ -304,7 +304,7 @@ class DomainDeleteFlowTest extends ResourceFlowTestCase<DomainDeleteFlow, Domain
|
||||
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")
|
||||
|
||||
@@ -19,6 +19,7 @@ import static google.registry.model.billing.BillingBase.Flag.AUTO_RENEW;
|
||||
import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.DEFAULT;
|
||||
import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.NONPREMIUM;
|
||||
import static google.registry.model.billing.BillingBase.RenewalPriceBehavior.SPECIFIED;
|
||||
import static google.registry.model.domain.fee.BaseFee.FeeType.CREATE;
|
||||
import static google.registry.model.domain.fee.BaseFee.FeeType.RENEW;
|
||||
import static google.registry.model.domain.token.AllocationToken.TokenType.SINGLE_USE;
|
||||
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_CREATE;
|
||||
@@ -28,7 +29,6 @@ import static google.registry.testing.DatabaseHelper.persistResource;
|
||||
import static google.registry.util.DateTimeUtils.END_OF_TIME;
|
||||
import static google.registry.util.DateTimeUtils.START_OF_TIME;
|
||||
import static org.joda.money.CurrencyUnit.USD;
|
||||
import static org.joda.time.DateTimeZone.UTC;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
@@ -49,6 +49,7 @@ import google.registry.model.domain.fee.Fee;
|
||||
import google.registry.model.domain.token.AllocationToken;
|
||||
import google.registry.model.eppinput.EppInput;
|
||||
import google.registry.model.tld.Tld;
|
||||
import google.registry.model.tld.Tld.TldState;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
|
||||
import google.registry.testing.DatabaseHelper;
|
||||
@@ -72,11 +73,11 @@ public class DomainPricingLogicTest {
|
||||
final JpaIntegrationTestExtension jpa =
|
||||
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
|
||||
|
||||
@Inject Clock clock = new FakeClock(DateTime.now(UTC));
|
||||
@Inject Clock clock = new FakeClock(DateTime.parse("2023-05-13T00:00:00.000Z"));
|
||||
@Mock EppInput eppInput;
|
||||
SessionMetadata sessionMetadata;
|
||||
@Mock FlowMetadata flowMetadata;
|
||||
Tld registry;
|
||||
Tld tld;
|
||||
Domain domain;
|
||||
|
||||
@BeforeEach
|
||||
@@ -86,7 +87,7 @@ public class DomainPricingLogicTest {
|
||||
domainPricingLogic =
|
||||
new DomainPricingLogic(
|
||||
new DomainPricingCustomLogic(eppInput, sessionMetadata, flowMetadata));
|
||||
registry =
|
||||
tld =
|
||||
persistResource(
|
||||
Tld.get("example")
|
||||
.asBuilder()
|
||||
@@ -133,12 +134,32 @@ public class DomainPricingLogicTest {
|
||||
return billingRecurrence;
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetDomainCreatePrice_sunrise_appliesDiscount() throws EppException {
|
||||
ImmutableSortedMap<DateTime, TldState> transitions =
|
||||
ImmutableSortedMap.<DateTime, TldState>naturalOrder()
|
||||
.put(START_OF_TIME, TldState.PREDELEGATION)
|
||||
.put(clock.nowUtc().minusHours(1), TldState.START_DATE_SUNRISE)
|
||||
.put(clock.nowUtc().plusHours(1), TldState.GENERAL_AVAILABILITY)
|
||||
.build();
|
||||
Tld sunriseTld = createTld("sunrise", transitions);
|
||||
assertThat(
|
||||
domainPricingLogic.getCreatePrice(
|
||||
sunriseTld, "domain.sunrise", clock.nowUtc(), 2, false, true, Optional.empty()))
|
||||
.isEqualTo(
|
||||
new FeesAndCredits.Builder()
|
||||
.setCurrency(USD)
|
||||
// 13 * 2 * 0.85 == 22.1
|
||||
.addFeeOrCredit(Fee.create(Money.of(USD, 22.1).getAmount(), CREATE, false))
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetDomainRenewPrice_oneYear_standardDomain_noBilling_isStandardPrice()
|
||||
throws EppException {
|
||||
assertThat(
|
||||
domainPricingLogic.getRenewPrice(
|
||||
registry, "standard.example", clock.nowUtc(), 1, null, Optional.empty()))
|
||||
tld, "standard.example", clock.nowUtc(), 1, null, Optional.empty()))
|
||||
.isEqualTo(
|
||||
new FeesAndCredits.Builder()
|
||||
.setCurrency(USD)
|
||||
@@ -151,7 +172,7 @@ public class DomainPricingLogicTest {
|
||||
throws EppException {
|
||||
assertThat(
|
||||
domainPricingLogic.getRenewPrice(
|
||||
registry, "standard.example", clock.nowUtc(), 5, null, Optional.empty()))
|
||||
tld, "standard.example", clock.nowUtc(), 5, null, Optional.empty()))
|
||||
.isEqualTo(
|
||||
new FeesAndCredits.Builder()
|
||||
.setCurrency(USD)
|
||||
@@ -164,7 +185,7 @@ public class DomainPricingLogicTest {
|
||||
throws EppException {
|
||||
assertThat(
|
||||
domainPricingLogic.getRenewPrice(
|
||||
registry, "premium.example", clock.nowUtc(), 1, null, Optional.empty()))
|
||||
tld, "premium.example", clock.nowUtc(), 1, null, Optional.empty()))
|
||||
.isEqualTo(
|
||||
new FeesAndCredits.Builder()
|
||||
.setCurrency(USD)
|
||||
@@ -177,7 +198,7 @@ public class DomainPricingLogicTest {
|
||||
throws EppException {
|
||||
assertThat(
|
||||
domainPricingLogic.getRenewPrice(
|
||||
registry, "premium.example", clock.nowUtc(), 5, null, Optional.empty()))
|
||||
tld, "premium.example", clock.nowUtc(), 5, null, Optional.empty()))
|
||||
.isEqualTo(
|
||||
new FeesAndCredits.Builder()
|
||||
.setCurrency(USD)
|
||||
@@ -189,7 +210,7 @@ public class DomainPricingLogicTest {
|
||||
void testGetDomainRenewPrice_oneYear_premiumDomain_default_isPremiumPrice() throws EppException {
|
||||
assertThat(
|
||||
domainPricingLogic.getRenewPrice(
|
||||
registry,
|
||||
tld,
|
||||
"premium.example",
|
||||
clock.nowUtc(),
|
||||
1,
|
||||
@@ -215,7 +236,7 @@ public class DomainPricingLogicTest {
|
||||
.build());
|
||||
assertThat(
|
||||
domainPricingLogic.getRenewPrice(
|
||||
registry,
|
||||
tld,
|
||||
"premium.example",
|
||||
clock.nowUtc(),
|
||||
1,
|
||||
@@ -244,7 +265,7 @@ public class DomainPricingLogicTest {
|
||||
AllocationTokenInvalidForPremiumNameException.class,
|
||||
() ->
|
||||
domainPricingLogic.getRenewPrice(
|
||||
registry,
|
||||
tld,
|
||||
"premium.example",
|
||||
clock.nowUtc(),
|
||||
1,
|
||||
@@ -256,7 +277,7 @@ public class DomainPricingLogicTest {
|
||||
void testGetDomainRenewPrice_multiYear_premiumDomain_default_isPremiumCost() throws EppException {
|
||||
assertThat(
|
||||
domainPricingLogic.getRenewPrice(
|
||||
registry,
|
||||
tld,
|
||||
"premium.example",
|
||||
clock.nowUtc(),
|
||||
5,
|
||||
@@ -283,7 +304,7 @@ public class DomainPricingLogicTest {
|
||||
.build());
|
||||
assertThat(
|
||||
domainPricingLogic.getRenewPrice(
|
||||
registry,
|
||||
tld,
|
||||
"premium.example",
|
||||
clock.nowUtc(),
|
||||
5,
|
||||
@@ -313,7 +334,7 @@ public class DomainPricingLogicTest {
|
||||
AllocationTokenInvalidForPremiumNameException.class,
|
||||
() ->
|
||||
domainPricingLogic.getRenewPrice(
|
||||
registry,
|
||||
tld,
|
||||
"premium.example",
|
||||
clock.nowUtc(),
|
||||
5,
|
||||
@@ -326,7 +347,7 @@ public class DomainPricingLogicTest {
|
||||
throws EppException {
|
||||
assertThat(
|
||||
domainPricingLogic.getRenewPrice(
|
||||
registry,
|
||||
tld,
|
||||
"standard.example",
|
||||
clock.nowUtc(),
|
||||
1,
|
||||
@@ -352,7 +373,7 @@ public class DomainPricingLogicTest {
|
||||
.build());
|
||||
assertThat(
|
||||
domainPricingLogic.getRenewPrice(
|
||||
registry,
|
||||
tld,
|
||||
"standard.example",
|
||||
clock.nowUtc(),
|
||||
1,
|
||||
@@ -370,7 +391,7 @@ public class DomainPricingLogicTest {
|
||||
throws EppException {
|
||||
assertThat(
|
||||
domainPricingLogic.getRenewPrice(
|
||||
registry,
|
||||
tld,
|
||||
"standard.example",
|
||||
clock.nowUtc(),
|
||||
5,
|
||||
@@ -397,7 +418,7 @@ public class DomainPricingLogicTest {
|
||||
.build());
|
||||
assertThat(
|
||||
domainPricingLogic.getRenewPrice(
|
||||
registry,
|
||||
tld,
|
||||
"standard.example",
|
||||
clock.nowUtc(),
|
||||
5,
|
||||
@@ -415,7 +436,7 @@ public class DomainPricingLogicTest {
|
||||
throws EppException {
|
||||
assertThat(
|
||||
domainPricingLogic.getRenewPrice(
|
||||
registry,
|
||||
tld,
|
||||
"premium.example",
|
||||
clock.nowUtc(),
|
||||
1,
|
||||
@@ -442,7 +463,7 @@ public class DomainPricingLogicTest {
|
||||
.build());
|
||||
assertThat(
|
||||
domainPricingLogic.getRenewPrice(
|
||||
registry,
|
||||
tld,
|
||||
"premium.example",
|
||||
clock.nowUtc(),
|
||||
1,
|
||||
@@ -460,7 +481,7 @@ public class DomainPricingLogicTest {
|
||||
throws EppException {
|
||||
assertThat(
|
||||
domainPricingLogic.getRenewPrice(
|
||||
registry,
|
||||
tld,
|
||||
"premium.example",
|
||||
clock.nowUtc(),
|
||||
5,
|
||||
@@ -488,7 +509,7 @@ public class DomainPricingLogicTest {
|
||||
.build());
|
||||
assertThat(
|
||||
domainPricingLogic.getRenewPrice(
|
||||
registry,
|
||||
tld,
|
||||
"premium.example",
|
||||
clock.nowUtc(),
|
||||
5,
|
||||
@@ -506,7 +527,7 @@ public class DomainPricingLogicTest {
|
||||
throws EppException {
|
||||
assertThat(
|
||||
domainPricingLogic.getRenewPrice(
|
||||
registry,
|
||||
tld,
|
||||
"standard.example",
|
||||
clock.nowUtc(),
|
||||
1,
|
||||
@@ -524,7 +545,7 @@ public class DomainPricingLogicTest {
|
||||
throws EppException {
|
||||
assertThat(
|
||||
domainPricingLogic.getRenewPrice(
|
||||
registry,
|
||||
tld,
|
||||
"standard.example",
|
||||
clock.nowUtc(),
|
||||
5,
|
||||
@@ -542,7 +563,7 @@ public class DomainPricingLogicTest {
|
||||
throws EppException {
|
||||
assertThat(
|
||||
domainPricingLogic.getRenewPrice(
|
||||
registry,
|
||||
tld,
|
||||
"standard.example",
|
||||
clock.nowUtc(),
|
||||
1,
|
||||
@@ -570,7 +591,7 @@ public class DomainPricingLogicTest {
|
||||
.build());
|
||||
assertThat(
|
||||
domainPricingLogic.getRenewPrice(
|
||||
registry,
|
||||
tld,
|
||||
"standard.example",
|
||||
clock.nowUtc(),
|
||||
1,
|
||||
@@ -601,7 +622,7 @@ public class DomainPricingLogicTest {
|
||||
.build());
|
||||
assertThat(
|
||||
domainPricingLogic.getRenewPrice(
|
||||
registry,
|
||||
tld,
|
||||
"standard.example",
|
||||
clock.nowUtc(),
|
||||
1,
|
||||
@@ -626,7 +647,7 @@ public class DomainPricingLogicTest {
|
||||
throws EppException {
|
||||
assertThat(
|
||||
domainPricingLogic.getRenewPrice(
|
||||
registry,
|
||||
tld,
|
||||
"standard.example",
|
||||
clock.nowUtc(),
|
||||
5,
|
||||
@@ -654,7 +675,7 @@ public class DomainPricingLogicTest {
|
||||
.build());
|
||||
assertThat(
|
||||
domainPricingLogic.getRenewPrice(
|
||||
registry,
|
||||
tld,
|
||||
"standard.example",
|
||||
clock.nowUtc(),
|
||||
5,
|
||||
@@ -673,7 +694,7 @@ public class DomainPricingLogicTest {
|
||||
throws EppException {
|
||||
assertThat(
|
||||
domainPricingLogic.getRenewPrice(
|
||||
registry,
|
||||
tld,
|
||||
"premium.example",
|
||||
clock.nowUtc(),
|
||||
1,
|
||||
@@ -692,7 +713,7 @@ public class DomainPricingLogicTest {
|
||||
throws EppException {
|
||||
assertThat(
|
||||
domainPricingLogic.getRenewPrice(
|
||||
registry,
|
||||
tld,
|
||||
"premium.example",
|
||||
clock.nowUtc(),
|
||||
5,
|
||||
@@ -713,15 +734,14 @@ public class DomainPricingLogicTest {
|
||||
IllegalArgumentException.class,
|
||||
() ->
|
||||
domainPricingLogic.getRenewPrice(
|
||||
registry, "standard.example", clock.nowUtc(), -1, null, Optional.empty()));
|
||||
tld, "standard.example", clock.nowUtc(), -1, null, Optional.empty()));
|
||||
assertThat(thrown).hasMessageThat().isEqualTo("Number of years must be positive");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testGetDomainTransferPrice_standardDomain_default_noBilling_defaultRenewalPrice()
|
||||
throws EppException {
|
||||
assertThat(
|
||||
domainPricingLogic.getTransferPrice(registry, "standard.example", clock.nowUtc(), null))
|
||||
assertThat(domainPricingLogic.getTransferPrice(tld, "standard.example", clock.nowUtc(), null))
|
||||
.isEqualTo(
|
||||
new FeesAndCredits.Builder()
|
||||
.setCurrency(USD)
|
||||
@@ -732,8 +752,7 @@ public class DomainPricingLogicTest {
|
||||
@Test
|
||||
void testGetDomainTransferPrice_premiumDomain_default_noBilling_premiumRenewalPrice()
|
||||
throws EppException {
|
||||
assertThat(
|
||||
domainPricingLogic.getTransferPrice(registry, "premium.example", clock.nowUtc(), null))
|
||||
assertThat(domainPricingLogic.getTransferPrice(tld, "premium.example", clock.nowUtc(), null))
|
||||
.isEqualTo(
|
||||
new FeesAndCredits.Builder()
|
||||
.setCurrency(USD)
|
||||
@@ -745,7 +764,7 @@ public class DomainPricingLogicTest {
|
||||
void testGetDomainTransferPrice_standardDomain_default_defaultRenewalPrice() throws EppException {
|
||||
assertThat(
|
||||
domainPricingLogic.getTransferPrice(
|
||||
registry,
|
||||
tld,
|
||||
"standard.example",
|
||||
clock.nowUtc(),
|
||||
persistDomainAndSetRecurrence("standard.example", DEFAULT, Optional.empty())))
|
||||
@@ -760,7 +779,7 @@ public class DomainPricingLogicTest {
|
||||
void testGetDomainTransferPrice_premiumDomain_default_premiumRenewalPrice() throws EppException {
|
||||
assertThat(
|
||||
domainPricingLogic.getTransferPrice(
|
||||
registry,
|
||||
tld,
|
||||
"premium.example",
|
||||
clock.nowUtc(),
|
||||
persistDomainAndSetRecurrence("premium.example", DEFAULT, Optional.empty())))
|
||||
@@ -776,7 +795,7 @@ public class DomainPricingLogicTest {
|
||||
throws EppException {
|
||||
assertThat(
|
||||
domainPricingLogic.getTransferPrice(
|
||||
registry,
|
||||
tld,
|
||||
"standard.example",
|
||||
clock.nowUtc(),
|
||||
persistDomainAndSetRecurrence("standard.example", NONPREMIUM, Optional.empty())))
|
||||
@@ -792,7 +811,7 @@ public class DomainPricingLogicTest {
|
||||
throws EppException {
|
||||
assertThat(
|
||||
domainPricingLogic.getTransferPrice(
|
||||
registry,
|
||||
tld,
|
||||
"premium.example",
|
||||
clock.nowUtc(),
|
||||
persistDomainAndSetRecurrence("premium.example", NONPREMIUM, Optional.empty())))
|
||||
@@ -808,7 +827,7 @@ public class DomainPricingLogicTest {
|
||||
throws EppException {
|
||||
assertThat(
|
||||
domainPricingLogic.getTransferPrice(
|
||||
registry,
|
||||
tld,
|
||||
"standard.example",
|
||||
clock.nowUtc(),
|
||||
persistDomainAndSetRecurrence(
|
||||
@@ -825,7 +844,7 @@ public class DomainPricingLogicTest {
|
||||
throws EppException {
|
||||
assertThat(
|
||||
domainPricingLogic.getTransferPrice(
|
||||
registry,
|
||||
tld,
|
||||
"premium.example",
|
||||
clock.nowUtc(),
|
||||
persistDomainAndSetRecurrence(
|
||||
|
||||
@@ -520,7 +520,7 @@ class DomainTransferRequestFlowTest
|
||||
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")
|
||||
|
||||
@@ -207,7 +207,7 @@ class HostUpdateFlowTest extends ResourceFlowTestCase<HostUpdateFlow, Host> {
|
||||
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()));
|
||||
|
||||
@@ -139,13 +139,12 @@ class RdapTestHelper {
|
||||
"RDDS Inaccuracy Complaint Form",
|
||||
"description",
|
||||
ImmutableList.of(
|
||||
"URL of the ICANN RDDS Inaccuracy Complaint Form:"
|
||||
+ " https://www.icann.org/wicf"),
|
||||
"URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf"),
|
||||
"links",
|
||||
ImmutableList.of(
|
||||
ImmutableMap.of(
|
||||
"rel", "alternate",
|
||||
"href", "https://www.icann.org/wicf",
|
||||
"href", "https://icann.org/wicf",
|
||||
"type", "text/html")))));
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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))));
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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<String> serviceAccounts =
|
||||
ImmutableList.of("service@email.test", "email@service.goog");
|
||||
private static final ImmutableSet<String> 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<String> provideServiceAccountEmails() {
|
||||
@Config("allowedServiceAccountEmails")
|
||||
ImmutableSet<String> provideAllowedServiceAccountEmails() {
|
||||
return serviceAccounts;
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Config("oauthClientId")
|
||||
String provideOauthClientId() {
|
||||
return "client-id";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String, String> headers = ArrayListMultimap.create();
|
||||
Multimap<String, String> 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<String, String> 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
|
||||
|
||||
@@ -1357,5 +1357,16 @@ public final class DatabaseHelper {
|
||||
.isEqualTo(1);
|
||||
}
|
||||
|
||||
public static void assertDnsRequestsWithRequestTime(DateTime requestTime, int numOfDomains) {
|
||||
assertThat(
|
||||
tm().transact(
|
||||
() ->
|
||||
tm().createQueryComposer(DnsRefreshRequest.class)
|
||||
.where("type", EQ, DnsUtils.TargetType.DOMAIN)
|
||||
.where("requestTime", EQ, requestTime)
|
||||
.count()))
|
||||
.isEqualTo(numOfDomains);
|
||||
}
|
||||
|
||||
private DatabaseHelper() {}
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
|
||||
@@ -22,6 +22,7 @@ import static google.registry.request.Action.Service.TOOLS;
|
||||
import static java.nio.charset.StandardCharsets.UTF_8;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyBoolean;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoMoreInteractions;
|
||||
@@ -29,6 +30,7 @@ import static org.mockito.Mockito.when;
|
||||
|
||||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.net.MediaType;
|
||||
import google.registry.request.Action.Service;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
@@ -46,7 +48,7 @@ class CurlCommandTest extends CommandTestCase<CurlCommand> {
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
command.setConnection(connection);
|
||||
when(connection.withService(any())).thenReturn(connectionForService);
|
||||
when(connection.withService(any(Service.class), anyBoolean())).thenReturn(connectionForService);
|
||||
}
|
||||
|
||||
@Captor ArgumentCaptor<ImmutableMap<String, String>> urlParamCaptor;
|
||||
@@ -54,7 +56,7 @@ class CurlCommandTest extends CommandTestCase<CurlCommand> {
|
||||
@Test
|
||||
void testGetInvocation() throws Exception {
|
||||
runCommand("--path=/foo/bar?a=1&b=2", "--service=TOOLS");
|
||||
verify(connection).withService(TOOLS);
|
||||
verify(connection).withService(eq(TOOLS), eq(false));
|
||||
verifyNoMoreInteractions(connection);
|
||||
verify(connectionForService)
|
||||
.sendGetRequest(eq("/foo/bar?a=1&b=2"), eq(ImmutableMap.<String, String>of()));
|
||||
@@ -63,7 +65,7 @@ class CurlCommandTest extends CommandTestCase<CurlCommand> {
|
||||
@Test
|
||||
void testExplicitGetInvocation() throws Exception {
|
||||
runCommand("--path=/foo/bar?a=1&b=2", "--request=GET", "--service=BACKEND");
|
||||
verify(connection).withService(BACKEND);
|
||||
verify(connection).withService(eq(BACKEND), eq(false));
|
||||
verifyNoMoreInteractions(connection);
|
||||
verify(connectionForService)
|
||||
.sendGetRequest(eq("/foo/bar?a=1&b=2"), eq(ImmutableMap.<String, String>of()));
|
||||
@@ -72,7 +74,7 @@ class CurlCommandTest extends CommandTestCase<CurlCommand> {
|
||||
@Test
|
||||
void testPostInvocation() throws Exception {
|
||||
runCommand("--path=/foo/bar?a=1&b=2", "--data=some data", "--service=DEFAULT");
|
||||
verify(connection).withService(DEFAULT);
|
||||
verify(connection).withService(eq(DEFAULT), eq(false));
|
||||
verifyNoMoreInteractions(connection);
|
||||
verify(connectionForService)
|
||||
.sendPostRequest(
|
||||
@@ -89,7 +91,7 @@ class CurlCommandTest extends CommandTestCase<CurlCommand> {
|
||||
"--data=some data",
|
||||
"--service=DEFAULT",
|
||||
"--content-type=application/json");
|
||||
verify(connection).withService(DEFAULT);
|
||||
verify(connection).withService(eq(DEFAULT), eq(false));
|
||||
verifyNoMoreInteractions(connection);
|
||||
verify(connectionForService)
|
||||
.sendPostRequest(
|
||||
@@ -118,7 +120,7 @@ class CurlCommandTest extends CommandTestCase<CurlCommand> {
|
||||
void testMultiDataPost() throws Exception {
|
||||
runCommand(
|
||||
"--path=/foo/bar?a=1&b=2", "--data=first=100", "-d", "second=200", "--service=PUBAPI");
|
||||
verify(connection).withService(PUBAPI);
|
||||
verify(connection).withService(eq(PUBAPI), eq(false));
|
||||
verifyNoMoreInteractions(connection);
|
||||
verify(connectionForService)
|
||||
.sendPostRequest(
|
||||
@@ -132,7 +134,7 @@ class CurlCommandTest extends CommandTestCase<CurlCommand> {
|
||||
void testDataDoesntSplit() throws Exception {
|
||||
runCommand(
|
||||
"--path=/foo/bar?a=1&b=2", "--data=one,two", "--service=PUBAPI");
|
||||
verify(connection).withService(PUBAPI);
|
||||
verify(connection).withService(eq(PUBAPI), eq(false));
|
||||
verifyNoMoreInteractions(connection);
|
||||
verify(connectionForService)
|
||||
.sendPostRequest(
|
||||
@@ -145,7 +147,20 @@ class CurlCommandTest extends CommandTestCase<CurlCommand> {
|
||||
@Test
|
||||
void testExplicitPostInvocation() throws Exception {
|
||||
runCommand("--path=/foo/bar?a=1&b=2", "--request=POST", "--service=TOOLS");
|
||||
verify(connection).withService(TOOLS);
|
||||
verify(connection).withService(eq(TOOLS), eq(false));
|
||||
verifyNoMoreInteractions(connection);
|
||||
verify(connectionForService)
|
||||
.sendPostRequest(
|
||||
eq("/foo/bar?a=1&b=2"),
|
||||
eq(ImmutableMap.<String, String>of()),
|
||||
eq(MediaType.PLAIN_TEXT_UTF_8),
|
||||
eq("".getBytes(UTF_8)));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testCanaryInvocation() throws Exception {
|
||||
runCommand("--path=/foo/bar?a=1&b=2", "--request=POST", "--service=TOOLS", "--canary");
|
||||
verify(connection).withService(eq(TOOLS), eq(true));
|
||||
verifyNoMoreInteractions(connection);
|
||||
verify(connectionForService)
|
||||
.sendPostRequest(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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}");
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
// 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.tools;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.request.Action.Service.DEFAULT;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
/** Unit tests for {@link google.registry.tools.ServiceConnection}. */
|
||||
public class ServiceConnectionTest {
|
||||
|
||||
@Test
|
||||
void testServerUrl_notCanary() {
|
||||
ServiceConnection connection = new ServiceConnection().withService(DEFAULT, false);
|
||||
String serverUrl = connection.getServer().toString();
|
||||
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-default.example.com");
|
||||
}
|
||||
}
|
||||
@@ -15,18 +15,24 @@
|
||||
package google.registry.tools.server;
|
||||
|
||||
import static com.google.common.truth.Truth.assertThat;
|
||||
import static google.registry.persistence.transaction.QueryComposer.Comparator.EQ;
|
||||
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
|
||||
import static google.registry.testing.DatabaseHelper.assertDnsRequestsWithRequestTime;
|
||||
import static google.registry.testing.DatabaseHelper.assertDomainDnsRequestWithRequestTime;
|
||||
import static google.registry.testing.DatabaseHelper.assertNoDnsRequestsExcept;
|
||||
import static google.registry.testing.DatabaseHelper.createTld;
|
||||
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
|
||||
import static google.registry.testing.DatabaseHelper.persistDeletedDomain;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import google.registry.dns.DnsUtils;
|
||||
import google.registry.model.common.DnsRefreshRequest;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions;
|
||||
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
|
||||
import google.registry.testing.FakeClock;
|
||||
import google.registry.testing.FakeResponse;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import org.joda.time.DateTime;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
@@ -46,20 +52,16 @@ public class RefreshDnsForAllDomainsActionTest {
|
||||
|
||||
@BeforeEach
|
||||
void beforeEach() {
|
||||
action = new RefreshDnsForAllDomainsAction();
|
||||
action.smearMinutes = 1;
|
||||
action.random = new Random();
|
||||
action.random.setSeed(123L);
|
||||
action.clock = clock;
|
||||
action.response = response;
|
||||
createTld("bar");
|
||||
action =
|
||||
new RefreshDnsForAllDomainsAction(
|
||||
response, ImmutableSet.of("bar"), Optional.of(10), new Random());
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_runAction_successfullyEnqueuesDnsRefreshes() throws Exception {
|
||||
persistActiveDomain("foo.bar");
|
||||
persistActiveDomain("low.bar");
|
||||
action.tlds = ImmutableSet.of("bar");
|
||||
action.run();
|
||||
assertDomainDnsRequestWithRequestTime("foo.bar", clock.nowUtc());
|
||||
assertDomainDnsRequestWithRequestTime("low.bar", clock.nowUtc());
|
||||
@@ -69,18 +71,27 @@ public class RefreshDnsForAllDomainsActionTest {
|
||||
void test_runAction_smearsOutDnsRefreshes() throws Exception {
|
||||
persistActiveDomain("foo.bar");
|
||||
persistActiveDomain("low.bar");
|
||||
action.tlds = ImmutableSet.of("bar");
|
||||
action.smearMinutes = 1000;
|
||||
action.run();
|
||||
assertDomainDnsRequestWithRequestTime("foo.bar", clock.nowUtc().plusMinutes(450));
|
||||
assertDomainDnsRequestWithRequestTime("low.bar", clock.nowUtc().plusMinutes(782));
|
||||
// Set batch size to 1 since each batch will be enqueud at the same time
|
||||
action =
|
||||
new RefreshDnsForAllDomainsAction(
|
||||
response, ImmutableSet.of("bar"), Optional.of(1), new Random());
|
||||
tm().transact(() -> action.refreshBatch("", 1000));
|
||||
tm().transact(() -> action.refreshBatch("", 1000));
|
||||
ImmutableList<DnsRefreshRequest> refreshRequests =
|
||||
tm().transact(
|
||||
() ->
|
||||
tm().createQueryComposer(DnsRefreshRequest.class)
|
||||
.where("type", EQ, DnsUtils.TargetType.DOMAIN)
|
||||
.list());
|
||||
assertThat(refreshRequests.size()).isEqualTo(2);
|
||||
assertThat(refreshRequests.get(0).getRequestTime())
|
||||
.isNotEqualTo(refreshRequests.get(1).getRequestTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_runAction_doesntRefreshDeletedDomain() throws Exception {
|
||||
persistActiveDomain("foo.bar");
|
||||
persistDeletedDomain("deleted.bar", clock.nowUtc().minusYears(1));
|
||||
action.tlds = ImmutableSet.of("bar");
|
||||
action.run();
|
||||
assertDomainDnsRequestWithRequestTime("foo.bar", clock.nowUtc());
|
||||
assertNoDnsRequestsExcept("foo.bar");
|
||||
@@ -92,7 +103,6 @@ public class RefreshDnsForAllDomainsActionTest {
|
||||
persistActiveDomain("foo.bar");
|
||||
persistActiveDomain("low.bar");
|
||||
persistActiveDomain("ignore.baz");
|
||||
action.tlds = ImmutableSet.of("bar");
|
||||
action.run();
|
||||
assertDomainDnsRequestWithRequestTime("foo.bar", clock.nowUtc());
|
||||
assertDomainDnsRequestWithRequestTime("low.bar", clock.nowUtc());
|
||||
@@ -100,13 +110,11 @@ public class RefreshDnsForAllDomainsActionTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
void test_smearMinutesMustBeSpecified() {
|
||||
action.tlds = ImmutableSet.of("bar");
|
||||
action.smearMinutes = 0;
|
||||
IllegalArgumentException thrown =
|
||||
assertThrows(IllegalArgumentException.class, () -> action.run());
|
||||
assertThat(thrown)
|
||||
.hasMessageThat()
|
||||
.isEqualTo("Must specify a positive number of smear minutes");
|
||||
void test_successfullyBatchesNames() {
|
||||
for (int i = 0; i <= 10; i++) {
|
||||
persistActiveDomain(String.format("test%s.bar", i));
|
||||
}
|
||||
action.run();
|
||||
assertDnsRequestsWithRequestTime(clock.nowUtc(), 11);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -55,7 +55,7 @@
|
||||
<fee:currency>USD</fee:currency>
|
||||
<fee:command>create</fee:command>
|
||||
<fee:period unit="y">1</fee:period>
|
||||
<fee:fee description="create">13.00</fee:fee>
|
||||
<fee:fee description="create">11.05</fee:fee>
|
||||
</fee:cd>
|
||||
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
|
||||
<fee:name>allowedinsunrise.tld</fee:name>
|
||||
@@ -83,7 +83,7 @@
|
||||
<fee:currency>USD</fee:currency>
|
||||
<fee:command>create</fee:command>
|
||||
<fee:period unit="y">1</fee:period>
|
||||
<fee:fee description="create">13.00</fee:fee>
|
||||
<fee:fee description="create">11.05</fee:fee>
|
||||
<fee:class>collision</fee:class>
|
||||
</fee:cd>
|
||||
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
|
||||
@@ -115,7 +115,7 @@
|
||||
<fee:currency>USD</fee:currency>
|
||||
<fee:command>create</fee:command>
|
||||
<fee:period unit="y">1</fee:period>
|
||||
<fee:fee description="create">70.00</fee:fee>
|
||||
<fee:fee description="create">59.50</fee:fee>
|
||||
<fee:class>premium-collision</fee:class>
|
||||
</fee:cd>
|
||||
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
<fee:currency>USD</fee:currency>
|
||||
<fee:command>create</fee:command>
|
||||
<fee:period unit="y">1</fee:period>
|
||||
<fee:fee description="create">13.00</fee:fee>
|
||||
<fee:fee description="create">11.05</fee:fee>
|
||||
</fee:cd>
|
||||
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
|
||||
<fee:name>allowedinsunrise.tld</fee:name>
|
||||
@@ -88,7 +88,7 @@
|
||||
<fee:currency>USD</fee:currency>
|
||||
<fee:command>create</fee:command>
|
||||
<fee:period unit="y">1</fee:period>
|
||||
<fee:fee description="create">13.00</fee:fee>
|
||||
<fee:fee description="create">11.05</fee:fee>
|
||||
<fee:class>collision</fee:class>
|
||||
</fee:cd>
|
||||
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
|
||||
@@ -121,7 +121,7 @@
|
||||
<fee:currency>USD</fee:currency>
|
||||
<fee:command>create</fee:command>
|
||||
<fee:period unit="y">1</fee:period>
|
||||
<fee:fee description="create">70.00</fee:fee>
|
||||
<fee:fee description="create">59.50</fee:fee>
|
||||
<fee:class>premium-collision</fee:class>
|
||||
</fee:cd>
|
||||
<fee:cd xmlns:fee="urn:ietf:params:xml:ns:fee-0.6">
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<fee:command>create</fee:command>
|
||||
<fee:currency>USD</fee:currency>
|
||||
<fee:period unit="y">1</fee:period>
|
||||
<fee:fee description="create">13.00</fee:fee>
|
||||
<fee:fee description="create">11.05</fee:fee>
|
||||
</fee:cd>
|
||||
<fee:cd avail="1">
|
||||
<fee:object>
|
||||
@@ -49,7 +49,7 @@
|
||||
<fee:command>create</fee:command>
|
||||
<fee:currency>USD</fee:currency>
|
||||
<fee:period unit="y">1</fee:period>
|
||||
<fee:fee description="create">13.00</fee:fee>
|
||||
<fee:fee description="create">11.05</fee:fee>
|
||||
<fee:class>collision</fee:class>
|
||||
</fee:cd>
|
||||
<fee:cd avail="1">
|
||||
@@ -59,7 +59,7 @@
|
||||
<fee:command>create</fee:command>
|
||||
<fee:currency>USD</fee:currency>
|
||||
<fee:period unit="y">1</fee:period>
|
||||
<fee:fee description="create">70.00</fee:fee>
|
||||
<fee:fee description="create">59.50</fee:fee>
|
||||
<fee:class>premium-collision</fee:class>
|
||||
</fee:cd>
|
||||
</fee:chkData>
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
</fee:object>
|
||||
<fee:command name="create">
|
||||
<fee:period unit="y">1</fee:period>
|
||||
<fee:fee description="create">13.00</fee:fee>
|
||||
<fee:fee description="create">11.05</fee:fee>
|
||||
</fee:command>
|
||||
</fee:cd>
|
||||
<fee:cd>
|
||||
@@ -102,7 +102,7 @@
|
||||
</fee:object>
|
||||
<fee:command name="create">
|
||||
<fee:period unit="y">1</fee:period>
|
||||
<fee:fee description="create">13.00</fee:fee>
|
||||
<fee:fee description="create">11.05</fee:fee>
|
||||
<fee:class>collision</fee:class>
|
||||
</fee:command>
|
||||
</fee:cd>
|
||||
@@ -142,7 +142,7 @@
|
||||
</fee:object>
|
||||
<fee:command name="create">
|
||||
<fee:period unit="y">1</fee:period>
|
||||
<fee:fee description="create">70.00</fee:fee>
|
||||
<fee:fee description="create">59.50</fee:fee>
|
||||
<fee:class>premium-collision</fee:class>
|
||||
</fee:command>
|
||||
</fee:cd>
|
||||
|
||||
@@ -156,12 +156,12 @@
|
||||
},
|
||||
{
|
||||
"title": "RDDS Inaccuracy Complaint Form",
|
||||
"description" : ["URL of the ICANN RDDS Inaccuracy Complaint Form: https://www.icann.org/wicf"],
|
||||
"description" : ["URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf"],
|
||||
"links" :
|
||||
[
|
||||
{
|
||||
"rel" : "alternate",
|
||||
"href" : "https://www.icann.org/wicf",
|
||||
"href" : "https://icann.org/wicf",
|
||||
"type" : "text/html"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -137,12 +137,12 @@
|
||||
},
|
||||
{
|
||||
"title": "RDDS Inaccuracy Complaint Form",
|
||||
"description" : ["URL of the ICANN RDDS Inaccuracy Complaint Form: https://www.icann.org/wicf"],
|
||||
"description" : ["URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf"],
|
||||
"links" :
|
||||
[
|
||||
{
|
||||
"rel" : "alternate",
|
||||
"href" : "https://www.icann.org/wicf",
|
||||
"href" : "https://icann.org/wicf",
|
||||
"type" : "text/html"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -157,12 +157,12 @@
|
||||
},
|
||||
{
|
||||
"title": "RDDS Inaccuracy Complaint Form",
|
||||
"description" : ["URL of the ICANN RDDS Inaccuracy Complaint Form: https://www.icann.org/wicf"],
|
||||
"description" : ["URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf"],
|
||||
"links" :
|
||||
[
|
||||
{
|
||||
"rel" : "alternate",
|
||||
"href" : "https://www.icann.org/wicf",
|
||||
"href" : "https://icann.org/wicf",
|
||||
"type" : "text/html"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -95,12 +95,12 @@
|
||||
},
|
||||
{
|
||||
"title": "RDDS Inaccuracy Complaint Form",
|
||||
"description": ["URL of the ICANN RDDS Inaccuracy Complaint Form: https://www.icann.org/wicf"],
|
||||
"description": ["URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf"],
|
||||
"links":
|
||||
[
|
||||
{
|
||||
"rel": "alternate",
|
||||
"href": "https://www.icann.org/wicf",
|
||||
"href": "https://icann.org/wicf",
|
||||
"type": "text/html"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -130,12 +130,12 @@
|
||||
},
|
||||
{
|
||||
"title": "RDDS Inaccuracy Complaint Form",
|
||||
"description": ["URL of the ICANN RDDS Inaccuracy Complaint Form: https://www.icann.org/wicf"],
|
||||
"description": ["URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf"],
|
||||
"links":
|
||||
[
|
||||
{
|
||||
"rel":"alternate",
|
||||
"href":"https://www.icann.org/wicf",
|
||||
"href":"https://icann.org/wicf",
|
||||
"type":"text/html"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -98,12 +98,12 @@
|
||||
},
|
||||
{
|
||||
"title": "RDDS Inaccuracy Complaint Form",
|
||||
"description":["URL of the ICANN RDDS Inaccuracy Complaint Form: https://www.icann.org/wicf"],
|
||||
"description":["URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf"],
|
||||
"links":[
|
||||
{
|
||||
"type":"text/html",
|
||||
"rel":"alternate",
|
||||
"href":"https://www.icann.org/wicf"
|
||||
"href":"https://icann.org/wicf"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -144,12 +144,12 @@
|
||||
},
|
||||
{
|
||||
"title": "RDDS Inaccuracy Complaint Form",
|
||||
"description" : ["URL of the ICANN RDDS Inaccuracy Complaint Form: https://www.icann.org/wicf"],
|
||||
"description" : ["URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf"],
|
||||
"links" :
|
||||
[
|
||||
{
|
||||
"rel" : "alternate",
|
||||
"href" : "https://www.icann.org/wicf",
|
||||
"href" : "https://icann.org/wicf",
|
||||
"type" : "text/html"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -59,12 +59,12 @@
|
||||
},
|
||||
{
|
||||
"title": "RDDS Inaccuracy Complaint Form",
|
||||
"description": ["URL of the ICANN RDDS Inaccuracy Complaint Form: https://www.icann.org/wicf"],
|
||||
"description": ["URL of the ICANN RDDS Inaccuracy Complaint Form: https://icann.org/wicf"],
|
||||
"links":
|
||||
[
|
||||
{
|
||||
"rel": "alternate",
|
||||
"href": "https://www.icann.org/wicf",
|
||||
"href": "https://icann.org/wicf",
|
||||
"type": "text/html"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -704,6 +704,7 @@
|
||||
anchor_tenant_add_grace_period_length interval not null,
|
||||
auto_renew_grace_period_length interval not null,
|
||||
automatic_transfer_length interval not null,
|
||||
breakglass_mode boolean not null,
|
||||
claims_period_end timestamptz not null,
|
||||
create_billing_cost_amount numeric(19, 2),
|
||||
create_billing_cost_currency text,
|
||||
|
||||
@@ -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<GoogleCredentials> refreshedCredentialsSupplier,
|
||||
@Named("iapClientId") Optional<String> iapClientId,
|
||||
@Named("idToken") Supplier<String> 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
|
||||
|
||||
@@ -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<String> gcpScopes;
|
||||
public int serverCertificateCacheSeconds;
|
||||
public Gcs gcs;
|
||||
|
||||
@@ -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<String> 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.
|
||||
*
|
||||
* <p>The token is cached for 1 hour to reduce repeated calls to the metadata server.
|
||||
*
|
||||
* @see <a href="https://cloud.google.com/docs/authentication/token-types#id-lifetime">ID token
|
||||
* lifetime</a>
|
||||
*/
|
||||
@Singleton
|
||||
@Provides
|
||||
@Named("idToken")
|
||||
static Supplier<String> 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) {
|
||||
|
||||
@@ -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<GoogleCredentials> refreshedCredentialsSupplier,
|
||||
@Named("iapClientId") Optional<String> iapClientId,
|
||||
@Named("idToken") Supplier<String> idTokenSupplier,
|
||||
FrontendMetrics metrics) {
|
||||
return new WhoisServiceHandler(
|
||||
config.whois.relayHost,
|
||||
config.whois.relayPath,
|
||||
refreshedCredentialsSupplier,
|
||||
iapClientId,
|
||||
metrics);
|
||||
config.whois.relayHost, config.whois.relayPath, idTokenSupplier, metrics);
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
||||
@@ -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: []
|
||||
|
||||
@@ -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<GoogleCredentials> refreshedCredentialsSupplier,
|
||||
Optional<String> iapClientId,
|
||||
Supplier<String> 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<X509Certificate> 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);
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <p>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.
|
||||
* <p>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<FullHttpResponse> {
|
||||
|
||||
@@ -79,21 +72,18 @@ public abstract class HttpsRelayServiceHandler extends ByteToMessageCodec<FullHt
|
||||
private final Map<String, Cookie> cookieStore = new LinkedHashMap<>();
|
||||
private final String relayHost;
|
||||
private final String relayPath;
|
||||
private final Supplier<GoogleCredentials> refreshedCredentialsSupplier;
|
||||
private final Optional<String> iapClientId;
|
||||
private final Supplier<String> idTokenSupplier;
|
||||
|
||||
protected final FrontendMetrics metrics;
|
||||
|
||||
HttpsRelayServiceHandler(
|
||||
String relayHost,
|
||||
String relayPath,
|
||||
Supplier<GoogleCredentials> refreshedCredentialsSupplier,
|
||||
Optional<String> iapClientId,
|
||||
Supplier<String> 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<FullHt
|
||||
protected FullHttpRequest decodeFullHttpRequest(ByteBuf byteBuf) {
|
||||
FullHttpRequest request =
|
||||
new DefaultFullHttpRequest(HttpVersion.HTTP_1_1, HttpMethod.POST, relayPath);
|
||||
GoogleCredentials credentials = refreshedCredentialsSupplier.get();
|
||||
request
|
||||
.headers()
|
||||
.set(HttpHeaderNames.USER_AGENT, "Proxy")
|
||||
.set(HttpHeaderNames.HOST, relayHost)
|
||||
.set(
|
||||
HttpHeaderNames.AUTHORIZATION, "Bearer " + credentials.getAccessToken().getTokenValue())
|
||||
.set(HttpHeaderNames.AUTHORIZATION, "Bearer " + idTokenSupplier.get())
|
||||
.setInt(HttpHeaderNames.CONTENT_LENGTH, byteBuf.readableBytes());
|
||||
// Set the Proxy-Authorization header if using IAP
|
||||
if (iapClientId.isPresent()) {
|
||||
IdTokenProvider idTokenProvider = (IdTokenProvider) credentials;
|
||||
try {
|
||||
// Note: we use Option.FORMAT_FULL to make sure the JWT we receive contains the email
|
||||
// address (as is required by IAP)
|
||||
IdToken idToken =
|
||||
idTokenProvider.idTokenWithAudience(
|
||||
iapClientId.get(), ImmutableList.of(Option.FORMAT_FULL));
|
||||
request
|
||||
.headers()
|
||||
.set(HttpHeaderNames.PROXY_AUTHORIZATION, "Bearer " + idToken.getTokenValue());
|
||||
} catch (IOException e) {
|
||||
logger.atSevere().withCause(e).log("Error when attempting to retrieve IAP ID token");
|
||||
}
|
||||
}
|
||||
request.content().writeBytes(byteBuf);
|
||||
return request;
|
||||
}
|
||||
@@ -204,6 +176,7 @@ public abstract class HttpsRelayServiceHandler extends ByteToMessageCodec<FullHt
|
||||
logger.atSevere().withCause(cause).log(
|
||||
"Inbound exception caught for channel %s", ctx.channel());
|
||||
}
|
||||
@SuppressWarnings("unused")
|
||||
ChannelFuture unusedFuture = ctx.close();
|
||||
}
|
||||
|
||||
@@ -222,6 +195,7 @@ public abstract class HttpsRelayServiceHandler extends ByteToMessageCodec<FullHt
|
||||
logger.atSevere().withCause(channelFuture.cause()).log(
|
||||
"Outbound exception caught for channel %s", channelFuture.channel());
|
||||
}
|
||||
@SuppressWarnings("unused")
|
||||
ChannelFuture unusedFuture = channelFuture.channel().close();
|
||||
}
|
||||
});
|
||||
@@ -230,6 +204,9 @@ public abstract class HttpsRelayServiceHandler extends ByteToMessageCodec<FullHt
|
||||
|
||||
/** Exception thrown when the response status from GAE is not 200. */
|
||||
public static class NonOkHttpResponseException extends Exception {
|
||||
|
||||
private static final long serialVersionUID = 5340993059579288708L;
|
||||
|
||||
NonOkHttpResponseException(FullHttpResponse response, Channel channel) {
|
||||
super(
|
||||
String.format(
|
||||
|
||||
@@ -16,7 +16,6 @@ package google.registry.proxy.handler;
|
||||
|
||||
import static com.google.common.base.Preconditions.checkArgument;
|
||||
|
||||
import com.google.auth.oauth2.GoogleCredentials;
|
||||
import google.registry.proxy.metric.FrontendMetrics;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import io.netty.channel.ChannelFutureListener;
|
||||
@@ -26,7 +25,6 @@ import io.netty.handler.codec.http.FullHttpRequest;
|
||||
import io.netty.handler.codec.http.HttpHeaderNames;
|
||||
import io.netty.handler.codec.http.HttpHeaderValues;
|
||||
import io.netty.handler.codec.http.HttpResponse;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/** Handler that processes WHOIS protocol logic. */
|
||||
@@ -35,10 +33,9 @@ public final class WhoisServiceHandler extends HttpsRelayServiceHandler {
|
||||
public WhoisServiceHandler(
|
||||
String relayHost,
|
||||
String relayPath,
|
||||
Supplier<GoogleCredentials> refreshedCredentialsSupplier,
|
||||
Optional<String> iapClientId,
|
||||
Supplier<String> idTokenSupplier,
|
||||
FrontendMetrics metrics) {
|
||||
super(relayHost, relayPath, refreshedCredentialsSupplier, iapClientId, metrics);
|
||||
super(relayHost, relayPath, idTokenSupplier, metrics);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -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<X509Certificate> unusedPromise =
|
||||
channel.attr(CLIENT_CERTIFICATE_PROMISE_KEY).get().setSuccess(certificate);
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <p>Subclass should implement an no-arg constructor that calls constructors of this class,
|
||||
* <p>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<Provider<? extends ChannelHandler>> excludeHandlerProvidersForTesting(
|
||||
ImmutableList<Provider<? extends ChannelHandler>> 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<String> 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<GoogleCredentials> 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<String> 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
|
||||
|
||||
@@ -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<String> 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<String> 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 {
|
||||
* <p>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.
|
||||
*
|
||||
* <p>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<String> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<X509Certificate> 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<X509Certificate> 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<EmbeddedChannel>() {
|
||||
@@ -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 = "<epp>stuff</epp>";
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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())
|
||||
|
||||
5
release/builder/go.mod
Normal file
5
release/builder/go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module nomulus/release/builder
|
||||
|
||||
go 1.21
|
||||
|
||||
require gopkg.in/yaml.v3 v3.0.1
|
||||
4
release/builder/go.sum
Normal file
4
release/builder/go.sum
Normal file
@@ -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=
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
98
util/src/main/java/google/registry/util/OidcTokenUtils.java
Normal file
98
util/src/main/java/google/registry/util/OidcTokenUtils.java
Normal file
@@ -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.
|
||||
*
|
||||
* <p>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 <a
|
||||
* href="https://cloud.google.com/iap/docs/authentication-howto#authenticating_from_a_desktop_app">
|
||||
* Authenticating from a desktop app</a>
|
||||
*/
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user