mirror of
https://github.com/google/nomulus.git
synced 2025-05-08 07:48:21 +02:00
Change the cookie auth mechanism to use IAP-provided JWTs (#1877)
This commit is contained in:
parent
2a222ca935
commit
f7b7461891
8 changed files with 135 additions and 114 deletions
|
@ -106,6 +106,12 @@ public final class RegistryConfig {
|
||||||
return config.gcpProject.projectId;
|
return config.gcpProject.projectId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Config("projectIdNumber")
|
||||||
|
public static long provideProjectIdNumber(RegistryConfigSettings config) {
|
||||||
|
return config.gcpProject.projectIdNumber;
|
||||||
|
}
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Config("locationId")
|
@Config("locationId")
|
||||||
public static String provideLocationId(RegistryConfigSettings config) {
|
public static String provideLocationId(RegistryConfigSettings config) {
|
||||||
|
|
|
@ -47,6 +47,7 @@ public class RegistryConfigSettings {
|
||||||
/** Configuration options that apply to the entire GCP project. */
|
/** Configuration options that apply to the entire GCP project. */
|
||||||
public static class GcpProject {
|
public static class GcpProject {
|
||||||
public String projectId;
|
public String projectId;
|
||||||
|
public long projectIdNumber;
|
||||||
public String locationId;
|
public String locationId;
|
||||||
public boolean isLocal;
|
public boolean isLocal;
|
||||||
public String defaultServiceUrl;
|
public String defaultServiceUrl;
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
gcpProject:
|
gcpProject:
|
||||||
# Globally unique GCP project ID
|
# Globally unique GCP project ID
|
||||||
projectId: registry-project-id
|
projectId: registry-project-id
|
||||||
|
# Corresponding project ID number
|
||||||
|
projectIdNumber: 123456789012
|
||||||
# Location of the GCP project, note that us-central1 and europe-west1 are special in that
|
# Location of the GCP project, note that us-central1 and europe-west1 are special in that
|
||||||
# they are used without the trailing number in GCP commands and Google Cloud Console.
|
# they are used without the trailing number in GCP commands and Google Cloud Console.
|
||||||
# See: https://cloud.google.com/appengine/docs/locations as an example
|
# See: https://cloud.google.com/appengine/docs/locations as an example
|
||||||
|
|
|
@ -14,13 +14,10 @@
|
||||||
|
|
||||||
package google.registry.request.auth;
|
package google.registry.request.auth;
|
||||||
|
|
||||||
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
|
|
||||||
import com.google.api.client.http.javanet.NetHttpTransport;
|
|
||||||
import com.google.api.client.json.JsonFactory;
|
|
||||||
import com.google.appengine.api.oauth.OAuthService;
|
import com.google.appengine.api.oauth.OAuthService;
|
||||||
import com.google.appengine.api.oauth.OAuthServiceFactory;
|
import com.google.appengine.api.oauth.OAuthServiceFactory;
|
||||||
|
import com.google.auth.oauth2.TokenVerifier;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.collect.ImmutableSet;
|
|
||||||
import dagger.Module;
|
import dagger.Module;
|
||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
import google.registry.config.RegistryConfig.Config;
|
import google.registry.config.RegistryConfig.Config;
|
||||||
|
@ -32,12 +29,14 @@ import javax.inject.Singleton;
|
||||||
@Module
|
@Module
|
||||||
public class AuthModule {
|
public class AuthModule {
|
||||||
|
|
||||||
|
private static final String IAP_ISSUER_URL = "https://cloud.google.com/iap";
|
||||||
|
|
||||||
/** Provides the custom authentication mechanisms (including OAuth). */
|
/** Provides the custom authentication mechanisms (including OAuth). */
|
||||||
@Provides
|
@Provides
|
||||||
ImmutableList<AuthenticationMechanism> provideApiAuthenticationMechanisms(
|
ImmutableList<AuthenticationMechanism> provideApiAuthenticationMechanisms(
|
||||||
OAuthAuthenticationMechanism oauthAuthenticationMechanism,
|
OAuthAuthenticationMechanism oauthAuthenticationMechanism,
|
||||||
CookieOAuth2AuthenticationMechanism cookieOAuth2AuthenticationMechanism) {
|
IapHeaderAuthenticationMechanism iapHeaderAuthenticationMechanism) {
|
||||||
return ImmutableList.of(oauthAuthenticationMechanism, cookieOAuth2AuthenticationMechanism);
|
return ImmutableList.of(oauthAuthenticationMechanism, iapHeaderAuthenticationMechanism);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Provides the OAuthService instance. */
|
/** Provides the OAuthService instance. */
|
||||||
|
@ -48,12 +47,9 @@ public class AuthModule {
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
GoogleIdTokenVerifier provideGoogleIdTokenVerifier(
|
TokenVerifier provideTokenVerifier(
|
||||||
@Config("allowedOauthClientIds") ImmutableSet<String> allowedOauthClientIds,
|
@Config("projectId") String projectId, @Config("projectIdNumber") long projectIdNumber) {
|
||||||
NetHttpTransport httpTransport,
|
String audience = String.format("/projects/%d/apps/%s", projectIdNumber, projectId);
|
||||||
JsonFactory jsonFactory) {
|
return TokenVerifier.newBuilder().setAudience(audience).setIssuer(IAP_ISSUER_URL).build();
|
||||||
return new GoogleIdTokenVerifier.Builder(httpTransport, jsonFactory)
|
|
||||||
.setAudience(allowedOauthClientIds)
|
|
||||||
.build();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,89 +0,0 @@
|
||||||
// Copyright 2022 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.request.auth;
|
|
||||||
|
|
||||||
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
|
|
||||||
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
|
|
||||||
import com.google.common.flogger.FluentLogger;
|
|
||||||
import google.registry.model.console.User;
|
|
||||||
import google.registry.model.console.UserDao;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.security.GeneralSecurityException;
|
|
||||||
import java.util.Optional;
|
|
||||||
import javax.annotation.Nullable;
|
|
||||||
import javax.inject.Inject;
|
|
||||||
import javax.servlet.http.Cookie;
|
|
||||||
import javax.servlet.http.HttpServletRequest;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A way to authenticate HTTP requests using OAuth2 ID tokens stored in cookies.
|
|
||||||
*
|
|
||||||
* <p>This is generic to Google Single-Sign-On and doesn't have any ties with Google App Engine.
|
|
||||||
*/
|
|
||||||
public class CookieOAuth2AuthenticationMechanism implements AuthenticationMechanism {
|
|
||||||
|
|
||||||
private static final String ID_TOKEN_COOKIE_NAME = "idToken";
|
|
||||||
|
|
||||||
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
|
||||||
|
|
||||||
private final GoogleIdTokenVerifier googleIdTokenVerifier;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
public CookieOAuth2AuthenticationMechanism(GoogleIdTokenVerifier googleIdTokenVerifier) {
|
|
||||||
this.googleIdTokenVerifier = googleIdTokenVerifier;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public AuthResult authenticate(HttpServletRequest request) {
|
|
||||||
String rawIdToken = getRawIdTokenFromCookie(request);
|
|
||||||
if (rawIdToken == null) {
|
|
||||||
return AuthResult.NOT_AUTHENTICATED;
|
|
||||||
}
|
|
||||||
GoogleIdToken googleIdToken;
|
|
||||||
try {
|
|
||||||
googleIdToken = googleIdTokenVerifier.verify(rawIdToken);
|
|
||||||
} catch (IOException | GeneralSecurityException e) {
|
|
||||||
logger.atInfo().withCause(e).log("Error when verifying access token");
|
|
||||||
return AuthResult.NOT_AUTHENTICATED;
|
|
||||||
}
|
|
||||||
// A null token means the provided ID token was invalid or expired
|
|
||||||
if (googleIdToken == null) {
|
|
||||||
logger.atInfo().log("Token %s failed validation", rawIdToken);
|
|
||||||
return AuthResult.NOT_AUTHENTICATED;
|
|
||||||
}
|
|
||||||
String emailAddress = googleIdToken.getPayload().getEmail();
|
|
||||||
Optional<User> maybeUser = UserDao.loadUser(emailAddress);
|
|
||||||
if (!maybeUser.isPresent()) {
|
|
||||||
logger.atInfo().log("No user found for email address %s", emailAddress);
|
|
||||||
return AuthResult.NOT_AUTHENTICATED;
|
|
||||||
}
|
|
||||||
return AuthResult.create(AuthLevel.USER, UserAuthInfo.create(maybeUser.get()));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private String getRawIdTokenFromCookie(HttpServletRequest request) {
|
|
||||||
if (request.getCookies() == null) {
|
|
||||||
logger.atInfo().log("No cookies passed in request");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
for (Cookie cookie : request.getCookies()) {
|
|
||||||
if (cookie.getName().equals(ID_TOKEN_COOKIE_NAME)) {
|
|
||||||
return cookie.getValue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
logger.atInfo().log("No ID token cookie");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
// Copyright 2022 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.request.auth;
|
||||||
|
|
||||||
|
import com.google.api.client.json.webtoken.JsonWebSignature;
|
||||||
|
import com.google.auth.oauth2.TokenVerifier;
|
||||||
|
import com.google.common.annotations.VisibleForTesting;
|
||||||
|
import com.google.common.flogger.FluentLogger;
|
||||||
|
import google.registry.config.RegistryEnvironment;
|
||||||
|
import google.registry.model.console.User;
|
||||||
|
import google.registry.model.console.UserDao;
|
||||||
|
import java.util.Optional;
|
||||||
|
import javax.annotation.Nullable;
|
||||||
|
import javax.inject.Inject;
|
||||||
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A way to authenticate HTTP requests that have gone through the GCP Identity-Aware Proxy.
|
||||||
|
*
|
||||||
|
* <p>When the user logs in, IAP provides a JWT in the <code>X-Goog-IAP-JWT-Assertion</code> header.
|
||||||
|
* This header is included on all requests to IAP-enabled services (which should be all of them that
|
||||||
|
* receive requests from the front end). The token verification libraries ensure that the signed
|
||||||
|
* token has the proper audience and issuer.
|
||||||
|
*
|
||||||
|
* @see <a href="https://cloud.google.com/iap/docs/signed-headers-howto">the documentation on GCP
|
||||||
|
* IAP's signed headers for more information.</a>
|
||||||
|
*/
|
||||||
|
public class IapHeaderAuthenticationMechanism implements AuthenticationMechanism {
|
||||||
|
|
||||||
|
private static final String ID_TOKEN_HEADER_NAME = "X-Goog-IAP-JWT-Assertion";
|
||||||
|
|
||||||
|
private static final FluentLogger logger = FluentLogger.forEnclosingClass();
|
||||||
|
|
||||||
|
// A workaround that allows "use" of the IAP-based authenticator when running local testing, i.e.
|
||||||
|
// the RegistryTestServer
|
||||||
|
private static Optional<User> userForTesting = Optional.empty();
|
||||||
|
|
||||||
|
private final TokenVerifier tokenVerifier;
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public IapHeaderAuthenticationMechanism(TokenVerifier tokenVerifier) {
|
||||||
|
this.tokenVerifier = tokenVerifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AuthResult authenticate(HttpServletRequest request) {
|
||||||
|
if (RegistryEnvironment.get().equals(RegistryEnvironment.UNITTEST)
|
||||||
|
&& userForTesting.isPresent()) {
|
||||||
|
return AuthResult.create(AuthLevel.USER, UserAuthInfo.create(userForTesting.get()));
|
||||||
|
}
|
||||||
|
String rawIdToken = request.getHeader(ID_TOKEN_HEADER_NAME);
|
||||||
|
if (rawIdToken == null) {
|
||||||
|
return AuthResult.NOT_AUTHENTICATED;
|
||||||
|
}
|
||||||
|
JsonWebSignature token;
|
||||||
|
try {
|
||||||
|
token = tokenVerifier.verify(rawIdToken);
|
||||||
|
} catch (TokenVerifier.VerificationException e) {
|
||||||
|
logger.atInfo().withCause(e).log("Error when verifying access token");
|
||||||
|
return AuthResult.NOT_AUTHENTICATED;
|
||||||
|
}
|
||||||
|
String emailAddress = (String) token.getPayload().get("email");
|
||||||
|
Optional<User> maybeUser = UserDao.loadUser(emailAddress);
|
||||||
|
if (!maybeUser.isPresent()) {
|
||||||
|
logger.atInfo().log("No user found for email address %s", emailAddress);
|
||||||
|
return AuthResult.NOT_AUTHENTICATED;
|
||||||
|
}
|
||||||
|
return AuthResult.create(AuthLevel.USER, UserAuthInfo.create(maybeUser.get()));
|
||||||
|
}
|
||||||
|
|
||||||
|
@VisibleForTesting
|
||||||
|
public static void setUserAuthInfoForTestServer(@Nullable User user) {
|
||||||
|
userForTesting = Optional.ofNullable(user);
|
||||||
|
}
|
||||||
|
}
|
|
@ -18,16 +18,15 @@ import static com.google.common.truth.Truth.assertThat;
|
||||||
import static google.registry.testing.DatabaseHelper.insertInDb;
|
import static google.registry.testing.DatabaseHelper.insertInDb;
|
||||||
import static org.mockito.Mockito.when;
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken;
|
|
||||||
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken.Payload;
|
import com.google.api.client.googleapis.auth.oauth2.GoogleIdToken.Payload;
|
||||||
import com.google.api.client.googleapis.auth.oauth2.GoogleIdTokenVerifier;
|
import com.google.api.client.json.webtoken.JsonWebSignature;
|
||||||
import com.google.api.client.json.webtoken.JsonWebSignature.Header;
|
import com.google.api.client.json.webtoken.JsonWebSignature.Header;
|
||||||
|
import com.google.auth.oauth2.TokenVerifier;
|
||||||
import com.google.common.truth.Truth8;
|
import com.google.common.truth.Truth8;
|
||||||
import google.registry.model.console.GlobalRole;
|
import google.registry.model.console.GlobalRole;
|
||||||
import google.registry.model.console.User;
|
import google.registry.model.console.User;
|
||||||
import google.registry.model.console.UserRoles;
|
import google.registry.model.console.UserRoles;
|
||||||
import google.registry.persistence.transaction.JpaTestExtensions;
|
import google.registry.persistence.transaction.JpaTestExtensions;
|
||||||
import java.security.GeneralSecurityException;
|
|
||||||
import javax.servlet.http.Cookie;
|
import javax.servlet.http.Cookie;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
@ -36,28 +35,33 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
import org.mockito.junit.jupiter.MockitoExtension;
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.mockito.junit.jupiter.MockitoSettings;
|
||||||
|
import org.mockito.quality.Strictness;
|
||||||
|
|
||||||
/** Tests for {@link CookieOAuth2AuthenticationMechanism}. */
|
/** Tests for {@link IapHeaderAuthenticationMechanism}. */
|
||||||
@ExtendWith(MockitoExtension.class)
|
@ExtendWith(MockitoExtension.class)
|
||||||
public class CookieOAuth2AuthenticationMechanismTest {
|
@MockitoSettings(strictness = Strictness.LENIENT)
|
||||||
|
public class IapHeaderAuthenticationMechanismTest {
|
||||||
|
|
||||||
@RegisterExtension
|
@RegisterExtension
|
||||||
public final JpaTestExtensions.JpaUnitTestExtension jpaExtension =
|
public final JpaTestExtensions.JpaUnitTestExtension jpaExtension =
|
||||||
new JpaTestExtensions.Builder().withEntityClass(User.class).buildUnitTestExtension();
|
new JpaTestExtensions.Builder().withEntityClass(User.class).buildUnitTestExtension();
|
||||||
|
|
||||||
@Mock private GoogleIdTokenVerifier tokenVerifier;
|
@Mock private TokenVerifier tokenVerifier;
|
||||||
@Mock private HttpServletRequest request;
|
@Mock private HttpServletRequest request;
|
||||||
|
|
||||||
private GoogleIdToken token;
|
private JsonWebSignature token;
|
||||||
private CookieOAuth2AuthenticationMechanism authenticationMechanism;
|
private IapHeaderAuthenticationMechanism authenticationMechanism;
|
||||||
|
|
||||||
@BeforeEach
|
@BeforeEach
|
||||||
void beforeEach() {
|
void beforeEach() throws Exception {
|
||||||
authenticationMechanism = new CookieOAuth2AuthenticationMechanism(tokenVerifier);
|
authenticationMechanism = new IapHeaderAuthenticationMechanism(tokenVerifier);
|
||||||
|
when(request.getHeader("X-Goog-IAP-JWT-Assertion")).thenReturn("jwtValue");
|
||||||
Payload payload = new Payload();
|
Payload payload = new Payload();
|
||||||
payload.setEmail("email@email.com");
|
payload.setEmail("email@email.com");
|
||||||
payload.setSubject("gaiaId");
|
payload.setSubject("gaiaId");
|
||||||
token = new GoogleIdToken(new Header(), payload, new byte[0], new byte[0]);
|
token = new JsonWebSignature(new Header(), payload, new byte[0], new byte[0]);
|
||||||
|
when(tokenVerifier.verify("jwtValue")).thenReturn(token);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@ -94,7 +98,7 @@ public class CookieOAuth2AuthenticationMechanismTest {
|
||||||
@Test
|
@Test
|
||||||
void testFailure_errorVerifyingToken() throws Exception {
|
void testFailure_errorVerifyingToken() throws Exception {
|
||||||
when(request.getCookies()).thenReturn(new Cookie[] {new Cookie("idToken", "asdf")});
|
when(request.getCookies()).thenReturn(new Cookie[] {new Cookie("idToken", "asdf")});
|
||||||
when(tokenVerifier.verify("asdf")).thenThrow(new GeneralSecurityException("hi"));
|
when(tokenVerifier.verify("asdf")).thenThrow(new TokenVerifier.VerificationException("hi"));
|
||||||
assertThat(authenticationMechanism.authenticate(request).isAuthenticated()).isFalse();
|
assertThat(authenticationMechanism.authenticate(request).isAuthenticated()).isFalse();
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,11 @@ import com.beust.jcommander.Parameter;
|
||||||
import com.beust.jcommander.Parameters;
|
import com.beust.jcommander.Parameters;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.net.HostAndPort;
|
import com.google.common.net.HostAndPort;
|
||||||
|
import google.registry.model.console.GlobalRole;
|
||||||
|
import google.registry.model.console.User;
|
||||||
|
import google.registry.model.console.UserRoles;
|
||||||
import google.registry.persistence.transaction.JpaTestExtensions;
|
import google.registry.persistence.transaction.JpaTestExtensions;
|
||||||
|
import google.registry.request.auth.IapHeaderAuthenticationMechanism;
|
||||||
import google.registry.testing.AppEngineExtension;
|
import google.registry.testing.AppEngineExtension;
|
||||||
import google.registry.testing.UserInfo;
|
import google.registry.testing.UserInfo;
|
||||||
import google.registry.tools.params.HostAndPortParameter;
|
import google.registry.tools.params.HostAndPortParameter;
|
||||||
|
@ -138,6 +142,16 @@ public final class RegistryTestServerMain {
|
||||||
loginIsAdmin ? UserInfo.createAdmin(loginEmail) : UserInfo.create(loginEmail))
|
loginIsAdmin ? UserInfo.createAdmin(loginEmail) : UserInfo.create(loginEmail))
|
||||||
.build();
|
.build();
|
||||||
appEngine.setUp();
|
appEngine.setUp();
|
||||||
|
UserRoles userRoles =
|
||||||
|
new UserRoles.Builder().setIsAdmin(loginIsAdmin).setGlobalRole(GlobalRole.FTE).build();
|
||||||
|
User user =
|
||||||
|
new User.Builder()
|
||||||
|
.setEmailAddress(loginEmail)
|
||||||
|
.setGaiaId("123457890")
|
||||||
|
.setUserRoles(userRoles)
|
||||||
|
.setRegistryLockPassword("registryLockPassword")
|
||||||
|
.build();
|
||||||
|
IapHeaderAuthenticationMechanism.setUserAuthInfoForTestServer(user);
|
||||||
new JpaTestExtensions.Builder().buildIntegrationTestExtension().beforeEach(null);
|
new JpaTestExtensions.Builder().buildIntegrationTestExtension().beforeEach(null);
|
||||||
AppEngineExtension.loadInitialData();
|
AppEngineExtension.loadInitialData();
|
||||||
System.out.printf("%sLoading fixtures...%s\n", BLUE, RESET);
|
System.out.printf("%sLoading fixtures...%s\n", BLUE, RESET);
|
||||||
|
|
Loading…
Add table
Reference in a new issue