Implement Keyless Delegated credential (#1836)

Add a implementation of Delegated credential without using downloaded private key.

This is a stop-gap implementation while waiting for a solution from the Java auth library.

Also added a verifier action to test the new credential in production. Testing is helpful because:

Configuration is per-environment, therefore, success in alpha does not fully validate prod.
The relevant use case is triggered by low-frequency activities. Problem may not pop out for hours or longer.
This commit is contained in:
Weimin Yu 2022-11-03 11:45:23 -04:00 committed by GitHub
parent 25bdf5bee4
commit 2812df303d
11 changed files with 547 additions and 0 deletions

View file

@ -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_ACTIONS;
import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_DELETE; import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_DELETE;
import static google.registry.batch.AsyncTaskEnqueuer.QUEUE_ASYNC_HOST_RENAME; 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.extractBooleanParameter;
import static google.registry.request.RequestParameters.extractIntParameter; import static google.registry.request.RequestParameters.extractIntParameter;
import static google.registry.request.RequestParameters.extractLongParameter; import static google.registry.request.RequestParameters.extractLongParameter;
@ -145,4 +146,11 @@ public class BatchModule {
static Queue provideAsyncHostRenamePullQueue() { static Queue provideAsyncHostRenamePullQueue() {
return getQueue(QUEUE_ASYNC_HOST_RENAME); 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);
}
} }

View file

@ -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.
*
* <p>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.
*
* <p>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<String, Runnable> 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.");
}
}
}

View file

@ -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<GroupsConnectionComponent> COMPONENT_SUPPLIER =
Suppliers.memoize(DaggerGroupsApiChecker_GroupsConnectionComponent::create);
public static void runGroupsApiChecks() {
GroupsConnectionComponent component = COMPONENT_SUPPLIER.get();
DirectoryGroupsConnection groupsConnection = component.groupsConnection();
List<Registrar> 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<String> 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();
}
}
}

View file

@ -14,14 +14,17 @@
package google.registry.config; package google.registry.config;
import static com.google.common.base.Preconditions.checkArgument;
import static java.nio.charset.StandardCharsets.UTF_8; import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.auth.ServiceAccountSigner;
import com.google.auth.oauth2.GoogleCredentials; import com.google.auth.oauth2.GoogleCredentials;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import dagger.Module; import dagger.Module;
import dagger.Provides; import dagger.Provides;
import google.registry.config.RegistryConfig.Config; import google.registry.config.RegistryConfig.Config;
import google.registry.keyring.api.KeyModule.Key; import google.registry.keyring.api.KeyModule.Key;
import google.registry.util.Clock;
import google.registry.util.GoogleCredentialsBundle; import google.registry.util.GoogleCredentialsBundle;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
@ -29,6 +32,7 @@ import java.io.UncheckedIOException;
import java.lang.annotation.Documented; import java.lang.annotation.Documented;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.time.Duration;
import javax.inject.Qualifier; import javax.inject.Qualifier;
import javax.inject.Singleton; import javax.inject.Singleton;
@ -158,6 +162,48 @@ public abstract class CredentialModule {
.createScoped(requiredScopes)); .createScoped(requiredScopes));
} }
/**
* Provides a {@link GoogleCredentialsBundle} with delegated access to Google Workspace APIs for
* the application default credential user.
*
* <p>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<String> defaultScopes,
@Config("delegatedCredentialOauthScopes") ImmutableList<String> 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.<String>builder().addAll(defaultScopes).addAll(delegationScopes).build(),
gSuiteAdminAccountEmailAddress,
clock,
tokenRefreshDelay);
return GoogleCredentialsBundle.create(credential);
}
/** Dagger qualifier for the scope-less Application Default Credential. */ /** Dagger qualifier for the scope-less Application Default Credential. */
@Qualifier @Qualifier
@Documented @Documented
@ -195,6 +241,15 @@ public abstract class CredentialModule {
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)
public @interface DelegatedCredential {} 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. */ /** Dagger qualifier for the local credential used in the nomulus tool. */
@Qualifier @Qualifier
@Documented @Documented

View file

@ -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}.
*
* <p>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.
*
* <p>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.
*
* <p>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 <a
* href="https://github.com/googleapis/google-auth-library-java/issues/1064">feature request</a>.
* This class is a stop-gap implementation.
*
* <p>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<String> 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<String> scopes,
String delegatingUserEmail,
Clock clock,
Duration tokenRefreshDelay) {
return new DelegatedCredentials(
signer, signer.getAccount(), scopes, delegatingUserEmail, clock, tokenRefreshDelay);
}
private DelegatedCredentials(
ServiceAccountSigner signer,
String delegatedServiceAccountUser,
Collection<String> 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> T getFromServiceLoader(Class<? extends T> 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<String, Object> 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<String, Object> 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;
}
}

View file

@ -1223,6 +1223,12 @@ public final class RegistryConfig {
return ImmutableList.copyOf(config.credentialOAuth.localCredentialOauthScopes); 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. */ /** OAuth client ID used by the nomulus tool. */
@Provides @Provides
@Config("toolsClientId") @Config("toolsClientId")

View file

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

View file

@ -340,6 +340,9 @@ credentialOAuth:
- https://www.googleapis.com/auth/userinfo.email - https://www.googleapis.com/auth/userinfo.email
# View and manage your applications deployed on Google App Engine # View and manage your applications deployed on Google App Engine
- https://www.googleapis.com/auth/appengine.admin - 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: icannReporting:
# URL we PUT monthly ICANN transactions reports to. # URL we PUT monthly ICANN transactions reports to.

View file

@ -293,6 +293,12 @@ have been in the database for a certain period of time. -->
<url-pattern>/_dr/task/wipeOutCloudSql</url-pattern> <url-pattern>/_dr/task/wipeOutCloudSql</url-pattern>
</servlet-mapping> </servlet-mapping>
<!-- Action to execute canned scripts -->
<servlet-mapping>
<servlet-name>backend-servlet</servlet-name>
<url-pattern>/_dr/task/executeCannedScript</url-pattern>
</servlet-mapping>
<!-- Security config --> <!-- Security config -->
<security-constraint> <security-constraint>
<web-resource-collection> <web-resource-collection>

View file

@ -17,6 +17,7 @@ package google.registry.module.backend;
import dagger.Module; import dagger.Module;
import dagger.Subcomponent; import dagger.Subcomponent;
import google.registry.batch.BatchModule; import google.registry.batch.BatchModule;
import google.registry.batch.CannedScriptExecutionAction;
import google.registry.batch.DeleteExpiredDomainsAction; import google.registry.batch.DeleteExpiredDomainsAction;
import google.registry.batch.DeleteLoadTestDataAction; import google.registry.batch.DeleteLoadTestDataAction;
import google.registry.batch.DeleteProberDataAction; import google.registry.batch.DeleteProberDataAction;
@ -102,6 +103,8 @@ interface BackendRequestComponent {
BrdaCopyAction brdaCopyAction(); BrdaCopyAction brdaCopyAction();
CannedScriptExecutionAction cannedScriptExecutionAction();
CopyDetailReportsAction copyDetailReportAction(); CopyDetailReportsAction copyDetailReportAction();
DeleteExpiredDomainsAction deleteExpiredDomainsAction(); DeleteExpiredDomainsAction deleteExpiredDomainsAction();

View file

@ -7,6 +7,7 @@ PATH CLASS
/_dr/task/deleteExpiredDomains DeleteExpiredDomainsAction GET n INTERNAL,API APP ADMIN /_dr/task/deleteExpiredDomains DeleteExpiredDomainsAction GET n INTERNAL,API APP ADMIN
/_dr/task/deleteLoadTestData DeleteLoadTestDataAction POST 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/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/expandRecurringBillingEvents ExpandRecurringBillingEventsAction GET n INTERNAL,API APP ADMIN
/_dr/task/exportDomainLists ExportDomainListsAction POST 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 /_dr/task/exportPremiumTerms ExportPremiumTermsAction POST n INTERNAL,API APP ADMIN