Add a GET action and tests for registry lock retrieval (#326)

* Add a GET action and tests for registry lock retrieval

* Create isVerified method

* Allow lock access for admins even if they're not enabled on the registrar

* Simple CR responses

* Move locks retrieval to the GET action

* add newline at eof

* Switch to using ID
This commit is contained in:
gbrodman 2019-11-05 13:19:32 -05:00 committed by GitHub
parent 301ab54fb4
commit 86e1fb85b6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 444 additions and 2 deletions

View file

@ -55,6 +55,12 @@
<url-pattern>/registrar-settings</url-pattern>
</servlet-mapping>
<!-- Registry lock get/post/verify. -->
<servlet-mapping>
<servlet-name>frontend-servlet</servlet-name>
<url-pattern>/registry-lock-get</url-pattern>
</servlet-mapping>
<!-- Security config -->
<security-constraint>
<web-resource-collection>

View file

@ -30,6 +30,7 @@ import google.registry.ui.server.registrar.ConsoleUiAction;
import google.registry.ui.server.registrar.OteStatusAction;
import google.registry.ui.server.registrar.RegistrarConsoleModule;
import google.registry.ui.server.registrar.RegistrarSettingsAction;
import google.registry.ui.server.registrar.RegistryLockGetAction;
/** Dagger component with per-request lifetime for "default" App Engine module. */
@RequestScope
@ -50,6 +51,8 @@ interface FrontendRequestComponent {
OteStatusAction oteStatusAction();
RegistrarSettingsAction registrarSettingsAction();
RegistryLockGetAction registryLockGetAction();
@Subcomponent.Builder
abstract class Builder implements RequestComponentBuilder<FrontendRequestComponent> {
@Override public abstract Builder requestModule(RequestModule requestModule);

View file

@ -347,7 +347,7 @@ public class AuthenticatedRegistrarAccessor {
/** Exception thrown when the current user doesn't have access to the requested Registrar. */
public static class RegistrarAccessDeniedException extends Exception {
RegistrarAccessDeniedException(String message) {
public RegistrarAccessDeniedException(String message) {
super(message);
}
}

View file

@ -176,6 +176,10 @@ public final class RegistryLock extends ImmutableObject implements Buildable {
this.completionTimestamp = toZonedDateTime(dateTime);
}
public boolean isVerified() {
return completionTimestamp != null;
}
@Override
public Builder asBuilder() {
return new Builder(clone(this));

View file

@ -0,0 +1,171 @@
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.ui.server.registrar;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.net.HttpHeaders.X_FRAME_OPTIONS;
import static google.registry.security.JsonResponseHelper.Status.SUCCESS;
import static google.registry.ui.server.registrar.RegistrarConsoleModule.PARAM_CLIENT_ID;
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import com.google.appengine.api.users.User;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.MediaType;
import com.google.gson.Gson;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarContact;
import google.registry.model.registry.RegistryLockDao;
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.request.auth.AuthenticatedRegistrarAccessor.RegistrarAccessDeniedException;
import google.registry.request.auth.UserAuthInfo;
import google.registry.schema.domain.RegistryLock;
import google.registry.security.JsonResponseHelper;
import java.util.Optional;
import javax.inject.Inject;
import org.joda.time.DateTime;
/**
* Servlet that allows for getting locks for a particular registrar.
*
* <p>Note: at the moment we have no mechanism for JSON GET/POSTs in the same class or at the same
* URL, which is why this is distinct from the {@link RegistryLockPostAction}.
*/
@Action(
service = Action.Service.DEFAULT,
path = RegistryLockGetAction.PATH,
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public final class RegistryLockGetAction implements Runnable {
public static final String PATH = "/registry-lock-get";
private static final String LOCK_ENABLED_FOR_CONTACT_PARAM = "lockEnabledForContact";
private static final String EMAIL_PARAM = "email";
private static final String LOCKS_PARAM = "locks";
private static final String FULLY_QUALIFIED_DOMAIN_NAME_PARAM = "fullyQualifiedDomainName";
private static final String LOCKED_TIME_PARAM = "lockedTime";
private static final String LOCKED_BY_PARAM = "lockedBy";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private static final Gson GSON = new Gson();
@VisibleForTesting Method method;
private final Response response;
private final AuthenticatedRegistrarAccessor registrarAccessor;
@VisibleForTesting AuthResult authResult;
@VisibleForTesting Optional<String> paramClientId;
@Inject
RegistryLockGetAction(
@RequestMethod Method method,
Response response,
AuthenticatedRegistrarAccessor registrarAccessor,
AuthResult authResult,
@Parameter(PARAM_CLIENT_ID) Optional<String> paramClientId) {
this.method = method;
this.response = response;
this.registrarAccessor = registrarAccessor;
this.authResult = authResult;
this.paramClientId = paramClientId;
}
@Override
public void run() {
checkArgument(Method.GET.equals(method), "Only GET requests allowed");
checkArgument(authResult.userAuthInfo().isPresent(), "User auth info must be present");
checkArgument(paramClientId.isPresent(), "clientId must be present");
response.setContentType(MediaType.JSON_UTF_8);
response.setHeader(X_FRAME_OPTIONS, "SAMEORIGIN"); // Disallow iframing.
response.setHeader("X-Ui-Compatible", "IE=edge"); // Ask IE not to be silly.
try {
ImmutableMap<String, ?> resultMap = getLockedDomainsMap(paramClientId.get());
ImmutableMap<String, ?> payload =
JsonResponseHelper.create(SUCCESS, "Successful locks retrieval", resultMap);
response.setPayload(GSON.toJson(payload));
} catch (RegistrarAccessDeniedException e) {
logger.atWarning().withCause(e).log(
"User %s doesn't have access to this registrar", authResult.userIdForLogging());
response.setStatus(SC_FORBIDDEN);
} catch (Exception e) {
logger.atWarning().withCause(e).log("Unexpected error when retrieving locks for a registrar");
response.setStatus(SC_INTERNAL_SERVER_ERROR);
}
}
private ImmutableMap<String, ?> getLockedDomainsMap(String clientId)
throws RegistrarAccessDeniedException {
// Note: admins always have access to the locks page
checkArgument(authResult.userAuthInfo().isPresent(), "User auth info must be present");
UserAuthInfo userAuthInfo = authResult.userAuthInfo().get();
boolean isAdmin = userAuthInfo.isUserAdmin();
Registrar registrar = getRegistrarAndVerifyLockAccess(clientId, isAdmin);
User user = userAuthInfo.user();
boolean isRegistryLockAllowed =
isAdmin
|| registrar.getContacts().stream()
.filter(contact -> contact.getEmailAddress().equals(user.getEmail()))
.findFirst()
.map(RegistrarContact::isRegistryLockAllowed)
.orElse(false);
return ImmutableMap.of(
LOCK_ENABLED_FOR_CONTACT_PARAM,
isRegistryLockAllowed,
EMAIL_PARAM,
user.getEmail(),
PARAM_CLIENT_ID,
registrar.getClientId(),
LOCKS_PARAM,
getLockedDomains(clientId));
}
private Registrar getRegistrarAndVerifyLockAccess(String clientId, boolean isAdmin)
throws RegistrarAccessDeniedException {
Registrar registrar = registrarAccessor.getRegistrar(clientId);
checkArgument(
isAdmin || registrar.isRegistryLockAllowed(),
"Registry lock not allowed for this registrar");
return registrar;
}
private ImmutableList<ImmutableMap<String, ?>> getLockedDomains(String clientId) {
ImmutableList<RegistryLock> locks =
RegistryLockDao.getByRegistrarId(clientId).stream()
.filter(RegistryLock::isVerified)
.collect(toImmutableList());
return locks.stream().map(this::lockToMap).collect(toImmutableList());
}
private ImmutableMap<String, ?> lockToMap(RegistryLock lock) {
return ImmutableMap.of(
FULLY_QUALIFIED_DOMAIN_NAME_PARAM,
lock.getDomainName(),
LOCKED_TIME_PARAM,
lock.getCompletionTimestamp().map(DateTime::toString).orElse(""),
LOCKED_BY_PARAM,
lock.isSuperuser() ? "admin" : lock.getRegistrarPocId());
}
}

View file

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

View file

@ -0,0 +1,256 @@
// Copyright 2019 The Nomulus Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package google.registry.ui.server.registrar;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.request.auth.AuthenticatedRegistrarAccessor.Role.OWNER;
import static google.registry.testing.AppEngineRule.makeRegistrar2;
import static google.registry.testing.AppEngineRule.makeRegistrarContact3;
import static google.registry.testing.DatastoreHelper.persistResource;
import static google.registry.testing.JUnitBackports.assertThrows;
import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
import com.google.api.client.http.HttpStatusCodes;
import com.google.appengine.api.users.User;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.gson.Gson;
import google.registry.model.registry.RegistryLockDao;
import google.registry.model.transaction.JpaTransactionManagerRule;
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.schema.domain.RegistryLock;
import google.registry.schema.domain.RegistryLock.Action;
import google.registry.testing.AppEngineRule;
import google.registry.testing.FakeResponse;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import org.joda.time.DateTime;
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.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
/** Unit tests for {@link RegistryLockGetAction}. */
@RunWith(JUnit4.class)
public final class RegistryLockGetActionTest {
private static final Gson GSON = new Gson();
@Rule public final AppEngineRule appEngineRule = AppEngineRule.builder().withDatastore().build();
@Rule
public final JpaTransactionManagerRule jpaTmRule =
new JpaTransactionManagerRule.Builder().build();
@Rule public final MockitoRule mocks = MockitoJUnit.rule();
private final FakeResponse response = new FakeResponse();
private final User user = new User("Marla.Singer@crr.com", "gmail.com", "12345");
private AuthResult authResult;
private AuthenticatedRegistrarAccessor accessor;
private RegistryLockGetAction action;
@Before
public void setup() {
jpaTmRule.getTxnClock().setTo(DateTime.parse("2000-06-08T22:00:00.0Z"));
authResult = AuthResult.create(AuthLevel.USER, UserAuthInfo.create(user, false));
accessor =
AuthenticatedRegistrarAccessor.createForTesting(
ImmutableSetMultimap.of(
"TheRegistrar", OWNER,
"NewRegistrar", OWNER));
action =
new RegistryLockGetAction(
Method.GET, response, accessor, authResult, Optional.of("TheRegistrar"));
}
@Test
public void testSuccess_retrievesLocks() {
RegistryLock regularLock =
new RegistryLock.Builder()
.setRepoId("repoId")
.setDomainName("example.test")
.setRegistrarId("TheRegistrar")
.setAction(Action.LOCK)
.setVerificationCode(UUID.randomUUID().toString())
.setRegistrarPocId("johndoe@theregistrar.com")
.setCompletionTimestamp(jpaTmRule.getTxnClock().nowUtc())
.build();
jpaTmRule.getTxnClock().advanceOneMilli();
RegistryLock adminLock =
new RegistryLock.Builder()
.setRepoId("repoId")
.setDomainName("adminexample.test")
.setRegistrarId("TheRegistrar")
.setAction(Action.LOCK)
.setVerificationCode(UUID.randomUUID().toString())
.isSuperuser(true)
.setCompletionTimestamp(jpaTmRule.getTxnClock().nowUtc())
.build();
RegistryLock incompleteLock =
new RegistryLock.Builder()
.setRepoId("repoId")
.setDomainName("incomplete.test")
.setRegistrarId("TheRegistrar")
.setAction(Action.LOCK)
.setVerificationCode(UUID.randomUUID().toString())
.setRegistrarPocId("johndoe@theregistrar.com")
.build();
RegistryLockDao.save(regularLock);
RegistryLockDao.save(adminLock);
RegistryLockDao.save(incompleteLock);
action.run();
assertThat(response.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_OK);
assertThat(GSON.fromJson(response.getPayload(), Map.class))
.containsExactly(
"status", "SUCCESS",
"message", "Successful locks retrieval",
"results",
ImmutableList.of(
ImmutableMap.of(
"lockEnabledForContact", true,
"email", "Marla.Singer@crr.com",
"clientId", "TheRegistrar",
"locks",
ImmutableList.of(
ImmutableMap.of(
"fullyQualifiedDomainName", "example.test",
"lockedTime", "2000-06-08T22:00:00.000Z",
"lockedBy", "johndoe@theregistrar.com"),
ImmutableMap.of(
"fullyQualifiedDomainName", "adminexample.test",
"lockedTime", "2000-06-08T22:00:00.001Z",
"lockedBy", "admin")))));
}
@Test
public void testFailure_invalidMethod() {
action.method = Method.POST;
assertThat(assertThrows(IllegalArgumentException.class, action::run))
.hasMessageThat()
.isEqualTo("Only GET requests allowed");
}
@Test
public void testFailure_noAuthInfo() {
action.authResult = AuthResult.NOT_AUTHENTICATED;
assertThat(assertThrows(IllegalArgumentException.class, action::run))
.hasMessageThat()
.isEqualTo("User auth info must be present");
}
@Test
public void testFailure_noClientId() {
action.paramClientId = Optional.empty();
assertThat(assertThrows(IllegalArgumentException.class, action::run))
.hasMessageThat()
.isEqualTo("clientId must be present");
}
@Test
public void testFailure_noRegistrarAccess() {
accessor = AuthenticatedRegistrarAccessor.createForTesting(ImmutableSetMultimap.of());
action =
new RegistryLockGetAction(
Method.GET, response, accessor, authResult, Optional.of("TheRegistrar"));
action.run();
assertThat(response.getStatus()).isEqualTo(SC_FORBIDDEN);
}
@Test
public void testSuccess_readOnlyAccessForOtherUsers() {
// If lock is not enabled for a user, this should be read-only
persistResource(
makeRegistrarContact3().asBuilder().setAllowedToSetRegistryLockPassword(true).build());
action.run();
assertThat(GSON.fromJson(response.getPayload(), Map.class).get("results"))
.isEqualTo(
ImmutableList.of(
ImmutableMap.of(
"lockEnabledForContact",
false,
"email",
"Marla.Singer@crr.com",
"clientId",
"TheRegistrar",
"locks",
ImmutableList.of())));
}
@Test
public void testSuccess_lockAllowedForAdmin() throws Exception {
// Locks are allowed for admins even when they're not enabled for the registrar
persistResource(makeRegistrar2().asBuilder().setRegistryLockAllowed(false).build());
authResult = AuthResult.create(AuthLevel.USER, UserAuthInfo.create(user, true));
action =
new RegistryLockGetAction(
Method.GET, response, accessor, authResult, Optional.of("TheRegistrar"));
action.run();
assertThat(GSON.fromJson(response.getPayload(), Map.class).get("results"))
.isEqualTo(
ImmutableList.of(
ImmutableMap.of(
"lockEnabledForContact",
true,
"email",
"Marla.Singer@crr.com",
"clientId",
"TheRegistrar",
"locks",
ImmutableList.of())));
}
@Test
public void testFailure_lockNotAllowedForRegistrar() {
// The UI shouldn't be making requests where lock isn't enabled for this registrar
action =
new RegistryLockGetAction(
Method.GET, response, accessor, authResult, Optional.of("NewRegistrar"));
action.run();
assertThat(response.getStatus()).isEqualTo(SC_INTERNAL_SERVER_ERROR);
}
@Test
public void testFailure_accessDenied() {
accessor = AuthenticatedRegistrarAccessor.createForTesting(ImmutableSetMultimap.of());
action =
new RegistryLockGetAction(
Method.GET, response, accessor, authResult, Optional.of("TheRegistrar"));
action.run();
assertThat(response.getStatus()).isEqualTo(SC_FORBIDDEN);
}
@Test
public void testFailure_badRegistrar() {
action =
new RegistryLockGetAction(
Method.GET, response, accessor, authResult, Optional.of("SomeBadRegistrar"));
action.run();
assertThat(response.getStatus()).isEqualTo(SC_FORBIDDEN);
}
}

View file

@ -5,3 +5,4 @@ PATH CLASS METHODS OK AUTH_METHODS
/registrar-ote-setup ConsoleOteSetupAction POST,GET n INTERNAL,API,LEGACY NONE PUBLIC
/registrar-ote-status OteStatusAction POST n API,LEGACY USER PUBLIC
/registrar-settings RegistrarSettingsAction POST n API,LEGACY USER PUBLIC
/registry-lock-get RegistryLockGetAction GET n API,LEGACY USER PUBLIC