// Copyright 2018 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;
import static com.google.common.base.Preconditions.checkArgument;
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.model.ofy.ObjectifyService.ofy;
import static google.registry.model.registry.Registry.TldState.GENERAL_AVAILABILITY;
import static google.registry.model.registry.Registry.TldState.START_DATE_SUNRISE;
import static google.registry.util.DateTimeUtils.START_OF_TIME;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
import com.googlecode.objectify.Key;
import google.registry.config.RegistryEnvironment;
import google.registry.model.common.GaeUserIdConverter;
import google.registry.model.pricing.StaticPremiumListPricingEngine;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarAddress;
import google.registry.model.registrar.RegistrarContact;
import google.registry.model.registry.Registry;
import google.registry.model.registry.Registry.TldState;
import google.registry.model.registry.label.PremiumList;
import google.registry.util.CidrAddressBlock;
import java.util.Collection;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
import java.util.regex.Pattern;
import org.joda.money.CurrencyUnit;
import org.joda.money.Money;
import org.joda.time.DateTime;
import org.joda.time.Duration;
/**
* Class to help build and persist all the OT&E entities in Datastore.
*
*
This includes the TLDs (Registries), Registrars, and the RegistrarContacts that can access the
* web console.
*
* This class is basically a "builder" for the parameters needed to generate the OT&E entities.
* Nothing is created until you call {@link #buildAndPersist}.
*
* Usage example:
*
*
{@code
* OteAccountBuilder.forClientId("example")
* .addContact("contact@email.com") // OPTIONAL
* .setPassword("password") // OPTIONAL
* .setCertificateHash(certificateHash) // OPTIONAL
* .setIpWhitelist(ImmutableList.of("1.1.1.1", "2.2.2.0/24")) // OPTIONAL
* .buildAndPersist();
* }
*/
public final class OteAccountBuilder {
/**
* Validation regex for registrar base client IDs (3-14 lowercase alphanumeric characters).
*
* The base client ID is appended with numbers to create four different test registrar accounts
* (e.g. reg-1, reg-3, reg-4, reg-5). Registrar client IDs are of type clIDType in eppcom.xsd
* which is limited to 16 characters, hence the limit of 14 here to account for the dash and
* numbers.
*
*
The base client ID is also used to generate the OT&E TLDs, hence the restriction to
* lowercase alphanumeric characters.
*/
private static final Pattern REGISTRAR_PATTERN = Pattern.compile("^[a-z0-9]{3,14}$");
// Durations are short so that registrars can test with quick transfer (etc.) turnaround.
private static final Duration SHORT_ADD_GRACE_PERIOD = Duration.standardMinutes(60);
private static final Duration SHORT_REDEMPTION_GRACE_PERIOD = Duration.standardMinutes(10);
private static final Duration SHORT_PENDING_DELETE_LENGTH = Duration.standardMinutes(5);
private static final String DEFAULT_PREMIUM_LIST = "default_sandbox_list";
private static final RegistrarAddress DEFAULT_ADDRESS =
new RegistrarAddress.Builder()
.setStreet(ImmutableList.of("e-street"))
.setCity("Neverland")
.setState("NY")
.setCountryCode("US")
.setZip("55555")
.build();
private static final ImmutableSortedMap EAP_FEE_SCHEDULE =
ImmutableSortedMap.of(
new DateTime(0),
Money.of(CurrencyUnit.USD, 0),
DateTime.parse("2018-03-01T00:00:00Z"),
Money.of(CurrencyUnit.USD, 100),
DateTime.parse("2030-03-01T00:00:00Z"),
Money.of(CurrencyUnit.USD, 0));
private final ImmutableMap clientIdToTld;
private final Registry sunriseTld;
private final Registry gaTld;
private final Registry eapTld;
private final ImmutableList.Builder contactsBuilder =
new ImmutableList.Builder<>();
private ImmutableList registrars;
private boolean replaceExisting = false;
private OteAccountBuilder(String baseClientId) {
checkState(
RegistryEnvironment.get() != RegistryEnvironment.PRODUCTION,
"Can't setup OT&E in production");
clientIdToTld = createClientIdToTldMap(baseClientId);
sunriseTld = createTld(baseClientId + "-sunrise", START_DATE_SUNRISE, false, 0);
gaTld = createTld(baseClientId + "-ga", GENERAL_AVAILABILITY, false, 2);
eapTld = createTld(baseClientId + "-eap", GENERAL_AVAILABILITY, true, 3);
registrars =
clientIdToTld.keySet().stream()
.map(OteAccountBuilder::createRegistrar)
.collect(toImmutableList());
}
/**
* Creates an OteAccountBuilder for the given base client ID.
*
* @param baseClientId the base clientId which will help name all the entities we create. Normally
* is the same as the "prod" clientId designated for this registrar.
*/
public static OteAccountBuilder forClientId(String baseClientId) {
return new OteAccountBuilder(baseClientId);
}
/**
* Set whether to replace any conflicting existing entities.
*
* If true, any existing entity that conflicts with the entities we want to create will be
* replaced with the newly created data.
*
*
If false, encountering an existing entity that conflicts with one we want to create will
* throw an exception during {@link #buildAndPersist}.
*
*
NOTE that if we fail, no entities are created (the creation is atomic).
*
*
Default is false (failing if entities exist)
*/
public OteAccountBuilder setReplaceExisting(boolean replaceExisting) {
this.replaceExisting = replaceExisting;
return this;
}
/**
* Adds a RegistrarContact with Web Console access.
*
*
NOTE: can be called more than once, adding multiple contacts. Each contact will have access
* to all OT&E Registrars.
*
* @param email the contact email that will have web-console access to all the Registrars. Must be
* from "our G Suite domain" (we have to be able to get its GaeUserId)
*/
public OteAccountBuilder addContact(String email) {
String gaeUserId =
checkNotNull(
GaeUserIdConverter.convertEmailAddressToGaeUserId(email),
"Email address %s is not associated with any GAE ID",
email);
registrars.forEach(
registrar -> contactsBuilder.add(createRegistrarContact(email, gaeUserId, registrar)));
return this;
}
/**
* Apply a function on all the OT&E Registrars.
*
*
Use this to set up registrar fields.
*
*
NOTE: DO NOT change anything that would affect the {@link Key#create} result on Registrars.
* If you want to make this function public, add a check that the Key.create on the registrars
* hasn't changed.
*
* @param func a function setting the requested fields on Registrar Builders. Will be applied to
* all the Registrars.
*/
private OteAccountBuilder transformRegistrars(
Function func) {
registrars =
registrars.stream()
.map(Registrar::asBuilder)
.map(func)
.map(Registrar.Builder::build)
.collect(toImmutableList());
return this;
}
/** Sets the EPP login password for all the OT&E Registrars. */
public OteAccountBuilder setPassword(String password) {
return transformRegistrars(builder -> builder.setPassword(password));
}
/** Sets the client certificate hash to all the OT&E Registrars. */
public OteAccountBuilder setCertificateHash(String certHash) {
return transformRegistrars(builder -> builder.setClientCertificateHash(certHash));
}
/** Sets the client certificate to all the OT&E Registrars. */
public OteAccountBuilder setCertificate(String asciiCert, DateTime now) {
return transformRegistrars(builder -> builder.setClientCertificate(asciiCert, now));
}
/** Sets the IP whitelist to all the OT&E Registrars. */
public OteAccountBuilder setIpWhitelist(Collection ipWhitelist) {
ImmutableList ipAddressWhitelist =
ipWhitelist.stream().map(CidrAddressBlock::create).collect(toImmutableList());
return transformRegistrars(builder -> builder.setIpAddressWhitelist(ipAddressWhitelist));
}
/**
* Persists all the OT&E entities to datastore.
*
* @return map from the new clientIds created to the new TLDs they have access to. Can be used to
* go over all the newly created Registrars / Registries / RegistrarContacts if any
* post-creation work is needed.
*/
public ImmutableMap buildAndPersist() {
// save all the entitiesl in a single transaction
ofy().transact(this::saveAllEntities);
return clientIdToTld;
}
/**
* Return map from the OT&E clientIds we will create to the new TLDs they will have access to.
*/
public ImmutableMap getClientIdToTldMap() {
return clientIdToTld;
}
/** Saves all the OT&E entities we created. */
private void saveAllEntities() {
ofy().assertInTransaction();
ImmutableList registries = ImmutableList.of(sunriseTld, gaTld, eapTld);
ImmutableList contacts = contactsBuilder.build();
if (!replaceExisting) {
ImmutableList> keys =
Streams.concat(registries.stream(), registrars.stream(), contacts.stream())
.map(Key::create)
.collect(toImmutableList());
Set> existingKeys = ofy().load().keys(keys).keySet();
checkState(
existingKeys.isEmpty(),
"Found existing object(s) conflicting with OT&E objects: %s",
existingKeys);
}
// Save the Registries (TLDs) first
ofy().save().entities(registries).now();
// Now we can set the allowedTlds for the registrars
registrars = registrars.stream().map(this::addAllowedTld).collect(toImmutableList());
// and we can save the registrars and contacts!
ofy().save().entities(registrars);
ofy().save().entities(contacts);
}
private Registrar addAllowedTld(Registrar registrar) {
String tld = clientIdToTld.get(registrar.getClientId());
if (registrar.getAllowedTlds().contains(tld)) {
return registrar;
}
return registrar
.asBuilder()
.setAllowedTldsUncached(Sets.union(registrar.getAllowedTlds(), ImmutableSet.of(tld)))
.build();
}
private static Registry createTld(
String tldName,
TldState initialTldState,
boolean isEarlyAccess,
int roidSuffix) {
String tldNameAlphaNumerical = tldName.replaceAll("[^a-z0-9]", "");
Optional premiumList = PremiumList.getUncached(DEFAULT_PREMIUM_LIST);
checkState(premiumList.isPresent(), "Couldn't find premium list %s.", DEFAULT_PREMIUM_LIST);
Registry.Builder builder =
new Registry.Builder()
.setTldStr(tldName)
.setPremiumPricingEngine(StaticPremiumListPricingEngine.NAME)
.setTldStateTransitions(ImmutableSortedMap.of(START_OF_TIME, initialTldState))
.setDnsWriters(ImmutableSet.of("VoidDnsWriter"))
.setPremiumList(premiumList.get())
.setRoidSuffix(
String.format(
"%S%X",
tldNameAlphaNumerical.substring(0, Math.min(tldNameAlphaNumerical.length(), 7)),
roidSuffix))
.setAddGracePeriodLength(SHORT_ADD_GRACE_PERIOD)
.setPendingDeleteLength(SHORT_PENDING_DELETE_LENGTH)
.setRedemptionGracePeriodLength(SHORT_REDEMPTION_GRACE_PERIOD);
if (isEarlyAccess) {
builder.setEapFeeSchedule(EAP_FEE_SCHEDULE);
}
return builder.build();
}
/**
* Creates the Registrar without the allowedTlds set - because we can't set allowedTlds before the
* TLD is saved.
*/
private static Registrar createRegistrar(String registrarName) {
return new Registrar.Builder()
.setClientId(registrarName)
.setRegistrarName(registrarName)
.setType(Registrar.Type.OTE)
.setLocalizedAddress(DEFAULT_ADDRESS)
.setEmailAddress("foo@neverland.com")
.setFaxNumber("+1.2125550100")
.setPhoneNumber("+1.2125550100")
.setIcannReferralEmail("nightmare@registrar.test")
.setState(Registrar.State.ACTIVE)
.build();
}
private static RegistrarContact createRegistrarContact(
String email, String gaeUserId, Registrar registrar) {
return new RegistrarContact.Builder()
.setParent(registrar)
.setName(email)
.setEmailAddress(email)
.setGaeUserId(gaeUserId)
.build();
}
/** Returns the ClientIds of the OT&E, with the TLDs each has access to. */
public static ImmutableMap createClientIdToTldMap(String baseClientId) {
checkArgument(
REGISTRAR_PATTERN.matcher(baseClientId).matches(),
"Invalid registrar name: %s",
baseClientId);
return new ImmutableMap.Builder()
.put(baseClientId + "-1", baseClientId + "-sunrise")
// The -2 registrar no longer exists because landrush no longer exists.
.put(baseClientId + "-3", baseClientId + "-ga")
.put(baseClientId + "-4", baseClientId + "-ga")
.put(baseClientId + "-5", baseClientId + "-eap")
.build();
}
/** Returns the base client ID that correspond to a given OT&E client ID. */
public static String getBaseClientId(String oteClientId) {
int index = oteClientId.lastIndexOf('-');
checkArgument(index > 0, "Invalid OT&E client ID: %s", oteClientId);
String baseClientId = oteClientId.substring(0, index);
checkArgument(
createClientIdToTldMap(baseClientId).containsKey(oteClientId),
"ID %s is not one of the OT&E client IDs for base %s",
oteClientId,
baseClientId);
return baseClientId;
}
}