diff --git a/core/src/main/java/google/registry/batch/BatchModule.java b/core/src/main/java/google/registry/batch/BatchModule.java index f452066fb..cdac49629 100644 --- a/core/src/main/java/google/registry/batch/BatchModule.java +++ b/core/src/main/java/google/registry/batch/BatchModule.java @@ -21,6 +21,7 @@ import static google.registry.batch.AsyncTaskEnqueuer.PARAM_RESOURCE_KEY; import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_ACTIONS; import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_DELETE; import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_HOST_RENAME; +import static google.registry.batch.CannedScriptExecutionAction.SCRIPT_PARAM; import static google.registry.request.RequestParameters.extractBooleanParameter; import static google.registry.request.RequestParameters.extractIntParameter; import static google.registry.request.RequestParameters.extractLongParameter; @@ -145,4 +146,11 @@ public class BatchModule { static Queue provideAsyncHostRenamePullQueue() { return getQueue(QUEUE_ASYNC_HOST_RENAME); } + + // TODO(b/234424397): remove method after credential changes are rolled out. + @Provides + @Parameter(SCRIPT_PARAM) + static String provideScriptName(HttpServletRequest req) { + return extractRequiredParameter(req, SCRIPT_PARAM); + } } diff --git a/core/src/main/java/google/registry/batch/CannedScriptExecutionAction.java b/core/src/main/java/google/registry/batch/CannedScriptExecutionAction.java new file mode 100644 index 000000000..f62098bed --- /dev/null +++ b/core/src/main/java/google/registry/batch/CannedScriptExecutionAction.java @@ -0,0 +1,74 @@ +// Copyright 2022 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.batch; + +import static google.registry.request.Action.Method.POST; + +import com.google.common.collect.ImmutableMap; +import com.google.common.flogger.FluentLogger; +import google.registry.batch.cannedscript.GroupsApiChecker; +import google.registry.request.Action; +import google.registry.request.Parameter; +import google.registry.request.auth.Auth; +import javax.inject.Inject; + +/** + * Action that executes a canned script specified by the caller. + * + *

This class is introduced to help the safe rollout of credential changes. The delegated + * credentials in particular, benefit from this: they require manual configuration of the peer + * system in each environment, and may wait hours or even days after deployment until triggered by + * user activities. + * + *

This action can be invoked using the Nomulus CLI command: {@code nomulus -e ${env} curl + * --service BACKEND -X POST -u '/_dr/task/executeCannedScript?script=${script_name}'} + */ +// TODO(b/234424397): remove class after credential changes are rolled out. +@Action( + service = Action.Service.BACKEND, + path = "/_dr/task/executeCannedScript", + method = POST, + automaticallyPrintOk = true, + auth = Auth.AUTH_INTERNAL_OR_ADMIN) +public class CannedScriptExecutionAction implements Runnable { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + static final String SCRIPT_PARAM = "script"; + + static final ImmutableMap SCRIPTS = + ImmutableMap.of("runGroupsApiChecks", GroupsApiChecker::runGroupsApiChecks); + + private final String scriptName; + + @Inject + CannedScriptExecutionAction(@Parameter(SCRIPT_PARAM) String scriptName) { + logger.atInfo().log("Received request to run script %s", scriptName); + this.scriptName = scriptName; + } + + @Override + public void run() { + if (!SCRIPTS.containsKey(scriptName)) { + throw new IllegalArgumentException("Script not found:" + scriptName); + } + try { + SCRIPTS.get(scriptName).run(); + logger.atInfo().log("Finished running %s.", scriptName); + } catch (Throwable t) { + logger.atWarning().withCause(t).log("Error executing %s", scriptName); + throw new RuntimeException("Execution failed."); + } + } +} diff --git a/core/src/main/java/google/registry/batch/cannedscript/GroupsApiChecker.java b/core/src/main/java/google/registry/batch/cannedscript/GroupsApiChecker.java new file mode 100644 index 000000000..26937b96d --- /dev/null +++ b/core/src/main/java/google/registry/batch/cannedscript/GroupsApiChecker.java @@ -0,0 +1,122 @@ +// Copyright 2022 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.batch.cannedscript; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static google.registry.util.RegistrarUtils.normalizeRegistrarId; + +import com.google.api.services.admin.directory.Directory; +import com.google.api.services.groupssettings.Groupssettings; +import com.google.common.base.Supplier; +import com.google.common.base.Suppliers; +import com.google.common.base.Throwables; +import com.google.common.collect.Streams; +import com.google.common.flogger.FluentLogger; +import dagger.Component; +import dagger.Module; +import dagger.Provides; +import google.registry.config.CredentialModule; +import google.registry.config.CredentialModule.AdcDelegatedCredential; +import google.registry.config.RegistryConfig.Config; +import google.registry.config.RegistryConfig.ConfigModule; +import google.registry.groups.DirectoryGroupsConnection; +import google.registry.model.registrar.Registrar; +import google.registry.model.registrar.RegistrarPoc; +import google.registry.util.GoogleCredentialsBundle; +import google.registry.util.UtilsModule; +import java.util.List; +import java.util.Set; +import javax.inject.Singleton; + +/** + * Verifies that the credential with the {@link AdcDelegatedCredential} annotation can be used to + * access the Google Workspace Groups API. + */ +// TODO(b/234424397): remove class after credential changes are rolled out. +public class GroupsApiChecker { + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + private static final Supplier COMPONENT_SUPPLIER = + Suppliers.memoize(DaggerGroupsApiChecker_GroupsConnectionComponent::create); + + public static void runGroupsApiChecks() { + GroupsConnectionComponent component = COMPONENT_SUPPLIER.get(); + DirectoryGroupsConnection groupsConnection = component.groupsConnection(); + + List registrars = + Streams.stream(Registrar.loadAllCached()) + .filter(registrar -> registrar.isLive() && registrar.getType() == Registrar.Type.REAL) + .collect(toImmutableList()); + for (Registrar registrar : registrars) { + for (final RegistrarPoc.Type type : RegistrarPoc.Type.values()) { + String groupKey = + String.format( + "%s-%s-contacts@%s", + normalizeRegistrarId(registrar.getRegistrarId()), + type.getDisplayName(), + component.gSuiteDomainName()); + try { + Set currentMembers = groupsConnection.getMembersOfGroup(groupKey); + logger.atInfo().log("Found %s members for %s.", currentMembers.size(), groupKey); + } catch (Exception e) { + Throwables.throwIfUnchecked(e); + throw new RuntimeException(e); + } + } + } + } + + @Singleton + @Component( + modules = { + ConfigModule.class, + CredentialModule.class, + GroupsApiModule.class, + UtilsModule.class + }) + interface GroupsConnectionComponent { + DirectoryGroupsConnection groupsConnection(); + + @Config("gSuiteDomainName") + String gSuiteDomainName(); + } + + @Module + static class GroupsApiModule { + @Provides + static Directory provideDirectory( + @AdcDelegatedCredential GoogleCredentialsBundle credentialsBundle, + @Config("projectId") String projectId) { + return new Directory.Builder( + credentialsBundle.getHttpTransport(), + credentialsBundle.getJsonFactory(), + credentialsBundle.getHttpRequestInitializer()) + .setApplicationName(projectId) + .build(); + } + + @Provides + static Groupssettings provideGroupsSettings( + @AdcDelegatedCredential GoogleCredentialsBundle credentialsBundle, + @Config("projectId") String projectId) { + return new Groupssettings.Builder( + credentialsBundle.getHttpTransport(), + credentialsBundle.getJsonFactory(), + credentialsBundle.getHttpRequestInitializer()) + .setApplicationName(projectId) + .build(); + } + } +} diff --git a/core/src/main/java/google/registry/config/CredentialModule.java b/core/src/main/java/google/registry/config/CredentialModule.java index 9e1f5b54a..39a36bee7 100644 --- a/core/src/main/java/google/registry/config/CredentialModule.java +++ b/core/src/main/java/google/registry/config/CredentialModule.java @@ -14,14 +14,17 @@ package google.registry.config; +import static com.google.common.base.Preconditions.checkArgument; import static java.nio.charset.StandardCharsets.UTF_8; +import com.google.auth.ServiceAccountSigner; import com.google.auth.oauth2.GoogleCredentials; import com.google.common.collect.ImmutableList; import dagger.Module; import dagger.Provides; import google.registry.config.RegistryConfig.Config; import google.registry.keyring.api.KeyModule.Key; +import google.registry.util.Clock; import google.registry.util.GoogleCredentialsBundle; import java.io.ByteArrayInputStream; import java.io.IOException; @@ -29,6 +32,7 @@ import java.io.UncheckedIOException; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.time.Duration; import javax.inject.Qualifier; import javax.inject.Singleton; @@ -158,6 +162,48 @@ public abstract class CredentialModule { .createScoped(requiredScopes)); } + /** + * Provides a {@link GoogleCredentialsBundle} with delegated access to Google Workspace APIs for + * the application default credential user. + * + *

The Workspace domain must grant delegated admin access to the default service account user + * (project-id@appspot.gserviceaccount.com on AppEngine) with all scopes in {@code defaultScopes} + * and {@code delegationScopes}. + */ + @AdcDelegatedCredential + @Provides + @Singleton + public static GoogleCredentialsBundle provideSelfSignedDelegatedCredential( + @Config("defaultCredentialOauthScopes") ImmutableList defaultScopes, + @Config("delegatedCredentialOauthScopes") ImmutableList delegationScopes, + @ApplicationDefaultCredential GoogleCredentialsBundle credentialsBundle, + @Config("gSuiteAdminAccountEmailAddress") String gSuiteAdminAccountEmailAddress, + @Config("tokenRefreshDelay") Duration tokenRefreshDelay, + Clock clock) { + GoogleCredentials signer = credentialsBundle.getGoogleCredentials(); + + checkArgument( + signer instanceof ServiceAccountSigner, + "Expecting a ServiceAccountSigner, found %s.", + signer.getClass().getSimpleName()); + + try { + // Refreshing as sanity check on the ADC. + signer.refresh(); + } catch (IOException e) { + throw new RuntimeException("Cannot refresh the ApplicationDefaultCredential", e); + } + + DelegatedCredentials credential = + DelegatedCredentials.createSelfSignedDelegatedCredential( + (ServiceAccountSigner) signer, + ImmutableList.builder().addAll(defaultScopes).addAll(delegationScopes).build(), + gSuiteAdminAccountEmailAddress, + clock, + tokenRefreshDelay); + return GoogleCredentialsBundle.create(credential); + } + /** Dagger qualifier for the scope-less Application Default Credential. */ @Qualifier @Documented @@ -195,6 +241,15 @@ public abstract class CredentialModule { @Retention(RetentionPolicy.RUNTIME) public @interface DelegatedCredential {} + /** + * Dagger qualifier for a credential with delegated admin access for a dasher domain (for Google + * Workspace) backed by the application default credential (ADC). + */ + @Qualifier + @Documented + @Retention(RetentionPolicy.RUNTIME) + public @interface AdcDelegatedCredential {} + /** Dagger qualifier for the local credential used in the nomulus tool. */ @Qualifier @Documented diff --git a/core/src/main/java/google/registry/config/DelegatedCredentials.java b/core/src/main/java/google/registry/config/DelegatedCredentials.java new file mode 100644 index 000000000..ce7902df6 --- /dev/null +++ b/core/src/main/java/google/registry/config/DelegatedCredentials.java @@ -0,0 +1,268 @@ +// Copyright 2022 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.config; + +import static com.google.common.base.Preconditions.checkArgument; + +import com.google.api.client.http.GenericUrl; +import com.google.api.client.http.HttpBackOffIOExceptionHandler; +import com.google.api.client.http.HttpBackOffUnsuccessfulResponseHandler; +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.HttpTransport; +import com.google.api.client.http.UrlEncodedContent; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.JsonObjectParser; +import com.google.api.client.json.gson.GsonFactory; +import com.google.api.client.json.webtoken.JsonWebSignature; +import com.google.api.client.json.webtoken.JsonWebToken; +import com.google.api.client.util.ExponentialBackOff; +import com.google.api.client.util.GenericData; +import com.google.api.client.util.StringUtils; +import com.google.auth.ServiceAccountSigner; +import com.google.auth.http.HttpTransportFactory; +import com.google.auth.oauth2.AccessToken; +import com.google.auth.oauth2.GoogleCredentials; +import com.google.common.base.Joiner; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import google.registry.util.Clock; +import java.io.IOException; +import java.math.BigDecimal; +import java.time.Duration; +import java.util.Collection; +import java.util.Date; +import java.util.Map; +import java.util.ServiceLoader; +import org.apache.commons.codec.binary.Base64; + +/** + * OAuth2 credentials for accessing Google Workspace APIs with domain-wide delegation. It fetches + * access tokens using JSON Web Tokens (JWT) signed by a user-provided {@link ServiceAccountSigner}. + * + *

This class accepts the application-default-credential as {@code ServiceAccountSigner}, + * avoiding the need for exported private keys. In this case, the default credential user itself + * (project-id@appspot.gserviceaccount.com on AppEngine) must have domain-wide delegation to the + * Workspace APIs. The default credential user also must have the Token Creator role to itself. + * + *

If the user provides a credential {@code S} that carries its own private key, such as {@link + * com.google.auth.oauth2.ServiceAccountCredentials}, this class can use {@code S} to impersonate + * another service account {@code D} and gain delegated access as {@code D}, as long as S has the + * Token Creator role to {@code D}. This usage is documented here for future reference. + * + *

As of October 2022, the functionalities described above are not implemented in the GCP Java + * Auth library, although they are available in the Python library. We have filed a feature request. + * This class is a stop-gap implementation. + * + *

The main body of this class is adapted from {@link + * com.google.auth.oauth2.ServiceAccountCredentials} with cosmetic changes. The important changes + * include the removal of all uses of the private key and the signing of the JWT (in {@link + * #signAssertion}). We choose not to extend {@code ServiceAccountCredentials} because it would add + * dependency to the non-public details of that class. + */ +public class DelegatedCredentials extends GoogleCredentials { + + private static final long serialVersionUID = 617127523756785546L; + + private static final String DEFAULT_TOKEN_URI = "https://accounts.google.com/o/oauth2/token"; + private static final String GRANT_TYPE = "urn:ietf:params:oauth:grant-type:jwt-bearer"; + + private static final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance(); + private static final HttpTransport HTTP_TRANSPORT = new NetHttpTransport(); + + private static final String VALUE_NOT_FOUND_MESSAGE = "%sExpected value %s not found."; + private static final String VALUE_WRONG_TYPE_MESSAGE = "%sExpected %s value %s of wrong type."; + private static final String PARSE_ERROR_PREFIX = "Error parsing token refresh response. "; + + private static final Duration MAX_TOKEN_REFRESH_DELAY = Duration.ofHours(1); + + private final ServiceAccountSigner signer; + private final String delegatedServiceAccountUser; + private final ImmutableList scopes; + private final String delegatingUserEmail; + + private final Clock clock; + private final Duration tokenRefreshDelay; + + private final HttpTransportFactory transportFactory; + + /** + * Creates a {@link DelegatedCredentials} instance that is self-signed by the signer, which must + * have delegated access to the Workspace APIs. + * + * @param signer Signs for the generated JWT tokens. This may be the application default + * credential + * @param scopes The scopes to use when generating JWT tokens + * @param delegatingUserEmail The Workspace user whose permissions are delegated to the signer + * @param clock Used for setting token expiration times. + * @param tokenRefreshDelay The lifetime of each token. Should not exceed one hour according to + * GCP recommendations. + * @return + */ + static DelegatedCredentials createSelfSignedDelegatedCredential( + ServiceAccountSigner signer, + Collection scopes, + String delegatingUserEmail, + Clock clock, + Duration tokenRefreshDelay) { + return new DelegatedCredentials( + signer, signer.getAccount(), scopes, delegatingUserEmail, clock, tokenRefreshDelay); + } + + private DelegatedCredentials( + ServiceAccountSigner signer, + String delegatedServiceAccountUser, + Collection scopes, + String delegatingUserEmail, + Clock clock, + Duration tokenRefreshDelay) { + checkArgument( + tokenRefreshDelay.getSeconds() <= MAX_TOKEN_REFRESH_DELAY.getSeconds(), + "Max refresh delay must not exceed %s.", + MAX_TOKEN_REFRESH_DELAY); + + this.signer = signer; + this.delegatedServiceAccountUser = delegatedServiceAccountUser; + this.scopes = ImmutableList.copyOf(scopes); + this.delegatingUserEmail = delegatingUserEmail; + + this.clock = clock; + this.tokenRefreshDelay = tokenRefreshDelay; + + this.transportFactory = + getFromServiceLoader( + HttpTransportFactory.class, DelegatedCredentials::provideHttpTransport); + } + + /** + * Refreshes the OAuth2 access token by getting a new access token using a JSON Web Token (JWT). + */ + @Override + public AccessToken refreshAccessToken() throws IOException { + JsonFactory jsonFactory = JSON_FACTORY; + long currentTime = clock.nowUtc().getMillis(); + String assertion = createAssertion(jsonFactory, currentTime); + + GenericData tokenRequest = new GenericData(); + tokenRequest.set("grant_type", GRANT_TYPE); + tokenRequest.set("assertion", assertion); + UrlEncodedContent content = new UrlEncodedContent(tokenRequest); + + HttpRequestFactory requestFactory = transportFactory.create().createRequestFactory(); + HttpRequest request = + requestFactory.buildPostRequest(new GenericUrl(DEFAULT_TOKEN_URI), content); + request.setParser(new JsonObjectParser(jsonFactory)); + + request.setIOExceptionHandler(new HttpBackOffIOExceptionHandler(new ExponentialBackOff())); + request.setUnsuccessfulResponseHandler( + new HttpBackOffUnsuccessfulResponseHandler(new ExponentialBackOff()) + .setBackOffRequired( + response -> { + int code = response.getStatusCode(); + return ( + // Server error --- includes timeout errors, which use 500 instead of 408 + code / 100 == 5 + // Forbidden error --- for historical reasons, used for rate_limit_exceeded + // errors instead of 429, but there currently seems no robust automatic way + // to + // distinguish these cases: see + // https://github.com/google/google-api-java-client/issues/662 + || code == 403); + })); + + HttpResponse response; + try { + response = request.execute(); + } catch (IOException e) { + throw new IOException( + String.format("Error getting access token for service account: %s", e.getMessage()), e); + } + + GenericData responseData = response.parseAs(GenericData.class); + String accessToken = validateString(responseData, "access_token", PARSE_ERROR_PREFIX); + int expiresInSeconds = validateInt32(responseData, "expires_in", PARSE_ERROR_PREFIX); + long expiresAtMilliseconds = clock.nowUtc().getMillis() + expiresInSeconds * 1000L; + return new AccessToken(accessToken, new Date(expiresAtMilliseconds)); + } + + String createAssertion(JsonFactory jsonFactory, long currentTime) throws IOException { + JsonWebSignature.Header header = new JsonWebSignature.Header(); + header.setAlgorithm("RS256"); + header.setType("JWT"); + + JsonWebToken.Payload payload = new JsonWebToken.Payload(); + payload.setIssuer(this.delegatedServiceAccountUser); + payload.setIssuedAtTimeSeconds(currentTime / 1000); + payload.setExpirationTimeSeconds(currentTime / 1000 + tokenRefreshDelay.getSeconds()); + payload.setSubject(delegatingUserEmail); + payload.put("scope", Joiner.on(' ').join(scopes)); + payload.setAudience(DEFAULT_TOKEN_URI); + + return signAssertion(jsonFactory, header, payload); + } + + String signAssertion( + JsonFactory jsonFactory, JsonWebSignature.Header header, JsonWebToken.Payload payload) + throws IOException { + String content = + Base64.encodeBase64URLSafeString(jsonFactory.toByteArray(header)) + + "." + + Base64.encodeBase64URLSafeString(jsonFactory.toByteArray(payload)); + byte[] contentBytes = StringUtils.getBytesUtf8(content); + byte[] signature = signer.sign(contentBytes); // Changed from ServiceAccountCredentials. + return content + "." + Base64.encodeBase64URLSafeString(signature); + } + + static HttpTransport provideHttpTransport() { + return HTTP_TRANSPORT; + } + + protected static T getFromServiceLoader(Class clazz, T defaultInstance) { + return Iterables.getFirst(ServiceLoader.load(clazz), defaultInstance); + } + + /** Return the specified string from JSON or throw a helpful error message. */ + static String validateString(Map map, String key, String errorPrefix) + throws IOException { + Object value = map.get(key); + if (value == null) { + throw new IOException(String.format(VALUE_NOT_FOUND_MESSAGE, errorPrefix, key)); + } + if (!(value instanceof String)) { + throw new IOException(String.format(VALUE_WRONG_TYPE_MESSAGE, errorPrefix, "string", key)); + } + return (String) value; + } + + /** Return the specified integer from JSON or throw a helpful error message. */ + static int validateInt32(Map map, String key, String errorPrefix) + throws IOException { + Object value = map.get(key); + if (value == null) { + throw new IOException(String.format(VALUE_NOT_FOUND_MESSAGE, errorPrefix, key)); + } + if (value instanceof BigDecimal) { + BigDecimal bigDecimalValue = (BigDecimal) value; + return bigDecimalValue.intValueExact(); + } + if (!(value instanceof Integer)) { + throw new IOException(String.format(VALUE_WRONG_TYPE_MESSAGE, errorPrefix, "integer", key)); + } + return (Integer) value; + } +} diff --git a/core/src/main/java/google/registry/config/RegistryConfig.java b/core/src/main/java/google/registry/config/RegistryConfig.java index 53459d42e..cd162f0b7 100644 --- a/core/src/main/java/google/registry/config/RegistryConfig.java +++ b/core/src/main/java/google/registry/config/RegistryConfig.java @@ -1223,6 +1223,12 @@ public final class RegistryConfig { return ImmutableList.copyOf(config.credentialOAuth.localCredentialOauthScopes); } + @Provides + @Config("tokenRefreshDelay") + public static java.time.Duration provideTokenRefreshDelay(RegistryConfigSettings config) { + return java.time.Duration.ofSeconds(config.credentialOAuth.tokenRefreshDelaySeconds); + } + /** OAuth client ID used by the nomulus tool. */ @Provides @Config("toolsClientId") diff --git a/core/src/main/java/google/registry/config/RegistryConfigSettings.java b/core/src/main/java/google/registry/config/RegistryConfigSettings.java index e9ab135f7..94e77240b 100644 --- a/core/src/main/java/google/registry/config/RegistryConfigSettings.java +++ b/core/src/main/java/google/registry/config/RegistryConfigSettings.java @@ -67,6 +67,7 @@ public class RegistryConfigSettings { public List defaultCredentialOauthScopes; public List delegatedCredentialOauthScopes; public List localCredentialOauthScopes; + public int tokenRefreshDelaySeconds; } /** Configuration options for the G Suite account used by Nomulus. */ diff --git a/core/src/main/java/google/registry/config/files/default-config.yaml b/core/src/main/java/google/registry/config/files/default-config.yaml index e43252e60..7e81620f3 100644 --- a/core/src/main/java/google/registry/config/files/default-config.yaml +++ b/core/src/main/java/google/registry/config/files/default-config.yaml @@ -340,6 +340,9 @@ credentialOAuth: - https://www.googleapis.com/auth/userinfo.email # View and manage your applications deployed on Google App Engine - https://www.googleapis.com/auth/appengine.admin + # The lifetime of an access token generated by our custom credentials classes + # Must be shorter than one hour. + tokenRefreshDelaySeconds: 1800 icannReporting: # URL we PUT monthly ICANN transactions reports to. diff --git a/core/src/main/java/google/registry/env/common/backend/WEB-INF/web.xml b/core/src/main/java/google/registry/env/common/backend/WEB-INF/web.xml index 50a6443ca..8ffdfa4a4 100644 --- a/core/src/main/java/google/registry/env/common/backend/WEB-INF/web.xml +++ b/core/src/main/java/google/registry/env/common/backend/WEB-INF/web.xml @@ -293,6 +293,12 @@ have been in the database for a certain period of time. --> /_dr/task/wipeOutCloudSql + + + backend-servlet + /_dr/task/executeCannedScript + + diff --git a/core/src/main/java/google/registry/module/backend/BackendRequestComponent.java b/core/src/main/java/google/registry/module/backend/BackendRequestComponent.java index 6e544ea01..dca1f8170 100644 --- a/core/src/main/java/google/registry/module/backend/BackendRequestComponent.java +++ b/core/src/main/java/google/registry/module/backend/BackendRequestComponent.java @@ -17,6 +17,7 @@ package google.registry.module.backend; import dagger.Module; import dagger.Subcomponent; import google.registry.batch.BatchModule; +import google.registry.batch.CannedScriptExecutionAction; import google.registry.batch.DeleteExpiredDomainsAction; import google.registry.batch.DeleteLoadTestDataAction; import google.registry.batch.DeleteProberDataAction; @@ -102,6 +103,8 @@ interface BackendRequestComponent { BrdaCopyAction brdaCopyAction(); + CannedScriptExecutionAction cannedScriptExecutionAction(); + CopyDetailReportsAction copyDetailReportAction(); DeleteExpiredDomainsAction deleteExpiredDomainsAction(); diff --git a/core/src/test/resources/google/registry/module/backend/backend_routing.txt b/core/src/test/resources/google/registry/module/backend/backend_routing.txt index 3486de387..e2b2e43c9 100644 --- a/core/src/test/resources/google/registry/module/backend/backend_routing.txt +++ b/core/src/test/resources/google/registry/module/backend/backend_routing.txt @@ -7,6 +7,7 @@ PATH CLASS /_dr/task/deleteExpiredDomains DeleteExpiredDomainsAction GET n INTERNAL,API APP ADMIN /_dr/task/deleteLoadTestData DeleteLoadTestDataAction POST n INTERNAL,API APP ADMIN /_dr/task/deleteProberData DeleteProberDataAction POST n INTERNAL,API APP ADMIN +/_dr/task/executeCannedScript CannedScriptExecutionAction POST y INTERNAL,API APP ADMIN /_dr/task/expandRecurringBillingEvents ExpandRecurringBillingEventsAction GET n INTERNAL,API APP ADMIN /_dr/task/exportDomainLists ExportDomainListsAction POST n INTERNAL,API APP ADMIN /_dr/task/exportPremiumTerms ExportPremiumTermsAction POST n INTERNAL,API APP ADMIN