1
0
mirror of https://github.com/google/nomulus synced 2026-01-31 10:02:28 +00:00

Compare commits

..

4 Commits

Author SHA1 Message Date
gbrodman
3f8145b44f Remove various ContactHistory references (#2949)
This keeps the ContactHistory class and tests, to avoid changing any
database-related code in this PR.
2026-01-29 21:42:59 +00:00
gbrodman
1fdacf25dc Remove pipeline/action to wipe out contact data (#2948)
We've wiped it all out now, so it's moot
2026-01-29 19:38:29 +00:00
gbrodman
41d26d8385 Remove references to contacts in domain flows (#2944)
We've moved on from contacts entirely now so the only thing we really
need to do is make sure that people don't include contacts in domain
creates or updates. This also makes auth code checking easier too,
because now the only auth code that you're allowed to provide is the
domain auth code (not a contact auth code)
2026-01-29 19:30:41 +00:00
Nilay Shah
71c9407f07 Add MosApiMetrics exporter (#2931)
* Add MosApiMetrics exporter with status code mapping

Introduces the metrics exporter for the MoSAPI system.

- Implements `MosApiMetrics` to export TLD and service states to Cloud Monitoring.
- Maps ICANN status codes to numeric gauges: 1 (UP), 0 (DOWN), and 2 (DISABLED/INCONCLUSIVE).
- Sets `MAX_TIMESERIES_PER_REQUEST` to 195 to respect Cloud Monitoring API limits

* Automate metric descriptor creation on startup in Cloud Monitoring

* Refactor MoSAPI metrics for resilience and standards

* Refactor and nits

- Kept projectName as part constant instead of inside method signature
- Added Summary logs for metrics execution
- Metric Executor defaults to Single Threaded

* junit test refactoring

* Fix Metric kind to GAUGE for all metrics

* Refactor MosApiMetrics to remove async ExecutorService

* Add LockHandler for Metric Descriptor creation

* Update LockHandler lease time to one hour and refactoring
2026-01-29 14:53:05 +00:00
59 changed files with 724 additions and 1846 deletions

View File

@@ -569,11 +569,6 @@ if (environment == 'alpha') {
mainClass: 'google.registry.beam.resave.ResaveAllEppResourcesPipeline',
metaData: 'google/registry/beam/resave_all_epp_resources_pipeline_metadata.json'
],
wipeOutContactHistoryPii:
[
mainClass: 'google.registry.beam.wipeout.WipeOutContactHistoryPiiPipeline',
metaData: 'google/registry/beam/wipe_out_contact_history_pii_pipeline_metadata.json'
],
]
project.tasks.create("stageBeamPipelines") {
doLast {

View File

@@ -131,12 +131,6 @@ public class BatchModule {
return extractOptionalDatetimeParameter(req, ExpandBillingRecurrencesAction.PARAM_END_TIME);
}
@Provides
@Parameter(WipeOutContactHistoryPiiAction.PARAM_CUTOFF_TIME)
static Optional<DateTime> provideCutoffTime(HttpServletRequest req) {
return extractOptionalDatetimeParameter(req, WipeOutContactHistoryPiiAction.PARAM_CUTOFF_TIME);
}
@Provides
@Parameter(ExpandBillingRecurrencesAction.PARAM_ADVANCE_CURSOR)
static boolean provideAdvanceCursor(HttpServletRequest req) {

View File

@@ -1,238 +0,0 @@
// Copyright 2025 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 com.google.common.net.MediaType.PLAIN_TEXT_UTF_8;
import static google.registry.flows.FlowUtils.marshalWithLenientRetry;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.ResourceUtils.readResourceUtf8;
import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static jakarta.servlet.http.HttpServletResponse.SC_NO_CONTENT;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import static java.nio.charset.StandardCharsets.US_ASCII;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import com.google.common.util.concurrent.RateLimiter;
import google.registry.config.RegistryConfig.Config;
import google.registry.flows.EppController;
import google.registry.flows.EppRequestSource;
import google.registry.flows.PasswordOnlyTransportCredentials;
import google.registry.flows.StatelessRequestSessionMetadata;
import google.registry.model.contact.Contact;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.Domain;
import google.registry.model.eppcommon.ProtocolDefinition;
import google.registry.model.eppoutput.EppOutput;
import google.registry.persistence.VKey;
import google.registry.request.Action;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.request.lock.LockHandler;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.logging.Level;
import javax.annotation.Nullable;
import org.joda.time.Duration;
/**
* An action that removes all contacts from all active (non-deleted) domains.
*
* <p>This implements part 1 of phase 3 of the Minimum Dataset migration, wherein we remove all uses
* of contact objects in preparation for later removing all contact data from the system.
*
* <p>This runs as a singly threaded, resumable action that loads batches of domains still
* containing contacts, and runs a superuser domain update on each one to remove the contacts,
* leaving behind a record recording that update.
*/
@Action(
service = Action.Service.BACKEND,
path = RemoveAllDomainContactsAction.PATH,
method = Action.Method.POST,
auth = Auth.AUTH_ADMIN)
public class RemoveAllDomainContactsAction implements Runnable {
public static final String PATH = "/_dr/task/removeAllDomainContacts";
private static final String LOCK_NAME = "Remove all domain contacts";
private static final String CONTACT_FMT = "<domain:contact type=\"%s\">%s</domain:contact>";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final EppController eppController;
private final String registryAdminClientId;
private final LockHandler lockHandler;
private final RateLimiter rateLimiter;
private final Response response;
private final String updateDomainXml;
private int successes = 0;
private int failures = 0;
private static final int BATCH_SIZE = 10000;
@Inject
RemoveAllDomainContactsAction(
EppController eppController,
@Config("registryAdminClientId") String registryAdminClientId,
LockHandler lockHandler,
@Named("standardRateLimiter") RateLimiter rateLimiter,
Response response) {
this.eppController = eppController;
this.registryAdminClientId = registryAdminClientId;
this.lockHandler = lockHandler;
this.rateLimiter = rateLimiter;
this.response = response;
this.updateDomainXml =
readResourceUtf8(RemoveAllDomainContactsAction.class, "domain_remove_contacts.xml");
}
@Override
public void run() {
response.setContentType(PLAIN_TEXT_UTF_8);
Callable<Void> runner =
() -> {
try {
runLocked();
response.setStatus(SC_OK);
} catch (Exception e) {
logger.atSevere().withCause(e).log("Errored out during execution.");
response.setStatus(SC_INTERNAL_SERVER_ERROR);
response.setPayload(String.format("Errored out with cause: %s", e));
}
return null;
};
if (!lockHandler.executeWithLocks(runner, null, Duration.standardHours(1), LOCK_NAME)) {
// Send a 200-series status code to prevent this conflicting action from retrying.
response.setStatus(SC_NO_CONTENT);
response.setPayload("Could not acquire lock; already running?");
}
}
private void runLocked() {
logger.atInfo().log("Removing contacts on all active domains.");
List<String> domainRepoIdsBatch;
do {
domainRepoIdsBatch =
tm().<List<String>>transact(
() ->
tm().getEntityManager()
.createQuery(
"""
SELECT repoId FROM Domain WHERE deletionTime = :end_of_time AND NOT (
adminContact IS NULL AND billingContact IS NULL
AND registrantContact IS NULL AND techContact IS NULL)
""")
.setParameter("end_of_time", END_OF_TIME)
.setMaxResults(BATCH_SIZE)
.getResultList());
for (String domainRepoId : domainRepoIdsBatch) {
rateLimiter.acquire();
runDomainUpdateFlow(domainRepoId);
}
} while (!domainRepoIdsBatch.isEmpty());
String msg =
String.format(
"Finished; %d domains were successfully updated and %d errored out.",
successes, failures);
logger.at(failures == 0 ? Level.INFO : Level.WARNING).log(msg);
response.setPayload(msg);
}
private void runDomainUpdateFlow(String repoId) {
// Create a new transaction that the flow's execution will be enlisted in that loads the domain
// transactionally. This way we can ensure that nothing else has modified the domain in question
// in the intervening period since the query above found it. If a single domain update fails
// permanently, log it and move on to not block processing all the other domains.
try {
boolean success = tm().transact(() -> runDomainUpdateFlowInner(repoId));
if (success) {
successes++;
} else {
failures++;
}
} catch (Throwable t) {
logger.atWarning().withCause(t).log(
"Failed updating domain with repoId %s; skipping.", repoId);
}
}
/**
* Runs the actual domain update flow and returns whether the contact removals were successful.
*/
private boolean runDomainUpdateFlowInner(String repoId) {
Domain domain = tm().loadByKey(VKey.create(Domain.class, repoId));
if (!domain.getDeletionTime().equals(END_OF_TIME)) {
// Domain has been deleted since the action began running; nothing further to be
// done here.
logger.atInfo().log("Nothing to process for deleted domain '%s'.", domain.getDomainName());
return false;
}
logger.atInfo().log("Attempting to remove contacts on domain '%s'.", domain.getDomainName());
StringBuilder sb = new StringBuilder();
ImmutableMap<VKey<? extends Contact>, Contact> contacts =
tm().loadByKeys(
domain.getContacts().stream()
.map(DesignatedContact::getContactKey)
.collect(ImmutableSet.toImmutableSet()));
// Collect all the (non-registrant) contacts referenced by the domain and compile an EPP XML
// string that removes each one.
for (DesignatedContact designatedContact : domain.getContacts()) {
@Nullable Contact contact = contacts.get(designatedContact.getContactKey());
if (contact == null) {
logger.atWarning().log(
"Domain '%s' referenced contact with repo ID '%s' that couldn't be" + " loaded.",
domain.getDomainName(), designatedContact.getContactKey().getKey());
continue;
}
sb.append(
String.format(
CONTACT_FMT,
Ascii.toLowerCase(designatedContact.getType().name()),
contact.getContactId()))
.append("\n");
}
String compiledXml =
updateDomainXml
.replace("%DOMAIN%", domain.getDomainName())
.replace("%CONTACTS%", sb.toString());
EppOutput output =
eppController.handleEppCommand(
new StatelessRequestSessionMetadata(
registryAdminClientId, ProtocolDefinition.getVisibleServiceExtensionUris()),
new PasswordOnlyTransportCredentials(),
EppRequestSource.BACKEND,
false,
true,
compiledXml.getBytes(US_ASCII));
if (output.isSuccess()) {
logger.atInfo().log(
"Successfully removed contacts from domain '%s'.", domain.getDomainName());
} else {
logger.atWarning().log(
"Failed removing contacts from domain '%s' with error %s.",
domain.getDomainName(), new String(marshalWithLenientRetry(output), US_ASCII));
}
return output.isSuccess();
}
}

View File

@@ -1,142 +0,0 @@
// Copyright 2021 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.beam.BeamUtils.createJobName;
import static google.registry.request.RequestParameters.PARAM_DRY_RUN;
import static jakarta.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import static jakarta.servlet.http.HttpServletResponse.SC_OK;
import com.google.api.services.dataflow.Dataflow;
import com.google.api.services.dataflow.model.LaunchFlexTemplateParameter;
import com.google.api.services.dataflow.model.LaunchFlexTemplateRequest;
import com.google.api.services.dataflow.model.LaunchFlexTemplateResponse;
import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import google.registry.beam.wipeout.WipeOutContactHistoryPiiPipeline;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.contact.ContactHistory;
import google.registry.request.Action;
import google.registry.request.Parameter;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.util.Clock;
import google.registry.util.RegistryEnvironment;
import jakarta.inject.Inject;
import java.io.IOException;
import java.util.Optional;
import org.joda.time.DateTime;
/**
* An action that launches {@link WipeOutContactHistoryPiiPipeline} to wipe out Personal
* Identifiable Information (PII) fields of {@link ContactHistory} entities.
*
* <p>{@link ContactHistory} entities should be retained in the database for only certain amount of
* time.
*/
@Action(
service = Action.Service.BACKEND,
path = WipeOutContactHistoryPiiAction.PATH,
auth = Auth.AUTH_ADMIN)
public class WipeOutContactHistoryPiiAction implements Runnable {
public static final String PATH = "/_dr/task/wipeOutContactHistoryPii";
public static final String PARAM_CUTOFF_TIME = "wipeoutTime";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final String PIPELINE_NAME = "wipe_out_contact_history_pii_pipeline";
private final Clock clock;
private final boolean isDryRun;
private final Optional<DateTime> maybeCutoffTime;
private final int minMonthsBeforeWipeOut;
private final String stagingBucketUrl;
private final String projectId;
private final String jobRegion;
private final Dataflow dataflow;
private final Response response;
@Inject
public WipeOutContactHistoryPiiAction(
Clock clock,
@Parameter(PARAM_DRY_RUN) boolean isDryRun,
@Parameter(PARAM_CUTOFF_TIME) Optional<DateTime> maybeCutoffTime,
@Config("minMonthsBeforeWipeOut") int minMonthsBeforeWipeOut,
@Config("beamStagingBucketUrl") String stagingBucketUrl,
@Config("projectId") String projectId,
@Config("defaultJobRegion") String jobRegion,
Dataflow dataflow,
Response response) {
this.clock = clock;
this.isDryRun = isDryRun;
this.maybeCutoffTime = maybeCutoffTime;
this.minMonthsBeforeWipeOut = minMonthsBeforeWipeOut;
this.stagingBucketUrl = stagingBucketUrl;
this.projectId = projectId;
this.jobRegion = jobRegion;
this.dataflow = dataflow;
this.response = response;
}
@Override
public void run() {
response.setContentType(MediaType.PLAIN_TEXT_UTF_8);
DateTime cutoffTime =
maybeCutoffTime.orElse(clock.nowUtc().minusMonths(minMonthsBeforeWipeOut));
LaunchFlexTemplateParameter launchParameter =
new LaunchFlexTemplateParameter()
.setJobName(
createJobName(
String.format(
"contact-history-pii-wipeout-%s",
cutoffTime.toString("yyyy-MM-dd't'HH-mm-ss'z'")),
clock))
.setContainerSpecGcsPath(
String.format("%s/%s_metadata.json", stagingBucketUrl, PIPELINE_NAME))
.setParameters(
ImmutableMap.of(
"registryEnvironment",
RegistryEnvironment.get().name(),
"cutoffTime",
cutoffTime.toString("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"),
"isDryRun",
Boolean.toString(isDryRun)));
logger.atInfo().log(
"Launching Beam pipeline to wipe out all PII of contact history entities prior to %s%s.",
cutoffTime, " in dry run mode");
try {
LaunchFlexTemplateResponse launchResponse =
dataflow
.projects()
.locations()
.flexTemplates()
.launch(
projectId,
jobRegion,
new LaunchFlexTemplateRequest().setLaunchParameter(launchParameter))
.execute();
logger.atInfo().log("Got response: %s", launchResponse.getJob().toPrettyString());
response.setStatus(SC_OK);
response.setPayload(
String.format(
"Launched contact history PII wipeout pipeline: %s",
launchResponse.getJob().getId()));
} catch (IOException e) {
logger.atWarning().withCause(e).log("Pipeline Launch failed");
response.setStatus(SC_INTERNAL_SERVER_ERROR);
response.setPayload(String.format("Pipeline launch failed: %s", e.getMessage()));
}
}
}

View File

@@ -1,166 +0,0 @@
// 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.beam.wipeout;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static org.apache.beam.sdk.values.TypeDescriptors.voids;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Streams;
import google.registry.beam.common.RegistryJpaIO;
import google.registry.model.contact.ContactHistory;
import google.registry.model.reporting.HistoryEntry.HistoryEntryId;
import google.registry.persistence.PersistenceModule.TransactionIsolationLevel;
import google.registry.persistence.VKey;
import java.io.Serializable;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.PipelineResult;
import org.apache.beam.sdk.coders.KvCoder;
import org.apache.beam.sdk.coders.StringUtf8Coder;
import org.apache.beam.sdk.coders.VarLongCoder;
import org.apache.beam.sdk.metrics.Counter;
import org.apache.beam.sdk.metrics.Metrics;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.transforms.MapElements;
import org.apache.beam.sdk.transforms.join.CoGroupByKey;
import org.apache.beam.sdk.transforms.join.KeyedPCollectionTuple;
import org.apache.beam.sdk.values.KV;
import org.apache.beam.sdk.values.PCollection;
import org.apache.beam.sdk.values.TupleTag;
import org.joda.time.DateTime;
/**
* Definition of a Dataflow Flex pipeline template, which finds out {@link ContactHistory} entries
* that are older than a given age (excluding the most recent one, even if it falls with the range)
* and wipe out PII information in them.
*
* <p>To stage this template locally, run {@code ./nom_build :core:sBP --environment=alpha \
* --pipeline=wipeOutContactHistoryPii}.
*
* <p>Then, you can run the staged template via the API client library, gCloud or a raw REST call.
*/
public class WipeOutContactHistoryPiiPipeline implements Serializable {
private static final long serialVersionUID = -4111052675715913820L;
private static final TupleTag<Long> REVISIONS_TO_WIPE = new TupleTag<>();
private static final TupleTag<Long> MOST_RECENT_REVISION = new TupleTag<>();
private final DateTime cutoffTime;
private final boolean dryRun;
private final Counter contactsInScope =
Metrics.counter("WipeOutContactHistoryPii", "contacts in scope");
private final Counter historiesToWipe =
Metrics.counter("WipeOutContactHistoryPii", "contact histories to wipe PII from");
private final Counter historiesWiped =
Metrics.counter("WipeOutContactHistoryPii", "contact histories actually updated");
WipeOutContactHistoryPiiPipeline(WipeOutContactHistoryPiiPipelineOptions options) {
dryRun = options.getIsDryRun();
cutoffTime = DateTime.parse(options.getCutoffTime());
}
void setup(Pipeline pipeline) {
KeyedPCollectionTuple.of(REVISIONS_TO_WIPE, getHistoryEntriesToWipe(pipeline))
.and(MOST_RECENT_REVISION, getMostRecentHistoryEntries(pipeline))
.apply("Group by contact", CoGroupByKey.create())
.apply(
"Wipe out PII",
MapElements.into(voids())
.via(
kv -> {
String repoId = kv.getKey();
long mostRecentRevision = kv.getValue().getOnly(MOST_RECENT_REVISION);
ImmutableList<Long> revisionsToWipe =
Streams.stream(kv.getValue().getAll(REVISIONS_TO_WIPE))
.filter(e -> e != mostRecentRevision)
.collect(toImmutableList());
if (revisionsToWipe.isEmpty()) {
return null;
}
contactsInScope.inc();
tm().transact(
() -> {
for (long revisionId : revisionsToWipe) {
historiesToWipe.inc();
ContactHistory history =
tm().loadByKey(
VKey.create(
ContactHistory.class,
new HistoryEntryId(repoId, revisionId)));
// In the unlikely case where multiple pipelines run at the
// same time, or where the runner decides to rerun a particular
// transform, we might have a history entry that has already been
// wiped at this point. There's no need to wipe it again.
if (!dryRun
&& history.getContactBase().isPresent()
&& history.getContactBase().get().getEmailAddress() != null) {
historiesWiped.inc();
tm().update(history.asBuilder().wipeOutPii().build());
}
}
});
return null;
}));
}
PCollection<KV<String, Long>> getHistoryEntriesToWipe(Pipeline pipeline) {
return pipeline.apply(
"Find contact histories to wipee",
// Email is one of the required fields in EPP, meaning it's initially not null when it
// is set by EPP flows (even though it is nullalbe in the SQL schema). Therefore,
// checking if it's null is one way to avoid processing contact history entities that
// have been processed previously. Refer to RFC 5733 for more information.
RegistryJpaIO.read(
"SELECT repoId, revisionId FROM ContactHistory WHERE resource.email IS NOT NULL"
+ " AND modificationTime < :cutoffTime",
ImmutableMap.of("cutoffTime", cutoffTime),
Object[].class,
row -> KV.of((String) row[0], (long) row[1]))
.withCoder(KvCoder.of(StringUtf8Coder.of(), VarLongCoder.of())));
}
PCollection<KV<String, Long>> getMostRecentHistoryEntries(Pipeline pipeline) {
return pipeline.apply(
"Find the most recent historiy entry for each contact",
RegistryJpaIO.read(
"SELECT repoId, revisionId FROM ContactHistory"
+ " WHERE (repoId, modificationTime) IN"
+ " (SELECT repoId, MAX(modificationTime) FROM ContactHistory GROUP BY repoId)",
ImmutableMap.of(),
Object[].class,
row -> KV.of((String) row[0], (long) row[1]))
.withCoder(KvCoder.of(StringUtf8Coder.of(), VarLongCoder.of())));
}
PipelineResult run(Pipeline pipeline) {
setup(pipeline);
return pipeline.run();
}
public static void main(String[] args) {
PipelineOptionsFactory.register(WipeOutContactHistoryPiiPipelineOptions.class);
WipeOutContactHistoryPiiPipelineOptions options =
PipelineOptionsFactory.fromArgs(args)
.withValidation()
.as(WipeOutContactHistoryPiiPipelineOptions.class);
// Repeatable read should be more than enough since we are dealing with old history entries that
// are otherwise immutable.
options.setIsolationOverride(TransactionIsolationLevel.TRANSACTION_REPEATABLE_READ);
Pipeline pipeline = Pipeline.create(options);
new WipeOutContactHistoryPiiPipeline(options).run(pipeline);
}
}

View File

@@ -1,37 +0,0 @@
// 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.beam.wipeout;
import google.registry.beam.common.RegistryPipelineOptions;
import org.apache.beam.sdk.options.Default;
import org.apache.beam.sdk.options.Description;
public interface WipeOutContactHistoryPiiPipelineOptions extends RegistryPipelineOptions {
@Description(
"A contact history entry with a history modification time before this time will have its PII"
+ " wiped, unless it is the most entry for the contact.")
String getCutoffTime();
void setCutoffTime(String value);
@Description(
"If true, the wiped out billing events will not be saved but the pipeline metrics counter"
+ " will still be updated.")
@Default.Boolean(false)
boolean getIsDryRun();
void setIsDryRun(boolean value);
}

View File

@@ -1264,12 +1264,6 @@ public final class RegistryConfig {
return ImmutableSet.copyOf(config.sslCertificateValidation.allowedEcdsaCurves);
}
@Provides
@Config("minMonthsBeforeWipeOut")
public static int provideMinMonthsBeforeWipeOut(RegistryConfigSettings config) {
return config.contactHistory.minMonthsBeforeWipeOut;
}
@Provides
@Config("jdbcBatchSize")
public static int provideHibernateJdbcBatchSize(RegistryConfigSettings config) {
@@ -1463,9 +1457,9 @@ public final class RegistryConfig {
}
@Provides
@Config("mosapiTldThreadCnt")
@Config("mosapiTldThreadCount")
public static int provideMosapiTldThreads(RegistryConfigSettings config) {
return config.mosapi.tldThreadCnt;
return config.mosapi.tldThreadCount;
}
private static String formatComments(String text) {

View File

@@ -39,7 +39,6 @@ public class RegistryConfigSettings {
public Beam beam;
public RegistryTool registryTool;
public SslCertificateValidation sslCertificateValidation;
public ContactHistory contactHistory;
public DnsUpdate dnsUpdate;
public BulkPricingPackageMonitoring bulkPricingPackageMonitoring;
public Bsa bsa;
@@ -223,11 +222,6 @@ public class RegistryConfigSettings {
public String expirationWarningEmailSubjectText;
}
/** Configuration for contact history. */
public static class ContactHistory {
public int minMonthsBeforeWipeOut;
}
/** Configuration for dns update. */
public static class DnsUpdate {
public String dnsUpdateFailEmailSubjectText;
@@ -272,6 +266,6 @@ public class RegistryConfigSettings {
public String entityType;
public List<String> tlds;
public List<String> services;
public int tldThreadCnt;
public int tldThreadCount;
}
}

View File

@@ -450,11 +450,6 @@ registryTool:
# OAuth client secret used by the tool.
clientSecret: YOUR_CLIENT_SECRET
# Configuration options for handling contact history.
contactHistory:
# The number of months that a ContactHistory entity should be stored in the database.
minMonthsBeforeWipeOut: 18
# Configuration options relevant to the DNS update functionality.
dnsUpdate:
dnsUpdateFailRegistryName: Example name
@@ -645,5 +640,5 @@ mosapi:
# Provides a fixed thread pool for parallel TLD processing.
# @see <a href="https://www.icann.org/mosapi-specification.pdf">
# ICANN MoSAPI Specification, Section 12.3</a>
tldThreadCnt: 4
tldThreadCount: 4

View File

@@ -22,7 +22,6 @@ import com.google.common.base.Strings;
import dagger.Module;
import dagger.Provides;
import google.registry.flows.picker.FlowPicker;
import google.registry.model.contact.ContactHistory;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
@@ -267,23 +266,6 @@ public class FlowModule {
return builder;
}
/**
* Provides a partially filled in {@link ContactHistory.Builder}
*
* <p>This is not marked with {@link FlowScope} so that each retry gets a fresh one. Otherwise,
* the fact that the builder is one-use would cause NPEs.
*/
@Provides
static ContactHistory.Builder provideContactHistoryBuilder(
Trid trid,
@InputXml byte[] inputXmlBytes,
@Superuser boolean isSuperuser,
@RegistrarId String registrarId,
EppInput eppInput) {
return makeHistoryEntryBuilder(
new ContactHistory.Builder(), trid, inputXmlBytes, isSuperuser, registrarId, eppInput);
}
/**
* Provides a partially filled in {@link HostHistory.Builder}
*

View File

@@ -16,7 +16,6 @@ package google.registry.flows;
import static com.google.common.collect.Sets.intersection;
import static google.registry.model.EppResourceUtils.isLinked;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
@@ -37,7 +36,6 @@ import google.registry.model.EppResource;
import google.registry.model.EppResource.ForeignKeyedEppResource;
import google.registry.model.EppResource.ResourceWithTransferData;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.contact.Contact;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Period;
@@ -124,14 +122,6 @@ public final class ResourceFlowUtils {
}
}
/** Check that the given AuthInfo is either missing or else is valid for the given resource. */
public static void verifyOptionalAuthInfo(Optional<AuthInfo> authInfo, Contact contact)
throws EppException {
if (authInfo.isPresent()) {
verifyAuthInfo(authInfo.get(), contact);
}
}
/** Check that the given AuthInfo is either missing or else is valid for the given resource. */
public static void verifyOptionalAuthInfo(Optional<AuthInfo> authInfo, Domain domain)
throws EppException {
@@ -142,37 +132,14 @@ public final class ResourceFlowUtils {
/** Check that the given {@link AuthInfo} is valid for the given domain. */
public static void verifyAuthInfo(AuthInfo authInfo, Domain domain) throws EppException {
final String authRepoId = authInfo.getPw().getRepoId();
String authPassword = authInfo.getPw().getValue();
if (authRepoId == null) {
// If no roid is specified, check the password against the domain's password.
String domainPassword = domain.getAuthInfo().getPw().getValue();
if (!domainPassword.equals(authPassword)) {
throw new BadAuthInfoForResourceException();
}
return;
}
// The roid should match one of the contacts.
Optional<VKey<Contact>> foundContact =
domain.getReferencedContacts().stream()
.filter(key -> key.getKey().equals(authRepoId))
.findFirst();
if (foundContact.isEmpty()) {
String authRepoId = authInfo.getPw().getRepoId();
// Previously one could auth against a contact, but we no longer hold any contact info
if (authRepoId != null) {
throw new BadAuthInfoForResourceException();
}
// Check the authInfo against the contact.
verifyAuthInfo(authInfo, tm().loadByKey(foundContact.get()));
}
/** Check that the given {@link AuthInfo} is valid for the given contact. */
public static void verifyAuthInfo(AuthInfo authInfo, Contact contact) throws EppException {
String authRepoId = authInfo.getPw().getRepoId();
String authPassword = authInfo.getPw().getValue();
String contactPassword = contact.getAuthInfo().getPw().getValue();
if (!contactPassword.equals(authPassword)
// It's unnecessary to specify a repoId on a contact auth info, but if it's there validate
// it. The usual case of this is validating a domain's auth using this method.
|| (authRepoId != null && !authRepoId.equals(contact.getRepoId()))) {
String domainPassword = domain.getAuthInfo().getPw().getValue();
if (!domainPassword.equals(authPassword)) {
throw new BadAuthInfoForResourceException();
}
}

View File

@@ -167,7 +167,6 @@ import org.joda.time.Duration;
* @error {@link DomainFlowUtils.DomainLabelBlockedByBsaException}
* @error {@link DomainFlowUtils.DomainLabelTooLongException}
* @error {@link DomainFlowUtils.DomainReservedException}
* @error {@link DomainFlowUtils.DuplicateContactForRoleException}
* @error {@link DomainFlowUtils.EmptyDomainNamePartException}
* @error {@link DomainFlowUtils.ExceedsMaxRegistrationYearsException}
* @error {@link DomainFlowUtils.ExpiredClaimException}
@@ -188,7 +187,6 @@ import org.joda.time.Duration;
* @error {@link DomainFlowUtils.MaxSigLifeNotSupportedException}
* @error {@link DomainFlowUtils.MissingBillingAccountMapException}
* @error {@link DomainFlowUtils.MissingClaimsNoticeException}
* @error {@link DomainFlowUtils.MissingContactTypeException}
* @error {@link DomainFlowUtils.NameserversNotAllowedForTldException}
* @error {@link DomainFlowUtils.NameserversNotSpecifiedForTldWithNameserverAllowListException}
* @error {@link DomainFlowUtils.PremiumNameBlockedException}
@@ -221,7 +219,8 @@ public final class DomainCreateFlow implements MutatingFlow {
@Inject DomainPricingLogic pricingLogic;
@Inject DomainDeletionTimeCache domainDeletionTimeCache;
@Inject DomainCreateFlow() {}
@Inject
DomainCreateFlow() {}
@Override
public EppResponse run() throws EppException {
@@ -378,12 +377,10 @@ public final class DomainCreateFlow implements MutatingFlow {
.setLaunchNotice(hasClaimsNotice ? launchCreate.get().getNotice() : null)
.setSmdId(signedMarkId)
.setDsData(secDnsCreate.map(SecDnsCreateExtension::getDsData).orElse(null))
.setRegistrant(command.getRegistrant())
.setAuthInfo(command.getAuthInfo())
.setDomainName(targetId)
.setNameservers(command.getNameservers().stream().collect(toImmutableSet()))
.setStatusValues(statuses)
.setContacts(command.getContacts())
.addGracePeriod(
GracePeriod.forBillingEvent(GracePeriodStatus.ADD, repoId, createBillingEvent))
.setLordnPhase(

View File

@@ -157,7 +157,8 @@ public final class DomainFlowTmchUtils {
}
/** The provided mark does not match the desired domain label. */
static class NoMarksFoundMatchingDomainException extends RequiredParameterMissingException {
public static class NoMarksFoundMatchingDomainException
extends RequiredParameterMissingException {
public NoMarksFoundMatchingDomainException() {
super("The provided mark does not match the desired domain label");
}

View File

@@ -19,7 +19,6 @@ import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Strings.emptyToNull;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableMap.toImmutableMap;
import static com.google.common.collect.ImmutableSetMultimap.toImmutableSetMultimap;
import static com.google.common.collect.Sets.difference;
import static com.google.common.collect.Sets.intersection;
import static com.google.common.collect.Sets.union;
@@ -45,10 +44,8 @@ import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.DateTimeUtils.isAtOrAfter;
import static google.registry.util.DateTimeUtils.leapSafeAddYears;
import static google.registry.util.DomainNameUtils.ACE_PREFIX;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.joining;
import com.google.common.base.Ascii;
import com.google.common.base.CharMatcher;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
@@ -57,9 +54,6 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
import com.google.common.net.InternetDomainName;
@@ -81,14 +75,12 @@ import google.registry.model.billing.BillingBase.Reason;
import google.registry.model.billing.BillingRecurrence;
import google.registry.model.contact.Contact;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.DesignatedContact.Type;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainCommand.Create;
import google.registry.model.domain.DomainCommand.CreateOrUpdate;
import google.registry.model.domain.DomainCommand.InvalidReferencesException;
import google.registry.model.domain.DomainCommand.Update;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.ForeignKeyedDesignatedContact;
import google.registry.model.domain.Period;
import google.registry.model.domain.Period.Unit;
import google.registry.model.domain.fee.BaseFee;
@@ -133,10 +125,8 @@ import google.registry.tldconfig.idn.IdnLabelValidator;
import google.registry.tools.DigestType;
import google.registry.util.Idn;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Set;
@@ -405,22 +395,11 @@ public class DomainFlowUtils {
return period;
}
/** Verify that no linked resources have disallowed statuses. */
static void verifyNotInPendingDelete(
Set<DesignatedContact> contacts,
Optional<VKey<Contact>> registrant,
Set<VKey<Host>> nameservers)
throws EppException {
ImmutableList.Builder<VKey<? extends EppResource>> keysToLoad = new ImmutableList.Builder<>();
contacts.stream().map(DesignatedContact::getContactKey).forEach(keysToLoad::add);
registrant.ifPresent(keysToLoad::add);
keysToLoad.addAll(nameservers);
verifyNotInPendingDelete(EppResource.loadByCacheIfEnabled(keysToLoad.build()).values());
}
private static void verifyNotInPendingDelete(Iterable<EppResource> resources)
throws EppException {
for (EppResource resource : resources) {
/** Verify that no linked nameservers have disallowed statuses. */
static void verifyNotInPendingDelete(ImmutableSet<VKey<Host>> nameservers)
throws StatusProhibitsOperationException {
for (EppResource resource :
EppResource.loadByCacheIfEnabled(ImmutableSet.copyOf(nameservers)).values()) {
if (resource.getStatusValues().contains(StatusValue.PENDING_DELETE)) {
throw new LinkedResourceInPendingDeleteProhibitsOperationException(
resource.getForeignKey());
@@ -428,15 +407,6 @@ public class DomainFlowUtils {
}
}
static void validateContactsHaveTypes(Set<DesignatedContact> contacts)
throws ParameterValuePolicyErrorException {
for (DesignatedContact contact : contacts) {
if (contact.getType() == null) {
throw new MissingContactTypeException();
}
}
}
static void validateNameserversCountForTld(String tld, InternetDomainName domainName, int count)
throws EppException {
// For TLDs with a nameserver allow list, all domains must have at least 1 nameserver.
@@ -451,36 +421,22 @@ public class DomainFlowUtils {
}
}
static void validateNoDuplicateContacts(Set<DesignatedContact> contacts)
/** Enforces absence of contact data on creation as part of the Minimum Dataset requirements. */
static void enforceContactAbsencesOnCreate(Create create)
throws ParameterValuePolicyErrorException {
ImmutableMultimap<Type, VKey<Contact>> contactsByType =
contacts.stream()
.collect(
toImmutableSetMultimap(
DesignatedContact::getType, DesignatedContact::getContactKey));
// If any contact type has multiple contacts:
if (contactsByType.asMap().values().stream().anyMatch(v -> v.size() > 1)) {
// Find the duplicates.
Map<Type, Collection<VKey<Contact>>> dupeKeysMap =
Maps.filterEntries(contactsByType.asMap(), e -> e.getValue().size() > 1);
ImmutableList<VKey<Contact>> dupeKeys =
dupeKeysMap.values().stream().flatMap(Collection::stream).collect(toImmutableList());
// Load the duplicates in one batch.
Map<VKey<? extends Contact>, Contact> dupeContacts = tm().loadByKeys(dupeKeys);
ImmutableMultimap.Builder<Type, VKey<Contact>> typesMap = new ImmutableMultimap.Builder<>();
dupeKeysMap.forEach(typesMap::putAll);
// Create an error message showing the type and contact IDs of the duplicates.
throw new DuplicateContactForRoleException(
Multimaps.transformValues(typesMap.build(), key -> dupeContacts.get(key).getContactId()));
}
enforceContactAbsences(create.getRegistrant(), create.getContacts());
}
/**
* Enforces the presence/absence of contact data on domain creates depending on the minimum data
* set migration schedule.
*/
static void validateCreateContactData(
/** Enforces absence of contact data on update as part of the Minimum Dataset requirements. */
static void enforceContactAbsencesOnUpdate(Update update)
throws ParameterValuePolicyErrorException {
Set<DesignatedContact> allDesignatedContacts =
Sets.union(update.getInnerAdd().getContacts(), update.getInnerRemove().getContacts());
enforceContactAbsences(update.getInnerChange().getRegistrant(), allDesignatedContacts);
}
/** Enforces the absence of contact data as part of the Minimum Dataset requirements. */
static void enforceContactAbsences(
Optional<VKey<Contact>> registrant, Set<DesignatedContact> contacts)
throws ParameterValuePolicyErrorException {
if (registrant.isPresent()) {
@@ -491,25 +447,6 @@ public class DomainFlowUtils {
}
}
/**
* Enforces the presence/absence of contact data on domain updates depending on the minimum data
* set migration schedule.
*/
static void validateUpdateContactData(
Optional<VKey<Contact>> existingRegistrant,
Optional<VKey<Contact>> newRegistrant,
Set<DesignatedContact> existingContacts,
Set<DesignatedContact> newContacts)
throws ParameterValuePolicyErrorException {
// Throw if the update specifies a new registrant that is different from the existing one.
if (newRegistrant.isPresent() && !newRegistrant.equals(existingRegistrant)) {
throw new RegistrantProhibitedException();
}
// Throw if the update specifies any new contacts that weren't already present on the domain.
if (!Sets.difference(newContacts, existingContacts).isEmpty()) {
throw new ContactsProhibitedException();
}
}
static void validateNameserversAllowedOnTld(String tld, Set<String> fullyQualifiedHostNames)
throws EppException {
@@ -1032,12 +969,9 @@ public class DomainFlowUtils {
/** Validate the contacts and nameservers specified in a domain create command. */
static void validateCreateCommandContactsAndNameservers(
Create command, Tld tld, InternetDomainName domainName) throws EppException {
verifyNotInPendingDelete(
command.getContacts(), command.getRegistrant(), command.getNameservers());
validateContactsHaveTypes(command.getContacts());
verifyNotInPendingDelete(command.getNameservers());
String tldStr = tld.getTldStr();
validateNoDuplicateContacts(command.getContacts());
validateCreateContactData(command.getRegistrant(), command.getContacts());
enforceContactAbsencesOnCreate(command);
ImmutableSet<String> hostNames = command.getNameserverHostNames();
validateNameserversCountForTld(tldStr, domainName, hostNames.size());
validateNameserversAllowedOnTld(tldStr, hostNames);
@@ -1143,17 +1077,6 @@ public class DomainFlowUtils {
.build();
}
static ImmutableSet<ForeignKeyedDesignatedContact> loadForeignKeyedDesignatedContacts(
ImmutableSet<DesignatedContact> contacts) {
ImmutableSet.Builder<ForeignKeyedDesignatedContact> builder = new ImmutableSet.Builder<>();
for (DesignatedContact contact : contacts) {
builder.add(
ForeignKeyedDesignatedContact.create(
contact.getType(), tm().loadByKey(contact.getContactKey()).getContactId()));
}
return builder.build();
}
/**
* Returns a set of DomainTransactionRecords which negate the most recent HistoryEntry's records.
*
@@ -1293,32 +1216,6 @@ public class DomainFlowUtils {
}
}
/** Missing type attribute for contact. */
static class MissingContactTypeException extends ParameterValuePolicyErrorException {
public MissingContactTypeException() {
super("Missing type attribute for contact");
}
}
/** More than one contact for a given role is not allowed. */
static class DuplicateContactForRoleException extends ParameterValuePolicyErrorException {
public DuplicateContactForRoleException(Multimap<Type, String> dupeContactsByType) {
super(
String.format(
"More than one contact for a given role is not allowed: %s",
dupeContactsByType.asMap().entrySet().stream()
.sorted(comparing(e -> e.getKey().name()))
.map(
e ->
String.format(
"role [%s] has contacts [%s]",
Ascii.toLowerCase(e.getKey().name()),
e.getValue().stream().sorted().collect(joining(", "))))
.collect(joining(", "))));
}
}
/** Declared launch extension phase does not match the current registry phase. */
static class LaunchPhaseMismatchException extends ParameterValuePolicyErrorException {
public LaunchPhaseMismatchException() {

View File

@@ -19,7 +19,6 @@ import static google.registry.flows.ResourceFlowUtils.loadAndVerifyExistence;
import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfo;
import static google.registry.flows.domain.DomainFlowUtils.addSecDnsExtensionIfPresent;
import static google.registry.flows.domain.DomainFlowUtils.handleFeeRequest;
import static google.registry.flows.domain.DomainFlowUtils.loadForeignKeyedDesignatedContacts;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.collect.ImmutableList;
@@ -126,15 +125,11 @@ public final class DomainInfoFlow implements MutatingFlow {
.setLastEppUpdateTime(domain.getLastEppUpdateTime())
.setRegistrationExpirationTime(domain.getRegistrationExpirationTime())
.setLastTransferTime(domain.getLastTransferTime());
domain
.getRegistrant()
.ifPresent(r -> infoBuilder.setRegistrant(tm().loadByKey(r).getContactId()));
// If authInfo is non-null, then the caller is authorized to see the full information since we
// will have already verified the authInfo is valid.
if (registrarId.equals(domain.getCurrentSponsorRegistrarId()) || authInfo.isPresent()) {
infoBuilder
.setContacts(loadForeignKeyedDesignatedContacts(domain.getContacts()))
.setSubordinateHosts(
hostsRequest.requestSubordinate() ? domain.getSubordinateHosts() : null)
.setCreationRegistrarId(domain.getCreationRegistrarId())

View File

@@ -29,14 +29,12 @@ import static google.registry.flows.ResourceFlowUtils.verifyOptionalAuthInfo;
import static google.registry.flows.ResourceFlowUtils.verifyResourceOwnership;
import static google.registry.flows.domain.DomainFlowUtils.checkAllowedAccessToTld;
import static google.registry.flows.domain.DomainFlowUtils.cloneAndLinkReferences;
import static google.registry.flows.domain.DomainFlowUtils.enforceContactAbsencesOnUpdate;
import static google.registry.flows.domain.DomainFlowUtils.updateDsData;
import static google.registry.flows.domain.DomainFlowUtils.validateContactsHaveTypes;
import static google.registry.flows.domain.DomainFlowUtils.validateDsData;
import static google.registry.flows.domain.DomainFlowUtils.validateFeesAckedIfPresent;
import static google.registry.flows.domain.DomainFlowUtils.validateNameserversAllowedOnTld;
import static google.registry.flows.domain.DomainFlowUtils.validateNameserversCountForTld;
import static google.registry.flows.domain.DomainFlowUtils.validateNoDuplicateContacts;
import static google.registry.flows.domain.DomainFlowUtils.validateUpdateContactData;
import static google.registry.flows.domain.DomainFlowUtils.verifyClientUpdateNotProhibited;
import static google.registry.flows.domain.DomainFlowUtils.verifyNotInPendingDelete;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_UPDATE;
@@ -64,8 +62,6 @@ import google.registry.flows.domain.DomainFlowUtils.RegistrantProhibitedExceptio
import google.registry.model.ImmutableObject;
import google.registry.model.billing.BillingBase.Reason;
import google.registry.model.billing.BillingEvent;
import google.registry.model.contact.Contact;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainCommand.Update;
import google.registry.model.domain.DomainCommand.Update.AddRemove;
@@ -88,7 +84,6 @@ import google.registry.model.poll.PendingActionNotificationResponse.DomainPendin
import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
import google.registry.model.tld.Tld;
import google.registry.persistence.VKey;
import jakarta.inject.Inject;
import java.util.Objects;
import java.util.Optional;
@@ -97,8 +92,8 @@ import org.joda.time.DateTime;
/**
* An EPP flow that updates a domain.
*
* <p>Updates can change contacts, nameservers and delegation signer data of a domain. Updates
* cannot change the domain's name.
* <p>Updates can change nameservers and delegation signer data of a domain. Updates cannot change
* the domain's name.
*
* <p>Some status values (those of the form "serverSomethingProhibited") can only be applied by the
* superuser. As such, adding or removing these statuses incurs a billing event. There will be only
@@ -113,7 +108,6 @@ import org.joda.time.DateTime;
* @error {@link google.registry.flows.exceptions.OnlyToolCanPassMetadataException}
* @error {@link google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException}
* @error {@link google.registry.flows.exceptions.ResourceStatusProhibitsOperationException}
* @error {@link DomainFlowUtils.DuplicateContactForRoleException}
* @error {@link DomainFlowUtils.EmptySecDnsUpdateException}
* @error {@link DomainFlowUtils.FeesMismatchException}
* @error {@link DomainFlowUtils.FeesRequiredForNonFreeOperationException}
@@ -121,7 +115,6 @@ import org.joda.time.DateTime;
* @error {@link DomainFlowUtils.LinkedResourcesDoNotExistException}
* @error {@link DomainFlowUtils.LinkedResourceInPendingDeleteProhibitsOperationException}
* @error {@link DomainFlowUtils.MaxSigLifeChangeNotSupportedException}
* @error {@link DomainFlowUtils.MissingContactTypeException}
* @error {@link DomainFlowUtils.NameserversNotAllowedForTldException}
* @error {@link NameserversNotSpecifiedForTldWithNameserverAllowListException}
* @error {@link DomainFlowUtils.NotAuthorizedForTldException}
@@ -158,7 +151,9 @@ public final class DomainUpdateFlow implements MutatingFlow {
@Inject EppResponse.Builder responseBuilder;
@Inject DomainUpdateFlowCustomLogic flowCustomLogic;
@Inject DomainPricingLogic pricingLogic;
@Inject DomainUpdateFlow() {}
@Inject
DomainUpdateFlow() {}
@Override
public EppResponse run() throws EppException {
@@ -179,7 +174,7 @@ public final class DomainUpdateFlow implements MutatingFlow {
Domain newDomain = performUpdate(command, existingDomain, now);
DomainHistory domainHistory =
historyBuilder.setType(DOMAIN_UPDATE).setDomain(newDomain).build();
validateNewState(existingDomain, newDomain);
validateNewState(newDomain);
if (requiresDnsUpdate(existingDomain, newDomain)) {
requestDomainDnsRefresh(targetId);
}
@@ -235,12 +230,7 @@ public final class DomainUpdateFlow implements MutatingFlow {
eppInput.getSingleExtension(FeeUpdateCommandExtension.class);
FeesAndCredits feesAndCredits = pricingLogic.getUpdatePrice(tld, targetId, now);
validateFeesAckedIfPresent(feeUpdate, feesAndCredits, false);
verifyNotInPendingDelete(
add.getContacts(),
command.getInnerChange().getRegistrant(),
add.getNameservers());
validateContactsHaveTypes(add.getContacts());
validateContactsHaveTypes(remove.getContacts());
verifyNotInPendingDelete(add.getNameservers());
validateNameserversAllowedOnTld(tldStr, add.getNameserverHostNames());
}
@@ -250,7 +240,6 @@ public final class DomainUpdateFlow implements MutatingFlow {
Optional<SecDnsUpdateExtension> secDnsUpdate =
eppInput.getSingleExtension(SecDnsUpdateExtension.class);
verifyAddsAndRemoves(domain.getNameservers(), add.getNameservers(), remove.getNameservers());
verifyAddsAndRemoves(domain.getContacts(), add.getContacts(), remove.getContacts());
verifyAddsAndRemoves(domain.getStatusValues(), add.getStatusValues(), remove.getStatusValues());
if (secDnsUpdate.isPresent()) {
SecDnsUpdateExtension ext = secDnsUpdate.get();
@@ -260,12 +249,7 @@ public final class DomainUpdateFlow implements MutatingFlow {
ext.getRemove().map(Remove::getDsData).orElse(ImmutableSet.of()));
}
Change change = command.getInnerChange();
// We have to verify no duplicate contacts _before_ constructing the domain because it is
// illegal to construct a domain with duplicate contacts.
Sets.SetView<DesignatedContact> newContacts =
union(Sets.difference(domain.getContacts(), remove.getContacts()), add.getContacts());
validateNoDuplicateContacts(newContacts);
enforceContactAbsencesOnUpdate(command);
Domain.Builder domainBuilder =
domain
@@ -285,9 +269,6 @@ public final class DomainUpdateFlow implements MutatingFlow {
.setLastEppUpdateRegistrarId(registrarId)
.addStatusValues(add.getStatusValues())
.removeStatusValues(remove.getStatusValues())
.removeContacts(remove.getContacts())
.addContacts(add.getContacts())
.setRegistrant(determineUpdatedRegistrant(change, domain))
.setAuthInfo(Optional.ofNullable(change.getAuthInfo()).orElse(domain.getAuthInfo()));
if (!add.getNameservers().isEmpty()) {
@@ -309,15 +290,6 @@ public final class DomainUpdateFlow implements MutatingFlow {
return domainBuilder.build();
}
private Optional<VKey<Contact>> determineUpdatedRegistrant(Change change, Domain domain) {
// During or after the minimum dataset transition, allow registrant to be removed.
if (change.getRegistrantContactId().isPresent()
&& change.getRegistrantContactId().get().isEmpty()) {
return Optional.empty();
}
return change.getRegistrant().or(domain::getRegistrant);
}
/**
* Checks whether the new state of the domain is valid.
*
@@ -325,13 +297,7 @@ public final class DomainUpdateFlow implements MutatingFlow {
* compliant with the additions or amendments, otherwise existing data can become invalid and
* cause Domain update failure.
*/
private static void validateNewState(Domain existingDomain, Domain newDomain)
throws EppException {
validateUpdateContactData(
existingDomain.getRegistrant(),
newDomain.getRegistrant(),
existingDomain.getContacts(),
newDomain.getContacts());
private static void validateNewState(Domain newDomain) throws EppException {
validateDsData(newDomain.getDsData());
validateNameserversCountForTld(
newDomain.getTld(),
@@ -345,8 +311,8 @@ public final class DomainUpdateFlow implements MutatingFlow {
Optional<MetadataExtension> metadataExtension =
eppInput.getSingleExtension(MetadataExtension.class);
if (metadataExtension.isPresent() && metadataExtension.get().getRequestedByRegistrar()) {
for (StatusValue statusValue
: symmetricDifference(existingDomain.getStatusValues(), newDomain.getStatusValues())) {
for (StatusValue statusValue :
symmetricDifference(existingDomain.getStatusValues(), newDomain.getStatusValues())) {
if (statusValue.isChargedStatus()) {
// Only charge once.
return Optional.of(

View File

@@ -27,7 +27,6 @@ import google.registry.model.UnsafeSerializable;
import google.registry.model.annotations.ExternalMessagingName;
import google.registry.model.annotations.IdAllocation;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.DomainRenewData;
@@ -272,12 +271,6 @@ public abstract class PollMessage extends ImmutableObject
return thisCastToDerived();
}
public B setContactHistoryId(HistoryEntryId historyId) {
getInstance().contactRepoId = historyId.getRepoId();
getInstance().contactHistoryRevisionId = historyId.getRevisionId();
return thisCastToDerived();
}
public B setHostHistoryId(HistoryEntryId historyId) {
getInstance().hostRepoId = historyId.getRepoId();
getInstance().hostHistoryRevisionId = historyId.getRevisionId();
@@ -290,9 +283,6 @@ public abstract class PollMessage extends ImmutableObject
if (history instanceof DomainHistory) {
return setDomainHistoryId(historyId);
}
if (history instanceof ContactHistory) {
return setContactHistoryId(historyId);
}
if (history instanceof HostHistory) {
return setHostHistoryId(historyId);
}

View File

@@ -23,8 +23,6 @@ import google.registry.model.EppResource;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import google.registry.model.annotations.IdAllocation;
import google.registry.model.contact.ContactBase;
import google.registry.model.contact.ContactHistory;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainHistory;
import google.registry.model.eppcommon.Trid;
@@ -348,8 +346,6 @@ public abstract class HistoryEntry extends ImmutableObject
HistoryEntry.Builder<? extends HistoryEntry, ?> createBuilderForResource(E parent) {
if (parent instanceof DomainBase) {
return new DomainHistory.Builder().setDomain((DomainBase) parent);
} else if (parent instanceof ContactBase) {
return new ContactHistory.Builder().setContact((ContactBase) parent);
} else if (parent instanceof HostBase) {
return new HostHistory.Builder().setHost((HostBase) parent);
} else {

View File

@@ -25,8 +25,6 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Streams;
import google.registry.model.EppResource;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.host.Host;
@@ -46,8 +44,6 @@ public class HistoryEntryDao {
public static ImmutableMap<Class<? extends EppResource>, Class<? extends HistoryEntry>>
RESOURCE_TYPES_TO_HISTORY_TYPES =
ImmutableMap.of(
Contact.class,
ContactHistory.class,
Domain.class,
DomainHistory.class,
Host.class,
@@ -59,7 +55,6 @@ public class HistoryEntryDao {
return tm().transact(
() ->
new ImmutableList.Builder<HistoryEntry>()
.addAll(loadAllHistoryObjects(ContactHistory.class, afterTime, beforeTime))
.addAll(loadAllHistoryObjects(DomainHistory.class, afterTime, beforeTime))
.addAll(loadAllHistoryObjects(HostHistory.class, afterTime, beforeTime))
.build());
@@ -121,7 +116,6 @@ public class HistoryEntryDao {
return tm().reTransact(
() ->
Streams.concat(
loadHistoryObjectByRegistrarsInternal(ContactHistory.class, registrarIds),
loadHistoryObjectByRegistrarsInternal(DomainHistory.class, registrarIds),
loadHistoryObjectByRegistrarsInternal(HostHistory.class, registrarIds))
.sorted(Comparator.comparing(HistoryEntry::getModificationTime))

View File

@@ -24,11 +24,9 @@ import google.registry.batch.DeleteLoadTestDataAction;
import google.registry.batch.DeleteProberDataAction;
import google.registry.batch.ExpandBillingRecurrencesAction;
import google.registry.batch.RelockDomainAction;
import google.registry.batch.RemoveAllDomainContactsAction;
import google.registry.batch.ResaveAllEppResourcesPipelineAction;
import google.registry.batch.ResaveEntityAction;
import google.registry.batch.SendExpiringCertificateNotificationEmailAction;
import google.registry.batch.WipeOutContactHistoryPiiAction;
import google.registry.bsa.BsaDownloadAction;
import google.registry.bsa.BsaRefreshAction;
import google.registry.bsa.BsaValidateAction;
@@ -276,8 +274,6 @@ interface RequestComponent {
ReadinessProbeActionFrontend readinessProbeActionFrontend();
RemoveAllDomainContactsAction removeAllDomainContactsAction();
RdapAutnumAction rdapAutnumAction();
RdapDomainAction rdapDomainAction();
@@ -350,8 +346,6 @@ interface RequestComponent {
VerifyOteAction verifyOteAction();
WipeOutContactHistoryPiiAction wipeOutContactHistoryPiiAction();
@Subcomponent.Builder
abstract class Builder implements RequestComponentBuilder<RequestComponent> {
@Override

View File

@@ -14,21 +14,346 @@
package google.registry.mosapi;
import static com.google.common.collect.ImmutableList.toImmutableList;
import com.google.api.client.googleapis.json.GoogleJsonResponseException;
import com.google.api.services.monitoring.v3.Monitoring;
import com.google.api.services.monitoring.v3.model.CreateTimeSeriesRequest;
import com.google.api.services.monitoring.v3.model.LabelDescriptor;
import com.google.api.services.monitoring.v3.model.Metric;
import com.google.api.services.monitoring.v3.model.MetricDescriptor;
import com.google.api.services.monitoring.v3.model.MonitoredResource;
import com.google.api.services.monitoring.v3.model.Point;
import com.google.api.services.monitoring.v3.model.TimeInterval;
import com.google.api.services.monitoring.v3.model.TimeSeries;
import com.google.api.services.monitoring.v3.model.TypedValue;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Ascii;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterators;
import com.google.common.flogger.FluentLogger;
import google.registry.config.RegistryConfig.Config;
import google.registry.mosapi.MosApiModels.ServiceStatus;
import google.registry.mosapi.MosApiModels.TldServiceState;
import google.registry.request.lock.LockHandler;
import google.registry.util.Clock;
import jakarta.inject.Inject;
import java.io.IOException;
import java.time.Instant;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;
import org.joda.time.Duration;
/** Metrics Exporter for MoSAPI. */
public class MosApiMetrics {
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
@Inject
public MosApiMetrics() {}
// Google Cloud Monitoring Limit: Max 200 TimeSeries per request
private static final int MAX_TIMESERIES_PER_REQUEST = 195;
public void recordStates(List<TldServiceState> states) {
// b/467541269: Logic to push status to Cloud Monitoring goes here
logger.atInfo().log("MoSAPI record metrics logic will be implemented from here");
private static final int METRICS_ALREADY_EXIST = 409;
// Magic String Constants
private static final String METRIC_DOMAIN = "custom.googleapis.com/mosapi/";
private static final String PROJECT_RESOURCE_PREFIX = "projects/";
private static final String RESOURCE_TYPE_GLOBAL = "global";
private static final String LABEL_PROJECT_ID = "project_id";
private static final String LABEL_TLD = "tld";
private static final String LABEL_SERVICE_TYPE = "service_type";
// Lock Constants
private static final String LOCK_NAME = "MosApiMetricCreation";
private static final Duration LOCK_LEASE_TIME = Duration.standardHours(1);
// Metric Names
private static final String METRIC_TLD_STATUS = "tld_status";
private static final String METRIC_SERVICE_STATUS = "service_status";
private static final String METRIC_EMERGENCY_USAGE = "emergency_usage";
private static final String GAUGE_METRIC_KIND = "GAUGE";
// Metric Display Names & Descriptions
private static final String DISPLAY_NAME_TLD_STATUS =
"Health of TLDs. 1 = UP, 0 = DOWN, 2= DISABLED/NOT_MONITORED";
private static final String DESC_TLD_STATUS = "Overall Health of TLDs reported from ICANN";
private static final String DISPLAY_NAME_SERVICE_STATUS =
"Health of Services. 1 = UP, 0 = DOWN, 2= DISABLED/NOT_MONITORED";
private static final String DESC_SERVICE_STATUS =
"Overall Health of Services reported from ICANN";
private static final String DISPLAY_NAME_EMERGENCY_USAGE =
"Percentage of Emergency Threshold Consumed";
private static final String DESC_EMERGENCY_USAGE =
"Downtime threshold that if reached by any of the monitored Services may cause the TLDs"
+ " Services emergency transition to an interim Registry Operator";
// MoSAPI Status Constants
private static final String STATUS_UP_INCONCLUSIVE = "UP-INCONCLUSIVE";
private static final String STATUS_DOWN = "DOWN";
private static final String STATUS_DISABLED = "DISABLED";
private final Monitoring monitoringClient;
private final String projectId;
private final String projectName;
private final Clock clock;
private final MonitoredResource monitoredResource;
private final LockHandler lockHandler;
// Flag to ensure we only create descriptors once, lazily
@VisibleForTesting static final AtomicBoolean isDescriptorInitialized = new AtomicBoolean(false);
@Inject
public MosApiMetrics(
Monitoring monitoringClient,
@Config("projectId") String projectId,
Clock clock,
LockHandler lockHandler) {
this.monitoringClient = monitoringClient;
this.projectId = projectId;
this.clock = clock;
this.projectName = PROJECT_RESOURCE_PREFIX + projectId;
this.lockHandler = lockHandler;
this.monitoredResource =
new MonitoredResource()
.setType(RESOURCE_TYPE_GLOBAL)
.setLabels(ImmutableMap.of(LABEL_PROJECT_ID, projectId));
}
/** Accepts a list of states and processes them in a single async batch task. */
public void recordStates(ImmutableList<TldServiceState> states) {
// If this is the first time we are recording, ensure descriptors exist.
ensureMetricDescriptorsWithLock();
pushBatchMetrics(states);
}
/**
* Attempts to create metric descriptors using a distributed lock.
*
* <p>If the lock is acquired, this instance creates the descriptors and marks itself initialized.
* If the lock is busy, it implies another instance is handling it, so we skip and proceed.
*/
private void ensureMetricDescriptorsWithLock() {
lockHandler.executeWithLocks(
() -> {
if (!isDescriptorInitialized.get()) {
createCustomMetricDescriptors();
isDescriptorInitialized.set(true);
}
return null;
},
null,
LOCK_LEASE_TIME,
LOCK_NAME);
}
// Defines the custom metrics in Cloud Monitoring
private void createCustomMetricDescriptors() {
// 1. TLD Status Descriptor
createMetricDescriptor(
METRIC_TLD_STATUS,
DISPLAY_NAME_TLD_STATUS,
DESC_TLD_STATUS,
"INT64",
ImmutableList.of(LABEL_TLD));
// 2. Service Status Descriptor
createMetricDescriptor(
METRIC_SERVICE_STATUS,
DISPLAY_NAME_SERVICE_STATUS,
DESC_SERVICE_STATUS,
"INT64",
ImmutableList.of(LABEL_TLD, LABEL_SERVICE_TYPE));
// 3. Emergency Usage Descriptor
createMetricDescriptor(
METRIC_EMERGENCY_USAGE,
DISPLAY_NAME_EMERGENCY_USAGE,
DESC_EMERGENCY_USAGE,
"DOUBLE",
ImmutableList.of(LABEL_TLD, LABEL_SERVICE_TYPE));
logger.atInfo().log("Metric descriptors ensured for project %s", projectId);
}
private void createMetricDescriptor(
String metricTypeSuffix,
String displayName,
String description,
String valueType,
ImmutableList<String> labelKeys) {
ImmutableList<LabelDescriptor> labelDescriptors =
labelKeys.stream()
.map(
key ->
new LabelDescriptor()
.setKey(key)
.setValueType("STRING")
.setDescription(
key.equals(LABEL_TLD)
? "The TLD being monitored"
: "The type of service"))
.collect(toImmutableList());
MetricDescriptor descriptor =
new MetricDescriptor()
.setType(METRIC_DOMAIN + metricTypeSuffix)
.setMetricKind(GAUGE_METRIC_KIND)
.setValueType(valueType)
.setDisplayName(displayName)
.setDescription(description)
.setLabels(labelDescriptors);
try {
monitoringClient
.projects()
.metricDescriptors()
.create(this.projectName, descriptor)
.execute();
} catch (GoogleJsonResponseException e) {
if (e.getStatusCode() == METRICS_ALREADY_EXIST) {
// the metric already exists. This is expected.
logger.atFine().log("Metric descriptor %s already exists.", metricTypeSuffix);
} else {
logger.atWarning().withCause(e).log(
"Failed to create metric descriptor %s. Status: %d",
metricTypeSuffix, e.getStatusCode());
}
} catch (Exception e) {
logger.atWarning().withCause(e).log(
"Unexpected error creating metric descriptor %s.", metricTypeSuffix);
}
}
private void pushBatchMetrics(ImmutableList<TldServiceState> states) {
Instant now = Instant.ofEpochMilli(clock.nowUtc().getMillis());
TimeInterval interval = new TimeInterval().setEndTime(now.toString());
Stream<TimeSeries> allTimeSeriesStream =
states.stream().flatMap(state -> createMetricsForState(state, interval));
Iterator<List<TimeSeries>> batchIterator =
Iterators.partition(allTimeSeriesStream.iterator(), MAX_TIMESERIES_PER_REQUEST);
int successCount = 0;
int failureCount = 0;
// Iterate and count
while (batchIterator.hasNext()) {
List<TimeSeries> batch = batchIterator.next();
try {
CreateTimeSeriesRequest request = new CreateTimeSeriesRequest().setTimeSeries(batch);
monitoringClient.projects().timeSeries().create(this.projectName, request).execute();
successCount++;
} catch (IOException e) {
failureCount++;
// Log individual batch failures, so we have the stack trace for debugging
logger.atWarning().withCause(e).log(
"Failed to push batch of %d time series.", batch.size());
}
}
// 4. Log the final summary
if (failureCount > 0) {
logger.atWarning().log(
"Metric push finished with errors. Batches Succeeded: %d, Failed: %d",
successCount, failureCount);
} else {
logger.atInfo().log("Metric push finished successfully. Batches Succeeded: %d", successCount);
}
}
/** Generates all TimeSeries (TLD + Services) for a single state object. */
private Stream<TimeSeries> createMetricsForState(TldServiceState state, TimeInterval interval) {
// 1. TLD Status
Stream<TimeSeries> tldStream = Stream.of(createTldStatusTimeSeries(state, interval));
// 2. Service Metrics (if any)
Stream<TimeSeries> serviceStream =
state.serviceStatuses().entrySet().stream()
.flatMap(
entry ->
createServiceMetricsStream(
state.tld(), entry.getKey(), entry.getValue(), interval));
return Stream.concat(tldStream, serviceStream);
}
private Stream<TimeSeries> createServiceMetricsStream(
String tld, String serviceType, ServiceStatus statusObj, TimeInterval interval) {
ImmutableMap<String, String> labels =
ImmutableMap.of(LABEL_TLD, tld, LABEL_SERVICE_TYPE, serviceType);
return Stream.of(
createTimeSeries(
METRIC_SERVICE_STATUS, labels, parseServiceStatus(statusObj.status()), interval),
createTimeSeries(METRIC_EMERGENCY_USAGE, labels, statusObj.emergencyThreshold(), interval));
}
private TimeSeries createTldStatusTimeSeries(TldServiceState state, TimeInterval interval) {
return createTimeSeries(
METRIC_TLD_STATUS,
ImmutableMap.of(LABEL_TLD, state.tld()),
parseTldStatus(state.status()),
interval);
}
private TimeSeries createTimeSeries(
String suffix, ImmutableMap<String, String> labels, Number val, TimeInterval interval) {
Metric metric = new Metric().setType(METRIC_DOMAIN + suffix).setLabels(labels);
TypedValue tv = new TypedValue();
if (val instanceof Double) {
tv.setDoubleValue((Double) val);
} else {
tv.setInt64Value(val.longValue());
}
return new TimeSeries()
.setMetric(metric)
.setResource(this.monitoredResource)
.setPoints(ImmutableList.of(new Point().setInterval(interval).setValue(tv)));
}
/**
* Translates MoSAPI status to a numeric metric.
*
* <p>Mappings: 1 (UP) = Healthy; 0 (DOWN) = Critical failure; 2 (UP-INCONCLUSIVE) = Disabled/Not
* Monitored/In Maintenance.
*
* <p>A status of 2 indicates the SLA monitoring system is under maintenance. The TLD is
* considered "UP" by default, but individual service checks are disabled. This distinguishes
* maintenance windows from actual availability or outages.
*
* @see <a href="https://www.icann.org/mosapi-specification.pdf">ICANN MoSAPI Spec Sec 5.1</a>
*/
private long parseTldStatus(String status) {
return switch (Ascii.toUpperCase(status)) {
case STATUS_DOWN -> 0;
case STATUS_UP_INCONCLUSIVE -> 2;
default -> 1; // status is up
};
}
/**
* Translates MoSAPI service status to a numeric metric.
*
* <p>Mappings: 1 (UP) = Healthy; 0 (DOWN) = Critical failure; 2 (DISABLED/UP-INCONCLUSIVE*) =
* Disabled/Not Monitored/In Maintenance.
*
* @see <a href="https://www.icann.org/mosapi-specification.pdf">ICANN MoSAPI Spec Sec 5.1</a>
*/
private long parseServiceStatus(String status) {
String serviceStatus = Ascii.toUpperCase(status);
if (serviceStatus.startsWith(STATUS_UP_INCONCLUSIVE)) {
return 2;
}
return switch (serviceStatus) {
case STATUS_DOWN -> 0;
case STATUS_DISABLED -> 2;
default -> 1; // status is Up
};
}
}

View File

@@ -26,11 +26,9 @@ import google.registry.mosapi.MosApiModels.ServiceStatus;
import google.registry.mosapi.MosApiModels.TldServiceState;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.stream.Collectors;
/** A service that provides business logic for interacting with MoSAPI Service State. */
public class MosApiStateService {
@@ -135,11 +133,12 @@ public class MosApiStateService {
tldExecutor))
.collect(toImmutableList());
List<TldServiceState> allStates =
ImmutableList<TldServiceState> allStates =
futures.stream()
.map(CompletableFuture::join)
.filter(Objects::nonNull)
.collect(Collectors.toList());
.filter(this::isValidForMetrics)
.collect(toImmutableList());
if (!allStates.isEmpty()) {
try {
@@ -152,4 +151,14 @@ public class MosApiStateService {
logger.atWarning().log("No successful TLD states fetched; skipping metrics push.");
}
}
private boolean isValidForMetrics(TldServiceState state) {
if (state.tld() == null || state.status() == null) {
logger.atSevere().log(
"Contract Violation: Received invalid state (TLD=%s, Status=%s). Skipping.",
state.tld(), state.status());
return false;
}
return true;
}
}

View File

@@ -200,7 +200,7 @@ public final class MosApiModule {
@Singleton
@Named("mosapiTldExecutor")
static ExecutorService provideMosapiTldExecutor(
@Config("mosapiTldThreadCnt") int threadPoolSize) {
@Config("mosapiTldThreadCount") int threadPoolSize) {
return Executors.newFixedThreadPool(threadPoolSize);
}
}

View File

@@ -1,39 +0,0 @@
{
"name": "Wipe Out PII From Old Contact History Entries",
"description": "An Apache Beam batch pipeline that finds old contact history entries and remove PII information from them.",
"parameters": [
{
"name": "registryEnvironment",
"label": "The Registry environment.",
"helpText": "The Registry environment.",
"is_optional": false,
"regexes": [
"^PRODUCTION|SANDBOX|CRASH|QA|ALPHA$"
]
},
{
"name": "isolationOverride",
"label": "The desired SQL transaction isolation level.",
"helpText": "The desired SQL transaction isolation level.",
"is_optional": true,
"regexes": [
"^[0-9A-Z_]+$"
]
},
{
"name": "cutoffTime",
"label": "The maximum history modification time of a contact history entry eligible for wipe out.",
"helpText": "If the history modificaiton time of contact history entry is older than this, and it is not the most recent entry of a contact, it will have its PII wiped out.",
"is_optional": true
},
{
"name": "isDryRun",
"label": "Whether this job is a dry run.",
"helpText": "If true, no changes will be saved to the database.",
"is_optional": true,
"regexes": [
"^true|false$"
]
}
]
}

View File

@@ -1,119 +0,0 @@
// Copyright 2025 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 com.google.common.truth.Truth.assertThat;
import static google.registry.model.common.FeatureFlag.FeatureName.MINIMUM_DATASET_CONTACTS_PROHIBITED;
import static google.registry.model.common.FeatureFlag.FeatureStatus.ACTIVE;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.loadByEntity;
import static google.registry.testing.DatabaseHelper.newDomain;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import static org.mockito.Mockito.mock;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.util.concurrent.RateLimiter;
import google.registry.flows.DaggerEppTestComponent;
import google.registry.flows.EppController;
import google.registry.flows.EppTestComponent.FakesAndMocksModule;
import google.registry.model.common.FeatureFlag;
import google.registry.model.contact.Contact;
import google.registry.model.domain.Domain;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeLockHandler;
import google.registry.testing.FakeResponse;
import java.util.Optional;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link RemoveAllDomainContactsAction}. */
class RemoveAllDomainContactsActionTest {
@RegisterExtension
final JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
private final FakeResponse response = new FakeResponse();
private final RateLimiter rateLimiter = mock(RateLimiter.class);
private RemoveAllDomainContactsAction action;
@BeforeEach
void beforeEach() {
createTld("tld");
persistResource(
new FeatureFlag.Builder()
.setFeatureName(MINIMUM_DATASET_CONTACTS_PROHIBITED)
.setStatusMap(ImmutableSortedMap.of(START_OF_TIME, ACTIVE))
.build());
EppController eppController =
DaggerEppTestComponent.builder()
.fakesAndMocksModule(FakesAndMocksModule.create(new FakeClock()))
.build()
.startRequest()
.eppController();
action =
new RemoveAllDomainContactsAction(
eppController, "NewRegistrar", new FakeLockHandler(true), rateLimiter, response);
}
@Test
void test_removesAllContactsFromMultipleDomains_andDoesntModifyDomainThatHasNoContacts() {
Contact c1 = persistActiveContact("contact12345");
Domain d1 = persistResource(newDomain("foo.tld", c1));
assertThat(d1.getAllContacts()).hasSize(3);
Contact c2 = persistActiveContact("contact23456");
Domain d2 = persistResource(newDomain("bar.tld", c2));
assertThat(d2.getAllContacts()).hasSize(3);
Domain d3 =
persistResource(
newDomain("baz.tld")
.asBuilder()
.setRegistrant(Optional.empty())
.setContacts(ImmutableSet.of())
.build());
assertThat(d3.getAllContacts()).isEmpty();
DateTime lastUpdate = d3.getUpdateTimestamp().getTimestamp();
action.run();
assertThat(loadByEntity(d1).getAllContacts()).isEmpty();
assertThat(loadByEntity(d2).getAllContacts()).isEmpty();
assertThat(loadByEntity(d3).getUpdateTimestamp().getTimestamp()).isEqualTo(lastUpdate);
}
@Test
void test_removesContacts_onDomainsThatOnlyPartiallyHaveContacts() {
Contact c1 = persistActiveContact("contact12345");
Domain d1 =
persistResource(
newDomain("foo.tld", c1).asBuilder().setContacts(ImmutableSet.of()).build());
assertThat(d1.getAllContacts()).hasSize(1);
Contact c2 = persistActiveContact("contact23456");
Domain d2 =
persistResource(
newDomain("bar.tld", c2).asBuilder().setRegistrant(Optional.empty()).build());
assertThat(d2.getAllContacts()).hasSize(2);
action.run();
assertThat(loadByEntity(d1).getAllContacts()).isEmpty();
assertThat(loadByEntity(d2).getAllContacts()).isEmpty();
}
}

View File

@@ -1,133 +0,0 @@
// Copyright 2021 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 com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.google.api.services.dataflow.model.LaunchFlexTemplateRequest;
import google.registry.beam.BeamActionTestBase;
import google.registry.testing.FakeClock;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
/** Unit tests for {@link WipeOutContactHistoryPiiAction}. */
class WipeOutContactHistoryPiiActionTest extends BeamActionTestBase {
private final DateTime now = DateTime.parse("2019-01-19T01:02:03Z");
private final FakeClock clock = new FakeClock(now);
private final Map<String, String> expectedParameters = new HashMap<>();
private final ArgumentCaptor<LaunchFlexTemplateRequest> launchRequest =
ArgumentCaptor.forClass(LaunchFlexTemplateRequest.class);
private WipeOutContactHistoryPiiAction action =
new WipeOutContactHistoryPiiAction(
clock,
false,
Optional.empty(),
8,
"tucketBucket",
"testProject",
"testRegion",
dataflow,
response);
@BeforeEach
void before() {
expectedParameters.put("registryEnvironment", "UNITTEST");
expectedParameters.put("isDryRun", "false");
expectedParameters.put("cutoffTime", "2018-05-19T01:02:03.000Z");
}
@Test
void testSuccess() throws Exception {
action.run();
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload())
.isEqualTo("Launched contact history PII wipeout pipeline: jobid");
verify(templates, times(1))
.launch(eq("testProject"), eq("testRegion"), launchRequest.capture());
assertThat(launchRequest.getValue().getLaunchParameter().getParameters())
.containsExactlyEntriesIn(expectedParameters);
}
@Test
void testSuccess_providedCutoffTime() throws Exception {
action =
new WipeOutContactHistoryPiiAction(
clock,
false,
Optional.of(now.minusYears(1)),
8,
"tucketBucket",
"testProject",
"testRegion",
dataflow,
response);
action.run();
expectedParameters.put("cutoffTime", "2018-01-19T01:02:03.000Z");
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload())
.isEqualTo("Launched contact history PII wipeout pipeline: jobid");
verify(templates, times(1))
.launch(eq("testProject"), eq("testRegion"), launchRequest.capture());
assertThat(launchRequest.getValue().getLaunchParameter().getParameters())
.containsExactlyEntriesIn(expectedParameters);
}
@Test
void testSuccess_dryRun() throws Exception {
action =
new WipeOutContactHistoryPiiAction(
clock,
true,
Optional.empty(),
8,
"tucketBucket",
"testProject",
"testRegion",
dataflow,
response);
action.run();
expectedParameters.put("isDryRun", "true");
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getPayload())
.isEqualTo("Launched contact history PII wipeout pipeline: jobid");
verify(templates, times(1))
.launch(eq("testProject"), eq("testRegion"), launchRequest.capture());
assertThat(launchRequest.getValue().getLaunchParameter().getParameters())
.containsExactlyEntriesIn(expectedParameters);
}
@Test
void testFailure_launchError() throws Exception {
when(launch.execute()).thenThrow(new IOException("cannot launch"));
action.run();
assertThat(response.getStatus()).isEqualTo(500);
assertThat(response.getPayload()).isEqualTo("Pipeline launch failed: cannot launch");
verify(templates, times(1))
.launch(eq("testProject"), eq("testRegion"), launchRequest.capture());
assertThat(launchRequest.getValue().getLaunchParameter().getParameters())
.containsExactlyEntriesIn(expectedParameters);
}
}

View File

@@ -22,7 +22,6 @@ import static google.registry.testing.DatabaseHelper.loadAllOf;
import static google.registry.testing.DatabaseHelper.loadByEntity;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistContactWithPendingTransfer;
import static google.registry.testing.DatabaseHelper.persistDomainWithDependentResources;
import static google.registry.testing.DatabaseHelper.persistDomainWithPendingTransfer;
import static google.registry.testing.DatabaseHelper.persistNewRegistrars;
@@ -89,18 +88,6 @@ public class ResaveAllEppResourcesPipelineTest {
assertThat(loadByEntity(contact)).isEqualTo(contact);
}
@Test
void testPipeline_fulfilledContactTransfer() {
Contact contact = persistActiveContact("test123");
DateTime now = fakeClock.nowUtc();
contact = persistContactWithPendingTransfer(contact, now, now.plusDays(5), now);
fakeClock.advanceBy(Duration.standardDays(10));
assertThat(loadByEntity(contact).getStatusValues()).contains(StatusValue.PENDING_TRANSFER);
runPipeline();
assertThat(loadByEntity(contact).getStatusValues())
.doesNotContain(StatusValue.PENDING_TRANSFER);
}
@Test
void testPipeline_fulfilledDomainTransfer() {
options.setFast(true);

View File

@@ -1,196 +0,0 @@
// 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.beam.wipeout;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.reporting.HistoryEntry.Type.CONTACT_CREATE;
import static google.registry.persistence.PersistenceModule.TransactionIsolationLevel.TRANSACTION_REPEATABLE_READ;
import static google.registry.testing.DatabaseHelper.loadAllOf;
import static google.registry.testing.DatabaseHelper.newContact;
import static google.registry.testing.DatabaseHelper.persistResource;
import static org.hibernate.cfg.AvailableSettings.ISOLATION;
import com.google.common.collect.ImmutableList;
import google.registry.beam.TestPipelineExtension;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactPhoneNumber;
import google.registry.model.reporting.HistoryEntryDao;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.persistence.transaction.JpaTestExtensions.JpaIntegrationTestExtension;
import google.registry.testing.FakeClock;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Unit tests for {@link WipeOutContactHistoryPiiPipeline}. */
public class WipeOutContactHistoryPiiPipelineTest {
private static final int MIN_AGE_IN_MONTHS = 18;
private static final DateTimeFormatter DATE_TIME_FORMATTER =
DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
private final FakeClock clock = new FakeClock(DateTime.parse("2020-02-02T12:34:56Z"));
private final WipeOutContactHistoryPiiPipelineOptions options =
PipelineOptionsFactory.create().as(WipeOutContactHistoryPiiPipelineOptions.class);
private Contact contact1;
private Contact contact2;
@RegisterExtension
final JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder()
.withClock(clock)
.withProperty(ISOLATION, TRANSACTION_REPEATABLE_READ.name())
.buildIntegrationTestExtension();
@RegisterExtension
final TestPipelineExtension pipeline =
TestPipelineExtension.create().enableAbandonedNodeEnforcement(true);
@BeforeEach
void beforeEach() {
contact1 =
persistResource(
newContact("my-contact1")
.asBuilder()
.setEmailAddress("test@example.com")
.setFaxNumber(
new ContactPhoneNumber.Builder().setPhoneNumber("+12122122122").build())
.build());
contact2 =
persistResource(
newContact("my-contact2")
.asBuilder()
.setEmailAddress("test@example.tld")
.setVoiceNumber(
new ContactPhoneNumber.Builder().setPhoneNumber("+19177199177").build())
.build());
// T = 0 month;
persistResource(createHistory(contact1));
// T = 5 months;
advanceMonths(5);
persistResource(createHistory(contact2));
// T = 10 months;
advanceMonths(5);
persistResource(createHistory(contact1));
persistResource(createHistory(contact2));
// T = 20 months;
advanceMonths(10);
persistResource(createHistory(contact2));
// T = 30 months;
advanceMonths(10);
options.setCutoffTime(DATE_TIME_FORMATTER.print(clock.nowUtc().minusMonths(MIN_AGE_IN_MONTHS)));
}
@Test
void testSuccess() {
// Before the pipeline runs, every history entry should have an emali address.
assertThat(
loadAllOf(ContactHistory.class).stream()
.filter(e -> e.getContactBase().get().getEmailAddress() != null)
.count())
.isEqualTo(5);
// Before the pipeline runs, contact history for contact1 should have fax numbers.
ImmutableList<ContactHistory> histories =
HistoryEntryDao.loadHistoryObjectsForResource(contact1.createVKey(), ContactHistory.class);
assertThat(
histories.stream().filter(e -> e.getContactBase().get().getFaxNumber() != null).count())
.isEqualTo(2);
// Before the pipeline runs, contact history for contact2 should have voice numbers.
histories =
HistoryEntryDao.loadHistoryObjectsForResource(contact2.createVKey(), ContactHistory.class);
assertThat(
histories.stream()
.filter(e -> e.getContactBase().get().getVoiceNumber() != null)
.count())
.isEqualTo(3);
WipeOutContactHistoryPiiPipeline wipeOutContactHistoryPiiPipeline =
new WipeOutContactHistoryPiiPipeline(options);
wipeOutContactHistoryPiiPipeline.run(pipeline).waitUntilFinish();
histories =
HistoryEntryDao.loadHistoryObjectsForResource(contact1.createVKey(), ContactHistory.class);
assertThat(histories.size()).isEqualTo(2);
ImmutableList<ContactHistory> wipedEntries =
histories.stream()
.filter(e -> e.getContactBase().get().getEmailAddress() == null)
.collect(toImmutableList());
// Only the history entry at T = 10 is wiped. The one at T = 10 is over 18 months old, but it
// is the most recent entry, so it is kept.
assertThat(wipedEntries.size()).isEqualTo(1);
assertThat(wipedEntries.get(0).getContactBase().get().getFaxNumber()).isNull();
// With a new history entry at T = 30, the one at T = 10 is eligible for wipe out. Note the
// current time itself (therefore the cutoff time) has not changed.
persistResource(createHistory(contact1));
wipeOutContactHistoryPiiPipeline.run(pipeline).waitUntilFinish();
histories =
HistoryEntryDao.loadHistoryObjectsForResource(contact1.createVKey(), ContactHistory.class);
assertThat(histories.size()).isEqualTo(3);
wipedEntries =
histories.stream()
.filter(e -> e.getContactBase().get().getEmailAddress() == null)
.collect(toImmutableList());
assertThat(wipedEntries.size()).isEqualTo(2);
// Check that the pipeline deals with multiple contacts correctly.
histories =
HistoryEntryDao.loadHistoryObjectsForResource(contact2.createVKey(), ContactHistory.class);
assertThat(histories.size()).isEqualTo(3);
wipedEntries =
histories.stream()
.filter(e -> e.getContactBase().get().getEmailAddress() == null)
.collect(toImmutableList());
// Only the history entry at T = 10 is wiped. The one at T = 10 is over 18 months old, but it
// is the most recent entry, so it is kept.
assertThat(wipedEntries.size()).isEqualTo(2);
assertThat(wipedEntries.get(0).getContactBase().get().getVoiceNumber()).isNull();
assertThat(wipedEntries.get(1).getContactBase().get().getVoiceNumber()).isNull();
}
@Test
void testSuccess_dryRun() {
options.setIsDryRun(true);
WipeOutContactHistoryPiiPipeline wipeOutContactHistoryPiiPipeline =
new WipeOutContactHistoryPiiPipeline(options);
wipeOutContactHistoryPiiPipeline.run(pipeline).waitUntilFinish();
ImmutableList<ContactHistory> histories =
HistoryEntryDao.loadHistoryObjectsForResource(contact1.createVKey(), ContactHistory.class);
assertThat(histories.size()).isEqualTo(2);
assertThat(
histories.stream()
.filter(e -> e.getContactBase().get().getEmailAddress() == null)
.collect(toImmutableList()))
.isEmpty();
}
private ContactHistory createHistory(Contact contact) {
return new ContactHistory.Builder()
.setContact(contact)
.setType(CONTACT_CREATE)
.setRegistrarId("TheRegistrar")
.setModificationTime(clock.nowUtc())
.build();
}
private void advanceMonths(int months) {
DateTime now = clock.nowUtc();
DateTime next = now.plusMonths(months);
clock.advanceBy(new Duration(now, next));
}
}

View File

@@ -24,13 +24,10 @@ import com.google.common.collect.Iterables;
import com.google.common.testing.TestLogHandler;
import google.registry.model.EppResource;
import google.registry.model.ForeignKeyUtils;
import google.registry.model.contact.ContactBase;
import google.registry.model.contact.ContactHistory;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainHistory;
import google.registry.model.eppinput.EppInput.ResourceCommandWrapper;
import google.registry.model.eppinput.ResourceCommand;
import google.registry.model.host.HostBase;
import google.registry.model.host.HostHistory;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.tmch.ClaimsList;
@@ -131,18 +128,12 @@ public abstract class ResourceFlowTestCase<F extends Flow, R extends EppResource
protected void assertLastHistoryContainsResource(EppResource resource) {
HistoryEntry historyEntry = Iterables.getLast(DatabaseHelper.getHistoryEntries(resource));
if (resource instanceof ContactBase) {
ContactHistory contactHistory = (ContactHistory) historyEntry;
// Don't use direct equals comparison since one might be a subclass of the other
assertAboutImmutableObjects()
.that(contactHistory.getContactBase().get())
.hasFieldsEqualTo(resource);
} else if (resource instanceof DomainBase) {
if (resource instanceof DomainBase) {
DomainHistory domainHistory = (DomainHistory) historyEntry;
assertAboutImmutableObjects()
.that(domainHistory.getDomainBase().get())
.isEqualExceptFields(resource, "gracePeriods", "dsData", "nsHosts");
} else if (resource instanceof HostBase) {
} else {
HostHistory hostHistory = (HostHistory) historyEntry;
// Don't use direct equals comparison since one might be a subclass of the other
assertAboutImmutableObjects()

View File

@@ -119,7 +119,6 @@ import google.registry.flows.domain.DomainFlowUtils.MalformedTcnIdException;
import google.registry.flows.domain.DomainFlowUtils.MaxSigLifeNotSupportedException;
import google.registry.flows.domain.DomainFlowUtils.MissingBillingAccountMapException;
import google.registry.flows.domain.DomainFlowUtils.MissingClaimsNoticeException;
import google.registry.flows.domain.DomainFlowUtils.MissingContactTypeException;
import google.registry.flows.domain.DomainFlowUtils.NameserversNotAllowedForTldException;
import google.registry.flows.domain.DomainFlowUtils.NameserversNotSpecifiedForTldWithNameserverAllowListException;
import google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException;
@@ -1874,15 +1873,6 @@ class DomainCreateFlowTest extends ResourceFlowTestCase<DomainCreateFlow, Domain
.build());
}
@Test
void testFailure_missingContactType() {
// We need to test for missing type, but not for invalid - the schema enforces that for us.
setEppInput("domain_create_missing_contact_type.xml");
persistContactsAndHosts();
EppException thrown = assertThrows(MissingContactTypeException.class, this::runFlow);
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_minimumDataset_noRegistrantButSomeOtherContactTypes() throws Exception {
setEppInput("domain_create_other_contact_types.xml");

View File

@@ -64,7 +64,6 @@ import google.registry.model.billing.BillingBase.RenewalPriceBehavior;
import google.registry.model.billing.BillingCancellation;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingRecurrence;
import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainAuthInfo;
import google.registry.model.domain.DomainHistory;
@@ -153,12 +152,6 @@ class DomainTransferApproveFlowTest
.build());
}
private void setEppLoader(String commandFilename) {
setEppInput(commandFilename);
// Replace the ROID in the xml file with the one generated in our test.
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
}
/**
* Runs a successful test, with the expectedCancellationBillingEvents parameter containing a list
* of billing event builders that will be filled out with the correct HistoryEntry parent as it is
@@ -184,7 +177,7 @@ class DomainTransferApproveFlowTest
String expectedXmlFilename,
DateTime expectedExpirationTime)
throws Exception {
setEppLoader(commandFilename);
setEppInput(commandFilename);
Tld registry = Tld.get(tld);
domain = reloadResourceByForeignKey();
// Make sure the implicit billing event is there; it will be deleted by the flow.
@@ -361,7 +354,7 @@ class DomainTransferApproveFlowTest
}
private void doFailingTest(String commandFilename) throws Exception {
setEppLoader(commandFilename);
setEppInput(commandFilename);
// Setup done; run the test.
assertMutatingFlow(true);
runFlow();
@@ -376,7 +369,7 @@ class DomainTransferApproveFlowTest
@Test
void testDryRun() throws Exception {
setEppLoader("domain_transfer_approve.xml");
setEppInput("domain_transfer_approve.xml");
dryRunFlowAssertResponse(loadFile("domain_transfer_approve_response.xml"));
}
@@ -492,14 +485,6 @@ class DomainTransferApproveFlowTest
"domain_transfer_approve_response.xml");
}
@Test
void testSuccess_contactAuthInfo() throws Exception {
doSuccessfulTest(
"tld",
"domain_transfer_approve_contact_authinfo.xml",
"domain_transfer_approve_response.xml");
}
@Test
void testSuccess_autorenewBeforeTransfer() throws Exception {
domain = reloadResourceByForeignKey();
@@ -619,14 +604,8 @@ class DomainTransferApproveFlowTest
}
@Test
void testFailure_badContactPassword() {
// Change the contact's password so it does not match the password in the file.
contact =
persistResource(
contact
.asBuilder()
.setAuthInfo(ContactAuthInfo.create(PasswordAuth.create("badpassword")))
.build());
void testFailure_contactPassword() {
// Contact passwords cannot be provided because we don't store contacts
EppException thrown =
assertThrows(
BadAuthInfoForResourceException.class,

View File

@@ -43,7 +43,6 @@ import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException;
import google.registry.flows.exceptions.NotPendingTransferException;
import google.registry.flows.exceptions.NotTransferInitiatorException;
import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainAuthInfo;
import google.registry.model.domain.DomainHistory;
@@ -75,8 +74,6 @@ class DomainTransferCancelFlowTest
private void doSuccessfulTest(String commandFilename) throws Exception {
setEppInput(commandFilename);
// Replace the ROID in the xml file with the one generated in our test.
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
// Make sure the implicit billing event is there; it will be deleted by the flow.
// We also expect to see autorenew events for the gaining and losing registrars.
assertBillingEvents(
@@ -187,8 +184,6 @@ class DomainTransferCancelFlowTest
private void doFailingTest(String commandFilename) throws Exception {
setEppInput(commandFilename);
// Replace the ROID in the xml file with the one generated in our test.
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
// Setup done; run the test.
assertMutatingFlow(true);
runFlow();
@@ -204,7 +199,6 @@ class DomainTransferCancelFlowTest
@Test
void testDryRun() throws Exception {
setEppInput("domain_transfer_cancel.xml");
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
dryRunFlowAssertResponse(loadFile("domain_transfer_cancel_response.xml"));
}
@@ -219,19 +213,8 @@ class DomainTransferCancelFlowTest
}
@Test
void testSuccess_contactAuthInfo() throws Exception {
doSuccessfulTest("domain_transfer_cancel_contact_authinfo.xml");
}
@Test
void testFailure_badContactPassword() {
// Change the contact's password so it does not match the password in the file.
contact =
persistResource(
contact
.asBuilder()
.setAuthInfo(ContactAuthInfo.create(PasswordAuth.create("badpassword")))
.build());
void testFailure_contactPassword() {
// Contact passwords cannot be provided because we don't store contacts
EppException thrown =
assertThrows(
BadAuthInfoForResourceException.class,

View File

@@ -20,7 +20,6 @@ import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_CREATE;
import static google.registry.testing.DatabaseHelper.createBillingEventForTransfer;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.getOnlyHistoryEntryOfType;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistDomainWithDependentResources;
import static google.registry.testing.DatabaseHelper.persistDomainWithPendingTransfer;
import static google.registry.testing.DatabaseHelper.persistResource;
@@ -36,7 +35,6 @@ import google.registry.model.billing.BillingBase.Flag;
import google.registry.model.billing.BillingBase.Reason;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingRecurrence;
import google.registry.model.contact.Contact;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.eppcommon.StatusValue;
@@ -72,7 +70,6 @@ abstract class DomainTransferFlowTestCase<F extends Flow, R extends EppResource>
static final DateTime EXTENDED_REGISTRATION_EXPIRATION_TIME =
REGISTRATION_EXPIRATION_TIME.plusYears(EXTENDED_REGISTRATION_YEARS);
protected Contact contact;
protected Domain domain;
Host subordinateHost;
private DomainHistory historyEntryDomainCreate;
@@ -104,12 +101,11 @@ abstract class DomainTransferFlowTestCase<F extends Flow, R extends EppResource>
/** Adds a domain with no pending transfer on it. */
void setupDomain(String label, String tld) {
createTld(tld);
contact = persistActiveContact("jd1234");
domain =
persistDomainWithDependentResources(
label,
tld,
contact,
null,
clock.nowUtc(),
DateTime.parse("1999-04-03T22:00:00.0Z"),
REGISTRATION_EXPIRATION_TIME);

View File

@@ -29,7 +29,6 @@ import google.registry.flows.ResourceFlowUtils.BadAuthInfoForResourceException;
import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.exceptions.NoTransferHistoryToQueryException;
import google.registry.flows.exceptions.NotAuthorizedToViewTransferException;
import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainAuthInfo;
import google.registry.model.eppcommon.AuthInfo.PasswordAuth;
@@ -52,8 +51,6 @@ class DomainTransferQueryFlowTest
private void doSuccessfulTest(
String commandFilename, String expectedXmlFilename, int numPollMessages) throws Exception {
setEppInput(commandFilename);
// Replace the ROID in the xml file with the one generated in our test.
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
// Setup done; run the test.
assertMutatingFlow(false);
runFlowAssertResponse(loadFile(expectedXmlFilename));
@@ -73,8 +70,6 @@ class DomainTransferQueryFlowTest
private void doFailingTest(String commandFilename) throws Exception {
setEppInput(commandFilename);
// Replace the ROID in the xml file with the one generated in our test.
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
// Setup done; run the test.
assertMutatingFlow(false);
runFlow();
@@ -105,13 +100,6 @@ class DomainTransferQueryFlowTest
"domain_transfer_query_domain_authinfo.xml", "domain_transfer_query_response.xml", 1);
}
@Test
void testSuccess_contactAuthInfo() throws Exception {
setRegistrarIdForFlow("ClientZ");
doSuccessfulTest(
"domain_transfer_query_contact_authinfo.xml", "domain_transfer_query_response.xml", 1);
}
@Test
void testSuccess_clientApproved() throws Exception {
changeTransferStatus(TransferStatus.CLIENT_APPROVED);
@@ -170,14 +158,7 @@ class DomainTransferQueryFlowTest
}
@Test
void testFailure_badContactPassword() {
// Change the contact's password so it does not match the password in the file.
contact =
persistResource(
contact
.asBuilder()
.setAuthInfo(ContactAuthInfo.create(PasswordAuth.create("badpassword")))
.build());
void testFailure_contactPasswordNotAllowed() {
EppException thrown =
assertThrows(
BadAuthInfoForResourceException.class,

View File

@@ -43,7 +43,6 @@ import google.registry.flows.ResourceFlowUtils.ResourceDoesNotExistException;
import google.registry.flows.ResourceFlowUtils.ResourceNotOwnedException;
import google.registry.flows.domain.DomainFlowUtils.NotAuthorizedForTldException;
import google.registry.flows.exceptions.NotPendingTransferException;
import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainAuthInfo;
import google.registry.model.domain.DomainHistory;
@@ -78,7 +77,6 @@ class DomainTransferRejectFlowTest
private void doSuccessfulTest(String commandFilename, String expectedXmlFilename)
throws Exception {
setEppInput(commandFilename);
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
// Make sure the implicit billing event is there; it will be deleted by the flow.
// We also expect to see autorenew events for the gaining and losing registrars.
assertBillingEvents(
@@ -149,8 +147,6 @@ class DomainTransferRejectFlowTest
private void doFailingTest(String commandFilename) throws Exception {
setEppInput(commandFilename);
// Replace the ROID in the xml file with the one generated in our test.
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
// Setup done; run the test.
assertMutatingFlow(true);
runFlow();
@@ -171,7 +167,6 @@ class DomainTransferRejectFlowTest
@Test
void testDryRun() throws Exception {
setEppInput("domain_transfer_reject.xml");
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
dryRunFlowAssertResponse(loadFile("domain_transfer_reject_response.xml"));
}
@@ -181,12 +176,6 @@ class DomainTransferRejectFlowTest
"domain_transfer_reject_domain_authinfo.xml", "domain_transfer_reject_response.xml");
}
@Test
void testSuccess_contactAuthInfo() throws Exception {
doSuccessfulTest(
"domain_transfer_reject_contact_authinfo.xml", "domain_transfer_reject_response.xml");
}
@Test
void testFailure_notAuthorizedForTld() {
persistResource(
@@ -209,14 +198,8 @@ class DomainTransferRejectFlowTest
}
@Test
void testFailure_badContactPassword() {
// Change the contact's password so it does not match the password in the file.
contact =
persistResource(
contact
.asBuilder()
.setAuthInfo(ContactAuthInfo.create(PasswordAuth.create("badpassword")))
.build());
void testFailure_contactPassword() {
// Contact passwords cannot be provided because we don't store contacts
EppException thrown =
assertThrows(
BadAuthInfoForResourceException.class,

View File

@@ -40,7 +40,6 @@ import static google.registry.testing.DatabaseHelper.loadByEntity;
import static google.registry.testing.DatabaseHelper.loadByKey;
import static google.registry.testing.DatabaseHelper.loadByKeys;
import static google.registry.testing.DatabaseHelper.loadRegistrar;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.testing.DomainSubject.assertAboutDomains;
import static google.registry.testing.EppExceptionSubject.assertAboutEppExceptions;
@@ -96,7 +95,6 @@ import google.registry.model.billing.BillingBase.RenewalPriceBehavior;
import google.registry.model.billing.BillingCancellation;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingRecurrence;
import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainAuthInfo;
import google.registry.model.domain.DomainHistory;
@@ -470,8 +468,6 @@ class DomainTransferRequestFlowTest
throws Exception {
setEppInput(commandFilename, substitutions);
ImmutableSet<GracePeriod> originalGracePeriods = domain.getGracePeriods();
// Replace the ROID in the xml file with the one generated in our test.
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
// For all of the other transfer flow tests, 'now' corresponds to day 3 of the transfer, but
// for the request test we want that same 'now' to be the initial request time, so we shift
// the transfer timeline 3 days later by adjusting the implicit transfer time here.
@@ -571,8 +567,6 @@ class DomainTransferRequestFlowTest
eppRequestSource = EppRequestSource.TOOL;
setEppInput(commandFilename, substitutions);
ImmutableSet<GracePeriod> originalGracePeriods = domain.getGracePeriods();
// Replace the ROID in the xml file with the one generated in our test.
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
// For all of the other transfer flow tests, 'now' corresponds to day 3 of the transfer, but
// for the request test we want that same 'now' to be the initial request time, so we shift
// the transfer timeline 3 days later by adjusting the implicit transfer time here.
@@ -626,8 +620,6 @@ class DomainTransferRequestFlowTest
String commandFilename, UserPrivileges userPrivileges, Map<String, String> substitutions)
throws Exception {
setEppInput(commandFilename, substitutions);
// Replace the ROID in the xml file with the one generated in our test.
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
// Setup done; run the test.
assertMutatingFlow(true);
runFlow(CommitMode.LIVE, userPrivileges);
@@ -657,7 +649,6 @@ class DomainTransferRequestFlowTest
void testDryRun() throws Exception {
setupDomain("example", "tld");
setEppInput("domain_transfer_request.xml");
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
dryRunFlowAssertResponse(loadFile("domain_transfer_request_response.xml"));
}
@@ -1184,7 +1175,6 @@ class DomainTransferRequestFlowTest
// This ensures that the transfer has non-premium cost, as otherwise, the fee extension would be
// required to ack the premium price.
setEppInput("domain_transfer_request.xml");
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
runFlowAssertResponse(loadFile("domain_transfer_request_response.xml"));
domain = loadByEntity(domain);
@@ -1238,7 +1228,6 @@ class DomainTransferRequestFlowTest
DateTime now = clock.nowUtc();
setEppInput("domain_transfer_request.xml");
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
runFlowAssertResponse(loadFile("domain_transfer_request_response.xml"));
domain = loadByEntity(domain);
@@ -1300,7 +1289,6 @@ class DomainTransferRequestFlowTest
DateTime now = clock.nowUtc();
setEppInput("domain_transfer_request.xml");
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
runFlowAssertResponse(loadFile("domain_transfer_request_response.xml"));
domain = loadByEntity(domain);
@@ -1361,7 +1349,6 @@ class DomainTransferRequestFlowTest
DateTime now = clock.nowUtc();
setEppInput("domain_transfer_request.xml");
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
runFlowAssertResponse(loadFile("domain_transfer_request_response.xml"));
domain = loadByEntity(domain);
@@ -1518,36 +1505,6 @@ class DomainTransferRequestFlowTest
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_badContactPassword() {
setupDomain("example", "tld");
// Change the contact's password so it does not match the password in the file.
contact =
persistResource(
contact
.asBuilder()
.setAuthInfo(ContactAuthInfo.create(PasswordAuth.create("badpassword")))
.build());
EppException thrown =
assertThrows(
BadAuthInfoForResourceException.class,
() -> doFailingTest("domain_transfer_request.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testFailure_badContactRepoId() {
setupDomain("example", "tld");
// Set the contact to a different ROID, but don't persist it; this is just so the substitution
// code above will write the wrong ROID into the file.
contact = contact.asBuilder().setRepoId("DEADBEEF_TLD-ROID").build();
EppException thrown =
assertThrows(
BadAuthInfoForResourceException.class,
() -> doFailingTest("domain_transfer_request.xml"));
assertAboutEppExceptions().that(thrown).marshalsToXml();
}
@Test
void testSuccess_clientApproved() throws Exception {
setupDomain("example", "tld");
@@ -1651,7 +1608,6 @@ class DomainTransferRequestFlowTest
setEppInput(
"domain_transfer_request_wildcard.xml",
ImmutableMap.of("YEARS", "1", "DOMAIN", "--invalid", "EXDATE", "2002-09-08T22:00:00.0Z"));
eppLoader.replaceAll("JD1234-REP", contact.getRepoId());
assertMutatingFlow(true);
ResourceDoesNotExistException thrown =
assertThrows(
@@ -1663,7 +1619,6 @@ class DomainTransferRequestFlowTest
@Test
void testFailure_nonexistentDomain() {
createTld("tld");
contact = persistActiveContact("jd1234");
ResourceDoesNotExistException thrown =
assertThrows(
ResourceDoesNotExistException.class,
@@ -1671,6 +1626,22 @@ class DomainTransferRequestFlowTest
assertThat(thrown).hasMessageThat().contains(String.format("(%s)", "example.tld"));
}
@Test
void testFailure_cannotUseContactAuthInfo() {
// RFC 5731: "An OPTIONAL "roid" attribute MUST be used to identify the registrant or contact
// object if and only if the given authInfo is associated with a registrant or contact object,
// and not the domain object itself."
//
// We have no contacts, so it cannot be valid to specify a roid
setupDomain("example", "tld");
assertAboutEppExceptions()
.that(
assertThrows(
BadAuthInfoForResourceException.class,
() -> doFailingTest("domain_transfer_request_contact_auth_info_failure.xml")))
.marshalsToXml();
}
@Test
void testFailure_periodInMonths() {
setupDomain("example", "tld");

View File

@@ -17,7 +17,6 @@ package google.registry.flows.poll;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.testing.DatabaseHelper.createHistoryEntryForEppResource;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistResource;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -28,7 +27,6 @@ import google.registry.flows.poll.PollAckFlow.InvalidMessageIdException;
import google.registry.flows.poll.PollAckFlow.MessageDoesNotExistException;
import google.registry.flows.poll.PollAckFlow.MissingMessageIdException;
import google.registry.flows.poll.PollAckFlow.NotAuthorizedToAckMessageException;
import google.registry.model.contact.Contact;
import google.registry.model.domain.Domain;
import google.registry.model.poll.PollMessage;
import google.registry.testing.DatabaseHelper;
@@ -43,7 +41,6 @@ class PollAckFlowTest extends FlowTestCase<PollAckFlow> {
private static final long MESSAGE_ID = 3;
private Domain domain;
private Contact contact;
@BeforeEach
void setUp() {
@@ -51,8 +48,7 @@ class PollAckFlowTest extends FlowTestCase<PollAckFlow> {
clock.setTo(DateTime.parse("2011-01-02T01:01:01Z"));
setRegistrarIdForFlow("NewRegistrar");
createTld("example");
contact = persistActiveContact("jd1234");
domain = persistResource(DatabaseHelper.newDomain("test.example", contact));
domain = persistResource(DatabaseHelper.newDomain("test.example"));
}
private void persistOneTimePollMessage(long messageId) {
@@ -85,43 +81,6 @@ class PollAckFlowTest extends FlowTestCase<PollAckFlow> {
dryRunFlowAssertResponse(loadFile("poll_ack_response_empty.xml"));
}
@Test
void testSuccess_contactPollMessage() throws Exception {
setEppInput("poll_ack.xml", ImmutableMap.of("MSGID", "3-2011"));
persistResource(
new PollMessage.OneTime.Builder()
.setId(MESSAGE_ID)
.setRegistrarId(getRegistrarIdForFlow())
.setEventTime(clock.nowUtc().minusDays(1))
.setMsg("Some poll message.")
.setHistoryEntry(createHistoryEntryForEppResource(contact))
.build());
assertMutatingFlow(true);
runFlowAssertResponse(loadFile("poll_ack_response_empty.xml"));
}
@Test
void testFailure_contactPollMessage_withIncorrectYearField() throws Exception {
setEppInput("poll_ack.xml", ImmutableMap.of("MSGID", "3-1999"));
persistResource(
new PollMessage.OneTime.Builder()
.setId(MESSAGE_ID)
.setRegistrarId(getRegistrarIdForFlow())
.setEventTime(clock.nowUtc().minusDays(1))
.setMsg("Some poll message.")
.setHistoryEntry(createHistoryEntryForEppResource(contact))
.build());
assertMutatingFlow(true);
assertThrows(MessageDoesNotExistException.class, this::runFlow);
}
@Test
void testSuccess_messageOnContact() throws Exception {
persistOneTimePollMessage(MESSAGE_ID);
assertMutatingFlow(true);
runFlowAssertResponse(loadFile("poll_ack_response_empty.xml"));
}
@Test
void testSuccess_recentActiveAutorenew() throws Exception {
setEppInput("poll_ack.xml", ImmutableMap.of("MSGID", "3-2010"));
@@ -184,21 +143,6 @@ class PollAckFlowTest extends FlowTestCase<PollAckFlow> {
assertThrows(InvalidMessageIdException.class, this::runFlow);
}
@Test
void testFailure_contactPollMessage_withMissingYearField() throws Exception {
setEppInput("poll_ack.xml", ImmutableMap.of("MSGID", "3"));
persistResource(
new PollMessage.OneTime.Builder()
.setId(MESSAGE_ID)
.setRegistrarId(getRegistrarIdForFlow())
.setEventTime(clock.nowUtc().minusDays(1))
.setMsg("Some poll message.")
.setHistoryEntry(createHistoryEntryForEppResource(contact))
.build());
assertMutatingFlow(true);
assertThrows(InvalidMessageIdException.class, this::runFlow);
}
@Test
void testFailure_invalidId_stringInsteadOfNumeric() throws Exception {
setEppInput("poll_ack.xml", ImmutableMap.of("MSGID", "ABC-12345"));

View File

@@ -16,7 +16,6 @@ package google.registry.flows.poll;
import static google.registry.testing.DatabaseHelper.createHistoryEntryForEppResource;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistActiveHost;
import static google.registry.testing.DatabaseHelper.persistNewRegistrar;
import static google.registry.testing.DatabaseHelper.persistResource;
@@ -27,8 +26,6 @@ import com.google.common.collect.ImmutableList;
import google.registry.flows.EppException;
import google.registry.flows.FlowTestCase;
import google.registry.flows.poll.PollRequestFlow.UnexpectedMessageIdException;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.domain.Domain;
import google.registry.model.eppcommon.Trid;
import google.registry.model.host.Host;
@@ -36,7 +33,6 @@ import google.registry.model.host.HostHistory;
import google.registry.model.poll.PendingActionNotificationResponse.DomainPendingActionNotificationResponse;
import google.registry.model.poll.PollMessage;
import google.registry.model.reporting.HistoryEntry;
import google.registry.model.transfer.TransferResponse.ContactTransferResponse;
import google.registry.model.transfer.TransferResponse.DomainTransferResponse;
import google.registry.model.transfer.TransferStatus;
import google.registry.testing.DatabaseHelper;
@@ -48,7 +44,6 @@ import org.junit.jupiter.api.Test;
class PollRequestFlowTest extends FlowTestCase<PollRequestFlow> {
private Domain domain;
private Contact contact;
private Host host;
@BeforeEach
@@ -58,8 +53,7 @@ class PollRequestFlowTest extends FlowTestCase<PollRequestFlow> {
setRegistrarIdForFlow("NewRegistrar");
createTld("example");
persistNewRegistrar("BadRegistrar");
contact = persistActiveContact("jd1234");
domain = persistResource(DatabaseHelper.newDomain("test.example", contact));
domain = persistResource(DatabaseHelper.newDomain("test.example"));
host = persistActiveHost("ns1.test.example");
}
@@ -99,31 +93,6 @@ class PollRequestFlowTest extends FlowTestCase<PollRequestFlow> {
runFlowAssertResponse(loadFile("poll_response_domain_transfer_no_cltrid.xml"));
}
@Test
void testSuccess_contactTransferPending() throws Exception {
setRegistrarIdForFlow("TheRegistrar");
persistResource(
new PollMessage.OneTime.Builder()
.setId(3L)
.setRegistrarId(getRegistrarIdForFlow())
.setEventTime(clock.nowUtc().minusDays(5))
.setMsg("Transfer requested.")
.setResponseData(
ImmutableList.of(
new ContactTransferResponse.Builder()
.setContactId("sh8013")
.setTransferStatus(TransferStatus.PENDING)
.setGainingRegistrarId(getRegistrarIdForFlow())
.setTransferRequestTime(clock.nowUtc().minusDays(5))
.setLosingRegistrarId("NewRegistrar")
.setPendingTransferExpirationTime(clock.nowUtc())
.build()))
.setHistoryEntry(createHistoryEntryForEppResource(contact))
.build());
assertMutatingFlow(false);
runFlowAssertResponse(loadFile("poll_response_contact_transfer.xml"));
}
@Test
void testSuccess_domainPendingActionComplete() throws Exception {
persistResource(
@@ -225,29 +194,6 @@ class PollRequestFlowTest extends FlowTestCase<PollRequestFlow> {
runFlowAssertResponse(loadFile("poll_response_empty.xml"));
}
@Test
void testSuccess_contactDelete() throws Exception {
// Contact delete poll messages do not have any response data, so ensure that no
// response data block is produced in the poll message.
HistoryEntry historyEntry =
persistResource(
new ContactHistory.Builder()
.setRegistrarId("NewRegistrar")
.setModificationTime(clock.nowUtc().minusDays(1))
.setType(HistoryEntry.Type.CONTACT_DELETE)
.setContact(contact)
.build());
persistResource(
new PollMessage.OneTime.Builder()
.setRegistrarId("NewRegistrar")
.setMsg("Deleted contact jd1234")
.setHistoryEntry(historyEntry)
.setEventTime(clock.nowUtc().minusDays(1))
.build());
assertMutatingFlow(false);
runFlowAssertResponse(loadFile("poll_response_contact_delete.xml"));
}
@Test
void testSuccess_hostDelete() throws Exception {
// Host delete poll messages do not have any response data, so ensure that no

View File

@@ -18,7 +18,6 @@ import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.poll.PollMessageExternalKeyConverter.makePollMessageExternalId;
import static google.registry.model.poll.PollMessageExternalKeyConverter.parsePollMessageExternalId;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistActiveHost;
import static google.registry.testing.DatabaseHelper.persistResource;
@@ -83,23 +82,6 @@ public class PollMessageExternalKeyConverterTest {
assertVKeysEqual(parsePollMessageExternalId("5-2007"), pollMessage.createVKey());
}
@Test
void testSuccess_contact() {
historyEntry =
persistResource(
DatabaseHelper.createHistoryEntryForEppResource(persistActiveContact("tim")));
PollMessage.OneTime pollMessage =
persistResource(
new PollMessage.OneTime.Builder()
.setRegistrarId("TheRegistrar")
.setEventTime(clock.nowUtc())
.setMsg("Test poll message")
.setHistoryEntry(historyEntry)
.build());
assertThat(makePollMessageExternalId(pollMessage)).isEqualTo("7-2007");
assertVKeysEqual(parsePollMessageExternalId("7-2007"), pollMessage.createVKey());
}
@Test
void testSuccess_host() {
historyEntry =

View File

@@ -18,7 +18,6 @@ import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.testing.DatabaseHelper.createTld;
import static google.registry.testing.DatabaseHelper.persistActiveContact;
import static google.registry.testing.DatabaseHelper.persistActiveDomain;
import static google.registry.testing.DatabaseHelper.persistResource;
import static java.nio.charset.StandardCharsets.UTF_8;
@@ -26,13 +25,12 @@ import static org.junit.Assert.assertThrows;
import com.google.common.collect.ImmutableSet;
import google.registry.model.EntityTestCase;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.Period;
import google.registry.model.eppcommon.Trid;
import google.registry.model.reporting.DomainTransactionRecord.TransactionReportField;
import google.registry.model.reporting.HistoryEntry.Type;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@@ -40,14 +38,13 @@ import org.junit.jupiter.api.Test;
/** Unit tests for {@link HistoryEntry}. */
class HistoryEntryTest extends EntityTestCase {
private Domain domain;
private DomainHistory domainHistory;
private Contact contact;
@BeforeEach
void setUp() {
createTld("foobar");
Domain domain = persistActiveDomain("foo.foobar");
contact = persistActiveContact("someone");
domain = persistActiveDomain("foo.foobar");
DomainTransactionRecord transactionRecord =
new DomainTransactionRecord.Builder()
.setTld("foobar")
@@ -91,12 +88,12 @@ class HistoryEntryTest extends EntityTestCase {
assertThrows(
IllegalArgumentException.class,
() ->
new ContactHistory.Builder()
new DomainHistory.Builder()
.setRevisionId(5L)
.setModificationTime(DateTime.parse("1985-07-12T22:30:00Z"))
.setRegistrarId("TheRegistrar")
.setReason("Reason")
.setType(HistoryEntry.Type.CONTACT_CREATE)
.setType(Type.DOMAIN_CREATE)
.build());
assertThat(thrown).hasMessageThat().isEqualTo("EPP resource must be specified");
}
@@ -107,10 +104,10 @@ class HistoryEntryTest extends EntityTestCase {
assertThrows(
IllegalArgumentException.class,
() ->
new ContactHistory.Builder()
.setContact(contact)
new DomainHistory.Builder()
.setRevisionId(5L)
.setModificationTime(DateTime.parse("1985-07-12T22:30:00Z"))
.setDomain(domain)
.setModificationTime(DateTime.parse("1985-07-12T22:30.00Z"))
.setRegistrarId("TheRegistrar")
.setReason("Reason")
.build());
@@ -123,12 +120,12 @@ class HistoryEntryTest extends EntityTestCase {
assertThrows(
IllegalArgumentException.class,
() ->
new ContactHistory.Builder()
.setContact(contact)
new DomainHistory.Builder()
.setRevisionId(5L)
.setType(HistoryEntry.Type.CONTACT_CREATE)
.setDomain(domain)
.setType(Type.DOMAIN_CREATE)
.setRegistrarId("TheRegistrar")
.setReason("Reason")
.setReason("reason")
.build());
assertThat(thrown).hasMessageThat().isEqualTo("Modification time must be specified");
}
@@ -139,11 +136,11 @@ class HistoryEntryTest extends EntityTestCase {
assertThrows(
IllegalArgumentException.class,
() ->
new ContactHistory.Builder()
new DomainHistory.Builder()
.setRevisionId(5L)
.setContact(contact)
.setType(HistoryEntry.Type.CONTACT_CREATE)
.setModificationTime(DateTime.parse("1985-07-12T22:30:00Z"))
.setDomain(domain)
.setType(Type.DOMAIN_CREATE)
.setModificationTime(DateTime.parse("1985-07-12T22:30.00Z"))
.setReason("Reason")
.build());
assertThat(thrown).hasMessageThat().isEqualTo("Registrar ID must be specified");
@@ -155,11 +152,11 @@ class HistoryEntryTest extends EntityTestCase {
assertThrows(
IllegalArgumentException.class,
() ->
new ContactHistory.Builder()
.setContact(contact)
new DomainHistory.Builder()
.setRevisionId(5L)
.setType(HistoryEntry.Type.SYNTHETIC)
.setModificationTime(DateTime.parse("1985-07-12T22:30:00Z"))
.setDomain(domain)
.setType(Type.SYNTHETIC)
.setModificationTime(DateTime.parse("1985-07-12T22:30.00Z"))
.setRegistrarId("TheRegistrar")
.setReason("Reason")
.setRequestedByRegistrar(true)

View File

@@ -0,0 +1,236 @@
// Copyright 2026 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.mosapi;
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.google.api.services.monitoring.v3.Monitoring;
import com.google.api.services.monitoring.v3.model.CreateTimeSeriesRequest;
import com.google.api.services.monitoring.v3.model.MetricDescriptor;
import com.google.api.services.monitoring.v3.model.TimeSeries;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import google.registry.mosapi.MosApiModels.ServiceStatus;
import google.registry.mosapi.MosApiModels.TldServiceState;
import google.registry.request.lock.LockHandler;
import google.registry.testing.FakeClock;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.Callable;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
/** Unit tests for {@link MosApiMetrics}. */
public class MosApiMetricsTest {
private static final String PROJECT_ID = "domain-registry-test";
private final Monitoring monitoringClient = mock(Monitoring.class);
private final Monitoring.Projects projects = mock(Monitoring.Projects.class);
private final LockHandler lockHandler = mock(LockHandler.class);
private final Monitoring.Projects.TimeSeries timeSeriesResource =
mock(Monitoring.Projects.TimeSeries.class);
private final Monitoring.Projects.TimeSeries.Create createRequest =
mock(Monitoring.Projects.TimeSeries.Create.class);
// Mocks for Metric Descriptors
private final Monitoring.Projects.MetricDescriptors metricDescriptorsResource =
mock(Monitoring.Projects.MetricDescriptors.class);
private final Monitoring.Projects.MetricDescriptors.Create createDescriptorRequest =
mock(Monitoring.Projects.MetricDescriptors.Create.class);
// Fixed Clock for deterministic testing
private final FakeClock clock = new FakeClock(DateTime.parse("2026-01-01T12:00:00Z"));
private MosApiMetrics mosApiMetrics;
@BeforeEach
void setUp() throws IOException, NoSuchFieldException, IllegalAccessException {
MosApiMetrics.isDescriptorInitialized.set(false);
when(monitoringClient.projects()).thenReturn(projects);
when(projects.timeSeries()).thenReturn(timeSeriesResource);
when(timeSeriesResource.create(anyString(), any(CreateTimeSeriesRequest.class)))
.thenReturn(createRequest);
// Setup for Metric Descriptors
when(projects.metricDescriptors()).thenReturn(metricDescriptorsResource);
when(metricDescriptorsResource.create(anyString(), any(MetricDescriptor.class)))
.thenReturn(createDescriptorRequest);
when(lockHandler.executeWithLocks(any(Callable.class), any(), any(), any()))
.thenAnswer(
invocation -> {
((Callable<?>) invocation.getArgument(0)).call();
return true;
});
mosApiMetrics = new MosApiMetrics(monitoringClient, PROJECT_ID, clock, lockHandler);
}
@Test
void testRecordStates_lazilyInitializesMetricDescriptors() throws IOException {
TldServiceState state = createTldState("test.tld", "UP", "UP");
mosApiMetrics.recordStates(ImmutableList.of(state));
ArgumentCaptor<MetricDescriptor> captor = ArgumentCaptor.forClass(MetricDescriptor.class);
verify(metricDescriptorsResource, times(3))
.create(eq("projects/" + PROJECT_ID), captor.capture());
List<MetricDescriptor> descriptors = captor.getAllValues();
// Verify TLD Status Descriptor
MetricDescriptor tldStatus =
descriptors.stream()
.filter(d -> d.getType().endsWith("tld_status"))
.findFirst()
.orElseThrow();
assertThat(tldStatus.getMetricKind()).isEqualTo("GAUGE");
assertThat(tldStatus.getValueType()).isEqualTo("INT64");
// Verify Service Status Descriptor
MetricDescriptor serviceStatus =
descriptors.stream()
.filter(d -> d.getType().endsWith("service_status"))
.findFirst()
.orElseThrow();
assertThat(serviceStatus.getMetricKind()).isEqualTo("GAUGE");
assertThat(serviceStatus.getValueType()).isEqualTo("INT64");
// Verify Emergency Usage Descriptor
MetricDescriptor emergencyUsage =
descriptors.stream()
.filter(d -> d.getType().endsWith("emergency_usage"))
.findFirst()
.orElseThrow();
assertThat(emergencyUsage.getMetricKind()).isEqualTo("GAUGE");
assertThat(emergencyUsage.getValueType()).isEqualTo("DOUBLE");
}
@Test
void testRecordStates_mapsStatusesToCorrectValues() throws IOException {
TldServiceState stateUp = createTldState("tld-up", "UP", "UP");
TldServiceState stateDown = createTldState("tld-down", "DOWN", "DOWN");
TldServiceState stateMaint = createTldState("tld-maint", "UP-INCONCLUSIVE", "DISABLED");
mosApiMetrics.recordStates(ImmutableList.of(stateUp, stateDown, stateMaint));
ArgumentCaptor<CreateTimeSeriesRequest> captor =
ArgumentCaptor.forClass(CreateTimeSeriesRequest.class);
verify(timeSeriesResource).create(eq("projects/" + PROJECT_ID), captor.capture());
List<TimeSeries> pushedSeries = captor.getValue().getTimeSeries();
// Verify TLD Status Mappings: 1 (UP), 0 (DOWN), 2 (UP-INCONCLUSIVE)
assertThat(getValueFor(pushedSeries, "tld-up", "tld_status")).isEqualTo(1);
assertThat(getValueFor(pushedSeries, "tld-down", "tld_status")).isEqualTo(0);
assertThat(getValueFor(pushedSeries, "tld-maint", "tld_status")).isEqualTo(2);
// Verify Service Status Mappings: UP -> 1, DOWN -> 0, DISABLED -> 2
assertThat(getValueFor(pushedSeries, "tld-up", "service_status")).isEqualTo(1);
assertThat(getValueFor(pushedSeries, "tld-down", "service_status")).isEqualTo(0);
assertThat(getValueFor(pushedSeries, "tld-maint", "service_status")).isEqualTo(2);
// 3. Verify Emergency Usage (DOUBLE)
assertThat(getValueFor(pushedSeries, "tld-up", "emergency_usage").doubleValue())
.isEqualTo(50.0);
assertThat(getValueFor(pushedSeries, "tld-down", "emergency_usage").doubleValue())
.isEqualTo(50.0);
assertThat(getValueFor(pushedSeries, "tld-maint", "emergency_usage").doubleValue())
.isEqualTo(50.0);
}
@Test
void testRecordStates_partitionsTimeSeries_atLimit() throws IOException {
ImmutableList<TldServiceState> largeBatch =
java.util.stream.IntStream.range(0, 70)
.mapToObj(i -> createTldState("tld-" + i, "UP", "UP"))
.collect(ImmutableList.toImmutableList());
mosApiMetrics.recordStates(largeBatch);
verify(timeSeriesResource, times(2))
.create(eq("projects/" + PROJECT_ID), any(CreateTimeSeriesRequest.class));
}
@Test
void testMetricStructure_containsExpectedLabelsAndResource() throws IOException {
TldServiceState state = createTldState("example.tld", "UP", "UP");
mosApiMetrics.recordStates(ImmutableList.of(state));
ArgumentCaptor<CreateTimeSeriesRequest> captor =
ArgumentCaptor.forClass(CreateTimeSeriesRequest.class);
verify(timeSeriesResource).create(anyString(), captor.capture());
TimeSeries ts = captor.getValue().getTimeSeries().get(0);
assertThat(ts.getMetric().getType()).startsWith("custom.googleapis.com/mosapi/");
assertThat(ts.getMetric().getLabels()).containsEntry("tld", "example.tld");
assertThat(ts.getResource().getType()).isEqualTo("global");
assertThat(ts.getResource().getLabels()).containsEntry("project_id", PROJECT_ID);
// Verify that the interval matches our fixed clock
assertThat(ts.getPoints().get(0).getInterval().getEndTime()).isEqualTo("2026-01-01T12:00:00Z");
}
/** Extracts the numeric value for a specific TLD and metric type from a list of TimeSeries. */
private Number getValueFor(List<TimeSeries> seriesList, String tld, String metricSuffix) {
String fullMetric = "custom.googleapis.com/mosapi/" + metricSuffix;
return seriesList.stream()
.filter(ts -> tld.equals(ts.getMetric().getLabels().get("tld")))
.filter(ts -> ts.getMetric().getType().equals(fullMetric))
.findFirst()
.map(
ts -> {
Double dVal = ts.getPoints().get(0).getValue().getDoubleValue();
if (dVal != null) {
return (Number) dVal;
}
// Fallback to Int64.
return (Number) ts.getPoints().get(0).getValue().getInt64Value();
})
.get();
}
@Test
void testRecordStates_skipsInitialization_ifLockNotAcquired() throws IOException {
when(lockHandler.executeWithLocks(any(Callable.class), any(), any(), any())).thenReturn(false);
TldServiceState state = createTldState("test.tld", "UP", "UP");
mosApiMetrics.recordStates(ImmutableList.of(state));
verify(metricDescriptorsResource, never()).create(anyString(), any());
}
/** Mocks a TldServiceState with a single service status. */
private TldServiceState createTldState(String tld, String tldStatus, String serviceStatus) {
ServiceStatus sStatus = mock(ServiceStatus.class);
when(sStatus.status()).thenReturn(serviceStatus);
when(sStatus.emergencyThreshold()).thenReturn(50.0);
TldServiceState state = mock(TldServiceState.class);
when(state.tld()).thenReturn(tld);
when(state.status()).thenReturn(tldStatus);
when(state.serviceStatuses()).thenReturn(ImmutableMap.of("dns", sStatus));
return state;
}
}

View File

@@ -169,4 +169,23 @@ class MosApiStateServiceTest {
&& states.stream()
.anyMatch(s -> s.tld().equals("tld1") && s.status().equals("Up"))));
}
@Test
void testTriggerMetrics_filtersOutInvalidContractStates() throws Exception {
// 1. Valid State
TldServiceState validState = new TldServiceState("tld1", 1L, "Up", ImmutableMap.of());
// 2. Invalid State (Status is NULL)
// We instantiate it directly to simulate a bad response object.
TldServiceState invalidState = new TldServiceState("tld2", 2L, null, ImmutableMap.of());
when(client.getTldServiceState("tld1")).thenReturn(validState);
when(client.getTldServiceState("tld2")).thenReturn(invalidState);
service.triggerMetricsForAllServiceStateSummaries();
// Verify: Only the valid state (tld1) is passed to recordStates
verify(metrics)
.recordStates(argThat(states -> states.size() == 1 && states.get(0).tld().equals("tld1")));
}
}

View File

@@ -28,17 +28,12 @@ import static google.registry.xjc.rgp.XjcRgpStatusValueType.TRANSFER_PERIOD;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.joda.money.CurrencyUnit.USD;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.net.InetAddresses;
import google.registry.model.billing.BillingBase.Flag;
import google.registry.model.billing.BillingBase.Reason;
import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingRecurrence;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactAddress;
import google.registry.model.contact.ContactPhoneNumber;
import google.registry.model.contact.PostalInfo;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainAuthInfo;
@@ -73,7 +68,6 @@ import google.registry.xjc.rgp.XjcRgpStatusType;
import google.registry.xjc.secdns.XjcSecdnsDsDataType;
import google.registry.xml.XmlException;
import java.io.ByteArrayOutputStream;
import java.util.Optional;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.junit.jupiter.api.BeforeEach;
@@ -109,7 +103,7 @@ public class DomainToXjcConverterTest {
assertThat(
bean.getContacts().stream()
.map(input -> String.format("%s %s", input.getType().toString(), input.getValue())))
.containsExactly("ADMIN 5372808-IRL", "TECH 5372808-TRL");
.containsExactly("ADMIN contact1234", "TECH contact1234");
assertThat(bean.getCrDate()).isEqualTo(DateTime.parse("1900-01-01T00:00:00Z"));
@@ -138,7 +132,7 @@ public class DomainToXjcConverterTest {
// name used to generate the IDN variant.
// TODO(b/26125498): bean.getOriginalName()
assertThat(bean.getRegistrant()).isEqualTo("5372808-ERL");
assertThat(bean.getRegistrant()).isEqualTo("contact1234");
// o Zero or more OPTIONAL <rgpStatus> element to represent
// "pendingDelete" sub-statuses, including "redemptionPeriod",
@@ -264,26 +258,6 @@ public class DomainToXjcConverterTest {
domain
.asBuilder()
.setAuthInfo(DomainAuthInfo.create(PasswordAuth.create("secret")))
.setContacts(
ImmutableSet.of(
DesignatedContact.create(
DesignatedContact.Type.ADMIN,
makeContact(
clock,
"10-Q9JYB4C",
"5372808-IRL",
"be that word our sign in parting",
"BOFH@cat.みんな")
.createVKey()),
DesignatedContact.create(
DesignatedContact.Type.TECH,
makeContact(
clock,
"11-Q9JYB4C",
"5372808-TRL",
"bird or fiend!? i shrieked upstarting",
"bog@cat.みんな")
.createVKey())))
.setCreationRegistrarId("TheRegistrar")
.setCreationTimeForTest(DateTime.parse("1900-01-01T00:00:00Z"))
.setPersistedCurrentSponsorRegistrarId("TheRegistrar")
@@ -298,15 +272,6 @@ public class DomainToXjcConverterTest {
makeHost(clock, "3-Q9JYB4C", "bird.or.devil.みんな", "1.2.3.4").createVKey(),
makeHost(clock, "4-Q9JYB4C", "ns2.cat.みんな", "bad:f00d:cafe::15:beef")
.createVKey()))
.setRegistrant(
Optional.of(
makeContact(
clock,
"12-Q9JYB4C",
"5372808-ERL",
"(◕‿◕) nevermore",
"prophet@evil.みんな")
.createVKey()))
.setRegistrationExpirationTime(DateTime.parse("1930-01-01T00:00:00Z"))
.setGracePeriods(
ImmutableSet.of(
@@ -404,37 +369,6 @@ public class DomainToXjcConverterTest {
return persistResource(domain);
}
private static Contact makeContact(
FakeClock clock, String repoId, String id, String name, String email) {
clock.advanceOneMilli();
return persistEppResource(
new Contact.Builder()
.setContactId(id)
.setEmailAddress(email)
.setPersistedCurrentSponsorRegistrarId("TheRegistrar")
.setCreationRegistrarId("TheRegistrar")
.setCreationTimeForTest(END_OF_TIME)
.setInternationalizedPostalInfo(
new PostalInfo.Builder()
.setType(PostalInfo.Type.INTERNATIONALIZED)
.setName(name)
.setOrg("SINNERS INCORPORATED")
.setAddress(
new ContactAddress.Builder()
.setStreet(ImmutableList.of("123 Example Boulevard"))
.setCity("KOKOMO")
.setState("BM")
.setZip("31337")
.setCountryCode("US")
.build())
.build())
.setRepoId(repoId)
.setVoiceNumber(
new ContactPhoneNumber.Builder().setPhoneNumber("+1.2126660420").build())
.setFaxNumber(new ContactPhoneNumber.Builder().setPhoneNumber("+1.2126660421").build())
.build());
}
private static Host makeHost(FakeClock clock, String repoId, String fqhn, String ip) {
clock.advanceOneMilli();
return persistEppResource(

View File

@@ -25,7 +25,6 @@ import static com.google.common.collect.MoreCollectors.onlyElement;
import static com.google.common.truth.Truth.assertThat;
import static com.google.common.truth.Truth.assertWithMessage;
import static google.registry.config.RegistryConfig.getContactAndHostRoidSuffix;
import static google.registry.config.RegistryConfig.getContactAutomaticTransferLength;
import static google.registry.model.EppResourceUtils.createDomainRepoId;
import static google.registry.model.EppResourceUtils.createRepoId;
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
@@ -76,7 +75,6 @@ import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactAuthInfo;
import google.registry.model.contact.ContactHistory;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.DesignatedContact.Type;
import google.registry.model.domain.Domain;
@@ -536,53 +534,6 @@ public final class DatabaseHelper {
.build();
}
public static Contact persistContactWithPendingTransfer(
Contact contact, DateTime requestTime, DateTime expirationTime, DateTime now) {
ContactHistory historyEntryContactTransfer =
persistResource(
new ContactHistory.Builder()
.setType(HistoryEntry.Type.CONTACT_TRANSFER_REQUEST)
.setContact(persistResource(contact))
.setModificationTime(now)
.setRegistrarId(contact.getCurrentSponsorRegistrarId())
.build());
return persistResource(
contact
.asBuilder()
.setPersistedCurrentSponsorRegistrarId("TheRegistrar")
.addStatusValue(StatusValue.PENDING_TRANSFER)
.setTransferData(
createContactTransferDataBuilder(requestTime, expirationTime)
.setPendingTransferExpirationTime(now.plus(getContactAutomaticTransferLength()))
.setServerApproveEntities(
historyEntryContactTransfer.getRepoId(),
historyEntryContactTransfer.getRevisionId(),
ImmutableSet.of(
// Pretend it's 3 days since the request
persistResource(
createPollMessageForImplicitTransfer(
contact,
historyEntryContactTransfer,
"NewRegistrar",
requestTime,
expirationTime,
null))
.createVKey(),
persistResource(
createPollMessageForImplicitTransfer(
contact,
historyEntryContactTransfer,
"TheRegistrar",
requestTime,
expirationTime,
null))
.createVKey()))
.setTransferRequestTrid(
Trid.create("transferClient-trid", "transferServer-trid"))
.build())
.build());
}
public static Domain persistDomainWithDependentResources(
String label,
String tld,

View File

@@ -1,20 +0,0 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<create>
<domain:create
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>example.tld</domain:name>
<domain:period unit="y">2</domain:period>
<domain:ns>
<domain:hostObj>ns1.example.net</domain:hostObj>
<domain:hostObj>ns2.example.net</domain:hostObj>
</domain:ns>
<domain:contact>sh8013</domain:contact>
<domain:authInfo>
<domain:pw>2fooBAR</domain:pw>
</domain:authInfo>
</domain:create>
</create>
<clTRID>ABC-12345</clTRID>
</command>
</epp>

View File

@@ -6,7 +6,7 @@
<domain:name>example.tld</domain:name>
<domain:period unit="y">1</domain:period>
<domain:authInfo>
<domain:pw roid="JD1234-REP">2fooBAR</domain:pw>
<domain:pw>fooBAR</domain:pw>
</domain:authInfo>
</domain:transfer>
</transfer>

View File

@@ -6,7 +6,7 @@
<domain:name>example.tld</domain:name>
<domain:period unit="y">1</domain:period>
<domain:authInfo>
<domain:pw roid="JD1234-REP">2fooBAR</domain:pw>
<domain:pw>fooBAR</domain:pw>
</domain:authInfo>
</domain:transfer>
</transfer>

View File

@@ -0,0 +1,15 @@
<epp xmlns="urn:ietf:params:xml:ns:epp-1.0">
<command>
<transfer op="request">
<domain:transfer
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>example.tld</domain:name>
<domain:period unit="y">1</domain:period>
<domain:authInfo>
<domain:pw roid="2-ROID">2fooBAR</domain:pw>
</domain:authInfo>
</domain:transfer>
</transfer>
<clTRID>ABC-12345</clTRID>
</command>
</epp>

View File

@@ -6,7 +6,7 @@
<domain:name>%DOMAIN%</domain:name>
<domain:period unit="y">%YEARS%</domain:period>
<domain:authInfo>
<domain:pw roid="JD1234-REP">2fooBAR</domain:pw>
<domain:pw>fooBAR</domain:pw>
</domain:authInfo>
</domain:transfer>
</transfer>

View File

@@ -6,7 +6,7 @@
<domain:name>example.tld</domain:name>
<domain:period unit="y">1</domain:period>
<domain:authInfo>
<domain:pw roid="JD1234-REP">2fooBAR</domain:pw>
<domain:pw>fooBAR</domain:pw>
</domain:authInfo>
</domain:transfer>
</transfer>

View File

@@ -6,7 +6,7 @@
<domain:name>example.tld</domain:name>
<domain:period unit="y">1</domain:period>
<domain:authInfo>
<domain:pw roid="JD1234-REP">2fooBAR</domain:pw>
<domain:pw>fooBAR</domain:pw>
</domain:authInfo>
</domain:transfer>
</transfer>

View File

@@ -6,7 +6,7 @@
<domain:name>example.tld</domain:name>
<domain:period unit="y">1</domain:period>
<domain:authInfo>
<domain:pw roid="JD1234-REP">2fooBAR</domain:pw>
<domain:pw>fooBAR</domain:pw>
</domain:authInfo>
</domain:transfer>
</transfer>

View File

@@ -6,7 +6,7 @@
<domain:name>example.tld</domain:name>
<domain:period unit="y">1</domain:period>
<domain:authInfo>
<domain:pw roid="JD1234-REP">2fooBAR</domain:pw>
<domain:pw>fooBAR</domain:pw>
</domain:authInfo>
</domain:transfer>
</transfer>

View File

@@ -6,7 +6,7 @@
<domain:name>example.tld</domain:name>
<domain:period unit="y">1</domain:period>
<domain:authInfo>
<domain:pw roid="JD1234-REP">2fooBAR</domain:pw>
<domain:pw>fooBAR</domain:pw>
</domain:authInfo>
</domain:transfer>
</transfer>

View File

@@ -5,7 +5,7 @@
xmlns:domain="urn:ietf:params:xml:ns:domain-1.0">
<domain:name>example.tld</domain:name>
<domain:authInfo>
<domain:pw roid="JD1234-REP">2fooBAR</domain:pw>
<domain:pw>fooBAR</domain:pw>
</domain:authInfo>
</domain:transfer>
</transfer>

View File

@@ -6,7 +6,7 @@
<domain:name>example.tld</domain:name>
<domain:period unit="m">1</domain:period>
<domain:authInfo>
<domain:pw roid="JD1234-REP">2fooBAR</domain:pw>
<domain:pw>fooBAR</domain:pw>
</domain:authInfo>
</domain:transfer>
</transfer>

View File

@@ -6,7 +6,7 @@
<domain:name>rich.example</domain:name>
<domain:period unit="y">1</domain:period>
<domain:authInfo>
<domain:pw roid="JD1234-REP">2fooBAR</domain:pw>
<domain:pw>fooBAR</domain:pw>
</domain:authInfo>
</domain:transfer>
</transfer>

View File

@@ -6,7 +6,7 @@
<domain:name>%DOMAIN%</domain:name>
<domain:period unit="y">%YEARS%</domain:period>
<domain:authInfo>
<domain:pw roid="JD1234-REP">2fooBAR</domain:pw>
<domain:pw>fooBAR</domain:pw>
</domain:authInfo>
</domain:transfer>
</transfer>

View File

@@ -46,7 +46,6 @@ BACKEND /_dr/task/readDnsRefreshRequests ReadDnsRefreshReques
BACKEND /_dr/task/refreshDnsForAllDomains RefreshDnsForAllDomainsAction GET n APP ADMIN
BACKEND /_dr/task/refreshDnsOnHostRename RefreshDnsOnHostRenameAction POST n APP ADMIN
BACKEND /_dr/task/relockDomain RelockDomainAction POST y APP ADMIN
BACKEND /_dr/task/removeAllDomainContacts RemoveAllDomainContactsAction POST n APP ADMIN
BACKEND /_dr/task/resaveAllEppResourcesPipeline ResaveAllEppResourcesPipelineAction GET n APP ADMIN
BACKEND /_dr/task/resaveEntity ResaveEntityAction POST n APP ADMIN
BACKEND /_dr/task/sendExpiringCertificateNotificationEmail SendExpiringCertificateNotificationEmailAction GET n APP ADMIN
@@ -58,7 +57,6 @@ BACKEND /_dr/task/tmchSmdrl TmchSmdrlAction
BACKEND /_dr/task/triggerMosApiServiceState TriggerServiceStateAction GET n APP ADMIN
BACKEND /_dr/task/updateRegistrarRdapBaseUrls UpdateRegistrarRdapBaseUrlsAction GET y APP ADMIN
BACKEND /_dr/task/uploadBsaUnavailableNames UploadBsaUnavailableDomainsAction GET,POST n APP ADMIN
BACKEND /_dr/task/wipeOutContactHistoryPii WipeOutContactHistoryPiiAction GET n APP ADMIN
PUBAPI /check CheckApiAction GET n NONE PUBLIC
PUBAPI /rdap/ RdapEmptyAction GET,HEAD n NONE PUBLIC
PUBAPI /rdap/autnum/(*) RdapAutnumAction GET,HEAD n NONE PUBLIC