1
0
mirror of https://github.com/google/nomulus synced 2026-05-20 06:41:51 +00:00

Compare commits

...

30 Commits

Author SHA1 Message Date
Pavlo Tkach
17a21f3326 Update renew flow to accept and process REMOVEPACKAGE token (#1768) 2022-09-02 17:32:59 -04:00
Pavlo Tkach
f623da9948 Prohibit renewals of package domains unless REMOVEPACKAGE token is included (#1758) 2022-08-31 18:58:31 -04:00
gbrodman
ddc4a615db Fix a few DB issues with the User class (#1766)
- Create a sequence to generate IDs for the user (this allows us to have
  Long ID types so that Hibernate can autogenerate IDs)
- Add an update timestamp column so we can extend BackupGroupRoot
- Add a restriction that there can't be multiple users with the same
  email address
2022-08-31 16:09:07 -04:00
sarahcaseybot
06a1fc0022 Add a packageToken EPP extension for use in the DomainInfo flow (#1760)
* Add a packageToken EPP extension for use in the DomainInfo flow

* small fixes

* Change namespace
2022-08-30 17:50:42 -04:00
sarahcaseybot
eec272b6ba Increase max backoff seconds for dns-publish queue (#1764) 2022-08-29 16:30:56 -04:00
Ben McIlwain
3d5b52b853 Rename ContactResource -> Contact (#1763)
* Rename ContactResource -> Contact

This is a follow-up to PR #1725 and #1733. Now all EPP resource entity class
names have been rationalized to match with their SQL table names.
2022-08-29 14:48:32 -04:00
Lai Jiang
bd4af052a6 Remove ofy support from Address (#1759) 2022-08-26 12:35:48 -04:00
Pavlo Tkach
78249e1329 Replace PubApi master calls with replica (#1742) 2022-08-26 10:15:30 -04:00
gbrodman
7aec579d96 Add DB annotations to console User and related classes (#1757)
We added the DB code last week, this is the corresponding bit now that
that has been released.
2022-08-25 16:54:39 -04:00
Lai Jiang
b9f8faa165 Drop autorenew poll message history id column from the domain table (#1743)
We stopped using the column since #1732.
2022-08-25 15:52:32 -04:00
Pavlo Tkach
b0e4e86586 Add registry email to bcc for outgoing DNS failure emails (#1755) 2022-08-25 14:15:20 -04:00
gbrodman
3412f4417f Allow UserAuthInfo to contain either old GAE Users or new console Users (#1744)
This means that LegacyAuthenticationMechanism or a to-be-created
OAuth2AuthenticationMechanism) can return a UserAuthInfo object that
contains either the GAE User or the console User as appropriate. The
goal is that the non-auth flows shouldn't have to know about which user
type it is. Note: the registry lock flow (for now) needs to know about
the separate types of auth because it is a separate level of auth from
the standard AuthenticatedRegistrarAccessor.

The AuthenticatedRegistrarAccessor code is a bit odd because the new
role system doesn't quite fit neatly into the old registrar ->
OWNER,ADMIN system but this is a fine approximation. Basically, any
new registrar role will map to the old OWNER role.
2022-08-24 14:18:32 -04:00
sarahcaseybot
db6329a070 Add the PackagePromotion table (#1745)
* Add the PackagePromotion table

* Add long id

* Add NOT NULL

* fix formatting

* make package price non null

* Add not nulls to java file

* Fix broken tests from merge conflicts
2022-08-24 14:16:34 -04:00
gbrodman
02af277148 Allow usage of allocation tokens in nomulus create_domain (#1756)
Useful when doing internal registrations like get.boo
2022-08-24 13:18:53 -04:00
sarahcaseybot
8b02f76ae9 Add currentPackageToken on create flow (#1751)
* Add currentPackageToken on create flow

* Change to Truth8 assertion

* Add check for specified renewal behavior
2022-08-23 14:47:41 -04:00
gbrodman
6dd96c247a Reset the claims list cache in any test that saves to it (#1754) 2022-08-22 15:58:45 -04:00
gbrodman
919c744d8c Update currently-active ICANN-provided SMD test file (#1753)
The test files they provided before have expired, and they only provide
one valid currently-active test file now, so only test that one.

The test files are located at https://newgtlds.icann.org/en/about/trademark-clearinghouse/registries-registrars
2022-08-22 13:59:38 -04:00
gbrodman
5bccd65bd7 Add main method to ResaveAllEppResourcesPipeline (#1748)
Not sure how this got missed before, I am pretty sure we tested this on
alpha.
2022-08-22 12:39:34 -04:00
Lai Jiang
5268e35155 Remove redundant test extension (#1752)
This extension field is already defined in the super class.

<!-- Reviewable:start -->
- - -
This change is [<img src="https://reviewable.io/review_button.svg" height="34" align="absmiddle" alt="Reviewable"/>](https://reviewable.io/reviews/google/nomulus/1752)
<!-- Reviewable:end -->
2022-08-21 12:15:15 -04:00
Lai Jiang
6e201450f0 Remove ofy support from PollMessage (#1732)
Also deletes the autorenew poll message history revision id field in
Domain, which is only needed to recreate the ofy key for the poll
message. The column already contains null values in it, making it
impossible to depend on it. The column itself will be deleted from the
schema after this PR is deployed.

The logic to update autorenew recurrence end time is changed
accordingly: When a poll message already exists, we simply update the
endtime, but when it no longer exists, i. e. when it's deleted
speculatively after a transfer request, we recreate one using the
history entry id that resulted in its creation (e. g. cancelled or rejected
transfer).

This should fix b/240984498. Though the exact reason for that bug is
still unclear to me. Namely, it throws an NPE at this line during an
explicit domain transfer approval:

https://cs.opensource.google/nomulus/nomulus/+/master:core/src/main/java/google/registry/flows/domain/DomainFlowUtils.java;l=603;bpv=1;bpt=0;drc=ede919d7dcdb7f209b074563b3d449ebee19118a

The domain in question has a null autorenewPollMessageHistoryId, but
that in itself should not have caused an NPE because we are not
operating on the null pointer. On that line the only possible way to
throw an NPE is for the domain itself to be null, but if that were the
case, the NPE would have been thrown at line 599 where we called a
method on the domain object.

Regardless of the cause, with this PR we are using an explicitly
provided history id and checking for its nullness before using it. If a
similar issue arises again, we should have a better idea why.

Lastly, the way poll message id is constructed is largely simplified in
PollMessageExternalKeyConverter as a result of the removal ofy parent
keys in PollMessage. This does present a possibility of failure when
immediately before deployment, a registrar requests a poll message and
received the old id, but by the time the registrar acks the id, the new
version is deployed and therefore does not recognize the old key. The
likelihood of this happening should be slim, and we could have prevented
it by letting the converter recognize both the old and the new key.
However, we would like to eventually phase out the old key, and in
theory a registrar could ack a poll message at any time after it was
requested. So, there is not a safe time by which all the old ids are
acked, lest we develop some elaborate scheme to keep track of which
messages were sent with an old id when requested and which of these old
ids are acked. Only then can we be truly safe to phase out the old id.
The benefit does not seem to warrant the effort. If a registrar does
encounter a situation like this, they could open a support bug to have
us manually ack the poll message for them.
2022-08-19 14:24:03 -04:00
sarahcaseybot
dda9a3ef7e Flyway files for PackagePromotion table (#1746)
* Include missing file

* Fix merge conflicts

* make package price non null
2022-08-19 12:53:58 -04:00
gbrodman
8c1fb6bf00 Add another TestCacheExtension usage (#1750) 2022-08-19 10:17:15 -04:00
gbrodman
4e21152f04 Add TestCacheExtension in ResourceFlowTC to fix flakes (#1749)
Basically, what's happening here is that some flow tests are adding
things to the claims list cache which is stored statically, meaning that
some other tests can pick those up when they shouldn't. By adding the
extension in RFTC, it'll clear out the caches after each test.
2022-08-18 15:04:29 -04:00
gbrodman
22193474d5 Add Flyway and golden files for console User object (#1747) 2022-08-17 16:48:14 -04:00
Pavlo Tkach
efd5244ebd Add email notification when DNS update fails (#1734) 2022-08-16 12:59:08 -04:00
gbrodman
87e5d19fe5 Allow anchor tenant creation via allocation token behavior (#1735)
* Allow anchor tenant creation via allocation token behavior

This also enforces that non-superusers cannot create registrations on
trademarked names prior to the sunrise period, even if they have an
allocation token with ANCHOR_TENANT behavior.
2022-08-15 12:42:16 -04:00
Lai Jiang
bbb6174c9f Remove InjectExtension (#1739)
It is only used to set the clock used by Ofy, and it admits itself being
an ugly hack...

Also applied IntelliJ suggestions on touched files.
2022-08-12 14:56:53 -04:00
gbrodman
2b826651e6 Create a registry lock permission and corresponding account manager role (#1740)
* Create a registry lock permission and corresponding account manager role

This allows us to distinguish between standard account managers and
users that might have the registry lock permission. This will make the
registry lock password-setting flow easier (user can reset their
password iff they have the REGISTRY_LOCK permission, instead of having a
separate boolean) and allows us to easily determine whether or not a
user should have access to registry lock views in the UI.
2022-08-12 12:18:09 -04:00
Lai Jiang
e4132db8ed Delete SetClockExtension (#1738)
We no longer write to commit logs, and the ReplayExtension that this
extension is supposed to be used with is already deleted.
2022-08-11 13:16:30 -04:00
Pavlo Tkach
45d90e7c68 Extend IP validation test with message verification (#1736) 2022-08-10 13:27:55 -04:00
262 changed files with 7692 additions and 5489 deletions

View File

@@ -29,7 +29,7 @@ import google.registry.config.RegistryEnvironment;
import google.registry.flows.poll.PollFlowUtils;
import google.registry.model.EppResource;
import google.registry.model.EppResourceUtils;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.Contact;
import google.registry.model.domain.Domain;
import google.registry.model.host.Host;
import google.registry.model.poll.PollMessage;
@@ -43,8 +43,8 @@ import google.registry.util.Clock;
import javax.inject.Inject;
/**
* Hard deletes load-test ContactResources, Hosts, their subordinate history entries, and the
* associated ForeignKey and EppResourceIndex entities.
* Hard deletes load-test Contacts, Hosts, their subordinate history entries, and the associated
* ForeignKey and EppResourceIndex entities.
*
* <p>This only deletes contacts and hosts, NOT domains. To delete domains, use {@link
* DeleteProberDataAction} and pass it the TLD(s) that the load test domains were created on. Note
@@ -92,7 +92,7 @@ public class DeleteLoadTestDataAction implements Runnable {
tm().transact(
() -> {
LOAD_TEST_REGISTRARS.forEach(this::deletePollMessages);
tm().loadAllOfStream(ContactResource.class).forEach(this::deleteContact);
tm().loadAllOfStream(Contact.class).forEach(this::deleteContact);
tm().loadAllOfStream(Host.class).forEach(this::deleteHost);
});
}
@@ -108,7 +108,7 @@ public class DeleteLoadTestDataAction implements Runnable {
}
}
private void deleteContact(ContactResource contact) {
private void deleteContact(Contact contact) {
if (!LOAD_TEST_REGISTRARS.contains(contact.getPersistedCurrentSponsorRegistrarId())) {
return;
}

View File

@@ -17,7 +17,7 @@ package google.registry.beam.common;
import static com.google.common.base.Verify.verify;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.Contact;
import google.registry.persistence.transaction.CriteriaQueryBuilder;
import google.registry.persistence.transaction.JpaTransactionManager;
import java.io.Serializable;
@@ -46,8 +46,7 @@ public class JpaDemoPipeline implements Serializable {
.apply(
"Read contacts",
RegistryJpaIO.read(
() -> CriteriaQueryBuilder.create(ContactResource.class).build(),
ContactResource::getRepoId))
() -> CriteriaQueryBuilder.create(Contact.class).build(), Contact::getRepoId))
.apply(
"Count Contacts",
ParDo.of(

View File

@@ -45,8 +45,8 @@ import google.registry.config.CredentialModule;
import google.registry.config.RegistryConfig.ConfigModule;
import google.registry.gcs.GcsUtils;
import google.registry.model.EppResource;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.host.Host;
@@ -140,7 +140,7 @@ import org.joda.time.DateTime;
* pairs of (contact/host repo ID: pending deposit) for all RDE pending deposits for further
* processing.
*
* <h3>{@link ContactResource}</h3>
* <h3>{@link Contact}</h3>
*
* We first join most recent contact histories, represented by (contact repo ID: contact history
* revision ID) pairs, with referenced contacts, represented by (contact repo ID: pending deposit)
@@ -150,13 +150,13 @@ import org.joda.time.DateTime;
*
* <h3>{@link Host}</h3>
*
* Similar to {@link ContactResource}, we join the most recent host history with referenced hosts to
* find most recent referenced hosts. For external hosts we do the same treatment as we did on
* contacts and obtain the (pending deposit: deposit fragment) pairs. For subordinate hosts, we need
* to find the superordinate domain in order to properly handle pending transfer in the deposit as
* well. So we first find the superordinate domain repo ID from the host and join the (superordinate
* domain repo ID: (subordinate host repo ID: (pending deposit: revision ID))) pair with the (domain
* repo ID: revision ID) pair obtained from the domain history query in order to map the host at
* Similar to {@link Contact}, we join the most recent host history with referenced hosts to find
* most recent referenced hosts. For external hosts we do the same treatment as we did on contacts
* and obtain the (pending deposit: deposit fragment) pairs. For subordinate hosts, we need to find
* the superordinate domain in order to properly handle pending transfer in the deposit as well. So
* we first find the superordinate domain repo ID from the host and join the (superordinate domain
* repo ID: (subordinate host repo ID: (pending deposit: revision ID))) pair with the (domain repo
* ID: revision ID) pair obtained from the domain history query in order to map the host at
* watermark to the domain at watermark. We then proceed to create the (pending deposit: deposit
* fragment) pair for subordinate hosts using the added domain information.
*
@@ -466,10 +466,10 @@ public class RdePipeline implements Serializable {
private PCollectionTuple processDomainHistories(PCollection<KV<String, Long>> domainHistories) {
Counter activeDomainCounter = Metrics.counter("RDE", "ActiveDomainBase");
Counter domainFragmentCounter = Metrics.counter("RDE", "DomainFragment");
Counter referencedContactCounter = Metrics.counter("RDE", "ReferencedContactResource");
Counter referencedContactCounter = Metrics.counter("RDE", "ReferencedContact");
Counter referencedHostCounter = Metrics.counter("RDE", "ReferencedHost");
return domainHistories.apply(
"Map DomainHistory to DepositFragment " + "and emit referenced ContactResource and Host",
"Map DomainHistory to DepositFragment " + "and emit referenced Contact and Host",
ParDo.of(
new DoFn<KV<String, Long>, KV<PendingDeposit, DepositFragment>>() {
@ProcessElement
@@ -533,17 +533,17 @@ public class RdePipeline implements Serializable {
PCollection<KV<String, PendingDeposit>> referencedContacts,
PCollection<KV<String, Long>> contactHistories) {
Counter contactFragmentCounter = Metrics.counter("RDE", "ContactFragment");
return removeUnreferencedResource(referencedContacts, contactHistories, ContactResource.class)
return removeUnreferencedResource(referencedContacts, contactHistories, Contact.class)
.apply(
"Map ContactResource to DepositFragment",
"Map Contact to DepositFragment",
FlatMapElements.into(
kvs(
TypeDescriptor.of(PendingDeposit.class),
TypeDescriptor.of(DepositFragment.class)))
.via(
(KV<String, CoGbkResult> kv) -> {
ContactResource contact =
(ContactResource)
Contact contact =
(Contact)
loadResourceByHistoryEntryId(
ContactHistory.class,
kv.getKey(),

View File

@@ -22,7 +22,7 @@ import com.google.common.collect.ImmutableSet;
import google.registry.beam.common.RegistryJpaIO;
import google.registry.beam.common.RegistryJpaIO.Read;
import google.registry.model.EppResource;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.Contact;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainBase;
import google.registry.model.host.Host;
@@ -33,6 +33,7 @@ import java.io.Serializable;
import java.util.concurrent.ThreadLocalRandom;
import org.apache.beam.sdk.Pipeline;
import org.apache.beam.sdk.PipelineResult;
import org.apache.beam.sdk.options.PipelineOptionsFactory;
import org.apache.beam.sdk.transforms.DoFn;
import org.apache.beam.sdk.transforms.GroupIntoBatches;
import org.apache.beam.sdk.transforms.ParDo;
@@ -52,7 +53,7 @@ import org.joda.time.DateTime;
public class ResaveAllEppResourcesPipeline implements Serializable {
private static final ImmutableSet<Class<? extends EppResource>> EPP_RESOURCE_CLASSES =
ImmutableSet.of(ContactResource.class, Domain.class, Host.class);
ImmutableSet.of(Contact.class, Domain.class, Host.class);
/**
* There exist three possible situations where we know we'll want to project domains to the
@@ -98,13 +99,13 @@ public class ResaveAllEppResourcesPipeline implements Serializable {
/** Projects to the current time and saves any contacts with expired transfers. */
private void fastResaveContacts(Pipeline pipeline) {
Read<ContactResource, ContactResource> read =
Read<Contact, Contact> read =
RegistryJpaIO.read(
"FROM Contact WHERE transferData.transferStatus = 'PENDING' AND"
+ " transferData.pendingTransferExpirationTime < current_timestamp()",
ContactResource.class,
Contact.class,
c -> c);
projectAndResaveResources(pipeline, ContactResource.class, read);
projectAndResaveResources(pipeline, Contact.class, read);
}
/**
@@ -172,4 +173,13 @@ public class ResaveAllEppResourcesPipeline implements Serializable {
resource.cloneProjectedAtTime(jpaTm().getTransactionTime()))));
}
}
public static void main(String[] args) {
PipelineOptionsFactory.register(ResaveAllEppResourcesPipelineOptions.class);
ResaveAllEppResourcesPipelineOptions options =
PipelineOptionsFactory.fromArgs(args)
.withValidation()
.as(ResaveAllEppResourcesPipelineOptions.class);
new ResaveAllEppResourcesPipeline(options).run();
}
}

View File

@@ -1300,6 +1300,36 @@ public final class RegistryConfig {
return config.sslCertificateValidation.expirationWarningEmailSubjectText;
}
@Provides
@Config("dnsUpdateFailEmailSubjectText")
public static String provideDnsUpdateFailEmailSubjectText(RegistryConfigSettings config) {
return config.dnsUpdate.dnsUpdateFailEmailSubjectText;
}
@Provides
@Config("dnsUpdateFailEmailBodyText")
public static String provideDnsUpdateFailEmailBodyText(RegistryConfigSettings config) {
return config.dnsUpdate.dnsUpdateFailEmailBodyText;
}
@Provides
@Config("dnsUpdateFailRegistryName")
public static String provideDnsUpdateFailRegistryName(RegistryConfigSettings config) {
return config.dnsUpdate.dnsUpdateFailRegistryName;
}
@Provides
@Config("registrySupportEmail")
public static InternetAddress provideRegistrySupportEmail(RegistryConfigSettings config) {
return parseEmailAddress(config.dnsUpdate.registrySupportEmail);
}
@Provides
@Config("registryCcEmail")
public static InternetAddress provideRegistryCcEmail(RegistryConfigSettings config) {
return parseEmailAddress(config.dnsUpdate.registryCcEmail);
}
@Provides
@Config("allowedEcdsaCurves")
public static ImmutableSet<String> provideAllowedEcdsaCurves(RegistryConfigSettings config) {

View File

@@ -42,6 +42,7 @@ public class RegistryConfigSettings {
public RegistryTool registryTool;
public SslCertificateValidation sslCertificateValidation;
public ContactHistory contactHistory;
public DnsUpdate dnsUpdate;
/** Configuration options that apply to the entire App Engine project. */
public static class AppEngine {
@@ -246,4 +247,13 @@ public class RegistryConfigSettings {
public int minMonthsBeforeWipeOut;
public int wipeOutQueryBatchSize;
}
/** Configuration for dns update. */
public static class DnsUpdate {
public String dnsUpdateFailEmailSubjectText;
public String dnsUpdateFailEmailBodyText;
public String dnsUpdateFailRegistryName;
public String registrySupportEmail;
public String registryCcEmail;
}
}

View File

@@ -477,6 +477,29 @@ contactHistory:
# The batch size for querying ContactHistory table in the database.
wipeOutQueryBatchSize: 500
# Configuration options relevant to the DNS update functionality.
dnsUpdate:
dnsUpdateFailRegistryName: Example name
registrySupportEmail: email@example.com
registryCcEmail: email@example.com
# Email subject text template to notify partners after repeatedly failing DNS update
dnsUpdateFailEmailSubjectText: "[ACTION REQUIRED]: Incomplete DNS Update"
# Email body text template for failing DNS update that accepts 5 parameters:
# registrar name, domain or host address, 'domain' or 'host' as a string that failed,
# registry support email (see dnsUpdateFailRegistrySupportEmail) and registry display name
dnsUpdateFailEmailBodyText: >
Dear %1$s,
We are contacting you regarding the changes you recently made to one of your %2$ss.
The DNS update for the %3$s %2$s failed to process. Please review your %2$s's DNS records
and ensure that it is valid before trying another update.
If you have any questions or require additional support, please contact us
at %4$s.
Regards,
%5$s
# Configuration options for checking SSL certificates.
sslCertificateValidation:
# A map specifying the maximum amount of days the certificate can be valid.

View File

@@ -14,6 +14,7 @@
package google.registry.dns;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.dns.DnsConstants.DNS_PUBLISH_PUSH_QUEUE_NAME;
import static google.registry.dns.DnsModule.PARAM_DNS_WRITER;
import static google.registry.dns.DnsModule.PARAM_DOMAINS;
@@ -22,6 +23,7 @@ import static google.registry.dns.DnsModule.PARAM_LOCK_INDEX;
import static google.registry.dns.DnsModule.PARAM_NUM_PUBLISH_LOCKS;
import static google.registry.dns.DnsModule.PARAM_PUBLISH_TASK_ENQUEUED;
import static google.registry.dns.DnsModule.PARAM_REFRESH_REQUEST_CREATED;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.request.Action.Method.POST;
import static google.registry.request.RequestParameters.PARAM_TLD;
import static google.registry.util.CollectionUtils.nullToEmpty;
@@ -32,11 +34,16 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.InternetDomainName;
import dagger.Lazy;
import google.registry.config.RegistryConfig.Config;
import google.registry.dns.DnsMetrics.ActionStatus;
import google.registry.dns.DnsMetrics.CommitStatus;
import google.registry.dns.DnsMetrics.PublishStatus;
import google.registry.dns.writer.DnsWriter;
import google.registry.model.domain.Domain;
import google.registry.model.host.Host;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.model.tld.Registry;
import google.registry.request.Action;
import google.registry.request.Action.Service;
@@ -49,10 +56,13 @@ import google.registry.request.lock.LockHandler;
import google.registry.util.Clock;
import google.registry.util.CloudTasksUtils;
import google.registry.util.DomainNameUtils;
import google.registry.util.EmailMessage;
import google.registry.util.SendEmailService;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
import javax.inject.Inject;
import javax.mail.internet.InternetAddress;
import org.joda.time.DateTime;
import org.joda.time.Duration;
@@ -102,6 +112,13 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
private final Clock clock;
private final CloudTasksUtils cloudTasksUtils;
private final Response response;
private final SendEmailService sendEmailService;
private final String dnsUpdateFailEmailSubjectText;
private final String dnsUpdateFailEmailBodyText;
private final String dnsUpdateFailRegistryName;
private final Lazy<InternetAddress> registrySupportEmail;
private final Lazy<InternetAddress> registryCcEmail;
private final InternetAddress gSuiteOutgoingEmailAddress;
@Inject
public PublishDnsUpdatesAction(
@@ -114,6 +131,12 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
@Parameter(PARAM_HOSTS) Set<String> hosts,
@Parameter(PARAM_TLD) String tld,
@Config("publishDnsUpdatesLockDuration") Duration timeout,
@Config("dnsUpdateFailEmailSubjectText") String dnsUpdateFailEmailSubjectText,
@Config("dnsUpdateFailEmailBodyText") String dnsUpdateFailEmailBodyText,
@Config("dnsUpdateFailRegistryName") String dnsUpdateFailRegistryName,
@Config("registrySupportEmail") Lazy<InternetAddress> registrySupportEmail,
@Config("registryCcEmail") Lazy<InternetAddress> registryCcEmail,
@Config("gSuiteOutgoingEmailAddress") InternetAddress gSuiteOutgoingEmailAddress,
@Header(APP_ENGINE_RETRY_HEADER) Optional<Integer> appEngineRetryCount,
@Header(CLOUD_TASKS_RETRY_HEADER) Optional<Integer> cloudTasksRetryCount,
DnsQueue dnsQueue,
@@ -122,11 +145,13 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
LockHandler lockHandler,
Clock clock,
CloudTasksUtils cloudTasksUtils,
SendEmailService sendEmailService,
Response response) {
this.dnsQueue = dnsQueue;
this.dnsWriterProxy = dnsWriterProxy;
this.dnsMetrics = dnsMetrics;
this.timeout = timeout;
this.sendEmailService = sendEmailService;
this.retryCount =
cloudTasksRetryCount.orElse(
appEngineRetryCount.orElseThrow(
@@ -143,6 +168,12 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
this.clock = clock;
this.cloudTasksUtils = cloudTasksUtils;
this.response = response;
this.dnsUpdateFailEmailBodyText = dnsUpdateFailEmailBodyText;
this.dnsUpdateFailEmailSubjectText = dnsUpdateFailEmailSubjectText;
this.dnsUpdateFailRegistryName = dnsUpdateFailRegistryName;
this.registrySupportEmail = registrySupportEmail;
this.registryCcEmail = registryCcEmail;
this.gSuiteOutgoingEmailAddress = gSuiteOutgoingEmailAddress;
}
private void recordActionResult(ActionStatus status) {
@@ -209,9 +240,35 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
} else if (retryCount < RETRIES_BEFORE_PERMANENT_FAILURE) {
// If the batch only contains 1 name, allow it more retries
throw e;
} else {
// By the time we get here there's either single domain or a single host
domains.stream()
.findFirst()
.ifPresent(
dn -> {
Optional<Domain> domain = loadByForeignKey(Domain.class, dn, clock.nowUtc());
if (domain.isPresent()) {
notifyWithEmailAboutDnsUpdateFailure(
domain.get().getCurrentSponsorRegistrarId(), dn, false);
} else {
logger.atSevere().log(String.format("Domain entity for %s not found", dn));
}
});
hosts.stream()
.findFirst()
.ifPresent(
hn -> {
Optional<Host> host = loadByForeignKey(Host.class, hn, clock.nowUtc());
if (host.isPresent()) {
notifyWithEmailAboutDnsUpdateFailure(
host.get().getPersistedCurrentSponsorRegistrarId(), hn, true);
} else {
logger.atSevere().log(String.format("Host entity for %s not found", hn));
}
});
}
// If we get here, we should terminate this task as it is likely a perpetually failing task.
// TODO(b/237302821): Send an email notifying partner the dns update failed
recordActionResult(ActionStatus.MAX_RETRIES_EXCEEDED);
response.setStatus(SC_ACCEPTED);
logger.atSevere().withCause(e).log("Terminated task after too many retries");
@@ -219,6 +276,53 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
return null;
}
private InternetAddress emailToInternetAddress(String email) {
try {
return new InternetAddress(email, true);
} catch (Exception e) {
logger.atWarning().withCause(e).log(
String.format(
"Could not parse email contact %s to send DNS failure notification", email));
return null;
}
}
/** Sends an email to partners regarding a failure during DNS update */
private void notifyWithEmailAboutDnsUpdateFailure(
String registrarId, String hostOrDomainName, Boolean isHost) {
Optional<Registrar> registrar = Registrar.loadByRegistrarIdCached(registrarId);
if (registrar.isPresent()) {
String body =
String.format(
dnsUpdateFailEmailBodyText,
registrar.get().getRegistrarName(),
hostOrDomainName,
isHost ? "host" : "domain",
registrySupportEmail.get().getAddress(),
dnsUpdateFailRegistryName);
ImmutableList<InternetAddress> recipients =
registrar.get().getContacts().stream()
.filter(c -> c.getTypes().contains(RegistrarPoc.Type.ADMIN))
.map(RegistrarPoc::getEmailAddress)
.map(this::emailToInternetAddress)
.collect(toImmutableList());
sendEmailService.sendEmail(
EmailMessage.newBuilder()
.setBody(body)
.setSubject(dnsUpdateFailEmailSubjectText)
.setRecipients(recipients)
.addBcc(registryCcEmail.get())
.setFrom(gSuiteOutgoingEmailAddress)
.build());
} else {
logger.atSevere().log(String.format("Could not find registrar %s", registrarId));
}
}
/** Splits the domains and hosts in a batch into smaller batches and adds them to the queue. */
private void splitBatch() {
ImmutableList<String> domainList = ImmutableList.copyOf(domains);
@@ -294,8 +398,7 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
int domainsPublished = 0;
int domainsRejected = 0;
for (String domain : nullToEmpty(domains)) {
if (!DomainNameUtils.isUnder(
InternetDomainName.from(domain), InternetDomainName.from(tld))) {
if (!DomainNameUtils.isUnder(InternetDomainName.from(domain), InternetDomainName.from(tld))) {
logger.atSevere().log("%s: skipping domain %s not under TLD.", tld, domain);
domainsRejected += 1;
} else {
@@ -310,8 +413,7 @@ public final class PublishDnsUpdatesAction implements Runnable, Callable<Void> {
int hostsPublished = 0;
int hostsRejected = 0;
for (String host : nullToEmpty(hosts)) {
if (!DomainNameUtils.isUnder(
InternetDomainName.from(host), InternetDomainName.from(tld))) {
if (!DomainNameUtils.isUnder(InternetDomainName.from(host), InternetDomainName.from(tld))) {
logger.atSevere().log("%s: skipping host %s not under TLD.", tld, host);
hostsRejected += 1;
} else {

View File

@@ -1,6 +1,6 @@
<datastore-indexes autoGenerate="false">
<!-- For finding contact resources by registrar. -->
<datastore-index kind="ContactResource" ancestor="false" source="manual">
<datastore-index kind="Contact" ancestor="false" source="manual">
<property name="currentSponsorClientId" direction="asc"/>
<property name="deletionTime" direction="asc"/>
<property name="searchName" direction="asc"/>
@@ -91,7 +91,7 @@
<property name="deletionTime" direction="asc"/>
<property name="fullyQualifiedHostName" direction="asc"/>
</datastore-index>
<datastore-index kind="ContactResource" ancestor="false" source="manual">
<datastore-index kind="Contact" ancestor="false" source="manual">
<property name="deletionTime" direction="asc"/>
<property name="searchName" direction="asc"/>
</datastore-index>

View File

@@ -10,10 +10,10 @@
<name>dns-publish</name>
<rate>100/s</rate>
<bucket-size>100</bucket-size>
<!-- 30 sec backoff increasing linearly up to 10 minutes. -->
<!-- 30 sec backoff increasing linearly up to 30 minutes. -->
<retry-parameters>
<min-backoff-seconds>30</min-backoff-seconds>
<max-backoff-seconds>600</max-backoff-seconds>
<max-backoff-seconds>1800</max-backoff-seconds>
<max-doublings>0</max-doublings>
</retry-parameters>
</queue>

View File

@@ -38,7 +38,7 @@ import google.registry.flows.exceptions.TooManyResourceChecksException;
import google.registry.model.EppResource;
import google.registry.model.EppResource.ForeignKeyedEppResource;
import google.registry.model.EppResource.ResourceWithTransferData;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.Contact;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.Period;
@@ -139,7 +139,7 @@ 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, ContactResource contact)
public static void verifyOptionalAuthInfo(Optional<AuthInfo> authInfo, Contact contact)
throws EppException {
if (authInfo.isPresent()) {
verifyAuthInfo(authInfo.get(), contact);
@@ -167,7 +167,7 @@ public final class ResourceFlowUtils {
return;
}
// The roid should match one of the contacts.
Optional<VKey<ContactResource>> foundContact =
Optional<VKey<Contact>> foundContact =
domain.getReferencedContacts().stream()
.filter(key -> key.getSqlKey().equals(authRepoId))
.findFirst();
@@ -179,8 +179,7 @@ public final class ResourceFlowUtils {
}
/** Check that the given {@link AuthInfo} is valid for the given contact. */
public static void verifyAuthInfo(AuthInfo authInfo, ContactResource contact)
throws EppException {
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();

View File

@@ -220,7 +220,8 @@ public class TlsCredentials implements TransportCredentials {
super(
clientInetAddr.isPresent()
? String.format(
"Registrar IP address %s is not in stored allow list", clientInetAddr.get())
"Registrar IP address %s is not in stored allow list",
clientInetAddr.get().getHostAddress())
: "Registrar IP address is not in stored allow list");
}
}

View File

@@ -26,8 +26,8 @@ import google.registry.flows.ExtensionManager;
import google.registry.flows.Flow;
import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactCommand.Check;
import google.registry.model.contact.ContactResource;
import google.registry.model.eppinput.ResourceCommand;
import google.registry.model.eppoutput.CheckData.ContactCheck;
import google.registry.model.eppoutput.CheckData.ContactCheckData;
@@ -62,7 +62,7 @@ public final class ContactCheckFlow implements Flow {
ImmutableList<String> targetIds = ((Check) resourceCommand).getTargetIds();
verifyTargetIdCount(targetIds, maxChecks);
ImmutableSet<String> existingIds =
checkResourcesExist(ContactResource.class, targetIds, clock.nowUtc());
checkResourcesExist(Contact.class, targetIds, clock.nowUtc());
ImmutableList.Builder<ContactCheck> checks = new ImmutableList.Builder<>();
for (String id : targetIds) {
boolean unused = !existingIds.contains(id);

View File

@@ -33,9 +33,9 @@ import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.flows.exceptions.ResourceAlreadyExistsForThisClientException;
import google.registry.flows.exceptions.ResourceCreateContentionException;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactCommand.Create;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppinput.ResourceCommand;
import google.registry.model.eppoutput.CreateData.ContactCreateData;
@@ -75,9 +75,9 @@ public final class ContactCreateFlow implements TransactionalFlow {
extensionManager.validate();
Create command = (Create) resourceCommand;
DateTime now = tm().getTransactionTime();
verifyResourceDoesNotExist(ContactResource.class, targetId, now, registrarId);
ContactResource newContact =
new ContactResource.Builder()
verifyResourceDoesNotExist(Contact.class, targetId, now, registrarId);
Contact newContact =
new Contact.Builder()
.setContactId(targetId)
.setAuthInfo(command.getAuthInfo())
.setCreationRegistrarId(registrarId)

View File

@@ -35,8 +35,8 @@ import google.registry.flows.FlowModule.Superuser;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppcommon.StatusValue;
@@ -91,15 +91,15 @@ public final class ContactDeleteFlow implements TransactionalFlow {
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate();
DateTime now = tm().getTransactionTime();
checkLinkedDomains(targetId, now, ContactResource.class);
ContactResource existingContact = loadAndVerifyExistence(ContactResource.class, targetId, now);
checkLinkedDomains(targetId, now, Contact.class);
Contact existingContact = loadAndVerifyExistence(Contact.class, targetId, now);
verifyNoDisallowedStatuses(existingContact, DISALLOWED_STATUSES);
verifyOptionalAuthInfo(authInfo, existingContact);
if (!isSuperuser) {
verifyResourceOwnership(registrarId, existingContact);
}
// Handle pending transfers on contact deletion.
ContactResource newContact =
Contact newContact =
existingContact.getStatusValues().contains(StatusValue.PENDING_TRANSFER)
? denyPendingTransfer(existingContact, SERVER_CANCELLED, now, registrarId)
: existingContact;

View File

@@ -24,10 +24,10 @@ import com.googlecode.objectify.Key;
import google.registry.flows.EppException;
import google.registry.flows.EppException.ParameterValuePolicyErrorException;
import google.registry.flows.EppException.ParameterValueSyntaxErrorException;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactAddress;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactHistory.ContactHistoryId;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.PostalInfo;
import google.registry.model.poll.PendingActionNotificationResponse.ContactPendingActionNotificationResponse;
import google.registry.model.poll.PollMessage;
@@ -61,7 +61,7 @@ public class ContactFlowUtils {
}
/** Check contact's state against server policy. */
static void validateContactAgainstPolicy(ContactResource contact) throws EppException {
static void validateContactAgainstPolicy(Contact contact) throws EppException {
if (contact.getDisclose() != null && !contact.getDisclose().getFlag()) {
throw new DeclineContactDisclosureFieldDisallowedPolicyException();
}

View File

@@ -27,8 +27,8 @@ import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.FlowModule.Superuser;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactInfoData;
import google.registry.model.contact.ContactResource;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppoutput.EppResponse;
@@ -69,7 +69,7 @@ public final class ContactInfoFlow implements Flow {
DateTime now = clock.nowUtc();
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate(); // There are no legal extensions for this flow.
ContactResource contact = loadAndVerifyExistence(ContactResource.class, targetId, now);
Contact contact = loadAndVerifyExistence(Contact.class, targetId, now);
if (!isSuperuser) {
verifyResourceOwnership(registrarId, contact);
}

View File

@@ -33,8 +33,8 @@ import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppinput.ResourceCommand;
@@ -74,7 +74,7 @@ public final class ContactTransferApproveFlow implements TransactionalFlow {
/**
* The logic in this flow, which handles client approvals, very closely parallels the logic in
* {@link ContactResource#cloneProjectedAtTime} which handles implicit server approvals.
* {@link Contact#cloneProjectedAtTime} which handles implicit server approvals.
*/
@Override
public EppResponse run() throws EppException {
@@ -82,11 +82,11 @@ public final class ContactTransferApproveFlow implements TransactionalFlow {
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate();
DateTime now = tm().getTransactionTime();
ContactResource existingContact = loadAndVerifyExistence(ContactResource.class, targetId, now);
Contact existingContact = loadAndVerifyExistence(Contact.class, targetId, now);
verifyOptionalAuthInfo(authInfo, existingContact);
verifyHasPendingTransfer(existingContact);
verifyResourceOwnership(registrarId, existingContact);
ContactResource newContact =
Contact newContact =
approvePendingTransfer(existingContact, TransferStatus.CLIENT_APPROVED, now);
ContactHistory contactHistory =
historyBuilder.setType(CONTACT_TRANSFER_APPROVE).setContact(newContact).build();

View File

@@ -33,8 +33,8 @@ import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppinput.ResourceCommand;
@@ -78,11 +78,11 @@ public final class ContactTransferCancelFlow implements TransactionalFlow {
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate();
DateTime now = tm().getTransactionTime();
ContactResource existingContact = loadAndVerifyExistence(ContactResource.class, targetId, now);
Contact existingContact = loadAndVerifyExistence(Contact.class, targetId, now);
verifyOptionalAuthInfo(authInfo, existingContact);
verifyHasPendingTransfer(existingContact);
verifyTransferInitiator(registrarId, existingContact);
ContactResource newContact =
Contact newContact =
denyPendingTransfer(existingContact, TransferStatus.CLIENT_CANCELLED, now, registrarId);
ContactHistory contactHistory =
historyBuilder.setType(CONTACT_TRANSFER_CANCEL).setContact(newContact).build();

View File

@@ -27,7 +27,7 @@ import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.flows.exceptions.NoTransferHistoryToQueryException;
import google.registry.flows.exceptions.NotAuthorizedToViewTransferException;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.Contact;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppoutput.EppResponse;
import google.registry.model.reporting.IcannReportingTypes.ActivityReportField;
@@ -66,8 +66,7 @@ public final class ContactTransferQueryFlow implements Flow {
public EppResponse run() throws EppException {
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate(); // There are no legal extensions for this flow.
ContactResource contact =
loadAndVerifyExistence(ContactResource.class, targetId, clock.nowUtc());
Contact contact = loadAndVerifyExistence(Contact.class, targetId, clock.nowUtc());
verifyOptionalAuthInfo(authInfo, contact);
// Most of the fields on the transfer response are required, so there's no way to return valid
// XML if the object has never been transferred (and hence the fields aren't populated).

View File

@@ -33,8 +33,8 @@ import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppoutput.EppResponse;
@@ -76,11 +76,11 @@ public final class ContactTransferRejectFlow implements TransactionalFlow {
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate();
DateTime now = tm().getTransactionTime();
ContactResource existingContact = loadAndVerifyExistence(ContactResource.class, targetId, now);
Contact existingContact = loadAndVerifyExistence(Contact.class, targetId, now);
verifyOptionalAuthInfo(authInfo, existingContact);
verifyHasPendingTransfer(existingContact);
verifyResourceOwnership(registrarId, existingContact);
ContactResource newContact =
Contact newContact =
denyPendingTransfer(existingContact, TransferStatus.CLIENT_REJECTED, now, registrarId);
ContactHistory contactHistory =
historyBuilder.setType(CONTACT_TRANSFER_REJECT).setContact(newContact).build();

View File

@@ -38,8 +38,8 @@ import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.flows.exceptions.AlreadyPendingTransferException;
import google.registry.flows.exceptions.ObjectAlreadySponsoredException;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppcommon.StatusValue;
@@ -96,7 +96,7 @@ public final class ContactTransferRequestFlow implements TransactionalFlow {
validateRegistrarIsLoggedIn(gainingClientId);
extensionManager.validate();
DateTime now = tm().getTransactionTime();
ContactResource existingContact = loadAndVerifyExistence(ContactResource.class, targetId, now);
Contact existingContact = loadAndVerifyExistence(Contact.class, targetId, now);
verifyAuthInfoPresentForResourceTransfer(authInfo);
verifyAuthInfo(authInfo.get(), existingContact);
// Verify that the resource does not already have a pending transfer.
@@ -146,10 +146,12 @@ public final class ContactTransferRequestFlow implements TransactionalFlow {
.asBuilder()
.setEventTime(now) // Unlike the serverApprove messages, this applies immediately.
.build();
ContactResource newContact = existingContact.asBuilder()
.setTransferData(pendingTransferData)
.addStatusValue(StatusValue.PENDING_TRANSFER)
.build();
Contact newContact =
existingContact
.asBuilder()
.setTransferData(pendingTransferData)
.addStatusValue(StatusValue.PENDING_TRANSFER)
.build();
tm().update(newContact);
tm().insertAll(
ImmutableSet.of(

View File

@@ -36,10 +36,10 @@ import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.TransactionalFlow;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.flows.exceptions.ResourceHasClientUpdateProhibitedException;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactCommand.Update;
import google.registry.model.contact.ContactCommand.Update.Change;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.PostalInfo;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.eppcommon.AuthInfo;
@@ -94,7 +94,7 @@ public final class ContactUpdateFlow implements TransactionalFlow {
extensionManager.validate();
Update command = (Update) resourceCommand;
DateTime now = tm().getTransactionTime();
ContactResource existingContact = loadAndVerifyExistence(ContactResource.class, targetId, now);
Contact existingContact = loadAndVerifyExistence(Contact.class, targetId, now);
verifyOptionalAuthInfo(authInfo, existingContact);
ImmutableSet<StatusValue> statusToRemove = command.getInnerRemove().getStatusValues();
ImmutableSet<StatusValue> statusesToAdd = command.getInnerAdd().getStatusValues();
@@ -104,7 +104,7 @@ public final class ContactUpdateFlow implements TransactionalFlow {
}
verifyNoDisallowedStatuses(existingContact, DISALLOWED_STATUSES);
checkSameValuesNotAddedAndRemoved(statusesToAdd, statusToRemove);
ContactResource.Builder builder = existingContact.asBuilder();
Contact.Builder builder = existingContact.asBuilder();
Change change = command.getInnerChange();
// The spec requires the following behaviors:
// * If you update part of a postal info, the fields that you didn't update are unchanged.
@@ -126,7 +126,7 @@ public final class ContactUpdateFlow implements TransactionalFlow {
builder.setInternationalizedPostalInfo(null);
}
}
ContactResource newContact =
Contact newContact =
builder
.setLastEppUpdateTime(now)
.setLastEppUpdateRegistrarId(registrarId)

View File

@@ -153,6 +153,7 @@ import org.joda.time.Duration;
* @error {@link DomainCreateFlow.AnchorTenantCreatePeriodException}
* @error {@link DomainCreateFlow.MustHaveSignedMarksInCurrentPhaseException}
* @error {@link DomainCreateFlow.NoGeneralRegistrationsInCurrentPhaseException}
* @error {@link DomainCreateFlow.NoTrademarkedRegistrationsBeforeSunriseException}
* @error {@link DomainCreateFlow.SignedMarksOnlyDuringSunriseException}
* @error {@link DomainFlowTmchUtils.NoMarksFoundMatchingDomainException}
* @error {@link DomainFlowTmchUtils.FoundMarkNotYetValidException}
@@ -376,8 +377,7 @@ public final class DomainCreateFlow implements TransactionalFlow {
.setIdnTableName(validateDomainNameWithIdnTables(domainName))
.setRegistrationExpirationTime(registrationExpirationTime)
.setAutorenewBillingEvent(autorenewBillingEvent.createVKey())
.setAutorenewPollMessage(
autorenewPollMessage.createVKey(), autorenewPollMessage.getHistoryRevisionId())
.setAutorenewPollMessage(autorenewPollMessage.createVKey())
.setLaunchNotice(hasClaimsNotice ? launchCreate.get().getNotice() : null)
.setSmdId(signedMarkId)
.setDsData(secDnsCreate.map(SecDnsCreateExtension::getDsData).orElse(null))
@@ -390,6 +390,11 @@ public final class DomainCreateFlow implements TransactionalFlow {
.addGracePeriod(
GracePeriod.forBillingEvent(GracePeriodStatus.ADD, repoId, createBillingEvent))
.build();
if (allocationToken.isPresent()
&& allocationToken.get().getTokenType().equals(TokenType.PACKAGE)) {
domain =
domain.asBuilder().setCurrentPackageToken(allocationToken.get().createVKey()).build();
}
DomainHistory domainHistory =
buildDomainHistory(domain, registry, now, period, registry.getAddGracePeriodLength());
if (reservationTypes.contains(NAME_COLLISION)) {
@@ -481,7 +486,8 @@ public final class DomainCreateFlow implements TransactionalFlow {
boolean isValidReservedCreate,
boolean hasSignedMarks)
throws NoGeneralRegistrationsInCurrentPhaseException,
MustHaveSignedMarksInCurrentPhaseException {
MustHaveSignedMarksInCurrentPhaseException,
NoTrademarkedRegistrationsBeforeSunriseException {
// We allow general registration during GA.
TldState currentState = registry.getTldState(now);
if (currentState.equals(GENERAL_AVAILABILITY)) {
@@ -496,16 +502,23 @@ public final class DomainCreateFlow implements TransactionalFlow {
// Bypass most TLD state checks if that behavior is specified by the token
if (behavior.equals(RegistrationBehavior.BYPASS_TLD_STATE)
|| behavior.equals(RegistrationBehavior.ANCHOR_TENANT)) {
// If bypassing TLD state checks, a post-sunrise state is always fine
if (!currentState.equals(START_DATE_SUNRISE)
&& registry.getTldStateTransitions().headMap(now).containsValue(START_DATE_SUNRISE)) {
return;
}
// Non-trademarked names with the state check bypassed are always available
if (!claimsList.getClaimKey(domainLabel).isPresent()) {
return;
}
if (!currentState.equals(START_DATE_SUNRISE)) {
// Trademarked domains cannot be registered until after the sunrise period has ended, unless
// a valid signed mark is provided. Signed marks can only be provided during sunrise.
// Thus, when bypassing TLD state checks, a post-sunrise state is always fine.
if (registry.getTldStateTransitions().headMap(now).containsValue(START_DATE_SUNRISE)) {
return;
} else {
// If sunrise hasn't happened yet, trademarked domains are unavailable
throw new NoTrademarkedRegistrationsBeforeSunriseException(domainLabel);
}
}
}
// Otherwise, signed marks are necessary and sufficient in the sunrise period
if (currentState.equals(START_DATE_SUNRISE)) {
if (!hasSignedMarks) {
@@ -724,6 +737,17 @@ public final class DomainCreateFlow implements TransactionalFlow {
}
}
/** Trademarked domains cannot be registered before the sunrise period. */
static class NoTrademarkedRegistrationsBeforeSunriseException
extends ParameterValuePolicyErrorException {
public NoTrademarkedRegistrationsBeforeSunriseException(String domainLabel) {
super(
String.format(
"The trademarked label %s cannot be registered before the sunrise period.",
domainLabel));
}
}
/** Anchor tenant domain create is for the wrong number of years. */
static class AnchorTenantCreatePeriodException extends ParameterValuePolicyErrorException {
public AnchorTenantCreatePeriodException(int invalidYears) {

View File

@@ -263,7 +263,8 @@ public final class DomainDeleteFlow implements TransactionalFlow {
// the updated recurring billing event, we'll need it later and can't reload it.
Recurring existingRecurring = tm().loadByKey(existingDomain.getAutorenewBillingEvent());
BillingEvent.Recurring recurringBillingEvent =
updateAutorenewRecurrenceEndTime(existingDomain, existingRecurring, now);
updateAutorenewRecurrenceEndTime(
existingDomain, existingRecurring, now, domainHistory.getDomainHistoryId());
// If there's a pending transfer, the gaining client's autorenew billing
// event and poll message will already have been deleted in
// ResourceDeleteFlow since it's listed in serverApproveEntities.

View File

@@ -79,7 +79,7 @@ import google.registry.model.billing.BillingEvent;
import google.registry.model.billing.BillingEvent.Flag;
import google.registry.model.billing.BillingEvent.Reason;
import google.registry.model.billing.BillingEvent.Recurring;
import google.registry.model.contact.ContactResource;
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;
@@ -112,6 +112,7 @@ import google.registry.model.domain.secdns.SecDnsUpdateExtension;
import google.registry.model.domain.secdns.SecDnsUpdateExtension.Add;
import google.registry.model.domain.secdns.SecDnsUpdateExtension.Remove;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.domain.token.AllocationToken.RegistrationBehavior;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppoutput.EppResponse.ResponseExtension;
import google.registry.model.host.Host;
@@ -270,7 +271,13 @@ public class DomainFlowUtils {
&& token.get().getDomainName().get().equals(domainName.toString())) {
return true;
}
// Otherwise check whether the metadata extension is being used by a superuser to specify that
// Otherwise, check to see if we're using the specialized anchor tenant registration behavior on
// the allocation token
if (token.isPresent()
&& token.get().getRegistrationBehavior().equals(RegistrationBehavior.ANCHOR_TENANT)) {
return true;
}
// Otherwise, check whether the metadata extension is being used by a superuser to specify that
// it's an anchor tenant creation.
return metadataExtension.isPresent() && metadataExtension.get().getIsAnchorTenant();
}
@@ -373,9 +380,7 @@ public class DomainFlowUtils {
/** Verify that no linked resources have disallowed statuses. */
static void verifyNotInPendingDelete(
Set<DesignatedContact> contacts,
VKey<ContactResource> registrant,
Set<VKey<Host>> nameservers)
Set<DesignatedContact> contacts, 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);
@@ -420,7 +425,7 @@ public class DomainFlowUtils {
static void validateNoDuplicateContacts(Set<DesignatedContact> contacts)
throws ParameterValuePolicyErrorException {
ImmutableMultimap<Type, VKey<ContactResource>> contactsByType =
ImmutableMultimap<Type, VKey<Contact>> contactsByType =
contacts.stream()
.collect(
toImmutableSetMultimap(
@@ -429,15 +434,13 @@ public class DomainFlowUtils {
// If any contact type has multiple contacts:
if (contactsByType.asMap().values().stream().anyMatch(v -> v.size() > 1)) {
// Find the duplicates.
Map<Type, Collection<VKey<ContactResource>>> dupeKeysMap =
Map<Type, Collection<VKey<Contact>>> dupeKeysMap =
Maps.filterEntries(contactsByType.asMap(), e -> e.getValue().size() > 1);
ImmutableList<VKey<ContactResource>> dupeKeys =
ImmutableList<VKey<Contact>> dupeKeys =
dupeKeysMap.values().stream().flatMap(Collection::stream).collect(toImmutableList());
// Load the duplicates in one batch.
Map<VKey<? extends ContactResource>, ContactResource> dupeContacts =
tm().loadByKeys(dupeKeys);
ImmutableMultimap.Builder<Type, VKey<ContactResource>> typesMap =
new ImmutableMultimap.Builder<>();
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(
@@ -446,7 +449,7 @@ public class DomainFlowUtils {
}
static void validateRequiredContactsPresent(
@Nullable VKey<ContactResource> registrant, Set<DesignatedContact> contacts)
@Nullable VKey<Contact> registrant, Set<DesignatedContact> contacts)
throws RequiredParameterMissingException {
if (registrant == null) {
throw new MissingRegistrantException();
@@ -584,7 +587,10 @@ public class DomainFlowUtils {
* <p>Returns the new autorenew recurring billing event.
*/
public static Recurring updateAutorenewRecurrenceEndTime(
Domain domain, Recurring existingRecurring, DateTime newEndTime) {
Domain domain,
Recurring existingRecurring,
DateTime newEndTime,
@Nullable DomainHistoryId historyId) {
Optional<PollMessage.Autorenew> autorenewPollMessage =
tm().loadByKeyIfPresent(domain.getAutorenewPollMessage());
@@ -592,20 +598,25 @@ public class DomainFlowUtils {
// create a new one at the same id. This can happen if a transfer was requested on a domain
// where all autorenew poll messages had already been delivered (this would cause the poll
// message to be deleted), and then subsequently the transfer was canceled, rejected, or deleted
// (which would cause the poll message to be recreated here).
PollMessage.Autorenew updatedAutorenewPollMessage =
autorenewPollMessage.isPresent()
? autorenewPollMessage.get().asBuilder().setAutorenewEndTime(newEndTime).build()
: newAutorenewPollMessage(domain)
.setId((Long) domain.getAutorenewPollMessage().getSqlKey())
.setAutorenewEndTime(newEndTime)
.setDomainHistoryId(
new DomainHistoryId(
domain.getRepoId(), domain.getAutorenewPollMessageHistoryId()))
.build();
// (which would cause the poll message to be recreated here). In the latter case, the history id
// of the event that created the new poll message will also be used.
PollMessage.Autorenew updatedAutorenewPollMessage;
if (autorenewPollMessage.isPresent()) {
updatedAutorenewPollMessage =
autorenewPollMessage.get().asBuilder().setAutorenewEndTime(newEndTime).build();
} else {
checkNotNull(
historyId, "Cannot create a new autorenew poll message without a domain history id");
updatedAutorenewPollMessage =
newAutorenewPollMessage(domain)
.setId((Long) domain.getAutorenewPollMessage().getSqlKey())
.setAutorenewEndTime(newEndTime)
.setDomainHistoryId(historyId)
.build();
}
// If the resultant autorenew poll message would have no poll messages to deliver, then just
// delete it. Otherwise save it with the new end time.
// delete it. Otherwise, save it with the new end time.
if (isAtOrAfter(updatedAutorenewPollMessage.getEventTime(), newEndTime)) {
autorenewPollMessage.ifPresent(autorenew -> tm().delete(autorenew));
} else {

View File

@@ -30,6 +30,7 @@ import google.registry.flows.EppException;
import google.registry.flows.ExtensionManager;
import google.registry.flows.Flow;
import google.registry.flows.FlowModule.RegistrarId;
import google.registry.flows.FlowModule.Superuser;
import google.registry.flows.FlowModule.TargetId;
import google.registry.flows.annotations.ReportingSpec;
import google.registry.flows.custom.DomainInfoFlowCustomLogic;
@@ -42,6 +43,8 @@ import google.registry.model.domain.DomainCommand.Info.HostsRequest;
import google.registry.model.domain.DomainInfoData;
import google.registry.model.domain.fee06.FeeInfoCommandExtensionV06;
import google.registry.model.domain.fee06.FeeInfoResponseExtensionV06;
import google.registry.model.domain.packagetoken.PackageTokenExtension;
import google.registry.model.domain.packagetoken.PackageTokenResponseExtension;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.domain.rgp.RgpInfoExtension;
import google.registry.model.eppcommon.AuthInfo;
@@ -85,13 +88,14 @@ public final class DomainInfoFlow implements Flow {
@Inject EppResponse.Builder responseBuilder;
@Inject DomainInfoFlowCustomLogic flowCustomLogic;
@Inject DomainPricingLogic pricingLogic;
@Inject @Superuser boolean isSuperuser;
@Inject
DomainInfoFlow() {}
@Override
public EppResponse run() throws EppException {
extensionManager.register(FeeInfoCommandExtensionV06.class);
extensionManager.register(FeeInfoCommandExtensionV06.class, PackageTokenExtension.class);
flowCustomLogic.beforeValidation();
validateRegistrarIsLoggedIn(registrarId);
extensionManager.validate();
@@ -150,6 +154,15 @@ public final class DomainInfoFlow implements Flow {
if (!gracePeriodStatuses.isEmpty()) {
extensions.add(RgpInfoExtension.create(gracePeriodStatuses));
}
Optional<PackageTokenExtension> packageInfo =
eppInput.getSingleExtension(PackageTokenExtension.class);
if (packageInfo.isPresent()) {
// Package info was requested.
if (isSuperuser || registrarId.equals(domain.getCurrentSponsorRegistrarId())) {
// Only show package info to owning registrar or superusers
extensions.add(PackageTokenResponseExtension.create(domain.getCurrentPackageToken()));
}
}
Optional<FeeInfoCommandExtensionV06> feeInfo =
eppInput.getSingleExtension(FeeInfoCommandExtensionV06.class);
if (feeInfo.isPresent()) { // Fee check was requested.

View File

@@ -30,6 +30,8 @@ import static google.registry.flows.domain.DomainFlowUtils.validateFeeChallenge;
import static google.registry.flows.domain.DomainFlowUtils.validateRegistrationPeriod;
import static google.registry.flows.domain.DomainFlowUtils.verifyRegistrarIsActive;
import static google.registry.flows.domain.DomainFlowUtils.verifyUnitIsYears;
import static google.registry.flows.domain.token.AllocationTokenFlowUtils.maybeApplyPackageRemovalToken;
import static google.registry.flows.domain.token.AllocationTokenFlowUtils.verifyTokenAllowedOnDomain;
import static google.registry.model.reporting.HistoryEntry.Type.DOMAIN_RENEW;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.DateTimeUtils.leapSafeAddYears;
@@ -119,6 +121,10 @@ import org.joda.time.Duration;
* @error {@link DomainFlowUtils.UnsupportedFeeAttributeException}
* @error {@link DomainRenewFlow.IncorrectCurrentExpirationDateException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.MissingRemovePackageTokenOnPackageDomainException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.RemovePackageTokenOnNonPackageDomainException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.AllocationTokenNotValidForDomainException}
* @error {@link
* google.registry.flows.domain.token.AllocationTokenFlowUtils.InvalidAllocationTokenException}
@@ -174,7 +180,11 @@ public final class DomainRenewFlow implements TransactionalFlow {
registrarId,
now,
eppInput.getSingleExtension(AllocationTokenExtension.class));
verifyRenewAllowed(authInfo, existingDomain, command);
verifyRenewAllowed(authInfo, existingDomain, command, allocationToken);
// If client passed an applicable static token this updates the domain
existingDomain = maybeApplyPackageRemovalToken(existingDomain, allocationToken);
int years = command.getPeriod().getValue();
DateTime newExpirationTime =
leapSafeAddYears(existingDomain.getRegistrationExpirationTime(), years); // Uncapped
@@ -223,7 +233,11 @@ public final class DomainRenewFlow implements TransactionalFlow {
.build();
// End the old autorenew billing event and poll message now. This may delete the poll message.
Recurring existingRecurring = tm().loadByKey(existingDomain.getAutorenewBillingEvent());
updateAutorenewRecurrenceEndTime(existingDomain, existingRecurring, now);
updateAutorenewRecurrenceEndTime(
existingDomain,
existingRecurring,
now,
new DomainHistoryId(domainHistoryKey.getParent().getName(), domainHistoryKey.getId()));
Domain newDomain =
existingDomain
.asBuilder()
@@ -231,9 +245,7 @@ public final class DomainRenewFlow implements TransactionalFlow {
.setLastEppUpdateRegistrarId(registrarId)
.setRegistrationExpirationTime(newExpirationTime)
.setAutorenewBillingEvent(newAutorenewEvent.createVKey())
.setAutorenewPollMessage(
newAutorenewPollMessage.createVKey(),
newAutorenewPollMessage.getHistoryRevisionId())
.setAutorenewPollMessage(newAutorenewPollMessage.createVKey())
.addGracePeriod(
GracePeriod.forBillingEvent(
GracePeriodStatus.RENEW, existingDomain.getRepoId(), explicitRenewEvent))
@@ -300,7 +312,11 @@ public final class DomainRenewFlow implements TransactionalFlow {
.build();
}
private void verifyRenewAllowed(Optional<AuthInfo> authInfo, Domain existingDomain, Renew command)
private void verifyRenewAllowed(
Optional<AuthInfo> authInfo,
Domain existingDomain,
Renew command,
Optional<AllocationToken> allocationToken)
throws EppException {
verifyOptionalAuthInfo(authInfo, existingDomain);
verifyNoDisallowedStatuses(existingDomain, RENEW_DISALLOWED_STATUSES);
@@ -310,6 +326,8 @@ public final class DomainRenewFlow implements TransactionalFlow {
checkHasBillingAccount(registrarId, existingDomain.getTld());
}
verifyUnitIsYears(command.getPeriod());
// We only allow __REMOVE_PACKAGE__ token on promo package domains for now
verifyTokenAllowedOnDomain(existingDomain, allocationToken);
// If the date they specify doesn't match the expiration, fail. (This is an idempotence check).
if (!command.getCurrentExpirationDate().equals(
existingDomain.getRegistrationExpirationTime().toLocalDate())) {
@@ -331,7 +349,11 @@ public final class DomainRenewFlow implements TransactionalFlow {
.setPeriodYears(years)
.setCost(renewCost)
.setEventTime(now)
.setAllocationToken(allocationToken.map(AllocationToken::createVKey).orElse(null))
.setAllocationToken(
allocationToken
.filter(t -> AllocationToken.TokenBehavior.DEFAULT.equals(t.getTokenBehavior()))
.map(AllocationToken::createVKey)
.orElse(null))
.setBillingTime(now.plus(Registry.get(tld).getRenewGracePeriodLength()))
.setDomainHistoryId(
new DomainHistoryId(domainHistoryKey.getParent().getName(), domainHistoryKey.getId()))

View File

@@ -251,8 +251,7 @@ public final class DomainRestoreRequestFlow implements TransactionalFlow {
.setGracePeriods(null)
.setDeletePollMessage(null)
.setAutorenewBillingEvent(autorenewEvent.createVKey())
.setAutorenewPollMessage(
autorenewPollMessage.createVKey(), autorenewPollMessage.getHistoryRevisionId())
.setAutorenewPollMessage(autorenewPollMessage.createVKey())
// Clear the autorenew end time so if it had expired but is now explicitly being restored,
// it won't immediately be deleted again.
.setAutorenewEndTime(Optional.empty())

View File

@@ -56,7 +56,6 @@ import google.registry.model.domain.DomainHistory.DomainHistoryId;
import google.registry.model.domain.GracePeriod;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.domain.token.AllocationTokenExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppinput.EppInput;
@@ -129,15 +128,12 @@ public final class DomainTransferApproveFlow implements TransactionalFlow {
extensionManager.validate();
DateTime now = tm().getTransactionTime();
Domain existingDomain = loadAndVerifyExistence(Domain.class, targetId, now);
// Currently we do not do anything with this allocation token, but just want it loaded and
// available in this flow in case we use it in the future
Optional<AllocationToken> allocationToken =
allocationTokenFlowUtils.verifyAllocationTokenIfPresent(
existingDomain,
Registry.get(existingDomain.getTld()),
registrarId,
now,
eppInput.getSingleExtension(AllocationTokenExtension.class));
allocationTokenFlowUtils.verifyAllocationTokenIfPresent(
existingDomain,
Registry.get(existingDomain.getTld()),
registrarId,
now,
eppInput.getSingleExtension(AllocationTokenExtension.class));
verifyOptionalAuthInfo(authInfo, existingDomain);
verifyHasPendingTransfer(existingDomain);
verifyResourceOwnership(registrarId, existingDomain);
@@ -182,9 +178,9 @@ public final class DomainTransferApproveFlow implements TransactionalFlow {
getOnlyElement(existingDomain.getGracePeriodsOfType(GracePeriodStatus.AUTO_RENEW), null);
if (autorenewGrace != null) {
// During a normal transfer, if the domain is in the auto-renew grace period, the auto-renew
// billing event is cancelled and the gaining registrar is charged for the one year renewal.
// billing event is cancelled and the gaining registrar is charged for the one-year renewal.
// But, if the superuser extension is used to request a transfer without an additional year
// then the gaining registrar is not charged for the one year renewal and the losing registrar
// then the gaining registrar is not charged for the one-year renewal and the losing registrar
// still needs to be charged for the auto-renew.
if (billingEvent.isPresent()) {
entitiesToSave.add(
@@ -198,7 +194,11 @@ public final class DomainTransferApproveFlow implements TransactionalFlow {
}
// Close the old autorenew event and poll message at the transfer time (aka now). This may end
// up deleting the poll message.
updateAutorenewRecurrenceEndTime(existingDomain, existingRecurring, now);
updateAutorenewRecurrenceEndTime(
existingDomain,
existingRecurring,
now,
new DomainHistoryId(domainHistoryKey.getParent().getName(), domainHistoryKey.getId()));
DateTime newExpirationTime =
computeExDateForApprovalTime(existingDomain, now, transferData.getTransferPeriod());
// Create a new autorenew event starting at the expiration time.
@@ -244,9 +244,7 @@ public final class DomainTransferApproveFlow implements TransactionalFlow {
.build())
.setRegistrationExpirationTime(newExpirationTime)
.setAutorenewBillingEvent(autorenewEvent.createVKey())
.setAutorenewPollMessage(
gainingClientAutorenewPollMessage.createVKey(),
gainingClientAutorenewPollMessage.getHistoryRevisionId())
.setAutorenewPollMessage(gainingClientAutorenewPollMessage.createVKey())
// Remove all the old grace periods and add a new one for the transfer.
.setGracePeriods(
billingEvent

View File

@@ -116,7 +116,8 @@ public final class DomainTransferCancelFlow implements TransactionalFlow {
// Reopen the autorenew event and poll message that we closed for the implicit transfer. This
// may recreate the autorenew poll message if it was deleted when the transfer request was made.
Recurring existingRecurring = tm().loadByKey(existingDomain.getAutorenewBillingEvent());
updateAutorenewRecurrenceEndTime(existingDomain, existingRecurring, END_OF_TIME);
updateAutorenewRecurrenceEndTime(
existingDomain, existingRecurring, END_OF_TIME, domainHistory.getDomainHistoryId());
// Delete the billing event and poll messages that were written in case the transfer would have
// been implicitly server approved.
tm().delete(existingDomain.getTransferData().getServerApproveEntities());

View File

@@ -117,7 +117,8 @@ public final class DomainTransferRejectFlow implements TransactionalFlow {
// Reopen the autorenew event and poll message that we closed for the implicit transfer. This
// may end up recreating the poll message if it was deleted upon the transfer request.
Recurring existingRecurring = tm().loadByKey(existingDomain.getAutorenewBillingEvent());
updateAutorenewRecurrenceEndTime(existingDomain, existingRecurring, END_OF_TIME);
updateAutorenewRecurrenceEndTime(
existingDomain, existingRecurring, END_OF_TIME, domainHistory.getDomainHistoryId());
// Delete the billing event and poll messages that were written in case the transfer would have
// been implicitly server approved.
tm().delete(existingDomain.getTransferData().getServerApproveEntities());

View File

@@ -57,12 +57,12 @@ import google.registry.model.billing.BillingEvent.Recurring;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainCommand.Transfer;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.DomainHistory.DomainHistoryId;
import google.registry.model.domain.Period;
import google.registry.model.domain.fee.FeeTransferCommandExtension;
import google.registry.model.domain.fee.FeeTransformResponseExtension;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.domain.superuser.DomainTransferRequestSuperuserExtension;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.domain.token.AllocationTokenExtension;
import google.registry.model.eppcommon.AuthInfo;
import google.registry.model.eppcommon.StatusValue;
@@ -169,15 +169,12 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
extensionManager.validate();
DateTime now = tm().getTransactionTime();
Domain existingDomain = loadAndVerifyExistence(Domain.class, targetId, now);
// Currently we do not do anything with this allocation token, but just want it loaded and
// available in this flow in case we use it in the future
Optional<AllocationToken> allocationToken =
allocationTokenFlowUtils.verifyAllocationTokenIfPresent(
existingDomain,
Registry.get(existingDomain.getTld()),
gainingClientId,
now,
eppInput.getSingleExtension(AllocationTokenExtension.class));
allocationTokenFlowUtils.verifyAllocationTokenIfPresent(
existingDomain,
Registry.get(existingDomain.getTld()),
gainingClientId,
now,
eppInput.getSingleExtension(AllocationTokenExtension.class));
Optional<DomainTransferRequestSuperuserExtension> superuserExtension =
eppInput.getSingleExtension(DomainTransferRequestSuperuserExtension.class);
Period period =
@@ -210,9 +207,12 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
.setId(domainHistoryKey.getId())
.setOtherRegistrarId(existingDomain.getCurrentSponsorRegistrarId());
DateTime automaticTransferTime =
superuserExtension.isPresent()
? now.plusDays(superuserExtension.get().getAutomaticTransferLength())
: now.plus(registry.getAutomaticTransferLength());
superuserExtension
.map(
domainTransferRequestSuperuserExtension ->
now.plusDays(
domainTransferRequestSuperuserExtension.getAutomaticTransferLength()))
.orElseGet(() -> now.plus(registry.getAutomaticTransferLength()));
// If the domain will be in the auto-renew grace period at the moment of transfer, the transfer
// will subsume the autorenew, so we don't add the normal extra year from the transfer.
// The gaining registrar is still billed for the extra year; the losing registrar will get a
@@ -261,7 +261,11 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
// the poll message if it has no events left. Note that if the automatic transfer succeeds, then
// cloneProjectedAtTime() will replace these old autorenew entities with the server approve ones
// that we've created in this flow and stored in pendingTransferData.
updateAutorenewRecurrenceEndTime(existingDomain, existingRecurring, automaticTransferTime);
updateAutorenewRecurrenceEndTime(
existingDomain,
existingRecurring,
automaticTransferTime,
new DomainHistoryId(domainHistoryKey.getParent().getName(), domainHistoryKey.getId()));
Domain newDomain =
existingDomain
.asBuilder()
@@ -325,7 +329,7 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
*
* <p>Even if not required, this policy is desirable because it dramatically simplifies the logic
* in transfer flows. Registrars appear to never request 2+ year transfers in practice, and they
* can always decompose an multi-year transfer into a 1-year transfer followed by a manual renewal
* can always decompose a multi-year transfer into a 1-year transfer followed by a manual renewal
* afterwards. The <a href="https://tools.ietf.org/html/rfc5731#section-3.2.4">EPP Domain RFC,
* section 3.2.4</a> says about EPP transfer periods that "the number of units available MAY be
* subject to limits imposed by the server" so we're just limiting the units to one.
@@ -370,7 +374,7 @@ public final class DomainTransferRequestFlow implements TransactionalFlow {
private DomainTransferResponse createResponse(
Period period, Domain existingDomain, Domain newDomain, DateTime now) {
// If the registration were approved this instant, this is what the new expiration would be,
// because we cap at 10 years from the moment of approval. This is different than the server
// because we cap at 10 years from the moment of approval. This is different from the server
// approval new expiration time, which is capped at 10 years from the server approve time.
DateTime approveNowExtendedRegistrationTime =
computeExDateForApprovalTime(existingDomain, now, period);

View File

@@ -15,6 +15,7 @@
package google.registry.flows.domain.token;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import com.google.common.base.Strings;
@@ -26,9 +27,12 @@ import google.registry.flows.EppException;
import google.registry.flows.EppException.AssociationProhibitsOperationException;
import google.registry.flows.EppException.AuthorizationErrorException;
import google.registry.flows.EppException.StatusProhibitsOperationException;
import google.registry.model.billing.BillingEvent.Recurring;
import google.registry.model.billing.BillingEvent.RenewalPriceBehavior;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainCommand;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.domain.token.AllocationToken.TokenBehavior;
import google.registry.model.domain.token.AllocationToken.TokenStatus;
import google.registry.model.domain.token.AllocationToken.TokenType;
import google.registry.model.domain.token.AllocationTokenExtension;
@@ -109,23 +113,27 @@ public class AllocationTokenFlowUtils {
private void validateToken(
InternetDomainName domainName, AllocationToken token, String registrarId, DateTime now)
throws EppException {
if (!token.getAllowedRegistrarIds().isEmpty()
&& !token.getAllowedRegistrarIds().contains(registrarId)) {
throw new AllocationTokenNotValidForRegistrarException();
}
if (!token.getAllowedTlds().isEmpty()
&& !token.getAllowedTlds().contains(domainName.parent().toString())) {
throw new AllocationTokenNotValidForTldException();
}
if (token.getDomainName().isPresent()
&& !token.getDomainName().get().equals(domainName.toString())) {
throw new AllocationTokenNotValidForDomainException();
}
// Tokens without status transitions will just have a single-entry NOT_STARTED map, so only
// check the status transitions map if it's non-trivial.
if (token.getTokenStatusTransitions().size() > 1
&& !TokenStatus.VALID.equals(token.getTokenStatusTransitions().getValueAtTime(now))) {
throw new AllocationTokenNotInPromotionException();
// Only tokens with default behavior require validation
if (TokenBehavior.DEFAULT.equals(token.getTokenBehavior())) {
if (!token.getAllowedRegistrarIds().isEmpty()
&& !token.getAllowedRegistrarIds().contains(registrarId)) {
throw new AllocationTokenNotValidForRegistrarException();
}
if (!token.getAllowedTlds().isEmpty()
&& !token.getAllowedTlds().contains(domainName.parent().toString())) {
throw new AllocationTokenNotValidForTldException();
}
if (token.getDomainName().isPresent()
&& !token.getDomainName().get().equals(domainName.toString())) {
throw new AllocationTokenNotValidForDomainException();
}
// Tokens without status transitions will just have a single-entry NOT_STARTED map, so only
// check the status transitions map if it's non-trivial.
if (token.getTokenStatusTransitions().size() > 1
&& !TokenStatus.VALID.equals(token.getTokenStatusTransitions().getValueAtTime(now))) {
throw new AllocationTokenNotInPromotionException();
}
}
}
@@ -137,8 +145,15 @@ public class AllocationTokenFlowUtils {
// See https://tools.ietf.org/html/draft-ietf-regext-allocation-token-04#section-2.1
throw new InvalidAllocationTokenException();
}
Optional<AllocationToken> maybeTokenEntity =
Optional<AllocationToken> maybeTokenEntity = AllocationToken.maybeGetStaticTokenInstance(token);
if (maybeTokenEntity.isPresent()) {
return maybeTokenEntity.get();
}
maybeTokenEntity =
tm().transact(() -> tm().loadByKeyIfPresent(VKey.create(AllocationToken.class, token)));
if (!maybeTokenEntity.isPresent()) {
throw new InvalidAllocationTokenException();
}
@@ -187,6 +202,49 @@ public class AllocationTokenFlowUtils {
tokenCustomLogic.validateToken(existingDomain, tokenEntity, registry, registrarId, now));
}
public static void verifyTokenAllowedOnDomain(
Domain domain, Optional<AllocationToken> allocationToken) throws EppException {
boolean domainHasPackageToken = domain.getCurrentPackageToken().isPresent();
boolean hasRemovePackageToken =
allocationToken.isPresent()
&& TokenBehavior.REMOVE_PACKAGE.equals(allocationToken.get().getTokenBehavior());
if (hasRemovePackageToken && !domainHasPackageToken) {
throw new RemovePackageTokenOnNonPackageDomainException();
} else if (!hasRemovePackageToken && domainHasPackageToken) {
throw new MissingRemovePackageTokenOnPackageDomainException();
}
}
public static Domain maybeApplyPackageRemovalToken(
Domain domain, Optional<AllocationToken> allocationToken) {
if (!allocationToken.isPresent()
|| !TokenBehavior.REMOVE_PACKAGE.equals(allocationToken.get().getTokenBehavior())) {
return domain;
}
Recurring newRecurringBillingEvent =
tm().loadByKey(domain.getAutorenewBillingEvent())
.asBuilder()
.setRenewalPriceBehavior(RenewalPriceBehavior.DEFAULT)
.setRenewalPrice(null)
.build();
// the Recurring billing event is reloaded later in the renew flow, so we synchronize changed
// RecurringBillingEvent with storage manually
tm().put(newRecurringBillingEvent);
jpaTm().getEntityManager().flush();
jpaTm().getEntityManager().clear();
// Remove current package token
return domain
.asBuilder()
.setCurrentPackageToken(null)
.setAutorenewBillingEvent(newRecurringBillingEvent.createVKey())
.build();
}
// Note: exception messages should be <= 32 characters long for domain check results
/** The allocation token is not currently valid. */
@@ -234,4 +292,20 @@ public class AllocationTokenFlowUtils {
super("The allocation token is invalid");
}
}
/** The __REMOVEPACKAGE__ token is missing on a renew package domain command */
public static class MissingRemovePackageTokenOnPackageDomainException
extends AssociationProhibitsOperationException {
MissingRemovePackageTokenOnPackageDomainException() {
super("Domains that are inside packages cannot be explicitly renewed");
}
}
/** The __REMOVEPACKAGE__ token is not allowed on non package domains */
public static class RemovePackageTokenOnNonPackageDomainException
extends AssociationProhibitsOperationException {
RemovePackageTokenOnNonPackageDomainException() {
super("__REMOVEPACKAGE__ token is not allowed on non package domains");
}
}
}

View File

@@ -18,8 +18,8 @@ import com.google.common.collect.ImmutableSet;
import google.registry.model.annotations.DeleteAfterMigration;
import google.registry.model.common.EntityGroupRoot;
import google.registry.model.common.GaeUserIdConverter;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.token.AllocationToken;
@@ -28,7 +28,6 @@ import google.registry.model.host.HostHistory;
import google.registry.model.index.EppResourceIndex;
import google.registry.model.index.EppResourceIndexBucket;
import google.registry.model.index.ForeignKeyIndex;
import google.registry.model.poll.PollMessage;
import google.registry.model.rde.RdeRevision;
import google.registry.model.registrar.Registrar;
import google.registry.model.reporting.HistoryEntry;
@@ -43,8 +42,8 @@ public final class EntityClasses {
public static final ImmutableSet<Class<? extends ImmutableObject>> ALL_CLASSES =
ImmutableSet.of(
AllocationToken.class,
Contact.class,
ContactHistory.class,
ContactResource.class,
Domain.class,
DomainHistory.class,
EntityGroupRoot.class,
@@ -58,9 +57,6 @@ public final class EntityClasses {
Host.class,
HostHistory.class,
Lock.class,
PollMessage.class,
PollMessage.Autorenew.class,
PollMessage.OneTime.class,
RdeRevision.class,
Registrar.class,
ServerSecret.class);

View File

@@ -32,7 +32,7 @@ import google.registry.config.RegistryConfig;
import google.registry.model.EppResource.BuilderWithTransferData;
import google.registry.model.EppResource.ForeignKeyedEppResource;
import google.registry.model.EppResource.ResourceWithTransferData;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.Contact;
import google.registry.model.domain.Domain;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.host.Host;
@@ -345,10 +345,10 @@ public final class EppResourceUtils {
public static ImmutableSet<VKey<Domain>> getLinkedDomainKeys(
VKey<? extends EppResource> key, DateTime now, @Nullable Integer limit) {
checkArgument(
key.getKind().equals(ContactResource.class) || key.getKind().equals(Host.class),
"key must be either VKey<ContactResource> or VKey<Host>, but it is %s",
key.getKind().equals(Contact.class) || key.getKind().equals(Host.class),
"key must be either VKey<Contact> or VKey<Host>, but it is %s",
key);
boolean isContactKey = key.getKind().equals(ContactResource.class);
boolean isContactKey = key.getKind().equals(Contact.class);
if (tm().isOfy()) {
com.googlecode.objectify.cmd.Query<Domain> query =
auditedOfy()

View File

@@ -25,7 +25,7 @@ import com.google.common.collect.Sets;
import google.registry.model.EppResource.BuilderWithTransferData;
import google.registry.model.EppResource.ForeignKeyedEppResource;
import google.registry.model.EppResource.ResourceWithTransferData;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.Contact;
import google.registry.model.domain.Domain;
import google.registry.model.eppcommon.StatusValue;
import google.registry.model.eppcommon.Trid;
@@ -60,7 +60,7 @@ public final class ResourceTransferUtils {
EppResource eppResource, TransferData transferData) {
assertIsContactOrDomain(eppResource);
TransferResponse.Builder<? extends TransferResponse, ?> builder;
if (eppResource instanceof ContactResource) {
if (eppResource instanceof Contact) {
builder = new ContactTransferResponse.Builder().setContactId(eppResource.getForeignKey());
} else {
DomainTransferData domainTransferData = (DomainTransferData) transferData;
@@ -93,7 +93,7 @@ public final class ResourceTransferUtils {
boolean actionResult,
DateTime processedDate) {
assertIsContactOrDomain(eppResource);
return eppResource instanceof ContactResource
return eppResource instanceof Contact
? ContactPendingActionNotificationResponse.create(
eppResource.getForeignKey(), actionResult, transferRequestTrid, processedDate)
: DomainPendingActionNotificationResponse.create(
@@ -101,7 +101,7 @@ public final class ResourceTransferUtils {
}
private static void assertIsContactOrDomain(EppResource eppResource) {
checkState(eppResource instanceof ContactResource || eppResource instanceof Domain);
checkState(eppResource instanceof Contact || eppResource instanceof Domain);
}
/** Update the relevant {@link ForeignKeyIndex} to cache the new deletion time. */

View File

@@ -63,5 +63,7 @@ public enum ConsolePermission {
/** View announcements in the UI. */
VIEW_ANNOUNCEMENTS,
/** Viewing a record of actions performed in the UI for a particular registrar. */
VIEW_ACTIVITY_LOG
VIEW_ACTIVITY_LOG,
/** View and perform registry locks. */
REGISTRY_LOCK
}

View File

@@ -44,7 +44,8 @@ public class ConsoleRoleDefinitions {
ConsolePermission.VIEW_OPERATIONAL_DATA,
ConsolePermission.SEND_ANNOUNCEMENTS,
ConsolePermission.VIEW_ANNOUNCEMENTS,
ConsolePermission.VIEW_ACTIVITY_LOG);
ConsolePermission.VIEW_ACTIVITY_LOG,
ConsolePermission.REGISTRY_LOCK);
/** Permissions for a registry support lead. */
static final ImmutableSet<ConsolePermission> SUPPORT_LEAD_PERMISSIONS =
@@ -76,10 +77,17 @@ public class ConsoleRoleDefinitions {
ConsolePermission.VIEW_OPERATIONAL_DATA,
ConsolePermission.VIEW_ANNOUNCEMENTS);
/** Permissions for a registrar partner account manager that can perform registry locks. */
static final ImmutableSet<ConsolePermission> ACCOUNT_MANAGER_WITH_REGISTRY_LOCK_PERMISSIONS =
new ImmutableSet.Builder<ConsolePermission>()
.addAll(ACCOUNT_MANAGER_PERMISSIONS)
.add(ConsolePermission.REGISTRY_LOCK)
.build();
/** Permissions for the tech contact of a registrar. */
static final ImmutableSet<ConsolePermission> TECH_CONTACT_PERMISSIONS =
new ImmutableSet.Builder<ConsolePermission>()
.addAll(ACCOUNT_MANAGER_PERMISSIONS)
.addAll(ACCOUNT_MANAGER_WITH_REGISTRY_LOCK_PERMISSIONS)
.add(
ConsolePermission.MANAGE_ACCREDITATION,
ConsolePermission.CONFIGURE_EPP_CONNECTION,

View File

@@ -15,6 +15,7 @@
package google.registry.model.console;
import static google.registry.model.console.ConsoleRoleDefinitions.ACCOUNT_MANAGER_PERMISSIONS;
import static google.registry.model.console.ConsoleRoleDefinitions.ACCOUNT_MANAGER_WITH_REGISTRY_LOCK_PERMISSIONS;
import static google.registry.model.console.ConsoleRoleDefinitions.PRIMARY_CONTACT_PERMISSIONS;
import static google.registry.model.console.ConsoleRoleDefinitions.TECH_CONTACT_PERMISSIONS;
@@ -25,6 +26,8 @@ public enum RegistrarRole {
/** The user is a standard account manager at a registrar. */
ACCOUNT_MANAGER(ACCOUNT_MANAGER_PERMISSIONS),
/** The user is an account manager that can perform registry locks. */
ACCOUNT_MANAGER_WITH_REGISTRY_LOCK(ACCOUNT_MANAGER_WITH_REGISTRY_LOCK_PERMISSIONS),
/** The user is a technical contact of a registrar. */
TECH_CONTACT(TECH_CONTACT_PERMISSIONS),
/** The user is the primary contact at a registrar. */

View File

@@ -24,28 +24,36 @@ import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.Table;
/** A console user, either a registry employee or a registrar partner. */
@Entity
@Table(
indexes = {
@Index(columnList = "gaiaId", name = "user_gaia_id_idx"),
@Index(columnList = "emailAddress", name = "user_email_address_idx")
})
public class User extends ImmutableObject implements Buildable {
/** Autogenerated unique ID of this user. */
private long id;
@Id private long id;
/** GAIA ID associated with the user in question. */
@Column(nullable = false)
private String gaiaId;
/** Email address of the user in question. */
@Column(nullable = false)
private String emailAddress;
/** Roles (which grant permissions) associated with this user. */
@Column(nullable = false)
private UserRoles userRoles;
/**
* Whether the contact is allowed to set their registry lock password through the registrar
* console. This will be set to false on contact creation and when the user sets a password.
*/
boolean allowedToSetRegistryLockPassword = false;
/**
* A hashed password that exists iff this contact is registry-lock-enabled. The hash is a base64
* encoded SHA256 string.
@@ -55,6 +63,10 @@ public class User extends ImmutableObject implements Buildable {
/** Randomly generated hash salt. */
String registryLockPasswordSalt;
public long getId() {
return id;
}
public String getGaiaId() {
return gaiaId;
}
@@ -67,11 +79,7 @@ public class User extends ImmutableObject implements Buildable {
return userRoles;
}
public boolean isAllowedToSetRegistryLockPassword() {
return allowedToSetRegistryLockPassword;
}
public boolean isRegistryLockAllowed() {
public boolean hasRegistryLockPassword() {
return !isNullOrEmpty(registryLockPasswordHash) && !isNullOrEmpty(registryLockPasswordSalt);
}
@@ -85,6 +93,22 @@ public class User extends ImmutableObject implements Buildable {
.equals(registryLockPasswordHash);
}
/**
* Whether the user has the registry lock permission on any registrar or globally.
*
* <p>If so, they should be allowed to (re)set their registry lock password.
*/
public boolean hasAnyRegistryLockPermission() {
if (userRoles == null) {
return false;
}
if (userRoles.isAdmin() || userRoles.hasGlobalPermission(ConsolePermission.REGISTRY_LOCK)) {
return true;
}
return userRoles.getRegistrarRoles().values().stream()
.anyMatch(role -> role.hasPermission(ConsolePermission.REGISTRY_LOCK));
}
@Override
public Builder asBuilder() {
return new Builder(clone(this));
@@ -99,6 +123,7 @@ public class User extends ImmutableObject implements Buildable {
super(user);
}
@Override
public User build() {
checkArgumentNotNull(getInstance().gaiaId, "Gaia ID cannot be null");
checkArgumentNotNull(getInstance().emailAddress, "Email address cannot be null");
@@ -123,25 +148,22 @@ public class User extends ImmutableObject implements Buildable {
return this;
}
public Builder setAllowedToSetRegistryLockPassword(boolean allowedToSetRegistryLockPassword) {
if (allowedToSetRegistryLockPassword) {
getInstance().registryLockPasswordSalt = null;
getInstance().registryLockPasswordHash = null;
}
getInstance().allowedToSetRegistryLockPassword = allowedToSetRegistryLockPassword;
public Builder removeRegistryLockPassword() {
getInstance().registryLockPasswordHash = null;
getInstance().registryLockPasswordSalt = null;
return this;
}
public Builder setRegistryLockPassword(String registryLockPassword) {
checkArgument(
getInstance().allowedToSetRegistryLockPassword,
"Not allowed to set registry lock password for this user");
getInstance().hasAnyRegistryLockPermission(), "User has no registry lock permission");
checkArgument(
!getInstance().hasRegistryLockPassword(), "User already has a password, remove it first");
checkArgument(
!isNullOrEmpty(registryLockPassword), "Registry lock password was null or empty");
getInstance().registryLockPasswordSalt = base64().encode(SALT_SUPPLIER.get());
getInstance().registryLockPasswordHash =
hashPassword(registryLockPassword, getInstance().registryLockPasswordSalt);
getInstance().allowedToSetRegistryLockPassword = false;
return this;
}
}

View File

@@ -21,6 +21,10 @@ import com.google.common.collect.ImmutableMap;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import java.util.Map;
import javax.persistence.Column;
import javax.persistence.Embeddable;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
/**
* Contains the global and per-registrar roles for a given user.
@@ -28,15 +32,19 @@ import java.util.Map;
* <p>See <a href="https://go/nomulus-console-authz">go/nomulus-console-authz</a> for more
* information.
*/
@Embeddable
public class UserRoles extends ImmutableObject implements Buildable {
/** Whether the user is a global admin, who has access to everything. */
@Column(nullable = false)
private boolean isAdmin = false;
/**
* The global role (e.g. {@link GlobalRole#SUPPORT_AGENT}) that the user has across all
* registrars.
*/
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private GlobalRole globalRole = GlobalRole.NONE;
/** Any per-registrar roles that this user may have. */

View File

@@ -15,7 +15,6 @@
package google.registry.model.contact;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Entity;
import google.registry.model.EppResource.ForeignKeyedEppResource;
import google.registry.model.annotations.ExternalMessagingName;
import google.registry.model.annotations.ReportedOn;
@@ -23,6 +22,10 @@ import google.registry.persistence.VKey;
import google.registry.persistence.WithStringVKey;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.Table;
import org.joda.time.DateTime;
/**
@@ -32,35 +35,35 @@ import org.joda.time.DateTime;
*/
@ReportedOn
@Entity
@javax.persistence.Entity(name = "Contact")
@javax.persistence.Table(
@com.googlecode.objectify.annotation.Entity
@Table(
name = "Contact",
indexes = {
@javax.persistence.Index(columnList = "creationTime"),
@javax.persistence.Index(columnList = "currentSponsorRegistrarId"),
@javax.persistence.Index(columnList = "deletionTime"),
@javax.persistence.Index(columnList = "contactId"),
@javax.persistence.Index(columnList = "searchName")
@Index(columnList = "creationTime"),
@Index(columnList = "currentSponsorRegistrarId"),
@Index(columnList = "deletionTime"),
@Index(columnList = "contactId"),
@Index(columnList = "searchName")
})
@ExternalMessagingName("contact")
@WithStringVKey
@Access(AccessType.FIELD)
public class ContactResource extends ContactBase implements ForeignKeyedEppResource {
public class Contact extends ContactBase implements ForeignKeyedEppResource {
@Override
public VKey<ContactResource> createVKey() {
return VKey.create(ContactResource.class, getRepoId(), Key.create(this));
public VKey<Contact> createVKey() {
return VKey.create(Contact.class, getRepoId(), Key.create(this));
}
@Override
@javax.persistence.Id
@Id
@Access(AccessType.PROPERTY)
public String getRepoId() {
return super.getRepoId();
}
@Override
public ContactResource cloneProjectedAtTime(DateTime now) {
public Contact cloneProjectedAtTime(DateTime now) {
return ContactBase.cloneContactProjectedAtTime(this, now);
}
@@ -69,12 +72,12 @@ public class ContactResource extends ContactBase implements ForeignKeyedEppResou
return new Builder(clone(this));
}
/** A builder for constructing {@link ContactResource}, since it is immutable. */
public static class Builder extends ContactBase.Builder<ContactResource, Builder> {
/** A builder for constructing {@link Contact}, since it is immutable. */
public static class Builder extends ContactBase.Builder<Contact, Builder> {
public Builder() {}
private Builder(ContactResource instance) {
private Builder(Contact instance) {
super(instance);
}

View File

@@ -14,7 +14,6 @@
package google.registry.model.contact;
import com.googlecode.objectify.annotation.Embed;
import google.registry.model.eppcommon.Address;
import javax.persistence.Embeddable;
@@ -30,7 +29,6 @@ import javax.persistence.Embeddable;
*
* @see PostalInfo
*/
@Embed
@Embeddable
public class ContactAddress extends Address {

View File

@@ -19,6 +19,7 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.model.EppResourceUtils.projectResourceOntoBuilderAtTime;
import com.google.common.collect.ImmutableList;
import com.googlecode.objectify.annotation.Ignore;
import com.googlecode.objectify.annotation.IgnoreSave;
import com.googlecode.objectify.annotation.Index;
import com.googlecode.objectify.annotation.OnLoad;
@@ -66,9 +67,9 @@ public class ContactBase extends EppResource implements ResourceWithTransferData
/**
* Localized postal info for the contact. All contained values must be representable in the 7-bit
* US-ASCII character set. Personal info; cleared by {@link ContactResource.Builder#wipeOut}.
* US-ASCII character set. Personal info; cleared by {@link Contact.Builder#wipeOut}.
*/
@IgnoreSave(IfNull.class)
@Ignore
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "name", column = @Column(name = "addr_local_name")),
@@ -94,9 +95,9 @@ public class ContactBase extends EppResource implements ResourceWithTransferData
/**
* Internationalized postal info for the contact. Personal info; cleared by {@link
* ContactResource.Builder#wipeOut}.
* Contact.Builder#wipeOut}.
*/
@IgnoreSave(IfNull.class)
@Ignore
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "name", column = @Column(name = "addr_i18n_name")),
@@ -123,11 +124,11 @@ public class ContactBase extends EppResource implements ResourceWithTransferData
/**
* Contact name used for name searches. This is set automatically to be the internationalized
* postal name, or if null, the localized postal name, or if that is null as well, null. Personal
* info; cleared by {@link ContactResource.Builder#wipeOut}.
* info; cleared by {@link Contact.Builder#wipeOut}.
*/
@Index String searchName;
/** Contacts voice number. Personal info; cleared by {@link ContactResource.Builder#wipeOut}. */
/** Contacts voice number. Personal info; cleared by {@link Contact.Builder#wipeOut}. */
@IgnoreSave(IfNull.class)
@Embedded
@AttributeOverrides({
@@ -136,7 +137,7 @@ public class ContactBase extends EppResource implements ResourceWithTransferData
})
ContactPhoneNumber voice;
/** Contacts fax number. Personal info; cleared by {@link ContactResource.Builder#wipeOut}. */
/** Contacts fax number. Personal info; cleared by {@link Contact.Builder#wipeOut}. */
@IgnoreSave(IfNull.class)
@Embedded
@AttributeOverrides({
@@ -145,7 +146,7 @@ public class ContactBase extends EppResource implements ResourceWithTransferData
})
ContactPhoneNumber fax;
/** Contacts email address. Personal info; cleared by {@link ContactResource.Builder#wipeOut}. */
/** Contacts email address. Personal info; cleared by {@link Contact.Builder#wipeOut}. */
@IgnoreSave(IfNull.class)
String email;
@@ -187,7 +188,7 @@ public class ContactBase extends EppResource implements ResourceWithTransferData
public VKey<? extends ContactBase> createVKey() {
throw new UnsupportedOperationException(
"ContactBase is not an actual persisted entity you can create a key to;"
+ " use ContactResource instead");
+ " use Contact instead");
}
@OnLoad
@@ -291,7 +292,7 @@ public class ContactBase extends EppResource implements ResourceWithTransferData
return new Builder<>(clone(this));
}
/** A builder for constructing {@link ContactResource}, since it is immutable. */
/** A builder for constructing {@link Contact}, since it is immutable. */
public static class Builder<T extends ContactBase, B extends Builder<T, B>>
extends EppResource.Builder<T, B> implements BuilderWithTransferData<ContactTransferData, B> {

View File

@@ -34,13 +34,13 @@ import javax.xml.bind.annotation.XmlType;
import javax.xml.bind.annotation.adapters.CollapsedStringAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
/** A collection of {@link ContactResource} commands. */
/** A collection of {@link Contact} commands. */
public class ContactCommand {
/** The fields on "chgType" from <a href="http://tools.ietf.org/html/rfc5733">RFC5733</a>. */
@XmlTransient
public static class ContactCreateOrChange extends ImmutableObject
implements ResourceCreateOrChange<ContactResource.Builder> {
implements ResourceCreateOrChange<Contact.Builder> {
/** Postal info for the contact. */
List<PostalInfo> postalInfo;
@@ -111,13 +111,13 @@ public class ContactCommand {
}
/**
* A create command for a {@link ContactResource}, mapping "createType" from <a
* A create command for a {@link Contact}, mapping "createType" from <a
* href="http://tools.ietf.org/html/rfc5733">RFC5733</a>}.
*/
@XmlType(propOrder = {"contactId", "postalInfo", "voice", "fax", "email", "authInfo", "disclose"})
@XmlRootElement
public static class Create extends ContactCreateOrChange
implements SingleResourceCommand, ResourceCreateOrChange<ContactResource.Builder> {
implements SingleResourceCommand, ResourceCreateOrChange<Contact.Builder> {
/**
* Unique identifier for this contact.
*
@@ -139,29 +139,29 @@ public class ContactCommand {
}
}
/** A delete command for a {@link ContactResource}. */
/** A delete command for a {@link Contact}. */
@XmlRootElement
public static class Delete extends AbstractSingleResourceCommand {}
/** An info request for a {@link ContactResource}. */
/** An info request for a {@link Contact}. */
@XmlRootElement
@XmlType(propOrder = {"targetId", "authInfo"})
public static class Info extends AbstractContactAuthCommand {}
/** A check request for {@link ContactResource}. */
/** A check request for {@link Contact}. */
@XmlRootElement
public static class Check extends ResourceCheck {}
/** A transfer operation for a {@link ContactResource}. */
/** A transfer operation for a {@link Contact}. */
@XmlRootElement
@XmlType(propOrder = {"targetId", "authInfo"})
public static class Transfer extends AbstractContactAuthCommand {}
/** An update to a {@link ContactResource}. */
/** An update to a {@link Contact}. */
@XmlRootElement
@XmlType(propOrder = {"targetId", "innerAdd", "innerRemove", "innerChange"})
public static class Update
extends ResourceUpdate<Update.AddRemove, ContactResource.Builder, Update.Change> {
extends ResourceUpdate<Update.AddRemove, Contact.Builder, Update.Change> {
@XmlElement(name = "chg")
protected Change innerChange;

View File

@@ -58,7 +58,7 @@ import javax.persistence.PostLoad;
@IdClass(ContactHistoryId.class)
public class ContactHistory extends HistoryEntry implements UnsafeSerializable {
// Store ContactBase instead of ContactResource so we don't pick up its @Id
// Store ContactBase instead of Contact so we don't pick up its @Id
// Nullable for the sake of pre-Registry-3.0 history objects
@DoNotCompare @Nullable ContactBase contactBase;
@@ -73,7 +73,7 @@ public class ContactHistory extends HistoryEntry implements UnsafeSerializable {
/** This method is private because it is only used by Hibernate. */
@SuppressWarnings("unused")
private void setContactRepoId(String contactRepoId) {
parent = Key.create(ContactResource.class, contactRepoId);
parent = Key.create(Contact.class, contactRepoId);
}
@Id
@@ -98,9 +98,9 @@ public class ContactHistory extends HistoryEntry implements UnsafeSerializable {
return Optional.ofNullable(contactBase);
}
/** The key to the {@link ContactResource} this is based off of. */
public VKey<ContactResource> getParentVKey() {
return VKey.create(ContactResource.class, getContactRepoId());
/** The key to the {@link Contact} this is based off of. */
public VKey<Contact> getParentVKey() {
return VKey.create(Contact.class, getContactRepoId());
}
/** Creates a {@link VKey} instance for this entity. */
@@ -112,8 +112,7 @@ public class ContactHistory extends HistoryEntry implements UnsafeSerializable {
@Override
public Optional<? extends EppResource> getResourceAtPointInTime() {
return getContactBase()
.map(contactBase -> new ContactResource.Builder().copyFrom(contactBase).build());
return getContactBase().map(contactBase -> new Contact.Builder().copyFrom(contactBase).build());
}
@PostLoad
@@ -210,7 +209,7 @@ public class ContactHistory extends HistoryEntry implements UnsafeSerializable {
}
public Builder setContactRepoId(String contactRepoId) {
getInstance().parent = Key.create(ContactResource.class, contactRepoId);
getInstance().parent = Key.create(Contact.class, contactRepoId);
return this;
}

View File

@@ -21,11 +21,11 @@ import javax.persistence.Embeddable;
/**
* EPP Contact Phone Number
*
* <p>This class is embedded inside a {@link ContactResource} hold the phone number of an EPP
* contact. The fields are all defined in the parent class {@link PhoneNumber}, but the subclass is
* still necessary to pick up the contact namespace.
* <p>This class is embedded inside a {@link Contact} hold the phone number of an EPP contact. The
* fields are all defined in the parent class {@link PhoneNumber}, but the subclass is still
* necessary to pick up the contact namespace.
*
* @see ContactResource
* @see Contact
*/
@Embed
@Embeddable

View File

@@ -16,7 +16,6 @@ package google.registry.model.contact;
import static com.google.common.base.Preconditions.checkState;
import com.googlecode.objectify.annotation.Embed;
import google.registry.model.Buildable;
import google.registry.model.Buildable.Overlayable;
import google.registry.model.ImmutableObject;
@@ -36,7 +35,6 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
* Implementation of both "postalInfoType" and "chgPostalInfoType" from <a href=
* "http://tools.ietf.org/html/rfc5733">RFC5733</a>.
*/
@Embed
@Embeddable
@XmlType(propOrder = {"name", "org", "address", "type"})
public class PostalInfo extends ImmutableObject

View File

@@ -22,7 +22,7 @@ import com.googlecode.objectify.annotation.Ignore;
import com.googlecode.objectify.annotation.Index;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.Contact;
import google.registry.persistence.VKey;
import javax.persistence.Embeddable;
import javax.xml.bind.annotation.XmlEnumValue;
@@ -64,7 +64,7 @@ public class DesignatedContact extends ImmutableObject implements UnsafeSerializ
REGISTRANT
}
public static DesignatedContact create(Type type, VKey<ContactResource> contact) {
public static DesignatedContact create(Type type, VKey<Contact> contact) {
DesignatedContact instance = new DesignatedContact();
instance.type = type;
instance.contactVKey = checkArgumentNotNull(contact, "Must specify contact key");
@@ -74,14 +74,14 @@ public class DesignatedContact extends ImmutableObject implements UnsafeSerializ
Type type;
@Index Key<ContactResource> contact;
@Ignore VKey<ContactResource> contactVKey;
@Index Key<Contact> contact;
@Ignore VKey<Contact> contactVKey;
public Type getType() {
return type;
}
public VKey<ContactResource> getContactKey() {
public VKey<Contact> getContactKey() {
return contactVKey;
}

View File

@@ -90,13 +90,13 @@ public class Domain extends DomainBase implements ForeignKeyedEppResource {
@Access(AccessType.PROPERTY)
@Column(name = "host_repo_id")
public Set<VKey<Host>> getNsHosts() {
return super.nsHosts;
return nsHosts;
}
/**
* Returns the set of {@link GracePeriod} associated with the domain.
*
* <p>This is the getter method specific for Hibernate to access the field so it is set to
* <p>This is the getter method specific for Hibernate to access the field, so it is set to
* private. The caller can use the public {@link #getGracePeriods()} to get the grace periods.
*
* <p>Note that we need to set `insertable = false, updatable = false` for @JoinColumn, otherwise
@@ -104,10 +104,7 @@ public class Domain extends DomainBase implements ForeignKeyedEppResource {
* deleting the whole entry from the table when the {@link GracePeriod} is removed from the set.
*/
@Access(AccessType.PROPERTY)
@OneToMany(
cascade = {CascadeType.ALL},
fetch = FetchType.EAGER,
orphanRemoval = true)
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
@JoinColumn(
name = "domainRepoId",
referencedColumnName = "repoId",
@@ -121,7 +118,7 @@ public class Domain extends DomainBase implements ForeignKeyedEppResource {
/**
* Returns the set of {@link DelegationSignerData} associated with the domain.
*
* <p>This is the getter method specific for Hibernate to access the field so it is set to
* <p>This is the getter method specific for Hibernate to access the field, so it is set to
* private. The caller can use the public {@link #getDsData()} to get the DS data.
*
* <p>Note that we need to set `insertable = false, updatable = false` for @JoinColumn, otherwise
@@ -130,10 +127,7 @@ public class Domain extends DomainBase implements ForeignKeyedEppResource {
* the set.
*/
@Access(AccessType.PROPERTY)
@OneToMany(
cascade = {CascadeType.ALL},
fetch = FetchType.EAGER,
orphanRemoval = true)
@OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, orphanRemoval = true)
@JoinColumn(
name = "domainRepoId",
referencedColumnName = "repoId",
@@ -182,10 +176,9 @@ public class Domain extends DomainBase implements ForeignKeyedEppResource {
}
public Builder copyFrom(DomainBase domainBase) {
this.getInstance().copyUpdateTimestamp(domainBase);
return this.setAuthInfo(domainBase.getAuthInfo())
.setAutorenewPollMessage(
domainBase.getAutorenewPollMessage(), domainBase.getAutorenewPollMessageHistoryId())
getInstance().copyUpdateTimestamp(domainBase);
return setAuthInfo(domainBase.getAuthInfo())
.setAutorenewPollMessage(domainBase.getAutorenewPollMessage())
.setAutorenewBillingEvent(domainBase.getAutorenewBillingEvent())
.setAutorenewEndTime(domainBase.getAutorenewEndTime())
.setContacts(domainBase.getContacts())

View File

@@ -52,7 +52,7 @@ import google.registry.model.EppResource;
import google.registry.model.EppResource.ResourceWithTransferData;
import google.registry.model.billing.BillingEvent;
import google.registry.model.common.EntityGroupRoot;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.Contact;
import google.registry.model.domain.launch.LaunchNotice;
import google.registry.model.domain.rgp.GracePeriodStatus;
import google.registry.model.domain.secdns.DelegationSignerData;
@@ -132,11 +132,11 @@ public class DomainBase extends EppResource
@EmptySetToNull @Index @Transient Set<VKey<Host>> nsHosts;
/** Contacts. */
VKey<ContactResource> adminContact;
VKey<Contact> adminContact;
VKey<ContactResource> billingContact;
VKey<ContactResource> techContact;
VKey<ContactResource> registrantContact;
VKey<Contact> billingContact;
VKey<Contact> techContact;
VKey<Contact> registrantContact;
/** Authorization info (aka transfer secret) of the domain. */
@Embedded
@@ -216,13 +216,6 @@ public class DomainBase extends EppResource
@Column(name = "autorenew_poll_message_id")
VKey<PollMessage.Autorenew> autorenewPollMessage;
/**
* History record for the autorenew poll message.
*
* <p>Here so we can restore the original ofy key from sql.
*/
@Ignore Long autorenewPollMessageHistoryId;
/** The unexpired grace periods for this domain (some of which may not be active yet). */
@Transient Set<GracePeriod> gracePeriods;
@@ -322,10 +315,6 @@ public class DomainBase extends EppResource
return autorenewPollMessage;
}
public Long getAutorenewPollMessageHistoryId() {
return autorenewPollMessageHistoryId;
}
public ImmutableSet<GracePeriod> getGracePeriods() {
return nullToEmptyImmutableCopy(gracePeriods);
}
@@ -509,9 +498,7 @@ public class DomainBase extends EppResource
// Set the speculatively-written new autorenew events as the domain's autorenew
// events.
.setAutorenewBillingEvent(transferData.getServerApproveAutorenewEvent())
.setAutorenewPollMessage(
transferData.getServerApproveAutorenewPollMessage(),
transferData.getServerApproveAutorenewPollMessageHistoryId());
.setAutorenewPollMessage(transferData.getServerApproveAutorenewPollMessage());
if (transferData.getTransferPeriod().getValue() == 1) {
// Set the grace period using a key to the prescheduled transfer billing event. Not using
// GracePeriod.forBillingEvent() here in order to avoid the actual Datastore fetch.
@@ -615,19 +602,19 @@ public class DomainBase extends EppResource
}
/** A key to the registrant who registered this domain. */
public VKey<ContactResource> getRegistrant() {
public VKey<Contact> getRegistrant() {
return registrantContact;
}
public VKey<ContactResource> getAdminContact() {
public VKey<Contact> getAdminContact() {
return adminContact;
}
public VKey<ContactResource> getBillingContact() {
public VKey<Contact> getBillingContact() {
return billingContact;
}
public VKey<ContactResource> getTechContact() {
public VKey<Contact> getTechContact() {
return techContact;
}
@@ -641,7 +628,7 @@ public class DomainBase extends EppResource
}
/** Returns all referenced contacts from this domain. */
public ImmutableSet<VKey<ContactResource>> getReferencedContacts() {
public ImmutableSet<VKey<Contact>> getReferencedContacts() {
return nullToEmptyImmutableCopy(getAllContacts(true)).stream()
.map(DesignatedContact::getContactKey)
.filter(Objects::nonNull)
@@ -784,7 +771,7 @@ public class DomainBase extends EppResource
return thisCastToDerived();
}
public B setRegistrant(VKey<ContactResource> registrant) {
public B setRegistrant(VKey<Contact> registrant) {
// Set the registrant field specifically.
getInstance().registrantContact = registrant;
return thisCastToDerived();
@@ -886,11 +873,8 @@ public class DomainBase extends EppResource
return thisCastToDerived();
}
public B setAutorenewPollMessage(
@Nullable VKey<PollMessage.Autorenew> autorenewPollMessage,
@Nullable Long autorenewPollMessageHistoryId) {
public B setAutorenewPollMessage(@Nullable VKey<PollMessage.Autorenew> autorenewPollMessage) {
getInstance().autorenewPollMessage = autorenewPollMessage;
getInstance().autorenewPollMessageHistoryId = autorenewPollMessageHistoryId;
return thisCastToDerived();
}

View File

@@ -32,7 +32,7 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import google.registry.model.EppResource;
import google.registry.model.ImmutableObject;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.Contact;
import google.registry.model.eppinput.ResourceCommand.AbstractSingleResourceCommand;
import google.registry.model.eppinput.ResourceCommand.ResourceCheck;
import google.registry.model.eppinput.ResourceCommand.ResourceCreateOrChange;
@@ -80,7 +80,7 @@ public class DomainCommand {
String registrantContactId;
/** A resolved key to the registrant who registered this domain. */
@XmlTransient VKey<ContactResource> registrant;
@XmlTransient VKey<Contact> registrant;
/** Authorization info (aka transfer secret) of the domain. */
DomainAuthInfo authInfo;
@@ -90,7 +90,7 @@ public class DomainCommand {
}
@Nullable
public VKey<ContactResource> getRegistrant() {
public VKey<Contact> getRegistrant() {
return registrant;
}
@@ -391,7 +391,7 @@ public class DomainCommand {
? null
: getOnlyElement(
loadByForeignKeysCached(
ImmutableSet.of(clone.registrantContactId), ContactResource.class, now)
ImmutableSet.of(clone.registrantContactId), Contact.class, now)
.values());
return clone;
}
@@ -431,8 +431,8 @@ public class DomainCommand {
for (ForeignKeyedDesignatedContact contact : contacts) {
foreignKeys.add(contact.contactId);
}
ImmutableMap<String, VKey<ContactResource>> loadedContacts =
loadByForeignKeysCached(foreignKeys.build(), ContactResource.class, now);
ImmutableMap<String, VKey<Contact>> loadedContacts =
loadByForeignKeysCached(foreignKeys.build(), Contact.class, now);
ImmutableSet.Builder<DesignatedContact> linkedContacts = new ImmutableSet.Builder<>();
for (ForeignKeyedDesignatedContact contact : contacts) {
linkedContacts.add(

View File

@@ -0,0 +1,23 @@
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.model.domain.packagetoken;
import google.registry.model.ImmutableObject;
import google.registry.model.eppinput.EppInput.CommandExtension;
import javax.xml.bind.annotation.XmlRootElement;
/** A package token extension that may be present on EPP domain commands. */
@XmlRootElement(name = "info")
public class PackageTokenExtension extends ImmutableObject implements CommandExtension {}

View File

@@ -0,0 +1,45 @@
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.model.domain.packagetoken;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.token.AllocationToken;
import google.registry.model.eppoutput.EppResponse.ResponseExtension;
import google.registry.persistence.VKey;
import google.registry.xml.TrimWhitespaceAdapter;
import java.util.Optional;
import javax.xml.bind.annotation.XmlRootElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
/**
* An XML data object that represents a package token extension that may be present on the response
* to EPP domain info commands.
*/
@XmlRootElement(name = "packageData")
public class PackageTokenResponseExtension extends ImmutableObject implements ResponseExtension {
/** Token string of the PACKAGE token the name belongs to. */
@XmlJavaTypeAdapter(TrimWhitespaceAdapter.class)
String token;
public static PackageTokenResponseExtension create(Optional<VKey<AllocationToken>> tokenKey) {
PackageTokenResponseExtension instance = new PackageTokenResponseExtension();
instance.token = "";
if (tokenKey.isPresent()) {
instance.token = tokenKey.get().getSqlKey().toString();
}
return instance;
}
}

View File

@@ -0,0 +1,27 @@
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
@XmlSchema(
namespace = "urn:google:params:xml:ns:packageToken-1.0",
xmlns =
@XmlNs(prefix = "packageToken", namespaceURI = "urn:google:params:xml:ns:packageToken-1.0"),
elementFormDefault = XmlNsForm.QUALIFIED)
@XmlAccessorType(XmlAccessType.FIELD)
package google.registry.model.domain.packagetoken;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlNs;
import javax.xml.bind.annotation.XmlNsForm;
import javax.xml.bind.annotation.XmlSchema;

View File

@@ -26,6 +26,7 @@ import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableMultimap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
@@ -79,6 +80,10 @@ import org.joda.time.DateTime;
public class AllocationToken extends BackupGroupRoot implements Buildable {
private static final long serialVersionUID = -3954475393220876903L;
private static final String REMOVE_PACKAGE = "__REMOVEPACKAGE__";
private static final ImmutableMap<String, TokenBehavior> STATIC_TOKEN_BEHAVIORS =
ImmutableMap.of(REMOVE_PACKAGE, TokenBehavior.REMOVE_PACKAGE);
// Promotions should only move forward, and ENDED / CANCELLED are terminal states.
private static final ImmutableMultimap<TokenStatus, TokenStatus> VALID_TOKEN_STATUS_TRANSITIONS =
@@ -87,6 +92,18 @@ public class AllocationToken extends BackupGroupRoot implements Buildable {
.putAll(VALID, ENDED, CANCELLED)
.build();
private static final ImmutableMap<String, AllocationToken> BEHAVIORAL_TOKENS =
ImmutableMap.of(
REMOVE_PACKAGE,
new AllocationToken.Builder()
.setTokenType(TokenType.UNLIMITED_USE)
.setToken(REMOVE_PACKAGE)
.build());
public static Optional<AllocationToken> maybeGetStaticTokenInstance(String name) {
return Optional.ofNullable(BEHAVIORAL_TOKENS.get(name));
}
/** Any special behavior that should be used when registering domains using this token. */
public enum RegistrationBehavior {
/** No special behavior */
@@ -103,10 +120,28 @@ public class AllocationToken extends BackupGroupRoot implements Buildable {
ANCHOR_TENANT
}
/** Single-use tokens are invalid after use. Infinite-use tokens, predictably, are not. */
/**
* Single-use tokens are invalid after use. Infinite-use tokens, predictably, are not. Package
* tokens are used in package promotions.
*/
public enum TokenType {
PACKAGE,
SINGLE_USE,
UNLIMITED_USE
UNLIMITED_USE,
}
/**
* System behaves differently based on a token it gets inside a command. This enumerates different
* types of behaviors we support.
*/
public enum TokenBehavior {
/** No special behavior */
DEFAULT,
/**
* REMOVE_PACKAGE triggers domain removal from promotional package, bypasses DEFAULT token
* validations.
*/
REMOVE_PACKAGE
}
/** The status of this token with regard to any potential promotion. */
@@ -251,8 +286,16 @@ public class AllocationToken extends BackupGroupRoot implements Buildable {
return registrationBehavior;
}
public TokenBehavior getTokenBehavior() {
return STATIC_TOKEN_BEHAVIORS.getOrDefault(token, TokenBehavior.DEFAULT);
}
@Override
public VKey<AllocationToken> createVKey() {
if (!AllocationToken.TokenBehavior.DEFAULT.equals(getTokenBehavior())) {
throw new IllegalArgumentException(
String.format("%s tokens are not stored in the database", getTokenBehavior()));
}
return VKey.create(AllocationToken.class, getToken(), Key.create(this));
}
@@ -274,6 +317,10 @@ public class AllocationToken extends BackupGroupRoot implements Buildable {
public AllocationToken build() {
checkArgumentNotNull(getInstance().tokenType, "Token type must be specified");
checkArgument(!Strings.isNullOrEmpty(getInstance().token), "Token must not be null or empty");
checkArgument(
!getInstance().tokenType.equals(TokenType.PACKAGE)
|| getInstance().renewalPriceBehavior.equals(RenewalPriceBehavior.SPECIFIED),
"Package tokens must have renewalPriceBehavior set to SPECIFIED");
checkArgument(
getInstance().domainName == null || TokenType.SINGLE_USE.equals(getInstance().tokenType),
"Domain name can only be specified for SINGLE_USE tokens");
@@ -281,6 +328,10 @@ public class AllocationToken extends BackupGroupRoot implements Buildable {
getInstance().redemptionHistoryEntry == null
|| TokenType.SINGLE_USE.equals(getInstance().tokenType),
"Redemption history entry can only be specified for SINGLE_USE tokens");
checkArgument(
getInstance().tokenType != TokenType.PACKAGE
|| getInstance().allowedClientIds.size() == 1,
"PACKAGE tokens must have exactly one allowed client registrar");
checkArgument(
getInstance().discountFraction > 0 || !getInstance().discountPremiums,
"Discount premiums can only be specified along with a discount fraction");

View File

@@ -0,0 +1,157 @@
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.model.domain.token;
import static com.google.common.base.Preconditions.checkArgument;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import google.registry.model.domain.token.AllocationToken.TokenType;
import google.registry.persistence.VKey;
import google.registry.persistence.converter.JodaMoneyType;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import org.hibernate.annotations.Columns;
import org.hibernate.annotations.Type;
import org.joda.money.Money;
import org.joda.time.DateTime;
/** An entity representing a package promotion. */
@Entity
@javax.persistence.Table(indexes = {@javax.persistence.Index(columnList = "token")})
public class PackagePromotion extends ImmutableObject implements Buildable {
/** An autogenerated identifier for the package promotion. */
@Id long packagePromotionId;
/** The allocation token string for the package. */
@Column(nullable = false)
VKey<AllocationToken> token;
/** The maximum number of active domains the package allows at any given time. */
@Column(nullable = false)
int maxDomains;
/** The maximum number of domains that can be created in the package each year. */
@Column(nullable = false)
int maxCreates;
/** The annual price of the package. */
@Type(type = JodaMoneyType.TYPE_NAME)
@Columns(
columns = {
@Column(name = "package_price_amount", nullable = false),
@Column(name = "package_price_currency", nullable = false)
})
Money packagePrice;
/** The next billing date of the package. */
@Column(nullable = false)
DateTime nextBillingDate = END_OF_TIME;
/** Date the last warning email was sent that the package has exceeded the maxDomains limit. */
@Nullable DateTime lastNotificationSent;
public VKey<AllocationToken> getToken() {
return token;
}
public int getMaxDomains() {
return maxDomains;
}
public int getMaxCreates() {
return maxCreates;
}
public Money getPackagePrice() {
return packagePrice;
}
public DateTime getNextBillingDate() {
return nextBillingDate;
}
public Optional<DateTime> getLastNotificationSent() {
return Optional.ofNullable(lastNotificationSent);
}
@Override
public Builder asBuilder() {
return new Builder(clone(this));
}
/** A builder for constructing {@link PackagePromotion} objects, since they are immutable. */
public static class Builder extends Buildable.Builder<PackagePromotion> {
public Builder() {}
private Builder(PackagePromotion instance) {
super(instance);
}
@Override
public PackagePromotion build() {
checkArgumentNotNull(getInstance().token, "Allocation token must be specified");
AllocationToken allocationToken = tm().transact(() -> tm().loadByKey(getInstance().token));
checkArgument(
allocationToken.tokenType == TokenType.PACKAGE,
"Allocation token must be a PACKAGE type");
return super.build();
}
public Builder setToken(AllocationToken token) {
checkArgumentNotNull(token, "Allocation token must not be null");
checkArgument(
token.tokenType == TokenType.PACKAGE, "Allocation token must be a PACKAGE type");
getInstance().token = token.createVKey();
return this;
}
public Builder setMaxDomains(int maxDomains) {
checkArgumentNotNull(maxDomains, "maxDomains must not be null");
getInstance().maxDomains = maxDomains;
return this;
}
public Builder setMaxCreates(int maxCreates) {
checkArgumentNotNull(maxCreates, "maxCreates must not be null");
getInstance().maxCreates = maxCreates;
return this;
}
public Builder setPackagePrice(Money packagePrice) {
checkArgumentNotNull(packagePrice, "Package price must not be null");
getInstance().packagePrice = packagePrice;
return this;
}
public Builder setNextBillingDate(@Nullable DateTime nextBillingDate) {
checkArgumentNotNull(nextBillingDate, "Next billing date must not be null");
getInstance().nextBillingDate = nextBillingDate;
return this;
}
public Builder setLastNotificationSent(@Nullable DateTime lastNotificationSent) {
getInstance().lastNotificationSent = lastNotificationSent;
return this;
}
}
}

View File

@@ -15,14 +15,11 @@
package google.registry.model.eppcommon;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Strings.nullToEmpty;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.util.CollectionUtils.nullToEmptyImmutableCopy;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.googlecode.objectify.annotation.AlsoLoad;
import com.googlecode.objectify.annotation.Ignore;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import google.registry.model.JsonMapBuilder;
@@ -59,40 +56,62 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
@MappedSuperclass
public class Address extends ImmutableObject implements Jsonifiable, UnsafeSerializable {
/** The schema validation will enforce that this has 3 lines at most. */
// TODO(b/177569726): Remove this field after migration. We need to figure out how to generate
// same XML from streetLine[1,2,3].
/**
* At most three lines of addresses parsed from XML elements.
*
* <p>This field is used to marshal to/unmarshal from XML elements. When persisting to/from SQL,
* the next three separate fields are used. Those lines are <em>only</em> used for persistence
* purpose and should not be directly used in Java.
*
* <p>We need to keep the list and the three fields in sync in all the following scenarios when an
* entity containing a {@link Address} is created:
*
* <ul>
* <li>When creating an {@link Address} directly in java, using the {@link Builder}: The {@link
* Builder#setStreet(ImmutableList)} sets both the list and the fields.
* <li>When unmarshalling from XML: The list will be set based on the content of the XML.
* Afterwards, {@link #afterUnmarshal(Unmarshaller, Object)}} will be called to set the
* fields.
* <li>When loading from the database: The fields will be set by the values in SQL. Afterwards,
* {@link #postLoad()} will be called to set the list.
* </ul>
*
* The syncing is especially important because when merging a detached entity into a session, JPA
* provides no guarantee that transient fields will be preserved. In fact, Hibernate chooses to
* discard them when returning a newly merged entity. This means that it is not enough to provide
* callbacks to populate the fields based on the list before persistence, as merging does not
* invoke the callbacks, and we would lose the address fields if only the list is set. Instead,
* the fields must be populated when the list is.
*
* <p>Schema validation will enforce the 3-line limit.
*/
@XmlJavaTypeAdapter(NormalizedStringAdapter.class)
@Transient
List<String> street;
protected List<String> street;
@Ignore @XmlTransient @IgnoredInDiffableMap String streetLine1;
@XmlTransient @IgnoredInDiffableMap protected String streetLine1;
@Ignore @XmlTransient @IgnoredInDiffableMap String streetLine2;
@XmlTransient @IgnoredInDiffableMap protected String streetLine2;
@Ignore @XmlTransient @IgnoredInDiffableMap String streetLine3;
@XmlTransient @IgnoredInDiffableMap protected String streetLine3;
@XmlJavaTypeAdapter(NormalizedStringAdapter.class)
String city;
protected String city;
@XmlElement(name = "sp")
@XmlJavaTypeAdapter(NormalizedStringAdapter.class)
String state;
protected String state;
@XmlElement(name = "pc")
@XmlJavaTypeAdapter(CollapsedStringAdapter.class)
String zip;
protected String zip;
@XmlElement(name = "cc")
@XmlJavaTypeAdapter(CollapsedStringAdapter.class)
String countryCode;
protected String countryCode;
public ImmutableList<String> getStreet() {
if (street == null && streetLine1 != null) {
return ImmutableList.of(streetLine1, nullToEmpty(streetLine2), nullToEmpty(streetLine3));
} else {
return nullToEmptyImmutableCopy(street);
}
return nullToEmptyImmutableCopy(street);
}
public String getCity() {
@@ -139,7 +158,13 @@ public class Address extends ImmutableObject implements Jsonifiable, UnsafeSeria
public Builder<T> setStreet(ImmutableList<String> street) {
checkArgument(
street == null || (!street.isEmpty() && street.size() <= 3),
"Street address must have [1-3] lines: %s", street);
"Street address must have [1-3] lines: %s",
street);
//noinspection ConstantConditions
checkArgument(
street.stream().noneMatch(String::isEmpty),
"Street address cannot contain empty string: %s",
street);
getInstance().street = street;
getInstance().streetLine1 = street.get(0);
getInstance().streetLine2 = street.size() >= 2 ? street.get(1) : null;
@@ -171,24 +196,13 @@ public class Address extends ImmutableObject implements Jsonifiable, UnsafeSeria
}
}
/**
* Sets {@link #streetLine1}, {@link #streetLine2} and {@link #streetLine3} after loading the
* entity from Datastore.
*
* <p>This callback method is used by Objectify to set streetLine[1,2,3] fields as they are not
* persisted in the Datastore.
*/
void onLoad(@AlsoLoad("street") List<String> street) {
mapStreetListToIndividualFields(street);
}
/**
* Sets {@link #street} after loading the entity from Cloud SQL.
*
* <p>This callback method is used by Hibernate to set {@link #street} field as it is not
* persisted in Cloud SQL. We are doing this because the street list field is exposed by Address
* class and is used everywhere in our code base. Also, setting/reading a list of strings is more
* convenient.
* <p>This callback method is used by Hibernate to set the {@link #street} field as it is not
* persisted in Cloud SQL. We are doing this because this field is exposed and used everywhere in
* our code base, whereas the individual {@code streetLine} fields are only used by Hibernate for
* persistence. Also, setting/reading a list of strings is more convenient.
*/
@PostLoad
void postLoad() {
@@ -206,12 +220,9 @@ public class Address extends ImmutableObject implements Jsonifiable, UnsafeSeria
*
* <p>This is a callback function that JAXB invokes after unmarshalling the XML message.
*/
@SuppressWarnings("unused")
void afterUnmarshal(Unmarshaller unmarshaller, Object parent) {
mapStreetListToIndividualFields(street);
}
private void mapStreetListToIndividualFields(List<String> street) {
if (street == null || street.size() == 0) {
if (street == null || street.isEmpty()) {
return;
}
streetLine1 = street.get(0);

View File

@@ -31,25 +31,27 @@ import java.io.ByteArrayOutputStream;
public class EppXmlTransformer {
// Hardcoded XML schemas, ordered with respect to dependency.
private static final ImmutableList<String> SCHEMAS = ImmutableList.of(
"eppcom.xsd",
"epp.xsd",
"contact.xsd",
"host.xsd",
"domain.xsd",
"rgp.xsd",
"secdns.xsd",
"fee06.xsd",
"fee11.xsd",
"fee12.xsd",
"metadata.xsd",
"mark.xsd",
"dsig.xsd",
"smd.xsd",
"launch.xsd",
"allocate.xsd",
"superuser.xsd",
"allocationToken-1.0.xsd");
private static final ImmutableList<String> SCHEMAS =
ImmutableList.of(
"eppcom.xsd",
"epp.xsd",
"contact.xsd",
"host.xsd",
"domain.xsd",
"rgp.xsd",
"secdns.xsd",
"fee06.xsd",
"fee11.xsd",
"fee12.xsd",
"metadata.xsd",
"mark.xsd",
"dsig.xsd",
"smd.xsd",
"launch.xsd",
"allocate.xsd",
"superuser.xsd",
"allocationToken-1.0.xsd",
"packageToken.xsd");
private static final XmlTransformer INPUT_TRANSFORMER =
new XmlTransformer(SCHEMAS, EppInput.class);

View File

@@ -20,8 +20,8 @@ import static com.google.common.base.Strings.nullToEmpty;
import com.google.common.collect.ImmutableSet;
import google.registry.model.EppResource;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactBase;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainBase;
import google.registry.model.host.Host;
@@ -130,12 +130,12 @@ public enum StatusValue implements EppEnum {
/** Enum to help clearly list which resource types a status value is allowed to be present on. */
private enum AllowedOn {
ALL(
Contact.class,
ContactBase.class,
ContactResource.class,
DomainBase.class,
Domain.class,
HostBase.class,
Host.class),
DomainBase.class,
Host.class,
HostBase.class),
NONE,
DOMAINS(DomainBase.class, Domain.class);

View File

@@ -45,6 +45,7 @@ import google.registry.model.domain.launch.LaunchDeleteExtension;
import google.registry.model.domain.launch.LaunchInfoExtension;
import google.registry.model.domain.launch.LaunchUpdateExtension;
import google.registry.model.domain.metadata.MetadataExtension;
import google.registry.model.domain.packagetoken.PackageTokenExtension;
import google.registry.model.domain.rgp.RgpUpdateExtension;
import google.registry.model.domain.secdns.SecDnsCreateExtension;
import google.registry.model.domain.secdns.SecDnsUpdateExtension;
@@ -362,6 +363,7 @@ public class EppInput extends ImmutableObject {
// Other extensions
@XmlElementRef(type = AllocationTokenExtension.class),
@XmlElementRef(type = MetadataExtension.class),
@XmlElementRef(type = PackageTokenExtension.class),
@XmlElementRef(type = RgpUpdateExtension.class),
@XmlElementRef(type = SecDnsCreateExtension.class),
@XmlElementRef(type = SecDnsUpdateExtension.class)

View File

@@ -43,6 +43,7 @@ import google.registry.model.domain.fee12.FeeRenewResponseExtensionV12;
import google.registry.model.domain.fee12.FeeTransferResponseExtensionV12;
import google.registry.model.domain.fee12.FeeUpdateResponseExtensionV12;
import google.registry.model.domain.launch.LaunchCheckResponseExtension;
import google.registry.model.domain.packagetoken.PackageTokenResponseExtension;
import google.registry.model.domain.rgp.RgpInfoExtension;
import google.registry.model.domain.secdns.SecDnsInfoExtension;
import google.registry.model.eppcommon.Trid;
@@ -121,28 +122,30 @@ public class EppResponse extends ImmutableObject implements ResponseOrGreeting {
/** Zero or more response extensions. */
@XmlElementRefs({
@XmlElementRef(type = FeeCheckResponseExtensionV06.class),
@XmlElementRef(type = FeeInfoResponseExtensionV06.class),
@XmlElementRef(type = FeeCreateResponseExtensionV06.class),
@XmlElementRef(type = FeeDeleteResponseExtensionV06.class),
@XmlElementRef(type = FeeRenewResponseExtensionV06.class),
@XmlElementRef(type = FeeTransferResponseExtensionV06.class),
@XmlElementRef(type = FeeUpdateResponseExtensionV06.class),
@XmlElementRef(type = FeeCheckResponseExtensionV11.class),
@XmlElementRef(type = FeeCreateResponseExtensionV11.class),
@XmlElementRef(type = FeeDeleteResponseExtensionV11.class),
@XmlElementRef(type = FeeRenewResponseExtensionV11.class),
@XmlElementRef(type = FeeTransferResponseExtensionV11.class),
@XmlElementRef(type = FeeUpdateResponseExtensionV11.class),
@XmlElementRef(type = FeeCheckResponseExtensionV12.class),
@XmlElementRef(type = FeeCreateResponseExtensionV12.class),
@XmlElementRef(type = FeeDeleteResponseExtensionV12.class),
@XmlElementRef(type = FeeRenewResponseExtensionV12.class),
@XmlElementRef(type = FeeTransferResponseExtensionV12.class),
@XmlElementRef(type = FeeUpdateResponseExtensionV12.class),
@XmlElementRef(type = LaunchCheckResponseExtension.class),
@XmlElementRef(type = RgpInfoExtension.class),
@XmlElementRef(type = SecDnsInfoExtension.class) })
@XmlElementRef(type = FeeCheckResponseExtensionV06.class),
@XmlElementRef(type = FeeInfoResponseExtensionV06.class),
@XmlElementRef(type = FeeCreateResponseExtensionV06.class),
@XmlElementRef(type = FeeDeleteResponseExtensionV06.class),
@XmlElementRef(type = FeeRenewResponseExtensionV06.class),
@XmlElementRef(type = FeeTransferResponseExtensionV06.class),
@XmlElementRef(type = FeeUpdateResponseExtensionV06.class),
@XmlElementRef(type = FeeCheckResponseExtensionV11.class),
@XmlElementRef(type = FeeCreateResponseExtensionV11.class),
@XmlElementRef(type = FeeDeleteResponseExtensionV11.class),
@XmlElementRef(type = FeeRenewResponseExtensionV11.class),
@XmlElementRef(type = FeeTransferResponseExtensionV11.class),
@XmlElementRef(type = FeeUpdateResponseExtensionV11.class),
@XmlElementRef(type = FeeCheckResponseExtensionV12.class),
@XmlElementRef(type = FeeCreateResponseExtensionV12.class),
@XmlElementRef(type = FeeDeleteResponseExtensionV12.class),
@XmlElementRef(type = FeeRenewResponseExtensionV12.class),
@XmlElementRef(type = FeeTransferResponseExtensionV12.class),
@XmlElementRef(type = FeeUpdateResponseExtensionV12.class),
@XmlElementRef(type = LaunchCheckResponseExtension.class),
@XmlElementRef(type = PackageTokenResponseExtension.class),
@XmlElementRef(type = RgpInfoExtension.class),
@XmlElementRef(type = SecDnsInfoExtension.class)
})
@XmlElementWrapper(name = "extension")
ImmutableList<? extends ResponseExtension> extensions;

View File

@@ -47,7 +47,7 @@ import google.registry.model.CacheUtils.AppEngineEnvironmentCacheLoader;
import google.registry.model.EppResource;
import google.registry.model.annotations.DeleteAfterMigration;
import google.registry.model.annotations.ReportedOn;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.Contact;
import google.registry.model.domain.Domain;
import google.registry.model.host.Host;
import google.registry.persistence.VKey;
@@ -70,10 +70,10 @@ import org.joda.time.DateTime;
@DeleteAfterMigration
public abstract class ForeignKeyIndex<E extends EppResource> extends BackupGroupRoot {
/** The {@link ForeignKeyIndex} type for {@link ContactResource} entities. */
/** The {@link ForeignKeyIndex} type for {@link Contact} entities. */
@ReportedOn
@Entity
public static class ForeignKeyContactIndex extends ForeignKeyIndex<ContactResource> {}
public static class ForeignKeyContactIndex extends ForeignKeyIndex<Contact> {}
/** The {@link ForeignKeyIndex} type for {@link Domain} entities. */
@ReportedOn
@@ -89,14 +89,14 @@ public abstract class ForeignKeyIndex<E extends EppResource> extends BackupGroup
Class<? extends EppResource>, Class<? extends ForeignKeyIndex<?>>>
RESOURCE_CLASS_TO_FKI_CLASS =
ImmutableBiMap.of(
ContactResource.class, ForeignKeyContactIndex.class,
Contact.class, ForeignKeyContactIndex.class,
Domain.class, ForeignKeyDomainIndex.class,
Host.class, ForeignKeyHostIndex.class);
private static final ImmutableMap<Class<? extends EppResource>, String>
RESOURCE_CLASS_TO_FKI_PROPERTY =
ImmutableMap.of(
ContactResource.class, "contactId",
Contact.class, "contactId",
Domain.class, "fullyQualifiedDomainName",
Host.class, "fullyQualifiedHostName");

View File

@@ -34,7 +34,7 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
*
* <p>All first class entities are represented as a resource class - {@link
* google.registry.model.domain.Domain}, {@link google.registry.model.host.Host}, {@link
* google.registry.model.contact.ContactResource}, and {@link
* google.registry.model.contact.Contact}, and {@link
* google.registry.model.registrar.Registrar}. Resource objects are written in a single shared
* entity group per TLD. All commands that operate on those entities are grouped in a "Command"
* class- {@link google.registry.model.domain.DomainCommand}, {@link

View File

@@ -20,21 +20,15 @@ import static google.registry.util.DateTimeUtils.END_OF_TIME;
import static google.registry.util.PreconditionsUtils.checkArgumentNotNull;
import com.google.common.collect.ImmutableList;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.EntitySubclass;
import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.Ignore;
import com.googlecode.objectify.annotation.Index;
import google.registry.model.Buildable;
import google.registry.model.EppResource;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import google.registry.model.annotations.ExternalMessagingName;
import google.registry.model.annotations.ReportedOn;
import google.registry.model.annotations.OfyIdAllocation;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactHistory.ContactHistoryId;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.domain.DomainHistory.DomainHistoryId;
@@ -55,16 +49,19 @@ import google.registry.persistence.VKey;
import google.registry.persistence.WithLongVKey;
import google.registry.util.NullIgnoringCollectionBuilder;
import java.util.Optional;
import javax.annotation.Nullable;
import javax.persistence.AttributeOverride;
import javax.persistence.AttributeOverrides;
import javax.persistence.Column;
import javax.persistence.DiscriminatorColumn;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Index;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.PostLoad;
import javax.persistence.Table;
import org.joda.time.DateTime;
/**
@@ -76,10 +73,10 @@ import org.joda.time.DateTime;
* retained for historical purposes, poll messages are truly deleted once they have been delivered
* and ACKed.
*
* <p>Poll messages are parented off of the {@link HistoryEntry} that resulted in their creation.
* This means that poll messages are contained in the Datastore entity group of the parent {@link
* EppResource} (which can be a domain, contact, or host). It is thus possible to perform a strongly
* consistent query to find all poll messages associated with a given EPP resource.
* <p>Poll messages have a reference to the revision id of the {@link HistoryEntry} that resulted in
* their creation or re-creation, in case of transfer cancellation/rejection. The revision ids are
* not used directly by the Java code, but their presence in the database makes it easier to
* diagnose potential issues related to poll messages.
*
* <p>Poll messages are identified externally by registrars using the format defined in {@link
* PollMessageExternalKeyConverter}.
@@ -88,29 +85,23 @@ import org.joda.time.DateTime;
* Command</a>
*/
@Entity
@ReportedOn
@ExternalMessagingName("message")
@javax.persistence.Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "type")
@javax.persistence.Table(
indexes = {
@javax.persistence.Index(columnList = "registrar_id"),
@javax.persistence.Index(columnList = "eventTime")
})
@Table(indexes = {@Index(columnList = "registrar_id"), @Index(columnList = "eventTime")})
public abstract class PollMessage extends ImmutableObject
implements Buildable, TransferServerApproveEntity, UnsafeSerializable {
/** Indicates the type of entity the poll message is for. */
public enum Type {
DOMAIN(1L, Domain.class),
CONTACT(2L, ContactResource.class),
CONTACT(2L, Contact.class),
HOST(3L, Host.class);
private final long id;
private final Class<?> clazz;
private final Class<? extends EppResource> clazz;
Type(long id, Class<?> clazz) {
Type(long id, Class<? extends EppResource> clazz) {
this.id = id;
this.clazz = clazz;
}
@@ -124,58 +115,40 @@ public abstract class PollMessage extends ImmutableObject
}
/** Returns the class of the underlying resource for the poll message type. */
public Class<?> getResourceClass() {
public Class<? extends EppResource> getResourceClass() {
return clazz;
}
/**
* Returns the type specified by the identifer, {@code Optional.empty()} if the id is out of
* range.
*/
public static Optional<Type> fromId(long id) {
for (Type val : values()) {
if (val.id == id) {
return Optional.of(val);
}
}
return Optional.empty();
}
}
/** Entity id. */
@Id
@javax.persistence.Id
@OfyIdAllocation
@Column(name = "poll_message_id")
Long id;
/** The registrar that this poll message will be delivered to. */
@Index
@Column(name = "registrar_id", nullable = false)
String clientId;
/** The time when the poll message should be delivered. May be in the future. */
@Index
@Column(nullable = false)
DateTime eventTime;
/** Human readable message that will be returned with this poll message. */
/** Human-readable message that will be returned with this poll message. */
@Column(name = "message")
String msg;
// TODO(b/456803336): Replace these fields with {Domain,Contact,Host}HistoryId objects.
String domainRepoId;
@Ignore String domainRepoId;
String contactRepoId;
@Ignore String contactRepoId;
String hostRepoId;
@Ignore String hostRepoId;
Long domainHistoryRevisionId;
@Ignore Long domainHistoryRevisionId;
Long contactHistoryRevisionId;
@Ignore Long contactHistoryRevisionId;
@Ignore Long hostHistoryRevisionId;
Long hostHistoryRevisionId;
public Long getId() {
return id;
@@ -206,7 +179,7 @@ public abstract class PollMessage extends ImmutableObject
/**
* Returns the contact repo id.
*
* <p>This may only be used on a ContactResource poll event.
* <p>This may only be used on a {@link Contact} poll event.
*/
public String getContactRepoId() {
checkArgument(getType() == Type.CONTACT);
@@ -228,32 +201,22 @@ public abstract class PollMessage extends ImmutableObject
* the resource.
*/
public String getResourceName() {
return domainRepoId != null
? domainRepoId
: (contactRepoId != null ? contactRepoId : hostRepoId);
return domainRepoId != null ? domainRepoId : contactRepoId != null ? contactRepoId : hostRepoId;
}
/** Gets the underlying history revision id, regardless of the type of the resource. */
public Long getHistoryRevisionId() {
return domainHistoryRevisionId != null
? domainHistoryRevisionId
: (contactHistoryRevisionId != null ? contactHistoryRevisionId : hostHistoryRevisionId);
: contactHistoryRevisionId != null ? contactHistoryRevisionId : hostHistoryRevisionId;
}
public Type getType() {
return domainRepoId != null ? Type.DOMAIN : (contactRepoId != null ? Type.CONTACT : Type.HOST);
return domainRepoId != null ? Type.DOMAIN : contactRepoId != null ? Type.CONTACT : Type.HOST;
}
public abstract ImmutableList<ResponseData> getResponseData();
@Override
public abstract VKey<? extends PollMessage> createVKey();
/** Static VKey factory method for use by VKeyTranslatorFactory. */
public static VKey<PollMessage> createVKey(Key<PollMessage> key) {
return VKey.create(PollMessage.class, key.getId(), key);
}
/** Override Buildable.asBuilder() to give this method stronger typing. */
@Override
public abstract Builder<?, ?> asBuilder();
@@ -318,9 +281,11 @@ public abstract class PollMessage extends ImmutableObject
// Set the appropriate field based on the history entry type.
if (history instanceof DomainHistory) {
return setDomainHistoryId(((DomainHistory) history).getDomainHistoryId());
} else if (history instanceof ContactHistory) {
}
if (history instanceof ContactHistory) {
return setContactHistoryId(((ContactHistory) history).getContactHistoryId());
} else if (history instanceof HostHistory) {
}
if (history instanceof HostHistory) {
return setHostHistoryId(((HostHistory) history).getHostHistoryId());
}
return thisCastToDerived();
@@ -330,7 +295,7 @@ public abstract class PollMessage extends ImmutableObject
* Given an array containing pairs of objects, verifies that both members of exactly one of the
* pairs is non-null.
*/
private boolean exactlyOnePairNonNull(Object... pairs) {
private static boolean exactlyOnePairNonNull(Object... pairs) {
int count = 0;
checkArgument(pairs.length % 2 == 0, "Odd number of arguments provided.");
for (int i = 0; i < pairs.length; i += 2) {
@@ -360,9 +325,9 @@ public abstract class PollMessage extends ImmutableObject
instance.contactHistoryRevisionId,
instance.hostRepoId,
instance.hostHistoryRevisionId),
"the repo id and history revision id must be defined for exactly one of domain, "
+ "contact or host: "
+ instance);
"The repo id and history revision id must be defined for exactly one of domain, "
+ "contact or host: %s",
instance);
return super.build();
}
}
@@ -372,13 +337,11 @@ public abstract class PollMessage extends ImmutableObject
*
* <p>One-time poll messages are deleted from Datastore once they have been delivered and ACKed.
*/
@EntitySubclass(index = false)
@javax.persistence.Entity
@Entity
@DiscriminatorValue("ONE_TIME")
@WithLongVKey(compositeKey = true)
public static class OneTime extends PollMessage {
@Ignore
@Embedded
@AttributeOverrides({
@AttributeOverride(
@@ -399,7 +362,6 @@ public abstract class PollMessage extends ImmutableObject
})
PendingActionNotificationResponse pendingActionNotificationResponse;
@Ignore
@Embedded
@AttributeOverrides({
@AttributeOverride(
@@ -420,35 +382,21 @@ public abstract class PollMessage extends ImmutableObject
})
TransferResponse transferResponse;
@Ignore
@Column(name = "transfer_response_domain_name")
String fullyQualifiedDomainName;
@Ignore
@Column(name = "transfer_response_domain_expiration_time")
DateTime extendedRegistrationExpirationTime;
@Ignore
@Column(name = "transfer_response_contact_id")
String contactId;
@Ignore
@Column(name = "transfer_response_host_id")
String hostId;
@Override
public VKey<OneTime> createVKey() {
return VKey.create(OneTime.class, getId(), Key.create(this));
}
/** Converts an unspecialized VKey&lt;PollMessage&gt; to a VKey of the derived class. */
public static @Nullable VKey<OneTime> convertVKey(@Nullable VKey<? extends PollMessage> key) {
if (key == null) {
return null;
}
Key<OneTime> ofyKey =
Key.create(key.getOfyKey().getParent(), OneTime.class, key.getOfyKey().getId());
return VKey.create(OneTime.class, key.getSqlKey(), ofyKey);
return VKey.createSql(OneTime.class, getId());
}
@Override
@@ -525,8 +473,7 @@ public abstract class PollMessage extends ImmutableObject
/** A builder for {@link OneTime} since it is immutable. */
public static class Builder extends PollMessage.Builder<OneTime, Builder> {
public Builder() {
}
public Builder() {}
private Builder(OneTime instance) {
super(instance);
@@ -587,14 +534,13 @@ public abstract class PollMessage extends ImmutableObject
}
/**
* An auto-renew poll message which recurs annually.
* An autorenew poll message which recurs annually.
*
* <p>Auto-renew poll messages are not deleted until the registration of their parent domain has
* been canceled, because there will always be a speculative renewal for next year until that
* happens.
*/
@EntitySubclass(index = false)
@javax.persistence.Entity
@Entity
@DiscriminatorValue("AUTORENEW")
@WithLongVKey(compositeKey = true)
public static class Autorenew extends PollMessage {
@@ -604,7 +550,6 @@ public abstract class PollMessage extends ImmutableObject
String targetId;
/** The autorenew recurs annually between {@link #eventTime} and this time. */
@Index
DateTime autorenewEndTime;
public String getTargetId() {
@@ -617,25 +562,14 @@ public abstract class PollMessage extends ImmutableObject
@Override
public VKey<Autorenew> createVKey() {
return VKey.create(Autorenew.class, getId(), Key.create(this));
}
/** Converts an unspecialized VKey&lt;PollMessage&gt; to a VKey of the derived class. */
public static @Nullable VKey<Autorenew> convertVKey(VKey<? extends PollMessage> key) {
if (key == null) {
return null;
}
Key<Autorenew> ofyKey =
Key.create(key.getOfyKey().getParent(), Autorenew.class, key.getOfyKey().getId());
return VKey.create(Autorenew.class, key.getSqlKey(), ofyKey);
return VKey.createSql(Autorenew.class, getId());
}
@Override
public ImmutableList<ResponseData> getResponseData() {
// Note that the event time is when the auto-renew occured, so the expiration time in the
// Note that the event time is when the autorenew occurred, so the expiration time in the
// response should be 1 year past that, since it denotes the new expiration time.
return ImmutableList.of(
DomainRenewData.create(getTargetId(), getEventTime().plusYears(1)));
return ImmutableList.of(DomainRenewData.create(getTargetId(), getEventTime().plusYears(1)));
}
@Override

View File

@@ -14,48 +14,38 @@
package google.registry.model.poll;
import com.google.common.base.Splitter;
import com.googlecode.objectify.Key;
import google.registry.model.reporting.HistoryEntry;
import google.registry.persistence.VKey;
import java.util.List;
/**
* A converter between external key strings for {@link PollMessage}s (i.e. what registrars use to
* identify and ACK them) and Datastore keys to the resource.
* identify and ACK them) and {@link VKey}s to the resource.
*
* <p>The format of the key string is A-B-C-D-E-F as follows:
* <p>The format of the key string is A-B as follows:
*
* <pre>
* A = EppResource.typeId (decimal)
* B = EppResource.repoId prefix (STRING)
* C = EppResource.repoId suffix (STRING)
* D = HistoryEntry.id (decimal)
* E = PollMessage.id (decimal)
* F = PollMessage.eventTime (decimal, year only)
* A = PollMessage.id (decimal)
* B = PollMessage.eventTime (decimal, year only)
* </pre>
*
* <p>A typical poll message ID might thus look like: 1-FE0F22-TLD-10071463070-10072612074-2018
* <p>A typical poll message ID might thus look like: 10072612074-2018
*/
public class PollMessageExternalKeyConverter {
public final class PollMessageExternalKeyConverter {
/** An exception thrown when an external key cannot be parsed. */
public static class PollMessageExternalKeyParseException extends RuntimeException {}
public static class PollMessageExternalKeyParseException extends RuntimeException {
private static final long serialVersionUID = 9026397649048831777L;
}
/** Returns an external poll message ID for the given poll message. */
public static String makePollMessageExternalId(PollMessage pollMessage) {
return String.format(
"%d-%s-%d-%d-%d",
pollMessage.getType().getId(),
pollMessage.getResourceName(),
pollMessage.getHistoryRevisionId(),
pollMessage.getId(),
pollMessage.getEventTime().getYear());
return String.format("%d-%d", pollMessage.getId(), pollMessage.getEventTime().getYear());
}
/**
* Returns an Objectify Key to a PollMessage corresponding with the external ID.
* Returns a {@link VKey} to a {@link PollMessage} corresponding with the external ID.
*
* <p>Note that the year field that is included at the end of the poll message isn't actually used
* for anything; it exists solely to create unique externally visible IDs for autorenews. We thus
@@ -66,26 +56,13 @@ public class PollMessageExternalKeyConverter {
*/
public static VKey<PollMessage> parsePollMessageExternalId(String externalKey) {
List<String> idComponents = Splitter.on('-').splitToList(externalKey);
if (idComponents.size() != 6) {
if (idComponents.size() != 2) {
throw new PollMessageExternalKeyParseException();
}
try {
Class<?> resourceClazz =
PollMessage.Type.fromId(Long.parseLong(idComponents.get(0)))
.orElseThrow(() -> new PollMessageExternalKeyParseException())
.getResourceClass();
return VKey.from(
Key.create(
Key.create(
Key.create(
null,
resourceClazz,
String.format("%s-%s", idComponents.get(1), idComponents.get(2))),
HistoryEntry.class,
Long.parseLong(idComponents.get(3))),
PollMessage.class,
Long.parseLong(idComponents.get(4))));
// Note that idComponents.get(5) is entirely ignored; we never use the year field internally.
Long id = Long.parseLong(idComponents.get(0));
return VKey.createSql(PollMessage.class, id);
// Note that idComponents.get(1) is entirely ignored; we never use the year field internally.
} catch (NumberFormatException e) {
throw new PollMessageExternalKeyParseException();
}

View File

@@ -59,10 +59,8 @@ import com.googlecode.objectify.Key;
import com.googlecode.objectify.annotation.Entity;
import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.Ignore;
import com.googlecode.objectify.annotation.IgnoreSave;
import com.googlecode.objectify.annotation.Index;
import com.googlecode.objectify.annotation.Parent;
import com.googlecode.objectify.condition.IfNull;
import google.registry.model.Buildable;
import google.registry.model.CreateAutoTimestamp;
import google.registry.model.ImmutableObject;
@@ -315,7 +313,7 @@ public class Registrar extends ImmutableObject
* Localized {@link RegistrarAddress} for this registrar. Contents can be represented in
* unrestricted UTF-8.
*/
@IgnoreSave(IfNull.class)
@Ignore
@Embedded
@AttributeOverrides({
@AttributeOverride(
@@ -340,7 +338,7 @@ public class Registrar extends ImmutableObject
* Internationalized {@link RegistrarAddress} for this registrar. All contained values must be
* representable in the 7-bit US-ASCII character set.
*/
@IgnoreSave(IfNull.class)
@Ignore
@Embedded
@AttributeOverrides({
@AttributeOverride(name = "streetLine1", column = @Column(name = "i18n_address_street_line1")),

View File

@@ -18,7 +18,6 @@ import static com.google.common.base.Preconditions.checkNotNull;
import static google.registry.util.CollectionUtils.forceEmptyToNull;
import com.google.common.annotations.VisibleForTesting;
import com.googlecode.objectify.annotation.Embed;
import google.registry.model.eppcommon.Address;
import javax.persistence.Embeddable;
@@ -29,7 +28,6 @@ import javax.persistence.Embeddable;
* all defined in parent class {@link Address} so that it can share it with other similar address
* classes.
*/
@Embed
@Embeddable
public class RegistrarAddress extends Address {

View File

@@ -34,10 +34,10 @@ import google.registry.model.EppResource;
import google.registry.model.ImmutableObject;
import google.registry.model.UnsafeSerializable;
import google.registry.model.annotations.ReportedOn;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactBase;
import google.registry.model.contact.ContactHistory;
import google.registry.model.contact.ContactHistory.ContactHistoryId;
import google.registry.model.contact.ContactResource;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainBase;
import google.registry.model.domain.DomainHistory;
@@ -392,7 +392,7 @@ public class HistoryEntry extends ImmutableObject implements Buildable, UnsafeSe
} else if (parentKind.equals(getKind(Host.class))) {
resultEntity =
new HostHistory.Builder().copyFrom(this).setHostRepoId(parent.getName()).build();
} else if (parentKind.equals(getKind(ContactResource.class))) {
} else if (parentKind.equals(getKind(Contact.class))) {
resultEntity =
new ContactHistory.Builder().copyFrom(this).setContactRepoId(parent.getName()).build();
} else {
@@ -418,7 +418,7 @@ public class HistoryEntry extends ImmutableObject implements Buildable, UnsafeSe
HostHistory.class,
new HostHistoryId(repoId, id),
Key.create(parent, HostHistory.class, id));
} else if (parentKind.equals(getKind(ContactResource.class))) {
} else if (parentKind.equals(getKind(Contact.class))) {
return VKey.create(
ContactHistory.class,
new ContactHistoryId(repoId, id),

View File

@@ -25,8 +25,8 @@ 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.contact.ContactResource;
import google.registry.model.domain.Domain;
import google.registry.model.domain.DomainHistory;
import google.registry.model.host.Host;
@@ -51,7 +51,7 @@ public class HistoryEntryDao {
public static ImmutableMap<Class<? extends EppResource>, Class<? extends HistoryEntry>>
RESOURCE_TYPES_TO_HISTORY_TYPES =
ImmutableMap.of(
ContactResource.class,
Contact.class,
ContactHistory.class,
Domain.class,
DomainHistory.class,

View File

@@ -40,7 +40,7 @@ public class EppHistoryVKeyTranslatorFactory
// one dedicated VKey class, e.g. DomainHistoryVKey, for each such kind of entity, and we need
// a way to map the raw Datastore key to its VKey class. So, we use the kind path as the key of
// the map, and the kind path is created by concatenating all the kind strings in a raw Datastore
// key, e.g. the map key for ContactPollMessageVKey is "ContactResource/HistoryEntry/PollMessage".
// key, e.g. the map key for ContactPollMessageVKey is "Contact/HistoryEntry/PollMessage".
@VisibleForTesting
static final ImmutableMap<String, Class<? extends EppHistoryVKey>> kindPathToVKeyClass =
ImmutableSet.of(DomainHistoryVKey.class).stream()

View File

@@ -0,0 +1,50 @@
// Copyright 2022 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.persistence.converter;
import google.registry.model.console.RegistrarRole;
import java.util.Map;
import javax.persistence.Converter;
/** JPA converter for storing / retrieving {@code Map<String, RegistrarRole>} objects. */
@Converter(autoApply = true)
public class RegistrarToRoleConverter
extends StringMapConverterBase<String, RegistrarRole, Map<String, RegistrarRole>> {
@Override
protected String convertKeyToString(String key) {
return key;
}
@Override
protected String convertValueToString(RegistrarRole value) {
return value.toString();
}
@Override
protected String convertStringToKey(String string) {
return string;
}
@Override
protected RegistrarRole convertStringToValue(String string) {
return RegistrarRole.valueOf(string);
}
@Override
protected Map<String, RegistrarRole> convertMapToDerivedType(Map<String, RegistrarRole> map) {
return map;
}
}

View File

@@ -15,7 +15,7 @@
package google.registry.rdap;
import static google.registry.flows.domain.DomainFlowUtils.validateDomainName;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.model.EppResourceUtils.loadByForeignKeyCached;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.HEAD;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
@@ -60,7 +60,7 @@ public class RdapDomainAction extends RdapActionBase {
}
// The query string is not used; the RDAP syntax is /rdap/domain/mydomain.com.
Optional<Domain> domain =
loadByForeignKey(
loadByForeignKeyCached(
Domain.class,
pathSearchString,
shouldIncludeDeleted() ? START_OF_TIME : rdapJsonFormatter.getRequestTime());

View File

@@ -15,7 +15,7 @@
package google.registry.rdap;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.model.EppResourceUtils.loadByForeignKeyCached;
import static google.registry.model.index.ForeignKeyIndex.loadAndGetKey;
import static google.registry.model.ofy.ObjectifyService.auditedOfy;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaJpaTm;
@@ -188,7 +188,8 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
private DomainSearchResponse searchByDomainNameWithoutWildcard(
final RdapSearchPattern partialStringQuery) {
Optional<Domain> domain =
loadByForeignKey(Domain.class, partialStringQuery.getInitialString(), getRequestTime());
loadByForeignKeyCached(
Domain.class, partialStringQuery.getInitialString(), getRequestTime());
return makeSearchResults(
shouldBeVisible(domain) ? ImmutableList.of(domain.get()) : ImmutableList.of());
}
@@ -389,7 +390,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
Optional<String> desiredRegistrar = getDesiredRegistrar();
if (desiredRegistrar.isPresent()) {
Optional<Host> host =
loadByForeignKey(
loadByForeignKeyCached(
Host.class,
partialStringQuery.getInitialString(),
shouldIncludeDeleted() ? START_OF_TIME : getRequestTime());
@@ -414,7 +415,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
// through the subordinate hosts. This is more efficient, and lets us permit wildcard searches
// with no initial string.
Domain domain =
loadByForeignKey(
loadByForeignKeyCached(
Domain.class,
partialStringQuery.getSuffix(),
shouldIncludeDeleted() ? START_OF_TIME : getRequestTime())
@@ -431,7 +432,7 @@ public class RdapDomainSearchAction extends RdapSearchActionBase {
if (partialStringQuery.matches(fqhn)) {
if (desiredRegistrar.isPresent()) {
Optional<Host> host =
loadByForeignKey(
loadByForeignKeyCached(
Host.class, fqhn, shouldIncludeDeleted() ? START_OF_TIME : getRequestTime());
if (host.isPresent()
&& desiredRegistrar

View File

@@ -14,7 +14,7 @@
package google.registry.rdap;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaJpaTm;
import static google.registry.rdap.RdapUtils.getRegistrarByIanaIdentifier;
import static google.registry.rdap.RdapUtils.getRegistrarByName;
import static google.registry.request.Action.Method.GET;
@@ -23,7 +23,7 @@ import static google.registry.request.Action.Method.HEAD;
import com.google.common.collect.ImmutableSet;
import com.google.common.primitives.Longs;
import com.google.re2j.Pattern;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.Contact;
import google.registry.model.registrar.Registrar;
import google.registry.persistence.VKey;
import google.registry.rdap.RdapJsonFormatter.OutputDataType;
@@ -69,14 +69,14 @@ public class RdapEntityAction extends RdapActionBase {
// RDAP Technical Implementation Guide 2.3.1 - MUST support contact entity lookup using the
// handle
if (ROID_PATTERN.matcher(pathSearchString).matches()) {
VKey<ContactResource> contactVKey = VKey.create(ContactResource.class, pathSearchString);
Optional<ContactResource> contactResource =
tm().transact(() -> tm().loadByKeyIfPresent(contactVKey));
VKey<Contact> contactVKey = VKey.create(Contact.class, pathSearchString);
Optional<Contact> contact =
replicaJpaTm().transact(() -> replicaJpaTm().loadByKeyIfPresent(contactVKey));
// As per Andy Newton on the regext mailing list, contacts by themselves have no role, since
// they are global, and might have different roles for different domains.
if (contactResource.isPresent() && isAuthorized(contactResource.get())) {
if (contact.isPresent() && isAuthorized(contact.get())) {
return rdapJsonFormatter.createRdapContactEntity(
contactResource.get(), ImmutableSet.of(), OutputDataType.FULL);
contact.get(), ImmutableSet.of(), OutputDataType.FULL);
}
}

View File

@@ -28,7 +28,7 @@ import com.google.common.collect.Streams;
import com.google.common.primitives.Booleans;
import com.google.common.primitives.Longs;
import com.googlecode.objectify.cmd.Query;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.Contact;
import google.registry.model.registrar.Registrar;
import google.registry.persistence.VKey;
import google.registry.persistence.transaction.CriteriaQueryBuilder;
@@ -253,7 +253,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
// see any names anyway. Also, if a registrar cursor is present, we have already moved past the
// contacts, and don't need to fetch them this time. We can skip contacts if subtype is
// REGISTRARS.
RdapResultSet<ContactResource> resultSet;
RdapResultSet<Contact> resultSet;
if (subtype == Subtype.REGISTRARS) {
resultSet = RdapResultSet.create(ImmutableList.of());
} else {
@@ -262,9 +262,9 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
resultSet = RdapResultSet.create(ImmutableList.of());
} else {
if (tm().isOfy()) {
Query<ContactResource> query =
Query<Contact> query =
queryItems(
ContactResource.class,
Contact.class,
"searchName",
partialStringQuery,
cursorQueryString, // if we get here and there's a cursor, it must be a contact
@@ -279,9 +279,9 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
replicaJpaTm()
.transact(
() -> {
CriteriaQueryBuilder<ContactResource> builder =
CriteriaQueryBuilder<Contact> builder =
queryItemsSql(
ContactResource.class,
Contact.class,
"searchName",
partialStringQuery,
cursorQueryString,
@@ -319,19 +319,20 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
}
// Handle queries without a wildcard (and not including deleted) -- load by ID.
if (!partialStringQuery.getHasWildcard() && !shouldIncludeDeleted()) {
ImmutableList<ContactResource> contactResourceList;
ImmutableList<Contact> contactList;
if (subtype == Subtype.REGISTRARS) {
contactResourceList = ImmutableList.of();
contactList = ImmutableList.of();
} else {
Optional<ContactResource> contactResource =
tm().transact(
Optional<Contact> contact =
replicaJpaTm()
.transact(
() ->
tm().loadByKeyIfPresent(
VKey.create(
ContactResource.class, partialStringQuery.getInitialString())));
contactResourceList =
(contactResource.isPresent() && shouldBeVisible(contactResource.get()))
? ImmutableList.of(contactResource.get())
replicaJpaTm()
.loadByKeyIfPresent(
VKey.create(Contact.class, partialStringQuery.getInitialString())));
contactList =
(contact.isPresent() && shouldBeVisible(contact.get()))
? ImmutableList.of(contact.get())
: ImmutableList.of();
}
ImmutableList<Registrar> registrarList;
@@ -341,9 +342,9 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
registrarList = getMatchingRegistrars(partialStringQuery.getInitialString());
}
return makeSearchResults(
contactResourceList,
contactList,
IncompletenessWarningType.COMPLETE,
contactResourceList.size(),
contactList.size(),
registrarList,
QueryType.HANDLE);
// Handle queries with a wildcard (or including deleted), but no suffix. Because the handle
@@ -381,7 +382,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
// get excluded due to permissioning. Any cursor present must be a contact cursor, because we
// would never return a registrar for this search.
int querySizeLimit = getStandardQuerySizeLimit();
RdapResultSet<ContactResource> contactResultSet;
RdapResultSet<Contact> contactResultSet;
if (subtype == Subtype.REGISTRARS) {
contactResultSet = RdapResultSet.create(ImmutableList.of());
} else {
@@ -389,7 +390,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
contactResultSet =
getMatchingResources(
queryItemsByKey(
ContactResource.class,
Contact.class,
partialStringQuery,
cursorQueryString,
getDeletedItemHandling(),
@@ -403,7 +404,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
() ->
getMatchingResourcesSql(
queryItemsByKeySql(
ContactResource.class,
Contact.class,
partialStringQuery,
cursorQueryString,
getDeletedItemHandling()),
@@ -434,7 +435,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
* properties of the {@link RdapResultSet} structure and passes them as separate arguments.
*/
private EntitySearchResponse makeSearchResults(
RdapResultSet<ContactResource> resultSet, List<Registrar> registrars, QueryType queryType) {
RdapResultSet<Contact> resultSet, List<Registrar> registrars, QueryType queryType) {
return makeSearchResults(
resultSet.resources(),
resultSet.incompletenessWarningType(),
@@ -459,7 +460,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
* @return an {@link RdapSearchResults} object
*/
private EntitySearchResponse makeSearchResults(
List<ContactResource> contacts,
List<Contact> contacts,
IncompletenessWarningType incompletenessWarningType,
int numContactsRetrieved,
List<Registrar> registrars,
@@ -483,7 +484,7 @@ public class RdapEntitySearchAction extends RdapSearchActionBase {
EntitySearchResponse.builder()
.setIncompletenessWarningType(incompletenessWarningType);
Optional<String> newCursor = Optional.empty();
for (ContactResource contact : Iterables.limit(contacts, rdapResultSetMaxSize)) {
for (Contact contact : Iterables.limit(contacts, rdapResultSetMaxSize)) {
// As per Andy Newton on the regext mailing list, contacts by themselves have no role, since
// they are global, and might have different roles for different domains.
builder

View File

@@ -20,7 +20,7 @@ import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.collect.ImmutableSet.toImmutableSet;
import static com.google.common.collect.ImmutableSetMultimap.toImmutableSetMultimap;
import static google.registry.model.EppResourceUtils.isLinked;
import static google.registry.persistence.transaction.TransactionManagerFactory.jpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaJpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.rdap.RdapIcannStandardInformation.CONTACT_REDACTED_VALUE;
import static google.registry.util.CollectionUtils.union;
@@ -39,9 +39,9 @@ import com.google.common.net.InetAddresses;
import com.google.gson.JsonArray;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.EppResource;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactAddress;
import google.registry.model.contact.ContactPhoneNumber;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.PostalInfo;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.DesignatedContact.Type;
@@ -357,10 +357,15 @@ public class RdapJsonFormatter {
// Kick off the database loads of the nameservers that we will need, so it can load
// asynchronously while we load and process the contacts.
ImmutableSet<Host> loadedHosts =
tm().transact(() -> ImmutableSet.copyOf(tm().loadByKeys(domain.getNameservers()).values()));
replicaJpaTm()
.transact(
() ->
ImmutableSet.copyOf(
replicaJpaTm().loadByKeys(domain.getNameservers()).values()));
// Load the registrant and other contacts and add them to the data.
ImmutableMap<VKey<? extends ContactResource>, ContactResource> loadedContacts =
tm().transact(() -> tm().loadByKeysIfPresent(domain.getReferencedContacts()));
ImmutableMap<VKey<? extends Contact>, Contact> loadedContacts =
replicaJpaTm()
.transact(() -> replicaJpaTm().loadByKeysIfPresent(domain.getReferencedContacts()));
// RDAP Response Profile 2.7.3, A domain MUST have the REGISTRANT, ADMIN, TECH roles and MAY
// have others. We also add the BILLING.
//
@@ -368,7 +373,7 @@ public class RdapJsonFormatter {
// fields we don't want to show (as opposed to not having contacts at all) because of GDPR etc.
//
// the GDPR redaction is handled in createRdapContactEntity
ImmutableSetMultimap<VKey<ContactResource>, Type> contactsToRoles =
ImmutableSetMultimap<VKey<Contact>, Type> contactsToRoles =
Streams.concat(
domain.getContacts().stream(),
Stream.of(DesignatedContact.create(Type.REGISTRANT, domain.getRegistrant())))
@@ -377,7 +382,7 @@ public class RdapJsonFormatter {
toImmutableSetMultimap(
DesignatedContact::getContactKey, DesignatedContact::getType));
for (VKey<ContactResource> contactKey : contactsToRoles.keySet()) {
for (VKey<Contact> contactKey : contactsToRoles.keySet()) {
Set<RdapEntity.Role> roles =
contactsToRoles.get(contactKey).stream()
.map(RdapJsonFormatter::convertContactTypeToRdapRole)
@@ -439,9 +444,11 @@ public class RdapJsonFormatter {
statuses.add(StatusValue.LINKED);
}
if (host.isSubordinate()
&& tm().transact(
&& replicaJpaTm()
.transact(
() ->
tm().loadByKey(host.getSuperordinateDomain())
replicaJpaTm()
.loadByKey(host.getSuperordinateDomain())
.cloneProjectedAtTime(getRequestTime())
.getStatusValues()
.contains(StatusValue.PENDING_TRANSFER))) {
@@ -488,16 +495,14 @@ public class RdapJsonFormatter {
}
/**
* Creates a JSON object for a {@link ContactResource} and associated contact type.
* Creates a JSON object for a {@link Contact} and associated contact type.
*
* @param contactResource the contact resource object from which the JSON object should be created
* @param contact the contact resource object from which the JSON object should be created
* @param roles the roles of this contact
* @param outputDataType whether to generate full or summary data
*/
RdapContactEntity createRdapContactEntity(
ContactResource contactResource,
Iterable<RdapEntity.Role> roles,
OutputDataType outputDataType) {
Contact contact, Iterable<RdapEntity.Role> roles, OutputDataType outputDataType) {
RdapContactEntity.Builder contactBuilder = RdapContactEntity.builder();
// RDAP Response Profile 2.7.1, 2.7.3 - we MUST have the contacts. 2.7.4 discusses censoring of
@@ -505,12 +510,12 @@ public class RdapJsonFormatter {
//
// 2.8 allows for unredacted output for authorized people.
boolean isAuthorized =
rdapAuthorization.isAuthorizedForRegistrar(contactResource.getCurrentSponsorRegistrarId());
rdapAuthorization.isAuthorizedForRegistrar(contact.getCurrentSponsorRegistrarId());
// ROID needs to be redacted if we aren't authorized, so we can't have a self-link for
// unauthorized users
if (isAuthorized) {
contactBuilder.linksBuilder().add(makeSelfLink("entity", contactResource.getRepoId()));
contactBuilder.linksBuilder().add(makeSelfLink("entity", contact.getRepoId()));
}
// Only show the "summary data remark" if the user is authorized to see this data - because
@@ -537,10 +542,10 @@ public class RdapJsonFormatter {
.remarksBuilder()
.add(RdapIcannStandardInformation.CONTACT_PERSONAL_DATA_HIDDEN_DATA_REMARK);
// to make sure we don't accidentally display data we shouldn't - we replace the
// contactResource with a safe resource. Then we can add any information we need (e.g. the
// contact with a safe resource. Then we can add any information we need (e.g. the
// Organization / state / country of the registrant), although we currently don't do that.
contactResource =
new ContactResource.Builder()
contact =
new Contact.Builder()
.setRepoId(CONTACT_REDACTED_VALUE)
.setVoiceNumber(
new ContactPhoneNumber.Builder().setPhoneNumber(CONTACT_REDACTED_VALUE).build())
@@ -565,7 +570,7 @@ public class RdapJsonFormatter {
// RDAP Response Profile 2.7.3 - we MUST provide a handle set with the ROID, subject to the
// redaction above.
contactBuilder.setHandle(contactResource.getRepoId());
contactBuilder.setHandle(contact.getRepoId());
// RDAP Response Profile doesn't mention status for contacts, so we only show it if we're both
// FULL and Authorized.
@@ -574,11 +579,11 @@ public class RdapJsonFormatter {
.statusBuilder()
.addAll(
makeStatusValueList(
isLinked(contactResource.createVKey(), getRequestTime())
? union(contactResource.getStatusValues(), StatusValue.LINKED)
: contactResource.getStatusValues(),
isLinked(contact.createVKey(), getRequestTime())
? union(contact.getStatusValues(), StatusValue.LINKED)
: contact.getStatusValues(),
false,
contactResource.getDeletionTime().isBefore(getRequestTime())));
contact.getDeletionTime().isBefore(getRequestTime())));
}
contactBuilder.rolesBuilder().addAll(roles);
@@ -589,9 +594,9 @@ public class RdapJsonFormatter {
// RDAP Response Profile 2.7.3 - we MUST have FN, ADR, TEL, EMAIL.
//
// Note that 2.7.5 also says the EMAIL must be omitted, so we'll omit it
PostalInfo postalInfo = contactResource.getInternationalizedPostalInfo();
PostalInfo postalInfo = contact.getInternationalizedPostalInfo();
if (postalInfo == null) {
postalInfo = contactResource.getLocalizedPostalInfo();
postalInfo = contact.getLocalizedPostalInfo();
}
if (postalInfo != null) {
if (postalInfo.getName() != null) {
@@ -602,11 +607,11 @@ public class RdapJsonFormatter {
}
addVCardAddressEntry(vcardBuilder, postalInfo.getAddress());
}
ContactPhoneNumber voicePhoneNumber = contactResource.getVoiceNumber();
ContactPhoneNumber voicePhoneNumber = contact.getVoiceNumber();
if (voicePhoneNumber != null) {
vcardBuilder.add(makePhoneEntry(PHONE_TYPE_VOICE, makePhoneString(voicePhoneNumber)));
}
ContactPhoneNumber faxPhoneNumber = contactResource.getFaxNumber();
ContactPhoneNumber faxPhoneNumber = contact.getFaxNumber();
if (faxPhoneNumber != null) {
vcardBuilder.add(makePhoneEntry(PHONE_TYPE_FAX, makePhoneString(faxPhoneNumber)));
}
@@ -634,7 +639,7 @@ public class RdapJsonFormatter {
// We also only add it for authorized users because millisecond times can fingerprint a user
// just as much as the handle can.
if (outputDataType == OutputDataType.FULL && isAuthorized) {
contactBuilder.eventsBuilder().addAll(makeOptionalEvents(contactResource));
contactBuilder.eventsBuilder().addAll(makeOptionalEvents(contact));
}
return contactBuilder.build();
}
@@ -899,7 +904,8 @@ public class RdapJsonFormatter {
.replace("%repoIdField%", repoIdFieldName)
.replace("%repoIdValue%", resourceVkey.getSqlKey().toString());
historyEntries =
jpaTm().transact(() -> jpaTm().getEntityManager().createQuery(jpql).getResultList());
replicaJpaTm()
.transact(() -> replicaJpaTm().getEntityManager().createQuery(jpql).getResultList());
}
for (HistoryEntry historyEntry : historyEntries) {
EventAction rdapEventAction =

View File

@@ -108,7 +108,7 @@ public final class RdapModule {
if (userAuthInfo.isUserAdmin()) {
return RdapAuthorization.ADMINISTRATOR_AUTHORIZATION;
}
ImmutableSet<String> clientIds = registrarAccessor.getAllClientIdWithRoles().keySet();
ImmutableSet<String> clientIds = registrarAccessor.getAllRegistrarIdsWithRoles().keySet();
if (clientIds.isEmpty()) {
logger.atWarning().log("Couldn't find registrar for User %s.", authResult.userIdForLogging());
return RdapAuthorization.PUBLIC_AUTHORIZATION;

View File

@@ -15,7 +15,7 @@
package google.registry.rdap;
import static google.registry.flows.host.HostFlowUtils.validateHostName;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.model.EppResourceUtils.loadByForeignKeyCached;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.HEAD;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
@@ -62,7 +62,7 @@ public class RdapNameserverAction extends RdapActionBase {
// If there are no undeleted nameservers with the given name, the foreign key should point to
// the most recently deleted one.
Optional<Host> host =
loadByForeignKey(
loadByForeignKeyCached(
Host.class,
pathSearchString,
shouldIncludeDeleted() ? START_OF_TIME : getRequestTime());

View File

@@ -14,7 +14,7 @@
package google.registry.rdap;
import static google.registry.model.EppResourceUtils.loadByForeignKey;
import static google.registry.model.EppResourceUtils.loadByForeignKeyCached;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaJpaTm;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.GET;
@@ -160,7 +160,7 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase {
.setIncompletenessWarningType(IncompletenessWarningType.COMPLETE);
Optional<Host> host =
loadByForeignKey(Host.class, partialStringQuery.getInitialString(), getRequestTime());
loadByForeignKeyCached(Host.class, partialStringQuery.getInitialString(), getRequestTime());
metricInformationBuilder.setNumHostsRetrieved(host.isPresent() ? 1 : 0);
@@ -176,7 +176,7 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase {
private NameserverSearchResponse searchByNameUsingSuperordinateDomain(
RdapSearchPattern partialStringQuery) {
Optional<Domain> domain =
loadByForeignKey(Domain.class, partialStringQuery.getSuffix(), getRequestTime());
loadByForeignKeyCached(Domain.class, partialStringQuery.getSuffix(), getRequestTime());
if (!domain.isPresent()) {
// Don't allow wildcards with suffixes which are not domains we manage. That would risk a
// table scan in many easily foreseeable cases. The user might ask for ns*.zombo.com,
@@ -194,7 +194,7 @@ public class RdapNameserverSearchAction extends RdapSearchActionBase {
// We can't just check that the host name starts with the initial query string, because
// then the query ns.exam*.example.com would match against nameserver ns.example.com.
if (partialStringQuery.matches(fqhn)) {
Optional<Host> host = loadByForeignKey(Host.class, fqhn, getRequestTime());
Optional<Host> host = loadByForeignKeyCached(Host.class, fqhn, getRequestTime());
if (shouldBeVisible(host)) {
hostList.add(host.get());
if (hostList.size() > rdapResultSetMaxSize) {

View File

@@ -16,9 +16,9 @@ package google.registry.rde;
import static google.registry.util.XmlEnumUtils.enumToXml;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactAddress;
import google.registry.model.contact.ContactPhoneNumber;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.Disclose;
import google.registry.model.contact.Disclose.PostalInfoChoice;
import google.registry.model.contact.PostalInfo;
@@ -39,16 +39,16 @@ import google.registry.xjc.rdecontact.XjcRdeContactTransferDataType;
import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
/** Utility class that turns {@link ContactResource} as {@link XjcRdeContactElement}. */
final class ContactResourceToXjcConverter {
/** Utility class that turns {@link Contact} as {@link XjcRdeContactElement}. */
final class ContactToXjcConverter {
/** Converts {@link ContactResource} to {@link XjcRdeContactElement}. */
static XjcRdeContactElement convert(ContactResource host) {
/** Converts {@link Contact} to {@link XjcRdeContactElement}. */
static XjcRdeContactElement convert(Contact host) {
return new XjcRdeContactElement(convertContact(host));
}
/** Converts {@link ContactResource} to {@link XjcRdeContact}. */
static XjcRdeContact convertContact(ContactResource model) {
/** Converts {@link Contact} to {@link XjcRdeContact}. */
static XjcRdeContact convertContact(Contact model) {
XjcRdeContact bean = new XjcRdeContact();
bean.setRoid(model.getRepoId());
for (StatusValue status : model.getStatusValues()) {
@@ -188,5 +188,5 @@ final class ContactResourceToXjcConverter {
return bean;
}
private ContactResourceToXjcConverter() {}
private ContactToXjcConverter() {}
}

View File

@@ -21,7 +21,7 @@ import com.google.common.base.Ascii;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.Contact;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.Domain;
import google.registry.model.domain.rgp.GracePeriodStatus;
@@ -168,11 +168,11 @@ final class DomainToXjcConverter {
// o An OPTIONAL <registrant> element that contain the identifier for
// the human or organizational social information object associated
// as the holder of the domain name object.
VKey<ContactResource> registrant = model.getRegistrant();
VKey<Contact> registrant = model.getRegistrant();
if (registrant == null) {
logger.atWarning().log("Domain %s has no registrant contact.", domainName);
} else {
ContactResource registrantContact = tm().transact(() -> tm().loadByKey(registrant));
Contact registrantContact = tm().transact(() -> tm().loadByKey(registrant));
checkState(
registrantContact != null,
"Registrant contact %s on domain %s does not exist",
@@ -304,7 +304,7 @@ final class DomainToXjcConverter {
"Contact key for type %s is null on domain %s",
model.getType(),
domainName);
ContactResource contact = tm().transact(() -> tm().loadByKey(model.getContactKey()));
Contact contact = tm().transact(() -> tm().loadByKey(model.getContactKey()));
checkState(
contact != null,
"Contact %s on domain %s does not exist",

View File

@@ -20,7 +20,7 @@ import static google.registry.persistence.transaction.TransactionManagerFactory.
import com.google.auto.value.AutoValue;
import com.google.common.collect.ImmutableMap;
import google.registry.model.EppResource;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.Contact;
import google.registry.model.domain.Domain;
import google.registry.model.host.Host;
import google.registry.model.rde.RdeMode;
@@ -66,8 +66,8 @@ public class RdeFragmenter {
result = Optional.of(marshaller.marshalDomain((Domain) resource, mode));
cache.put(WatermarkModePair.create(watermark, mode), result);
return result;
} else if (resource instanceof ContactResource) {
result = Optional.of(marshaller.marshalContact((ContactResource) resource));
} else if (resource instanceof Contact) {
result = Optional.of(marshaller.marshalContact((Contact) resource));
cache.put(WatermarkModePair.create(watermark, RdeMode.FULL), result);
cache.put(WatermarkModePair.create(watermark, RdeMode.THIN), result);
return result;

View File

@@ -20,7 +20,7 @@ import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.flogger.FluentLogger;
import com.googlecode.objectify.Key;
import google.registry.model.ImmutableObject;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.Contact;
import google.registry.model.domain.Domain;
import google.registry.model.host.Host;
import google.registry.model.rde.RdeMode;
@@ -117,10 +117,10 @@ public final class RdeMarshaller implements Serializable {
}
}
/** Turns {@link ContactResource} object into an XML fragment. */
public DepositFragment marshalContact(ContactResource contact) {
return marshalResource(RdeResourceType.CONTACT, contact,
ContactResourceToXjcConverter.convert(contact));
/** Turns {@link Contact} object into an XML fragment. */
public DepositFragment marshalContact(Contact contact) {
return marshalResource(
RdeResourceType.CONTACT, contact, ContactToXjcConverter.convert(contact));
}
/** Turns {@link Domain} object into an XML fragment. */

View File

@@ -45,7 +45,7 @@ import google.registry.gcs.GcsUtils;
import google.registry.keyring.api.KeyModule.Key;
import google.registry.model.common.Cursor;
import google.registry.model.common.Cursor.CursorType;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.Contact;
import google.registry.model.domain.Domain;
import google.registry.model.host.Host;
import google.registry.model.index.EppResourceIndex;
@@ -82,8 +82,8 @@ import org.joda.time.Duration;
* type and loads the embedded resource from it, which is then projected to watermark time to
* account for things like pending transfer.
*
* <p>Only {@link ContactResource}s and {@link Host}s that are referenced by an included {@link
* Domain} will be included in the corresponding pending deposit.
* <p>Only {@link Contact}s and {@link Host}s that are referenced by an included {@link Domain} will
* be included in the corresponding pending deposit.
*
* <p>{@link Registrar} entities, both active and inactive, are included in all deposits. They are
* not rewinded point-in-time.

View File

@@ -42,7 +42,7 @@ public abstract class AuthResult {
userAuthInfo ->
String.format(
"%s %s",
userAuthInfo.isUserAdmin() ? "admin" : "user", userAuthInfo.user().getEmail()))
userAuthInfo.isUserAdmin() ? "admin" : "user", userAuthInfo.getEmailAddress()))
.orElse("<logged-out user>");
}

View File

@@ -40,8 +40,8 @@ import javax.inject.Inject;
* <p>A user has OWNER role on a Registrar if there exists a {@link RegistrarPoc} with that user's
* gaeId and the registrar as a parent.
*
* <p>An "admin" has in addition OWNER role on {@code #registryAdminClientId} and to all non-{@code
* REAL} registrars (see {@link Registrar#getType}).
* <p>An "admin" has in addition OWNER role on {@code #registryAdminRegistrarId} and to all
* non-{@code REAL} registrars (see {@link Registrar#getType}).
*
* <p>An "admin" also has ADMIN role on ALL registrars.
*
@@ -76,7 +76,7 @@ public class AuthenticatedRegistrarAccessor {
private final boolean isAdmin;
/**
* Gives all roles a user has for a given clientId.
* Gives all roles a user has for a given registrar ID.
*
* <p>The order is significant, with "more specific to this user" coming first.
*
@@ -107,13 +107,13 @@ public class AuthenticatedRegistrarAccessor {
@Inject
public AuthenticatedRegistrarAccessor(
AuthResult authResult,
@Config("registryAdminClientId") String registryAdminClientId,
@Config("registryAdminClientId") String registryAdminRegistrarId,
@Config("gSuiteSupportGroupEmailAddress") Optional<String> gSuiteSupportGroupEmailAddress,
Lazy<GroupsConnection> lazyGroupsConnection) {
this.isAdmin = userIsAdmin(authResult, gSuiteSupportGroupEmailAddress, lazyGroupsConnection);
this.userIdForLogging = authResult.userIdForLogging();
this.roleMap = createRoleMap(authResult, this.isAdmin, registryAdminClientId);
this.roleMap = createRoleMap(authResult, this.isAdmin, registryAdminRegistrarId);
logger.atInfo().log("%s has the following roles: %s", userIdForLogging(), roleMap);
}
@@ -129,7 +129,7 @@ public class AuthenticatedRegistrarAccessor {
* Creates a "logged-in user" accessor with a given role map, used for tests.
*
* <p>The user will be allowed to create Registrars (and hence do OT&amp;E setup) iff they have
* the role of ADMIN for at least one clientId.
* the role of ADMIN for at least one registrar ID.
*
* <p>The user's "name" in logs and exception messages is "TestUserId".
*/
@@ -148,59 +148,62 @@ public class AuthenticatedRegistrarAccessor {
}
/**
* A map that gives all roles a user has for a given clientId.
* A map that gives all roles a user has for a given registrar ID.
*
* <p>Throws a {@link RegistrarAccessDeniedException} if the user is not logged in.
*
* <p>The result is ordered starting from "most specific to this user".
*
* <p>If you want to load the {@link Registrar} object from these (or any other) {@code clientId},
* in order to perform actions on behalf of a user, you must use {@link #getRegistrar} which makes
* sure the user has permissions.
* <p>If you want to load the {@link Registrar} object from these (or any other) {@code
* registrarId}, in order to perform actions on behalf of a user, you must use {@link
* #getRegistrar} which makes sure the user has permissions.
*
* <p>Note that this is an OPTIONAL step in the authentication - only used if we don't have any
* other clue as to the requested {@code clientId}. It is perfectly OK to get a {@code clientId}
* from any other source, as long as the registrar is then loaded using {@link #getRegistrar}.
* other clue as to the requested {@code registrarId}. It is perfectly OK to get a {@code
* registrarId} from any other source, as long as the registrar is then loaded using {@link
* #getRegistrar}.
*/
public ImmutableSetMultimap<String, Role> getAllClientIdWithRoles() {
public ImmutableSetMultimap<String, Role> getAllRegistrarIdsWithRoles() {
return roleMap;
}
/**
* Returns all the roles the current user has on the given registrar.
*
* <p>This is syntactic sugar for {@code getAllClientIdWithRoles().get(clientId)}.
* <p>This is syntactic sugar for {@code getAllRegistrarIdsWithRoles().get(registrarId)}.
*/
public ImmutableSet<Role> getRolesForRegistrar(String clientId) {
return getAllClientIdWithRoles().get(clientId);
public ImmutableSet<Role> getRolesForRegistrar(String registrarId) {
return getAllRegistrarIdsWithRoles().get(registrarId);
}
/**
* Checks if we have a given role for a given registrar.
*
* <p>This is syntactic sugar for {@code getAllClientIdWithRoles().containsEntry(clientId, role)}.
* <p>This is syntactic sugar for {@code getAllRegistrarIdsWithRoles().containsEntry(registrarId,
* role)}.
*/
public boolean hasRoleOnRegistrar(Role role, String clientId) {
return getAllClientIdWithRoles().containsEntry(clientId, role);
public boolean hasRoleOnRegistrar(Role role, String registrarId) {
return getAllRegistrarIdsWithRoles().containsEntry(registrarId, role);
}
/**
* "Guesses" which client ID the user wants from all those they have access to.
*
* <p>If no such ClientIds exist, throws a RegistrarAccessDeniedException.
* <p>If no such registrar IDs exist, throws a RegistrarAccessDeniedException.
*
* <p>This should be the ClientId "most likely wanted by the user".
* <p>This should be the registrar ID "most likely wanted by the user".
*
* <p>If you want to load the {@link Registrar} object from this (or any other) {@code clientId},
* in order to perform actions on behalf of a user, you must use {@link #getRegistrar} which makes
* sure the user has permissions.
* <p>If you want to load the {@link Registrar} object from this (or any other) {@code
* registrarId}, in order to perform actions on behalf of a user, you must use {@link
* #getRegistrar} which makes sure the user has permissions.
*
* <p>Note that this is an OPTIONAL step in the authentication - only used if we don't have any
* other clue as to the requested {@code clientId}. It is perfectly OK to get a {@code clientId}
* from any other source, as long as the registrar is then loaded using {@link #getRegistrar}.
* other clue as to the requested {@code registrarId}. It is perfectly OK to get a {@code
* registrarId} from any other source, as long as the registrar is then loaded using {@link
* #getRegistrar}.
*/
public String guessClientId() throws RegistrarAccessDeniedException {
return getAllClientIdWithRoles().keySet().stream()
public String guessRegistrarId() throws RegistrarAccessDeniedException {
return getAllRegistrarIdsWithRoles().keySet().stream()
.findFirst()
.orElseThrow(
() ->
@@ -227,7 +230,7 @@ public class AuthenticatedRegistrarAccessor {
if (!registrarId.equals(registrar.getRegistrarId())) {
logger.atSevere().log(
"registrarLoader.apply(clientId) returned a Registrar with a different clientId. "
"registrarLoader.apply(registrarId) returned a Registrar with a different registrarId. "
+ "Requested: %s, returned: %s.",
registrarId, registrar.getRegistrarId());
throw new RegistrarAccessDeniedException("Internal error - please check logs");
@@ -237,7 +240,7 @@ public class AuthenticatedRegistrarAccessor {
}
public void verifyAccess(String registrarId) throws RegistrarAccessDeniedException {
ImmutableSet<Role> roles = getAllClientIdWithRoles().get(registrarId);
ImmutableSet<Role> roles = getAllRegistrarIdsWithRoles().get(registrarId);
if (roles.isEmpty()) {
throw new RegistrarAccessDeniedException(
@@ -279,53 +282,55 @@ public class AuthenticatedRegistrarAccessor {
AuthResult authResult,
Optional<String> gSuiteSupportGroupEmailAddress,
Lazy<GroupsConnection> lazyGroupsConnection) {
if (!authResult.userAuthInfo().isPresent()) {
return false;
}
UserAuthInfo userAuthInfo = authResult.userAuthInfo().get();
User user = userAuthInfo.user();
// both GAE project admin and members of the gSuiteSupportGroupEmailAddress are considered
// admins for the RegistrarConsole.
return !bypassAdminCheck
&& (userAuthInfo.isUserAdmin()
|| checkIsSupport(
lazyGroupsConnection, user.getEmail(), gSuiteSupportGroupEmailAddress));
lazyGroupsConnection,
userAuthInfo.getEmailAddress(),
gSuiteSupportGroupEmailAddress));
}
/**
* Returns a map of registrar client IDs to roles for all registrars that the user has access to.
*/
/** Returns a map of registrar IDs to roles for all registrars that the user has access to. */
private static ImmutableSetMultimap<String, Role> createRoleMap(
AuthResult authResult,
boolean isAdmin,
String registryAdminClientId) {
AuthResult authResult, boolean isAdmin, String registryAdminRegistrarId) {
if (!authResult.userAuthInfo().isPresent()) {
return ImmutableSetMultimap.of();
}
UserAuthInfo userAuthInfo = authResult.userAuthInfo().get();
User user = userAuthInfo.user();
ImmutableSetMultimap.Builder<String, Role> builder = new ImmutableSetMultimap.Builder<>();
logger.atInfo().log("Checking registrar contacts for user ID %s.", user.getUserId());
UserAuthInfo userAuthInfo = authResult.userAuthInfo().get();
if (userAuthInfo.appEngineUser().isPresent()) {
User user = userAuthInfo.appEngineUser().get();
logger.atInfo().log("Checking registrar contacts for user ID %s.", user.getUserId());
// Find all registrars that have a registrar contact with this user's ID.
jpaTm()
.transact(
() ->
jpaTm()
.query(
"SELECT r FROM Registrar r INNER JOIN RegistrarPoc rp ON "
+ "r.clientIdentifier = rp.registrarId WHERE rp.gaeUserId = "
+ ":gaeUserId AND r.state != :state",
Registrar.class)
.setParameter("gaeUserId", user.getUserId())
.setParameter("state", State.DISABLED)
.getResultStream()
.forEach(registrar -> builder.put(registrar.getRegistrarId(), Role.OWNER)));
// Find all registrars that have a registrar contact with this user's ID.
jpaTm()
.transact(
() ->
jpaTm()
.query(
"SELECT r FROM Registrar r INNER JOIN RegistrarPoc rp ON "
+ "r.clientIdentifier = rp.registrarId WHERE rp.gaeUserId = "
+ ":gaeUserId AND r.state != :state",
Registrar.class)
.setParameter("gaeUserId", user.getUserId())
.setParameter("state", State.DISABLED)
.getResultStream()
.forEach(registrar -> builder.put(registrar.getRegistrarId(), Role.OWNER)));
} else {
userAuthInfo
.consoleUser()
.get()
.getUserRoles()
.getRegistrarRoles()
.forEach((k, v) -> builder.put(k, Role.OWNER));
}
// Admins have ADMIN access to all registrars, and also OWNER access to the registry registrar
// and all non-REAL or non-live registrars.
@@ -337,7 +342,7 @@ public class AuthenticatedRegistrarAccessor {
registrar -> {
if (registrar.getType() != Registrar.Type.REAL
|| !registrar.isLive()
|| registrar.getRegistrarId().equals(registryAdminClientId)) {
|| registrar.getRegistrarId().equals(registryAdminRegistrarId)) {
builder.put(registrar.getRegistrarId(), Role.OWNER);
}
builder.put(registrar.getRegistrarId(), Role.ADMIN);

View File

@@ -23,7 +23,7 @@ import java.util.Optional;
public abstract class UserAuthInfo {
/** User object from the AppEngine Users API. */
public abstract User user();
public abstract Optional<User> appEngineUser();
/**
* Whether the user is an admin.
@@ -34,16 +34,37 @@ public abstract class UserAuthInfo {
*/
public abstract boolean isUserAdmin();
public abstract Optional<google.registry.model.console.User> consoleUser();
/** Used by the OAuth authentication mechanism (only) to return information about the session. */
public abstract Optional<OAuthTokenInfo> oauthTokenInfo();
public String getEmailAddress() {
return appEngineUser()
.map(User::getEmail)
.orElseGet(() -> consoleUser().get().getEmailAddress());
}
public String getUsername() {
return appEngineUser()
.map(User::getNickname)
.orElseGet(() -> consoleUser().get().getEmailAddress());
}
public static UserAuthInfo create(
User user, boolean isUserAdmin) {
return new AutoValue_UserAuthInfo(user, isUserAdmin, Optional.empty());
return new AutoValue_UserAuthInfo(
Optional.of(user), isUserAdmin, Optional.empty(), Optional.empty());
}
public static UserAuthInfo create(
User user, boolean isUserAdmin, OAuthTokenInfo oauthTokenInfo) {
return new AutoValue_UserAuthInfo(user, isUserAdmin, Optional.of(oauthTokenInfo));
return new AutoValue_UserAuthInfo(
Optional.of(user), isUserAdmin, Optional.empty(), Optional.of(oauthTokenInfo));
}
public static UserAuthInfo create(google.registry.model.console.User user) {
return new AutoValue_UserAuthInfo(
Optional.empty(), user.getUserRoles().isAdmin(), Optional.of(user), Optional.empty());
}
}

View File

@@ -19,7 +19,7 @@ import static com.google.common.base.Preconditions.checkState;
import com.google.common.base.Ascii;
import com.google.common.base.Strings;
import google.registry.model.EppResource;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.Contact;
import google.registry.model.domain.Domain;
import google.registry.model.host.Host;
import google.registry.model.index.ForeignKeyIndex;
@@ -31,7 +31,7 @@ class CommandUtilities {
/** A useful parameter enum for commands that operate on {@link EppResource} objects. */
public enum ResourceType {
CONTACT(ContactResource.class),
CONTACT(Contact.class),
HOST(Host.class),
DOMAIN(Domain.class);

View File

@@ -46,6 +46,11 @@ final class CreateDomainCommand extends CreateOrUpdateDomainCommand
description = "Force the creation of premium domains.")
private boolean forcePremiums;
@Parameter(
names = "--allocation_token",
description = "Allocation token to use when creating the domain(s)")
private String allocationToken;
@Inject
@Named("base64StringGenerator")
StringGenerator passwordGenerator;
@@ -92,7 +97,8 @@ final class CreateDomainCommand extends CreateOrUpdateDomainCommand
"currency", currency,
"price", cost,
"dsRecords", DsRecord.convertToSoy(dsRecords),
"reason", reason);
"reason", reason,
"allocationToken", allocationToken);
if (requestedByRegistrar != null) {
soyMapData.put("requestedByRegistrar", requestedByRegistrar.toString());
}

View File

@@ -18,7 +18,7 @@ import static google.registry.model.EppResourceUtils.loadByForeignKey;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.Contact;
import java.util.List;
/** Command to show one or more contacts. */
@@ -34,7 +34,7 @@ final class GetContactCommand extends GetEppResourceCommand {
public void runAndPrint() {
for (String contactId : mainParameters) {
printResource(
"Contact", contactId, loadByForeignKey(ContactResource.class, contactId, readTimestamp));
"Contact", contactId, loadByForeignKey(Contact.class, contactId, readTimestamp));
}
}
}

View File

@@ -54,7 +54,7 @@ import org.joda.time.DateTime;
* <p>This removes years off a domain's registration period. Note that the expiration time cannot be
* set to prior than the present. Reversal of the charges for these years (if desired) must happen
* out of band, as they may already have been billed out and thus cannot and won't be reversed in
* Datastore.
* the database.
*/
@Parameters(separators = " =", commandDescription = "Unrenew a domain.")
@NonFinalForTesting
@@ -217,7 +217,8 @@ class UnrenewDomainCommand extends ConfirmingCommand implements CommandWithRemot
.build();
// End the old autorenew billing event and poll message now.
Recurring existingRecurring = tm().loadByKey(domain.getAutorenewBillingEvent());
updateAutorenewRecurrenceEndTime(domain, existingRecurring, now);
updateAutorenewRecurrenceEndTime(
domain, existingRecurring, now, domainHistory.getDomainHistoryId());
Domain newDomain =
domain
.asBuilder()
@@ -225,9 +226,7 @@ class UnrenewDomainCommand extends ConfirmingCommand implements CommandWithRemot
.setLastEppUpdateTime(now)
.setLastEppUpdateRegistrarId(domain.getCurrentSponsorRegistrarId())
.setAutorenewBillingEvent(newAutorenewEvent.createVKey())
.setAutorenewPollMessage(
newAutorenewPollMessage.createVKey(),
newAutorenewPollMessage.getHistoryRevisionId())
.setAutorenewPollMessage(newAutorenewPollMessage.createVKey())
.build();
// In order to do it'll need to write out a new HistoryEntry (likely of type SYNTHETIC), a new
// autorenew billing event and poll message, and a new one time poll message at the present time

View File

@@ -15,13 +15,13 @@
package google.registry.tools.params;
import google.registry.model.EppResource;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.Contact;
import google.registry.model.domain.Domain;
import google.registry.model.host.Host;
/** Enum to make it easy for a command to accept a flag that specifies an EppResource subclass. */
public enum EppResourceTypeParameter {
CONTACT(ContactResource.class),
CONTACT(Contact.class),
DOMAIN(Domain.class),
HOST(Host.class);

View File

@@ -109,13 +109,13 @@ public final class ConsoleUiAction extends HtmlAction {
.render());
return;
}
ImmutableSetMultimap<String, Role> roleMap = registrarAccessor.getAllClientIdWithRoles();
ImmutableSetMultimap<String, Role> roleMap = registrarAccessor.getAllRegistrarIdsWithRoles();
soyMapData.put("allClientIds", roleMap.keySet());
soyMapData.put("environment", RegistryEnvironment.get().toString());
// We set the initial value to the value that will show if guessClientId throws.
String clientId = "<null>";
try {
clientId = paramClientId.orElse(registrarAccessor.guessClientId());
clientId = paramClientId.orElse(registrarAccessor.guessRegistrarId());
soyMapData.put("clientId", clientId);
soyMapData.put("isOwner", roleMap.containsEntry(clientId, OWNER));
soyMapData.put("isAdmin", roleMap.containsEntry(clientId, ADMIN));

View File

@@ -18,7 +18,6 @@ import static com.google.common.net.HttpHeaders.LOCATION;
import static com.google.common.net.HttpHeaders.X_FRAME_OPTIONS;
import static javax.servlet.http.HttpServletResponse.SC_MOVED_TEMPORARILY;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
@@ -27,6 +26,7 @@ import google.registry.request.Action;
import google.registry.request.RequestMethod;
import google.registry.request.Response;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.UserAuthInfo;
import google.registry.security.XsrfTokenManager;
import java.util.HashMap;
import java.util.Map;
@@ -86,16 +86,15 @@ public abstract class HtmlAction implements Runnable {
}
response.setContentType(MediaType.HTML_UTF_8);
User user = authResult.userAuthInfo().get().user();
UserAuthInfo authInfo = authResult.userAuthInfo().get();
// Using HashMap to allow null values
HashMap<String, Object> data = new HashMap<>();
data.put("logoFilename", logoFilename);
data.put("productName", productName);
data.put("username", user.getNickname());
data.put("username", authInfo.getUsername());
data.put("logoutUrl", userService.createLogoutURL(getPath()));
data.put("analyticsConfig", analyticsConfig);
data.put("xsrfToken", xsrfTokenManager.generateToken(user.getEmail()));
data.put("xsrfToken", xsrfTokenManager.generateToken(authInfo.getEmailAddress()));
logger.atInfo().log(
"User %s is accessing %s with method %s.",

View File

@@ -29,6 +29,7 @@ import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import com.google.gson.Gson;
import google.registry.model.console.ConsolePermission;
import google.registry.model.domain.RegistryLock;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
@@ -42,6 +43,7 @@ import google.registry.request.auth.Auth;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.AuthenticatedRegistrarAccessor;
import google.registry.request.auth.AuthenticatedRegistrarAccessor.RegistrarAccessDeniedException;
import google.registry.request.auth.UserAuthInfo;
import google.registry.security.JsonResponseHelper;
import java.util.Objects;
import java.util.Optional;
@@ -153,24 +155,35 @@ public final class RegistryLockGetAction implements JsonGetAction {
boolean isAdmin = registrarAccessor.isAdmin();
Registrar registrar = getRegistrarAndVerifyLockAccess(registrarAccessor, registrarId, isAdmin);
User user = authResult.userAuthInfo().get().user();
Optional<RegistrarPoc> contactOptional = getContactMatchingLogin(user, registrar);
boolean isRegistryLockAllowed =
isAdmin || contactOptional.map(RegistrarPoc::isRegistryLockAllowed).orElse(false);
UserAuthInfo userAuthInfo = authResult.userAuthInfo().get();
// Split logic depending on whether we are using the old auth system or the new one
boolean isRegistryLockAllowed;
String relevantEmail;
if (userAuthInfo.appEngineUser().isPresent()) {
User user = userAuthInfo.appEngineUser().get();
Optional<RegistrarPoc> contactOptional = getContactMatchingLogin(user, registrar);
isRegistryLockAllowed =
isAdmin || contactOptional.map(RegistrarPoc::isRegistryLockAllowed).orElse(false);
relevantEmail =
isAdmin
? user.getEmail()
// if the contact isn't present, we shouldn't display the email anyway
: contactOptional.flatMap(RegistrarPoc::getRegistryLockEmailAddress).orElse("");
} else {
google.registry.model.console.User user = userAuthInfo.consoleUser().get();
isRegistryLockAllowed =
user.getUserRoles().hasPermission(registrarId, ConsolePermission.REGISTRY_LOCK);
relevantEmail = user.getEmailAddress();
}
// Use the contact's registry lock email if it's present, else use the login email (for admins)
String relevantEmail =
isAdmin
? user.getEmail()
// if the contact isn't present, we shouldn't display the email anyway so empty is fine
: contactOptional.flatMap(RegistrarPoc::getRegistryLockEmailAddress).orElse("");
return ImmutableMap.of(
LOCK_ENABLED_FOR_CONTACT_PARAM,
isRegistryLockAllowed,
EMAIL_PARAM,
relevantEmail,
PARAM_CLIENT_ID,
registrar.getRegistrarId(),
registrarId,
LOCKS_PARAM,
getLockedDomains(registrarId, isAdmin));
}

View File

@@ -184,10 +184,29 @@ public class RegistryLockPostAction implements Runnable, JsonActionRunner.JsonAc
private String verifyPasswordAndGetEmail(
UserAuthInfo userAuthInfo, RegistryLockPostInput postInput)
throws RegistrarAccessDeniedException {
User user = userAuthInfo.user();
if (registrarAccessor.isAdmin()) {
return user.getEmail();
return userAuthInfo.getEmailAddress();
}
if (userAuthInfo.appEngineUser().isPresent()) {
return verifyPasswordAndGetEmailLegacyUser(userAuthInfo.appEngineUser().get(), postInput);
} else {
return verifyPasswordAndGetEmailConsoleUser(userAuthInfo.consoleUser().get(), postInput);
}
}
private String verifyPasswordAndGetEmailConsoleUser(
google.registry.model.console.User user, RegistryLockPostInput postInput)
throws RegistrarAccessDeniedException {
// Verify that the registrar has locking enabled
getRegistrarAndVerifyLockAccess(registrarAccessor, postInput.registrarId, false);
checkArgument(
user.verifyRegistryLockPassword(postInput.password),
"Incorrect registry lock password for user");
return user.getEmailAddress();
}
private String verifyPasswordAndGetEmailLegacyUser(User user, RegistryLockPostInput postInput)
throws RegistrarAccessDeniedException {
// Verify that the user can access the registrar, that the user has
// registry lock enabled, and that the user provided a correct password
Registrar registrar =

View File

@@ -23,8 +23,8 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import google.registry.model.EppResource;
import google.registry.model.contact.Contact;
import google.registry.model.contact.ContactPhoneNumber;
import google.registry.model.contact.ContactResource;
import google.registry.model.contact.PostalInfo;
import google.registry.model.domain.DesignatedContact;
import google.registry.model.domain.DesignatedContact.Type;
@@ -124,7 +124,7 @@ final class DomainWhoisResponse extends WhoisResponseImpl {
}
/** Returns the contact of the given type. */
private Optional<VKey<ContactResource>> getContactReference(Type type) {
private Optional<VKey<Contact>> getContactReference(Type type) {
Optional<DesignatedContact> contactOfType =
domain.getContacts().stream().filter(d -> d.getType() == type).findFirst();
return contactOfType.map(DesignatedContact::getContactKey);
@@ -145,15 +145,15 @@ final class DomainWhoisResponse extends WhoisResponseImpl {
/** Emit the contact entry of the given type. */
DomainEmitter emitContact(
String contactType, Optional<VKey<ContactResource>> contact, boolean preferUnicode) {
String contactType, Optional<VKey<Contact>> contact, boolean preferUnicode) {
if (!contact.isPresent()) {
return this;
}
// If we refer to a contact that doesn't exist, that's a bug. It means referential integrity
// has somehow been broken. We skip the rest of this contact, but log it to hopefully bring it
// someone's attention.
ContactResource contactResource = EppResource.loadCached(contact.get());
if (contactResource == null) {
Contact contact1 = EppResource.loadCached(contact.get());
if (contact1 == null) {
logger.atSevere().log(
"(BUG) Broken reference found from domain %s to contact %s.",
domain.getDomainName(), contact);
@@ -162,11 +162,10 @@ final class DomainWhoisResponse extends WhoisResponseImpl {
PostalInfo postalInfo =
chooseByUnicodePreference(
preferUnicode,
contactResource.getLocalizedPostalInfo(),
contactResource.getInternationalizedPostalInfo());
contact1.getLocalizedPostalInfo(),
contact1.getInternationalizedPostalInfo());
// ICANN Consistent Labeling & Display policy requires that this be the ROID.
emitField(
ImmutableList.of("Registry", contactType, "ID"), contactResource.getRepoId(), fullOutput);
emitField(ImmutableList.of("Registry", contactType, "ID"), contact1.getRepoId(), fullOutput);
if (postalInfo != null) {
emitFieldIfDefined(ImmutableList.of(contactType, "Name"), postalInfo.getName(), fullOutput);
emitFieldIfDefined(
@@ -175,10 +174,9 @@ final class DomainWhoisResponse extends WhoisResponseImpl {
fullOutput || contactType.equals("Registrant"));
emitAddress(contactType, postalInfo.getAddress(), fullOutput);
}
emitPhone(contactType, "Phone", contactResource.getVoiceNumber());
emitPhone(contactType, "Fax", contactResource.getFaxNumber());
String emailFieldContent =
fullOutput ? contactResource.getEmailAddress() : whoisRedactedEmailText;
emitPhone(contactType, "Phone", contact1.getVoiceNumber());
emitPhone(contactType, "Fax", contact1.getFaxNumber());
String emailFieldContent = fullOutput ? contact1.getEmailAddress() : whoisRedactedEmailText;
emitField(ImmutableList.of(contactType, "Email"), emailFieldContent);
return this;
}

View File

@@ -17,7 +17,7 @@ package google.registry.whois;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.persistence.transaction.TransactionManagerFactory.replicaJpaTm;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
@@ -53,12 +53,14 @@ final class NameserverWhoisResponse extends WhoisResponseImpl {
ImmutableMap<Host, String> hostRegistrars =
subordinateHosts.isEmpty()
? ImmutableMap.of()
: tm().transact(
: replicaJpaTm()
.transact(
() ->
Maps.toMap(
subordinateHosts.iterator(),
host ->
tm().loadByKey(host.getSuperordinateDomain())
replicaJpaTm()
.loadByKey(host.getSuperordinateDomain())
.cloneProjectedAtTime(getTimestamp())
.getCurrentSponsorRegistrarId()));

Some files were not shown because too many files have changed in this diff Show More