Compare commits

...
Author SHA1 Message Date
Weimin YuandGitHub 9e3c58989a Add an IDN helper (#2217)
* Add an IDN helper

Add a helper that checks the validity of labels in IDNs.
All organizes TLDs according to the IDNs they support.
2023-11-10 19:55:04 -05:00
Lai JiangandGitHub cf9c1ec7c3 Use Java 8 runtime on sandbox and production (#2218)
Java 17 injects unexpected headers to X-Forwarded-For, which causes
issues with validating incoming IP addresses.

This is a partial reversion of #2201. We are still keeping Java 17 in other environment but sandbox and production needs to be able to parse the header to accept incoming EPP connections from registrars. Once we fix it we will re-enable Java 17 in these environment.
2023-11-10 14:39:16 -05:00
Pavlo TkachandGitHub 69ea87be31 Add handler for Console API requests and XSRF token creation and verification (#2211) 2023-11-09 17:51:53 -05:00
Lai JiangandGitHub 779d0c9d37 Add a fallback token verifier (#2216)
This allows us to switch the proxy to a different client ID without
disrupting the service. This is a temporary measure and will be removed
once the switch is complete.
2023-11-09 16:05:14 -05:00
Weimin YuandGitHub 2855944214 Add TLD BSA enroll start date to schema (#2215)
Also adds a placeholder getter in the Tld class, so that it can be
mocked/spied in tests. This way more BSA related code can be submitted
before the schema is deployed to prod.
2023-11-09 13:52:45 -05:00
Ben McIlwainandGitHub 992d1c1349 Reduce the QPS of the refresh DNS for all domains action (#2212)
This also adds a targeted QPS as a parameter in case we need to manually bump it
up (or down) for some reason without having to make code changes and re-deploy.
2023-11-08 13:47:37 -05:00
Pavlo TkachandGitHub f50290ce1d Add static IP connector to crash and alpha configs (#2213) 2023-11-08 13:26:32 -05:00
Pavlo TkachandGitHub e647d4e215 Add retry to cloud build node installation (#2210) 2023-11-06 09:15:36 -05:00
44 changed files with 2876 additions and 2235 deletions
@@ -0,0 +1,108 @@
// 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.bsa;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.Maps.transformValues;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.collect.Sets.SetView;
import google.registry.model.tld.Tld;
import google.registry.model.tld.Tld.TldType;
import google.registry.model.tld.Tlds;
import google.registry.tldconfig.idn.IdnLabelValidator;
import google.registry.tldconfig.idn.IdnTableEnum;
import google.registry.util.Clock;
import javax.inject.Inject;
import org.joda.time.DateTime;
/**
* Checks labels' validity wrt Idns in TLDs enrolled with BSA.
*
* <p>Each instance takes a snapshot of the TLDs at instantiation time, and should be limited to the
* Request scope.
*/
public class IdnChecker {
private static final IdnLabelValidator IDN_LABEL_VALIDATOR = new IdnLabelValidator();
private final ImmutableMap<IdnTableEnum, ImmutableSet<Tld>> idnToTlds;
private final ImmutableSet<Tld> allTlds;
@Inject
IdnChecker(Clock clock) {
this.idnToTlds = getIdnToTldMap(clock.nowUtc());
allTlds = idnToTlds.values().stream().flatMap(ImmutableSet::stream).collect(toImmutableSet());
}
// TODO(11/30/2023): Remove below when new Tld schema is deployed and the `getBsaEnrollStartTime`
// method is no longer hardcoded.
@VisibleForTesting
IdnChecker(ImmutableMap<IdnTableEnum, ImmutableSet<Tld>> idnToTlds) {
this.idnToTlds = idnToTlds;
allTlds = idnToTlds.values().stream().flatMap(ImmutableSet::stream).collect(toImmutableSet());
}
/** Returns all IDNs in which the {@code label} is valid. */
ImmutableSet<IdnTableEnum> getAllValidIdns(String label) {
return idnToTlds.keySet().stream()
.filter(idnTable -> idnTable.getTable().isValidLabel(label))
.collect(toImmutableSet());
}
/**
* Returns the TLDs that support at least one IDN in the {@code idnTables}.
*
* @param idnTables String names of {@link IdnTableEnum} values
*/
public ImmutableSet<Tld> getSupportingTlds(ImmutableSet<String> idnTables) {
return idnTables.stream()
.map(IdnTableEnum::valueOf)
.filter(idnToTlds::containsKey)
.map(idnToTlds::get)
.flatMap(ImmutableSet::stream)
.collect(toImmutableSet());
}
/**
* Returns the TLDs that do not support any IDN in the {@code idnTables}.
*
* @param idnTables String names of {@link IdnTableEnum} values
*/
public SetView<Tld> getForbiddingTlds(ImmutableSet<String> idnTables) {
return Sets.difference(allTlds, getSupportingTlds(idnTables));
}
private static boolean isEnrolledWithBsa(Tld tld, DateTime now) {
DateTime enrollTime = tld.getBsaEnrollStartTime();
return enrollTime != null && enrollTime.isBefore(now);
}
private static ImmutableMap<IdnTableEnum, ImmutableSet<Tld>> getIdnToTldMap(DateTime now) {
ImmutableMultimap.Builder<IdnTableEnum, Tld> idnToTldMap = new ImmutableMultimap.Builder();
Tlds.getTldEntitiesOfType(TldType.REAL).stream()
.filter(tld -> isEnrolledWithBsa(tld, now))
.forEach(
tld -> {
for (IdnTableEnum idn : IDN_LABEL_VALIDATOR.getIdnTablesForTld(tld)) {
idnToTldMap.put(idn, tld);
}
});
return ImmutableMap.copyOf(transformValues(idnToTldMap.build().asMap(), ImmutableSet::copyOf));
}
}
@@ -1192,6 +1192,12 @@ public final class RegistryConfig {
return config.auth.oauthClientId;
}
@Provides
@Config("fallbackOauthClientId")
public static String provideFallbackOauthClientId(RegistryConfigSettings config) {
return config.auth.fallbackOauthClientId;
}
/**
* Provides the OAuth scopes required for accessing Google APIs using the default credential.
*/
@@ -61,6 +61,7 @@ public class RegistryConfigSettings {
public static class Auth {
public List<String> allowedServiceAccountEmails;
public String oauthClientId;
public String fallbackOauthClientId;
}
/** Configuration options for accessing Google APIs. */
@@ -321,6 +321,10 @@ auth:
# the same as this one.
oauthClientId: iap-oauth-clientid
# Same as above, but serve as a fallback, so we can switch the client ID of
# the proxy without downtime.
fallbackOauthClientId: fallback-oauth-clientid
credentialOAuth:
# OAuth scopes required for accessing Google APIs using the default
# credential.
@@ -18,6 +18,13 @@
value="alpha"/>
</system-properties>
<!-- Enable external traffic to go through VPC, required for static ip -->
<vpc-access-connector>
<name>projects/domain-registry-alpha/locations/us-central1/connectors/appengine-connector</name>
<egress-setting>all-traffic</egress-setting>
</vpc-access-connector>
<static-files>
<include path="/*.html" expiration="1m"/>
</static-files>
@@ -18,6 +18,12 @@
value="crash"/>
</system-properties>
<!-- Enable external traffic to go through VPC, required for static ip -->
<vpc-access-connector>
<name>projects/domain-registry-crash/locations/us-central1/connectors/appengine-connector</name>
<egress-setting>all-traffic</egress-setting>
</vpc-access-connector>
<static-files>
<include path="/*.html" expiration="1m"/>
</static-files>
@@ -1,9 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java17</runtime>
<runtime>java8</runtime>
<service>backend</service>
<app-engine-apis>true</app-engine-apis>
<!--app-engine-apis>true</app-engine-apis-->
<threadsafe>true</threadsafe>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4_1G</instance-class>
<basic-scaling>
@@ -1,9 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java17</runtime>
<runtime>java8</runtime>
<service>default</service>
<app-engine-apis>true</app-engine-apis>
<!--app-engine-apis>true</app-engine-apis-->
<threadsafe>true</threadsafe>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4_1G</instance-class>
<manual-scaling>
@@ -1,9 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java17</runtime>
<runtime>java8</runtime>
<service>pubapi</service>
<app-engine-apis>true</app-engine-apis>
<!--app-engine-apis>true</app-engine-apis-->
<threadsafe>true</threadsafe>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4_1G</instance-class>
<manual-scaling>
@@ -1,9 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java17</runtime>
<runtime>java8</runtime>
<service>tools</service>
<app-engine-apis>true</app-engine-apis>
<!--app-engine-apis>true</app-engine-apis-->
<threadsafe>true</threadsafe>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4_1G</instance-class>
<basic-scaling>
@@ -22,6 +22,12 @@
<include path="/*.html" expiration="1h"/>
</static-files>
<!-- Enable external traffic to go through VPC, required for static ip -->
<vpc-access-connector>
<name>projects/domain-registry-qa/locations/us-central1/connectors/appengine-connector</name>
<egress-setting>all-traffic</egress-setting>
</vpc-access-connector>
<!-- Prevent uncaught servlet errors from leaking a stack trace. -->
<static-error-handlers>
<handler file="error.html"/>
@@ -1,9 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java17</runtime>
<runtime>java8</runtime>
<service>backend</service>
<app-engine-apis>true</app-engine-apis>
<!--app-engine-apis>true</app-engine-apis-->
<threadsafe>true</threadsafe>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4</instance-class>
<basic-scaling>
@@ -1,9 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java17</runtime>
<runtime>java8</runtime>
<service>default</service>
<app-engine-apis>true</app-engine-apis>
<!--app-engine-apis>true</app-engine-apis-->
<threadsafe>true</threadsafe>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4_1G</instance-class>
<manual-scaling>
@@ -1,9 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java17</runtime>
<runtime>java8</runtime>
<service>pubapi</service>
<app-engine-apis>true</app-engine-apis>
<!--app-engine-apis>true</app-engine-apis-->
<threadsafe>true</threadsafe>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4_1G</instance-class>
<manual-scaling>
@@ -1,9 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<appengine-web-app xmlns="http://appengine.google.com/ns/1.0">
<runtime>java17</runtime>
<runtime>java8</runtime>
<service>tools</service>
<app-engine-apis>true</app-engine-apis>
<!--app-engine-apis>true</app-engine-apis-->
<threadsafe>true</threadsafe>
<sessions-enabled>true</sessions-enabled>
<instance-class>B4</instance-class>
<basic-scaling>
@@ -550,6 +550,10 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
@JsonSerialize(using = SortedEnumSetSerializer.class)
Set<IdnTableEnum> idnTables;
// TODO(11/30/2023): uncomment below two lines
// /** The start time of this TLD's enrollment in the BSA program, if applicable. */
// @JsonIgnore @Nullable DateTime bsaEnrollStartTime;
public String getTldStr() {
return tldStr;
}
@@ -569,6 +573,15 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
return tldType;
}
/** Returns the time when this TLD was enrolled in the Brand Safety Alliance (BSA) program. */
@JsonIgnore // Annotation can be removed once we add the field and annotate it.
@Nullable
public DateTime getBsaEnrollStartTime() {
// TODO(11/30/2023): uncomment below.
// return this.bsaEnrollStartTime;
return null;
}
/** Retrieve whether invoicing is enabled. */
public boolean isInvoicingEnabled() {
return invoicingEnabled;
@@ -939,6 +952,7 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
}
public Builder setReservedListsByName(Set<String> reservedListNames) {
// TODO(b/309175133): forbid if enrolled with BSA
checkArgument(reservedListNames != null, "reservedListNames must not be null");
ImmutableSet.Builder<ReservedList> builder = new ImmutableSet.Builder<>();
for (String reservedListName : reservedListNames) {
@@ -958,6 +972,7 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
}
public Builder setReservedLists(Set<ReservedList> reservedLists) {
// TODO(b/309175133): forbid if enrolled with BSA
checkArgumentNotNull(reservedLists, "reservedLists must not be null");
ImmutableSet.Builder<String> nameBuilder = new ImmutableSet.Builder<>();
for (ReservedList reservedList : reservedLists) {
@@ -1076,6 +1091,7 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
}
public Builder setIdnTables(ImmutableSet<IdnTableEnum> idnTables) {
// TODO(b/309175133): forbid if enrolled with BSA.
getInstance().idnTables = idnTables;
return this;
}
@@ -1085,6 +1101,13 @@ public class Tld extends ImmutableObject implements Buildable, UnsafeSerializabl
return this;
}
public Builder setBsaEnrollStartTime(DateTime enrollTime) {
// TODO(b/309175133): forbid if enrolled with BSA
// TODO(11/30/2023): uncomment below line
// getInstance().bsaEnrollStartTime = enrollTime;
return this;
}
@Override
public Tld build() {
final Tld instance = getInstance();
@@ -15,6 +15,7 @@
package google.registry.request;
import com.google.common.net.MediaType;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import org.joda.time.DateTime;
@@ -51,4 +52,11 @@ public interface Response {
* @see HttpServletResponse#setDateHeader(String, long)
*/
void setDateHeader(String header, DateTime timestamp);
/**
* Adds a cookie to the response
*
* @see HttpServletResponse#addCookie(Cookie)
*/
void addCookie(Cookie cookie);
}
@@ -17,6 +17,7 @@ package google.registry.request;
import com.google.common.net.MediaType;
import java.io.IOException;
import javax.inject.Inject;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;
import org.joda.time.DateTime;
@@ -58,4 +59,9 @@ public final class ResponseImpl implements Response {
public void setDateHeader(String header, DateTime timestamp) {
rsp.setDateHeader(header, timestamp.getMillis());
}
@Override
public void addCookie(Cookie cookie) {
rsp.addCookie(cookie);
}
}
@@ -55,6 +55,9 @@ public class AuthModule {
@Qualifier
@interface RegularOidc {}
@Qualifier
@interface RegularOidcFallback {}
@Provides
@IapOidc
@Singleton
@@ -71,6 +74,14 @@ public class AuthModule {
return TokenVerifier.newBuilder().setAudience(clientId).setIssuer(REGULAR_ISSUER_URL).build();
}
@Provides
@RegularOidcFallback
@Singleton
TokenVerifier provideFallbackRegularTokenVerifier(
@Config("fallbackOauthClientId") String clientId) {
return TokenVerifier.newBuilder().setAudience(clientId).setIssuer(REGULAR_ISSUER_URL).build();
}
@Provides
@IapOidc
@Singleton
@@ -25,6 +25,7 @@ import google.registry.model.console.User;
import google.registry.model.console.UserDao;
import google.registry.request.auth.AuthModule.IapOidc;
import google.registry.request.auth.AuthModule.RegularOidc;
import google.registry.request.auth.AuthModule.RegularOidcFallback;
import google.registry.request.auth.AuthSettings.AuthLevel;
import java.util.Optional;
import javax.annotation.Nullable;
@@ -53,6 +54,8 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
protected final TokenVerifier tokenVerifier;
protected final Optional<TokenVerifier> fallbackTokenVerifier;
protected final TokenExtractor tokenExtractor;
private final ImmutableSet<String> serviceAccountEmails;
@@ -60,9 +63,11 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
protected OidcTokenAuthenticationMechanism(
ImmutableSet<String> serviceAccountEmails,
TokenVerifier tokenVerifier,
@Nullable TokenVerifier fallbackTokenVerifier,
TokenExtractor tokenExtractor) {
this.serviceAccountEmails = serviceAccountEmails;
this.tokenVerifier = tokenVerifier;
this.fallbackTokenVerifier = Optional.ofNullable(fallbackTokenVerifier);
this.tokenExtractor = tokenExtractor;
}
@@ -77,7 +82,7 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
if (rawIdToken == null) {
return AuthResult.NOT_AUTHENTICATED;
}
JsonWebSignature token;
JsonWebSignature token = null;
try {
token = tokenVerifier.verify(rawIdToken);
} catch (Exception e) {
@@ -86,8 +91,25 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
RegistryEnvironment.get().equals(RegistryEnvironment.PRODUCTION)
? "Raw token redacted in prod"
: rawIdToken);
return AuthResult.NOT_AUTHENTICATED;
}
if (token == null) {
if (fallbackTokenVerifier.isPresent()) {
try {
token = fallbackTokenVerifier.get().verify(rawIdToken);
} catch (Exception e) {
logger.atInfo().withCause(e).log(
"Failed OIDC fallback verification attempt:\n%s",
RegistryEnvironment.get().equals(RegistryEnvironment.PRODUCTION)
? "Raw token redacted in prod"
: rawIdToken);
return AuthResult.NOT_AUTHENTICATED;
}
} else {
return AuthResult.NOT_AUTHENTICATED;
}
}
String email = (String) token.getPayload().get("email");
if (email == null) {
logger.atWarning().log("No email address from the OIDC token:\n%s", token.getPayload());
@@ -141,7 +163,7 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
@Config("allowedServiceAccountEmails") ImmutableSet<String> serviceAccountEmails,
@IapOidc TokenVerifier tokenVerifier,
@IapOidc TokenExtractor tokenExtractor) {
super(serviceAccountEmails, tokenVerifier, tokenExtractor);
super(serviceAccountEmails, tokenVerifier, null, tokenExtractor);
}
}
@@ -161,8 +183,9 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
protected RegularOidcAuthenticationMechanism(
@Config("allowedServiceAccountEmails") ImmutableSet<String> serviceAccountEmails,
@RegularOidc TokenVerifier tokenVerifier,
@RegularOidcFallback TokenVerifier fallbackTokenVerifier,
@RegularOidc TokenExtractor tokenExtractor) {
super(serviceAccountEmails, tokenVerifier, tokenExtractor);
super(serviceAccountEmails, tokenVerifier, fallbackTokenVerifier, tokenExtractor);
}
}
}
@@ -34,7 +34,7 @@ import org.joda.time.Duration;
/** Helper class for generating and validate XSRF tokens. */
public final class XsrfTokenManager {
/** HTTP header used for transmitting XSRF tokens. */
/** HTTP header or cookie name used for transmitting XSRF tokens. */
public static final String X_CSRF_TOKEN = "X-CSRF-Token";
/** POST parameter used for transmitting XSRF tokens. */
@@ -38,9 +38,7 @@ public final class IdnLabelValidator {
public Optional<String> findValidIdnTableForTld(String label, String tldStr) {
String unicodeString = Idn.toUnicode(label);
Tld tld = Tld.get(tldStr); // uses the cache
ImmutableSet<IdnTableEnum> idnTablesForTld = tld.getIdnTables();
ImmutableSet<IdnTableEnum> idnTables =
idnTablesForTld.isEmpty() ? DEFAULT_IDN_TABLES : idnTablesForTld;
ImmutableSet<IdnTableEnum> idnTables = getIdnTablesForTld(tld);
for (IdnTableEnum idnTable : idnTables) {
if (idnTable.getTable().isValidLabel(unicodeString)) {
return Optional.of(idnTable.getTable().getName());
@@ -48,4 +46,10 @@ public final class IdnLabelValidator {
}
return Optional.empty();
}
/** Returns the names of the IDN tables supported by a {@code tld}. */
public ImmutableSet<IdnTableEnum> getIdnTablesForTld(Tld tld) {
ImmutableSet<IdnTableEnum> idnTablesForTld = tld.getIdnTables();
return idnTablesForTld.isEmpty() ? DEFAULT_IDN_TABLES : idnTablesForTld;
}
}
@@ -84,7 +84,7 @@ public final class IdnTable {
* Returns true if the given label is valid for this IDN table. A label is considered valid if all
* of its codepoints are in the IDN table.
*/
boolean isValidLabel(String label) {
public boolean isValidLabel(String label) {
final int length = label.length();
for (int i = 0; i < length; ) {
int codepoint = label.codePointAt(i);
@@ -58,13 +58,25 @@ public class RefreshDnsForAllDomainsAction implements Runnable {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
/** The number of DNS updates to enqueue per transaction. */
private static final int DEFAULT_BATCH_SIZE = 250;
/**
* The default number of DNS updates it is safe to execute per minute.
*
* <p>This is mostly a guess based on existing system performance, but the point is to be on the
* safe side and not cause contention with ongoing DNS updates from clients.
*/
private static final int DEFAULT_REFRESH_QPS = 7;
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 int refreshQps;
private final Random random;
@Inject
@@ -72,10 +84,12 @@ public class RefreshDnsForAllDomainsAction implements Runnable {
Response response,
@Parameter(PARAM_TLDS) ImmutableSet<String> tlds,
@Parameter("batchSize") Optional<Integer> batchSize,
@Parameter("refreshQps") Optional<Integer> refreshQps,
Random random) {
this.response = response;
this.tlds = tlds;
this.batchSize = batchSize.orElse(DEFAULT_BATCH_SIZE);
this.refreshQps = refreshQps.orElse(DEFAULT_REFRESH_QPS);
this.random = random;
}
@@ -83,7 +97,7 @@ public class RefreshDnsForAllDomainsAction implements Runnable {
public void run() {
assertTldsExist(tlds);
checkArgument(batchSize > 0, "Must specify a positive number for batch size");
int smearMinutes = tm().transact(this::calculateSmearMinutes, TRANSACTION_REPEATABLE_READ);
Duration smear = tm().transact(this::calculateSmear, TRANSACTION_REPEATABLE_READ);
ImmutableList<String> domainsBatch;
@Nullable String lastInPreviousBatch = null;
@@ -91,17 +105,16 @@ public class RefreshDnsForAllDomainsAction implements Runnable {
Optional<String> lastInPreviousBatchOpt = Optional.ofNullable(lastInPreviousBatch);
domainsBatch =
tm().transact(
() -> refreshBatch(lastInPreviousBatchOpt, smearMinutes),
TRANSACTION_REPEATABLE_READ);
() -> refreshBatch(lastInPreviousBatchOpt, smear), TRANSACTION_REPEATABLE_READ);
lastInPreviousBatch = domainsBatch.isEmpty() ? null : getLast(domainsBatch);
} while (domainsBatch.size() == batchSize);
}
/**
* Calculates the number of smear minutes to enqueue refreshes so that the DNS queue does not get
* Calculates the smear duration to enqueue refreshes so that the DNS queue does not get
* overloaded.
*/
private int calculateSmearMinutes() {
private Duration calculateSmear() {
Long activeDomains =
tm().query(
"SELECT COUNT(*) FROM Domain WHERE tld IN (:tlds) AND deletionTime = :endOfTime",
@@ -109,7 +122,7 @@ public class RefreshDnsForAllDomainsAction implements Runnable {
.setParameter("tlds", tlds)
.setParameter("endOfTime", END_OF_TIME)
.getSingleResult();
return Math.max(activeDomains.intValue() / 1000, 1);
return Duration.standardSeconds(Math.max(activeDomains / refreshQps, 1));
}
private ImmutableList<String> getBatch(Optional<String> lastInPreviousBatch) {
@@ -127,11 +140,12 @@ public class RefreshDnsForAllDomainsAction implements Runnable {
}
@VisibleForTesting
ImmutableList<String> refreshBatch(Optional<String> lastInPreviousBatch, int smearMinutes) {
ImmutableList<String> refreshBatch(Optional<String> lastInPreviousBatch, Duration smear) {
ImmutableList<String> domainBatch = getBatch(lastInPreviousBatch);
try {
// Smear the task execution time over the next N minutes.
requestDomainDnsRefresh(domainBatch, Duration.standardMinutes(random.nextInt(smearMinutes)));
// Smear the task execution time over the next N seconds.
requestDomainDnsRefresh(
domainBatch, Duration.standardSeconds(random.nextInt((int) smear.getStandardSeconds())));
} catch (Throwable t) {
logger.atSevere().withCause(t).log("Error while enqueuing DNS refresh batch");
response.setStatus(HttpStatus.SC_OK);
@@ -81,4 +81,10 @@ public class ToolsServerModule {
static Optional<Integer> provideBatchSize(HttpServletRequest req) {
return extractOptionalIntParameter(req, "batchSize");
}
@Provides
@Parameter("refreshQps")
static Optional<Integer> provideRefreshQps(HttpServletRequest req) {
return extractOptionalIntParameter(req, "refreshQps");
}
}
@@ -0,0 +1,72 @@
// 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.ui.server.console;
import static google.registry.request.Action.Method.GET;
import com.google.api.client.http.HttpStatusCodes;
import google.registry.model.console.User;
import google.registry.security.XsrfTokenManager;
import google.registry.ui.server.registrar.ConsoleApiParams;
import java.util.Arrays;
import java.util.Optional;
import javax.servlet.http.Cookie;
/** Base class for handling Console API requests */
public abstract class ConsoleApiAction implements Runnable {
protected ConsoleApiParams consoleApiParams;
public ConsoleApiAction(ConsoleApiParams consoleApiParams) {
this.consoleApiParams = consoleApiParams;
}
@Override
public final void run() {
// Shouldn't be even possible because of Auth annotations on the various implementing classes
if (!consoleApiParams.authResult().userAuthInfo().get().consoleUser().isPresent()) {
consoleApiParams.response().setStatus(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED);
return;
}
User user = consoleApiParams.authResult().userAuthInfo().get().consoleUser().get();
if (consoleApiParams.request().getMethod().equals(GET.toString())) {
getHandler(user);
} else {
if (verifyXSRF()) {
postHandler(user);
}
}
}
protected void postHandler(User user) {
throw new UnsupportedOperationException("Console API POST handler not implemented");
}
protected void getHandler(User user) {
throw new UnsupportedOperationException("Console API GET handler not implemented");
}
private boolean verifyXSRF() {
Optional<Cookie> maybeCookie =
Arrays.stream(consoleApiParams.request().getCookies())
.filter(c -> XsrfTokenManager.X_CSRF_TOKEN.equals(c.getName()))
.findFirst();
if (!maybeCookie.isPresent()
|| !consoleApiParams.xsrfTokenManager().validateToken(maybeCookie.get().getValue())) {
consoleApiParams.response().setStatus(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED);
return false;
}
return true;
}
}
@@ -21,12 +21,10 @@ import com.google.common.collect.ImmutableMap;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.console.User;
import google.registry.request.Action;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.UserAuthInfo;
import google.registry.ui.server.registrar.JsonGetAction;
import google.registry.ui.server.registrar.ConsoleApiParams;
import javax.inject.Inject;
import javax.servlet.http.Cookie;
import org.json.JSONObject;
@Action(
@@ -34,12 +32,10 @@ import org.json.JSONObject;
path = ConsoleUserDataAction.PATH,
method = {GET},
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public class ConsoleUserDataAction implements JsonGetAction {
public class ConsoleUserDataAction extends ConsoleApiAction {
public static final String PATH = "/console-api/userdata";
private final AuthResult authResult;
private final Response response;
private final String productName;
private final String supportPhoneNumber;
private final String supportEmail;
@@ -47,14 +43,12 @@ public class ConsoleUserDataAction implements JsonGetAction {
@Inject
public ConsoleUserDataAction(
AuthResult authResult,
Response response,
ConsoleApiParams consoleApiParams,
@Config("productName") String productName,
@Config("supportEmail") String supportEmail,
@Config("supportPhoneNumber") String supportPhoneNumber,
@Config("technicalDocsUrl") String technicalDocsUrl) {
this.response = response;
this.authResult = authResult;
super(consoleApiParams);
this.productName = productName;
this.supportEmail = supportEmail;
this.supportPhoneNumber = supportPhoneNumber;
@@ -62,13 +56,15 @@ public class ConsoleUserDataAction implements JsonGetAction {
}
@Override
public void run() {
UserAuthInfo authInfo = authResult.userAuthInfo().get();
if (!authInfo.consoleUser().isPresent()) {
response.setStatus(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED);
return;
}
User user = authInfo.consoleUser().get();
protected void getHandler(User user) {
// As this is a first GET request we use it as an opportunity to set a XSRF cookie
// for angular to read - https://angular.io/guide/http-security-xsrf-protection
Cookie xsrfCookie =
new Cookie(
consoleApiParams.xsrfTokenManager().X_CSRF_TOKEN,
consoleApiParams.xsrfTokenManager().generateToken(user.getEmailAddress()));
xsrfCookie.setSecure(true);
consoleApiParams.response().addCookie(xsrfCookie);
JSONObject json =
new JSONObject(
@@ -90,7 +86,7 @@ public class ConsoleUserDataAction implements JsonGetAction {
// Is used by UI to construct a link to registry resources
"technicalDocsUrl", technicalDocsUrl));
response.setPayload(json.toString());
response.setStatus(HttpStatusCodes.STATUS_CODE_OK);
consoleApiParams.response().setPayload(json.toString());
consoleApiParams.response().setStatus(HttpStatusCodes.STATUS_CODE_OK);
}
}
@@ -0,0 +1,41 @@
// 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.ui.server.registrar;
import com.google.auto.value.AutoValue;
import google.registry.request.Response;
import google.registry.request.auth.AuthResult;
import google.registry.security.XsrfTokenManager;
import javax.servlet.http.HttpServletRequest;
/** Groups necessary dependencies for Console API actions * */
@AutoValue
public abstract class ConsoleApiParams {
public static ConsoleApiParams create(
HttpServletRequest request,
Response response,
AuthResult authResult,
XsrfTokenManager xsrfTokenManager) {
return new AutoValue_ConsoleApiParams(request, response, authResult, xsrfTokenManager);
}
public abstract HttpServletRequest request();
public abstract Response response();
public abstract AuthResult authResult();
public abstract XsrfTokenManager xsrfTokenManager();
}
@@ -28,6 +28,10 @@ import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.request.OptionalJsonPayload;
import google.registry.request.Parameter;
import google.registry.request.RequestScope;
import google.registry.request.Response;
import google.registry.request.auth.AuthResult;
import google.registry.security.XsrfTokenManager;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
import org.joda.time.DateTime;
@@ -35,9 +39,18 @@ import org.joda.time.DateTime;
/** Dagger module for the Registrar Console parameters. */
@Module
public final class RegistrarConsoleModule {
static final String PARAM_CLIENT_ID = "clientId";
@Provides
@RequestScope
ConsoleApiParams provideConsoleApiParams(
HttpServletRequest request,
Response response,
AuthResult authResult,
XsrfTokenManager xsrfTokenManager) {
return ConsoleApiParams.create(request, response, authResult, xsrfTokenManager);
}
@Provides
@Parameter(PARAM_CLIENT_ID)
static Optional<String> provideOptionalClientId(HttpServletRequest req) {
@@ -0,0 +1,94 @@
// 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.bsa;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.tldconfig.idn.IdnTableEnum.EXTENDED_LATIN;
import static google.registry.tldconfig.idn.IdnTableEnum.JA;
import static google.registry.tldconfig.idn.IdnTableEnum.UNCONFUSABLE_LATIN;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import google.registry.model.tld.Tld;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
public class IdnCheckerTest {
@Mock Tld jaonly;
@Mock Tld jandelatin;
@Mock Tld strictlatin;
IdnChecker idnChecker;
@BeforeEach
void setup() {
idnChecker =
new IdnChecker(
ImmutableMap.of(
JA,
ImmutableSet.of(jandelatin, jaonly),
EXTENDED_LATIN,
ImmutableSet.of(jandelatin),
UNCONFUSABLE_LATIN,
ImmutableSet.of(strictlatin)));
}
@Test
void getAllValidIdns_allTlds() {
assertThat(idnChecker.getAllValidIdns("all"))
.containsExactly(EXTENDED_LATIN, JA, UNCONFUSABLE_LATIN);
}
@Test
void getAllValidIdns_notJa() {
assertThat(idnChecker.getAllValidIdns("à")).containsExactly(EXTENDED_LATIN, UNCONFUSABLE_LATIN);
}
@Test
void getAllValidIdns_extendedLatinOnly() {
assertThat(idnChecker.getAllValidIdns("á")).containsExactly(EXTENDED_LATIN);
}
@Test
void getAllValidIdns_jaOnly() {
assertThat(idnChecker.getAllValidIdns("")).containsExactly(JA);
}
@Test
void getAllValidIdns_none() {
assertThat(idnChecker.getAllValidIdns("д")).isEmpty();
}
@Test
void getSupportingTlds_singleTld_success() {
assertThat(idnChecker.getSupportingTlds(ImmutableSet.of("EXTENDED_LATIN")))
.containsExactly(jandelatin);
}
@Test
void getSupportingTlds_multiTld_success() {
assertThat(idnChecker.getSupportingTlds(ImmutableSet.of("JA")))
.containsExactly(jandelatin, jaonly);
}
@Test
void getForbiddingTlds_success() {
assertThat(idnChecker.getForbiddingTlds(ImmutableSet.of("JA"))).containsExactly(strictlatin);
}
}
@@ -19,7 +19,6 @@ import static com.google.common.truth.Truth.assertThat;
import static google.registry.request.auth.AuthModule.BEARER_PREFIX;
import static google.registry.request.auth.AuthModule.IAP_HEADER_NAME;
import static google.registry.testing.DatabaseHelper.insertInDb;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
@@ -70,7 +69,7 @@ public class OidcTokenAuthenticationMechanismTest {
private AuthResult authResult;
private OidcTokenAuthenticationMechanism authenticationMechanism =
new OidcTokenAuthenticationMechanism(serviceAccounts, tokenVerifier, e -> rawToken) {};
new OidcTokenAuthenticationMechanism(serviceAccounts, tokenVerifier, null, e -> rawToken) {};
@RegisterExtension
public final JpaTestExtensions.JpaUnitTestExtension jpaExtension =
@@ -78,7 +77,7 @@ public class OidcTokenAuthenticationMechanismTest {
@BeforeEach
void beforeEach() throws Exception {
when(tokenVerifier.verify(eq(rawToken))).thenReturn(jwt);
when(tokenVerifier.verify(rawToken)).thenReturn(jwt);
payload.setEmail(email);
payload.setSubject(gaiaId);
insertInDb(user);
@@ -98,18 +97,30 @@ public class OidcTokenAuthenticationMechanismTest {
@Test
void testAuthenticate_noTokenFromRequest() {
authenticationMechanism =
new OidcTokenAuthenticationMechanism(serviceAccounts, tokenVerifier, e -> null) {};
new OidcTokenAuthenticationMechanism(serviceAccounts, tokenVerifier, null, e -> null) {};
authResult = authenticationMechanism.authenticate(request);
assertThat(authResult).isEqualTo(AuthResult.NOT_AUTHENTICATED);
}
@Test
void testAuthenticate_invalidToken() throws Exception {
when(tokenVerifier.verify(eq(rawToken))).thenThrow(new VerificationException("Bad token"));
when(tokenVerifier.verify(rawToken)).thenThrow(new VerificationException("Bad token"));
authResult = authenticationMechanism.authenticate(request);
assertThat(authResult).isEqualTo(AuthResult.NOT_AUTHENTICATED);
}
@Test
void testAuthenticate_fallbackVerifier() throws Exception {
TokenVerifier fallbackVerifier = mock(TokenVerifier.class);
when(tokenVerifier.verify(rawToken)).thenThrow(new VerificationException("Bad token"));
when(fallbackVerifier.verify(rawToken)).thenReturn(jwt);
authenticationMechanism =
new OidcTokenAuthenticationMechanism(
serviceAccounts, tokenVerifier, fallbackVerifier, e -> rawToken) {};
authResult = authenticationMechanism.authenticate(request);
assertThat(authResult.isAuthenticated()).isEqualTo(true);
}
@Test
void testAuthenticate_noEmailAddress() throws Exception {
payload.setEmail(null);
@@ -223,5 +234,12 @@ public class OidcTokenAuthenticationMechanismTest {
String provideOauthClientId() {
return "client-id";
}
@Provides
@Singleton
@Config("fallbackOauthClientId")
String provideFallbackOauthClientId() {
return "fallback-client-id";
}
}
}
@@ -0,0 +1,46 @@
// 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.testing;
import static org.mockito.Mockito.mock;
import com.google.appengine.api.users.UserService;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.UserAuthInfo;
import google.registry.security.XsrfTokenManager;
import google.registry.ui.server.registrar.ConsoleApiParams;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
import org.joda.time.DateTime;
public final class FakeConsoleApiParams {
public static ConsoleApiParams get(Optional<AuthResult> maybeAuthResult) {
AuthResult authResult =
maybeAuthResult.orElseGet(
() ->
AuthResult.createUser(
UserAuthInfo.create(
new com.google.appengine.api.users.User(
"JohnDoe@theregistrar.com", "theregistrar.com"),
false)));
return ConsoleApiParams.create(
mock(HttpServletRequest.class),
new FakeResponse(),
authResult,
new XsrfTokenManager(
new FakeClock(DateTime.parse("2020-02-02T01:23:45Z")), mock(UserService.class)));
}
}
@@ -22,8 +22,10 @@ import static java.util.Collections.unmodifiableMap;
import com.google.common.base.Throwables;
import com.google.common.net.MediaType;
import google.registry.request.Response;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.Cookie;
import org.joda.time.DateTime;
/** Fake implementation of {@link Response} for testing. */
@@ -36,6 +38,8 @@ public final class FakeResponse implements Response {
private boolean wasMutuallyExclusiveResponseSet;
private String lastResponseStackTrace;
private ArrayList<Cookie> cookies = new ArrayList<>();
public int getStatus() {
return status;
}
@@ -83,6 +87,15 @@ public final class FakeResponse implements Response {
headers.put(checkNotNull(header), checkNotNull(timestamp));
}
@Override
public void addCookie(Cookie cookie) {
cookies.add(cookie);
}
public ArrayList<Cookie> getCookies() {
return cookies;
}
private void checkResponsePerformedOnce() {
checkState(
!wasMutuallyExclusiveResponseSet,
@@ -14,11 +14,14 @@
package google.registry.tldconfig.idn;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth8.assertThat;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistResource;
import com.google.common.collect.ImmutableSet;
import google.registry.model.tld.Tld;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import org.junit.jupiter.api.Test;
@@ -118,4 +121,24 @@ class IdnLabelValidatorTest {
// Extended Latin shouldn't include Japanese characters
assertThat(idnLabelValidator.findValidIdnTableForTld("みんな", "tld")).isEmpty();
}
@Test
void testGetIdnTablesForTld_custom() {
persistResource(
createTld("tld")
.asBuilder()
.setIdnTables(ImmutableSet.of(IdnTableEnum.EXTENDED_LATIN))
.build());
Tld tld = tm().transact(() -> tm().loadByKey(Tld.createVKey("tld")));
assertThat(idnLabelValidator.getIdnTablesForTld(tld))
.containsExactly(IdnTableEnum.EXTENDED_LATIN);
}
@Test
void testGetIdnTablesForTld_default() {
persistResource(createTld("tld").asBuilder().build());
Tld tld = tm().transact(() -> tm().loadByKey(Tld.createVKey("tld")));
assertThat(idnLabelValidator.getIdnTablesForTld(tld))
.containsExactly(IdnTableEnum.EXTENDED_LATIN, IdnTableEnum.JA);
}
}
@@ -16,6 +16,7 @@ package google.registry.tools;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.EntityYamlUtils.createObjectMapper;
import static google.registry.model.domain.token.AllocationToken.TokenType.DEFAULT_PROMO;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistPremiumList;
import static google.registry.testing.DatabaseHelper.persistResource;
@@ -47,6 +48,8 @@ import google.registry.model.tld.label.PremiumListDao;
import java.io.File;
import java.util.logging.Logger;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
@@ -98,6 +101,20 @@ public class ConfigureTldCommandTest extends CommandTestCase<ConfigureTldCommand
assertThat(updatedTld.getCreateBillingCost()).isEqualTo(Money.of(USD, 25));
testTldConfiguredSuccessfully(updatedTld, "tld.yaml");
assertThat(updatedTld.getBreakglassMode()).isFalse();
assertThat(tld.getBsaEnrollStartTime()).isNull();
}
@Test
void testSuccess_updateTld_bsaTimeUnaffected() throws Exception {
Tld tld = createTld("tld");
DateTime bsaStartTime = DateTime.now(DateTimeZone.UTC);
tm().transact(() -> tm().put(tld.asBuilder().setBsaEnrollStartTime(bsaStartTime).build()));
File tldFile = tmpDir.resolve("tld.yaml").toFile();
Files.asCharSink(tldFile, UTF_8).write(loadFile(getClass(), "tld.yaml"));
runCommandForced("--input=" + tldFile);
// TODO(11/30/2023): uncomment below two lines
// Tld updatedTld = Tld.get("tld");
// assertThat(tld.getBsaEnrollStartTime()).isEqualTo(bsaStartTime);
}
@Test
@@ -35,6 +35,7 @@ import google.registry.testing.FakeResponse;
import java.util.Optional;
import java.util.Random;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
@@ -55,7 +56,7 @@ public class RefreshDnsForAllDomainsActionTest {
createTld("bar");
action =
new RefreshDnsForAllDomainsAction(
response, ImmutableSet.of("bar"), Optional.of(10), new Random());
response, ImmutableSet.of("bar"), Optional.of(10), Optional.empty(), new Random());
}
@Test
@@ -74,9 +75,9 @@ public class RefreshDnsForAllDomainsActionTest {
// 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(Optional.empty(), 1000));
tm().transact(() -> action.refreshBatch(Optional.empty(), 1000));
response, ImmutableSet.of("bar"), Optional.of(1), Optional.of(7), new Random());
tm().transact(() -> action.refreshBatch(Optional.empty(), Duration.standardMinutes(1000)));
tm().transact(() -> action.refreshBatch(Optional.empty(), Duration.standardMinutes(1000)));
ImmutableList<DnsRefreshRequest> refreshRequests =
tm().transact(
() ->
@@ -19,6 +19,7 @@ import static com.google.common.truth.Truth.assertWithMessage;
import com.google.common.collect.ImmutableSet;
import google.registry.request.Action;
import google.registry.request.JsonActionRunner;
import google.registry.ui.server.console.ConsoleApiAction;
import google.registry.ui.server.registrar.HtmlAction;
import google.registry.ui.server.registrar.JsonGetAction;
import io.github.classgraph.ClassGraph;
@@ -34,6 +35,7 @@ final class ActionMembershipTest {
// 1. Extending HtmlAction to signal that we are serving an HTML page
// 2. Extending JsonAction to show that we are serving JSON POST requests
// 3. Extending JsonGetAction to serve JSON GET requests
// 4. Extending ConsoleApiAction to serve JSON requests
ImmutableSet.Builder<String> failingClasses = new ImmutableSet.Builder<>();
try (ScanResult scanResult =
new ClassGraph().enableAnnotationInfo().whitelistPackages("google.registry.ui").scan()) {
@@ -41,7 +43,8 @@ final class ActionMembershipTest {
.getClassesWithAnnotation(Action.class.getName())
.forEach(
classInfo -> {
if (!classInfo.extendsSuperclass(HtmlAction.class.getName())
if (!classInfo.extendsSuperclass(ConsoleApiAction.class.getName())
&& !classInfo.extendsSuperclass(HtmlAction.class.getName())
&& !classInfo.implementsInterface(JsonActionRunner.JsonAction.class.getName())
&& !classInfo.implementsInterface(JsonGetAction.class.getName())) {
failingClasses.add(classInfo.getName());
@@ -14,7 +14,9 @@
package google.registry.ui.server.console;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.Mockito.when;
import com.google.api.client.http.HttpStatusCodes;
import com.google.gson.Gson;
@@ -22,12 +24,18 @@ import google.registry.model.console.GlobalRole;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.request.Action;
import google.registry.request.RequestModule;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.UserAuthInfo;
import google.registry.testing.FakeConsoleApiParams;
import google.registry.testing.FakeResponse;
import google.registry.ui.server.registrar.ConsoleApiParams;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Map;
import java.util.Optional;
import javax.servlet.http.Cookie;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
@@ -35,12 +43,31 @@ import org.junit.jupiter.api.extension.RegisterExtension;
class ConsoleUserDataActionTest {
private static final Gson GSON = RequestModule.provideGson();
private FakeResponse response = new FakeResponse();
private ConsoleApiParams consoleApiParams;
@RegisterExtension
final JpaTestExtensions.JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
@Test
void testSuccess_hasXSRFCookie() throws IOException {
User user =
new User.Builder()
.setEmailAddress("email@email.com")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build())
.build();
AuthResult authResult = AuthResult.createUser(UserAuthInfo.create(user));
ConsoleUserDataAction action =
createAction(
Optional.of(FakeConsoleApiParams.get(Optional.of(authResult))), Action.Method.GET);
action.run();
ArrayList<Cookie> cookies = ((FakeResponse) consoleApiParams.response()).getCookies();
assertThat(cookies.stream().map(cookie -> cookie.getName()).collect(toImmutableList()))
.containsExactly("X-CSRF-Token");
}
@Test
void testSuccess_getContactInfo() throws IOException {
User user =
@@ -49,10 +76,15 @@ class ConsoleUserDataActionTest {
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build())
.build();
ConsoleUserDataAction action = createAction(AuthResult.createUser(UserAuthInfo.create(user)));
AuthResult authResult = AuthResult.createUser(UserAuthInfo.create(user));
ConsoleUserDataAction action =
createAction(
Optional.of(FakeConsoleApiParams.get(Optional.of(authResult))), Action.Method.GET);
action.run();
assertThat(response.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_OK);
Map jsonObject = GSON.fromJson(response.getPayload(), Map.class);
assertThat(((FakeResponse) consoleApiParams.response()).getStatus())
.isEqualTo(HttpStatusCodes.STATUS_CODE_OK);
Map jsonObject =
GSON.fromJson(((FakeResponse) consoleApiParams.response()).getPayload(), Map.class);
assertThat(jsonObject)
.containsExactly(
"isAdmin",
@@ -71,19 +103,18 @@ class ConsoleUserDataActionTest {
@Test
void testFailure_notAConsoleUser() throws IOException {
ConsoleUserDataAction action =
createAction(
AuthResult.createUser(
UserAuthInfo.create(
new com.google.appengine.api.users.User(
"JohnDoe@theregistrar.com", "theregistrar.com"),
false)));
ConsoleUserDataAction action = createAction(Optional.empty(), Action.Method.GET);
action.run();
assertThat(response.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED);
assertThat(((FakeResponse) consoleApiParams.response()).getStatus())
.isEqualTo(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED);
}
private ConsoleUserDataAction createAction(AuthResult authResult) throws IOException {
private ConsoleUserDataAction createAction(
Optional<ConsoleApiParams> maybeConsoleApiParams, Action.Method method) throws IOException {
consoleApiParams =
maybeConsoleApiParams.orElseGet(() -> FakeConsoleApiParams.get(Optional.empty()));
when(consoleApiParams.request().getMethod()).thenReturn(method.toString());
return new ConsoleUserDataAction(
authResult, response, "Nomulus", "support@example.com", "+1 (212) 867 5309", "test");
consoleApiParams, "Nomulus", "support@example.com", "+1 (212) 867 5309", "test");
}
}
@@ -261,11 +261,11 @@ td.section {
</tr>
<tr>
<td class="property_name">generated on</td>
<td class="property_value">2023-11-02 18:26:18.901466</td>
<td class="property_value">2023-11-09 01:49:59.861801</td>
</tr>
<tr>
<td class="property_name">last flyway file</td>
<td id="lastFlywayFile" class="property_value">V149__add_bsa_domain_in_use_table.sql</td>
<td id="lastFlywayFile" class="property_value">V150__add_tld_bsa_enroll_date.sql</td>
</tr>
</tbody>
</table>
@@ -285,7 +285,7 @@ td.section {
generated on
</text>
<text text-anchor="start" x="3835.5" y="-10.8" font-family="Helvetica,sans-Serif" font-size="14.00">
2023-11-02 18:26:18.901466
2023-11-09 01:49:59.861801
</text>
<polygon fill="none" stroke="#888888" points="3748,-4 3748,-44 4013,-44 4013,-4 3748,-4" /> <!-- allocationtoken_a08ccbef -->
<g id="node1" class="node">
File diff suppressed because it is too large Load Diff
+1
View File
@@ -147,3 +147,4 @@ V146__last_update_time_via_epp.sql
V147__drop_gaia_id_from_user.sql
V148__add_bsa_download_and_label_tables.sql
V149__add_bsa_domain_in_use_table.sql
V150__add_tld_bsa_enroll_date.sql
@@ -0,0 +1,15 @@
-- 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.
ALTER TABLE public."Tld" ADD COLUMN bsa_enroll_start_time timestamptz;
@@ -1146,7 +1146,8 @@ CREATE TABLE public."Tld" (
dns_ds_ttl interval,
dns_ns_ttl interval,
idn_tables text[],
breakglass_mode boolean DEFAULT false NOT NULL
breakglass_mode boolean DEFAULT false NOT NULL,
bsa_enroll_start_time timestamp with time zone
);
+3 -1
View File
@@ -35,7 +35,9 @@ apt-get install curl -y
apt-get install npm -y
npm cache clean -f
npm install -g n
n 16.19.0
# Retrying because fails are possible for node.js intallation. See -
# https://github.com/nodejs/build/issues/1993
for i in {1..5}; do n 16.19.0 && break || sleep 15; done
# Install gcloud
# Cribbed from https://cloud.google.com/sdk/docs/quickstart-debian-ubuntu
apt-get install lsb-release -y