diff --git a/java/google/registry/env/common/default/WEB-INF/web.xml b/java/google/registry/env/common/default/WEB-INF/web.xml index ad42c0f06..bcbc77703 100644 --- a/java/google/registry/env/common/default/WEB-INF/web.xml +++ b/java/google/registry/env/common/default/WEB-INF/web.xml @@ -37,6 +37,12 @@ /registrar-settings + + + frontend-servlet + /registrar-ote-setup + + diff --git a/java/google/registry/model/common/GaeUserIdConverter.java b/java/google/registry/model/common/GaeUserIdConverter.java index a4fc98305..57d08e156 100644 --- a/java/google/registry/model/common/GaeUserIdConverter.java +++ b/java/google/registry/model/common/GaeUserIdConverter.java @@ -14,6 +14,7 @@ package google.registry.model.common; +import static com.google.common.base.Preconditions.checkState; import static google.registry.model.ofy.ObjectifyService.allocateId; import static google.registry.model.ofy.ObjectifyService.ofy; @@ -24,6 +25,7 @@ import com.googlecode.objectify.annotation.Id; import google.registry.model.ImmutableObject; import google.registry.model.annotations.NotBackedUp; import google.registry.model.annotations.NotBackedUp.Reason; +import java.util.List; /** * A helper class to convert email addresses to GAE user ids. It does so by persisting a User @@ -46,8 +48,9 @@ public class GaeUserIdConverter extends ImmutableObject { public static String convertEmailAddressToGaeUserId(String emailAddress) { final GaeUserIdConverter gaeUserIdConverter = new GaeUserIdConverter(); gaeUserIdConverter.id = allocateId(); - gaeUserIdConverter.user = - new User(emailAddress, Splitter.on('@').splitToList(emailAddress).get(1)); + List emailParts = Splitter.on('@').splitToList(emailAddress); + checkState(emailParts.size() == 2, "'%s' is not a valid email address", emailAddress); + gaeUserIdConverter.user = new User(emailAddress, emailParts.get(1)); try { // Perform these operations in a transactionless context to avoid enlisting in some outer diff --git a/java/google/registry/module/frontend/BUILD b/java/google/registry/module/frontend/BUILD index 796bee607..fc2d681fc 100644 --- a/java/google/registry/module/frontend/BUILD +++ b/java/google/registry/module/frontend/BUILD @@ -21,6 +21,7 @@ java_library( "//java/google/registry/request:modules", "//java/google/registry/request/auth", "//java/google/registry/ui", + "//java/google/registry/ui/server/otesetup", "//java/google/registry/ui/server/registrar", "//java/google/registry/util", "@com_google_appengine_api_1_0_sdk", diff --git a/java/google/registry/module/frontend/FrontendRequestComponent.java b/java/google/registry/module/frontend/FrontendRequestComponent.java index 19c22964b..3869425a1 100644 --- a/java/google/registry/module/frontend/FrontendRequestComponent.java +++ b/java/google/registry/module/frontend/FrontendRequestComponent.java @@ -25,6 +25,7 @@ import google.registry.monitoring.whitebox.WhiteboxModule; import google.registry.request.RequestComponentBuilder; import google.registry.request.RequestModule; import google.registry.request.RequestScope; +import google.registry.ui.server.otesetup.ConsoleOteSetupAction; import google.registry.ui.server.registrar.ConsoleUiAction; import google.registry.ui.server.registrar.RegistrarConsoleModule; import google.registry.ui.server.registrar.RegistrarSettingsAction; @@ -33,13 +34,14 @@ import google.registry.ui.server.registrar.RegistrarSettingsAction; @RequestScope @Subcomponent( modules = { - RegistrarConsoleModule.class, DnsModule.class, EppTlsModule.class, + RegistrarConsoleModule.class, RequestModule.class, WhiteboxModule.class, }) interface FrontendRequestComponent { + ConsoleOteSetupAction consoleOteSetupAction(); ConsoleUiAction consoleUiAction(); EppConsoleAction eppConsoleAction(); EppTlsAction eppTlsAction(); diff --git a/java/google/registry/request/auth/AuthenticatedRegistrarAccessor.java b/java/google/registry/request/auth/AuthenticatedRegistrarAccessor.java index b04475a65..c1d080730 100644 --- a/java/google/registry/request/auth/AuthenticatedRegistrarAccessor.java +++ b/java/google/registry/request/auth/AuthenticatedRegistrarAccessor.java @@ -68,6 +68,12 @@ public class AuthenticatedRegistrarAccessor { private final String userIdForLogging; + /** + * Whether this user is an Admin, meaning either a GAE-admin or a member of the Support G Suite + * group. + */ + private final boolean isAdmin; + /** * Gives all roles a user has for a given clientId. * @@ -103,33 +109,41 @@ public class AuthenticatedRegistrarAccessor { @Config("registryAdminClientId") String registryAdminClientId, @Config("gSuiteSupportGroupEmailAddress") Optional gSuiteSupportGroupEmailAddress, Lazy lazyGroupsConnection) { - this( - authResult.userIdForLogging(), - createRoleMap( - authResult, - registryAdminClientId, - lazyGroupsConnection, - gSuiteSupportGroupEmailAddress)); + this.isAdmin = userIsAdmin(authResult, gSuiteSupportGroupEmailAddress, lazyGroupsConnection); - logger.atInfo().log( - "%s has the following roles: %s", authResult.userIdForLogging(), roleMap); + this.userIdForLogging = authResult.userIdForLogging(); + this.roleMap = createRoleMap(authResult, this.isAdmin, registryAdminClientId); + + logger.atInfo().log("%s has the following roles: %s", userIdForLogging(), roleMap); } private AuthenticatedRegistrarAccessor( - String userIdForLogging, ImmutableSetMultimap roleMap) { + String userIdForLogging, boolean isAdmin, ImmutableSetMultimap roleMap) { this.userIdForLogging = checkNotNull(userIdForLogging); this.roleMap = checkNotNull(roleMap); + this.isAdmin = isAdmin; } /** * Creates a "logged-in user" accessor with a given role map, used for tests. * + *

The user will be allowed to create Registrars (and hence do OT&E setup) iff they have + * the role of ADMIN for at least one clientId. + * *

The user's "name" in logs and exception messages is "TestUserId". */ @VisibleForTesting public static AuthenticatedRegistrarAccessor createForTesting( ImmutableSetMultimap roleMap) { - return new AuthenticatedRegistrarAccessor("TestUserId", roleMap); + boolean isAdmin = roleMap.values().contains(Role.ADMIN); + return new AuthenticatedRegistrarAccessor("TestUserId", isAdmin, roleMap); + } + + /** + * Returns whether this user is allowed to create new Registrars and TLDs. + */ + public boolean isAdmin() { + return isAdmin; } /** @@ -261,11 +275,32 @@ public class AuthenticatedRegistrarAccessor { } } + private static boolean userIsAdmin( + AuthResult authResult, + Optional gSuiteSupportGroupEmailAddress, + Lazy lazyGroupsConnection) { + + if (!authResult.userAuthInfo().isPresent()) { + return false; + } + + UserAuthInfo userAuthInfo = authResult.userAuthInfo().get(); + + User user = userAuthInfo.user(); + + // both GAE project admin and members of the gSuiteSupportGroupEmailAddress are considered + // admins for the RegistrarConsole. + return bypassAdminCheck + ? false + : userAuthInfo.isUserAdmin() + || checkIsSupport( + lazyGroupsConnection, user.getEmail(), gSuiteSupportGroupEmailAddress); + } + private static ImmutableSetMultimap createRoleMap( AuthResult authResult, - String registryAdminClientId, - Lazy lazyGroupsConnection, - Optional gSuiteSupportGroupEmailAddress) { + boolean isAdmin, + String registryAdminClientId) { if (!authResult.userAuthInfo().isPresent()) { return ImmutableSetMultimap.of(); @@ -274,17 +309,11 @@ public class AuthenticatedRegistrarAccessor { UserAuthInfo userAuthInfo = authResult.userAuthInfo().get(); User user = userAuthInfo.user(); - // both GAE project admin and members of the gSuiteSupportGroupEmailAddress are considered - // admins for the RegistrarConsole. - boolean isAdmin = - bypassAdminCheck - ? false - : userAuthInfo.isUserAdmin() - || checkIsSupport( - lazyGroupsConnection, user.getEmail(), gSuiteSupportGroupEmailAddress); ImmutableSetMultimap.Builder builder = new ImmutableSetMultimap.Builder<>(); + logger.atInfo().log("Checking registrar contacts for user ID %s", user.getUserId()); + ofy() .load() .type(RegistrarContact.class) diff --git a/java/google/registry/ui/server/otesetup/BUILD b/java/google/registry/ui/server/otesetup/BUILD new file mode 100644 index 000000000..4ce526598 --- /dev/null +++ b/java/google/registry/ui/server/otesetup/BUILD @@ -0,0 +1,43 @@ +package( + default_visibility = ["//visibility:public"], +) + +licenses(["notice"]) # Apache 2.0 + +java_library( + name = "otesetup", + srcs = glob(["*.java"]), + resources = [ + "//java/google/registry/ui/css:registrar_bin.css.js", + "//java/google/registry/ui/css:registrar_dbg.css.js", + ], + deps = [ + "//java/google/registry/config", + "//java/google/registry/export/sheet", + "//java/google/registry/flows", + "//java/google/registry/model", + "//java/google/registry/request", + "//java/google/registry/request/auth", + "//java/google/registry/security", + "//java/google/registry/ui/forms", + "//java/google/registry/ui/server", + "//java/google/registry/ui/soy:soy_java_wrappers", + "//java/google/registry/ui/soy/otesetup:soy_java_wrappers", + "//java/google/registry/util", + "//third_party/objectify:objectify-v4_1", + "@com_google_appengine_api_1_0_sdk", + "@com_google_auto_value", + "@com_google_code_findbugs_jsr305", + "@com_google_dagger", + "@com_google_flogger", + "@com_google_flogger_system_backend", + "@com_google_guava", + "@com_google_monitoring_client_metrics", + "@com_google_re2j", + "@io_bazel_rules_closure//closure/templates", + "@javax_inject", + "@javax_servlet_api", + "@joda_time", + "@org_joda_money", + ], +) diff --git a/java/google/registry/ui/server/otesetup/ConsoleOteSetupAction.java b/java/google/registry/ui/server/otesetup/ConsoleOteSetupAction.java new file mode 100644 index 000000000..6b9ada0a5 --- /dev/null +++ b/java/google/registry/ui/server/otesetup/ConsoleOteSetupAction.java @@ -0,0 +1,243 @@ +// Copyright 2018 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.otesetup; +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 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.otesetup.ConsoleSoyInfo; +import google.registry.util.StringGenerator; +import java.util.HashMap; +import java.util.Optional; +import javax.inject.Inject; +import javax.servlet.http.HttpServletRequest; + +/** + * Action that serves OT&E setup web page. + * + *

This Action does 2 things: - for GET, just returns the form that asks for the clientId and + * email. - for POST, receives the clientId and email and generates the OTE entities. + * + *

TODO(b/120201577): once we can have 2 different Actions with the same path (different + * Methods), separate this class to 2 Actions. + */ +@Action( + path = ConsoleOteSetupAction.PATH, + method = {Method.POST, Method.GET}, + auth = Auth.AUTH_PUBLIC) +public final class ConsoleOteSetupAction implements Runnable { + + private static final int PASSWORD_LENGTH = 16; + + private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + + public static final String PATH = "/registrar-ote-setup"; + + private static final Supplier TOFU_SUPPLIER = + SoyTemplateUtils.createTofuSupplier( + google.registry.ui.soy.ConsoleSoyInfo.getInstance(), + google.registry.ui.soy.FormsSoyInfo.getInstance(), + google.registry.ui.soy.otesetup.ConsoleSoyInfo.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 RegistryEnvironment registryEnvironment; + @Inject SendEmailUtils sendEmailUtils; + @Inject @Config("logoFilename") String logoFilename; + @Inject @Config("productName") String productName; + @Inject @Config("base64StringGenerator") StringGenerator passwordGenerator; + @Inject @Parameter("clientId") Optional clientId; + @Inject @Parameter("email") Optional email; + + @Inject 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); + checkState(registryEnvironment != RegistryEnvironment.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())); + response.setContentType(MediaType.HTML_UTF_8); + + if (!registrarAccessor.isAdmin()) { + response.setStatus(SC_FORBIDDEN); + response.setPayload( + TOFU_SUPPLIER + .get() + .newRenderer(ConsoleSoyInfo.WHOAREYOU) + .setCssRenamingMap(CSS_RENAMING_MAP_SUPPLIER.get()) + .setData(data) + .render()); + return; + } + switch (method) { + case POST: + runPost(data); + return; + case GET: + runGet(data); + return; + default: + return; + } + } + + private void runPost(HashMap data) { + // This is intentionally outside of the "try/catch", since not having these fields means someone + // tried to "manually" POST to this page. No need to send out a "pretty" result in that case. + checkState(clientId.isPresent() && email.isPresent(), "Must supply clientId and email"); + + data.put("baseClientId", clientId.get()); + data.put("contactEmail", email.get()); + + try { + String password = passwordGenerator.createString(PASSWORD_LENGTH); + ImmutableMap clientIdToTld = + OteAccountBuilder.forClientId(clientId.get()) + .addContact(email.get()) + .setPassword(password) + .buildAndPersist(); + + sendExternalUpdates(clientIdToTld); + + data.put("clientIdToTld", clientIdToTld); + data.put("password", password); + + response.setPayload( + TOFU_SUPPLIER + .get() + .newRenderer(ConsoleSoyInfo.RESULT_SUCCESS) + .setCssRenamingMap(CSS_RENAMING_MAP_SUPPLIER.get()) + .setData(data) + .render()); + } catch (Throwable e) { + logger.atWarning().withCause(e).log( + "Failed to setup OT&E. clientId: %s, email: %s", clientId.get(), email.get()); + data.put("errorMessage", e.getMessage()); + response.setPayload( + TOFU_SUPPLIER + .get() + .newRenderer(ConsoleSoyInfo.FORM_PAGE) + .setCssRenamingMap(CSS_RENAMING_MAP_SUPPLIER.get()) + .setData(data) + .render()); + } + } + + private void runGet(HashMap data) { + // set the values to pre-fill, if given + data.put("baseClientId", clientId.orElse(null)); + data.put("contactEmail", email.orElse(null)); + + response.setPayload( + TOFU_SUPPLIER + .get() + .newRenderer(ConsoleSoyInfo.FORM_PAGE) + .setCssRenamingMap(CSS_RENAMING_MAP_SUPPLIER.get()) + .setData(data) + .render()); + } + + + private void sendExternalUpdates(ImmutableMap clientIdToTld) { + if (!sendEmailUtils.hasRecepients()) { + return; + } + String environment = Ascii.toLowerCase(String.valueOf(registryEnvironment)); + StringBuilder builder = new StringBuilder(); + builder.append( + String.format( + "The following entities were created in %s by %s:\n", + environment, registrarAccessor.userIdForLogging())); + clientIdToTld.forEach( + (clientId, tld) -> + builder.append( + String.format(" Registrar %s with access to TLD %s\n", clientId, tld))); + builder.append(String.format("Gave user %s web access to these Registrars\n", email.get())); + sendEmailUtils.sendEmail( + String.format( + "OT&E for registrar %s created in %s", + clientId.get(), + environment), + builder.toString()); + } +} diff --git a/java/google/registry/ui/server/registrar/RegistrarConsoleModule.java b/java/google/registry/ui/server/registrar/RegistrarConsoleModule.java index 6d6262dbe..d14e89b8b 100644 --- a/java/google/registry/ui/server/registrar/RegistrarConsoleModule.java +++ b/java/google/registry/ui/server/registrar/RegistrarConsoleModule.java @@ -41,4 +41,16 @@ public final class RegistrarConsoleModule { static String provideClientId(HttpServletRequest req) { return extractRequiredParameter(req, PARAM_CLIENT_ID); } + + @Provides + @Parameter("email") + static Optional provideOptionalEmail(HttpServletRequest req) { + return extractOptionalParameter(req, "email"); + } + + @Provides + @Parameter("email") + static String provideEmail(HttpServletRequest req) { + return extractRequiredParameter(req, "email"); + } } diff --git a/java/google/registry/ui/soy/Forms.soy b/java/google/registry/ui/soy/Forms.soy index ea3a0accd..ae1199545 100644 --- a/java/google/registry/ui/soy/Forms.soy +++ b/java/google/registry/ui/soy/Forms.soy @@ -295,6 +295,21 @@ {/template} +/** Submit button. */ +{template .submitRow} + {@param label: string} + + + + + + +{/template} + /** Drop-down select button widget. */ {template .menuButton} diff --git a/java/google/registry/ui/soy/otesetup/BUILD b/java/google/registry/ui/soy/otesetup/BUILD new file mode 100644 index 000000000..f19b6651c --- /dev/null +++ b/java/google/registry/ui/soy/otesetup/BUILD @@ -0,0 +1,21 @@ +package( + default_visibility = ["//java/google/registry:registry_project"], +) + +licenses(["notice"]) # Apache 2.0 + +load("@io_bazel_rules_closure//closure:defs.bzl", "closure_java_template_library", "closure_js_template_library") + +closure_js_template_library( + name = "otesetup", + srcs = glob(["*.soy"]), + data = ["//java/google/registry/ui/css:registrar_raw"], + globals = "//java/google/registry/ui:globals.txt", + deps = ["//java/google/registry/ui/soy"], +) + +closure_java_template_library( + name = "soy_java_wrappers", + srcs = glob(["*.soy"]), + java_package = "google.registry.ui.soy.otesetup", +) diff --git a/java/google/registry/ui/soy/otesetup/Console.soy b/java/google/registry/ui/soy/otesetup/Console.soy new file mode 100644 index 000000000..e96adf4a1 --- /dev/null +++ b/java/google/registry/ui/soy/otesetup/Console.soy @@ -0,0 +1,169 @@ +// Copyright 2018 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. + +{namespace registry.soy.registrar.console} + + +/** + * Main page for the OT&E creation. Holds a form with the required data. + */ +{template .formPage} + {@param xsrfToken: string} /** Security token. */ + {@param username: string} /** Arbitrary username to display. */ + {@param logoutUrl: string} /** Generated URL for logging out of Google. */ + {@param productName: string} /** Name to display for this software product. */ + {@param logoFilename: string} + {@param? errorMessage: string} /** If set - display the error message above the form. */ + {@param? baseClientId: string} /** If set - an initial value to fill for the base client ID. */ + {@param? contactEmail: string} /** If set - an initial value to fill for the email. */ + + {call registry.soy.console.header} + {param app: 'registrar' /} + {param subtitle: 'OT&E setup' /} + {/call} + {call registry.soy.console.googlebar data="all" /} +

+{/template} + + +/** + * Result page for a successful OT&E setup. + */ +{template .resultSuccess} + {@param baseClientId: string} /** The base clientId used for the OT&E setup. */ + {@param contactEmail: string} /** The contact's email added to the registrars. */ + {@param clientIdToTld: map} /** The created registrars->TLD mapping. */ + {@param password: string} /** The password given for the created registrars. */ + {@param username: string} /** Arbitrary username to display. */ + {@param logoutUrl: string} /** Generated URL for logging out of Google. */ + {@param productName: string} /** Name to display for this software product. */ + {@param logoFilename: string} + + {call registry.soy.console.header} + {param app: 'registrar' /} + {param subtitle: 'OT&E setup' /} + {/call} + {call registry.soy.console.googlebar data="all" /} + +{/template} + + +/** Form for getting registrar info. */ +{template .form_ visibility="private"} + {@param xsrfToken: string} /** Security token. */ + {@param? baseClientId: string} /** If set - an initial value to fill for the base client ID. */ + {@param? contactEmail: string} /** If set - an initial value to fill for the email. */ +
+ + {call registry.soy.forms.inputFieldRowWithValue} + {param label: 'Base clientId' /} + {param name: 'clientId' /} + {param value: $baseClientId /} + {param placeholder: 'registrar\'s ID' /} + {param description kind="text"} + must 1) consist of only lowercase letters, numbers, or hyphens, + 2) start with a letter, and 3) be between 3 and 14 characters (inclusive). + We require 1 and 2 since the registrar name will be used to create TLDs, + and we require 3 since we append \"-[1234]\" to the name to create client + IDs which are required by the EPP XML schema to be between 3-16 chars. + {/param} + {param readonly: false /} + {/call} + {call registry.soy.forms.inputFieldRowWithValue} + {param label: 'contact email' /} + {param name: 'email' /} + {param value: $contactEmail /} + {param placeholder: 'registrar\'s assigned email' /} + {param description kind="text"} + Will be granted web-console access to the OTE registrars. + {/param} + {param readonly: false /} + {/call} + {call registry.soy.forms.submitRow} + {param label: 'create' /} + {/call} +
+ +
+{/template} + + +/** + * Who goes thar?! + */ +{template .whoareyou} + {@param username: string} /** Arbitrary username to display. */ + {@param logoutUrl: string} /** Generated URL for logging out of Google. */ + {@param logoFilename: string} + {@param productName: string} + {call registry.soy.console.header} + {param app: 'registrar' /} + {param subtitle: 'Not Authorized' /} + {/call} +
+ + {$productName} + +

You need permission

+

+ Only {$productName} Admins have access to this page. + You are signed in as {$username}. +

+
+{/template} + + diff --git a/java/google/registry/ui/soy/registrar/AdminSettings.soy b/java/google/registry/ui/soy/registrar/AdminSettings.soy index 960cb2ac2..d9e18993e 100644 --- a/java/google/registry/ui/soy/registrar/AdminSettings.soy +++ b/java/google/registry/ui/soy/registrar/AdminSettings.soy @@ -27,7 +27,6 @@ set or remove TLDs this client is allowed access to. -
@@ -44,8 +43,11 @@ class="{css('kd-button')} {css('btn-add')}">Add
- - + + + + +

Generate new OT&E accounts here {/template} diff --git a/javatests/google/registry/module/frontend/testdata/frontend_routing.txt b/javatests/google/registry/module/frontend/testdata/frontend_routing.txt index 6b91c3704..f60c5ccf9 100644 --- a/javatests/google/registry/module/frontend/testdata/frontend_routing.txt +++ b/javatests/google/registry/module/frontend/testdata/frontend_routing.txt @@ -1,5 +1,6 @@ -PATH CLASS METHODS OK AUTH_METHODS MIN USER_POLICY -/_dr/epp EppTlsAction POST n INTERNAL,API APP PUBLIC -/registrar ConsoleUiAction GET n INTERNAL,API,LEGACY NONE PUBLIC -/registrar-settings RegistrarSettingsAction POST n API,LEGACY USER PUBLIC -/registrar-xhr EppConsoleAction POST n API,LEGACY USER PUBLIC +PATH CLASS METHODS OK AUTH_METHODS MIN USER_POLICY +/_dr/epp EppTlsAction POST n INTERNAL,API APP PUBLIC +/registrar ConsoleUiAction GET n INTERNAL,API,LEGACY NONE PUBLIC +/registrar-ote-setup ConsoleOteSetupAction POST,GET n INTERNAL,API,LEGACY NONE PUBLIC +/registrar-settings RegistrarSettingsAction POST n API,LEGACY USER PUBLIC +/registrar-xhr EppConsoleAction POST n API,LEGACY USER PUBLIC diff --git a/javatests/google/registry/server/Fixture.java b/javatests/google/registry/server/Fixture.java index c6d3935ac..afe875d8e 100644 --- a/javatests/google/registry/server/Fixture.java +++ b/javatests/google/registry/server/Fixture.java @@ -22,6 +22,7 @@ import static google.registry.testing.DatastoreHelper.loadRegistrar; import static google.registry.testing.DatastoreHelper.newContactResource; import static google.registry.testing.DatastoreHelper.newDomainResource; import static google.registry.testing.DatastoreHelper.persistActiveHost; +import static google.registry.testing.DatastoreHelper.persistPremiumList; import static google.registry.testing.DatastoreHelper.persistResource; import com.google.common.collect.ImmutableList; @@ -58,6 +59,9 @@ public enum Fixture { public void load() { createTlds("xn--q9jyb4c", "example"); + // Used for OT&E TLDs + persistPremiumList("default_sandbox_list"); + ContactResource google = persistResource(newContactResource("google") .asBuilder() .setLocalizedPostalInfo(new PostalInfo.Builder() diff --git a/javatests/google/registry/server/RegistryTestServer.java b/javatests/google/registry/server/RegistryTestServer.java index 3835ed235..d48f24f73 100644 --- a/javatests/google/registry/server/RegistryTestServer.java +++ b/javatests/google/registry/server/RegistryTestServer.java @@ -85,7 +85,8 @@ public final class RegistryTestServer { // Registrar Console route("/registrar", FrontendServlet.class), - route("/registrar-settings", FrontendServlet.class)); + route("/registrar-settings", FrontendServlet.class), + route("/registrar-ote-setup", FrontendServlet.class)); private static final ImmutableList> FILTERS = ImmutableList.of( ObjectifyFilter.class, diff --git a/javatests/google/registry/ui/server/otesetup/BUILD b/javatests/google/registry/ui/server/otesetup/BUILD new file mode 100644 index 000000000..25d855581 --- /dev/null +++ b/javatests/google/registry/ui/server/otesetup/BUILD @@ -0,0 +1,45 @@ +package( + default_testonly = 1, + default_visibility = ["//java/google/registry:registry_project"], +) + +licenses(["notice"]) # Apache 2.0 + +load("//java/com/google/testing/builddefs:GenTestRules.bzl", "GenTestRules") + +java_library( + name = "otesetup", + srcs = glob(["*.java"]), + deps = [ + "//java/google/registry/config", + "//java/google/registry/model", + "//java/google/registry/request", + "//java/google/registry/request/auth", + "//java/google/registry/security", + "//java/google/registry/ui/server", + "//java/google/registry/ui/server/otesetup", + "//java/google/registry/util", + "//javatests/google/registry/testing", + "//third_party/objectify:objectify-v4_1", + "@com_google_appengine_api_1_0_sdk", + "@com_google_flogger", + "@com_google_flogger_system_backend", + "@com_google_guava", + "@com_google_guava_testlib", + "@com_google_monitoring_client_contrib", + "@com_google_truth", + "@com_google_truth_extensions_truth_java8_extension", + "@com_googlecode_json_simple", + "@javax_servlet_api", + "@joda_time", + "@junit", + "@org_joda_money", + "@org_mockito_all", + ], +) + +GenTestRules( + name = "GeneratedTestRules", + test_files = glob(["*Test.java"]), + deps = [":otesetup"], +) diff --git a/javatests/google/registry/ui/server/otesetup/ConsoleOteSetupActionTest.java b/javatests/google/registry/ui/server/otesetup/ConsoleOteSetupActionTest.java new file mode 100644 index 000000000..79e0f79d2 --- /dev/null +++ b/javatests/google/registry/ui/server/otesetup/ConsoleOteSetupActionTest.java @@ -0,0 +1,173 @@ +// Copyright 2018 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.otesetup; + +import static com.google.common.net.HttpHeaders.LOCATION; +import static com.google.common.truth.Truth.assertThat; +import static com.google.common.truth.Truth8.assertThat; +import static google.registry.model.registrar.Registrar.loadByClientId; +import static google.registry.testing.DatastoreHelper.persistPremiumList; +import static google.registry.testing.JUnitBackports.assertThrows; +import static javax.servlet.http.HttpServletResponse.SC_MOVED_TEMPORARILY; +import static org.mockito.Mockito.when; + +import com.google.appengine.api.users.User; +import com.google.appengine.api.users.UserServiceFactory; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableSetMultimap; +import google.registry.config.RegistryEnvironment; +import google.registry.model.registry.Registry; +import google.registry.request.Action.Method; +import google.registry.request.auth.AuthLevel; +import google.registry.request.auth.AuthResult; +import google.registry.request.auth.AuthenticatedRegistrarAccessor; +import google.registry.request.auth.UserAuthInfo; +import google.registry.security.XsrfTokenManager; +import google.registry.testing.AppEngineRule; +import google.registry.testing.DeterministicStringGenerator; +import google.registry.testing.FakeClock; +import google.registry.testing.FakeResponse; +import google.registry.testing.MockitoJUnitRule; +import google.registry.ui.server.SendEmailUtils; +import google.registry.util.SendEmailService; +import java.util.Optional; +import java.util.Properties; +import javax.mail.Message; +import javax.mail.Session; +import javax.mail.internet.MimeMessage; +import javax.servlet.http.HttpServletRequest; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; + +@RunWith(JUnit4.class) +public final class ConsoleOteSetupActionTest { + + @Rule + public final AppEngineRule appEngineRule = AppEngineRule.builder() + .withDatastore() + .build(); + + @Rule public final MockitoJUnitRule mocks = MockitoJUnitRule.create(); + + private final FakeResponse response = new FakeResponse(); + private final ConsoleOteSetupAction action = new ConsoleOteSetupAction(); + private final User user = new User("marla.singer@example.com", "gmail.com", "12345"); + + @Mock HttpServletRequest request; + @Mock SendEmailService emailService; + Message message; + + @Before + public void setUp() { + persistPremiumList("default_sandbox_list", "sandbox,USD 1000"); + + action.req = request; + action.method = Method.GET; + action.response = response; + action.registrarAccessor = + AuthenticatedRegistrarAccessor.createForTesting( + ImmutableSetMultimap.of("unused", AuthenticatedRegistrarAccessor.Role.ADMIN)); + action.userService = UserServiceFactory.getUserService(); + action.xsrfTokenManager = new XsrfTokenManager(new FakeClock(), action.userService); + action.authResult = AuthResult.create(AuthLevel.USER, UserAuthInfo.create(user, false)); + action.registryEnvironment = RegistryEnvironment.UNITTEST; + action.sendEmailUtils = + new SendEmailUtils( + "outgoing@registry.example", + "UnitTest Registry", + ImmutableList.of("notification@test.example", "notification2@test.example"), + emailService); + action.logoFilename = "logo.png"; + action.productName = "Nomulus"; + action.clientId = Optional.empty(); + action.email = Optional.empty(); + action.passwordGenerator = new DeterministicStringGenerator("abcdefghijklmnopqrstuvwxyz"); + + message = new MimeMessage(Session.getDefaultInstance(new Properties(), null)); + when(emailService.createMessage()).thenReturn(message); + } + + @Test + public void testNoUser_redirect() { + when(request.getRequestURI()).thenReturn("/test"); + action.authResult = AuthResult.NOT_AUTHENTICATED; + action.run(); + assertThat(response.getStatus()).isEqualTo(SC_MOVED_TEMPORARILY); + assertThat(response.getHeaders().get(LOCATION)).isEqualTo("/_ah/login?continue=%2Ftest"); + } + + @Test + public void testGet_authorized() { + action.run(); + assertThat(response.getPayload()).contains("

OT&E Setup Page

"); + } + + @Test + public void testGet_authorized_onProduction() { + action.registryEnvironment = RegistryEnvironment.PRODUCTION; + assertThrows(IllegalStateException.class, action::run); + } + + @Test + public void testGet_unauthorized() { + action.registrarAccessor = + AuthenticatedRegistrarAccessor.createForTesting(ImmutableSetMultimap.of()); + action.run(); + assertThat(response.getPayload()).contains("

You need permission

"); + } + + @Test + public void testPost_authorized() throws Exception { + action.clientId = Optional.of("myclientid"); + action.email = Optional.of("contact@registry.example"); + action.method = Method.POST; + action.run(); + + // We just check some samples to make sure OteAccountBuilder was called successfully. We aren't + // checking that all the entities are there or that they have the correct values. + assertThat(loadByClientId("myclientid-3")).isPresent(); + assertThat(Registry.get("myclientid-ga")).isNotNull(); + assertThat(loadByClientId("myclientid-5").get().getContacts().asList().get(0).getEmailAddress()) + .isEqualTo("contact@registry.example"); + assertThat(response.getPayload()) + .contains("

OT&E successfully created for registrar myclientid!

"); + assertThat(message.getSubject()).isEqualTo("OT&E for registrar myclientid created in unittest"); + assertThat(message.getContent()) + .isEqualTo( + "" + + "The following entities were created in unittest by TestUserId:\n" + + " Registrar myclientid-1 with access to TLD myclientid-sunrise\n" + + " Registrar myclientid-2 with access to TLD myclientid-landrush\n" + + " Registrar myclientid-3 with access to TLD myclientid-ga\n" + + " Registrar myclientid-4 with access to TLD myclientid-ga\n" + + " Registrar myclientid-5 with access to TLD myclientid-eap\n" + + "Gave user contact@registry.example web access to these Registrars\n"); + } + + @Test + public void testPost_unauthorized() { + action.registrarAccessor = + AuthenticatedRegistrarAccessor.createForTesting(ImmutableSetMultimap.of()); + action.clientId = Optional.of("myclientid"); + action.email = Optional.of("contact@registry.example"); + action.method = Method.POST; + action.run(); + assertThat(response.getPayload()).contains("

You need permission

"); + } +}