1
0
mirror of https://github.com/google/nomulus synced 2026-01-08 07:11:44 +00:00

Add necessary changes to provision QA with Terraform (#2618)

Also programmatically determine backend service IDs.
This commit is contained in:
Lai Jiang
2024-12-12 13:39:18 -05:00
committed by GitHub
parent c6a6bc7e25
commit f9d2839590
28 changed files with 231 additions and 148 deletions

View File

@@ -46,7 +46,6 @@ import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.net.URI;
import java.net.URL;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.function.Supplier;
@@ -118,12 +117,6 @@ public final class RegistryConfig {
return config.gcpProject.projectIdNumber;
}
@Provides
@Config("backendServiceIds")
public static Map<String, Long> provideBackendServiceIds(RegistryConfigSettings config) {
return config.gcpProject.backendServiceIds;
}
@Provides
@Config("baseDomain")
public static String provideBaseDomain(RegistryConfigSettings config) {

View File

@@ -56,7 +56,6 @@ public class RegistryConfigSettings {
public String bsaServiceUrl;
public String toolsServiceUrl;
public String pubapiServiceUrl;
public Map<String, Long> backendServiceIds;
public String baseDomain;
}

View File

@@ -24,15 +24,6 @@ gcpProject:
toolsServiceUrl: https://tools.example.com
pubapiServiceUrl: https://pubapi.example.com
# The backend service IDs created when setting up GKE routes. They will be included in the
# audience field in the JWT that IAP creates.
# See: https://cloud.google.com/iap/docs/signed-headers-howto#verifying_the_jwt_payload
backendServiceIds:
frontend: 12345
backend: 12345
pubapi: 12345
console: 12345
# The base domain name of the registry service. Services are reachable at [service].baseDomain.
baseDomain: registry.test

View File

@@ -14,19 +14,31 @@
package google.registry.request.auth;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.net.HttpHeaders.AUTHORIZATION;
import static google.registry.util.RegistryEnvironment.UNITTEST;
import com.google.cloud.compute.v1.BackendService;
import com.google.cloud.compute.v1.BackendServicesClient;
import com.google.cloud.compute.v1.BackendServicesSettings;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.re2j.Matcher;
import com.google.re2j.Pattern;
import dagger.Lazy;
import dagger.Module;
import dagger.Provides;
import google.registry.config.CredentialModule.ApplicationDefaultCredential;
import google.registry.config.RegistryConfig.Config;
import google.registry.request.auth.OidcTokenAuthenticationMechanism.IapOidcAuthenticationMechanism;
import google.registry.request.auth.OidcTokenAuthenticationMechanism.RegularOidcAuthenticationMechanism;
import google.registry.request.auth.OidcTokenAuthenticationMechanism.TokenExtractor;
import google.registry.request.auth.OidcTokenAuthenticationMechanism.TokenVerifier;
import google.registry.util.GoogleCredentialsBundle;
import google.registry.util.RegistryEnvironment;
import java.util.Map;
import java.io.IOException;
import javax.annotation.Nullable;
import javax.inject.Named;
import javax.inject.Qualifier;
import javax.inject.Singleton;
@@ -44,6 +56,13 @@ public class AuthModule {
private static final String IAP_GKE_AUDIENCE_FORMAT = "/projects/%d/global/backendServices/%d";
private static final String IAP_ISSUER_URL = "https://cloud.google.com/iap";
private static final String REGULAR_ISSUER_URL = "https://accounts.google.com";
// The backend service IDs created when setting up GKE routes. They will be included in the
// audience field in the JWT that IAP creates.
// See: https://cloud.google.com/iap/docs/signed-headers-howto#verifying_the_jwt_payload
// The automatically generated backend service ID has the following format:
// gkemcg1-default-console[-canary]-80-(some random string)
private static final Pattern BACKEND_END_PATTERN =
Pattern.compile(".*-default-((frontend|backend|console|pubapi)(-canary)?)-80-.*");
/** Provides the custom authentication mechanisms. */
@Provides
@@ -68,13 +87,18 @@ public class AuthModule {
TokenVerifier provideIapTokenVerifier(
@Config("projectId") String projectId,
@Config("projectIdNumber") long projectIdNumber,
@Config("backendServiceIds") Map<String, Long> backendServiceIds) {
@Named("backendServiceIdMap") ImmutableMap<String, Long> backendServiceIdMap) {
com.google.auth.oauth2.TokenVerifier.Builder tokenVerifierBuilder =
com.google.auth.oauth2.TokenVerifier.newBuilder().setIssuer(IAP_ISSUER_URL);
return (String service, String token) -> {
String audience;
if (RegistryEnvironment.isOnJetty()) {
long backendServiceId = backendServiceIds.get(service);
Long backendServiceId = backendServiceIdMap.get(service);
checkNotNull(
backendServiceId,
"Backend service ID not found for service: %s, available IDs are %s",
service,
backendServiceIdMap);
audience = String.format(IAP_GKE_AUDIENCE_FORMAT, projectIdNumber, backendServiceId);
} else {
audience = String.format(IAP_GAE_AUDIENCE_FORMAT, projectIdNumber, projectId);
@@ -116,4 +140,38 @@ public class AuthModule {
return null;
};
}
@Provides
@Singleton
static BackendServicesClient provideBackendServicesClients(
@ApplicationDefaultCredential GoogleCredentialsBundle credentialsBundle) {
try {
return BackendServicesClient.create(
BackendServicesSettings.newBuilder()
.setCredentialsProvider(credentialsBundle::getGoogleCredentials)
.build());
} catch (IOException e) {
throw new RuntimeException(e);
}
}
@Provides
@Singleton
@Named("backendServiceIdMap")
static ImmutableMap<String, Long> provideBackendServiceList(
Lazy<BackendServicesClient> client, @Config("projectId") String projectId) {
if (RegistryEnvironment.isInTestServer() || RegistryEnvironment.get() == UNITTEST) {
return ImmutableMap.of();
}
ImmutableMap.Builder<String, Long> builder = ImmutableMap.builder();
for (BackendService service : client.get().list(projectId).iterateAll()) {
String name = service.getName();
Matcher matcher = BACKEND_END_PATTERN.matcher(name);
if (!matcher.matches()) {
continue;
}
builder.put(matcher.group(1), service.getId());
}
return builder.build();
}
}

View File

@@ -87,6 +87,9 @@ public abstract class OidcTokenAuthenticationMechanism implements Authentication
if (RegistryEnvironment.isOnJetty()) {
String hostname = request.getServerName();
service = Splitter.on('.').split(hostname).iterator().next();
if (request.getHeader("canary") != null) {
service += "-canary";
}
}
token = tokenVerifier.verify(service, rawIdToken);
} catch (Exception e) {

View File

@@ -80,15 +80,14 @@ class CurlCommand implements CommandWithConnection {
required = true)
private String serviceName;
@Parameter(
names = {"--canary"},
description = "If set, use the canary end-point; otherwise use the regular end-point.")
private Boolean canary = Boolean.FALSE;
@Inject
@Config("useGke")
boolean useGke;
@Inject
@Config("useCanary")
boolean useCanary;
@Override
public void setConnection(ServiceConnection connection) {
this.connection = connection;
@@ -109,7 +108,7 @@ class CurlCommand implements CommandWithConnection {
? GkeService.valueOf(Ascii.toUpperCase(serviceName))
: GaeService.valueOf(Ascii.toUpperCase(serviceName));
ServiceConnection connectionToService = connection.withService(service, canary);
ServiceConnection connectionToService = connection.withService(service, useCanary);
String response =
(method == Method.GET)
? connectionToService.sendGetRequest(path, ImmutableMap.<String, String>of())

View File

@@ -73,6 +73,9 @@ final class RegistryCli implements CommandRunner {
@Parameter(names = "--gke", description = "Whether to use GKE runtime, instead of GAE")
private boolean useGke = false;
@Parameter(names = "--canary", description = "Whether to connect to the canary instances")
private boolean useCanary = false;
// Do not make this final - compile-time constant inlining may interfere with JCommander.
@ParametersDelegate private LoggingParameters loggingParams = new LoggingParameters();
@@ -166,6 +169,7 @@ final class RegistryCli implements CommandRunner {
.credentialFilePath(credentialJson)
.sqlAccessInfoFile(sqlAccessInfoFile)
.useGke(useGke)
.useCanary(useCanary)
.build();
// JCommander stores sub-commands as nested JCommander objects containing a list of user objects
@@ -196,9 +200,9 @@ final class RegistryCli implements CommandRunner {
System.err.println("===================================================================");
System.err.println(
"""
This error is likely the result of having another instance of
nomulus running at the same time. Check your system, shut down
the other instance, and try again.""");
This error is likely the result of having another instance of
nomulus running at the same time. Check your system, shut down
the other instance, and try again.""");
System.err.println("===================================================================");
} else {
throw e;

View File

@@ -195,6 +195,9 @@ interface RegistryToolComponent {
@BindsInstance
Builder useGke(@Config("useGke") boolean useGke);
@BindsInstance
Builder useCanary(@Config("useCanary") boolean useCanary);
RegistryToolComponent build();
}
}

View File

@@ -65,8 +65,11 @@ public class ServiceConnection {
private final HttpRequestFactory requestFactory;
@Inject
ServiceConnection(@Config("useGke") boolean useGke, HttpRequestFactory requestFactory) {
this(useGke ? GkeService.BACKEND : GaeService.TOOLS, requestFactory, false);
ServiceConnection(
@Config("useGke") boolean useGke,
@Config("useCanary") boolean useCanary,
HttpRequestFactory requestFactory) {
this(useGke ? GkeService.BACKEND : GaeService.TOOLS, requestFactory, useCanary);
}
private ServiceConnection(Service service, HttpRequestFactory requestFactory, boolean useCanary) {

View File

@@ -157,8 +157,9 @@ public class ConsoleUsersAction extends ConsoleApiAction {
return;
}
ImmutableList<User> allRegistrarUsers = getAllRegistrarUsers(registrarId);
if (allRegistrarUsers.size() >= 4)
if (allRegistrarUsers.size() >= 4) {
throw new BadRequestException("Total users amount per registrar is limited to 4");
}
updateUserRegistrarRoles(
this.userData.get().emailAddress,
@@ -198,8 +199,9 @@ public class ConsoleUsersAction extends ConsoleApiAction {
private void runCreateInTransaction() throws IOException {
ImmutableList<User> allRegistrarUsers = getAllRegistrarUsers(registrarId);
if (allRegistrarUsers.size() >= 4)
if (allRegistrarUsers.size() >= 4) {
throw new BadRequestException("Total users amount per registrar is limited to 4");
}
String nextAvailableEmail =
IntStream.range(1, 5)

View File

@@ -26,12 +26,13 @@ import static org.mockito.Mockito.when;
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken.Payload;
import com.google.api.client.json.webtoken.JsonWebSignature;
import com.google.api.client.json.webtoken.JsonWebSignature.Header;
import com.google.auth.oauth2.GoogleCredentials;
import com.google.auth.oauth2.TokenVerifier.VerificationException;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import dagger.Component;
import dagger.Module;
import dagger.Provides;
import google.registry.config.CredentialModule.ApplicationDefaultCredential;
import google.registry.config.RegistryConfig.Config;
import google.registry.model.console.GlobalRole;
import google.registry.model.console.User;
@@ -40,8 +41,8 @@ import google.registry.persistence.transaction.JpaTestExtensions;
import google.registry.request.auth.AuthSettings.AuthLevel;
import google.registry.request.auth.OidcTokenAuthenticationMechanism.IapOidcAuthenticationMechanism;
import google.registry.request.auth.OidcTokenAuthenticationMechanism.RegularOidcAuthenticationMechanism;
import google.registry.util.GoogleCredentialsBundle;
import jakarta.servlet.http.HttpServletRequest;
import java.util.Map;
import javax.inject.Singleton;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
@@ -228,9 +229,9 @@ public class OidcTokenAuthenticationMechanismTest {
@Provides
@Singleton
@Config("backendServiceIds")
Map<String, Long> provideBackendServiceIds() {
return ImmutableMap.of();
@ApplicationDefaultCredential
GoogleCredentialsBundle provideGoogleCredentialBundle() {
return GoogleCredentialsBundle.create(GoogleCredentials.newBuilder().build());
}
}
}

View File

@@ -157,19 +157,6 @@ class CurlCommandTest extends CommandTestCase<CurlCommand> {
eq("".getBytes(UTF_8)));
}
@Test
void testCanaryInvocation() throws Exception {
runCommand("--path=/foo/bar?a=1&b=2", "--request=POST", "--service=TOOLS", "--canary");
verify(connection).withService(eq(TOOLS), eq(true));
verifyNoMoreInteractions(connection);
verify(connectionForService)
.sendPostRequest(
eq("/foo/bar?a=1&b=2"),
eq(ImmutableMap.<String, String>of()),
eq(MediaType.PLAIN_TEXT_UTF_8),
eq("".getBytes(UTF_8)));
}
@Test
@MockitoSettings(strictness = Strictness.LENIENT)
void testGetWithBody() {

View File

@@ -85,7 +85,7 @@ final class GcpProjectConnectionTest {
when(lowLevelHttpResponse.getStatusCode()).thenReturn(200);
httpTransport = new TestHttpTransport();
connection = new ServiceConnection(false, httpTransport.createRequestFactory());
connection = new ServiceConnection(false, false, httpTransport.createRequestFactory());
}
@Test

View File

@@ -37,7 +37,8 @@ public class ServiceConnectionTest {
@Test
void testSuccess_serverUrl_notCanary() {
ServiceConnection connection = new ServiceConnection(false, null).withService(DEFAULT, false);
ServiceConnection connection =
new ServiceConnection(false, false, null).withService(DEFAULT, false);
String serverUrl = connection.getServer().toString();
assertThat(serverUrl).isEqualTo("https://default.example.com"); // See default-config.yaml
}
@@ -48,14 +49,15 @@ public class ServiceConnectionTest {
assertThrows(
IllegalArgumentException.class,
() -> {
new ServiceConnection(true, null).withService(DEFAULT, true);
new ServiceConnection(true, false, null).withService(DEFAULT, true);
});
assertThat(thrown).hasMessageThat().contains("Cannot switch from GkeService to GaeService");
}
@Test
void testSuccess_serverUrl_gae_canary() {
ServiceConnection connection = new ServiceConnection(false, null).withService(DEFAULT, true);
ServiceConnection connection =
new ServiceConnection(false, false, null).withService(DEFAULT, true);
String serverUrl = connection.getServer().toString();
assertThat(serverUrl).isEqualTo("https://nomulus-dot-default.example.com");
}
@@ -71,7 +73,7 @@ public class ServiceConnectionTest {
when(request.execute()).thenReturn(response);
when(response.getContent()).thenReturn(ByteArrayInputStream.nullInputStream());
ServiceConnection connection =
new ServiceConnection(true, factory).withService(GkeService.PUBAPI, true);
new ServiceConnection(true, false, factory).withService(GkeService.PUBAPI, true);
String serverUrl = connection.getServer().toString();
assertThat(serverUrl).isEqualTo("https://pubapi.registry.test");
connection.sendGetRequest("/path", ImmutableMap.of());