diff --git a/core/src/main/java/google/registry/env/common/default/WEB-INF/web.xml b/core/src/main/java/google/registry/env/common/default/WEB-INF/web.xml index 5a472a3ee..e6766c8f6 100644 --- a/core/src/main/java/google/registry/env/common/default/WEB-INF/web.xml +++ b/core/src/main/java/google/registry/env/common/default/WEB-INF/web.xml @@ -94,6 +94,12 @@ /registry-lock-verify + + + frontend-servlet + /console-api/* + + diff --git a/core/src/main/java/google/registry/model/CreateAutoTimestamp.java b/core/src/main/java/google/registry/model/CreateAutoTimestamp.java index 30282f6a1..85b5f8ddf 100644 --- a/core/src/main/java/google/registry/model/CreateAutoTimestamp.java +++ b/core/src/main/java/google/registry/model/CreateAutoTimestamp.java @@ -16,6 +16,7 @@ package google.registry.model; import static google.registry.persistence.transaction.TransactionManagerFactory.tm; +import com.google.gson.annotations.Expose; import javax.annotation.Nullable; import javax.persistence.Column; import javax.persistence.Embeddable; @@ -28,6 +29,7 @@ import org.joda.time.DateTime; public class CreateAutoTimestamp extends ImmutableObject implements UnsafeSerializable { @Column(nullable = false) + @Expose DateTime creationTime; @PrePersist diff --git a/core/src/main/java/google/registry/model/EppResource.java b/core/src/main/java/google/registry/model/EppResource.java index bd175fa7f..c2bc9b0be 100644 --- a/core/src/main/java/google/registry/model/EppResource.java +++ b/core/src/main/java/google/registry/model/EppResource.java @@ -31,6 +31,7 @@ import com.github.benmanes.caffeine.cache.LoadingCache; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; +import com.google.gson.annotations.Expose; import google.registry.config.RegistryConfig; import google.registry.dns.RefreshDnsAction; import google.registry.model.eppcommon.StatusValue; @@ -67,7 +68,7 @@ public abstract class EppResource extends UpdateAutoTimestampEntity implements B * * @see RFC 5730 */ - @Transient String repoId; + @Expose @Transient String repoId; /** * The ID of the registrar that is currently sponsoring this resource. @@ -75,7 +76,7 @@ public abstract class EppResource extends UpdateAutoTimestampEntity implements B *

This can be null in the case of pre-Registry-3.0-migration history objects with null * resource fields. */ - String currentSponsorRegistrarId; + @Expose String currentSponsorRegistrarId; /** * The ID of the registrar that created this resource. @@ -83,7 +84,7 @@ public abstract class EppResource extends UpdateAutoTimestampEntity implements B *

This can be null in the case of pre-Registry-3.0-migration history objects with null * resource fields. */ - String creationRegistrarId; + @Expose String creationRegistrarId; /** * The ID of the registrar that last updated this resource. @@ -92,7 +93,7 @@ public abstract class EppResource extends UpdateAutoTimestampEntity implements B * edits; it only includes EPP-visible modifications such as {@literal }. Can be null if * the resource has never been modified. */ - String lastEppUpdateRegistrarId; + @Expose String lastEppUpdateRegistrarId; /** * The time when this resource was created. @@ -106,6 +107,7 @@ public abstract class EppResource extends UpdateAutoTimestampEntity implements B */ // Need to override the default non-null column attribute. @AttributeOverride(name = "creationTime", column = @Column) + @Expose CreateAutoTimestamp creationTime = CreateAutoTimestamp.create(null); /** @@ -130,10 +132,10 @@ public abstract class EppResource extends UpdateAutoTimestampEntity implements B * edits; it only includes EPP-visible modifications such as {@literal }. Can be null if * the resource has never been modified. */ - DateTime lastEppUpdateTime; + @Expose DateTime lastEppUpdateTime; /** Status values associated with this resource. */ - Set statuses; + @Expose Set statuses; /** * When this domain/host's DNS was requested to be refreshed, or null if its DNS is up-to-date. diff --git a/core/src/main/java/google/registry/model/domain/DomainBase.java b/core/src/main/java/google/registry/model/domain/DomainBase.java index 2f3a9c337..eb297840b 100644 --- a/core/src/main/java/google/registry/model/domain/DomainBase.java +++ b/core/src/main/java/google/registry/model/domain/DomainBase.java @@ -40,6 +40,7 @@ import com.google.common.collect.ImmutableSet; import com.google.common.collect.ImmutableSortedSet; import com.google.common.collect.Ordering; import com.google.common.collect.Sets; +import com.google.gson.annotations.Expose; import google.registry.flows.ResourceFlowUtils; import google.registry.model.EppResource; import google.registry.model.EppResource.ResourceWithTransferData; @@ -121,20 +122,20 @@ public class DomainBase extends EppResource * * @invariant domainName == domainName.toLowerCase(Locale.ENGLISH) */ - String domainName; + @Expose String domainName; /** The top level domain this is under, de-normalized from {@link #domainName}. */ String tld; /** References to hosts that are the nameservers for the domain. */ - @Transient Set> nsHosts; + @Expose @Transient Set> nsHosts; /** Contacts. */ - VKey adminContact; + @Expose VKey adminContact; - VKey billingContact; - VKey techContact; - VKey registrantContact; + @Expose VKey billingContact; + @Expose VKey techContact; + @Expose VKey registrantContact; /** Authorization info (aka transfer secret) of the domain. */ @Embedded @@ -175,10 +176,10 @@ public class DomainBase extends EppResource String idnTableName; /** Fully qualified host names of this domain's active subordinate hosts. */ - Set subordinateHosts; + @Expose Set subordinateHosts; /** When this domain's registration will expire. */ - DateTime registrationExpirationTime; + @Expose DateTime registrationExpirationTime; /** * The poll message associated with this domain being deleted. @@ -230,7 +231,7 @@ public class DomainBase extends EppResource * *

Can be null if the resource has never been transferred. */ - DateTime lastTransferTime; + @Expose DateTime lastTransferTime; /** * When the domain's autorenewal status will expire. diff --git a/core/src/main/java/google/registry/module/frontend/FrontendRequestComponent.java b/core/src/main/java/google/registry/module/frontend/FrontendRequestComponent.java index b812a7fbe..a0bd284ff 100644 --- a/core/src/main/java/google/registry/module/frontend/FrontendRequestComponent.java +++ b/core/src/main/java/google/registry/module/frontend/FrontendRequestComponent.java @@ -25,6 +25,7 @@ import google.registry.monitoring.whitebox.WhiteboxModule; import google.registry.request.RequestComponentBuilder; import google.registry.request.RequestModule; import google.registry.request.RequestScope; +import google.registry.ui.server.console.ConsoleDomainGetAction; import google.registry.ui.server.registrar.ConsoleOteSetupAction; import google.registry.ui.server.registrar.ConsoleRegistrarCreatorAction; import google.registry.ui.server.registrar.ConsoleUiAction; @@ -61,6 +62,8 @@ interface FrontendRequestComponent { RegistryLockVerifyAction registryLockVerifyAction(); + ConsoleDomainGetAction consoleDomainGetAction(); + @Subcomponent.Builder abstract class Builder implements RequestComponentBuilder { @Override public abstract Builder requestModule(RequestModule requestModule); diff --git a/core/src/main/java/google/registry/persistence/VKey.java b/core/src/main/java/google/registry/persistence/VKey.java index 2c3fceb61..c2222e4a3 100644 --- a/core/src/main/java/google/registry/persistence/VKey.java +++ b/core/src/main/java/google/registry/persistence/VKey.java @@ -22,6 +22,7 @@ import com.google.common.base.Joiner; import com.google.common.base.Splitter; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; +import com.google.gson.annotations.Expose; import google.registry.model.EppResource; import google.registry.model.ImmutableObject; import google.registry.model.contact.Contact; @@ -52,7 +53,7 @@ public class VKey extends ImmutableObject implements Serializable { .collect(toImmutableMap(Class::getSimpleName, identity())); // The primary key for the referenced entity. - Serializable key; + @Expose Serializable key; Class kind; diff --git a/core/src/main/java/google/registry/ui/server/console/ConsoleDomainGetAction.java b/core/src/main/java/google/registry/ui/server/console/ConsoleDomainGetAction.java new file mode 100644 index 000000000..af284528f --- /dev/null +++ b/core/src/main/java/google/registry/ui/server/console/ConsoleDomainGetAction.java @@ -0,0 +1,91 @@ +// 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; + +import static google.registry.persistence.transaction.TransactionManagerFactory.tm; + +import com.google.api.client.http.HttpStatusCodes; +import com.google.gson.Gson; +import google.registry.model.EppResourceUtils; +import google.registry.model.console.ConsolePermission; +import google.registry.model.console.User; +import google.registry.model.domain.Domain; +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.UserAuthInfo; +import google.registry.ui.server.registrar.JsonGetAction; +import java.util.Optional; +import javax.inject.Inject; + +/** Returns a JSON representation of a domain to the registrar console. */ +@Action( + service = Action.Service.DEFAULT, + path = ConsoleDomainGetAction.PATH, + auth = Auth.AUTH_PUBLIC_LOGGED_IN) +public class ConsoleDomainGetAction implements JsonGetAction { + + public static final String PATH = "/console-api/domain"; + + private final AuthResult authResult; + private final Response response; + private final Gson gson; + private final String paramDomain; + + @Inject + public ConsoleDomainGetAction( + AuthResult authResult, + Response response, + Gson gson, + @Parameter("domain") String paramDomain) { + this.authResult = authResult; + this.response = response; + this.gson = gson; + this.paramDomain = paramDomain; + } + + @Override + public void run() { + if (!authResult.isAuthenticated() || !authResult.userAuthInfo().isPresent()) { + response.setStatus(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED); + return; + } + UserAuthInfo authInfo = authResult.userAuthInfo().get(); + if (!authInfo.consoleUser().isPresent()) { + response.setStatus(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED); + return; + } + User user = authInfo.consoleUser().get(); + Optional possibleDomain = + tm().transact( + () -> + EppResourceUtils.loadByForeignKeyCached( + Domain.class, paramDomain, tm().getTransactionTime())); + if (!possibleDomain.isPresent()) { + response.setStatus(HttpStatusCodes.STATUS_CODE_NOT_FOUND); + return; + } + Domain domain = possibleDomain.get(); + if (!user.getUserRoles() + .hasPermission(domain.getCurrentSponsorRegistrarId(), ConsolePermission.DOWNLOAD_DOMAINS)) { + response.setStatus(HttpStatusCodes.STATUS_CODE_NOT_FOUND); + return; + } + response.setStatus(HttpStatusCodes.STATUS_CODE_OK); + response.setPayload(gson.toJson(domain)); + } +} diff --git a/core/src/main/java/google/registry/ui/server/registrar/RegistrarConsoleModule.java b/core/src/main/java/google/registry/ui/server/registrar/RegistrarConsoleModule.java index 6c1ddc61b..f78ca9c94 100644 --- a/core/src/main/java/google/registry/ui/server/registrar/RegistrarConsoleModule.java +++ b/core/src/main/java/google/registry/ui/server/registrar/RegistrarConsoleModule.java @@ -156,4 +156,10 @@ public final class RegistrarConsoleModule { static Boolean provideIsLock(HttpServletRequest req) { return extractBooleanParameter(req, "isLock"); } + + @Provides + @Parameter("domain") + static String provideDomain(HttpServletRequest req) { + return extractRequiredParameter(req, "domain"); + } } diff --git a/core/src/test/java/google/registry/ui/server/console/ConsoleDomainGetActionTest.java b/core/src/test/java/google/registry/ui/server/console/ConsoleDomainGetActionTest.java new file mode 100644 index 000000000..0fc7957e0 --- /dev/null +++ b/core/src/test/java/google/registry/ui/server/console/ConsoleDomainGetActionTest.java @@ -0,0 +1,139 @@ +// 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; + +import static com.google.common.truth.Truth.assertThat; +import static google.registry.testing.DatabaseHelper.createTld; +import static org.mockito.Mockito.mock; + +import com.google.api.client.http.HttpStatusCodes; +import com.google.common.collect.ImmutableMap; +import com.google.gson.Gson; +import google.registry.model.console.RegistrarRole; +import google.registry.model.console.User; +import google.registry.model.console.UserRoles; +import google.registry.persistence.transaction.JpaTestExtensions; +import google.registry.request.auth.AuthLevel; +import google.registry.request.auth.AuthResult; +import google.registry.request.auth.UserAuthInfo; +import google.registry.testing.DatabaseHelper; +import google.registry.testing.FakeResponse; +import google.registry.util.UtilsModule; +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.ConsoleDomainGetAction}. */ +public class ConsoleDomainGetActionTest { + + 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() { + createTld("tld"); + DatabaseHelper.persistActiveDomain("exists.tld"); + } + + @Test + void testSuccess_fullJsonRepresentation() { + ConsoleDomainGetAction action = + createAction( + AuthResult.create( + AuthLevel.USER, + UserAuthInfo.create( + createUser( + new UserRoles.Builder() + .setRegistrarRoles( + ImmutableMap.of("TheRegistrar", RegistrarRole.ACCOUNT_MANAGER)) + .build()))), + "exists.tld"); + action.run(); + assertThat(RESPONSE.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_OK); + assertThat(RESPONSE.getPayload()) + .isEqualTo( + "{\"domainName\":\"exists.tld\",\"adminContact\":{\"key\":\"3-ROID\"},\"techContact\":" + + "{\"key\":\"3-ROID\"},\"registrantContact\":{\"key\":\"3-ROID\"},\"registrationExpirationTime\":" + + "\"294247-01-10T04:00:54.775Z\",\"lastTransferTime\":\"null\",\"repoId\":\"2-TLD\"," + + "\"currentSponsorRegistrarId\":\"TheRegistrar\",\"creationRegistrarId\":\"TheRegistrar\"," + + "\"creationTime\":{\"creationTime\":\"1970-01-01T00:00:00.000Z\"},\"lastEppUpdateTime\":\"null\"," + + "\"statuses\":[\"INACTIVE\"]}"); + } + + @Test + void testFailure_emptyAuth() { + ConsoleDomainGetAction action = createAction(AuthResult.NOT_AUTHENTICATED, "exists.tld"); + action.run(); + assertThat(RESPONSE.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED); + } + + @Test + void testFailure_appAuth() { + ConsoleDomainGetAction action = createAction(AuthResult.create(AuthLevel.APP), "exists.tld"); + action.run(); + assertThat(RESPONSE.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED); + } + + @Test + void testFailure_wrongTypeOfUser() { + ConsoleDomainGetAction action = + createAction( + AuthResult.create( + AuthLevel.USER, + UserAuthInfo.create(mock(com.google.appengine.api.users.User.class), false)), + "exists.tld"); + action.run(); + assertThat(RESPONSE.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_UNAUTHORIZED); + } + + @Test + void testFailure_noAccessToRegistrar() { + ConsoleDomainGetAction action = + createAction( + AuthResult.create( + AuthLevel.USER, UserAuthInfo.create(createUser(new UserRoles.Builder().build()))), + "exists.tld"); + action.run(); + assertThat(RESPONSE.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_NOT_FOUND); + } + + @Test + void testFailure_nonexistentDomain() { + ConsoleDomainGetAction action = + createAction( + AuthResult.create( + AuthLevel.USER, + UserAuthInfo.create(createUser(new UserRoles.Builder().setIsAdmin(true).build()))), + "nonexistent.tld"); + action.run(); + assertThat(RESPONSE.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_NOT_FOUND); + } + + private User createUser(UserRoles userRoles) { + return new User.Builder() + .setEmailAddress("email@email.com") + .setGaiaId("gaiaId") + .setUserRoles(userRoles) + .build(); + } + + private ConsoleDomainGetAction createAction(AuthResult authResult, String domain) { + return new ConsoleDomainGetAction(authResult, RESPONSE, GSON, domain); + } +} diff --git a/core/src/test/resources/google/registry/module/frontend/frontend_routing.txt b/core/src/test/resources/google/registry/module/frontend/frontend_routing.txt index 57babedbe..fbc0f9ba7 100644 --- a/core/src/test/resources/google/registry/module/frontend/frontend_routing.txt +++ b/core/src/test/resources/google/registry/module/frontend/frontend_routing.txt @@ -1,5 +1,6 @@ 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 diff --git a/util/src/main/java/google/registry/util/DateTimeTypeAdapter.java b/util/src/main/java/google/registry/util/DateTimeTypeAdapter.java new file mode 100644 index 000000000..4c922baa8 --- /dev/null +++ b/util/src/main/java/google/registry/util/DateTimeTypeAdapter.java @@ -0,0 +1,41 @@ +// 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.util; + +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.util.Objects; +import org.joda.time.DateTime; +import org.joda.time.format.ISODateTimeFormat; + +/** GSON type adapter for Joda {@link DateTime} objects. */ +public class DateTimeTypeAdapter extends TypeAdapter { + + @Override + public void write(JsonWriter out, DateTime value) throws IOException { + out.value(Objects.toString(value)); + } + + @Override + public DateTime read(JsonReader in) throws IOException { + String stringValue = in.nextString(); + if (stringValue.equals("null")) { + return null; + } + return ISODateTimeFormat.dateTime().withZoneUTC().parseDateTime(stringValue); + } +} diff --git a/util/src/main/java/google/registry/util/UtilsModule.java b/util/src/main/java/google/registry/util/UtilsModule.java index 78d10b9b1..655dd48bf 100644 --- a/util/src/main/java/google/registry/util/UtilsModule.java +++ b/util/src/main/java/google/registry/util/UtilsModule.java @@ -16,6 +16,8 @@ package google.registry.util; import com.google.appengine.api.modules.ModulesService; import com.google.appengine.api.modules.ModulesServiceFactory; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import dagger.Binds; import dagger.Module; import dagger.Provides; @@ -25,6 +27,7 @@ import java.security.SecureRandom; import java.util.Random; import javax.inject.Named; import javax.inject.Singleton; +import org.joda.time.DateTime; /** Dagger module to provide instances of various utils classes. */ @Module @@ -83,4 +86,13 @@ public abstract class UtilsModule { public static StringGenerator provideDigitsOnlyStringGenerator(SecureRandom secureRandom) { return new RandomStringGenerator(StringGenerator.Alphabets.DIGITS_ONLY, secureRandom); } + + @Singleton + @Provides + public static Gson provideGson() { + return new GsonBuilder() + .registerTypeAdapter(DateTime.class, new DateTimeTypeAdapter()) + .excludeFieldsWithoutExposeAnnotation() + .create(); + } }