This CL include changes in the registrar console that makes it possible to designate an abuse contact in domain WHOIS record, per ICANN's CL&D requirement.

Frontend validation: ensures that only one WHOIS abuse contact exist per registrar. Any existing WHOIS abuse contact will be overridden when a new one is designated.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=155289097
This commit is contained in:
jianglai 2017-05-06 10:10:10 -07:00 committed by Ben McIlwain
parent 275d6ddc10
commit 2846f9c6b9
6 changed files with 162 additions and 9 deletions

View file

@ -108,6 +108,7 @@ registry.json.RegistrarAddress;
* emailAddress: string, * emailAddress: string,
* visibleInWhoisAsAdmin: boolean, * visibleInWhoisAsAdmin: boolean,
* visibleInWhoisAsTech: boolean, * visibleInWhoisAsTech: boolean,
* visibleInDomainWhoisAsAbuse: boolean,
* phoneNumber: (string?|undefined), * phoneNumber: (string?|undefined),
* faxNumber: (string?|undefined), * faxNumber: (string?|undefined),
* types: (string?|undefined) * types: (string?|undefined)

View file

@ -196,6 +196,8 @@ registry.registrar.ContactSettings.prototype.prepareUpdate =
} }
contact.visibleInWhoisAsAdmin = contact.visibleInWhoisAsAdmin == 'true'; contact.visibleInWhoisAsAdmin = contact.visibleInWhoisAsAdmin == 'true';
contact.visibleInWhoisAsTech = contact.visibleInWhoisAsTech == 'true'; contact.visibleInWhoisAsTech = contact.visibleInWhoisAsTech == 'true';
contact.visibleInDomainWhoisAsAbuse =
contact.visibleInDomainWhoisAsAbuse == 'true';
contact.types = ''; contact.types = '';
for (var tNdx in contact.type) { for (var tNdx in contact.type) {
if (contact.type[tNdx]) { if (contact.type[tNdx]) {
@ -206,6 +208,14 @@ registry.registrar.ContactSettings.prototype.prepareUpdate =
} }
} }
delete contact['type']; delete contact['type'];
// Override previous domain WHOIS abuse contact.
if (contact.visibleInDomainWhoisAsAbuse) {
for (var c in modelCopy.contacts) {
if (modelCopy.contacts[c].emailAddress != contact.emailAddress) {
modelCopy.contacts[c].visibleInDomainWhoisAsAbuse = false;
}
}
}
this.nextId = contact.emailAddress; this.nextId = contact.emailAddress;
}; };

View file

@ -29,6 +29,7 @@ import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Multimap; import com.google.common.collect.Multimap;
import com.googlecode.objectify.Work; import com.googlecode.objectify.Work;
import google.registry.config.RegistryConfig.Config; import google.registry.config.RegistryConfig.Config;
@ -52,6 +53,7 @@ import java.util.HashSet;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import javax.annotation.Nullable;
import javax.inject.Inject; import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
@ -270,11 +272,25 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA
} }
} }
ensurePhoneNumberNotRemovedForContactTypes(oldContactsByType, newContactsByType, Type.TECH); ensurePhoneNumberNotRemovedForContactTypes(oldContactsByType, newContactsByType, Type.TECH);
Optional<RegistrarContact> domainWhoisAbuseContact =
getDomainWhoisVisibleAbuseContact(updatedContacts);
// If the new set has a domain WHOIS abuse contact, it must have a phone number.
if (domainWhoisAbuseContact.isPresent()
&& domainWhoisAbuseContact.get().getPhoneNumber() == null) {
throw new ContactRequirementException(
"The abuse contact visible in domain WHOIS query must have a phone number");
}
// If there was a domain WHOIS abuse contact in the old set, the new set must have one.
if (getDomainWhoisVisibleAbuseContact(existingContacts).isPresent()
&& !domainWhoisAbuseContact.isPresent()) {
throw new ContactRequirementException(
"An abuse contact visible in domain WHOIS query must be designated");
}
} }
/** /**
* Ensure that for each given registrar type, a phone number is present after update, if there * Ensure that for each given registrar type, a phone number is present after update, if there was
* was one before. * one before.
*/ */
private static void ensurePhoneNumberNotRemovedForContactTypes( private static void ensurePhoneNumberNotRemovedForContactTypes(
Multimap<RegistrarContact.Type, RegistrarContact> oldContactsByType, Multimap<RegistrarContact.Type, RegistrarContact> oldContactsByType,
@ -291,6 +307,24 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA
} }
} }
/**
* Retrieves the registrar contact whose phone number and email address is visible in domain WHOIS
* query as abuse contact (if any).
*
* <p>Frontend processing ensures that only one contact can be set as abuse contact in domain
* WHOIS record. Therefore it is possible to return inside the loop once one such contact is
* found.
*/
private static Optional<RegistrarContact> getDomainWhoisVisibleAbuseContact(
Set<RegistrarContact> contacts) {
return Iterables.tryFind(contacts, new Predicate<RegistrarContact>() {
@Override
public boolean apply(@Nullable RegistrarContact contact) {
return contact.getVisibleInDomainWhoisAsAbuse();
}
});
}
/** /**
* Determines if any changes were made to the registrar besides the lastUpdateTime, and if so, * Determines if any changes were made to the registrar besides the lastUpdateTime, and if so,
* sends an email with a diff of the changes to the configured notification email address and * sends an email with a diff of the changes to the configured notification email address and

View file

@ -46,7 +46,9 @@
{param name: $c['name'] /} {param name: $c['name'] /}
{param emailAddress: $c['emailAddress'] /} {param emailAddress: $c['emailAddress'] /}
{param visibleInWhois: {param visibleInWhois:
($c['visibleInWhoisAsAdmin'] or $c['visibleInWhoisAsTech']) /} ($c['visibleInWhoisAsAdmin']
or $c['visibleInWhoisAsTech']
or $c['visibleInDomainWhoisAsAbuse']) /}
{param phoneNumber: $c['phoneNumber'] /} {param phoneNumber: $c['phoneNumber'] /}
{param faxNumber: $c['faxNumber'] /} {param faxNumber: $c['faxNumber'] /}
{/call} {/call}
@ -199,12 +201,17 @@
<p class="{css setting-item-list}"> <p class="{css setting-item-list}">
{let $visibleAsAdmin: $item['visibleInWhoisAsAdmin'] == true /} {let $visibleAsAdmin: $item['visibleInWhoisAsAdmin'] == true /}
{let $visibleAsTech: $item['visibleInWhoisAsTech'] == true /} {let $visibleAsTech: $item['visibleInWhoisAsTech'] == true /}
{if (not $visibleAsAdmin) and (not $visibleAsTech)} {let $visibleAsDomainAbuse: $item['visibleInDomainWhoisAsAbuse'] == true /}
{if (not $visibleAsAdmin) and (not $visibleAsTech) and (not $visibleAsDomainAbuse)}
<span class="{css whois-not-visible}">Not visible in WHOIS</span> <span class="{css whois-not-visible}">Not visible in WHOIS</span>
{else} {else}
{if $visibleAsAdmin}Admin{/if} {if $visibleAsAdmin}Registrar Admin{/if}
{if $visibleAsAdmin and $visibleAsTech},{sp}{/if} {if $visibleAsAdmin and $visibleAsTech},{sp}{/if}
{if $visibleAsTech}Technical{/if} {if $visibleAsTech}Registrar Technical{/if}
{if $visibleAsTech}
{if $visibleAsDomainAbuse},{sp}{/if}
{elseif $visibleAsAdmin and $visibleAsDomainAbuse},{sp}{/if}
{if $visibleAsDomainAbuse}Domain Abuse{/if}
{/if} {/if}
{/template} {/template}
@ -231,31 +238,48 @@
</tr> </tr>
<tr><td colspan="2"><hr></tr> <tr><td colspan="2"><hr></tr>
{call .whoisVisibleRadios_} {call .whoisVisibleRadios_}
{param description: 'Show in WHOIS as Admin contact' /} {param description: 'Show in Registrar WHOIS record as Admin contact' /}
{param fieldName: $namePrefix + 'visibleInWhoisAsAdmin' /} {param fieldName: $namePrefix + 'visibleInWhoisAsAdmin' /}
{param visible: $item['visibleInWhoisAsAdmin'] == true /} {param visible: $item['visibleInWhoisAsAdmin'] == true /}
{/call} {/call}
{call .whoisVisibleRadios_} {call .whoisVisibleRadios_}
{param description: 'Show in WHOIS as Technical contact' /} {param description: 'Show in Registrar WHOIS record as Technical contact' /}
{param fieldName: $namePrefix + 'visibleInWhoisAsTech' /} {param fieldName: $namePrefix + 'visibleInWhoisAsTech' /}
{param visible: $item['visibleInWhoisAsTech'] == true /} {param visible: $item['visibleInWhoisAsTech'] == true /}
{/call} {/call}
{call .whoisVisibleRadios_}
{param description:
'Show Phone and Email in Domain WHOIS Record as Registrar Abuse Contact' +
' (Per CL&D Requirements)'
/}
{param note:
'*Can only apply to one contact. Selecting Yes for this contact will' +
' force this setting for all other contacts to be No.'
/}
{param fieldName: $namePrefix + 'visibleInDomainWhoisAsAbuse' /}
{param visible: $item['visibleInDomainWhoisAsAbuse'] == true /}
{/call}
{/template} {/template}
/** @private */ /** @private */
{template .whoisVisibleRadios_ private="true"} {template .whoisVisibleRadios_ private="true"}
{@param description: string} {@param description: string}
{@param? note: string}
{@param fieldName: string} {@param fieldName: string}
{@param visible: bool} {@param visible: bool}
<tr class="{css kd-settings-pane-section}"> <tr class="{css kd-settings-pane-section}">
<td> <td>
<label for="{$fieldName}">{$description}</label> <label for="{$fieldName}">{$description}</label>
{if $note}
<span class="{css description}">{$note}</span>
{/if}
</td> </td>
<td class="{css setting}"> <td class="{css setting}">
<label for="{$fieldName}"> <label for="{$fieldName}">
<input <input
name="{$fieldName}" name="{$fieldName}"
id="{$fieldName}.true"
type="radio" type="radio"
value="true" value="true"
{if $visible} checked{/if}>{sp}Yes {if $visible} checked{/if}>{sp}Yes
@ -263,6 +287,7 @@
<label for="{$fieldName}"> <label for="{$fieldName}">
<input <input
name="{$fieldName}" name="{$fieldName}"
id="{$fieldName}.false"
type="radio" type="radio"
value="false" value="false"
{if not $visible} checked{/if}>{sp}No {if not $visible} checked{/if}>{sp}No

View file

@ -142,7 +142,7 @@ function testItemEditButtons() {
function testItemEdit() { function testItemEdit() {
testItemView(); testItemView();
registry.testing.click($('reg-app-btn-edit')); registry.testing.click($('reg-app-btn-edit'));
document.forms.namedItem('item').elements['contacts[0].name'].value = 'bob'; $('contacts[0].name').setAttribute('value', 'bob');
registry.testing.click($('reg-app-btn-save')); registry.testing.click($('reg-app-btn-save'));
testContact.name = 'bob'; testContact.name = 'bob';
registry.testing.assertReqMockRsp( registry.testing.assertReqMockRsp(
@ -248,6 +248,36 @@ function testOneOfManyUpdate() {
} }
function testDomainWhoisAbuseContactOverride() {
registry.registrar.ConsoleTestUtil.visit(test, {
path: 'contact-settings/test@example.com',
xsrfToken: test.testXsrfToken,
testClientId: test.testClientId
});
var oldDomainWhoisAbuseContact = createTestContact('old@asdf.com');
oldDomainWhoisAbuseContact.visibleInDomainWhoisAsAbuse = true;
var testContacts = [oldDomainWhoisAbuseContact, testContact];
registry.testing.assertReqMockRsp(
test.testXsrfToken, '/registrar-settings', {op: 'read', args: {}},
{status: 'SUCCESS', message: 'OK', results: [{contacts: testContacts}]});
// Edit testContact.
registry.testing.click($('reg-app-btn-edit'));
$('contacts[1].visibleInDomainWhoisAsAbuse.true')
.setAttribute('checked', 'checked');
$('contacts[1].visibleInDomainWhoisAsAbuse.false').removeAttribute('checked');
registry.testing.click($('reg-app-btn-save'));
// Should save them all back, and flip the old abuse contact's visibility
// boolean.
testContact.visibleInDomainWhoisAsAbuse = true;
oldDomainWhoisAbuseContact.visibleInDomainWhoisAsAbuse = false;
registry.testing.assertReqMockRsp(
test.testXsrfToken, '/registrar-settings',
{op: 'update', args: {contacts: testContacts, readonly: false}},
{status: 'SUCCESS', message: 'OK', results: [{contacts: testContacts}]});
}
function testDelete() { function testDelete() {
registry.registrar.ConsoleTestUtil.visit(test, { registry.registrar.ConsoleTestUtil.visit(test, {
path: 'contact-settings/test@example.com', path: 'contact-settings/test@example.com',
@ -305,6 +335,7 @@ function createTestContact(opt_email) {
faxNumber: '+1.2345551234', faxNumber: '+1.2345551234',
visibleInWhoisAsAdmin: false, visibleInWhoisAsAdmin: false,
visibleInWhoisAsTech: false, visibleInWhoisAsTech: false,
visibleInDomainWhoisAsAbuse: false,
types: 'ADMIN' types: 'ADMIN'
}; };
} }
@ -318,6 +349,7 @@ function createTestContact(opt_email) {
function simulateJsonForContact(contact) { function simulateJsonForContact(contact) {
contact.visibleInWhoisAsAdmin = contact.visibleInWhoisAsAdmin == 'true'; contact.visibleInWhoisAsAdmin = contact.visibleInWhoisAsAdmin == 'true';
contact.visibleInWhoisAsTech = contact.visibleInWhoisAsTech == 'true'; contact.visibleInWhoisAsTech = contact.visibleInWhoisAsTech == 'true';
contact.visibleInDomainWhoisAsAbuse = contact.visibleInDomainWhoisAsAbuse == 'true';
contact.types = ''; contact.types = '';
for (var tNdx in contact.type) { for (var tNdx in contact.type) {
if (contact.type[tNdx]) { if (contact.type[tNdx]) {

View file

@ -128,4 +128,55 @@ public class ContactSettingsTest extends RegistrarSettingsActionTestCase {
assertThat(response).containsEntry("message", "Please provide a phone number for at least one " assertThat(response).containsEntry("message", "Please provide a phone number for at least one "
+ RegistrarContact.Type.TECH.getDisplayName() + " contact"); + RegistrarContact.Type.TECH.getDisplayName() + " contact");
} }
@Test
public void testPost_updateContacts_cannotRemoveWhoisAbuseContact_error() throws Exception {
// First make the contact's info visible in whois as abuse contact info.
Registrar registrar = Registrar.loadByClientId(CLIENT_ID);
RegistrarContact rc =
AppEngineRule.makeRegistrarContact2()
.asBuilder()
.setVisibleInDomainWhoisAsAbuse(true)
.build();
// Lest we anger the timestamp inversion bug.
persistResource(registrar);
persistSimpleResource(rc);
// Now try to remove the contact.
rc = rc.asBuilder().setVisibleInDomainWhoisAsAbuse(false).build();
Map<String, Object> reqJson = registrar.toJsonMap();
reqJson.put("contacts", ImmutableList.of(rc.toJsonMap()));
Map<String, Object> response =
action.handleJsonRequest(ImmutableMap.of("op", "update", "args", reqJson));
assertThat(response).containsEntry("status", "ERROR");
assertThat(response)
.containsEntry(
"message", "An abuse contact visible in domain WHOIS query must be designated");
}
@Test
public void testPost_updateContacts_whoisAbuseContactMustHavePhoneNumber_error()
throws Exception {
// First make the contact's info visible in whois as abuse contact info.
Registrar registrar = Registrar.loadByClientId(CLIENT_ID);
RegistrarContact rc =
AppEngineRule.makeRegistrarContact2()
.asBuilder()
.setVisibleInDomainWhoisAsAbuse(true)
.build();
// Lest we anger the timestamp inversion bug.
persistResource(registrar);
persistSimpleResource(rc);
// Now try to set the phone number to null.
rc = rc.asBuilder().setPhoneNumber(null).build();
Map<String, Object> reqJson = registrar.toJsonMap();
reqJson.put("contacts", ImmutableList.of(rc.toJsonMap()));
Map<String, Object> response =
action.handleJsonRequest(ImmutableMap.of("op", "update", "args", reqJson));
assertThat(response).containsEntry("status", "ERROR");
assertThat(response)
.containsEntry(
"message", "The abuse contact visible in domain WHOIS query must have a phone number");
}
} }