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:
@@ -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) {
|
||||
|
||||
@@ -56,7 +56,6 @@ public class RegistryConfigSettings {
|
||||
public String bsaServiceUrl;
|
||||
public String toolsServiceUrl;
|
||||
public String pubapiServiceUrl;
|
||||
public Map<String, Long> backendServiceIds;
|
||||
public String baseDomain;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -195,6 +195,9 @@ interface RegistryToolComponent {
|
||||
@BindsInstance
|
||||
Builder useGke(@Config("useGke") boolean useGke);
|
||||
|
||||
@BindsInstance
|
||||
Builder useCanary(@Config("useCanary") boolean useCanary);
|
||||
|
||||
RegistryToolComponent build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user