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 extends T> 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();
+ }
}