Import code from internal repository to git

This commit is contained in:
Justine Tunney 2016-03-01 17:18:14 -05:00
commit 0ef0c933d2
2490 changed files with 281594 additions and 0 deletions

View file

@ -0,0 +1,105 @@
// Copyright 2016 Google Inc. 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 com.google.domain.registry.ui.server;
import static com.google.domain.registry.security.XsrfTokenManager.generateToken;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import com.google.common.net.MediaType;
import com.google.template.soy.data.SoyMapData;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/** Abstract servlet for serving HTML pages. */
public abstract class AbstractUiServlet extends HttpServlet {
protected String userId;
protected String userName;
protected String userActionName;
protected String userActionHref;
protected boolean userIsAdmin;
@Override
public void service(HttpServletRequest req, HttpServletResponse rsp)
throws ServletException, IOException {
UserService userService = UserServiceFactory.getUserService();
if (userService.isUserLoggedIn()) {
User u = userService.getCurrentUser();
userId = u.getUserId();
userName = u.getNickname();
userActionName = "Sign out";
userActionHref = userService.createLogoutURL(req.getRequestURI());
userIsAdmin = userService.isUserAdmin();
} else {
userId = null;
userName = null;
userActionName = "Sign in";
userActionHref = userService.createLoginURL(req.getRequestURI());
userIsAdmin = false;
}
super.service(req, rsp);
}
@Override
public void doGet(HttpServletRequest req, HttpServletResponse rsp)
throws ServletException, IOException {
rsp.addHeader("X-Frame-Options", "SAMEORIGIN"); // Disallow iframing.
rsp.setHeader("X-Ui-Compatible", "IE=edge"); // Ask IE not to be silly.
rsp.setContentType(MediaType.HTML_UTF_8.toString());
UserService userService = UserServiceFactory.getUserService();
if (!userService.isUserLoggedIn()) {
rsp.sendRedirect(userService.createLoginURL(req.getRequestURI()));
return;
}
rsp.getWriter().write(get(req));
}
/**
* Subclasses may override this method to access request params, or
* get() to simply return content.
*/
protected String get(@SuppressWarnings("unused") HttpServletRequest req) {
return get();
}
/** Override this to just return content. */
protected String get() {
throw new UnsupportedOperationException();
}
/**
* Returns a map with {@code (user: (id,name,actionName,actionHref), gaeUserId:, xsrfToken:)}
*/
protected SoyMapData getTemplateArgs(String xsrfToken) {
SoyMapData user = new SoyMapData();
user.put("id", userId);
user.put("name", userName);
user.put("actionName", userActionName);
user.put("actionHref", userActionHref);
user.put("isAdmin", userIsAdmin);
SoyMapData result = new SoyMapData();
result.put("user", user);
result.put("gaeUserId", userId);
result.put("xsrfToken", generateToken(xsrfToken));
return result;
}
}

View file

@ -0,0 +1,32 @@
package(default_visibility = ["//java/com/google/domain/registry:registry_project"])
java_library(
name = "server",
srcs = glob(["*.java"]),
resources = [
"//java/com/google/domain/registry/ui:globals.txt",
"//java/com/google/domain/registry/ui/css:admin_bin.css.js",
"//java/com/google/domain/registry/ui/css:admin_dbg.css.js",
"//java/com/google/domain/registry/ui/css:registrar_bin.css.js",
"//java/com/google/domain/registry/ui/css:registrar_dbg.css.js",
],
deps = [
"//java/com/google/common/base",
"//java/com/google/common/collect",
"//java/com/google/common/io",
"//java/com/google/common/net",
"//java/com/google/domain/registry/model",
"//java/com/google/domain/registry/security",
"//java/com/google/domain/registry/ui",
"//java/com/google/domain/registry/ui/forms",
"//java/com/google/domain/registry/util",
"//third_party/java/appengine:appengine-api",
"//third_party/java/joda_time",
"//third_party/java/json_simple",
"//third_party/java/jsr305_annotations",
"//third_party/java/servlet/servlet_api",
"//third_party/closure/templates",
],
)

View file

@ -0,0 +1,373 @@
// Copyright 2016 Google Inc. 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 com.google.domain.registry.ui.server;
import static com.google.common.collect.Range.atLeast;
import static com.google.common.collect.Range.atMost;
import static com.google.common.collect.Range.closed;
import static com.google.domain.registry.util.DomainNameUtils.canonicalizeDomainName;
import com.google.common.base.Function;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.net.InternetDomainName;
import com.google.domain.registry.model.registrar.Registrar;
import com.google.domain.registry.model.registrar.RegistrarAddress;
import com.google.domain.registry.model.registrar.RegistrarContact;
import com.google.domain.registry.ui.forms.FormField;
import com.google.domain.registry.ui.forms.FormFieldException;
import com.google.domain.registry.ui.forms.FormFields;
import com.google.domain.registry.util.CidrAddressBlock;
import com.google.domain.registry.util.X509Utils;
import java.security.cert.CertificateParsingException;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
/** Form fields for validating input for the {@code Registrar} class. */
public final class RegistrarFormFields {
public static final Pattern BASE64_PATTERN = Pattern.compile("[+/a-zA-Z0-9]*");
public static final Pattern ASCII_PATTERN = Pattern.compile("\\p{ASCII}*");
public static final String ASCII_ERROR = "Please only use ASCII-US characters.";
private static final Function<String, CidrAddressBlock> CIDR_TRANSFORM =
new Function<String, CidrAddressBlock>() {
@Nullable
@Override
public CidrAddressBlock apply(@Nullable String input) {
try {
return input != null ? CidrAddressBlock.create(input) : null;
} catch (IllegalArgumentException e) {
throw new FormFieldException("Not a valid CIDR notation IP-address block.", e);
}
}};
private static final Function<String, String> HOSTNAME_TRANSFORM =
new Function<String, String>() {
@Nullable
@Override
public String apply(@Nullable String input) {
if (input == null) {
return null;
}
if (!InternetDomainName.isValid(input)) {
throw new FormFieldException("Not a valid hostname.");
}
return canonicalizeDomainName(input);
}};
public static final FormField<String, String> NAME_FIELD =
FormFields.NAME.asBuilderNamed("registrarName")
.required()
.build();
public static final FormField<String, String> EMAIL_ADDRESS_FIELD =
FormFields.EMAIL.asBuilderNamed("emailAddress")
.matches(ASCII_PATTERN, ASCII_ERROR)
.required()
.build();
public static final FormField<String, String> ICANN_REFERRAL_EMAIL_FIELD =
FormFields.EMAIL.asBuilderNamed("icannReferralEmail")
.matches(ASCII_PATTERN, ASCII_ERROR)
.required()
.build();
public static final FormField<String, String> PHONE_NUMBER_FIELD =
FormFields.PHONE_NUMBER.asBuilder()
.matches(ASCII_PATTERN, ASCII_ERROR)
.build();
public static final FormField<String, String> FAX_NUMBER_FIELD =
FormFields.PHONE_NUMBER.asBuilderNamed("faxNumber")
.matches(ASCII_PATTERN, ASCII_ERROR)
.build();
public static final FormField<String, Registrar.State> STATE_FIELD =
FormField.named("state")
.emptyToNull()
.asEnum(Registrar.State.class)
.build();
public static final FormField<List<String>, Set<String>> ALLOWED_TLDS_FIELD =
FormFields.LABEL.asBuilderNamed("allowedTlds")
.asSet()
.build();
public static final FormField<String, String> WHOIS_SERVER_FIELD =
FormFields.LABEL.asBuilderNamed("whoisServer")
.transform(HOSTNAME_TRANSFORM)
.build();
public static final FormField<Boolean, Boolean> BLOCK_PREMIUM_NAMES_FIELD =
FormField.named("blockPremiumNames", Boolean.class)
.build();
public static final FormField<String, String> DRIVE_FOLDER_ID_FIELD =
FormFields.XS_NORMALIZED_STRING.asBuilderNamed("driveFolderId")
.build();
public static final FormField<String, String> CLIENT_CERTIFICATE_HASH_FIELD =
FormField.named("clientCertificateHash")
.emptyToNull()
.matches(BASE64_PATTERN, "Field must contain a base64 value.")
.range(closed(1, 255))
.build();
private static final FormField<String, String> X509_PEM_CERTIFICATE =
FormField.named("certificate")
.emptyToNull()
.transform(new Function<String, String>() {
@Nullable
@Override
public String apply(@Nullable String input) {
if (input == null) {
return null;
}
try {
X509Utils.loadCertificate(input);
} catch (CertificateParsingException e) {
throw new FormFieldException("Invalid X.509 PEM certificate");
}
return input;
}})
.build();
public static final FormField<String, String> CLIENT_CERTIFICATE_FIELD =
X509_PEM_CERTIFICATE.asBuilderNamed("clientCertificate").build();
public static final FormField<String, String> FAILOVER_CLIENT_CERTIFICATE_FIELD =
X509_PEM_CERTIFICATE.asBuilderNamed("failoverClientCertificate").build();
public static final FormField<Long, Long> BILLING_IDENTIFIER_FIELD =
FormField.named("billingIdentifier", Long.class)
.range(atLeast(1))
.build();
public static final FormField<Long, Long> IANA_IDENTIFIER_FIELD =
FormField.named("ianaIdentifier", Long.class)
.range(atLeast(1))
.build();
public static final FormField<String, String> URL_FIELD =
FormFields.MIN_TOKEN.asBuilderNamed("url")
.build();
public static final FormField<String, String> REFERRAL_URL_FIELD =
FormFields.MIN_TOKEN.asBuilderNamed("referralUrl")
.build();
public static final FormField<List<String>, List<CidrAddressBlock>> IP_ADDRESS_WHITELIST_FIELD =
FormField.named("ipAddressWhitelist")
.emptyToNull()
.transform(CidrAddressBlock.class, CIDR_TRANSFORM)
.asList()
.build();
public static final FormField<String, String> PASSWORD_FIELD =
FormFields.PASSWORD.asBuilderNamed("password")
.build();
public static final FormField<String, String> PHONE_PASSCODE_FIELD =
FormField.named("phonePasscode")
.emptyToNull()
.matches(Registrar.PHONE_PASSCODE_PATTERN)
.build();
public static final FormField<String, String> CONTACT_NAME_FIELD =
FormFields.NAME.asBuilderNamed("name")
.required()
.build();
public static final FormField<String, String> CONTACT_EMAIL_ADDRESS_FIELD =
FormFields.EMAIL.asBuilderNamed("emailAddress")
.required()
.build();
public static final FormField<Boolean, Boolean> CONTACT_VISIBLE_IN_WHOIS_AS_ADMIN_FIELD =
FormField.named("visibleInWhoisAsAdmin", Boolean.class)
.build();
public static final FormField<Boolean, Boolean> CONTACT_VISIBLE_IN_WHOIS_AS_TECH_FIELD =
FormField.named("visibleInWhoisAsTech", Boolean.class)
.build();
public static final FormField<String, String> CONTACT_PHONE_NUMBER_FIELD =
FormFields.PHONE_NUMBER.asBuilder()
.build();
public static final FormField<String, String> CONTACT_FAX_NUMBER_FIELD =
FormFields.PHONE_NUMBER.asBuilderNamed("faxNumber")
.build();
public static final FormField<String, String> CONTACT_GAE_USER_ID_FIELD =
FormFields.NAME.asBuilderNamed("gaeUserId")
.build();
public static final FormField<String, Set<RegistrarContact.Type>> CONTACT_TYPES =
FormField.named("types")
.uppercased()
.asEnum(RegistrarContact.Type.class)
.asSet(Splitter.on(',').omitEmptyStrings().trimResults())
.build();
public static final Function<Map<String, ?>, RegistrarContact.Builder>
REGISTRAR_CONTACT_TRANSFORM = new Function<Map<String, ?>, RegistrarContact.Builder>() {
@Nullable
@Override
public RegistrarContact.Builder apply(@Nullable Map<String, ?> args) {
if (args == null) {
return null;
}
RegistrarContact.Builder builder = new RegistrarContact.Builder();
for (String name : CONTACT_NAME_FIELD.extractUntyped(args).asSet()) {
builder.setName(name);
}
for (String emailAddress : CONTACT_EMAIL_ADDRESS_FIELD.extractUntyped(args).asSet()) {
builder.setEmailAddress(emailAddress);
}
for (Boolean visible :
CONTACT_VISIBLE_IN_WHOIS_AS_ADMIN_FIELD.extractUntyped(args).asSet()) {
builder.setVisibleInWhoisAsAdmin(visible);
}
for (Boolean visible :
CONTACT_VISIBLE_IN_WHOIS_AS_TECH_FIELD.extractUntyped(args).asSet()) {
builder.setVisibleInWhoisAsTech(visible);
}
for (String phoneNumber : CONTACT_PHONE_NUMBER_FIELD.extractUntyped(args).asSet()) {
builder.setPhoneNumber(phoneNumber);
}
for (String faxNumber : CONTACT_FAX_NUMBER_FIELD.extractUntyped(args).asSet()) {
builder.setFaxNumber(faxNumber);
}
for (Set<RegistrarContact.Type> types : CONTACT_TYPES.extractUntyped(args).asSet()) {
builder.setTypes(types);
}
for (String gaeUserId : CONTACT_GAE_USER_ID_FIELD.extractUntyped(args).asSet()) {
builder.setGaeUserId(gaeUserId);
}
return builder;
}};
public static final FormField<List<Map<String, ?>>, List<RegistrarContact.Builder>>
CONTACTS_FIELD = FormField.mapNamed("contacts")
.transform(RegistrarContact.Builder.class, REGISTRAR_CONTACT_TRANSFORM)
.asList()
.build();
public static final FormField<List<String>, List<String>> I18N_STREET_FIELD =
FormFields.XS_NORMALIZED_STRING.asBuilderNamed("street")
.range(closed(1, 255))
.matches(ASCII_PATTERN, ASCII_ERROR)
.asList()
.range(closed(1, 3))
.required()
.build();
public static final FormField<List<String>, List<String>> L10N_STREET_FIELD =
FormFields.XS_NORMALIZED_STRING.asBuilderNamed("street")
.range(closed(1, 255))
.asList()
.range(closed(1, 3))
.required()
.build();
public static final FormField<String, String> I18N_CITY_FIELD =
FormFields.NAME.asBuilderNamed("city")
.matches(ASCII_PATTERN, ASCII_ERROR)
.required()
.build();
public static final FormField<String, String> L10N_CITY_FIELD =
FormFields.NAME.asBuilderNamed("city")
.required()
.build();
public static final FormField<String, String> I18N_STATE_FIELD =
FormFields.XS_NORMALIZED_STRING.asBuilderNamed("state")
.range(atMost(255))
.matches(ASCII_PATTERN, ASCII_ERROR)
.build();
public static final FormField<String, String> L10N_STATE_FIELD =
FormFields.XS_NORMALIZED_STRING.asBuilderNamed("state")
.range(atMost(255))
.build();
public static final FormField<String, String> I18N_ZIP_FIELD =
FormFields.XS_TOKEN.asBuilderNamed("zip")
.range(atMost(16))
.matches(ASCII_PATTERN, ASCII_ERROR)
.build();
public static final FormField<String, String> L10N_ZIP_FIELD =
FormFields.XS_TOKEN.asBuilderNamed("zip")
.range(atMost(16))
.build();
public static final FormField<String, String> COUNTRY_CODE_FIELD =
FormFields.COUNTRY_CODE.asBuilder()
.required()
.build();
public static final FormField<Map<String, ?>, RegistrarAddress> L10N_ADDRESS_FIELD =
FormField.mapNamed("localizedAddress")
.transform(RegistrarAddress.class, newAddressTransform(
L10N_STREET_FIELD, L10N_CITY_FIELD, L10N_STATE_FIELD, L10N_ZIP_FIELD))
.build();
private static Function<Map<String, ?>, RegistrarAddress> newAddressTransform(
final FormField<List<String>, List<String>> streetField,
final FormField<String, String> cityField,
final FormField<String, String> stateField,
final FormField<String, String> zipField) {
return new Function<Map<String, ?>, RegistrarAddress>() {
@Nullable
@Override
public RegistrarAddress apply(@Nullable Map<String, ?> args) {
if (args == null) {
return null;
}
RegistrarAddress.Builder builder = new RegistrarAddress.Builder();
String countryCode = COUNTRY_CODE_FIELD.extractUntyped(args).get();
builder.setCountryCode(countryCode);
for (List<String> streets : streetField.extractUntyped(args).asSet()) {
builder.setStreet(ImmutableList.copyOf(streets));
}
for (String city : cityField.extractUntyped(args).asSet()) {
builder.setCity(city);
}
for (String state : stateField.extractUntyped(args).asSet()) {
if ("US".equals(countryCode)) {
state = state.toUpperCase();
if (!StateCode.US_MAP.containsKey(state)) {
throw new FormFieldException(stateField, "Unknown US state code.");
}
}
builder.setState(state);
}
for (String zip : zipField.extractUntyped(args).asSet()) {
builder.setZip(zip);
}
return builder.build();
}
};
}
}

View file

@ -0,0 +1,139 @@
// Copyright 2016 Google Inc. 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 com.google.domain.registry.ui.server;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Suppliers.memoize;
import static com.google.common.io.Resources.asCharSource;
import static com.google.common.io.Resources.getResource;
import static java.nio.charset.StandardCharsets.UTF_8;
import com.google.common.base.Joiner;
import com.google.common.base.Splitter;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableMap;
import com.google.common.io.Resources;
import com.google.domain.registry.ui.ConsoleDebug;
import com.google.template.soy.SoyFileSet;
import com.google.template.soy.SoyUtils;
import com.google.template.soy.base.SoySyntaxException;
import com.google.template.soy.parseinfo.SoyFileInfo;
import com.google.template.soy.shared.SoyCssRenamingMap;
import com.google.template.soy.tofu.SoyTofu;
import java.io.IOException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** Helper methods for rendering Soy templates from Java code. */
public final class SoyTemplateUtils {
/** Returns a memoized supplier containing compiled tofu. */
public static Supplier<SoyTofu> createTofuSupplier(final SoyFileInfo... soyInfos) {
return memoize(new Supplier<SoyTofu>() {
@Override
public SoyTofu get() {
ConsoleDebug debugMode = ConsoleDebug.get();
SoyFileSet.Builder builder = SoyFileSet.builder();
for (SoyFileInfo soyInfo : soyInfos) {
builder.add(getResource(soyInfo.getClass(), soyInfo.getFileName()));
}
Map<String, Object> globals = new HashMap<>();
try {
globals.putAll(SoyUtils.parseCompileTimeGlobals(asCharSource(SOY_GLOBALS, UTF_8)));
} catch (SoySyntaxException | IOException e) {
throw new RuntimeException("Failed to load soy globals", e);
}
globals.put("DEBUG", debugMode.ordinal());
builder.setCompileTimeGlobals(globals);
return builder.build().compileToTofu();
}});
}
/** Returns a memoized supplier of the thing you pass to {@code setCssRenamingMap()}. */
public static Supplier<SoyCssRenamingMap> createCssRenamingMapSupplier(
final URL cssMap,
final URL cssMapDebug) {
return memoize(new Supplier<SoyCssRenamingMap>() {
@Override
public SoyCssRenamingMap get() {
final ImmutableMap<String, String> renames = getCssRenames(cssMap, cssMapDebug);
return new SoyCssRenamingMap() {
@Override
public String get(String cssClassName) {
List<String> result = new ArrayList<>();
for (String part : CSS_CLASS_SPLITTER.split(cssClassName)) {
result.add(firstNonNull(renames.get(part), part));
}
return CSS_CLASS_JOINER.join(result);
}};
}});
}
private static ImmutableMap<String, String> getCssRenames(URL cssMap, URL cssMapDebug) {
try {
switch (ConsoleDebug.get()) {
case RAW:
return ImmutableMap.of(); // See firstNonNull() above for clarification.
case DEBUG:
return extractCssRenames(Resources.toString(cssMapDebug, UTF_8));
default:
return extractCssRenames(Resources.toString(cssMap, UTF_8));
}
} catch (IOException e) {
throw new RuntimeException("Failed to load css map", e);
}
}
/**
* Extract class name rewrites from a {@code .css.js} mapping file.
*
* <p>This is the file created when you pass {@code --css_renaming_output_file} to the Closure
* Stylesheets compiler. In order for this to work, {@code --output_renaming_map_format} should
* be {@code CLOSURE_COMPILED} or {@code CLOSURE_UNCOMPILED}.
*
* <p>Here's an example of what the {@code .css.js} file looks like:<pre>
*
* goog.setCssNameMapping({
* "nonLatin": "a",
* "secondary": "b",
* "mobile": "c"
* });</pre>
*
* <p>This is a burden that's only necessary for tofu, since the closure compiler is smart enough
* to substitute CSS class names when soy is compiled to JavaScript.
*/
private static ImmutableMap<String, String> extractCssRenames(String json) {
ImmutableMap.Builder<String, String> builder = new ImmutableMap.Builder<>();
Matcher matcher = KEY_VALUE_PATTERN.matcher(json);
while (matcher.find()) {
builder.put(matcher.group(1), matcher.group(2));
}
return builder.build();
}
private static final URL SOY_GLOBALS = getResource("com/google/domain/registry/ui/globals.txt");
private static final Splitter CSS_CLASS_SPLITTER = Splitter.on('-');
private static final Joiner CSS_CLASS_JOINER = Joiner.on('-');
private static final Pattern KEY_VALUE_PATTERN =
Pattern.compile("['\"]([^'\"]+)['\"]: ['\"]([^'\"]+)['\"]");
private SoyTemplateUtils() {}
}

View file

@ -0,0 +1,82 @@
// Copyright 2016 Google Inc. 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 com.google.domain.registry.ui.server;
import com.google.common.collect.ImmutableBiMap;
/**
* Bimap of state codes and names for the US Regime.
*
* @see "http://statetable.com/"
*/
public final class StateCode {
public static final ImmutableBiMap<String, String> US_MAP =
new ImmutableBiMap.Builder<String, String>()
.put("AL", "Alabama")
.put("AK", "Alaska")
.put("AZ", "Arizona")
.put("AR", "Arkansas")
.put("CA", "California")
.put("CO", "Colorado")
.put("CT", "Connecticut")
.put("DE", "Delaware")
.put("FL", "Florida")
.put("GA", "Georgia")
.put("HI", "Hawaii")
.put("ID", "Idaho")
.put("IL", "Illinois")
.put("IN", "Indiana")
.put("IA", "Iowa")
.put("KS", "Kansas")
.put("KY", "Kentucky")
.put("LA", "Louisiana")
.put("ME", "Maine")
.put("MD", "Maryland")
.put("MA", "Massachusetts")
.put("MI", "Michigan")
.put("MN", "Minnesota")
.put("MS", "Mississippi")
.put("MO", "Missouri")
.put("MT", "Montana")
.put("NE", "Nebraska")
.put("NV", "Nevada")
.put("NH", "New Hampshire")
.put("NJ", "New Jersey")
.put("NM", "New Mexico")
.put("NY", "New York")
.put("NC", "North Carolina")
.put("ND", "North Dakota")
.put("OH", "Ohio")
.put("OK", "Oklahoma")
.put("OR", "Oregon")
.put("PA", "Pennsylvania")
.put("RI", "Rhode Island")
.put("SC", "South Carolina")
.put("SD", "South Dakota")
.put("TN", "Tennessee")
.put("TX", "Texas")
.put("UT", "Utah")
.put("VT", "Vermont")
.put("VA", "Virginia")
.put("WA", "Washington")
.put("WV", "West Virginia")
.put("WI", "Wisconsin")
.put("WY", "Wyoming")
.put("DC", "Washington DC")
.build();
private StateCode() {}
}

View file

@ -0,0 +1,131 @@
// Copyright 2016 Google Inc. 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 com.google.domain.registry.ui.server.admin;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.domain.registry.config.RegistryConfig;
import com.google.domain.registry.config.RegistryEnvironment;
import com.google.domain.registry.request.HttpException.NotFoundException;
import com.google.domain.registry.security.JsonResponseHelper;
import com.google.domain.registry.security.JsonTransportServlet;
import com.google.domain.registry.ui.forms.FormFieldException;
import java.util.Map;
import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest;
/** A servlet for callbacks that manipulate resources. */
public abstract class AdminResourceServlet extends JsonTransportServlet {
private static final RegistryConfig CONFIG = RegistryEnvironment.get().config();
public static final String XSRF_SCOPE = "admin";
public AdminResourceServlet() {
super(XSRF_SCOPE, true);
}
@Override
public Map<String, Object> doJsonPost(HttpServletRequest req, Map<String, ?> params) {
String op = Optional.fromNullable((String) params.get("op")).or("read");
@SuppressWarnings("unchecked")
Map<String, ?> args = (Map<String, Object>) Optional.<Object>fromNullable(params.get("args"))
.or(ImmutableMap.of());
try {
switch (op) {
case "create":
return create(req, args);
case "update":
return update(req, args);
case "delete":
return delete(req, args);
case "read":
return read(req, args);
default:
throw new UnsupportedOperationException("Unknown operation: " + op);
}
} catch (FormFieldException e) {
return JsonResponseHelper.createFormFieldError(e.getMessage(), e.getFieldName());
}
}
@SuppressWarnings("unused")
Map<String, Object> create(HttpServletRequest req, Map<String, ?> args) {
throw new UnsupportedOperationException();
}
@SuppressWarnings("unused")
Map<String, Object> read(HttpServletRequest req, Map<String, ?> args) {
throw new UnsupportedOperationException();
}
@SuppressWarnings("unused")
Map<String, Object> update(HttpServletRequest req, Map<String, ?> args) {
throw new UnsupportedOperationException();
}
@SuppressWarnings("unused")
Map<String, Object> delete(HttpServletRequest req, Map<String, ?> args) {
throw new UnsupportedOperationException();
}
@SuppressWarnings("unchecked")
protected static <T> ImmutableList<T> getParamList(Map<String, ?> map, String identifier) {
return ImmutableList.copyOf(
Optional.fromNullable((Iterable<T>) map.get(identifier)).or(ImmutableList.<T>of()));
}
/** Like checkNotNull, but throws NotFoundException if given arg is null. */
protected static <T> T checkExists(@Nullable T obj, String msg) {
if (obj == null) {
throw new NotFoundException(msg);
}
return obj;
}
protected static String getValAsString(Map<String, ?> map, String identifier) {
return (String) map.get(identifier);
}
/** Returns the last path element or null if no path separator exists. */
@VisibleForTesting
String parsePath(HttpServletRequest req) {
String uri = req.getRequestURI();
String prefix = CONFIG.getAdminServletPathPrefix() + "/";
checkArgument(
uri.startsWith(prefix),
"Request URI must start with: %s",
prefix);
return uri.substring(prefix.length());
}
/** @return the last path element or null if no path separator exists. */
@Nullable
protected String parseId(HttpServletRequest req) {
String[] pathParts = parsePath(req).split("/");
return pathParts.length < 2 ? null : pathParts[pathParts.length - 1];
}
/** Like parseId but path must contain at least one path separator. */
protected String checkParseId(HttpServletRequest req) {
return checkNotNull(parseId(req), "Path must be of the form (/<collection>)+/<id>");
}
}

View file

@ -0,0 +1,48 @@
// Copyright 2016 Google Inc. 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 com.google.domain.registry.ui.server.admin;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Supplier;
import com.google.common.io.Resources;
import com.google.domain.registry.ui.server.AbstractUiServlet;
import com.google.domain.registry.ui.server.SoyTemplateUtils;
import com.google.domain.registry.ui.soy.admin.ConsoleSoyInfo;
import com.google.template.soy.shared.SoyCssRenamingMap;
import com.google.template.soy.tofu.SoyTofu;
/** UI for Registry operations. */
public class AdminUiServlet extends AbstractUiServlet {
@VisibleForTesting
static final Supplier<SoyTofu> TOFU_SUPPLIER =
SoyTemplateUtils.createTofuSupplier(
com.google.domain.registry.ui.soy.ConsoleSoyInfo.getInstance(),
com.google.domain.registry.ui.soy.admin.ConsoleSoyInfo.getInstance());
public static final Supplier<SoyCssRenamingMap> CSS_RENAMING_MAP_SUPPLIER =
SoyTemplateUtils.createCssRenamingMapSupplier(
Resources.getResource("com/google/domain/registry/ui/css/admin_bin.css.js"),
Resources.getResource("com/google/domain/registry/ui/css/admin_dbg.css.js"));
@Override
protected String get() {
return TOFU_SUPPLIER.get()
.newRenderer(ConsoleSoyInfo.MAIN)
.setCssRenamingMap(CSS_RENAMING_MAP_SUPPLIER.get())
.setData(getTemplateArgs(AdminResourceServlet.XSRF_SCOPE))
.render();
}
}

View file

@ -0,0 +1,36 @@
package(default_visibility = ["//java/com/google/domain/registry:registry_project"])
java_library(
name = "admin",
srcs = glob(["*.java"]),
resources = [
"//java/com/google/domain/registry/ui/css:admin_bin.css.js",
"//java/com/google/domain/registry/ui/css:admin_dbg.css.js",
],
deps = [
"//java/com/google/common/annotations",
"//java/com/google/common/base",
"//java/com/google/common/collect",
"//java/com/google/common/io",
"//java/com/google/domain/registry/config",
"//java/com/google/domain/registry/export/sheet",
"//java/com/google/domain/registry/flows",
"//java/com/google/domain/registry/model",
"//java/com/google/domain/registry/request",
"//java/com/google/domain/registry/security",
"//java/com/google/domain/registry/security:servlets",
"//java/com/google/domain/registry/ui/forms",
"//java/com/google/domain/registry/ui/server",
"//java/com/google/domain/registry/ui/server/registrar",
"//java/com/google/domain/registry/ui/soy:soy_java_wrappers",
"//java/com/google/domain/registry/ui/soy/admin:soy_java_wrappers",
"//java/com/google/domain/registry/util",
"//third_party/java/joda_time",
"//third_party/java/jsr305_annotations",
"//third_party/java/objectify:objectify-v4_1",
"//third_party/java/servlet/servlet_api",
"//third_party/closure/templates",
],
)

View file

@ -0,0 +1,165 @@
// Copyright 2016 Google Inc. 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 com.google.domain.registry.ui.server.admin;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.domain.registry.export.sheet.SyncRegistrarsSheetTask;
import com.google.domain.registry.model.registrar.Registrar;
import com.google.domain.registry.model.registrar.RegistrarContact;
import com.google.domain.registry.ui.forms.FormFields;
import com.google.domain.registry.ui.server.RegistrarFormFields;
import com.googlecode.objectify.VoidWork;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
/**
* Admin servlet that allows creating or updating a registrar. Deletes are not allowed so as to
* preserve history.
*/
public class RegistrarServlet extends AdminResourceServlet {
@Override
public Map<String, Object> create(HttpServletRequest req, final Map<String, ?> args) {
final String clientIdentifier = FormFields.CLID.convert(parseId(req)).get();
ofy().transact(new VoidWork() {
@Override
public void vrun() {
Registrar registrar = new Registrar.Builder()
.setClientIdentifier(clientIdentifier)
.setRegistrarName(clientIdentifier)
.setType(Registrar.Type.TEST)
.setState(Registrar.State.ACTIVE)
.setAllowedTlds(ImmutableSet.<String>of())
.build();
Registrar.Builder builder = registrar.asBuilder();
Set<RegistrarContact> contacts = update(registrar, builder, args);
registrar = builder.build();
ofy().save().entity(registrar);
if (!contacts.isEmpty()) {
RegistrarContact.updateContacts(registrar, contacts);
}
}});
return ImmutableMap.<String, Object>of("results", ImmutableList.of(clientIdentifier + ": ok"));
}
@Override
public Map<String, Object> read(HttpServletRequest req, Map<String, ?> args) {
String clientIdentifier = parseId(req);
if (clientIdentifier == null) {
List<Map<String, ?>> registrars = new ArrayList<>();
for (Registrar registrar : Registrar.loadAll()) {
registrars.add(registrar.toJsonMap());
}
return ImmutableMap.<String, Object>of("set", registrars);
}
Registrar registrar = Registrar.loadByClientId(clientIdentifier);
checkExists(registrar, "No registrar exists with the given client id: " + clientIdentifier);
return ImmutableMap.<String, Object>of("item", registrar.toJsonMap());
}
@Override
public Map<String, Object> update(HttpServletRequest req, final Map<String, ?> args) {
final String clientIdentifier = checkParseId(req);
ofy().transact(new VoidWork() {
@Override
public void vrun() {
Registrar registrar = Registrar.loadByClientId(clientIdentifier);
Registrar.Builder builder = checkExists(
registrar,
"No registrar exists with the given client id: " + clientIdentifier)
.asBuilder();
Set<RegistrarContact> updatedContacts = update(registrar, builder, args);
if (!updatedContacts.isEmpty()) {
builder.setContactsRequireSyncing(true);
}
Registrar updatedRegistrar = builder.build();
ofy().save().entity(updatedRegistrar);
if (!updatedContacts.isEmpty()) {
RegistrarContact.updateContacts(updatedRegistrar, updatedContacts);
}
SyncRegistrarsSheetTask.enqueueBackendTask();
}});
return ImmutableMap.<String, Object>of("results", ImmutableList.of(clientIdentifier + ": ok"));
}
/**
* Admin fields are updated and then a chained call is made to
* {@link com.google.domain.registry.ui.server.registrar.RegistrarServlet#update(
* Registrar, Registrar.Builder, Map)}
* for the shared fields.
*/
private static Set<RegistrarContact> update(
Registrar existingRegistrarObj, Registrar.Builder builder, Map<String, ?> args) {
// Admin only settings
for (Registrar.State state :
RegistrarFormFields.STATE_FIELD.extractUntyped(args).asSet()) {
builder.setState(state);
}
builder.setAllowedTlds(
RegistrarFormFields.ALLOWED_TLDS_FIELD.extractUntyped(args).or(ImmutableSet.<String>of()));
Boolean blockPremiumNames =
RegistrarFormFields.BLOCK_PREMIUM_NAMES_FIELD.extractUntyped(args).orNull();
builder.setBlockPremiumNames(blockPremiumNames == null ? false : blockPremiumNames);
for (String password :
RegistrarFormFields.PASSWORD_FIELD.extractUntyped(args).asSet()) {
builder.setPassword(password);
}
for (Long billingIdentifier :
RegistrarFormFields.BILLING_IDENTIFIER_FIELD.extractUntyped(args).asSet()) {
builder.setBillingIdentifier(billingIdentifier);
}
// Resources
for (String driveFolderId :
RegistrarFormFields.DRIVE_FOLDER_ID_FIELD.extractUntyped(args).asSet()) {
builder.setDriveFolderId(driveFolderId);
}
// WHOIS
for (String registrarName :
RegistrarFormFields.NAME_FIELD.extractUntyped(args).asSet()) {
builder.setRegistrarName(registrarName);
}
for (Long ianaIdentifier :
RegistrarFormFields.IANA_IDENTIFIER_FIELD.extractUntyped(args).asSet()) {
builder.setIanaIdentifier(ianaIdentifier);
}
builder.setIcannReferralEmail(
RegistrarFormFields.ICANN_REFERRAL_EMAIL_FIELD.extractUntyped(args).get());
// Security
for (String phonePasscode :
RegistrarFormFields.PHONE_PASSCODE_FIELD.extractUntyped(args).asSet()) {
builder.setPhonePasscode(phonePasscode);
}
// Will this ever get used?
builder.setUrl(
RegistrarFormFields.URL_FIELD.extractUntyped(args).orNull());
return com.google.domain.registry.ui.server.registrar.RegistrarServlet.update(
existingRegistrarObj, builder, args);
}
}

View file

@ -0,0 +1,172 @@
// Copyright 2016 Google Inc. 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 com.google.domain.registry.ui.server.admin;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import static com.google.domain.registry.model.registry.Registries.getTlds;
import static com.google.domain.registry.util.DomainNameUtils.canonicalizeDomainName;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSortedMap;
import com.google.common.collect.Ordering;
import com.google.domain.registry.model.registry.Registry;
import com.google.domain.registry.model.registry.Registry.TldState;
import com.google.domain.registry.util.SystemClock;
import com.googlecode.objectify.VoidWork;
import org.joda.time.DateTime;
import org.joda.time.Duration;
import org.joda.time.Seconds;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
/**
* RESTful CRUD bindings for Registry objects.
*/
public class RegistryServlet extends AdminResourceServlet {
private static final SystemClock CLOCK = new SystemClock();
@Override
Map<String, Object> create(HttpServletRequest req, Map<String, ?> args) {
final String tld = checkParseId(req);
checkArgument(tld.equals(canonicalizeDomainName(tld)));
try {
ofy().transact(new VoidWork() {
@Override
public void vrun() {
ofy().save().entity(new Registry.Builder().setTldStr(tld).build());
}});
return ImmutableMap.<String, Object>of("results", ImmutableList.of(tld + ": ok"));
} catch (Exception e) {
return ImmutableMap.<String, Object>of("results", ImmutableList.of(tld + ": " + e));
}
}
@Override
Map<String, Object> read(HttpServletRequest req, Map<String, ?> args) {
String id = parseId(req);
if (id != null) {
return readTld(id);
}
// Collection request; if no item specified, return all.
return ImmutableMap.<String, Object>of("set", readTlds(getTlds()));
}
Map<String, Object> readTld(String tld) {
return ImmutableMap.<String, Object>of("item", toResultObject(
checkExists(Registry.get(tld), "No registry exists for the given tld: " + tld)));
}
List<Map<String, ?>> readTlds(Set<String> tlds) {
List<Map<String, ?>> registries = new ArrayList<>();
for (String tld : tlds) {
registries.add(toResultObject(Registry.get(tld)));
}
return registries;
}
Map<String, Object> toResultObject(Registry r) {
return new ImmutableSortedMap.Builder<String, Object>(Ordering.natural())
.put("name", r.getTld().toString())
.put("state", r.getTldState(CLOCK.nowUtc()).toString())
.put("tldStateTransitions", r.getTldStateTransitions().toString())
.put("creationTime", Objects.toString(r.getCreationTime(), ""))
.put("lastUpdateTime", Objects.toString(r.getUpdateAutoTimestamp().getTimestamp(), ""))
.put("addGracePeriod", r.getAddGracePeriodLength().toStandardSeconds().toString())
.put("autoRenewGracePeriod", r.getAutoRenewGracePeriodLength().toStandardSeconds().toString())
.put("redemptionGracePeriod", r.getRedemptionGracePeriodLength().toStandardSeconds().toString())
.put("renewGracePeriod", r.getRenewGracePeriodLength().toStandardSeconds().toString())
.put("transferGracePeriod", r.getTransferGracePeriodLength().toStandardSeconds().toString())
.put("automaticTransferLength", r.getAutomaticTransferLength().toStandardSeconds().toString())
.put("pendingDeleteLength", r.getPendingDeleteLength().toStandardSeconds().toString())
.build();
}
ImmutableSortedMap<DateTime, TldState> getTldStateTransitions(Map<String, ?> args) {
ImmutableSortedMap.Builder<DateTime, TldState> builder = ImmutableSortedMap.naturalOrder();
for (Map<String, ?> tldStateTransition
: AdminResourceServlet.<Map<String, ?>>getParamList(args, "tldStateTransitions")) {
builder.put(
DateTime.parse(getValAsString(tldStateTransition, "transitionTime")),
TldState.valueOf(getValAsString(tldStateTransition, "tldState")));
}
return builder.build();
}
Duration getValAsDuration(Map<String, ?> map, String identifier) {
return Seconds.parseSeconds(getValAsString(map, identifier)).toStandardDuration();
}
@Override
Map<String, Object> update(HttpServletRequest req, Map<String, ?> args) {
String tld = checkParseId(req);
ImmutableSortedMap<DateTime, TldState> tldStateTransitions =
getTldStateTransitions(args);
Duration addGracePeriodLength = getValAsDuration(args, "addGracePeriod");
Duration autoRenewGracePeriodLength = getValAsDuration(args, "autoRenewGracePeriod");
Duration redemptionGracePeriodLength = getValAsDuration(args, "redemptionGracePeriod");
Duration renewGracePeriodLength = getValAsDuration(args, "renewGracePeriod");
Duration transferGracePeriodLength = getValAsDuration(args, "transferGracePeriod");
Duration automaticTransferLength = getValAsDuration(args, "automaticTransferLength");
Duration pendingDeleteLength = getValAsDuration(args, "pendingDeleteLength");
try {
final Registry.Builder registry = Registry.get(tld).asBuilder();
if (!tldStateTransitions.isEmpty()) {
registry.setTldStateTransitions(tldStateTransitions);
}
if (!addGracePeriodLength.equals(Duration.ZERO)) {
registry.setAddGracePeriodLength(addGracePeriodLength);
}
if (!autoRenewGracePeriodLength.equals(Duration.ZERO)) {
registry.setAutoRenewGracePeriodLength(autoRenewGracePeriodLength);
}
if (!redemptionGracePeriodLength.equals(Duration.ZERO)) {
registry.setRedemptionGracePeriodLength(redemptionGracePeriodLength);
}
if (!renewGracePeriodLength.equals(Duration.ZERO)) {
registry.setRenewGracePeriodLength(renewGracePeriodLength);
}
if (!transferGracePeriodLength.equals(Duration.ZERO)) {
registry.setTransferGracePeriodLength(transferGracePeriodLength);
}
if (!automaticTransferLength.equals(Duration.ZERO)) {
registry.setAutomaticTransferLength(automaticTransferLength);
}
if (!pendingDeleteLength.equals(Duration.ZERO)) {
registry.setPendingDeleteLength(pendingDeleteLength);
}
ofy().transact(new VoidWork(){
@Override
public void vrun() {
ofy().save().entity(registry.build());
}});
return ImmutableMap.<String, Object>of("results", "OK");
} catch (Exception e) {
return ImmutableMap.<String, Object>of("results", e.toString());
}
}
}

View file

@ -0,0 +1,337 @@
// Copyright 2016 Google Inc. 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 com.google.domain.registry.ui.server.admin;
import static com.google.common.base.Predicates.not;
import static com.google.common.collect.Maps.toMap;
import static com.google.domain.registry.flows.EppXmlTransformer.unmarshal;
import static com.google.domain.registry.flows.FlowRegistry.getFlowClass;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import static com.google.domain.registry.util.CollectionUtils.isNullOrEmpty;
import static com.google.domain.registry.util.DomainNameUtils.ACE_PREFIX;
import static java.util.Arrays.asList;
import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Multiset;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.flows.Flow;
import com.google.domain.registry.flows.contact.ContactCreateFlow;
import com.google.domain.registry.flows.contact.ContactDeleteFlow;
import com.google.domain.registry.flows.contact.ContactTransferApproveFlow;
import com.google.domain.registry.flows.contact.ContactTransferCancelFlow;
import com.google.domain.registry.flows.contact.ContactTransferRejectFlow;
import com.google.domain.registry.flows.contact.ContactTransferRequestFlow;
import com.google.domain.registry.flows.contact.ContactUpdateFlow;
import com.google.domain.registry.flows.domain.DomainApplicationCreateFlow;
import com.google.domain.registry.flows.domain.DomainApplicationDeleteFlow;
import com.google.domain.registry.flows.domain.DomainApplicationUpdateFlow;
import com.google.domain.registry.flows.domain.DomainCreateFlow;
import com.google.domain.registry.flows.domain.DomainDeleteFlow;
import com.google.domain.registry.flows.domain.DomainRenewFlow;
import com.google.domain.registry.flows.domain.DomainRestoreRequestFlow;
import com.google.domain.registry.flows.domain.DomainTransferApproveFlow;
import com.google.domain.registry.flows.domain.DomainTransferCancelFlow;
import com.google.domain.registry.flows.domain.DomainTransferRejectFlow;
import com.google.domain.registry.flows.domain.DomainTransferRequestFlow;
import com.google.domain.registry.flows.domain.DomainUpdateFlow;
import com.google.domain.registry.flows.host.HostCreateFlow;
import com.google.domain.registry.flows.host.HostDeleteFlow;
import com.google.domain.registry.flows.host.HostUpdateFlow;
import com.google.domain.registry.model.domain.DomainCommand;
import com.google.domain.registry.model.domain.fee.FeeCreateExtension;
import com.google.domain.registry.model.domain.launch.LaunchCreateExtension;
import com.google.domain.registry.model.domain.secdns.SecDnsCreateExtension;
import com.google.domain.registry.model.domain.secdns.SecDnsUpdateExtension;
import com.google.domain.registry.model.eppinput.EppInput;
import com.google.domain.registry.model.eppinput.EppInput.ResourceCommandWrapper;
import com.google.domain.registry.model.host.HostCommand;
import com.google.domain.registry.model.reporting.HistoryEntry;
import com.google.domain.registry.security.JsonTransportServlet;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import javax.annotation.Nonnull;
import javax.servlet.http.HttpServletRequest;
/**
* A servlet that verifies a registrar's OTE status. Note that this is eventually consistent, so
* OT&amp;E commands that have been run just previously to verification may not be picked up yet.
*/
public class VerifyOteServlet extends JsonTransportServlet {
public static final String XSRF_SCOPE = "admin";
public VerifyOteServlet() {
super(XSRF_SCOPE, true);
}
@Override
@SuppressWarnings("unchecked")
public Map<String, Object> doJsonPost(HttpServletRequest req, Map<String, ?> params) {
final boolean summarize = Boolean.parseBoolean((String) params.get("summarize"));
return toMap(
(List<String>) params.get("registrars"),
new Function<String, Object>() {
@Nonnull
@Override
public Object apply(@Nonnull String registrar) {
return checkRegistrar(registrar, summarize);
}});
}
/** Checks whether the provided registrar has passed OT&amp;E and returns relevant information. */
private String checkRegistrar(String registrarName, boolean summarize) {
HistoryEntryStats historyEntryStats =
new HistoryEntryStats().recordRegistrarHistory(registrarName);
List<String> failureMessages = historyEntryStats.findFailures();
String passedFraction = String.format(
"%2d/%2d", StatType.NUM_REQUIREMENTS - failureMessages.size(), StatType.NUM_REQUIREMENTS);
String status = failureMessages.isEmpty() ? "PASS" : "FAIL";
return summarize
? String.format(
"Num actions: %4d - Reqs passed: %s - Overall: %s",
historyEntryStats.statCounts.size(),
passedFraction,
status)
: String.format(
"%s\n%s\nRequirements passed: %s\nOverall OT&E status: %s\n",
historyEntryStats,
Joiner.on('\n').join(failureMessages),
passedFraction,
status);
}
private static final Predicate<EppInput> HAS_CLAIMS_NOTICE = new Predicate<EppInput>() {
@Override
public boolean apply(@Nonnull EppInput eppInput) {
LaunchCreateExtension launchCreate =
eppInput.getSingleExtension(LaunchCreateExtension.class);
return launchCreate != null && launchCreate.getNotice() != null;
}};
private static final Predicate<EppInput> HAS_FEE = new Predicate<EppInput>() {
@Override
public boolean apply(@Nonnull EppInput eppInput) {
return eppInput.getSingleExtension(FeeCreateExtension.class) != null;
}};
private static final Predicate<EppInput> HAS_SEC_DNS = new Predicate<EppInput>() {
@Override
public boolean apply(@Nonnull EppInput eppInput) {
return (eppInput.getSingleExtension(SecDnsCreateExtension.class) != null)
|| (eppInput.getSingleExtension(SecDnsUpdateExtension.class) != null);
}};
private static final Predicate<EppInput> IS_SUNRISE = new Predicate<EppInput>() {
@Override
public boolean apply(@Nonnull EppInput eppInput) {
LaunchCreateExtension launchCreate =
eppInput.getSingleExtension(LaunchCreateExtension.class);
return launchCreate != null && !isNullOrEmpty(launchCreate.getSignedMarks());
}};
private static final Predicate<EppInput> IS_IDN = new Predicate<EppInput>() {
@Override
public boolean apply(@Nonnull EppInput eppInput) {
return ((DomainCommand.Create) ((ResourceCommandWrapper)
eppInput.getCommandWrapper().getCommand()).getResourceCommand())
.getFullyQualifiedDomainName().startsWith(ACE_PREFIX);
}};
private static final Predicate<EppInput> IS_SUBORDINATE = new Predicate<EppInput>() {
@Override
public boolean apply(@Nonnull EppInput eppInput) {
return !isNullOrEmpty(((HostCommand.Create) ((ResourceCommandWrapper)
eppInput.getCommandWrapper().getCommand()).getResourceCommand())
.getInetAddresses());
}};
private static Predicate<EppInput> isFlow(final Class<? extends Flow> flowClass) {
return new Predicate<EppInput>() {
@Override
public boolean apply(@Nonnull EppInput eppInput) {
try {
return flowClass.equals(getFlowClass(eppInput));
} catch (EppException e) {
throw new RuntimeException(e);
}
}};
}
/** Enum defining the distinct statistics (types of registrar actions) to record. */
public enum StatType {
CONTACT_CREATES(0, isFlow(ContactCreateFlow.class)),
CONTACT_DELETES(0, isFlow(ContactDeleteFlow.class)),
CONTACT_TRANSFER_APPROVES(0, isFlow(ContactTransferApproveFlow.class)),
CONTACT_TRANSFER_CANCELS(0, isFlow(ContactTransferCancelFlow.class)),
CONTACT_TRANSFER_REJECTS(0, isFlow(ContactTransferRejectFlow.class)),
CONTACT_TRANSFER_REQUESTS(0, isFlow(ContactTransferRequestFlow.class)),
CONTACT_UPDATES(0, isFlow(ContactUpdateFlow.class)),
DOMAIN_APPLICATION_CREATES(0, isFlow(DomainApplicationCreateFlow.class)),
DOMAIN_APPLICATION_CREATES_LANDRUSH(
1, isFlow(DomainApplicationCreateFlow.class), not(IS_SUNRISE)),
DOMAIN_APPLICATION_CREATES_SUNRISE(1, isFlow(DomainApplicationCreateFlow.class), IS_SUNRISE),
DOMAIN_APPLICATION_DELETES(2, isFlow(DomainApplicationDeleteFlow.class)),
DOMAIN_APPLICATION_UPDATES(2, isFlow(DomainApplicationUpdateFlow.class)),
DOMAIN_CREATES(0, isFlow(DomainCreateFlow.class)),
DOMAIN_CREATES_ASCII(1, isFlow(DomainCreateFlow.class), not(IS_IDN)),
DOMAIN_CREATES_IDN(1, isFlow(DomainCreateFlow.class), IS_IDN),
DOMAIN_CREATES_WITH_CLAIMS_NOTICE(1, isFlow(DomainCreateFlow.class), HAS_CLAIMS_NOTICE),
DOMAIN_CREATES_WITH_FEE(1, isFlow(DomainCreateFlow.class), HAS_FEE),
DOMAIN_CREATES_WITH_SEC_DNS(1, isFlow(DomainCreateFlow.class), HAS_SEC_DNS),
DOMAIN_CREATES_WITHOUT_SEC_DNS(0, isFlow(DomainCreateFlow.class), not(HAS_SEC_DNS)),
DOMAIN_DELETES(2, isFlow(DomainDeleteFlow.class)),
DOMAIN_RENEWS(0, isFlow(DomainRenewFlow.class)),
DOMAIN_RESTORES(1, isFlow(DomainRestoreRequestFlow.class)),
DOMAIN_TRANSFER_APPROVES(1, isFlow(DomainTransferApproveFlow.class)),
DOMAIN_TRANSFER_CANCELS(1, isFlow(DomainTransferCancelFlow.class)),
DOMAIN_TRANSFER_REJECTS(1, isFlow(DomainTransferRejectFlow.class)),
DOMAIN_TRANSFER_REQUESTS(1, isFlow(DomainTransferRequestFlow.class)),
DOMAIN_UPDATES(0, isFlow(DomainUpdateFlow.class)),
DOMAIN_UPDATES_WITH_SEC_DNS(1, isFlow(DomainUpdateFlow.class), HAS_SEC_DNS),
DOMAIN_UPDATES_WITHOUT_SEC_DNS(0, isFlow(DomainUpdateFlow.class), not(HAS_SEC_DNS)),
HOST_CREATES(0, isFlow(HostCreateFlow.class)),
HOST_CREATES_EXTERNAL(0, isFlow(HostCreateFlow.class), not(IS_SUBORDINATE)),
HOST_CREATES_SUBORDINATE(1, isFlow(HostCreateFlow.class), IS_SUBORDINATE),
HOST_DELETES(1, isFlow(HostDeleteFlow.class)),
HOST_UPDATES(1, isFlow(HostUpdateFlow.class)),
UNCLASSIFIED_FLOWS(0, Predicates.<EppInput>alwaysFalse());
/** The number of StatTypes with a non-zero requirement. */
private static final int NUM_REQUIREMENTS = FluentIterable.from(asList(values()))
.filter(new Predicate<StatType>() {
@Override
public boolean apply(@Nonnull StatType statType) {
return statType.requirement > 0;
}})
.size();
/** Required number of times registrars must complete this action. */
final int requirement;
/** Filters to determine if this action was performed by an EppInput. */
private Predicate<EppInput>[] filters;
@SafeVarargs
StatType(int requirement, Predicate<EppInput>... filters) {
this.requirement = requirement;
this.filters = filters;
}
/** Returns a more human-readable translation of the enum constant. */
String description() {
return this.name().replace('_', ' ').toLowerCase();
}
/** An {@link EppInput} might match multiple actions, so check if this action matches. */
boolean matches(EppInput eppInput) {
return Predicates.and(filters).apply(eppInput);
}
}
/** Class to represent stats derived from HistoryEntry objects on actions taken by registrars. */
static class HistoryEntryStats {
/** Stores counts of how many times each action type was performed. */
Multiset<StatType> statCounts = HashMultiset.create();
/**
* Records data in the passed historyEntryStats object on what actions have been performed by
* the four numbered OT&amp;E variants of the registrar name.
*/
HistoryEntryStats recordRegistrarHistory(String registrarName) {
ImmutableList.Builder<String> clientIds = new ImmutableList.Builder<>();
for (int i = 1; i <= 4; i++) {
clientIds.add(String.format("%s-%d", registrarName, i));
}
for (HistoryEntry historyEntry :
ofy().load().type(HistoryEntry.class).filter("clientId in", clientIds.build()).list()) {
try {
record(historyEntry);
} catch (EppException e) {
throw new RuntimeException(e);
}
}
return this;
}
/** Interprets the data in the provided HistoryEntry and increments counters. */
void record(HistoryEntry historyEntry) throws EppException {
byte[] xmlBytes = historyEntry.getXmlBytes();
// xmlBytes can be null on contact create and update for safe-harbor compliance.
//
// TODO(b/26161587): inspect the history entry itself to handle this properly.
if (xmlBytes == null) {
return;
}
final EppInput eppInput = unmarshal(xmlBytes);
if (!statCounts.addAll(
FluentIterable.from(EnumSet.allOf(StatType.class))
.filter(
new Predicate<StatType>() {
@Override
public boolean apply(@Nonnull StatType statType) {
return statType.matches(eppInput);
}
})
.toList())) {
statCounts.add(StatType.UNCLASSIFIED_FLOWS);
}
}
/**
* Returns a list of failure messages describing any cases where the passed stats fail to
* meet the required thresholds, or the empty list if all requirements are met.
*/
List<String> findFailures() {
List<String> messages = new ArrayList<>();
for (StatType statType : StatType.values()) {
if (statCounts.count(statType) < statType.requirement) {
messages.add(String.format(
"Failure: %s %s found.",
(statType.requirement == 1 ? "No" : "Not enough"),
statType.description()));
}
}
return messages;
}
/** Returns a string showing all possible actions and how many times each was performed. */
@Override
public String toString() {
return FluentIterable.from(EnumSet.allOf(StatType.class))
.transform(
new Function<StatType, String>() {
@Nonnull
@Override
public String apply(@Nonnull StatType statType) {
return String.format(
"%s: %d", statType.description(), statCounts.count(statType));
}
})
.append(String.format("TOTAL: %d", statCounts.size()))
.join(Joiner.on("\n"));
}
}
}

View file

@ -0,0 +1,16 @@
// Copyright 2016 Google Inc. 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.
@javax.annotation.ParametersAreNonnullByDefault
package com.google.domain.registry.ui.server.admin;

View file

@ -0,0 +1,24 @@
package(default_visibility = ["//java/com/google/domain/registry:registry_project"])
java_library(
name = "api",
srcs = glob(["*.java"]),
deps = [
"//java/com/google/common/annotations",
"//java/com/google/common/base",
"//java/com/google/common/collect",
"//java/com/google/common/net",
"//java/com/google/domain/registry/config",
"//java/com/google/domain/registry/flows",
"//java/com/google/domain/registry/model",
"//java/com/google/domain/registry/ui/server",
"//java/com/google/domain/registry/ui/soy/api:soy_java_wrappers",
"//java/com/google/domain/registry/util",
"//third_party/java/json_simple",
"//third_party/java/jsr305_annotations",
"//third_party/java/servlet/servlet_api",
"//third_party/closure/templates",
],
)

View file

@ -0,0 +1,138 @@
// Copyright 2016 Google Inc. 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 com.google.domain.registry.ui.server.api;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Strings.nullToEmpty;
import static com.google.common.net.HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN;
import static com.google.domain.registry.model.domain.DomainUtils.getTldFromDomainName;
import static com.google.domain.registry.model.eppcommon.ProtocolDefinition.ServiceExtension.FEE_0_6;
import static com.google.domain.registry.ui.server.SoyTemplateUtils.createTofuSupplier;
import static com.google.domain.registry.util.DomainNameUtils.canonicalizeDomainName;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.json.simple.JSONValue.toJSONString;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.net.MediaType;
import com.google.domain.registry.config.RegistryEnvironment;
import com.google.domain.registry.flows.EppException;
import com.google.domain.registry.flows.EppXmlTransformer;
import com.google.domain.registry.flows.FlowRunner;
import com.google.domain.registry.flows.FlowRunner.CommitMode;
import com.google.domain.registry.flows.FlowRunner.UserPrivileges;
import com.google.domain.registry.flows.SessionMetadata.SessionSource;
import com.google.domain.registry.flows.StatelessRequestSessionMetadata;
import com.google.domain.registry.flows.domain.DomainCheckFlow;
import com.google.domain.registry.model.domain.fee.FeeCheckResponseExtension;
import com.google.domain.registry.model.domain.fee.FeeCheckResponseExtension.FeeCheck;
import com.google.domain.registry.model.eppcommon.Trid;
import com.google.domain.registry.model.eppinput.EppInput;
import com.google.domain.registry.model.eppoutput.CheckData.DomainCheck;
import com.google.domain.registry.model.eppoutput.CheckData.DomainCheckData;
import com.google.domain.registry.model.eppoutput.Response;
import com.google.domain.registry.ui.soy.api.DomainCheckFeeEppSoyInfo;
import com.google.template.soy.tofu.SoyTofu;
import java.io.IOException;
import java.util.Map;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* A servlet that returns availability and premium checks as json.
* <p>
* This servlet returns plain JSON without a safety prefix, so it's vital that the output not be
* user controlled, lest it open an XSS vector. Do not modify this to return the domain name in the
* response.
*/
public class CheckApiServlet extends HttpServlet {
private static final Supplier<SoyTofu> TOFU_SUPPLIER =
createTofuSupplier(DomainCheckFeeEppSoyInfo.getInstance());
@Override
public void doGet(HttpServletRequest req, HttpServletResponse rsp) throws IOException {
Map<String, ?> response = doCheck(req.getParameter("domain"));
rsp.setHeader("Content-Disposition", "attachment");
rsp.setHeader("X-Content-Type-Options", "nosniff");
rsp.setHeader(ACCESS_CONTROL_ALLOW_ORIGIN, "*");
rsp.setContentType(MediaType.JSON_UTF_8.toString());
rsp.getWriter().write(toJSONString(response));
}
private StatelessRequestSessionMetadata sessionMetadata = new StatelessRequestSessionMetadata(
RegistryEnvironment.get().config().getCheckApiServletRegistrarClientId(),
false,
false,
ImmutableSet.of(FEE_0_6.getUri()),
SessionSource.HTTP);
// TODO(rgr): add whitebox instrumentation for this?
private Map<String, ?> doCheck(String domainString) {
try {
domainString = canonicalizeDomainName(nullToEmpty(domainString));
// Validate the TLD.
getTldFromDomainName(domainString);
} catch (IllegalStateException | IllegalArgumentException e) {
return fail("Must supply a valid second level domain name");
}
try {
byte[] inputXmlBytes = TOFU_SUPPLIER.get()
.newRenderer(DomainCheckFeeEppSoyInfo.DOMAINCHECKFEE)
.setData(ImmutableMap.of("domainName", domainString))
.render()
.getBytes(UTF_8);
Response response = new FlowRunner(
DomainCheckFlow.class,
EppXmlTransformer.<EppInput>unmarshal(inputXmlBytes),
Trid.create(CheckApiServlet.class.getSimpleName()),
sessionMetadata,
inputXmlBytes,
null)
.run(CommitMode.LIVE, UserPrivileges.NORMAL)
.getResponse();
DomainCheckData checkData = (DomainCheckData) response.getResponseData().get(0);
DomainCheck check = (DomainCheck) checkData.getChecks().get(0);
boolean available = check.getName().getAvail();
ImmutableMap.Builder<String, Object> builder = new ImmutableMap.Builder<String, Object>()
.put("status", "success")
.put("available", available);
if (available) {
FeeCheckResponseExtension feeCheckResponse =
(FeeCheckResponseExtension) response.getExtensions().get(0);
FeeCheck feeCheck = feeCheckResponse.getChecks().get(0);
builder.put("tier", firstNonNull(feeCheck.getFeeClass(), "standard"));
} else {
builder.put("reason", check.getReason());
}
return builder.build();
} catch (EppException e) {
return fail(e.getMessage());
} catch (Exception e) {
e.printStackTrace();
return fail("Invalid request");
}
}
private Map<String, String> fail(String reason) {
return ImmutableMap.of(
"status", "error",
"reason", reason);
}
}

View file

@ -0,0 +1,16 @@
// Copyright 2016 Google Inc. 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.
@javax.annotation.ParametersAreNonnullByDefault
package com.google.domain.registry.ui.server.api;

View file

@ -0,0 +1,16 @@
// Copyright 2016 Google Inc. 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.
@javax.annotation.ParametersAreNonnullByDefault
package com.google.domain.registry.ui.server;

View file

@ -0,0 +1,40 @@
package(default_visibility = ["//java/com/google/domain/registry:registry_project"])
java_library(
name = "registrar",
srcs = glob(["*.java"]),
resources = [
"//java/com/google/domain/registry/ui/css:registrar_bin.css.js",
"//java/com/google/domain/registry/ui/css:registrar_dbg.css.js",
],
deps = [
"//java/com/google/common/annotations",
"//java/com/google/common/base",
"//java/com/google/common/collect",
"//java/com/google/common/io",
"//java/com/google/common/net",
"//java/com/google/domain/registry/config",
"//java/com/google/domain/registry/export/sheet",
"//java/com/google/domain/registry/flows",
"//java/com/google/domain/registry/model",
"//java/com/google/domain/registry/request",
"//java/com/google/domain/registry/security",
"//java/com/google/domain/registry/security:servlets",
"//java/com/google/domain/registry/ui/forms",
"//java/com/google/domain/registry/ui/server",
"//java/com/google/domain/registry/ui/soy:soy_java_wrappers",
"//java/com/google/domain/registry/ui/soy/registrar:soy_java_wrappers",
"//java/com/google/domain/registry/util",
"//third_party/java/appengine:appengine-api",
"//third_party/java/braintree",
"//third_party/java/dagger",
"//third_party/java/joda_money",
"//third_party/java/jsr305_annotations",
"//third_party/java/jsr330_inject",
"//third_party/java/objectify:objectify-v4_1",
"//third_party/java/servlet/servlet_api",
"//third_party/closure/templates",
],
)

View file

@ -0,0 +1,76 @@
// Copyright 2016 Google Inc. 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 com.google.domain.registry.ui.server.registrar;
import com.google.appengine.api.users.UserServiceFactory;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Supplier;
import com.google.common.io.Resources;
import com.google.domain.registry.config.RegistryEnvironment;
import com.google.domain.registry.flows.EppConsoleServlet;
import com.google.domain.registry.ui.server.AbstractUiServlet;
import com.google.domain.registry.ui.server.SoyTemplateUtils;
import com.google.domain.registry.ui.soy.registrar.ConsoleSoyInfo;
import com.google.domain.registry.util.NonFinalForTesting;
import com.google.template.soy.data.SoyMapData;
import com.google.template.soy.shared.SoyCssRenamingMap;
import com.google.template.soy.tofu.SoyTofu;
import javax.servlet.http.HttpServletRequest;
/** Main registrar console servlet that serves the client code. */
public final class ConsoleUiServlet extends AbstractUiServlet {
@VisibleForTesting
static final Supplier<SoyTofu> TOFU_SUPPLIER =
SoyTemplateUtils.createTofuSupplier(
com.google.domain.registry.ui.soy.ConsoleSoyInfo.getInstance(),
com.google.domain.registry.ui.soy.registrar.ConsoleSoyInfo.getInstance());
@VisibleForTesting
public static final Supplier<SoyCssRenamingMap> CSS_RENAMING_MAP_SUPPLIER =
SoyTemplateUtils.createCssRenamingMapSupplier(
Resources.getResource("com/google/domain/registry/ui/css/registrar_bin.css.js"),
Resources.getResource("com/google/domain/registry/ui/css/registrar_dbg.css.js"));
@NonFinalForTesting
private static SessionUtils sessionUtils = new SessionUtils(UserServiceFactory.getUserService());
@Override
protected String get(HttpServletRequest req) {
if (!RegistryEnvironment.get().config().isRegistrarConsoleEnabled()) {
return TOFU_SUPPLIER.get()
.newRenderer(ConsoleSoyInfo.DISABLED)
.setCssRenamingMap(CSS_RENAMING_MAP_SUPPLIER.get())
.render();
}
SoyMapData data = getTemplateArgs(EppConsoleServlet.XSRF_SCOPE);
if (!sessionUtils.checkRegistrarConsoleLogin(req)) {
data.getMapData("user").put("actionName", "Logout and switch to another account");
return TOFU_SUPPLIER.get()
.newRenderer(ConsoleSoyInfo.WHOAREYOU)
.setCssRenamingMap(CSS_RENAMING_MAP_SUPPLIER.get())
.setData(data)
.render();
}
data.put("clientId", req.getSession().getAttribute(SessionUtils.CLIENT_ID_ATTRIBUTE));
return TOFU_SUPPLIER.get()
.newRenderer(ConsoleSoyInfo.MAIN)
.setCssRenamingMap(CSS_RENAMING_MAP_SUPPLIER.get())
.setData(data)
.render();
}
}

View file

@ -0,0 +1,329 @@
// Copyright 2016 Google Inc. 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 com.google.domain.registry.ui.server.registrar;
import static com.google.common.base.MoreObjects.firstNonNull;
import static com.google.common.base.Strings.emptyToNull;
import static com.google.common.base.Verify.verify;
import static com.google.domain.registry.security.JsonResponseHelper.Status.ERROR;
import static com.google.domain.registry.security.JsonResponseHelper.Status.SUCCESS;
import static java.util.Arrays.asList;
import com.google.common.base.Function;
import com.google.common.collect.ImmutableMap;
import com.google.domain.registry.config.ConfigModule.Config;
import com.google.domain.registry.request.Action;
import com.google.domain.registry.request.JsonActionRunner;
import com.google.domain.registry.request.JsonActionRunner.JsonAction;
import com.google.domain.registry.security.JsonResponseHelper;
import com.google.domain.registry.ui.forms.FormField;
import com.google.domain.registry.ui.forms.FormFieldException;
import com.google.domain.registry.util.FormattingLogger;
import com.braintreegateway.BraintreeGateway;
import com.braintreegateway.Result;
import com.braintreegateway.Transaction;
import com.braintreegateway.TransactionRequest;
import com.braintreegateway.ValidationError;
import com.braintreegateway.ValidationErrors;
import org.joda.money.CurrencyUnit;
import org.joda.money.IllegalCurrencyException;
import org.joda.money.Money;
import java.math.BigDecimal;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Pattern;
import javax.inject.Inject;
/**
* Action handling submission of customer payment form.
*
* <h3>Request Object</h3>
*
* <p>The request payload is a JSON object with the following fields:
*
* <dl>
* <dt>amount
* <dd>String containing a fixed point value representing the amount of money the registrar
* customer wishes to send the registry. This amount is arbitrary and entered manually by the
* customer in the payment form, as there is currently no integration with the billing system.
* <dt>currency
* <dd>String containing a three letter ISO currency code, which is used to look up the Braintree
* merchant account ID to which payment should be posted.
* <dt>paymentMethodNonce
* <dd>UUID nonce string supplied by the Braintree JS SDK representing the selected payment method.
* </dl>
*
* <h3>Response Object</h3>
*
* <p>The response payload will be a JSON response object (as defined by {@link JsonResponseHelper})
* which, if successful, will contain a single result object with the following fields:
*
* <dl>
* <dt>id
* <dd>String containing transaction ID returned by Braintree gateway.
* <dt>formattedAmount
* <dd>String containing amount paid, which can be displayed to the customer on a success page.
* </dl>
*
* <p><b>Note:</b> These definitions corresponds to Closure Compiler extern
* {@code registry.rpc.Payment} which must be updated should these definitions change.
*
* <h3>PCI Compliance</h3>
*
* <p>The request object will not contain credit card information, but rather a
* {@code payment_method_nonce} field that's populated by the Braintree JS SDK iframe.
*
* @see RegistrarPaymentSetupAction
*/
@Action(
path = "/registrar-payment",
method = Action.Method.POST,
xsrfProtection = true,
xsrfScope = "console",
requireLogin = true)
public final class RegistrarPaymentAction implements Runnable, JsonAction {
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
private static final FormField<String, BigDecimal> AMOUNT_FIELD =
FormField.named("amount")
.trimmed()
.emptyToNull()
.required()
.matches(Pattern.compile("-?\\d+(?:\\.\\d+)?"), "Invalid number.")
.transform(BigDecimal.class, new Function<String, BigDecimal>() {
@Override
public BigDecimal apply(String value) {
BigDecimal result = new BigDecimal(value);
if (result.signum() != 1) {
throw new FormFieldException("Must be a positive number.");
}
return result;
}})
.build();
private static final FormField<String, CurrencyUnit> CURRENCY_FIELD =
FormField.named("currency")
.trimmed()
.emptyToNull()
.required()
.matches(Pattern.compile("[A-Z]{3}"), "Invalid currency code.")
.transform(CurrencyUnit.class, new Function<String, CurrencyUnit>() {
@Override
public CurrencyUnit apply(String value) {
try {
return CurrencyUnit.of(value);
} catch (IllegalCurrencyException ignored) {
throw new FormFieldException("Unknown ISO currency code.");
}
}})
.build();
private static final FormField<String, String> PAYMENT_METHOD_NONCE_FIELD =
FormField.named("paymentMethodNonce")
.trimmed()
.emptyToNull()
.required()
.build();
@Inject BraintreeGateway braintreeGateway;
@Inject JsonActionRunner jsonActionRunner;
@Inject @Config("braintreeMerchantAccountIds") ImmutableMap<CurrencyUnit, String> accountIds;
@Inject RegistrarPaymentAction() {}
@Override
public void run() {
jsonActionRunner.run(this);
}
@Override
public Map<String, Object> handleJsonRequest(Map<String, ?> json) {
logger.infofmt("Processing payment: %s", json);
String paymentMethodNonce;
Money amount;
String merchantAccountId;
try {
paymentMethodNonce = PAYMENT_METHOD_NONCE_FIELD.extractUntyped(json).get();
try {
amount = Money.of(
CURRENCY_FIELD.extractUntyped(json).get(),
AMOUNT_FIELD.extractUntyped(json).get());
} catch (ArithmeticException e) {
// This happens when amount has more precision than the currency allows, e.g. $3.141.
throw new FormFieldException(AMOUNT_FIELD.name(), e.getMessage());
}
merchantAccountId = accountIds.get(amount.getCurrencyUnit());
if (merchantAccountId == null) {
throw new FormFieldException(CURRENCY_FIELD.name(), "Unsupported currency.");
}
} catch (FormFieldException e) {
logger.warning(e.toString());
return JsonResponseHelper.createFormFieldError(e.getMessage(), e.getFieldName());
}
Result<Transaction> result =
braintreeGateway.transaction().sale(
new TransactionRequest()
.amount(amount.getAmount())
.paymentMethodNonce(paymentMethodNonce)
.merchantAccountId(merchantAccountId)
.options()
.submitForSettlement(true)
.done());
if (result.isSuccess()) {
return handleSuccessResponse(result.getTarget());
} else if (result.getTransaction() != null) {
Transaction transaction = result.getTransaction();
switch (transaction.getStatus()) {
case PROCESSOR_DECLINED:
return handleProcessorDeclined(transaction);
case SETTLEMENT_DECLINED:
return handleSettlementDecline(transaction);
case GATEWAY_REJECTED:
return handleRejection(transaction);
default:
return handleMiscProcessorError(transaction);
}
} else {
return handleValidationErrorResponse(result.getErrors());
}
}
/**
* Handles a transaction success response.
*
* @see "https://developers.braintreepayments.com/reference/response/transaction/java#success"
* @see "https://developers.braintreepayments.com/reference/general/statuses#transaction"
*/
private Map<String, Object> handleSuccessResponse(Transaction transaction) {
// XXX: Currency scaling: https://github.com/braintree/braintree_java/issues/33
Money amount =
Money.of(CurrencyUnit.of(transaction.getCurrencyIsoCode()),
transaction.getAmount().stripTrailingZeros());
logger.infofmt("Transaction for %s via %s %s with ID: %s",
amount,
transaction.getPaymentInstrumentType(), // e.g. credit_card, paypal_account
transaction.getStatus(), // e.g. SUBMITTED_FOR_SETTLEMENT
transaction.getId());
return JsonResponseHelper
.create(SUCCESS, "Payment processed successfully", asList(
ImmutableMap.of(
"id", transaction.getId(),
"formattedAmount", formatMoney(amount))));
}
/**
* Handles a processor declined response.
*
* <p>This happens when the customer's bank blocks the transaction.
*
* @see "https://developers.braintreepayments.com/reference/response/transaction/java#processor-declined"
* @see "https://articles.braintreepayments.com/control-panel/transactions/declines"
*/
private Map<String, Object> handleProcessorDeclined(Transaction transaction) {
logger.warningfmt("Processor declined: %s %s",
transaction.getProcessorResponseCode(), transaction.getProcessorResponseText());
return JsonResponseHelper.create(ERROR,
"Payment declined: " + transaction.getProcessorResponseText());
}
/**
* Handles a settlement declined response.
*
* <p>This is a very rare condition that, for all intents and purposes, means the same thing as a
* processor declined response.
*
* @see "https://developers.braintreepayments.com/reference/response/transaction/java#processor-settlement-declined"
* @see "https://articles.braintreepayments.com/control-panel/transactions/declines"
*/
private Map<String, Object> handleSettlementDecline(Transaction transaction) {
logger.warningfmt("Settlement declined: %s %s",
transaction.getProcessorSettlementResponseCode(),
transaction.getProcessorSettlementResponseText());
return JsonResponseHelper.create(ERROR,
"Payment declined: " + transaction.getProcessorSettlementResponseText());
}
/**
* Handles a gateway rejection response.
*
* <p>This happens when a transaction is blocked due to settings we configured ourselves in the
* Braintree control panel.
*
* @see "https://developers.braintreepayments.com/reference/response/transaction/java#gateway-rejection"
* @see "https://articles.braintreepayments.com/control-panel/transactions/gateway-rejections"
* @see "https://articles.braintreepayments.com/guides/fraud-tools/avs-cvv"
* @see "https://articles.braintreepayments.com/guides/fraud-tools/overview"
*/
private Map<String, Object> handleRejection(Transaction transaction) {
logger.warningfmt("Gateway rejection: %s", transaction.getGatewayRejectionReason());
switch (transaction.getGatewayRejectionReason()) {
case DUPLICATE:
return JsonResponseHelper.create(ERROR, "Payment rejected: Possible duplicate.");
case AVS:
return JsonResponseHelper.create(ERROR, "Payment rejected: Invalid address.");
case CVV:
return JsonResponseHelper.create(ERROR, "Payment rejected: Invalid CVV code.");
case AVS_AND_CVV:
return JsonResponseHelper.create(ERROR, "Payment rejected: Invalid address and CVV code.");
case FRAUD:
return JsonResponseHelper.create(ERROR,
"Our merchant gateway suspects this payment of fraud. Please contact support.");
default:
return JsonResponseHelper.create(ERROR,
"Payment rejected: " + transaction.getGatewayRejectionReason());
}
}
/** Handles a miscellaneous transaction processing error response. */
private Map<String, Object> handleMiscProcessorError(Transaction transaction) {
logger.warningfmt("Error processing transaction: %s %s %s",
transaction.getStatus(),
transaction.getProcessorResponseCode(),
transaction.getProcessorResponseText());
return JsonResponseHelper.create(ERROR,
"Payment failure: "
+ firstNonNull(
emptyToNull(transaction.getProcessorResponseText()),
transaction.getStatus().toString()));
}
/**
* Handles a validation error response from Braintree.
*
* @see "https://developers.braintreepayments.com/reference/response/transaction/java#validation-errors"
* @see "https://developers.braintreepayments.com/reference/general/validation-errors/all/java"
*/
private Map<String, Object> handleValidationErrorResponse(ValidationErrors validationErrors) {
List<ValidationError> errors = validationErrors.getAllDeepValidationErrors();
verify(!errors.isEmpty(), "Payment failed but validation error list was empty");
for (ValidationError error : errors) {
logger.warningfmt("Payment validation failed on field: %s\nCode: %s\nMessage: %s",
error.getAttribute(), error.getCode(), error.getMessage());
}
return JsonResponseHelper
.createFormFieldError(errors.get(0).getMessage(), errors.get(0).getAttribute());
}
private static String formatMoney(Money amount) {
String symbol = amount.getCurrencyUnit().getSymbol(Locale.US);
BigDecimal number = amount.getAmount().setScale(amount.getCurrencyUnit().getDecimalPlaces());
return symbol.length() == 1 ? symbol + number : amount.toString();
}
}

View file

@ -0,0 +1,110 @@
// Copyright 2016 Google Inc. 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 com.google.domain.registry.ui.server.registrar;
import static com.google.common.base.Functions.toStringFunction;
import static com.google.domain.registry.security.JsonResponseHelper.Status.ERROR;
import static com.google.domain.registry.security.JsonResponseHelper.Status.SUCCESS;
import static java.util.Arrays.asList;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.ImmutableMap;
import com.google.domain.registry.config.ConfigModule.Config;
import com.google.domain.registry.config.RegistryEnvironment;
import com.google.domain.registry.request.Action;
import com.google.domain.registry.request.JsonActionRunner;
import com.google.domain.registry.request.JsonActionRunner.JsonAction;
import com.google.domain.registry.security.JsonResponseHelper;
import com.braintreegateway.BraintreeGateway;
import org.joda.money.CurrencyUnit;
import java.util.Map;
import javax.inject.Inject;
/**
* Action returning information needed to render payment form in browser.
*
* <h3>Request Object</h3>
*
* <p>The request payload must be an empty JSON object.
*
* <h3>Response Object</h3>
*
* <p>The response payload will be a JSON response object (as defined by {@link JsonResponseHelper})
* containing a single result object with the following fields:
*
* <dl>
* <dt>brainframe
* <dd>URL for iframe that loads Braintree payment method selector.
* <dt>token
* <dd>Nonce string obtained from the Braintree API which is needed by the Braintree JS SDK.
* <dt>currencies
* <dd>Array of strings, each containing a three letter currency code, which should be displayed to
* the customer in a drop-down field. This will be all currencies for which a Braintree merchant
* account exists. A currency will even be displayed if no TLD is enabled on the customer
* account that bills in that currency.
* </dl>
*
* <p><b>Note:</b> These definitions corresponds to Closure Compiler extern
* {@code registry.rpc.PaymentSetup} which must be updated should these definitions change.
*
* @see RegistrarPaymentAction
* @see "https://developers.braintreepayments.com/start/hello-server/java#generate-a-client-token"
*/
@Action(
path = "/registrar-payment-setup",
method = Action.Method.POST,
xsrfProtection = true,
xsrfScope = "console",
requireLogin = true)
public final class RegistrarPaymentSetupAction implements Runnable, JsonAction {
@Inject BraintreeGateway braintreeGateway;
@Inject JsonActionRunner jsonActionRunner;
@Inject RegistryEnvironment environment;
@Inject @Config("brainframe") String brainframe;
@Inject @Config("braintreeMerchantAccountIds") ImmutableMap<CurrencyUnit, String> accountIds;
@Inject RegistrarPaymentSetupAction() {}
@Override
public void run() {
jsonActionRunner.run(this);
}
@Override
public Map<String, Object> handleJsonRequest(Map<String, ?> json) {
if (!json.isEmpty()) {
return JsonResponseHelper.create(ERROR, "JSON request object must be empty");
}
// Prevent registrar customers from accidentally remitting payment via OT&E environment.
if (environment == RegistryEnvironment.SANDBOX) {
// XXX: payment.js is hard-coded to display a specific SOY error template when encountering
// an error message with this specific value.
return JsonResponseHelper.create(ERROR, "sandbox");
}
return JsonResponseHelper
.create(SUCCESS, "Success", asList(
ImmutableMap.of(
"brainframe", brainframe,
"token", braintreeGateway.clientToken().generate(),
"currencies",
FluentIterable.from(accountIds.keySet())
.transform(toStringFunction())
.toList())));
}
}

View file

@ -0,0 +1,261 @@
// Copyright 2016 Google Inc. 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 com.google.domain.registry.ui.server.registrar;
import static com.google.common.collect.Iterables.any;
import static com.google.common.collect.Iterables.concat;
import static com.google.common.collect.Sets.difference;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
import static com.google.domain.registry.security.JsonResponseHelper.Status.SUCCESS;
import com.google.common.base.Function;
import com.google.common.base.Predicate;
import com.google.common.collect.FluentIterable;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import com.google.domain.registry.config.RegistryConfig;
import com.google.domain.registry.config.RegistryEnvironment;
import com.google.domain.registry.export.sheet.SyncRegistrarsSheetTask;
import com.google.domain.registry.model.registrar.Registrar;
import com.google.domain.registry.model.registrar.RegistrarContact;
import com.google.domain.registry.security.JsonResponseHelper;
import com.google.domain.registry.ui.forms.FormException;
import com.google.domain.registry.ui.server.RegistrarFormFields;
import com.google.domain.registry.util.CidrAddressBlock;
import com.google.domain.registry.util.CollectionUtils;
import com.google.domain.registry.util.DiffUtils;
import com.google.domain.registry.util.SendEmailUtils;
import com.googlecode.objectify.Work;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
/**
* Admin servlet that allows creating or updating a registrar. Deletes are not allowed so as to
* preserve history.
*/
public class RegistrarServlet extends ResourceServlet {
private static final RegistryConfig CONFIG = RegistryEnvironment.get().config();
private static final Predicate<RegistrarContact> HAS_PHONE = new Predicate<RegistrarContact>() {
@Override
public boolean apply(RegistrarContact contact) {
return contact.getPhoneNumber() != null;
}};
/** Thrown when a set of contacts doesn't meet certain constraints. */
private static class ContactRequirementException extends FormException {
ContactRequirementException(String msg) {
super(msg);
}
ContactRequirementException(RegistrarContact.Type type) {
super("Must have at least one " + type.getDisplayName() + " contact");
}
}
@Override
public Map<String, Object> read(HttpServletRequest req, Map<String, ?> args) {
String clientId = sessionUtils.getRegistrarClientId(req);
Registrar registrar = getCheckedRegistrar(clientId);
return JsonResponseHelper.create(SUCCESS, "Success", registrar.toJsonMap());
}
@Override
Map<String, Object> update(HttpServletRequest req, final Map<String, ?> args) {
final String clientId = sessionUtils.getRegistrarClientId(req);
return ofy().transact(new Work<Map<String, Object>>() {
@Override
public Map<String, Object> run() {
Registrar existingRegistrar = getCheckedRegistrar(clientId);
ImmutableSet<RegistrarContact> oldContacts = existingRegistrar.getContacts();
Map<String, Object> existingRegistrarMap =
expandRegistrarWithContacts(existingRegistrar, oldContacts);
Registrar.Builder builder = existingRegistrar.asBuilder();
ImmutableSet<RegistrarContact> updatedContacts = update(existingRegistrar, builder, args);
if (!updatedContacts.isEmpty()) {
builder.setContactsRequireSyncing(true);
}
Registrar updatedRegistrar = builder.build();
ofy().save().entity(updatedRegistrar);
if (!updatedContacts.isEmpty()) {
checkContactRequirements(oldContacts, updatedContacts);
RegistrarContact.updateContacts(updatedRegistrar, updatedContacts);
}
// Update the registrar map with updated contacts to bypass Objectify caching issues that
// come into play with calling getContacts().
Map<String, Object> updatedRegistrarMap =
expandRegistrarWithContacts(updatedRegistrar, updatedContacts);
sendExternalUpdatesIfNecessary(
updatedRegistrar.getRegistrarName(),
existingRegistrarMap,
updatedRegistrarMap);
return JsonResponseHelper.create(
SUCCESS,
"Saved " + clientId,
updatedRegistrar.toJsonMap());
}});
}
private Map<String, Object> expandRegistrarWithContacts(
Registrar registrar, Iterable<RegistrarContact> contacts) {
ImmutableSet<Map<String, Object>> expandedContacts = FluentIterable.from(contacts)
.transform(new Function<RegistrarContact, Map<String, Object>>() {
@Override
public Map<String, Object> apply(RegistrarContact contact) {
return contact.toDiffableFieldMap();
}})
.toSet();
// Use LinkedHashMap here to preserve ordering; null values mean we can't use ImmutableMap.
LinkedHashMap<String, Object> result = new LinkedHashMap<>();
result.putAll(registrar.toDiffableFieldMap());
result.put("contacts", expandedContacts);
return result;
}
/**
* 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
* enqueues a task to re-sync the registrar sheet.
*/
private void sendExternalUpdatesIfNecessary(
String registrarName,
Map<String, Object> existingRegistrar,
Map<String, Object> updatedRegistrar) {
Map<?, ?> diffs = DiffUtils.deepDiff(existingRegistrar, updatedRegistrar);
@SuppressWarnings("unchecked")
Set<String> changedKeys = (Set<String>) diffs.keySet();
if (CollectionUtils.difference(changedKeys, "lastUpdateTime").isEmpty()) {
return;
}
SyncRegistrarsSheetTask.enqueueBackendTask();
ImmutableList<String> toEmailAddress = CONFIG.getRegistrarChangesNotificationEmailAddresses();
if (!toEmailAddress.isEmpty()) {
SendEmailUtils.sendEmail(
toEmailAddress,
String.format("Registrar %s updated", registrarName),
"The following changes were made to the registrar:\n"
+ DiffUtils.prettyPrintDiffedMap(diffs, null));
}
}
private Registrar getCheckedRegistrar(String clientId) {
return checkExists(Registrar.loadByClientId(clientId),
"No registrar exists with the given client id: " + clientId);
}
/**
* Enforces business logic checks on registrar contacts.
*
* @throws FormException if the checks fail.
*/
void checkContactRequirements(
Set<RegistrarContact> existingContacts, Set<RegistrarContact> updatedContacts) {
// Check that no two contacts use the same email address.
Set<String> emails = new HashSet<>();
for (RegistrarContact contact : updatedContacts) {
if (!emails.add(contact.getEmailAddress())) {
throw new ContactRequirementException(String.format(
"One email address (%s) cannot be used for multiple contacts",
contact.getEmailAddress()));
}
}
// Check that required contacts don't go away, once they are set.
Multimap<RegistrarContact.Type, RegistrarContact> oldContactsByType = HashMultimap.create();
for (RegistrarContact contact : existingContacts) {
for (RegistrarContact.Type t : contact.getTypes()) {
oldContactsByType.put(t, contact);
}
}
Multimap<RegistrarContact.Type, RegistrarContact> newContactsByType = HashMultimap.create();
for (RegistrarContact contact : updatedContacts) {
for (RegistrarContact.Type t : contact.getTypes()) {
newContactsByType.put(t, contact);
}
}
for (RegistrarContact.Type t
: difference(oldContactsByType.keySet(), newContactsByType.keySet())) {
if (t.isRequired()) {
throw new ContactRequirementException(t);
}
}
// Ensure at least one tech contact has a phone number if one was present before.
if (any(oldContactsByType.get(RegistrarContact.Type.TECH), HAS_PHONE)
&& !any(newContactsByType.get(RegistrarContact.Type.TECH), HAS_PHONE)) {
throw new ContactRequirementException(String.format(
"At least one %s contact must have a phone number",
RegistrarContact.Type.TECH.getDisplayName()));
}
}
/**
* Updates a registrar builder with the supplied args from the http request, and returns a list of
* the new registrar contacts.
*/
public static ImmutableSet<RegistrarContact> update(
Registrar existingRegistrarObj, Registrar.Builder builder, Map<String, ?> args) {
// WHOIS
builder.setWhoisServer(
RegistrarFormFields.WHOIS_SERVER_FIELD.extractUntyped(args).orNull());
builder.setReferralUrl(
RegistrarFormFields.REFERRAL_URL_FIELD.extractUntyped(args).orNull());
for (String email :
RegistrarFormFields.EMAIL_ADDRESS_FIELD.extractUntyped(args).asSet()) {
builder.setEmailAddress(email);
}
builder.setPhoneNumber(
RegistrarFormFields.PHONE_NUMBER_FIELD.extractUntyped(args).orNull());
builder.setFaxNumber(
RegistrarFormFields.FAX_NUMBER_FIELD.extractUntyped(args).orNull());
builder.setLocalizedAddress(
RegistrarFormFields.L10N_ADDRESS_FIELD.extractUntyped(args).orNull());
// Security
builder.setIpAddressWhitelist(
RegistrarFormFields.IP_ADDRESS_WHITELIST_FIELD.extractUntyped(args).or(
ImmutableList.<CidrAddressBlock>of()));
for (String certificate
: RegistrarFormFields.CLIENT_CERTIFICATE_FIELD.extractUntyped(args).asSet()) {
builder.setClientCertificate(certificate, ofy().getTransactionTime());
}
for (String certificate
: RegistrarFormFields.FAILOVER_CLIENT_CERTIFICATE_FIELD.extractUntyped(args).asSet()) {
builder.setFailoverClientCertificate(certificate, ofy().getTransactionTime());
}
builder.setUrl(
RegistrarFormFields.URL_FIELD.extractUntyped(args).orNull());
builder.setReferralUrl(
RegistrarFormFields.REFERRAL_URL_FIELD.extractUntyped(args).orNull());
// Contact
ImmutableSet.Builder<RegistrarContact> contacts = new ImmutableSet.Builder<>();
for (RegistrarContact.Builder contactBuilder
: concat(RegistrarFormFields.CONTACTS_FIELD.extractUntyped(args).asSet())) {
contacts.add(contactBuilder.setParent(existingRegistrarObj).build());
}
return contacts.build();
}
}

View file

@ -0,0 +1,107 @@
// Copyright 2016 Google Inc. 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 com.google.domain.registry.ui.server.registrar;
import static com.google.appengine.api.users.UserServiceFactory.getUserService;
import static com.google.domain.registry.flows.EppConsoleServlet.XSRF_SCOPE;
import static com.google.domain.registry.security.JsonResponseHelper.Status.ERROR;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableMap;
import com.google.domain.registry.request.HttpException.NotFoundException;
import com.google.domain.registry.security.JsonResponseHelper;
import com.google.domain.registry.security.JsonTransportServlet;
import com.google.domain.registry.ui.forms.FormException;
import com.google.domain.registry.ui.forms.FormFieldException;
import com.google.domain.registry.util.NonFinalForTesting;
import java.util.Map;
import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest;
/** A servlet for callbacks that manipulate resources. */
public abstract class ResourceServlet extends JsonTransportServlet {
private static final String OP_PARAM = "op";
private static final String ARGS_PARAM = "args";
@NonFinalForTesting
protected static SessionUtils sessionUtils = new SessionUtils(getUserService());
public ResourceServlet() {
super(XSRF_SCOPE, false);
}
@Override
public Map<String, Object> doJsonPost(HttpServletRequest req, Map<String, ?> params) {
if (!sessionUtils.isLoggedIn()) {
return JsonResponseHelper.create(ERROR, "Not logged in");
}
if (!sessionUtils.checkRegistrarConsoleLogin(req)) {
return JsonResponseHelper.create(ERROR, "Not authorized to access Registrar Console");
}
String op = Optional.fromNullable((String) params.get(OP_PARAM)).or("read");
@SuppressWarnings("unchecked")
Map<String, ?> args = (Map<String, Object>)
Optional.<Object>fromNullable(params.get(ARGS_PARAM)).or(ImmutableMap.of());
try {
switch (op) {
case "create":
return create(req, args);
case "update":
return update(req, args);
case "delete":
return delete(req, args);
case "read":
return read(req, args);
default:
return JsonResponseHelper.create(ERROR, "Unknown operation: " + op);
}
} catch (FormFieldException e) {
return JsonResponseHelper.createFormFieldError(e.getMessage(), e.getFieldName());
} catch (FormException ee) {
return JsonResponseHelper.create(ERROR, ee.getMessage());
}
}
@SuppressWarnings("unused")
Map<String, Object> create(HttpServletRequest req, Map<String, ?> args) {
throw new UnsupportedOperationException();
}
@SuppressWarnings("unused")
Map<String, Object> read(HttpServletRequest req, Map<String, ?> args) {
throw new UnsupportedOperationException();
}
@SuppressWarnings("unused")
Map<String, Object> update(HttpServletRequest req, Map<String, ?> args) {
throw new UnsupportedOperationException();
}
@SuppressWarnings("unused")
Map<String, Object> delete(HttpServletRequest req, Map<String, ?> args) {
throw new UnsupportedOperationException();
}
/** Like checkNotNull, but throws NotFoundException if given arg is null. */
protected static <T> T checkExists(@Nullable T obj, String msg) {
if (obj == null) {
throw new NotFoundException(msg);
}
return obj;
}
}

View file

@ -0,0 +1,164 @@
// Copyright 2016 Google Inc. 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 com.google.domain.registry.ui.server.registrar;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Verify.verify;
import static com.google.common.net.HttpHeaders.LOCATION;
import static com.google.domain.registry.model.ofy.ObjectifyService.ofy;
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.base.Optional;
import com.google.common.base.Predicate;
import com.google.common.collect.FluentIterable;
import com.google.domain.registry.model.registrar.Registrar;
import com.google.domain.registry.model.registrar.RegistrarContact;
import com.google.domain.registry.util.FormattingLogger;
import javax.annotation.CheckReturnValue;
import javax.annotation.Nonnull;
import javax.annotation.concurrent.Immutable;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
/** HTTP session management helper class. */
@Immutable
public class SessionUtils {
private static final FormattingLogger logger = FormattingLogger.getLoggerForCallerClass();
public static final String CLIENT_ID_ATTRIBUTE = "clientId";
private final UserService userService;
public SessionUtils(UserService userService) {
this.userService = checkNotNull(userService);
}
/**
* Redirects client to login URL if they aren't authenticated with the App Engine user service.
*
* @return {@code false} if request should abort.
*/
@CheckReturnValue
public boolean redirectIfNotLoggedIn(HttpServletRequest req, HttpServletResponse rsp) {
if (!isLoggedIn()) {
logger.info("User not logged in to App Engine UserService.");
rsp.setStatus(SC_MOVED_TEMPORARILY);
rsp.setHeader(LOCATION, userService.createLoginURL(req.getRequestURI()));
return false;
}
return true;
}
/**
* Checks GAE user has access to Registrar Console.
*
* <p>This routine will first check the HTTP session (creating one if it doesn't exist) for the
* {@code clientId} attribute:
*
* <ul>
* <li>If it does not exist, then we will attempt to guess the {@link Registrar} with which the
* user's GAIA ID is associated. The {@code clientId} of the first matching {@code Registrar} will
* then be stored to the the HTTP session.
* <li>If it does exist, then we'll fetch the Registrar from the datastore to make sure access
* wasn't revoked. This should only cost one memcache read.
* </ul>
*
* <p><b>Note:</b> You should call {@link #redirectIfNotLoggedIn} before calling this method.
*
* @return {@code false} if user does not have access, in which case the caller should write an
* error response and abort the request.
*/
@CheckReturnValue
public boolean checkRegistrarConsoleLogin(HttpServletRequest req) {
HttpSession session = req.getSession();
User user = userService.getCurrentUser();
checkState(user != null, "You forgot to call redirectIfNotLoggedIn()");
String clientId = (String) session.getAttribute(CLIENT_ID_ATTRIBUTE);
if (clientId == null) {
Optional<Registrar> registrar = guessRegistrar(user.getUserId());
if (!registrar.isPresent()) {
logger.infofmt("User not associated with any Registrar: %s (%s)",
user.getUserId(), user.getEmail());
return false;
}
verify(hasAccessToRegistrar(registrar.get(), user.getUserId()));
session.setAttribute(CLIENT_ID_ATTRIBUTE, registrar.get().getClientIdentifier());
} else {
if (!hasAccessToRegistrar(clientId, user.getUserId())) {
logger.infofmt("Registrar Console access revoked: %s for %s (%s)",
clientId, user.getEmail(), user.getUserId());
session.invalidate();
return false;
}
}
return true;
}
/**
* Returns {@link Registrar} clientId associated with HTTP session.
*
* @throws IllegalStateException if you forgot to call {@link #checkRegistrarConsoleLogin}.
*/
@CheckReturnValue
public String getRegistrarClientId(HttpServletRequest req) {
String clientId = (String) req.getSession().getAttribute(CLIENT_ID_ATTRIBUTE);
checkState(clientId != null, "You forgot to call checkRegistrarConsoleLogin()");
return clientId;
}
/** @see UserService#isUserLoggedIn() */
public boolean isLoggedIn() {
return userService.isUserLoggedIn();
}
/** Returns first {@link Registrar} that {@code gaeUserId} is authorized to administer. */
private static Optional<Registrar> guessRegistrar(String gaeUserId) {
RegistrarContact contact = ofy().load()
.type(RegistrarContact.class)
.filter("gaeUserId", gaeUserId)
.first().now();
if (contact == null) {
return Optional.absent();
}
return Optional.of(ofy().load().key(contact.getParent()).safe());
}
/** @see #hasAccessToRegistrar(Registrar, String) */
private static boolean hasAccessToRegistrar(String clientId, final String gaeUserId) {
Registrar registrar = Registrar.loadByClientId(clientId);
if (registrar == null) {
logger.warningfmt("Registrar '%s' disappeared from the datastore!", clientId);
return false;
}
return hasAccessToRegistrar(registrar, gaeUserId);
}
/** Returns {@code true} if {@code gaeUserId} is listed in contacts. */
private static boolean hasAccessToRegistrar(Registrar registrar, final String gaeUserId) {
return FluentIterable
.from(registrar.getContacts())
.anyMatch(new Predicate<RegistrarContact>() {
@Override
public boolean apply(@Nonnull RegistrarContact contact) {
return gaeUserId.equals(contact.getGaeUserId());
}});
}
}

View file

@ -0,0 +1,16 @@
// Copyright 2016 Google Inc. 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.
@javax.annotation.ParametersAreNonnullByDefault
package com.google.domain.registry.ui.server.registrar;