mirror of
https://github.com/google/nomulus
synced 2026-01-31 10:02:28 +00:00
Compare commits
4 Commits
nomulus-20
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f8145b44f | ||
|
|
1fdacf25dc | ||
|
|
41d26d8385 | ||
|
|
71c9407f07 |
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}
|
||||
*
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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$"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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)
|
||||
|
||||
236
core/src/test/java/google/registry/mosapi/MosApiMetricsTest.java
Normal file
236
core/src/test/java/google/registry/mosapi/MosApiMetricsTest.java
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user