Create console settings contact endpoints (#2033)

This commit is contained in:
Pavlo Tkach 2023-05-31 16:34:57 -04:00 committed by GitHub
parent 74baae397a
commit db1b92638f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 470 additions and 23 deletions

View file

@ -29,6 +29,7 @@ import static java.util.stream.Collectors.joining;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.ImmutableSortedSet;
import com.google.gson.annotations.Expose;
import google.registry.model.Buildable;
import google.registry.model.ImmutableObject;
import google.registry.model.JsonMapBuilder;
@ -94,7 +95,7 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
}
/** The name of the contact. */
String name;
@Expose String name;
/**
* The contact email address of the contact.
@ -102,24 +103,24 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
* <p>This is different from the login email which is assgined to the regstrar and cannot be
* changed.
*/
@Id String emailAddress;
@Expose @Id String emailAddress;
@Id String registrarId;
@Expose @Id public String registrarId;
/** External email address of this contact used for registry lock confirmations. */
String registryLockEmailAddress;
/** The voice number of the contact. */
String phoneNumber;
@Expose String phoneNumber;
/** The fax number of the contact. */
String faxNumber;
@Expose String faxNumber;
/**
* Multiple types are used to associate the registrar contact with various mailing groups. This
* data is internal to the registry.
*/
Set<Type> types;
@Expose Set<Type> types;
/** A GAIA email address that was assigned to the registrar for console login purpose. */
String loginEmailAddress;
@ -127,19 +128,19 @@ public class RegistrarPoc extends ImmutableObject implements Jsonifiable, Unsafe
/**
* Whether this contact is publicly visible in WHOIS registrar query results as an Admin contact.
*/
boolean visibleInWhoisAsAdmin = false;
@Expose boolean visibleInWhoisAsAdmin = false;
/**
* Whether this contact is publicly visible in WHOIS registrar query results as a Technical
* contact.
*/
boolean visibleInWhoisAsTech = false;
@Expose boolean visibleInWhoisAsTech = false;
/**
* Whether this contact's phone number and email address is publicly visible in WHOIS domain query
* results as registrar abuse contact info.
*/
boolean visibleInDomainWhoisAsAbuse = false;
@Expose boolean visibleInDomainWhoisAsAbuse = false;
/**
* Whether the contact is allowed to set their registry lock password through the registrar

View file

@ -26,6 +26,7 @@ import google.registry.request.RequestComponentBuilder;
import google.registry.request.RequestModule;
import google.registry.request.RequestScope;
import google.registry.ui.server.console.ConsoleDomainGetAction;
import google.registry.ui.server.console.settings.ContactAction;
import google.registry.ui.server.registrar.ConsoleOteSetupAction;
import google.registry.ui.server.registrar.ConsoleRegistrarCreatorAction;
import google.registry.ui.server.registrar.ConsoleUiAction;
@ -64,6 +65,8 @@ interface FrontendRequestComponent {
ConsoleDomainGetAction consoleDomainGetAction();
ContactAction contactAction();
@Subcomponent.Builder
abstract class Builder implements RequestComponentBuilder<FrontendRequestComponent> {
@Override public abstract Builder requestModule(RequestModule requestModule);

View file

@ -0,0 +1,29 @@
// Copyright 2023 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.request;
import java.lang.annotation.Documented;
import javax.inject.Qualifier;
/**
* Dagger qualifier for the HTTP request payload as parsed JSON wrapped in Optional. Can be used for
* any kind of request methods - GET, POST, etc. Will provide Optional.empty() if body is not
* present.
*
* @see RequestModule
*/
@Qualifier
@Documented
public @interface OptionalJsonPayload {}

View file

@ -31,6 +31,8 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.io.ByteStreams;
import com.google.common.io.CharStreams;
import com.google.common.net.MediaType;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.protobuf.ByteString;
import dagger.Module;
import dagger.Provides;
@ -194,8 +196,7 @@ public final class RequestModule {
@JsonPayload
@SuppressWarnings("unchecked")
static Map<String, Object> provideJsonPayload(
@Header("Content-Type") MediaType contentType,
@Payload String payload) {
@Header("Content-Type") MediaType contentType, @Payload String payload) {
if (!JSON_UTF_8.is(contentType.withCharset(UTF_8))) {
throw new UnsupportedMediaTypeException(
String.format("Expected %s Content-Type", JSON_UTF_8.withoutParameters()));
@ -247,4 +248,15 @@ public final class RequestModule {
static Optional<Integer> provideCloudTasksRetryCount(HttpServletRequest req) {
return extractOptionalHeader(req, CLOUD_TASKS_RETRY_HEADER).map(Integer::parseInt);
}
@Provides
@OptionalJsonPayload
public static Optional<JsonObject> provideJsonBody(HttpServletRequest req, Gson gson) {
try {
JsonObject body = gson.fromJson(req.getReader(), JsonObject.class);
return Optional.of(body);
} catch (Exception e) {
return Optional.empty();
}
}
}

View file

@ -0,0 +1,145 @@
// Copyright 2023 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.console.settings;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.GET;
import static google.registry.request.Action.Method.POST;
import com.google.api.client.http.HttpStatusCodes;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.flogger.FluentLogger;
import com.google.gson.Gson;
import google.registry.model.console.ConsolePermission;
import google.registry.model.console.User;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.persistence.transaction.QueryComposer.Comparator;
import google.registry.request.Action;
import google.registry.request.Parameter;
import google.registry.request.Response;
import google.registry.request.auth.Auth;
import google.registry.request.auth.AuthResult;
import google.registry.ui.forms.FormException;
import google.registry.ui.server.registrar.JsonGetAction;
import google.registry.ui.server.registrar.RegistrarSettingsAction;
import java.util.Collections;
import java.util.Optional;
import javax.inject.Inject;
import javax.servlet.http.HttpServletRequest;
@Action(
service = Action.Service.DEFAULT,
path = ContactAction.PATH,
method = {GET, POST},
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public class ContactAction implements JsonGetAction {
static final String PATH = "/console-api/settings/contacts/";
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
private final HttpServletRequest req;
private final AuthResult authResult;
private final Response response;
private final Gson gson;
private final Optional<ImmutableSet<RegistrarPoc>> contacts;
private final String registrarId;
@Inject
public ContactAction(
HttpServletRequest req,
AuthResult authResult,
Response response,
Gson gson,
@Parameter("registrarId") String registrarId,
@Parameter("contacts") Optional<ImmutableSet<RegistrarPoc>> contacts) {
this.authResult = authResult;
this.response = response;
this.gson = gson;
this.registrarId = registrarId;
this.contacts = contacts;
this.req = req;
}
@Override
public void run() {
User user = authResult.userAuthInfo().get().consoleUser().get();
if (req.getMethod().equals(GET.toString())) {
getHandler(user);
} else {
postHandler(user);
}
}
private void getHandler(User user) {
if (!user.getUserRoles().hasPermission(registrarId, ConsolePermission.VIEW_REGISTRAR_DETAILS)) {
response.setStatus(HttpStatusCodes.STATUS_CODE_FORBIDDEN);
return;
}
ImmutableList<RegistrarPoc> am =
tm().transact(
() ->
tm().createQueryComposer(RegistrarPoc.class)
.where("registrarId", Comparator.EQ, registrarId)
.list());
response.setStatus(HttpStatusCodes.STATUS_CODE_OK);
response.setPayload(gson.toJson(am));
}
private void postHandler(User user) {
if (!user.getUserRoles().hasPermission(registrarId, ConsolePermission.EDIT_REGISTRAR_DETAILS)) {
response.setStatus(HttpStatusCodes.STATUS_CODE_FORBIDDEN);
return;
}
if (!contacts.isPresent()) {
response.setStatus(HttpStatusCodes.STATUS_CODE_BAD_REQUEST);
response.setPayload(gson.toJson("Contacts parameter is not present"));
return;
}
Registrar registrar =
Registrar.loadByRegistrarId(registrarId)
.orElseThrow(
() ->
new IllegalArgumentException(
String.format("Unknown registrar %s", registrarId)));
ImmutableSet<RegistrarPoc> oldContacts = registrar.getContacts();
// TODO: @ptkach - refactor out contacts update functionality after RegistrarSettingsAction is
// deprecated
ImmutableSet<RegistrarPoc> updatedContacts =
RegistrarSettingsAction.readContacts(
registrar,
oldContacts,
Collections.singletonMap(
"contacts",
contacts.get().stream().map(c -> c.toJsonMap()).collect(toImmutableList())));
try {
RegistrarSettingsAction.checkContactRequirements(oldContacts, updatedContacts);
} catch (FormException e) {
logger.atWarning().withCause(e).log(
"Error processing contacts post request for registrar: %s", registrarId);
response.setStatus(HttpStatusCodes.STATUS_CODE_BAD_REQUEST);
response.setPayload(e.getMessage());
return;
}
RegistrarPoc.updateContacts(registrar, updatedContacts);
response.setStatus(HttpStatusCodes.STATUS_CODE_OK);
}
}

View file

@ -19,8 +19,13 @@ import static google.registry.request.RequestParameters.extractOptionalIntParame
import static google.registry.request.RequestParameters.extractOptionalParameter;
import static google.registry.request.RequestParameters.extractRequiredParameter;
import com.google.common.collect.ImmutableSet;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import dagger.Module;
import dagger.Provides;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.request.OptionalJsonPayload;
import google.registry.request.Parameter;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
@ -162,4 +167,24 @@ public final class RegistrarConsoleModule {
static String provideDomain(HttpServletRequest req) {
return extractRequiredParameter(req, "domain");
}
@Provides
@Parameter("contacts")
public static Optional<ImmutableSet<RegistrarPoc>> provideContacts(
Gson gson, @OptionalJsonPayload Optional<JsonObject> payload) {
if (payload.isPresent() && payload.get().has("contacts")) {
return Optional.of(
ImmutableSet.copyOf(
gson.fromJson(payload.get().get("contacts").getAsJsonArray(), RegistrarPoc[].class)));
}
return Optional.empty();
}
@Provides
@Parameter("registrarId")
static String provideRegistrarId(HttpServletRequest req) {
return extractRequiredParameter(req, "registrarId");
}
}

View file

@ -473,7 +473,7 @@ public class RegistrarSettingsAction implements Runnable, JsonActionRunner.JsonA
*
* @throws FormException if the checks fail.
*/
void checkContactRequirements(
public static void checkContactRequirements(
ImmutableSet<RegistrarPoc> existingContacts, ImmutableSet<RegistrarPoc> updatedContacts) {
// Check that no two contacts use the same email address.
Set<String> emails = new HashSet<>();

View file

@ -0,0 +1,231 @@
// Copyright 2023 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.console.settings;
import static com.google.common.collect.ImmutableList.toImmutableList;
import static com.google.common.truth.Truth.assertThat;
import static google.registry.model.registrar.RegistrarPoc.Type.WHOIS;
import static google.registry.testing.DatabaseHelper.insertInDb;
import static google.registry.testing.DatabaseHelper.loadAllOf;
import static google.registry.testing.SqlHelper.saveRegistrar;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.google.api.client.http.HttpStatusCodes;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.gson.Gson;
import google.registry.model.console.GlobalRole;
import google.registry.model.console.RegistrarRole;
import google.registry.model.console.User;
import google.registry.model.console.UserRoles;
import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.request.Action;
import google.registry.request.RequestModule;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.AuthSettings.AuthLevel;
import google.registry.request.auth.UserAuthInfo;
import google.registry.testing.FakeResponse;
import google.registry.ui.server.registrar.RegistrarConsoleModule;
import google.registry.util.UtilsModule;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.HashMap;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Tests for {@link google.registry.ui.server.console.settings.ContactAction}. */
class ContactActionTest {
private static String jsonRegistrar1 =
"{\"name\":\"Test Registrar 1\","
+ "\"emailAddress\":\"test.registrar1@example.com\","
+ "\"registrarId\":\"registrarId\","
+ "\"phoneNumber\":\"+1.9999999999\",\"faxNumber\":\"+1.9999999991\","
+ "\"types\":[\"WHOIS\"],\"visibleInWhoisAsAdmin\":true,"
+ "\"visibleInWhoisAsTech\":false,\"visibleInDomainWhoisAsAbuse\":false}";
private static String jsonRegistrar2 =
"{\"name\":\"Test Registrar 2\","
+ "\"emailAddress\":\"test.registrar2@example.com\","
+ "\"registrarId\":\"registrarId\","
+ "\"phoneNumber\":\"+1.1234567890\",\"faxNumber\":\"+1.1234567891\","
+ "\"types\":[\"WHOIS\"],\"visibleInWhoisAsAdmin\":true,"
+ "\"visibleInWhoisAsTech\":false,\"visibleInDomainWhoisAsAbuse\":false}";
private Registrar testRegistrar;
private final HttpServletRequest request = mock(HttpServletRequest.class);
private RegistrarPoc testRegistrarPoc;
private static final Gson GSON = UtilsModule.provideGson();
private static final FakeResponse RESPONSE = new FakeResponse();
@RegisterExtension
final JpaTestExtensions.JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().buildIntegrationTestExtension();
@BeforeEach
void beforeEach() {
testRegistrar = saveRegistrar("registrarId");
testRegistrarPoc =
new RegistrarPoc.Builder()
.setRegistrar(testRegistrar)
.setName("Test Registrar 1")
.setEmailAddress("test.registrar1@example.com")
.setPhoneNumber("+1.9999999999")
.setFaxNumber("+1.9999999991")
.setTypes(ImmutableSet.of(WHOIS))
.setVisibleInWhoisAsAdmin(true)
.setVisibleInWhoisAsTech(false)
.setVisibleInDomainWhoisAsAbuse(false)
.build();
}
@Test
void testSuccess_getContactInfo() throws IOException {
insertInDb(testRegistrarPoc);
ContactAction action =
createAction(
Action.Method.GET,
AuthResult.create(
AuthLevel.USER,
UserAuthInfo.create(
createUser(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build()))),
testRegistrar.getRegistrarId(),
null);
action.run();
assertThat(RESPONSE.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_OK);
assertThat(RESPONSE.getPayload()).isEqualTo("[" + jsonRegistrar1 + "]");
}
@Test
void testSuccess_postCreateContactInfo() throws IOException {
ContactAction action =
createAction(
Action.Method.POST,
AuthResult.create(
AuthLevel.USER,
UserAuthInfo.create(
createUser(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build()))),
testRegistrar.getRegistrarId(),
"[" + jsonRegistrar1 + "," + jsonRegistrar2 + "]");
action.run();
assertThat(RESPONSE.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_OK);
assertThat(
loadAllOf(RegistrarPoc.class).stream()
.filter(r -> r.registrarId.equals(testRegistrar.getRegistrarId()))
.map(r -> r.getName())
.collect(toImmutableList()))
.containsExactly("Test Registrar 1", "Test Registrar 2");
}
@Test
void testSuccess_postUpdateContactInfo() throws IOException {
testRegistrarPoc = testRegistrarPoc.asBuilder().setEmailAddress("incorrect@email.com").build();
insertInDb(testRegistrarPoc);
ContactAction action =
createAction(
Action.Method.POST,
AuthResult.create(
AuthLevel.USER,
UserAuthInfo.create(
createUser(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build()))),
testRegistrar.getRegistrarId(),
"[" + jsonRegistrar1 + "," + jsonRegistrar2 + "]");
action.run();
assertThat(RESPONSE.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_OK);
HashMap<String, String> testResult = new HashMap<>();
loadAllOf(RegistrarPoc.class).stream()
.filter(r -> r.registrarId.equals(testRegistrar.getRegistrarId()))
.forEach(r -> testResult.put(r.getName(), r.getEmailAddress()));
assertThat(testResult)
.containsExactly(
"Test Registrar 1",
"test.registrar1@example.com",
"Test Registrar 2",
"test.registrar2@example.com");
}
@Test
void testSuccess_postDeleteContactInfo() throws IOException {
insertInDb(testRegistrarPoc);
ContactAction action =
createAction(
Action.Method.POST,
AuthResult.create(
AuthLevel.USER,
UserAuthInfo.create(
createUser(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build()))),
testRegistrar.getRegistrarId(),
"[" + jsonRegistrar2 + "]");
action.run();
assertThat(RESPONSE.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_OK);
assertThat(
loadAllOf(RegistrarPoc.class).stream()
.filter(r -> r.registrarId.equals(testRegistrar.getRegistrarId()))
.map(r -> r.getName())
.collect(toImmutableList()))
.containsExactly("Test Registrar 2");
}
@Test
void testFailure_postDeleteContactInfo_missingPermission() throws IOException {
insertInDb(testRegistrarPoc);
ContactAction action =
createAction(
Action.Method.POST,
AuthResult.create(
AuthLevel.USER,
UserAuthInfo.create(
createUser(
new UserRoles.Builder()
.setRegistrarRoles(
ImmutableMap.of(
testRegistrar.getRegistrarId(), RegistrarRole.ACCOUNT_MANAGER))
.build()))),
testRegistrar.getRegistrarId(),
"[" + jsonRegistrar2 + "]");
action.run();
assertThat(RESPONSE.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_FORBIDDEN);
}
private User createUser(UserRoles userRoles) {
return new User.Builder()
.setEmailAddress("email@email.com")
.setGaiaId("gaiaId")
.setUserRoles(userRoles)
.build();
}
private ContactAction createAction(
Action.Method method, AuthResult authResult, String registrarId, String contacts)
throws IOException {
when(request.getMethod()).thenReturn(method.toString());
if (method.equals(Action.Method.GET)) {
return new ContactAction(request, authResult, RESPONSE, GSON, registrarId, Optional.empty());
} else {
when(request.getReader())
.thenReturn(new BufferedReader(new StringReader("{\"contacts\":" + contacts + "}")));
Optional<ImmutableSet<RegistrarPoc>> maybeContacts =
RegistrarConsoleModule.provideContacts(
GSON, RequestModule.provideJsonBody(request, GSON));
return new ContactAction(request, authResult, RESPONSE, GSON, registrarId, maybeContacts);
}
}
}

View file

@ -1,11 +1,12 @@
PATH CLASS METHODS OK AUTH_METHODS MIN USER_POLICY
/_dr/epp EppTlsAction POST n INTERNAL,API APP PUBLIC
/console-api/domain ConsoleDomainGetAction GET n API,LEGACY USER PUBLIC
/registrar ConsoleUiAction GET n INTERNAL,API,LEGACY NONE PUBLIC
/registrar-create ConsoleRegistrarCreatorAction POST,GET n INTERNAL,API,LEGACY NONE PUBLIC
/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
/registry-lock-post RegistryLockPostAction POST n API,LEGACY USER PUBLIC
/registry-lock-verify RegistryLockVerifyAction GET n API,LEGACY USER PUBLIC
PATH CLASS METHODS OK AUTH_METHODS MIN USER_POLICY
/_dr/epp EppTlsAction POST n INTERNAL,API APP PUBLIC
/console-api/domain ConsoleDomainGetAction GET n API,LEGACY USER PUBLIC
/console-api/settings/contacts/ ContactAction GET,POST n API,LEGACY USER PUBLIC
/registrar ConsoleUiAction GET n INTERNAL,API,LEGACY NONE PUBLIC
/registrar-create ConsoleRegistrarCreatorAction POST,GET n INTERNAL,API,LEGACY NONE PUBLIC
/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
/registry-lock-post RegistryLockPostAction POST n API,LEGACY USER PUBLIC
/registry-lock-verify RegistryLockVerifyAction GET n API,LEGACY USER PUBLIC