diff --git a/java/google/registry/tools/BUILD b/java/google/registry/tools/BUILD index b1d1d4f6b..c65b96d92 100644 --- a/java/google/registry/tools/BUILD +++ b/java/google/registry/tools/BUILD @@ -75,6 +75,7 @@ java_library( "@com_google_appengine_api_1_0_sdk", "@com_google_appengine_remote_api", "@com_google_appengine_remote_api//:link", + "@com_google_auto_value", "@com_google_code_findbugs_jsr305", "@com_google_dagger", "@com_google_guava", diff --git a/java/google/registry/tools/CreateDomainCommand.java b/java/google/registry/tools/CreateDomainCommand.java index 0ce3157ca..be328afeb 100644 --- a/java/google/registry/tools/CreateDomainCommand.java +++ b/java/google/registry/tools/CreateDomainCommand.java @@ -59,7 +59,8 @@ final class CreateDomainCommand extends CreateOrUpdateDomainCommand { "registrant", registrant, "admins", admins, "techs", techs, - "password", password)); + "password", password, + "dsRecords", DsRecord.convertToSoy(dsRecords))); } } } diff --git a/java/google/registry/tools/CreateOrUpdateDomainCommand.java b/java/google/registry/tools/CreateOrUpdateDomainCommand.java index b5ca48fbb..62306e5e4 100644 --- a/java/google/registry/tools/CreateOrUpdateDomainCommand.java +++ b/java/google/registry/tools/CreateOrUpdateDomainCommand.java @@ -15,11 +15,20 @@ package google.registry.tools; import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.collect.ImmutableList.toImmutableList; import static google.registry.util.CollectionUtils.findDuplicates; +import com.beust.jcommander.IStringConverter; import com.beust.jcommander.Parameter; +import com.google.auto.value.AutoValue; +import com.google.common.base.Ascii; +import com.google.common.base.CharMatcher; import com.google.common.base.Joiner; +import com.google.common.base.Splitter; import com.google.common.collect.ImmutableSet; +import com.google.common.io.BaseEncoding; +import com.google.template.soy.data.SoyListData; +import com.google.template.soy.data.SoyMapData; import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -67,8 +76,78 @@ abstract class CreateOrUpdateDomainCommand extends MutatingEppToolCommand { ) String password; + @Parameter( + names = "--ds_records", + description = + "Comma-separated list of DS records. Each DS record is given as " + + " , in order, as it appears in the Zonefile.", + converter = DsRecordConverter.class + ) + List dsRecords = new ArrayList<>(); + Set domains; + @AutoValue + abstract static class DsRecord { + private static final Splitter SPLITTER = + Splitter.on(CharMatcher.whitespace()).omitEmptyStrings(); + + public abstract int keyTag(); + public abstract int alg(); + public abstract int digestType(); + public abstract String digest(); + + private static DsRecord create(int keyTag, int alg, int digestType, String digest) { + digest = Ascii.toUpperCase(digest); + checkArgument( + BaseEncoding.base16().canDecode(digest), + "digest should be even-lengthed hex, but is %s (length %s)", + digest, + digest.length()); + return new AutoValue_CreateOrUpdateDomainCommand_DsRecord(keyTag, alg, digestType, digest); + } + + /** + * Parses a string representation of the DS record. + * + *

The string format accepted is "[keyTag] [alg] [digestType] [digest]" (i.e., the 4 + * arguments separated by any number of spaces, as it appears in the Zone file) + */ + public static DsRecord parse(String dsRecord) { + List elements = SPLITTER.splitToList(dsRecord); + checkArgument( + elements.size() == 4, + "dsRecord %s should have 4 parts, but has %s", + dsRecord, + elements.size()); + return DsRecord.create( + Integer.parseUnsignedInt(elements.get(0)), + Integer.parseUnsignedInt(elements.get(1)), + Integer.parseUnsignedInt(elements.get(2)), + elements.get(3)); + } + + public SoyMapData toSoyData() { + return new SoyMapData( + "keyTag", keyTag(), + "alg", alg(), + "digestType", digestType(), + "digest", digest()); + } + + public static SoyListData convertToSoy(List dsRecords) { + return new SoyListData( + dsRecords.stream().map(DsRecord::toSoyData).collect(toImmutableList())); + } + } + + public static class DsRecordConverter implements IStringConverter { + @Override + public DsRecord convert(String dsRecord) { + return DsRecord.parse(dsRecord); + } + } + @Override protected void initEppToolCommand() throws Exception { checkArgument(nameservers.size() <= 13, "There can be at most 13 nameservers."); diff --git a/java/google/registry/tools/LockDomainCommand.java b/java/google/registry/tools/LockDomainCommand.java index dcb43f2f6..7dfd79427 100644 --- a/java/google/registry/tools/LockDomainCommand.java +++ b/java/google/registry/tools/LockDomainCommand.java @@ -71,7 +71,11 @@ public class LockDomainCommand extends LockOrUnlockDomainCommand { "removeAdmins", ImmutableList.of(), "removeTechs", ImmutableList.of(), "removeStatuses", ImmutableList.of(), - "change", false)); + "change", false, + "secdns", false, + "addDsRecords", ImmutableList.of(), + "removeDsRecords", ImmutableList.of(), + "removeAllDsRecords", false)); } } } diff --git a/java/google/registry/tools/UnlockDomainCommand.java b/java/google/registry/tools/UnlockDomainCommand.java index 55c45554b..5abee0982 100644 --- a/java/google/registry/tools/UnlockDomainCommand.java +++ b/java/google/registry/tools/UnlockDomainCommand.java @@ -72,7 +72,11 @@ public class UnlockDomainCommand extends LockOrUnlockDomainCommand { "removeTechs", ImmutableList.of(), "removeStatuses", statusesToRemove.stream().map(StatusValue::getXmlName).collect(toImmutableList()), - "change", false)); + "change", false, + "secdns", false, + "addDsRecords", ImmutableList.of(), + "removeDsRecords", ImmutableList.of(), + "removeAllDsRecords", false)); } } } diff --git a/java/google/registry/tools/UpdateDomainCommand.java b/java/google/registry/tools/UpdateDomainCommand.java index 4daec5114..effd8685e 100644 --- a/java/google/registry/tools/UpdateDomainCommand.java +++ b/java/google/registry/tools/UpdateDomainCommand.java @@ -70,6 +70,13 @@ final class UpdateDomainCommand extends CreateOrUpdateDomainCommand { ) private List addStatuses = new ArrayList<>(); + @Parameter( + names = "--add_ds_records", + description = "DS records to add. Cannot be set if --ds_records or --clear_ds_records is set.", + converter = DsRecordConverter.class + ) + private List addDsRecords = new ArrayList<>(); + @Parameter( names = "--remove_nameservers", description = @@ -96,6 +103,21 @@ final class UpdateDomainCommand extends CreateOrUpdateDomainCommand { ) private List removeStatuses = new ArrayList<>(); + @Parameter( + names = "--remove_ds_records", + description = + "DS records to remove. Cannot be set if --ds_records or --clear_ds_records is set.", + converter = DsRecordConverter.class + ) + private List removeDsRecords = new ArrayList<>(); + + @Parameter( + names = "--clear_ds_records", + description = + "removes all DS records. Is implied true if --ds_records is set." + ) + boolean clearDsRecords = false; + @Override protected void initMutatingEppToolCommand() { if (!nameservers.isEmpty()) { @@ -123,6 +145,15 @@ final class UpdateDomainCommand extends CreateOrUpdateDomainCommand { + "you cannot use the add_statuses and remove_statuses flags."); } + if (!dsRecords.isEmpty() || clearDsRecords){ + checkArgument( + addDsRecords.isEmpty() && removeDsRecords.isEmpty(), + "If you provide the ds_records or clear_ds_records flags, " + + "you cannot use the add_ds_records and remove_ds_records flags."); + addDsRecords = dsRecords; + clearDsRecords = true; + } + for (String domain : domains) { if (!nameservers.isEmpty() || !admins.isEmpty() || !techs.isEmpty() || !statuses.isEmpty()) { DateTime now = DateTime.now(UTC); @@ -185,7 +216,13 @@ final class UpdateDomainCommand extends CreateOrUpdateDomainCommand { boolean change = registrant != null || password != null; - if (!add && !remove && !change) { + boolean secdns = + !addDsRecords.isEmpty() + || !removeDsRecords.isEmpty() + || !dsRecords.isEmpty() + || clearDsRecords; + + if (!add && !remove && !change && !secdns) { logger.infofmt("No changes need to be made to domain %s", domain); continue; } @@ -207,7 +244,11 @@ final class UpdateDomainCommand extends CreateOrUpdateDomainCommand { "removeStatuses", removeStatuses, "change", change, "registrant", registrant, - "password", password)); + "password", password, + "secdns", secdns, + "addDsRecords", DsRecord.convertToSoy(addDsRecords), + "removeDsRecords", DsRecord.convertToSoy(removeDsRecords), + "removeAllDsRecords", clearDsRecords)); } } diff --git a/java/google/registry/tools/soy/DomainCreate.soy b/java/google/registry/tools/soy/DomainCreate.soy index 2499a960b..f4455f8d7 100644 --- a/java/google/registry/tools/soy/DomainCreate.soy +++ b/java/google/registry/tools/soy/DomainCreate.soy @@ -24,6 +24,7 @@ {@param admins: list} {@param techs: list} {@param password: string} + {@param dsRecords: list<[keyTag:int, alg:int, digestType:int, digest:string]>} @@ -53,6 +54,20 @@ + {if length($dsRecords) > 0} + + + {for $dsRecord in $dsRecords} + + {$dsRecord.keyTag} + {$dsRecord.alg} + {$dsRecord.digestType} + {$dsRecord.digest} + + {/for} + + + {/if} RegistryTool diff --git a/java/google/registry/tools/soy/DomainUpdate.soy b/java/google/registry/tools/soy/DomainUpdate.soy index c5cc24185..825a30d30 100644 --- a/java/google/registry/tools/soy/DomainUpdate.soy +++ b/java/google/registry/tools/soy/DomainUpdate.soy @@ -31,6 +31,10 @@ {@param change: bool} {@param? registrant: string} {@param? password: string} + {@param secdns: bool} + {@param addDsRecords: list<[keyTag:int, alg:int, digestType:int, digest:string]>} + {@param removeDsRecords: list<[keyTag:int, alg:int, digestType:int, digest:string]>} + {@param removeAllDsRecords: bool} @@ -92,6 +96,41 @@ {/if} + {if $secdns} + + + {if $removeAllDsRecords} + + true + + {/if} + {if length($removeDsRecords) > 0} + + {for $dsRecord in $removeDsRecords} + + {$dsRecord.keyTag} + {$dsRecord.alg} + {$dsRecord.digestType} + {$dsRecord.digest} + + {/for} + + {/if} + {if length($addDsRecords) > 0} + + {for $dsRecord in $addDsRecords} + + {$dsRecord.keyTag} + {$dsRecord.alg} + {$dsRecord.digestType} + {$dsRecord.digest} + + {/for} + + {/if} + + + {/if} RegistryTool diff --git a/javatests/google/registry/tools/CreateDomainCommandTest.java b/javatests/google/registry/tools/CreateDomainCommandTest.java index 6729adfb5..8ef70be48 100644 --- a/javatests/google/registry/tools/CreateDomainCommandTest.java +++ b/javatests/google/registry/tools/CreateDomainCommandTest.java @@ -40,6 +40,8 @@ public class CreateDomainCommandTest extends EppToolCommandTestCase + runCommandForced( + "--client=NewRegistrar", + "--registrant=crr-admin", + "--admins=crr-admin", + "--techs=crr-tech", + "--ds_records=1 2 3 ab cd", + "example.tld")); + assertThat(thrown).hasMessageThat().contains("should have 4 parts, but has 5"); + } + + @Test + public void testFailure_keyTagNotNumber() throws Exception { + IllegalArgumentException thrown = + expectThrows( + IllegalArgumentException.class, + () -> + runCommandForced( + "--client=NewRegistrar", + "--registrant=crr-admin", + "--admins=crr-admin", + "--techs=crr-tech", + "--ds_records=x 2 3 abcd", + "example.tld")); + assertThat(thrown).hasMessageThat().contains("\"x\""); + } + + @Test + public void testFailure_algNotNumber() throws Exception { + IllegalArgumentException thrown = + expectThrows( + IllegalArgumentException.class, + () -> + runCommandForced( + "--client=NewRegistrar", + "--registrant=crr-admin", + "--admins=crr-admin", + "--techs=crr-tech", + "--ds_records=1 x 3 abcd", + "example.tld")); + assertThat(thrown).hasMessageThat().contains("\"x\""); + } + + @Test + public void testFailure_digestTypeNotNumber() throws Exception { + IllegalArgumentException thrown = + expectThrows( + IllegalArgumentException.class, + () -> + runCommandForced( + "--client=NewRegistrar", + "--registrant=crr-admin", + "--admins=crr-admin", + "--techs=crr-tech", + "--ds_records=1 2 x abcd", + "example.tld")); + assertThat(thrown).hasMessageThat().contains("\"x\""); + } + + @Test + public void testFailure_digestNotHex() throws Exception { + IllegalArgumentException thrown = + expectThrows( + IllegalArgumentException.class, + () -> + runCommandForced( + "--client=NewRegistrar", + "--registrant=crr-admin", + "--admins=crr-admin", + "--techs=crr-tech", + "--ds_records=1 2 3 xbcd", + "example.tld")); + assertThat(thrown).hasMessageThat().contains("XBCD"); + } + + @Test + public void testFailure_digestNotEvenLengthed() throws Exception { + IllegalArgumentException thrown = + expectThrows( + IllegalArgumentException.class, + () -> + runCommandForced( + "--client=NewRegistrar", + "--registrant=crr-admin", + "--admins=crr-admin", + "--techs=crr-tech", + "--ds_records=1 2 3 abcde", + "example.tld")); + assertThat(thrown).hasMessageThat().contains("length 5"); + } } diff --git a/javatests/google/registry/tools/UpdateDomainCommandTest.java b/javatests/google/registry/tools/UpdateDomainCommandTest.java index 720f4d15a..2c9fcdaf6 100644 --- a/javatests/google/registry/tools/UpdateDomainCommandTest.java +++ b/javatests/google/registry/tools/UpdateDomainCommandTest.java @@ -42,10 +42,12 @@ public class UpdateDomainCommandTest extends EppToolCommandTestCase + runCommandForced( + "--client=NewRegistrar", + "--add_ds_records=1 2 3 abcd", + "--ds_records=4 5 6 EF01", + "example.tld")); + assertThat(thrown) + .hasMessageThat() + .contains( + "If you provide the ds_records or clear_ds_records flags, " + + "you cannot use the add_ds_records and remove_ds_records flags."); + } + + @Test + public void testFailure_provideDsRecordsAndRemoveDsRecords() throws Exception { + IllegalArgumentException thrown = + expectThrows( + IllegalArgumentException.class, + () -> + runCommandForced( + "--client=NewRegistrar", + "--remove_ds_records=7 8 9 12ab", + "--ds_records=4 5 6 EF01", + "example.tld")); + assertThat(thrown) + .hasMessageThat() + .contains( + "If you provide the ds_records or clear_ds_records flags, " + + "you cannot use the add_ds_records and remove_ds_records flags."); + } + + @Test + public void testFailure_clearDsRecordsAndAddDsRecords() throws Exception { + IllegalArgumentException thrown = + expectThrows( + IllegalArgumentException.class, + () -> + runCommandForced( + "--client=NewRegistrar", + "--add_ds_records=1 2 3 abcd", + "--clear_ds_records", + "example.tld")); + assertThat(thrown) + .hasMessageThat() + .contains( + "If you provide the ds_records or clear_ds_records flags, " + + "you cannot use the add_ds_records and remove_ds_records flags."); + } + + @Test + public void testFailure_clearDsRecordsAndRemoveDsRecords() throws Exception { + IllegalArgumentException thrown = + expectThrows( + IllegalArgumentException.class, + () -> + runCommandForced( + "--client=NewRegistrar", + "--remove_ds_records=7 8 9 12ab", + "--clear_ds_records", + "example.tld")); + assertThat(thrown) + .hasMessageThat() + .contains( + "If you provide the ds_records or clear_ds_records flags, " + + "you cannot use the add_ds_records and remove_ds_records flags."); + } } diff --git a/javatests/google/registry/tools/server/testdata/domain_create_complete.xml b/javatests/google/registry/tools/server/testdata/domain_create_complete.xml index c792955fc..d09d33a3c 100644 --- a/javatests/google/registry/tools/server/testdata/domain_create_complete.xml +++ b/javatests/google/registry/tools/server/testdata/domain_create_complete.xml @@ -20,6 +20,28 @@ + + + + 1 + 2 + 3 + ABCD + + + 4 + 5 + 6 + EF01 + + + 60485 + 5 + 2 + D4B7D520E7BB5F0F67674A0CCEB1E3E0614B93C4F9E99B8383F6A1E4469DA50A + + + RegistryTool diff --git a/javatests/google/registry/tools/server/testdata/domain_update_add.xml b/javatests/google/registry/tools/server/testdata/domain_update_add.xml index 9b2ca3d9e..dd7c09501 100644 --- a/javatests/google/registry/tools/server/testdata/domain_update_add.xml +++ b/javatests/google/registry/tools/server/testdata/domain_update_add.xml @@ -16,6 +16,24 @@ + + + + + 1 + 2 + 3 + ABCD + + + 4 + 5 + 6 + EF01 + + + + RegistryTool diff --git a/javatests/google/registry/tools/server/testdata/domain_update_clear_ds_records.xml b/javatests/google/registry/tools/server/testdata/domain_update_clear_ds_records.xml new file mode 100644 index 000000000..3a66e24fc --- /dev/null +++ b/javatests/google/registry/tools/server/testdata/domain_update_clear_ds_records.xml @@ -0,0 +1,19 @@ + + + + + + example.tld + + + + + + true + + + + RegistryTool + + diff --git a/javatests/google/registry/tools/server/testdata/domain_update_complete.xml b/javatests/google/registry/tools/server/testdata/domain_update_complete.xml index f3af0e25c..f3d92293f 100644 --- a/javatests/google/registry/tools/server/testdata/domain_update_complete.xml +++ b/javatests/google/registry/tools/server/testdata/domain_update_complete.xml @@ -30,6 +30,38 @@ + + + + + 7 + 8 + 9 + 12AB + + + 6 + 5 + 4 + 34CD + + + + + 1 + 2 + 3 + ABCD + + + 4 + 5 + 6 + EF01 + + + + RegistryTool diff --git a/javatests/google/registry/tools/server/testdata/domain_update_complete_abc.xml b/javatests/google/registry/tools/server/testdata/domain_update_complete_abc.xml index 8ef8853c0..74e96c09f 100644 --- a/javatests/google/registry/tools/server/testdata/domain_update_complete_abc.xml +++ b/javatests/google/registry/tools/server/testdata/domain_update_complete_abc.xml @@ -30,6 +30,38 @@ + + + + + 7 + 8 + 9 + 12AB + + + 6 + 5 + 4 + 34CD + + + + + 1 + 2 + 3 + ABCD + + + 4 + 5 + 6 + EF01 + + + + RegistryTool diff --git a/javatests/google/registry/tools/server/testdata/domain_update_remove.xml b/javatests/google/registry/tools/server/testdata/domain_update_remove.xml index fab3206c5..f5f77fbcf 100644 --- a/javatests/google/registry/tools/server/testdata/domain_update_remove.xml +++ b/javatests/google/registry/tools/server/testdata/domain_update_remove.xml @@ -15,6 +15,24 @@ + + + + + 7 + 8 + 9 + 12AB + + + 6 + 5 + 4 + 34CD + + + + RegistryTool diff --git a/javatests/google/registry/tools/server/testdata/domain_update_set_ds_records.xml b/javatests/google/registry/tools/server/testdata/domain_update_set_ds_records.xml new file mode 100644 index 000000000..8cc00ceb6 --- /dev/null +++ b/javatests/google/registry/tools/server/testdata/domain_update_set_ds_records.xml @@ -0,0 +1,33 @@ + + + + + + example.tld + + + + + + true + + + + 1 + 2 + 3 + ABCD + + + 4 + 5 + 6 + EF01 + + + + + RegistryTool + +