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. */
+
+{/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}
+
+{/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.
-
");
+ }
+
+ @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("