Convert RegistrarServlet to RegistrarAction

Convert to an action and remove ResourceServlet, JsonTransportServlet and
JsonTransportServlet, all of which exist only to support it.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=137519385
This commit is contained in:
mmuller 2016-10-28 09:32:24 -07:00 committed by Ben McIlwain
parent 2e4273c4f4
commit bbd20ec71d
24 changed files with 330 additions and 858 deletions

View file

@ -17,6 +17,7 @@ package google.registry.config;
import static google.registry.config.ConfigUtils.makeUrl;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import dagger.Module;
import dagger.Provides;
@ -325,6 +326,13 @@ public final class ConfigModule {
}
}
@Provides
@Config("registrarChangesNotificationEmailAddresses")
public static ImmutableList<String> provideRegistrarChangesNotificationEmailAddresses(
RegistryConfig config) {
return config.getRegistrarChangesNotificationEmailAddresses();
}
/**
* Returns the publicly accessible domain name for the running Google Apps instance.
*

View file

@ -163,7 +163,7 @@ public interface RegistryConfig {
* Returns the email address(es) that notifications of registrar and/or registrar contact updates
* should be sent to, or the empty list if updates should not be sent.
*
* @see google.registry.ui.server.registrar.RegistrarServlet
* @see google.registry.ui.server.registrar.RegistrarAction
*/
public ImmutableList<String> getRegistrarChangesNotificationEmailAddresses();

View file

@ -25,17 +25,6 @@
<url-pattern>/registrar-xhr</url-pattern>
</servlet-mapping>
<servlet>
<display-name>Registrar Self-serve Settings</display-name>
<servlet-name>registrar-settings</servlet-name>
<servlet-class>google.registry.ui.server.registrar.RegistrarServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>registrar-settings</servlet-name>
<url-pattern>/registrar-settings</url-pattern>
</servlet-mapping>
<!-- Registrar Console. -->
<servlet-mapping>
<servlet-name>frontend-servlet</servlet-name>
@ -54,6 +43,12 @@
<url-pattern>/registrar-payment</url-pattern>
</servlet-mapping>
<!-- Registrar Self-serve Settings. -->
<servlet-mapping>
<servlet-name>frontend-servlet</servlet-name>
<url-pattern>/registrar-settings</url-pattern>
</servlet-mapping>
<!-- HTTP WHOIS. -->
<servlet-mapping>
<servlet-name>frontend-servlet</servlet-name>

View file

@ -36,6 +36,7 @@ import google.registry.rdap.RdapNameserverSearchAction;
import google.registry.request.RequestModule;
import google.registry.request.RequestScope;
import google.registry.ui.server.registrar.ConsoleUiAction;
import google.registry.ui.server.registrar.RegistrarAction;
import google.registry.ui.server.registrar.RegistrarPaymentAction;
import google.registry.ui.server.registrar.RegistrarPaymentSetupAction;
import google.registry.ui.server.registrar.RegistrarUserModule;
@ -65,6 +66,7 @@ interface FrontendRequestComponent {
RdapAutnumAction rdapAutnumAction();
RegistrarPaymentAction registrarPaymentAction();
RegistrarPaymentSetupAction registrarPaymentSetupAction();
RegistrarAction registrarAction();
RdapDomainAction rdapDomainAction();
RdapDomainSearchAction rdapDomainSearchAction();
RdapEntityAction rdapEntityAction();

View file

@ -35,9 +35,13 @@ public final class JsonActionRunner {
Map<String, ?> handleJsonRequest(Map<String, ?> json);
}
@Inject @JsonPayload Map<String, Object> payload;
@Inject JsonResponse response;
@Inject JsonActionRunner() {}
@JsonPayload Map<String, Object> payload;
JsonResponse response;
@Inject public JsonActionRunner(@JsonPayload Map<String, Object> payload, JsonResponse response) {
this.payload = payload;
this.response = response;
}
/** Delegates request to {@code action}. */
public void run(JsonAction action) {

View file

@ -28,16 +28,3 @@ java_library(
"//java/google/registry/util",
],
)
java_library(
name = "servlets",
srcs = glob(["*Servlet.java"]),
deps = [
":security",
"//java/com/google/common/base",
"//third_party/java/appengine:appengine-api",
"//third_party/java/joda_time",
"//third_party/java/servlet/servlet_api",
"//java/google/registry/request",
],
)

View file

@ -1,74 +0,0 @@
// Copyright 2016 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.security;
import static com.google.common.base.Preconditions.checkNotNull;
import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
import static javax.servlet.http.HttpServletResponse.SC_OK;
import google.registry.request.HttpException;
import java.io.IOException;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* Secure servlet that speaks JSON for both input and output.
*
* <p>This servlet accepts only JSON inputs (using the payload) and returns only JSON
* responses, using and various security best practices such as a parser breaker,
* {@code Content-Disposition: attachment}, etc.
*
* @see JsonHttp
*/
public abstract class JsonTransportServlet extends XsrfProtectedServlet {
protected JsonTransportServlet(String xsrfScope, boolean requireAdmin) {
super(xsrfScope, requireAdmin);
}
/**
* Verify that this is a well-formed request and then execute it. A well-formed request will have
* either a JSON string in the "json" param that evaluates to a map, or nothing in "json".
*/
@Override
protected final void doPost(HttpServletRequest req, HttpServletResponse rsp) throws IOException {
Map<String, ?> input = JsonHttp.read(req);
if (input == null) {
rsp.sendError(SC_BAD_REQUEST, "Malformed JSON");
return;
}
Map<String, ?> output;
try {
output = doJsonPost(req, input);
} catch (HttpException e) {
e.send(rsp);
return;
}
checkNotNull(output, "doJsonPost() returned null");
rsp.setStatus(SC_OK);
JsonHttp.write(rsp, output);
}
/**
* Handler for HTTP POST requests.
*
* @param req Servlet request object.
* @param input JSON request object or empty if none was provided.
* @return an arbitrary JSON object. Must not be {@code null}.
* @throws HttpException in order to send a non-200 status code / message to the client.
*/
public abstract Map<String, Object> doJsonPost(HttpServletRequest req, Map<String, ?> input);
}

View file

@ -1,83 +0,0 @@
// Copyright 2016 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.security;
import static com.google.appengine.api.users.UserServiceFactory.getUserService;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.nullToEmpty;
import static google.registry.security.XsrfTokenManager.X_CSRF_TOKEN;
import static google.registry.security.XsrfTokenManager.validateToken;
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import com.google.appengine.api.users.UserService;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.joda.time.Duration;
/**
* Servlet with Cross-Site Request Forgery (XSRF) protection.
*
* <p>This servlet enforces XSRF protection on all requests by checking the value provided in the
* "X-CSRF-Token" header. It can also optionally enforce that only admin users can call it.
*
* <p>All servlets that handle client requests should use XSRF protection.
*/
public abstract class XsrfProtectedServlet extends HttpServlet {
private static final Duration XSRF_VALIDITY = Duration.standardDays(1);
/** Used to validate XSRF tokens. */
private String xsrfScope;
/** Whether to do a security check for admin status. */
private boolean requireAdmin;
/** Gets the XSRF scope for this servlet. */
public String getScope() {
return xsrfScope;
}
protected XsrfProtectedServlet(String xsrfScope, boolean requireAdmin) {
this.xsrfScope = checkNotNull(xsrfScope);
this.requireAdmin = requireAdmin;
}
@Override
public final void service(HttpServletRequest req, HttpServletResponse rsp)
throws IOException, ServletException {
if (!validateToken(nullToEmpty(req.getHeader(X_CSRF_TOKEN)), xsrfScope, XSRF_VALIDITY)) {
rsp.sendError(SC_FORBIDDEN, "Invalid " + X_CSRF_TOKEN);
return;
}
if (!validateAdmin()) {
rsp.sendError(SC_FORBIDDEN, "Administrator access only");
return;
}
doPost(req, rsp);
}
/**
* If this is an admin-only servlet, require admin permissions or being in development mode. Such
* servlets should primarily be defended by being marked internal-only in web.xml, but it's worth
* adding a defense-in-depth.
*/
private boolean validateAdmin() {
UserService userService = getUserService();
return requireAdmin ? (userService.isUserLoggedIn() && userService.isUserAdmin()) : true;
}
}

View file

@ -26,7 +26,6 @@ java_library(
"//java/google/registry/model",
"//java/google/registry/request",
"//java/google/registry/security",
"//java/google/registry/security:servlets",
"//java/google/registry/ui/forms",
"//java/google/registry/ui/server",
"//java/google/registry/ui/soy:soy_java_wrappers",

View file

@ -18,23 +18,29 @@ 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 google.registry.model.ofy.ObjectifyService.ofy;
import static google.registry.security.JsonResponseHelper.Status.ERROR;
import static google.registry.security.JsonResponseHelper.Status.SUCCESS;
import com.google.common.base.Function;
import com.google.common.base.Optional;
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.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Multimap;
import com.googlecode.objectify.Work;
import google.registry.config.RegistryConfig;
import google.registry.config.RegistryEnvironment;
import google.registry.config.ConfigModule.Config;
import google.registry.export.sheet.SyncRegistrarsSheetAction;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarContact;
import google.registry.request.Action;
import google.registry.request.HttpException.BadRequestException;
import google.registry.request.JsonActionRunner;
import google.registry.security.JsonResponseHelper;
import google.registry.ui.forms.FormException;
import google.registry.ui.forms.FormFieldException;
import google.registry.ui.server.RegistrarFormFields;
import google.registry.util.CidrAddressBlock;
import google.registry.util.CollectionUtils;
@ -44,15 +50,33 @@ import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import javax.inject.Inject;
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 {
@Action(
path = RegistrarAction.PATH,
requireLogin = true,
xsrfProtection = true,
xsrfScope = "console",
method = Action.Method.POST)
public class RegistrarAction implements Runnable, JsonActionRunner.JsonAction {
private static final RegistryConfig CONFIG = RegistryEnvironment.get().config();
public static final String PATH = "/registrar-settings";
private static final String OP_PARAM = "op";
private static final String ARGS_PARAM = "args";
@Inject HttpServletRequest request;
@Inject SessionUtils sessionUtils;
@Inject JsonActionRunner jsonActionRunner;
@Inject Registrar initialRegistrar;
@Inject @Config("registrarChangesNotificationEmailAddresses") ImmutableList<String>
registrarChangesNotificationEmailAddresses;
@Inject RegistrarAction() {}
private static final Predicate<RegistrarContact> HAS_PHONE = new Predicate<RegistrarContact>() {
@Override
@ -71,25 +95,20 @@ public class RegistrarServlet extends ResourceServlet {
}
}
@Override
public Map<String, Object> read(HttpServletRequest req, Map<String, ?> args) {
String clientId = sessionUtils.getRegistrarClientId(req);
Registrar registrar = getCheckedRegistrar(clientId);
public Map<String, Object> read(Map<String, ?> args, Registrar registrar) {
return JsonResponseHelper.create(SUCCESS, "Success", registrar.toJsonMap());
}
@Override
Map<String, Object> update(HttpServletRequest req, final Map<String, ?> args) {
final String clientId = sessionUtils.getRegistrarClientId(req);
Map<String, Object> update(final Map<String, ?> args, final Registrar registrar) {
final String clientId = sessionUtils.getRegistrarClientId(request);
return ofy().transact(new Work<Map<String, Object>>() {
@Override
public Map<String, Object> run() {
Registrar existingRegistrar = getCheckedRegistrar(clientId);
ImmutableSet<RegistrarContact> oldContacts = existingRegistrar.getContacts();
ImmutableSet<RegistrarContact> oldContacts = registrar.getContacts();
Map<String, Object> existingRegistrarMap =
expandRegistrarWithContacts(existingRegistrar, oldContacts);
Registrar.Builder builder = existingRegistrar.asBuilder();
ImmutableSet<RegistrarContact> updatedContacts = update(existingRegistrar, builder, args);
expandRegistrarWithContacts(oldContacts, registrar);
Registrar.Builder builder = registrar.asBuilder();
ImmutableSet<RegistrarContact> updatedContacts = update(registrar, builder, args);
if (!updatedContacts.isEmpty()) {
builder.setContactsRequireSyncing(true);
}
@ -102,7 +121,7 @@ public class RegistrarServlet extends ResourceServlet {
// 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);
expandRegistrarWithContacts(updatedContacts, updatedRegistrar);
sendExternalUpdatesIfNecessary(
updatedRegistrar.getRegistrarName(),
existingRegistrarMap,
@ -114,8 +133,8 @@ public class RegistrarServlet extends ResourceServlet {
}});
}
private Map<String, Object> expandRegistrarWithContacts(
Registrar registrar, Iterable<RegistrarContact> contacts) {
private Map<String, Object> expandRegistrarWithContacts(Iterable<RegistrarContact> contacts,
Registrar registrar) {
ImmutableSet<Map<String, Object>> expandedContacts = FluentIterable.from(contacts)
.transform(new Function<RegistrarContact, Map<String, Object>>() {
@Override
@ -146,21 +165,15 @@ public class RegistrarServlet extends ResourceServlet {
return;
}
SyncRegistrarsSheetAction.enqueueBackendTask();
ImmutableList<String> toEmailAddress = CONFIG.getRegistrarChangesNotificationEmailAddresses();
if (!toEmailAddress.isEmpty()) {
if (!registrarChangesNotificationEmailAddresses.isEmpty()) {
SendEmailUtils.sendEmail(
toEmailAddress,
registrarChangesNotificationEmailAddresses,
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.
*
@ -255,4 +268,42 @@ public class RegistrarServlet extends ResourceServlet {
return contacts.build();
}
@Override
public Map<String, Object> handleJsonRequest(Map<String, ?> input) {
if (input == null) {
throw new BadRequestException("Malformed JSON");
}
if (!sessionUtils.checkRegistrarConsoleLogin(request)) {
return JsonResponseHelper.create(ERROR, "Not authorized to access Registrar Console");
}
// Process the operation. Though originally derived from a CRUD
// handlder, registrar-settings really only supports read and update.
String op = Optional.fromNullable((String) input.get(OP_PARAM)).or("read");
@SuppressWarnings("unchecked")
Map<String, ?> args = (Map<String, Object>)
Optional.<Object>fromNullable(input.get(ARGS_PARAM)).or(ImmutableMap.of());
try {
switch (op) {
case "update":
return update(args, initialRegistrar);
case "read":
return read(args, initialRegistrar);
default:
return JsonResponseHelper.create(ERROR, "Unknown or unsupported operation: " + op);
}
} catch (FormFieldException e) {
return JsonResponseHelper.createFormFieldError(e.getMessage(), e.getFieldName());
} catch (FormException ee) {
return JsonResponseHelper.create(ERROR, ee.getMessage());
}
}
@Override
public void run() {
jsonActionRunner.run(this);
}
}

View file

@ -1,105 +0,0 @@
// Copyright 2016 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.ui.server.registrar;
import static com.google.appengine.api.users.UserServiceFactory.getUserService;
import static google.registry.flows.EppConsoleAction.XSRF_SCOPE;
import static google.registry.security.JsonResponseHelper.Status.ERROR;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableMap;
import google.registry.request.HttpException.NotFoundException;
import google.registry.security.JsonResponseHelper;
import google.registry.security.JsonTransportServlet;
import google.registry.ui.forms.FormException;
import google.registry.ui.forms.FormFieldException;
import google.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;
}
}