Add the ability to setup OT&E from the web console

We create a new endpoint with a simple form that will let admins (including
support) setup OT&E for registrars.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=226570568
This commit is contained in:
guyben 2018-12-21 19:09:36 -08:00 committed by jianglai
parent 040319a95d
commit 2777018d6a
17 changed files with 804 additions and 34 deletions

View file

@ -37,6 +37,12 @@
<url-pattern>/registrar-settings</url-pattern> <url-pattern>/registrar-settings</url-pattern>
</servlet-mapping> </servlet-mapping>
<!-- OT&E creation console. -->
<servlet-mapping>
<servlet-name>frontend-servlet</servlet-name>
<url-pattern>/registrar-ote-setup</url-pattern>
</servlet-mapping>
<!-- Security config --> <!-- Security config -->
<security-constraint> <security-constraint>
<web-resource-collection> <web-resource-collection>

View file

@ -14,6 +14,7 @@
package google.registry.model.common; 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.allocateId;
import static google.registry.model.ofy.ObjectifyService.ofy; 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.ImmutableObject;
import google.registry.model.annotations.NotBackedUp; import google.registry.model.annotations.NotBackedUp;
import google.registry.model.annotations.NotBackedUp.Reason; 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 * 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) { public static String convertEmailAddressToGaeUserId(String emailAddress) {
final GaeUserIdConverter gaeUserIdConverter = new GaeUserIdConverter(); final GaeUserIdConverter gaeUserIdConverter = new GaeUserIdConverter();
gaeUserIdConverter.id = allocateId(); gaeUserIdConverter.id = allocateId();
gaeUserIdConverter.user = List<String> emailParts = Splitter.on('@').splitToList(emailAddress);
new User(emailAddress, Splitter.on('@').splitToList(emailAddress).get(1)); checkState(emailParts.size() == 2, "'%s' is not a valid email address", emailAddress);
gaeUserIdConverter.user = new User(emailAddress, emailParts.get(1));
try { try {
// Perform these operations in a transactionless context to avoid enlisting in some outer // Perform these operations in a transactionless context to avoid enlisting in some outer

View file

@ -21,6 +21,7 @@ java_library(
"//java/google/registry/request:modules", "//java/google/registry/request:modules",
"//java/google/registry/request/auth", "//java/google/registry/request/auth",
"//java/google/registry/ui", "//java/google/registry/ui",
"//java/google/registry/ui/server/otesetup",
"//java/google/registry/ui/server/registrar", "//java/google/registry/ui/server/registrar",
"//java/google/registry/util", "//java/google/registry/util",
"@com_google_appengine_api_1_0_sdk", "@com_google_appengine_api_1_0_sdk",

View file

@ -25,6 +25,7 @@ import google.registry.monitoring.whitebox.WhiteboxModule;
import google.registry.request.RequestComponentBuilder; import google.registry.request.RequestComponentBuilder;
import google.registry.request.RequestModule; import google.registry.request.RequestModule;
import google.registry.request.RequestScope; 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.ConsoleUiAction;
import google.registry.ui.server.registrar.RegistrarConsoleModule; import google.registry.ui.server.registrar.RegistrarConsoleModule;
import google.registry.ui.server.registrar.RegistrarSettingsAction; import google.registry.ui.server.registrar.RegistrarSettingsAction;
@ -33,13 +34,14 @@ import google.registry.ui.server.registrar.RegistrarSettingsAction;
@RequestScope @RequestScope
@Subcomponent( @Subcomponent(
modules = { modules = {
RegistrarConsoleModule.class,
DnsModule.class, DnsModule.class,
EppTlsModule.class, EppTlsModule.class,
RegistrarConsoleModule.class,
RequestModule.class, RequestModule.class,
WhiteboxModule.class, WhiteboxModule.class,
}) })
interface FrontendRequestComponent { interface FrontendRequestComponent {
ConsoleOteSetupAction consoleOteSetupAction();
ConsoleUiAction consoleUiAction(); ConsoleUiAction consoleUiAction();
EppConsoleAction eppConsoleAction(); EppConsoleAction eppConsoleAction();
EppTlsAction eppTlsAction(); EppTlsAction eppTlsAction();

View file

@ -68,6 +68,12 @@ public class AuthenticatedRegistrarAccessor {
private final String userIdForLogging; 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. * Gives all roles a user has for a given clientId.
* *
@ -103,33 +109,41 @@ public class AuthenticatedRegistrarAccessor {
@Config("registryAdminClientId") String registryAdminClientId, @Config("registryAdminClientId") String registryAdminClientId,
@Config("gSuiteSupportGroupEmailAddress") Optional<String> gSuiteSupportGroupEmailAddress, @Config("gSuiteSupportGroupEmailAddress") Optional<String> gSuiteSupportGroupEmailAddress,
Lazy<GroupsConnection> lazyGroupsConnection) { Lazy<GroupsConnection> lazyGroupsConnection) {
this( this.isAdmin = userIsAdmin(authResult, gSuiteSupportGroupEmailAddress, lazyGroupsConnection);
authResult.userIdForLogging(),
createRoleMap(
authResult,
registryAdminClientId,
lazyGroupsConnection,
gSuiteSupportGroupEmailAddress));
logger.atInfo().log( this.userIdForLogging = authResult.userIdForLogging();
"%s has the following roles: %s", authResult.userIdForLogging(), roleMap); this.roleMap = createRoleMap(authResult, this.isAdmin, registryAdminClientId);
logger.atInfo().log("%s has the following roles: %s", userIdForLogging(), roleMap);
} }
private AuthenticatedRegistrarAccessor( private AuthenticatedRegistrarAccessor(
String userIdForLogging, ImmutableSetMultimap<String, Role> roleMap) { String userIdForLogging, boolean isAdmin, ImmutableSetMultimap<String, Role> roleMap) {
this.userIdForLogging = checkNotNull(userIdForLogging); this.userIdForLogging = checkNotNull(userIdForLogging);
this.roleMap = checkNotNull(roleMap); this.roleMap = checkNotNull(roleMap);
this.isAdmin = isAdmin;
} }
/** /**
* Creates a "logged-in user" accessor with a given role map, used for tests. * Creates a "logged-in user" accessor with a given role map, used for tests.
* *
* <p>The user will be allowed to create Registrars (and hence do OT&amp;E setup) iff they have
* the role of ADMIN for at least one clientId.
*
* <p>The user's "name" in logs and exception messages is "TestUserId". * <p>The user's "name" in logs and exception messages is "TestUserId".
*/ */
@VisibleForTesting @VisibleForTesting
public static AuthenticatedRegistrarAccessor createForTesting( public static AuthenticatedRegistrarAccessor createForTesting(
ImmutableSetMultimap<String, Role> roleMap) { ImmutableSetMultimap<String, Role> 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<String> gSuiteSupportGroupEmailAddress,
Lazy<GroupsConnection> 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<String, Role> createRoleMap( private static ImmutableSetMultimap<String, Role> createRoleMap(
AuthResult authResult, AuthResult authResult,
String registryAdminClientId, boolean isAdmin,
Lazy<GroupsConnection> lazyGroupsConnection, String registryAdminClientId) {
Optional<String> gSuiteSupportGroupEmailAddress) {
if (!authResult.userAuthInfo().isPresent()) { if (!authResult.userAuthInfo().isPresent()) {
return ImmutableSetMultimap.of(); return ImmutableSetMultimap.of();
@ -274,17 +309,11 @@ public class AuthenticatedRegistrarAccessor {
UserAuthInfo userAuthInfo = authResult.userAuthInfo().get(); UserAuthInfo userAuthInfo = authResult.userAuthInfo().get();
User user = userAuthInfo.user(); 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<String, Role> builder = new ImmutableSetMultimap.Builder<>(); ImmutableSetMultimap.Builder<String, Role> builder = new ImmutableSetMultimap.Builder<>();
logger.atInfo().log("Checking registrar contacts for user ID %s", user.getUserId());
ofy() ofy()
.load() .load()
.type(RegistrarContact.class) .type(RegistrarContact.class)

View file

@ -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",
],
)

View file

@ -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&amp;E setup web page.
*
* <p>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.
*
* <p>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<SoyTofu> 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<SoyCssRenamingMap> 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<String> clientId;
@Inject @Parameter("email") Optional<String> 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<String, Object> 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<String, Object> 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<String, String> 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<String, Object> 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<String, String> 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());
}
}

View file

@ -41,4 +41,16 @@ public final class RegistrarConsoleModule {
static String provideClientId(HttpServletRequest req) { static String provideClientId(HttpServletRequest req) {
return extractRequiredParameter(req, PARAM_CLIENT_ID); return extractRequiredParameter(req, PARAM_CLIENT_ID);
} }
@Provides
@Parameter("email")
static Optional<String> provideOptionalEmail(HttpServletRequest req) {
return extractOptionalParameter(req, "email");
}
@Provides
@Parameter("email")
static String provideEmail(HttpServletRequest req) {
return extractRequiredParameter(req, "email");
}
} }

View file

@ -295,6 +295,21 @@
</tr> </tr>
{/template} {/template}
/** Submit button. */
{template .submitRow}
{@param label: string}
<tr>
<td></td>
<td>
<input
id="submit-button"
type="submit"
value="{$label}"
class="{css('kd-button')} {css('kd-button-submit')}">
</td>
</tr>
{/template}
/** Drop-down select button widget. */ /** Drop-down select button widget. */
{template .menuButton} {template .menuButton}

View file

@ -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",
)

View file

@ -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&amp;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" /}
<div id="reg-content-and-footer">
<div id="reg-content">
<h1>OT&E Setup Page</h1>
{if isNonnull($errorMessage)}
<h2 class="{css('kd-errormessage')}">Failed: {$errorMessage}</h2>
{/if}
{call .form_ data="all" /}
</div>
{call registry.soy.console.footer /}
</div>
{/template}
/**
* Result page for a successful OT&amp;E setup.
*/
{template .resultSuccess}
{@param baseClientId: string} /** The base clientId used for the OT&amp;E setup. */
{@param contactEmail: string} /** The contact's email added to the registrars. */
{@param clientIdToTld: map<string, string>} /** 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" /}
<div id="reg-content-and-footer">
<div id="reg-content" class="{css('item')} {css('registrar')}">
<h1>OT&E successfully created for registrar {$baseClientId}!</h1>
<table>
<tr>
<td>
<label class="{css('setting-label')}">EPP credentials</label>
<span class="{css('description')}">Copy and paste this into an email to the registrars</span>
</td>
<td class="{css('setting')}">
<textarea rows="7" cols="100" readonly>
{for $clientId in mapKeys($clientIdToTld)}
Login: {$clientId}{sp}
Password: {$password}{sp}
Tld: {$clientIdToTld[$clientId]}{\n}
{/for}
</textarea>
</td>
</table>
Gave <label>{$contactEmail}</label> web-console access to these registrars.
<h1>Don't forget to set the <label>Certificate</label> and <label>IP-whitelist</label> for these Registrars!</h1>
Links to the security page for your convenience:<br>
{for $clientId in mapKeys($clientIdToTld)}
<a href="/registrar?clientId={$clientId}#security-settings" target="_blank">{$clientId}</a><br>
{/for}
</div>
{call registry.soy.console.footer /}
</div>
{/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. */
<form name="item" class="{css('item')}" method="post" action="/registrar-ote-setup">
<table>
{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}
</table>
<input type="hidden" name="xsrfToken" value="{$xsrfToken}">
</form>
{/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}
<div class="{css('whoAreYou')}">
<a class="{css('logo')}" href="/registrar">
<img src="/assets/images/{$logoFilename}" alt="{$productName}">
</a>
<h1>You need permission</h1>
<p>
Only {$productName} Admins have access to this page.
You are signed in as <strong>{$username}</strong>.
<div>
<a href="{$logoutUrl}"
class="{css('kd-button')} {css('kd-button-submit')}"
tabindex="-1">Logout and switch to another account</a>{sp}
<a href="/registrar"
class="{css('kd-button')} {css('kd-button-submit')}"
tabindex="-1">Go to the Registrar web console</a>
</div>
</div>
{/template}

View file

@ -27,7 +27,6 @@
<label class="{css('setting-label')}">Allowed TLDs</label> <label class="{css('setting-label')}">Allowed TLDs</label>
<span class="{css('description')}">set or remove TLDs this <span class="{css('description')}">set or remove TLDs this
client is allowed access to.</span> client is allowed access to.</span>
</td>
<td class="{css('setting')}"> <td class="{css('setting')}">
<div class="{css('info')} {css('summary')}"> <div class="{css('info')} {css('summary')}">
<div id="tlds"> <div id="tlds">
@ -44,8 +43,11 @@
class="{css('kd-button')} {css('btn-add')}">Add</button> class="{css('kd-button')} {css('btn-add')}">Add</button>
</div> </div>
</div> </div>
</td> <tr class="{css('kd-settings-pane-section')}">
<td>
<label class="{css('setting-label')}">OT&amp;E setup page</label>
<td class="{css('setting')}">
<p>Generate new OT&amp;E accounts <a href="/registrar-ote-setup">here</a>
</table> </table>
</form> </form>
{/template} {/template}

View file

@ -1,5 +1,6 @@
PATH CLASS METHODS OK AUTH_METHODS MIN USER_POLICY PATH CLASS METHODS OK AUTH_METHODS MIN USER_POLICY
/_dr/epp EppTlsAction POST n INTERNAL,API APP PUBLIC /_dr/epp EppTlsAction POST n INTERNAL,API APP PUBLIC
/registrar ConsoleUiAction GET n INTERNAL,API,LEGACY NONE PUBLIC /registrar ConsoleUiAction GET n INTERNAL,API,LEGACY NONE PUBLIC
/registrar-settings RegistrarSettingsAction POST n API,LEGACY USER PUBLIC /registrar-ote-setup ConsoleOteSetupAction POST,GET n INTERNAL,API,LEGACY NONE PUBLIC
/registrar-xhr EppConsoleAction POST n API,LEGACY USER PUBLIC /registrar-settings RegistrarSettingsAction POST n API,LEGACY USER PUBLIC
/registrar-xhr EppConsoleAction POST n API,LEGACY USER PUBLIC

View file

@ -22,6 +22,7 @@ import static google.registry.testing.DatastoreHelper.loadRegistrar;
import static google.registry.testing.DatastoreHelper.newContactResource; import static google.registry.testing.DatastoreHelper.newContactResource;
import static google.registry.testing.DatastoreHelper.newDomainResource; import static google.registry.testing.DatastoreHelper.newDomainResource;
import static google.registry.testing.DatastoreHelper.persistActiveHost; import static google.registry.testing.DatastoreHelper.persistActiveHost;
import static google.registry.testing.DatastoreHelper.persistPremiumList;
import static google.registry.testing.DatastoreHelper.persistResource; import static google.registry.testing.DatastoreHelper.persistResource;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
@ -58,6 +59,9 @@ public enum Fixture {
public void load() { public void load() {
createTlds("xn--q9jyb4c", "example"); createTlds("xn--q9jyb4c", "example");
// Used for OT&E TLDs
persistPremiumList("default_sandbox_list");
ContactResource google = persistResource(newContactResource("google") ContactResource google = persistResource(newContactResource("google")
.asBuilder() .asBuilder()
.setLocalizedPostalInfo(new PostalInfo.Builder() .setLocalizedPostalInfo(new PostalInfo.Builder()

View file

@ -85,7 +85,8 @@ public final class RegistryTestServer {
// Registrar Console // Registrar Console
route("/registrar", FrontendServlet.class), route("/registrar", FrontendServlet.class),
route("/registrar-settings", FrontendServlet.class)); route("/registrar-settings", FrontendServlet.class),
route("/registrar-ote-setup", FrontendServlet.class));
private static final ImmutableList<Class<? extends Filter>> FILTERS = ImmutableList.of( private static final ImmutableList<Class<? extends Filter>> FILTERS = ImmutableList.of(
ObjectifyFilter.class, ObjectifyFilter.class,

View file

@ -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"],
)

View file

@ -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("<h1>OT&E Setup Page</h1>");
}
@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("<h1>You need permission</h1>");
}
@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("<h1>OT&E successfully created for registrar myclientid!</h1>");
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("<h1>You need permission</h1>");
}
}