From 304e7c9726e5564f0b72771afca8d9536f2e8e68 Mon Sep 17 00:00:00 2001
From: Pavlo Tkach <3469726+ptkach@users.noreply.github.com>
Date: Wed, 12 Jul 2023 16:19:20 -0400
Subject: [PATCH] Add console-api/settings/security endpoint (#2057)
---
.../registry/model/registrar/Registrar.java | 7 +-
.../frontend/FrontendRequestComponent.java | 3 +
.../console/settings/SecurityAction.java | 171 ++++++++++++++++
.../registrar/RegistrarConsoleModule.java | 12 ++
.../console/settings/SecurityActionTest.java | 183 ++++++++++++++++++
.../module/frontend/frontend_routing.txt | 1 +
.../registry/util/CidrAddressBlock.java | 20 ++
.../google/registry/util/UtilsModule.java | 2 +
8 files changed, 396 insertions(+), 3 deletions(-)
create mode 100644 core/src/main/java/google/registry/ui/server/console/settings/SecurityAction.java
create mode 100644 core/src/test/java/google/registry/ui/server/console/settings/SecurityActionTest.java
diff --git a/core/src/main/java/google/registry/model/registrar/Registrar.java b/core/src/main/java/google/registry/model/registrar/Registrar.java
index e7ec521c1..6e837ca97 100644
--- a/core/src/main/java/google/registry/model/registrar/Registrar.java
+++ b/core/src/main/java/google/registry/model/registrar/Registrar.java
@@ -50,6 +50,7 @@ import com.google.common.collect.Maps;
import com.google.common.collect.Range;
import com.google.common.collect.Sets;
import com.google.common.collect.Streams;
+import com.google.gson.annotations.Expose;
import com.google.re2j.Pattern;
import google.registry.model.Buildable;
import google.registry.model.CreateAutoTimestamp;
@@ -253,7 +254,7 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
// Authentication.
/** X.509 PEM client certificate(s) used to authenticate registrar to EPP service. */
- String clientCertificate;
+ @Expose String clientCertificate;
/** Base64 encoded SHA256 hash of {@link #clientCertificate}. */
String clientCertificateHash;
@@ -263,13 +264,13 @@ public class Registrar extends UpdateAutoTimestampEntity implements Buildable, J
*
*
This allows registrars to migrate certificates without downtime.
*/
- String failoverClientCertificate;
+ @Expose String failoverClientCertificate;
/** Base64 encoded SHA256 hash of {@link #failoverClientCertificate}. */
String failoverClientCertificateHash;
/** An allow list of netmasks (in CIDR notation) which the client is allowed to connect from. */
- List ipAddressAllowList;
+ @Expose List ipAddressAllowList;
/** A hashed password for EPP access. The hash is a base64 encoded SHA256 string. */
String passwordHash;
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 e2d7ac0b5..53841e6ac 100644
--- a/core/src/main/java/google/registry/module/frontend/FrontendRequestComponent.java
+++ b/core/src/main/java/google/registry/module/frontend/FrontendRequestComponent.java
@@ -28,6 +28,7 @@ import google.registry.request.RequestScope;
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.registrar.ConsoleOteSetupAction;
import google.registry.ui.server.registrar.ConsoleRegistrarCreatorAction;
import google.registry.ui.server.registrar.ConsoleUiAction;
@@ -70,6 +71,8 @@ interface FrontendRequestComponent {
RegistrarsAction registrarsAction();
+ SecurityAction securityAction();
+
@Subcomponent.Builder
abstract class Builder implements RequestComponentBuilder {
@Override public abstract Builder requestModule(RequestModule requestModule);
diff --git a/core/src/main/java/google/registry/ui/server/console/settings/SecurityAction.java b/core/src/main/java/google/registry/ui/server/console/settings/SecurityAction.java
new file mode 100644
index 000000000..0e69da19b
--- /dev/null
+++ b/core/src/main/java/google/registry/ui/server/console/settings/SecurityAction.java
@@ -0,0 +1,171 @@
+// 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.GET;
+import static google.registry.request.Action.Method.POST;
+
+import avro.shaded.com.google.common.collect.ImmutableList;
+import com.google.api.client.http.HttpStatusCodes;
+import com.google.gson.Gson;
+import google.registry.flows.certs.CertificateChecker;
+import google.registry.flows.certs.CertificateChecker.InsecureCertificateException;
+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;
+import javax.servlet.http.HttpServletRequest;
+
+@Action(
+ service = Action.Service.DEFAULT,
+ path = SecurityAction.PATH,
+ method = {GET, POST},
+ auth = Auth.AUTH_PUBLIC_LOGGED_IN)
+public class SecurityAction implements JsonGetAction {
+
+ static final String PATH = "/console-api/settings/security";
+ private final HttpServletRequest req;
+ private final AuthResult authResult;
+ private final Response response;
+ private final Gson gson;
+ private final String registrarId;
+ private AuthenticatedRegistrarAccessor registrarAccessor;
+ private Optional registrar;
+ private CertificateChecker certificateChecker;
+
+ @Inject
+ public SecurityAction(
+ HttpServletRequest req,
+ AuthResult authResult,
+ Response response,
+ Gson gson,
+ CertificateChecker certificateChecker,
+ AuthenticatedRegistrarAccessor registrarAccessor,
+ @Parameter("registrarId") String registrarId,
+ @Parameter("registrar") Optional registrar) {
+ this.req = req;
+ this.authResult = authResult;
+ this.response = response;
+ this.gson = gson;
+ this.registrarId = registrarId;
+ this.registrarAccessor = registrarAccessor;
+ this.registrar = registrar;
+ this.certificateChecker = certificateChecker;
+ }
+
+ @Override
+ public void run() {
+ if (req.getMethod().equals(GET.toString())) {
+ getHandler();
+ } else {
+ postHandler();
+ }
+ }
+
+ private void getHandler() {
+ try {
+ Registrar registrar = registrarAccessor.getRegistrar(registrarId);
+ response.setStatus(HttpStatusCodes.STATUS_CODE_OK);
+ response.setPayload(gson.toJson(registrar));
+ } catch (RegistrarAccessDeniedException e) {
+ response.setStatus(HttpStatusCodes.STATUS_CODE_FORBIDDEN);
+ response.setPayload(e.getMessage());
+ }
+ }
+
+ private void postHandler() {
+ User user = authResult.userAuthInfo().get().consoleUser().get();
+ if (!user.getUserRoles().hasPermission(registrarId, ConsolePermission.EDIT_REGISTRAR_DETAILS)) {
+ response.setStatus(HttpStatusCodes.STATUS_CODE_FORBIDDEN);
+ return;
+ }
+
+ if (!registrar.isPresent()) {
+ response.setStatus(HttpStatusCodes.STATUS_CODE_BAD_REQUEST);
+ response.setPayload(gson.toJson("'registrar' parameter is not present"));
+ return;
+ }
+
+ Registrar savedRegistrar;
+ try {
+ savedRegistrar = registrarAccessor.getRegistrar(registrarId);
+ } catch (RegistrarAccessDeniedException e) {
+ response.setStatus(HttpStatusCodes.STATUS_CODE_FORBIDDEN);
+ response.setPayload(e.getMessage());
+ return;
+ }
+
+ tm().transact(() -> setResponse(savedRegistrar));
+ }
+
+ private void setResponse(Registrar savedRegistrar) {
+ Registrar registrarParameter = registrar.get();
+ Registrar.Builder updatedRegistrar =
+ savedRegistrar
+ .asBuilder()
+ .setIpAddressAllowList(registrarParameter.getIpAddressAllowList());
+
+ boolean hasInvalidCerts =
+ ImmutableList.of(
+ registrarParameter.getClientCertificate(),
+ registrarParameter.getFailoverClientCertificate())
+ .stream()
+ .filter(Optional::isPresent)
+ .map(Optional::get)
+ .anyMatch(
+ cert -> {
+ try {
+ certificateChecker.validateCertificate(cert);
+ return false;
+ } catch (InsecureCertificateException e) {
+ return true;
+ }
+ });
+
+ if (hasInvalidCerts) {
+ response.setStatus(HttpStatusCodes.STATUS_CODE_BAD_REQUEST);
+ response.setPayload("Insecure Certificate in parameter");
+ return;
+ }
+
+ registrarParameter
+ .getClientCertificate()
+ .ifPresent(
+ newClientCert -> {
+ updatedRegistrar.setClientCertificate(newClientCert, tm().getTransactionTime());
+ });
+
+ registrarParameter
+ .getFailoverClientCertificate()
+ .ifPresent(
+ failoverCert -> {
+ updatedRegistrar.setFailoverClientCertificate(
+ failoverCert, tm().getTransactionTime());
+ });
+
+ tm().put(updatedRegistrar.build());
+ response.setStatus(HttpStatusCodes.STATUS_CODE_OK);
+ }
+}
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 9061cb9ab..429421bc4 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
@@ -24,6 +24,7 @@ import com.google.gson.Gson;
import com.google.gson.JsonObject;
import dagger.Module;
import dagger.Provides;
+import google.registry.model.registrar.Registrar;
import google.registry.model.registrar.RegistrarPoc;
import google.registry.request.OptionalJsonPayload;
import google.registry.request.Parameter;
@@ -187,4 +188,15 @@ public final class RegistrarConsoleModule {
static String provideRegistrarId(HttpServletRequest req) {
return extractRequiredParameter(req, "registrarId");
}
+
+ @Provides
+ @Parameter("registrar")
+ public static Optional provideRegistrar(
+ Gson gson, @OptionalJsonPayload Optional payload) {
+ if (payload.isPresent() && payload.get().has("registrar")) {
+ return Optional.of(gson.fromJson(payload.get().get("registrar"), Registrar.class));
+ }
+
+ return Optional.empty();
+ }
}
diff --git a/core/src/test/java/google/registry/ui/server/console/settings/SecurityActionTest.java b/core/src/test/java/google/registry/ui/server/console/settings/SecurityActionTest.java
new file mode 100644
index 000000000..b422a191e
--- /dev/null
+++ b/core/src/test/java/google/registry/ui/server/console/settings/SecurityActionTest.java
@@ -0,0 +1,183 @@
+// 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.testing.CertificateSamples.SAMPLE_CERT;
+import static google.registry.testing.CertificateSamples.SAMPLE_CERT2;
+import static google.registry.testing.DatabaseHelper.loadRegistrar;
+import static google.registry.testing.DatabaseHelper.persistResource;
+import static google.registry.testing.SqlHelper.saveRegistrar;
+import static google.registry.util.DateTimeUtils.START_OF_TIME;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.google.api.client.http.HttpStatusCodes;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.ImmutableSetMultimap;
+import com.google.common.collect.ImmutableSortedMap;
+import com.google.common.net.InetAddresses;
+import com.google.gson.Gson;
+import google.registry.flows.certs.CertificateChecker;
+import google.registry.model.console.GlobalRole;
+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.Action;
+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.UserAuthInfo;
+import google.registry.testing.FakeClock;
+import google.registry.testing.FakeResponse;
+import google.registry.ui.server.registrar.RegistrarConsoleModule;
+import google.registry.util.CidrAddressBlock;
+import google.registry.util.UtilsModule;
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.Optional;
+import javax.servlet.http.HttpServletRequest;
+import org.joda.time.DateTime;
+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.SecurityAction}. */
+class SecurityActionTest {
+
+ private static String jsonRegistrar1 =
+ String.format(
+ "{\"registrarId\": \"registrarId\", \"clientCertificate\": \"%s\","
+ + " \"ipAddressAllowList\": [\"192.168.1.1/32\"]}",
+ SAMPLE_CERT2);
+ private static final Gson GSON = UtilsModule.provideGson();
+ private final HttpServletRequest request = mock(HttpServletRequest.class);
+ private final FakeClock clock = new FakeClock();
+ private Registrar testRegistrar;
+ private FakeResponse response = new FakeResponse();
+
+ private AuthenticatedRegistrarAccessor registrarAccessor =
+ AuthenticatedRegistrarAccessor.createForTesting(
+ ImmutableSetMultimap.of("registrarId", AuthenticatedRegistrarAccessor.Role.ADMIN));
+
+ private CertificateChecker certificateChecker =
+ new CertificateChecker(
+ ImmutableSortedMap.of(START_OF_TIME, 20825, DateTime.parse("2020-09-01T00:00:00Z"), 398),
+ 30,
+ 15,
+ 2048,
+ ImmutableSet.of("secp256r1", "secp384r1"),
+ clock);
+
+ @RegisterExtension
+ final JpaTestExtensions.JpaIntegrationTestExtension jpa =
+ new JpaTestExtensions.Builder().withClock(clock).buildIntegrationTestExtension();
+
+ @BeforeEach
+ void beforeEach() {
+ testRegistrar = saveRegistrar("registrarId");
+ }
+
+ @Test
+ void testSuccess_getRegistrarInfo() throws IOException {
+ persistResource(
+ testRegistrar
+ .asBuilder()
+ .setClientCertificate(SAMPLE_CERT, clock.nowUtc())
+ .setIpAddressAllowList(
+ ImmutableSet.of(
+ CidrAddressBlock.create(InetAddresses.forString("192.168.1.1"), 32),
+ CidrAddressBlock.create(InetAddresses.forString("2001:db8::1"), 128)))
+ .build());
+ SecurityAction action =
+ createAction(
+ Action.Method.GET,
+ AuthResult.create(
+ AuthLevel.USER,
+ UserAuthInfo.create(
+ createUser(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build()))),
+ testRegistrar.getRegistrarId());
+ action.run();
+ assertThat(response.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_OK);
+ String payload = response.getPayload().replace("\\n", "").replace("\\u003d", "=");
+ assertThat(payload).contains(SAMPLE_CERT.replace("\n", ""));
+ assertThat(payload).contains("192.168.1.1/32");
+ assertThat(payload).contains("2001:db8:0:0:0:0:0:1/128");
+ }
+
+ @Test
+ void testSuccess_postRegistrarInfo() throws IOException {
+ clock.setTo(DateTime.parse("2020-11-01T00:00:00Z"));
+ SecurityAction action =
+ createAction(
+ Action.Method.POST,
+ AuthResult.create(
+ AuthLevel.USER,
+ UserAuthInfo.create(
+ createUser(new UserRoles.Builder().setGlobalRole(GlobalRole.FTE).build()))),
+ testRegistrar.getRegistrarId());
+ action.run();
+ assertThat(response.getStatus()).isEqualTo(HttpStatusCodes.STATUS_CODE_OK);
+ Registrar r = loadRegistrar(testRegistrar.getRegistrarId());
+ assertThat(r.getClientCertificateHash().get())
+ .isEqualTo("GNd6ZP8/n91t9UTnpxR8aH7aAW4+CpvufYx9ViGbcMY");
+ assertThat(r.getIpAddressAllowList().get(0).getIp()).isEqualTo("192.168.1.1");
+ assertThat(r.getIpAddressAllowList().get(0).getNetmask()).isEqualTo(32);
+ }
+
+ private User createUser(UserRoles userRoles) {
+ return new User.Builder()
+ .setEmailAddress("email@email.com")
+ .setGaiaId("TestUserId")
+ .setUserRoles(userRoles)
+ .build();
+ }
+
+ private SecurityAction createAction(
+ Action.Method method, AuthResult authResult, String registrarId) throws IOException {
+ when(request.getMethod()).thenReturn(method.toString());
+ if (method.equals(Action.Method.GET)) {
+ return new SecurityAction(
+ request,
+ authResult,
+ response,
+ GSON,
+ certificateChecker,
+ registrarAccessor,
+ registrarId,
+ Optional.empty());
+ } else {
+ doReturn(new BufferedReader(new StringReader("{\"registrar\":" + jsonRegistrar1 + "}")))
+ .when(request)
+ .getReader();
+ Optional maybeRegistrar =
+ RegistrarConsoleModule.provideRegistrar(
+ GSON, RequestModule.provideJsonBody(request, GSON));
+ return new SecurityAction(
+ request,
+ authResult,
+ response,
+ GSON,
+ certificateChecker,
+ registrarAccessor,
+ registrarId,
+ maybeRegistrar);
+ }
+ }
+}
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 863b2e757..731e191eb 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
@@ -3,6 +3,7 @@ PATH CLASS METHODS OK AUTH_ME
/console-api/domain ConsoleDomainGetAction GET n API,LEGACY USER PUBLIC
/console-api/registrars RegistrarsAction GET n API,LEGACY USER PUBLIC
/console-api/settings/contacts ContactAction GET,POST n API,LEGACY USER PUBLIC
+/console-api/settings/security SecurityAction GET,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
diff --git a/util/src/main/java/google/registry/util/CidrAddressBlock.java b/util/src/main/java/google/registry/util/CidrAddressBlock.java
index de20650fd..2c569acf2 100644
--- a/util/src/main/java/google/registry/util/CidrAddressBlock.java
+++ b/util/src/main/java/google/registry/util/CidrAddressBlock.java
@@ -19,6 +19,10 @@ import static com.google.common.base.Preconditions.checkArgument;
import com.google.common.collect.AbstractSequentialIterator;
import com.google.common.flogger.FluentLogger;
import com.google.common.net.InetAddresses;
+import com.google.gson.TypeAdapter;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonWriter;
+import java.io.IOException;
import java.io.Serializable;
import java.net.InetAddress;
import java.net.UnknownHostException;
@@ -476,4 +480,20 @@ public class CidrAddressBlock implements Iterable, Serializable {
public String toString() {
return getCidrString(ip, netmask);
}
+
+ public static class CidrAddressBlockAdapter extends TypeAdapter {
+ @Override
+ public CidrAddressBlock read(JsonReader reader) throws IOException {
+ String stringValue = reader.nextString();
+ if (stringValue.equals("null")) {
+ return null;
+ }
+ return new CidrAddressBlock(stringValue);
+ }
+
+ @Override
+ public void write(JsonWriter writer, CidrAddressBlock cidrAddressBlock) throws IOException {
+ writer.value(cidrAddressBlock.toString());
+ }
+ }
}
diff --git a/util/src/main/java/google/registry/util/UtilsModule.java b/util/src/main/java/google/registry/util/UtilsModule.java
index d3ab99bbb..4ce5e5679 100644
--- a/util/src/main/java/google/registry/util/UtilsModule.java
+++ b/util/src/main/java/google/registry/util/UtilsModule.java
@@ -19,6 +19,7 @@ import com.google.gson.GsonBuilder;
import dagger.Binds;
import dagger.Module;
import dagger.Provides;
+import google.registry.util.CidrAddressBlock.CidrAddressBlockAdapter;
import java.security.NoSuchAlgorithmException;
import java.security.ProviderException;
import java.security.SecureRandom;
@@ -79,6 +80,7 @@ public abstract class UtilsModule {
public static Gson provideGson() {
return new GsonBuilder()
.registerTypeAdapter(DateTime.class, new DateTimeTypeAdapter())
+ .registerTypeAdapter(CidrAddressBlock.class, new CidrAddressBlockAdapter())
.excludeFieldsWithoutExposeAnnotation()
.create();
}