1
0
mirror of https://github.com/google/nomulus synced 2026-01-08 15:21:46 +00:00

Add backend for editing whois-visible fields (#2100)

This includes a bit of refactoring of the GSON creation. There can exist
some objects (e.g. Address) where the JSON representation is not equal to the
representation that we store in the database. For these objects, when
deserializing, we should update the objects so that they reflect the
proper DB structure (indeed, this is already what we do for the XML
parsing of Address).
This commit is contained in:
gbrodman
2023-08-22 16:40:02 -04:00
committed by GitHub
parent 1dcbc9e0cb
commit 97676d1a1f
9 changed files with 403 additions and 40 deletions

View File

@@ -26,6 +26,7 @@ import google.registry.model.ImmutableObject;
import google.registry.model.JsonMapBuilder;
import google.registry.model.Jsonifiable;
import google.registry.model.UnsafeSerializable;
import google.registry.tools.GsonUtils.GsonPostProcessable;
import java.util.List;
import java.util.Map;
import java.util.Objects;
@@ -55,7 +56,8 @@ import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
@XmlTransient
@Embeddable
@MappedSuperclass
public class Address extends ImmutableObject implements Jsonifiable, UnsafeSerializable {
public class Address extends ImmutableObject
implements Jsonifiable, UnsafeSerializable, GsonPostProcessable {
/**
* At most three lines of addresses parsed from XML elements.
@@ -152,6 +154,16 @@ public class Address extends ImmutableObject implements Jsonifiable, UnsafeSeria
return new Builder<>(clone(this));
}
@Override
public void postProcess() {
if (street == null || street.isEmpty()) {
return;
}
streetLine1 = street.get(0);
streetLine2 = street.size() >= 2 ? street.get(1) : null;
streetLine3 = street.size() >= 3 ? street.get(2) : null;
}
/** A builder for constructing {@link Address}. */
public static class Builder<T extends Address> extends Buildable.Builder<T> {
@@ -228,11 +240,6 @@ public class Address extends ImmutableObject implements Jsonifiable, UnsafeSeria
*/
@SuppressWarnings("unused")
void afterUnmarshal(Unmarshaller unmarshaller, Object parent) {
if (street == null || street.isEmpty()) {
return;
}
streetLine1 = street.get(0);
streetLine2 = street.size() >= 2 ? street.get(1) : null;
streetLine3 = street.size() >= 3 ? street.get(2) : null;
postProcess();
}
}

View File

@@ -242,7 +242,7 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
@Expose Set<String> allowedTlds;
/** Host name of WHOIS server. */
String whoisServer;
@Expose String whoisServer;
/** Base URLs for the registrar's RDAP servers. */
Set<String> rdapBaseUrls;
@@ -326,10 +326,10 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
RegistrarAddress internationalizedAddress;
/** Voice number. */
String phoneNumber;
@Expose String phoneNumber;
/** Fax number. */
String faxNumber;
@Expose String faxNumber;
/** Email address. */
@Expose String emailAddress;
@@ -364,7 +364,7 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
@Expose @Nullable Map<CurrencyUnit, String> billingAccountMap;
/** URL of registrar's website. */
String url;
@Expose String url;
/**
* ICANN referral email address.

View File

@@ -29,6 +29,7 @@ import google.registry.ui.server.console.ConsoleDomainGetAction;
import google.registry.ui.server.console.RegistrarsAction;
import google.registry.ui.server.console.settings.ContactAction;
import google.registry.ui.server.console.settings.SecurityAction;
import google.registry.ui.server.console.settings.WhoisRegistrarFieldsAction;
import google.registry.ui.server.registrar.ConsoleOteSetupAction;
import google.registry.ui.server.registrar.ConsoleRegistrarCreatorAction;
import google.registry.ui.server.registrar.ConsoleUiAction;
@@ -73,6 +74,8 @@ interface FrontendRequestComponent {
SecurityAction securityAction();
WhoisRegistrarFieldsAction whoisRegistrarFieldsAction();
@Subcomponent.Builder
abstract class Builder implements RequestComponentBuilder<FrontendRequestComponent> {
@Override public abstract Builder requestModule(RequestModule requestModule);

View File

@@ -31,28 +31,22 @@ 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.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.protobuf.ByteString;
import dagger.Module;
import dagger.Provides;
import google.registry.model.adapters.CurrencyJsonAdapter;
import google.registry.request.HttpException.BadRequestException;
import google.registry.request.HttpException.UnsupportedMediaTypeException;
import google.registry.request.auth.AuthResult;
import google.registry.request.lock.LockHandler;
import google.registry.request.lock.LockHandlerImpl;
import google.registry.util.CidrAddressBlock;
import google.registry.util.CidrAddressBlock.CidrAddressBlockAdapter;
import google.registry.util.DateTimeTypeAdapter;
import google.registry.tools.GsonUtils;
import java.io.IOException;
import java.util.Map;
import java.util.Optional;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.joda.money.CurrencyUnit;
import org.joda.time.DateTime;
import org.json.simple.JSONValue;
import org.json.simple.parser.ParseException;
@@ -80,12 +74,7 @@ public final class RequestModule {
@VisibleForTesting
@Provides
public static Gson provideGson() {
return new GsonBuilder()
.registerTypeAdapter(DateTime.class, new DateTimeTypeAdapter())
.registerTypeAdapter(CidrAddressBlock.class, new CidrAddressBlockAdapter())
.registerTypeAdapter(CurrencyUnit.class, new CurrencyJsonAdapter())
.excludeFieldsWithoutExposeAnnotation()
.create();
return GsonUtils.provideGson();
}
@Provides
@@ -265,7 +254,8 @@ public final class RequestModule {
@OptionalJsonPayload
public static Optional<JsonElement> provideJsonBody(HttpServletRequest req, Gson gson) {
try {
return Optional.of(gson.fromJson(req.getReader(), JsonElement.class));
// GET requests return a null reader and thus a null JsonObject, which is fine
return Optional.ofNullable(gson.fromJson(req.getReader(), JsonElement.class));
} catch (IOException e) {
return Optional.empty();
}

View File

@@ -0,0 +1,81 @@
// Copyright 2017 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.tools;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;
import com.google.gson.TypeAdapterFactory;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import google.registry.model.adapters.CurrencyJsonAdapter;
import google.registry.util.CidrAddressBlock;
import google.registry.util.CidrAddressBlock.CidrAddressBlockAdapter;
import google.registry.util.DateTimeTypeAdapter;
import java.io.IOException;
import org.joda.money.CurrencyUnit;
import org.joda.time.DateTime;
/** Utility class for methods related to GSON and necessary GSON processing. */
public class GsonUtils {
/** Interface to enable GSON post-processing on a particular object after deserialization. */
public interface GsonPostProcessable {
void postProcess();
}
/**
* Some objects may require post-processing after deserialization from JSON.
*
* <p>We do this upon deserialization in order to make sure that the object matches the format
* that we expect to be stored in the database. See {@link
* google.registry.model.eppcommon.Address} for an example.
*/
public static class GsonPostProcessableTypeAdapterFactory implements TypeAdapterFactory {
@Override
public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
TypeAdapter<T> originalAdapter = gson.getDelegateAdapter(this, type);
if (!GsonPostProcessable.class.isAssignableFrom(type.getRawType())) {
return originalAdapter;
}
return new TypeAdapter<T>() {
@Override
public void write(JsonWriter out, T value) throws IOException {
originalAdapter.write(out, value);
}
@Override
public T read(JsonReader in) throws IOException {
T t = originalAdapter.read(in);
((GsonPostProcessable) t).postProcess();
return t;
}
};
}
}
public static Gson provideGson() {
return new GsonBuilder()
.registerTypeAdapter(DateTime.class, new DateTimeTypeAdapter())
.registerTypeAdapter(CidrAddressBlock.class, new CidrAddressBlockAdapter())
.registerTypeAdapter(CurrencyUnit.class, new CurrencyJsonAdapter())
.registerTypeAdapterFactory(new GsonPostProcessableTypeAdapterFactory())
.excludeFieldsWithoutExposeAnnotation()
.create();
}
private GsonUtils() {}
}

View File

@@ -131,7 +131,7 @@ public class ContactAction implements JsonGetAction {
oldContacts,
Collections.singletonMap(
"contacts",
contacts.get().stream().map(c -> c.toJsonMap()).collect(toImmutableList())));
contacts.get().stream().map(RegistrarPoc::toJsonMap).collect(toImmutableList())));
try {
RegistrarSettingsAction.checkContactRequirements(oldContacts, updatedContacts);
} catch (FormException e) {

View File

@@ -0,0 +1,107 @@
// 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 google.registry.persistence.transaction.TransactionManagerFactory.tm;
import static google.registry.request.Action.Method.POST;
import com.google.api.client.http.HttpStatusCodes;
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.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.request.auth.AuthenticatedRegistrarAccessor;
import google.registry.request.auth.AuthenticatedRegistrarAccessor.RegistrarAccessDeniedException;
import google.registry.ui.server.registrar.JsonGetAction;
import java.util.Optional;
import javax.inject.Inject;
/**
* Console action for editing fields on a registrar that are visible in WHOIS/RDAP.
*
* <p>This doesn't cover many of the registrar fields but rather only those that are visible in
* WHOIS/RDAP and don't have any other obvious means of edit.
*/
@Action(
service = Action.Service.DEFAULT,
path = WhoisRegistrarFieldsAction.PATH,
method = {POST},
auth = Auth.AUTH_PUBLIC_LOGGED_IN)
public class WhoisRegistrarFieldsAction implements JsonGetAction {
static final String PATH = "/console-api/settings/whois-fields";
private final AuthResult authResult;
private final Response response;
private final Gson gson;
private AuthenticatedRegistrarAccessor registrarAccessor;
private Optional<Registrar> registrar;
@Inject
public WhoisRegistrarFieldsAction(
AuthResult authResult,
Response response,
Gson gson,
AuthenticatedRegistrarAccessor registrarAccessor,
@Parameter("registrar") Optional<Registrar> registrar) {
this.authResult = authResult;
this.response = response;
this.gson = gson;
this.registrarAccessor = registrarAccessor;
this.registrar = registrar;
}
@Override
public void run() {
if (!registrar.isPresent()) {
response.setStatus(HttpStatusCodes.STATUS_CODE_BAD_REQUEST);
response.setPayload(gson.toJson("'registrar' parameter is not present"));
return;
}
User user = authResult.userAuthInfo().get().consoleUser().get();
if (!user.getUserRoles()
.hasPermission(
registrar.get().getRegistrarId(), ConsolePermission.EDIT_REGISTRAR_DETAILS)) {
response.setStatus(HttpStatusCodes.STATUS_CODE_FORBIDDEN);
return;
}
tm().transact(() -> loadAndModifyRegistrar(registrar.get()));
}
private void loadAndModifyRegistrar(Registrar providedRegistrar) {
Registrar savedRegistrar;
try {
// reload to make sure the object has all the correct fields
savedRegistrar = registrarAccessor.getRegistrar(providedRegistrar.getRegistrarId());
} catch (RegistrarAccessDeniedException e) {
response.setStatus(HttpStatusCodes.STATUS_CODE_FORBIDDEN);
response.setPayload(e.getMessage());
return;
}
Registrar.Builder newRegistrar = savedRegistrar.asBuilder();
newRegistrar.setWhoisServer(providedRegistrar.getWhoisServer());
newRegistrar.setUrl(providedRegistrar.getUrl());
newRegistrar.setLocalizedAddress(providedRegistrar.getLocalizedAddress());
tm().put(newRegistrar.build());
response.setStatus(HttpStatusCodes.STATUS_CODE_OK);
}
}

View File

@@ -0,0 +1,174 @@
// 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.truth.Truth.assertThat;
import static google.registry.model.ImmutableObjectSubject.assertAboutImmutableObjects;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.google.api.client.http.HttpStatusCodes;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.Maps;
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.persistence.transaction.JpaTestExtensions;
import google.registry.request.RequestModule;
import google.registry.request.auth.AuthResult;
import google.registry.request.auth.AuthSettings.AuthLevel;
import google.registry.request.auth.AuthenticatedRegistrarAccessor;
import google.registry.request.auth.AuthenticatedRegistrarAccessor.Role;
import google.registry.request.auth.UserAuthInfo;
import google.registry.testing.DatabaseHelper;
import google.registry.testing.FakeClock;
import google.registry.testing.FakeResponse;
import google.registry.ui.server.registrar.RegistrarConsoleModule;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.HashMap;
import javax.servlet.http.HttpServletRequest;
import org.joda.time.DateTime;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
/** Tests for {@link WhoisRegistrarFieldsAction}. */
public class WhoisRegistrarFieldsActionTest {
private static final Gson GSON = RequestModule.provideGson();
private final FakeClock clock = new FakeClock(DateTime.parse("2023-08-01T00:00:00.000Z"));
private final FakeResponse fakeResponse = new FakeResponse();
private final HttpServletRequest request = mock(HttpServletRequest.class);
private final AuthenticatedRegistrarAccessor registrarAccessor =
AuthenticatedRegistrarAccessor.createForTesting(
ImmutableSetMultimap.of("TheRegistrar", Role.OWNER, "NewRegistrar", Role.OWNER));
private final HashMap<String, Object> uiRegistrarMap =
Maps.newHashMap(
ImmutableMap.of(
"registrarId",
"TheRegistrar",
"whoisServer",
"whois.nic.google",
"type",
"REAL",
"emailAddress",
"the.registrar@example.com",
"state",
"ACTIVE",
"url",
"\"http://my.fake.url\"",
"localizedAddress",
"{\"street\": [\"123 Example Boulevard\"], \"city\": \"Williamsburg\", \"state\":"
+ " \"NY\", \"zip\": \"11201\", \"countryCode\": \"US\"}"));
@RegisterExtension
final JpaTestExtensions.JpaIntegrationTestExtension jpa =
new JpaTestExtensions.Builder().withClock(clock).buildIntegrationTestExtension();
@Test
void testSuccess_setsAllFields() throws Exception {
Registrar oldRegistrar = Registrar.loadRequiredRegistrarCached("TheRegistrar");
assertThat(oldRegistrar.getWhoisServer()).isEqualTo("whois.nic.fakewhois.example");
assertThat(oldRegistrar.getUrl()).isEqualTo("http://my.fake.url");
ImmutableMap<String, Object> addressMap =
ImmutableMap.of(
"street",
ImmutableList.of("123 Fake St"),
"city",
"Fakeville",
"state",
"NL",
"zip",
"10011",
"countryCode",
"CA");
uiRegistrarMap.putAll(
ImmutableMap.of(
"whoisServer",
"whois.nic.google",
"url",
"\"https://newurl.example\"",
"localizedAddress",
"{\"street\": [\"123 Fake St\"], \"city\": \"Fakeville\", \"state\":"
+ " \"NL\", \"zip\": \"10011\", \"countryCode\": \"CA\"}"));
WhoisRegistrarFieldsAction action = createAction();
action.run();
assertThat(fakeResponse.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_OK);
Registrar newRegistrar = Registrar.loadByRegistrarId("TheRegistrar").get(); // skip cache
assertThat(newRegistrar.getWhoisServer()).isEqualTo("whois.nic.google");
assertThat(newRegistrar.getUrl()).isEqualTo("https://newurl.example");
assertThat(newRegistrar.getLocalizedAddress().toJsonMap()).isEqualTo(addressMap);
// the non-changed fields should be the same
assertAboutImmutableObjects()
.that(newRegistrar)
.isEqualExceptFields(oldRegistrar, "whoisServer", "url", "localizedAddress");
}
@Test
void testFailure_noAccessToRegistrar() throws Exception {
Registrar newRegistrar = Registrar.loadByRegistrarIdCached("NewRegistrar").get();
AuthResult onlyTheRegistrar =
AuthResult.create(
AuthLevel.USER,
UserAuthInfo.create(
new User.Builder()
.setEmailAddress("email@email.example")
.setUserRoles(
new UserRoles.Builder()
.setRegistrarRoles(
ImmutableMap.of("TheRegistrar", RegistrarRole.PRIMARY_CONTACT))
.build())
.build()));
uiRegistrarMap.put("registrarId", "NewRegistrar");
WhoisRegistrarFieldsAction action = createAction(onlyTheRegistrar);
action.run();
assertThat(fakeResponse.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_FORBIDDEN);
// should be no change
assertThat(DatabaseHelper.loadByEntity(newRegistrar)).isEqualTo(newRegistrar);
}
private AuthResult defaultUserAuth() {
return AuthResult.create(
AuthLevel.USER,
UserAuthInfo.create(
new User.Builder()
.setEmailAddress("email@email.example")
.setUserRoles(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build())
.build()));
}
private WhoisRegistrarFieldsAction createAction() throws IOException {
return createAction(defaultUserAuth());
}
private WhoisRegistrarFieldsAction createAction(AuthResult authResult) throws IOException {
when(request.getReader())
.thenReturn(new BufferedReader(new StringReader(uiRegistrarMap.toString())));
return new WhoisRegistrarFieldsAction(
authResult,
fakeResponse,
GSON,
registrarAccessor,
RegistrarConsoleModule.provideRegistrar(
GSON, RequestModule.provideJsonBody(request, GSON)));
}
}

View File

@@ -1,14 +1,15 @@
PATH CLASS METHODS OK AUTH_METHODS MIN USER_POLICY
/_dr/epp EppTlsAction POST n API APP PUBLIC
/console-api/domain ConsoleDomainGetAction GET n API,LEGACY USER PUBLIC
/console-api/registrars RegistrarsAction GET,POST n API,LEGACY USER PUBLIC
/console-api/settings/contacts ContactAction GET,POST n API,LEGACY USER PUBLIC
/console-api/settings/security SecurityAction POST n API,LEGACY USER PUBLIC
/registrar ConsoleUiAction GET n API,LEGACY NONE PUBLIC
/registrar-create ConsoleRegistrarCreatorAction POST,GET n API,LEGACY NONE PUBLIC
/registrar-ote-setup ConsoleOteSetupAction POST,GET n 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 NONE PUBLIC
PATH CLASS METHODS OK AUTH_METHODS MIN USER_POLICY
/_dr/epp EppTlsAction POST n API APP PUBLIC
/console-api/domain ConsoleDomainGetAction GET n API,LEGACY USER PUBLIC
/console-api/registrars RegistrarsAction GET,POST n API,LEGACY USER PUBLIC
/console-api/settings/contacts ContactAction GET,POST n API,LEGACY USER PUBLIC
/console-api/settings/security SecurityAction POST n API,LEGACY USER PUBLIC
/console-api/settings/whois-fields WhoisRegistrarFieldsAction POST n API,LEGACY USER PUBLIC
/registrar ConsoleUiAction GET n API,LEGACY NONE PUBLIC
/registrar-create ConsoleRegistrarCreatorAction POST,GET n API,LEGACY NONE PUBLIC
/registrar-ote-setup ConsoleOteSetupAction POST,GET n 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 NONE PUBLIC