diff --git a/core/src/main/java/google/registry/ui/server/SoyTemplateUtils.java b/core/src/main/java/google/registry/ui/server/SoyTemplateUtils.java index 612054a74..a7ece3b08 100644 --- a/core/src/main/java/google/registry/ui/server/SoyTemplateUtils.java +++ b/core/src/main/java/google/registry/ui/server/SoyTemplateUtils.java @@ -19,6 +19,7 @@ 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.annotations.VisibleForTesting; import com.google.common.base.Joiner; import com.google.common.base.Splitter; import com.google.common.base.Supplier; @@ -42,6 +43,12 @@ import java.util.Map; /** Helper methods for rendering Soy templates from Java code. */ public final class SoyTemplateUtils { + @VisibleForTesting + public static final Supplier CSS_RENAMING_MAP_SUPPLIER = + SoyTemplateUtils.createCssRenamingMapSupplier( + Resources.getResource("google/registry/ui/css/registrar_bin.css.js"), + Resources.getResource("google/registry/ui/css/registrar_dbg.css.js")); + /** Returns a memoized supplier containing compiled tofu. */ public static Supplier createTofuSupplier(final SoyFileInfo... soyInfos) { return memoize( diff --git a/core/src/main/java/google/registry/ui/server/registrar/ConsoleOteSetupAction.java b/core/src/main/java/google/registry/ui/server/registrar/ConsoleOteSetupAction.java index 7d7542888..6510e1e98 100644 --- a/core/src/main/java/google/registry/ui/server/registrar/ConsoleOteSetupAction.java +++ b/core/src/main/java/google/registry/ui/server/registrar/ConsoleOteSetupAction.java @@ -15,45 +15,30 @@ package google.registry.ui.server.registrar; import static com.google.common.base.Preconditions.checkState; -import static com.google.common.net.HttpHeaders.LOCATION; -import static com.google.common.net.HttpHeaders.X_FRAME_OPTIONS; import static google.registry.config.RegistryEnvironment.PRODUCTION; +import static google.registry.ui.server.SoyTemplateUtils.CSS_RENAMING_MAP_SUPPLIER; import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; -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.annotations.VisibleForTesting; import com.google.common.base.Ascii; import com.google.common.base.Supplier; import com.google.common.collect.ImmutableMap; import com.google.common.flogger.FluentLogger; -import com.google.common.io.Resources; -import com.google.common.net.MediaType; -import com.google.template.soy.shared.SoyCssRenamingMap; import com.google.template.soy.tofu.SoyTofu; -import google.registry.config.RegistryConfig.Config; import google.registry.config.RegistryEnvironment; import google.registry.model.OteAccountBuilder; import google.registry.request.Action; import google.registry.request.Action.Method; import google.registry.request.Parameter; -import google.registry.request.RequestMethod; -import google.registry.request.Response; import google.registry.request.auth.Auth; -import google.registry.request.auth.AuthResult; import google.registry.request.auth.AuthenticatedRegistrarAccessor; -import google.registry.security.XsrfTokenManager; import google.registry.ui.server.SendEmailUtils; import google.registry.ui.server.SoyTemplateUtils; import google.registry.ui.soy.registrar.OteSetupConsoleSoyInfo; import google.registry.util.StringGenerator; import java.util.HashMap; -import java.util.Map; import java.util.Optional; import javax.inject.Inject; import javax.inject.Named; -import javax.servlet.http.HttpServletRequest; /** * Action that serves OT&E setup web page. @@ -69,14 +54,9 @@ import javax.servlet.http.HttpServletRequest; path = ConsoleOteSetupAction.PATH, method = {Method.POST, Method.GET}, auth = Auth.AUTH_PUBLIC) -public final class ConsoleOteSetupAction implements Runnable { +public final class ConsoleOteSetupAction extends HtmlAction { public static final String PATH = "/registrar-ote-setup"; - @VisibleForTesting // webdriver and screenshot tests need this - public static final Supplier CSS_RENAMING_MAP_SUPPLIER = - SoyTemplateUtils.createCssRenamingMapSupplier( - Resources.getResource("google/registry/ui/css/registrar_bin.css.js"), - Resources.getResource("google/registry/ui/css/registrar_dbg.css.js")); private static final int PASSWORD_LENGTH = 16; private static final FluentLogger logger = FluentLogger.forEnclosingClass(); private static final Supplier TOFU_SUPPLIER = @@ -85,27 +65,10 @@ public final class ConsoleOteSetupAction implements Runnable { google.registry.ui.soy.FormsSoyInfo.getInstance(), google.registry.ui.soy.AnalyticsSoyInfo.getInstance(), google.registry.ui.soy.registrar.OteSetupConsoleSoyInfo.getInstance()); - @Inject HttpServletRequest req; - @Inject @RequestMethod Method method; - @Inject Response response; + @Inject AuthenticatedRegistrarAccessor registrarAccessor; - @Inject UserService userService; - @Inject XsrfTokenManager xsrfTokenManager; - @Inject AuthResult authResult; @Inject SendEmailUtils sendEmailUtils; - @Inject - @Config("logoFilename") - String logoFilename; - - @Inject - @Config("productName") - String productName; - - @Inject - @Config("analyticsConfig") - Map analyticsConfig; - @Inject @Named("base58StringGenerator") StringGenerator passwordGenerator; @@ -126,43 +89,9 @@ public final class ConsoleOteSetupAction implements Runnable { ConsoleOteSetupAction() {} @Override - public void run() { - response.setHeader(X_FRAME_OPTIONS, "SAMEORIGIN"); // Disallow iframing. - response.setHeader("X-Ui-Compatible", "IE=edge"); // Ask IE not to be silly. - - logger.atInfo().log( - "User %s is accessing the OT&E setup page. Method= %s", - registrarAccessor.userIdForLogging(), method); + public void runAfterLogin(HashMap data) { checkState( !RegistryEnvironment.get().equals(PRODUCTION), "Can't create OT&E in prod"); - if (!authResult.userAuthInfo().isPresent()) { - response.setStatus(SC_MOVED_TEMPORARILY); - String location; - try { - location = userService.createLoginURL(req.getRequestURI()); - } catch (IllegalArgumentException e) { - // UserServiceImpl.createLoginURL() throws IllegalArgumentException if underlying API call - // returns an error code of NOT_ALLOWED. createLoginURL() assumes that the error is caused - // by an invalid URL. But in fact, the error can also occur if UserService doesn't have any - // user information, which happens when the request has been authenticated as internal. In - // this case, we want to avoid dying before we can send the redirect, so just redirect to - // the root path. - location = "/"; - } - response.setHeader(LOCATION, location); - return; - } - User user = authResult.userAuthInfo().get().user(); - - // Using HashMap to allow null values - HashMap data = new HashMap<>(); - data.put("logoFilename", logoFilename); - data.put("productName", productName); - data.put("username", user.getNickname()); - data.put("logoutUrl", userService.createLogoutURL(PATH)); - data.put("xsrfToken", xsrfTokenManager.generateToken(user.getEmail())); - data.put("analyticsConfig", analyticsConfig); - response.setContentType(MediaType.HTML_UTF_8); if (!registrarAccessor.isAdmin()) { response.setStatus(SC_FORBIDDEN); @@ -187,6 +116,11 @@ public final class ConsoleOteSetupAction implements Runnable { } } + @Override + public String getPath() { + return PATH; + } + private void runPost(HashMap data) { try { checkState(clientId.isPresent() && email.isPresent(), "Must supply clientId and email"); diff --git a/core/src/main/java/google/registry/ui/server/registrar/ConsoleRegistrarCreatorAction.java b/core/src/main/java/google/registry/ui/server/registrar/ConsoleRegistrarCreatorAction.java index 83098c812..e9397e276 100644 --- a/core/src/main/java/google/registry/ui/server/registrar/ConsoleRegistrarCreatorAction.java +++ b/core/src/main/java/google/registry/ui/server/registrar/ConsoleRegistrarCreatorAction.java @@ -18,27 +18,18 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import static com.google.common.collect.ImmutableList.toImmutableList; import static com.google.common.collect.ImmutableMap.toImmutableMap; -import static com.google.common.net.HttpHeaders.LOCATION; -import static com.google.common.net.HttpHeaders.X_FRAME_OPTIONS; import static google.registry.model.common.GaeUserIdConverter.convertEmailAddressToGaeUserId; import static google.registry.model.ofy.ObjectifyService.ofy; import static google.registry.model.transaction.TransactionManagerFactory.tm; +import static google.registry.ui.server.SoyTemplateUtils.CSS_RENAMING_MAP_SUPPLIER; import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; -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.annotations.VisibleForTesting; import com.google.common.base.Ascii; import com.google.common.base.Splitter; import com.google.common.base.Supplier; import com.google.common.collect.ImmutableMap; import com.google.common.flogger.FluentLogger; -import com.google.common.io.Resources; -import com.google.common.net.MediaType; -import com.google.template.soy.shared.SoyCssRenamingMap; import com.google.template.soy.tofu.SoyTofu; -import google.registry.config.RegistryConfig.Config; import google.registry.config.RegistryEnvironment; import google.registry.model.registrar.Registrar; import google.registry.model.registrar.RegistrarAddress; @@ -46,23 +37,17 @@ import google.registry.model.registrar.RegistrarContact; import google.registry.request.Action; import google.registry.request.Action.Method; import google.registry.request.Parameter; -import google.registry.request.RequestMethod; -import google.registry.request.Response; import google.registry.request.auth.Auth; -import google.registry.request.auth.AuthResult; import google.registry.request.auth.AuthenticatedRegistrarAccessor; -import google.registry.security.XsrfTokenManager; import google.registry.ui.server.SendEmailUtils; import google.registry.ui.server.SoyTemplateUtils; import google.registry.ui.soy.registrar.RegistrarCreateConsoleSoyInfo; import google.registry.util.StringGenerator; import java.util.HashMap; -import java.util.Map; import java.util.Optional; import java.util.stream.Stream; import javax.inject.Inject; import javax.inject.Named; -import javax.servlet.http.HttpServletRequest; import org.joda.money.CurrencyUnit; /** @@ -79,7 +64,7 @@ import org.joda.money.CurrencyUnit; path = ConsoleRegistrarCreatorAction.PATH, method = {Method.POST, Method.GET}, auth = Auth.AUTH_PUBLIC) -public final class ConsoleRegistrarCreatorAction implements Runnable { +public final class ConsoleRegistrarCreatorAction extends HtmlAction { private static final int PASSWORD_LENGTH = 16; private static final int PASSCODE_LENGTH = 5; @@ -95,23 +80,8 @@ public final class ConsoleRegistrarCreatorAction implements Runnable { google.registry.ui.soy.AnalyticsSoyInfo.getInstance(), google.registry.ui.soy.registrar.RegistrarCreateConsoleSoyInfo.getInstance()); - @VisibleForTesting // webdriver and screenshot tests need this - public static final Supplier CSS_RENAMING_MAP_SUPPLIER = - SoyTemplateUtils.createCssRenamingMapSupplier( - Resources.getResource("google/registry/ui/css/registrar_bin.css.js"), - Resources.getResource("google/registry/ui/css/registrar_dbg.css.js")); - - @Inject HttpServletRequest req; - @Inject @RequestMethod Method method; - @Inject Response response; @Inject AuthenticatedRegistrarAccessor registrarAccessor; - @Inject UserService userService; - @Inject XsrfTokenManager xsrfTokenManager; - @Inject AuthResult authResult; @Inject SendEmailUtils sendEmailUtils; - @Inject @Config("logoFilename") String logoFilename; - @Inject @Config("productName") String productName; - @Inject @Config("analyticsConfig") Map analyticsConfig; @Inject @Named("base58StringGenerator") StringGenerator passwordGenerator; @Inject @Named("digitOnlyStringGenerator") StringGenerator passcodeGenerator; @Inject @Parameter("clientId") Optional clientId; @@ -137,42 +107,7 @@ public final class ConsoleRegistrarCreatorAction implements Runnable { @Inject ConsoleRegistrarCreatorAction() {} @Override - public void run() { - response.setHeader(X_FRAME_OPTIONS, "SAMEORIGIN"); // Disallow iframing. - response.setHeader("X-Ui-Compatible", "IE=edge"); // Ask IE not to be silly. - - logger.atInfo().log( - "User %s is accessing the Registrar creation page. Method= %s", - registrarAccessor.userIdForLogging(), method); - if (!authResult.userAuthInfo().isPresent()) { - response.setStatus(SC_MOVED_TEMPORARILY); - String location; - try { - location = userService.createLoginURL(req.getRequestURI()); - } catch (IllegalArgumentException e) { - // UserServiceImpl.createLoginURL() throws IllegalArgumentException if underlying API call - // returns an error code of NOT_ALLOWED. createLoginURL() assumes that the error is caused - // by an invalid URL. But in fact, the error can also occur if UserService doesn't have any - // user information, which happens when the request has been authenticated as internal. In - // this case, we want to avoid dying before we can send the redirect, so just redirect to - // the root path. - location = "/"; - } - response.setHeader(LOCATION, location); - return; - } - User user = authResult.userAuthInfo().get().user(); - - // Using HashMap to allow null values - HashMap data = new HashMap<>(); - data.put("logoFilename", logoFilename); - data.put("productName", productName); - data.put("username", user.getNickname()); - data.put("logoutUrl", userService.createLogoutURL(PATH)); - data.put("xsrfToken", xsrfTokenManager.generateToken(user.getEmail())); - data.put("analyticsConfig", analyticsConfig); - response.setContentType(MediaType.HTML_UTF_8); - + public void runAfterLogin(HashMap data) { if (!registrarAccessor.isAdmin()) { response.setStatus(SC_FORBIDDEN); response.setPayload( @@ -196,6 +131,11 @@ public final class ConsoleRegistrarCreatorAction implements Runnable { } } + @Override + public String getPath() { + return PATH; + } + private void checkPresent(Optional value, String name) { checkState(value.isPresent(), "Missing value for %s", name); } @@ -226,7 +166,6 @@ public final class ConsoleRegistrarCreatorAction implements Runnable { private void runPost(HashMap data) { try { - checkPresent(clientId, "clientId"); checkPresent(name, "name"); checkPresent(billingAccount, "billingAccount"); @@ -321,11 +260,11 @@ public final class ConsoleRegistrarCreatorAction implements Runnable { data.put("errorMessage", e.getMessage()); response.setPayload( TOFU_SUPPLIER - .get() - .newRenderer(RegistrarCreateConsoleSoyInfo.FORM_PAGE) - .setCssRenamingMap(CSS_RENAMING_MAP_SUPPLIER.get()) - .setData(data) - .render()); + .get() + .newRenderer(RegistrarCreateConsoleSoyInfo.FORM_PAGE) + .setCssRenamingMap(CSS_RENAMING_MAP_SUPPLIER.get()) + .setData(data) + .render()); } } @@ -357,8 +296,8 @@ public final class ConsoleRegistrarCreatorAction implements Runnable { String environment = Ascii.toLowerCase(String.valueOf(RegistryEnvironment.get())); String body = String.format( - "The following registrar was created in %s by %s:\n", - environment, registrarAccessor.userIdForLogging()) + "The following registrar was created in %s by %s:\n", + environment, registrarAccessor.userIdForLogging()) + toEmailLine(clientId, "clientId") + toEmailLine(name, "name") + toEmailLine(billingAccount, "billingAccount") diff --git a/core/src/main/java/google/registry/ui/server/registrar/ConsoleUiAction.java b/core/src/main/java/google/registry/ui/server/registrar/ConsoleUiAction.java index 22f7604c3..688573507 100644 --- a/core/src/main/java/google/registry/ui/server/registrar/ConsoleUiAction.java +++ b/core/src/main/java/google/registry/ui/server/registrar/ConsoleUiAction.java @@ -14,47 +14,35 @@ package google.registry.ui.server.registrar; -import static com.google.common.net.HttpHeaders.LOCATION; -import static com.google.common.net.HttpHeaders.X_FRAME_OPTIONS; import static google.registry.request.auth.AuthenticatedRegistrarAccessor.Role.ADMIN; import static google.registry.request.auth.AuthenticatedRegistrarAccessor.Role.OWNER; +import static google.registry.ui.server.SoyTemplateUtils.CSS_RENAMING_MAP_SUPPLIER; import static google.registry.ui.server.registrar.RegistrarConsoleModule.PARAM_CLIENT_ID; import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; -import static javax.servlet.http.HttpServletResponse.SC_MOVED_TEMPORARILY; import static javax.servlet.http.HttpServletResponse.SC_SERVICE_UNAVAILABLE; -import com.google.appengine.api.users.User; -import com.google.appengine.api.users.UserService; -import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Supplier; import com.google.common.collect.ImmutableSetMultimap; import com.google.common.flogger.FluentLogger; -import com.google.common.io.Resources; -import com.google.common.net.MediaType; import com.google.template.soy.data.SoyMapData; -import com.google.template.soy.shared.SoyCssRenamingMap; import com.google.template.soy.tofu.SoyTofu; import google.registry.config.RegistryConfig.Config; import google.registry.config.RegistryEnvironment; import google.registry.request.Action; import google.registry.request.Parameter; -import google.registry.request.Response; import google.registry.request.auth.Auth; -import google.registry.request.auth.AuthResult; import google.registry.request.auth.AuthenticatedRegistrarAccessor; import google.registry.request.auth.AuthenticatedRegistrarAccessor.RegistrarAccessDeniedException; import google.registry.request.auth.AuthenticatedRegistrarAccessor.Role; -import google.registry.security.XsrfTokenManager; import google.registry.ui.server.SoyTemplateUtils; import google.registry.ui.soy.registrar.ConsoleSoyInfo; -import java.util.Map; +import java.util.HashMap; import java.util.Optional; import javax.inject.Inject; -import javax.servlet.http.HttpServletRequest; /** Action that serves Registrar Console single HTML page (SPA). */ @Action(service = Action.Service.DEFAULT, path = ConsoleUiAction.PATH, auth = Auth.AUTH_PUBLIC) -public final class ConsoleUiAction implements Runnable { +public final class ConsoleUiAction extends HtmlAction { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); @@ -66,27 +54,8 @@ public final class ConsoleUiAction implements Runnable { google.registry.ui.soy.registrar.ConsoleSoyInfo.getInstance(), google.registry.ui.soy.AnalyticsSoyInfo.getInstance()); - @VisibleForTesting // webdriver and screenshot tests need this - public static final Supplier CSS_RENAMING_MAP_SUPPLIER = - SoyTemplateUtils.createCssRenamingMapSupplier( - Resources.getResource("google/registry/ui/css/registrar_bin.css.js"), - Resources.getResource("google/registry/ui/css/registrar_dbg.css.js")); - - @Inject HttpServletRequest req; - @Inject Response response; @Inject RegistrarConsoleMetrics registrarConsoleMetrics; @Inject AuthenticatedRegistrarAccessor registrarAccessor; - @Inject UserService userService; - @Inject XsrfTokenManager xsrfTokenManager; - @Inject AuthResult authResult; - - @Inject - @Config("logoFilename") - String logoFilename; - - @Inject - @Config("productName") - String productName; @Inject @Config("integrationEmail") @@ -112,10 +81,6 @@ public final class ConsoleUiAction implements Runnable { @Config("registrarConsoleEnabled") boolean enabled; - @Inject - @Config("analyticsConfig") - Map analyticsConfig; - @Inject @Parameter(PARAM_CLIENT_ID) Optional paramClientId; @@ -124,37 +89,15 @@ public final class ConsoleUiAction implements Runnable { ConsoleUiAction() {} @Override - public void run() { - if (!authResult.userAuthInfo().isPresent()) { - response.setStatus(SC_MOVED_TEMPORARILY); - String location; - try { - location = userService.createLoginURL(req.getRequestURI()); - } catch (IllegalArgumentException e) { - // UserServiceImpl.createLoginURL() throws IllegalArgumentException if underlying API call - // returns an error code of NOT_ALLOWED. createLoginURL() assumes that the error is caused - // by an invalid URL. But in fact, the error can also occur if UserService doesn't have any - // user information, which happens when the request has been authenticated as internal. In - // this case, we want to avoid dying before we can send the redirect, so just redirect to - // the root path. - location = "/"; - } - response.setHeader(LOCATION, location); - return; - } - User user = authResult.userAuthInfo().get().user(); - response.setContentType(MediaType.HTML_UTF_8); - response.setHeader(X_FRAME_OPTIONS, "SAMEORIGIN"); // Disallow iframing. - response.setHeader("X-Ui-Compatible", "IE=edge"); // Ask IE not to be silly. - SoyMapData data = new SoyMapData(); - data.put("logoFilename", logoFilename); - data.put("productName", productName); - data.put("integrationEmail", integrationEmail); - data.put("supportEmail", supportEmail); - data.put("announcementsEmail", announcementsEmail); - data.put("supportPhoneNumber", supportPhoneNumber); - data.put("technicalDocsUrl", technicalDocsUrl); - data.put("analyticsConfig", analyticsConfig); + public void runAfterLogin(HashMap data) { + SoyMapData soyMapData = new SoyMapData(); + data.forEach((key, value) -> soyMapData.put(key, value)); + + soyMapData.put("integrationEmail", integrationEmail); + soyMapData.put("supportEmail", supportEmail); + soyMapData.put("announcementsEmail", announcementsEmail); + soyMapData.put("supportPhoneNumber", supportPhoneNumber); + soyMapData.put("technicalDocsUrl", technicalDocsUrl); if (!enabled) { response.setStatus(SC_SERVICE_UNAVAILABLE); response.setPayload( @@ -162,23 +105,20 @@ public final class ConsoleUiAction implements Runnable { .get() .newRenderer(ConsoleSoyInfo.DISABLED) .setCssRenamingMap(CSS_RENAMING_MAP_SUPPLIER.get()) - .setData(data) + .setData(soyMapData) .render()); return; } - data.put("username", user.getNickname()); - data.put("logoutUrl", userService.createLogoutURL(PATH)); - data.put("xsrfToken", xsrfTokenManager.generateToken(user.getEmail())); ImmutableSetMultimap roleMap = registrarAccessor.getAllClientIdWithRoles(); - data.put("allClientIds", roleMap.keySet()); - data.put("environment", RegistryEnvironment.get().toString()); + soyMapData.put("allClientIds", roleMap.keySet()); + soyMapData.put("environment", RegistryEnvironment.get().toString()); // We set the initial value to the value that will show if guessClientId throws. String clientId = ""; try { clientId = paramClientId.orElse(registrarAccessor.guessClientId()); - data.put("clientId", clientId); - data.put("isOwner", roleMap.containsEntry(clientId, OWNER)); - data.put("isAdmin", roleMap.containsEntry(clientId, ADMIN)); + soyMapData.put("clientId", clientId); + soyMapData.put("isOwner", roleMap.containsEntry(clientId, OWNER)); + soyMapData.put("isAdmin", roleMap.containsEntry(clientId, ADMIN)); // We want to load the registrar even if we won't use it later (even if we remove the // requireFeeExtension) - to make sure the user indeed has access to the guessed registrar. @@ -197,7 +137,7 @@ public final class ConsoleUiAction implements Runnable { .get() .newRenderer(ConsoleSoyInfo.WHOAREYOU) .setCssRenamingMap(CSS_RENAMING_MAP_SUPPLIER.get()) - .setData(data) + .setData(soyMapData) .render()); registrarConsoleMetrics.registerConsoleRequest( clientId, paramClientId.isPresent(), roleMap.get(clientId), "FORBIDDEN"); @@ -213,10 +153,15 @@ public final class ConsoleUiAction implements Runnable { .get() .newRenderer(ConsoleSoyInfo.MAIN) .setCssRenamingMap(CSS_RENAMING_MAP_SUPPLIER.get()) - .setData(data) + .setData(soyMapData) .render(); response.setPayload(payload); registrarConsoleMetrics.registerConsoleRequest( clientId, paramClientId.isPresent(), roleMap.get(clientId), "SUCCESS"); } + + @Override + public String getPath() { + return PATH; + } } diff --git a/core/src/main/java/google/registry/ui/server/registrar/HtmlAction.java b/core/src/main/java/google/registry/ui/server/registrar/HtmlAction.java new file mode 100644 index 000000000..02323269f --- /dev/null +++ b/core/src/main/java/google/registry/ui/server/registrar/HtmlAction.java @@ -0,0 +1,109 @@ +// Copyright 2019 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.common.net.HttpHeaders.LOCATION; +import static com.google.common.net.HttpHeaders.X_FRAME_OPTIONS; +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.flogger.FluentLogger; +import com.google.common.net.MediaType; +import google.registry.config.RegistryConfig.Config; +import google.registry.request.Action; +import google.registry.request.RequestMethod; +import google.registry.request.Response; +import google.registry.request.auth.AuthResult; +import google.registry.security.XsrfTokenManager; +import java.util.HashMap; +import java.util.Map; +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; + +/** + * Handles some of the nitty-gritty of responding to requests that should return HTML, including + * login, redirects, analytics, and some headers. + * + * If the user is not logged in, this will redirect to the login URL. + */ +public abstract class HtmlAction implements Runnable { + + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + @Inject HttpServletRequest req; + @Inject Response response; + @Inject UserService userService; + @Inject XsrfTokenManager xsrfTokenManager; + @Inject AuthResult authResult; + @Inject @RequestMethod Action.Method method; + + @Inject + @Config("logoFilename") + String logoFilename; + + @Inject + @Config("productName") + String productName; + + @Inject + @Config("analyticsConfig") + Map analyticsConfig; + + @Override + public void run() { + response.setHeader(X_FRAME_OPTIONS, "SAMEORIGIN"); // Disallow iframing. + response.setHeader("X-Ui-Compatible", "IE=edge"); // Ask IE not to be silly. + + if (!authResult.userAuthInfo().isPresent()) { + response.setStatus(SC_MOVED_TEMPORARILY); + String location; + try { + location = userService.createLoginURL(req.getRequestURI()); + } catch (IllegalArgumentException e) { + // UserServiceImpl.createLoginURL() throws IllegalArgumentException if underlying API call + // returns an error code of NOT_ALLOWED. createLoginURL() assumes that the error is caused + // by an invalid URL. But in fact, the error can also occur if UserService doesn't have any + // user information, which happens when the request has been authenticated as internal. In + // this case, we want to avoid dying before we can send the redirect, so just redirect to + // the root path. + location = "/"; + } + response.setHeader(LOCATION, location); + return; + } + response.setContentType(MediaType.HTML_UTF_8); + + User user = authResult.userAuthInfo().get().user(); + + // Using HashMap to allow null values + HashMap data = new HashMap<>(); + data.put("logoFilename", logoFilename); + data.put("productName", productName); + data.put("username", user.getNickname()); + data.put("logoutUrl", userService.createLogoutURL(getPath())); + data.put("analyticsConfig", analyticsConfig); + data.put("xsrfToken", xsrfTokenManager.generateToken(user.getEmail())); + + logger.atInfo().log( + "User %s is accessing %s. Method= %s", + authResult.userIdForLogging(), getClass().getName(), method); + runAfterLogin(data); + } + + public abstract void runAfterLogin(HashMap data); + + public abstract String getPath(); +} diff --git a/core/src/main/java/google/registry/ui/server/registrar/JsonGetAction.java b/core/src/main/java/google/registry/ui/server/registrar/JsonGetAction.java new file mode 100644 index 000000000..fb38b7c9c --- /dev/null +++ b/core/src/main/java/google/registry/ui/server/registrar/JsonGetAction.java @@ -0,0 +1,21 @@ +// Copyright 2019 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; + +/** + * Marker interface for {@link google.registry.request.Action}s that serve GET requests and return + * JSON, rather than HTML. + */ +public interface JsonGetAction extends Runnable {} diff --git a/core/src/main/java/google/registry/ui/server/registrar/RegistryLockGetAction.java b/core/src/main/java/google/registry/ui/server/registrar/RegistryLockGetAction.java index d9cfcde26..7ab0ba959 100644 --- a/core/src/main/java/google/registry/ui/server/registrar/RegistryLockGetAction.java +++ b/core/src/main/java/google/registry/ui/server/registrar/RegistryLockGetAction.java @@ -16,7 +16,6 @@ package google.registry.ui.server.registrar; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.collect.ImmutableList.toImmutableList; -import static com.google.common.net.HttpHeaders.X_FRAME_OPTIONS; import static google.registry.security.JsonResponseHelper.Status.SUCCESS; import static google.registry.ui.server.registrar.RegistrarConsoleModule.PARAM_CLIENT_ID; import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN; @@ -58,7 +57,7 @@ import org.joda.time.DateTime; service = Action.Service.DEFAULT, path = RegistryLockGetAction.PATH, auth = Auth.AUTH_PUBLIC_LOGGED_IN) -public final class RegistryLockGetAction implements Runnable { +public final class RegistryLockGetAction implements JsonGetAction { public static final String PATH = "/registry-lock-get"; @@ -98,8 +97,6 @@ public final class RegistryLockGetAction implements Runnable { checkArgument(authResult.userAuthInfo().isPresent(), "User auth info must be present"); checkArgument(paramClientId.isPresent(), "clientId must be present"); response.setContentType(MediaType.JSON_UTF_8); - response.setHeader(X_FRAME_OPTIONS, "SAMEORIGIN"); // Disallow iframing. - response.setHeader("X-Ui-Compatible", "IE=edge"); // Ask IE not to be silly. try { ImmutableMap resultMap = getLockedDomainsMap(paramClientId.get()); diff --git a/core/src/test/java/google/registry/ui/server/ActionMembershipTest.java b/core/src/test/java/google/registry/ui/server/ActionMembershipTest.java new file mode 100644 index 000000000..080ea9caa --- /dev/null +++ b/core/src/test/java/google/registry/ui/server/ActionMembershipTest.java @@ -0,0 +1,59 @@ +// Copyright 2019 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; + +import static com.google.common.truth.Truth.assertWithMessage; + +import com.google.common.collect.ImmutableSet; +import google.registry.request.Action; +import google.registry.request.JsonActionRunner; +import google.registry.ui.server.registrar.HtmlAction; +import google.registry.ui.server.registrar.JsonGetAction; +import io.github.classgraph.ClassGraph; +import io.github.classgraph.ScanResult; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; + +@RunWith(JUnit4.class) +public final class ActionMembershipTest { + + @Test + public void testAllActionsEitherHtmlOrJson() { + // All UI actions should serve valid HTML or JSON. There are three valid options: + // 1. Extending HtmlAction to signal that we are serving an HTML page + // 2. Extending JsonAction to show that we are serving JSON POST requests + // 3. Extending JsonGetAction to serve JSON GET requests + ImmutableSet.Builder failingClasses = new ImmutableSet.Builder<>(); + try (ScanResult scanResult = + new ClassGraph().enableAnnotationInfo().whitelistPackages("google.registry.ui").scan()) { + scanResult + .getClassesWithAnnotation(Action.class.getName()) + .forEach( + classInfo -> { + if (!classInfo.extendsSuperclass(HtmlAction.class.getName()) + && !classInfo.implementsInterface(JsonActionRunner.JsonAction.class.getName()) + && !classInfo.implementsInterface(JsonGetAction.class.getName())) { + failingClasses.add(classInfo.getName()); + } + }); + } + assertWithMessage( + "All UI actions must implement / extend HtmlAction, JsonGetAction, " + + "or JsonActionRunner.JsonAction. Failing classes:") + .that(failingClasses.build()) + .isEmpty(); + } +} diff --git a/core/src/test/java/google/registry/ui/server/registrar/ConsoleUiActionTest.java b/core/src/test/java/google/registry/ui/server/registrar/ConsoleUiActionTest.java index b951198eb..aa7a4bc34 100644 --- a/core/src/test/java/google/registry/ui/server/registrar/ConsoleUiActionTest.java +++ b/core/src/test/java/google/registry/ui/server/registrar/ConsoleUiActionTest.java @@ -28,6 +28,7 @@ import com.google.appengine.api.users.UserServiceFactory; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSetMultimap; import com.google.common.net.MediaType; +import google.registry.request.Action.Method; import google.registry.request.auth.AuthLevel; import google.registry.request.auth.AuthResult; import google.registry.request.auth.AuthenticatedRegistrarAccessor; @@ -77,6 +78,7 @@ public class ConsoleUiActionTest { action.registrarConsoleMetrics = new RegistrarConsoleMetrics(); action.userService = UserServiceFactory.getUserService(); action.xsrfTokenManager = new XsrfTokenManager(new FakeClock(), action.userService); + action.method = Method.GET; action.paramClientId = Optional.empty(); action.authResult = AuthResult.create(AuthLevel.USER, UserAuthInfo.create(user, false)); action.analyticsConfig = ImmutableMap.of("googleAnalyticsId", "sampleId");