diff --git a/gcp-common/src/main/java/io/micronaut/gcp/credentials/AuthenticationLoggingInterceptor.java b/gcp-common/src/main/java/io/micronaut/gcp/credentials/AuthenticationLoggingInterceptor.java deleted file mode 100644 index e825a5483..000000000 --- a/gcp-common/src/main/java/io/micronaut/gcp/credentials/AuthenticationLoggingInterceptor.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2017-2023 original authors - * - * 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 - * - * https://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 io.micronaut.gcp.credentials; - -import com.google.auth.RequestMetadataCallback; -import io.micronaut.aop.MethodInterceptor; -import io.micronaut.aop.MethodInvocationContext; -import io.micronaut.core.annotation.Nullable; -import io.micronaut.core.type.MutableArgumentValue; -import jakarta.inject.Singleton; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.io.IOException; -import java.util.List; -import java.util.Map; - -/** - * An interceptor for managed instances of {@link com.google.auth.oauth2.GoogleCredentials} that logs certain types of - * authentication errors that the GCP libraries handle silently as infinitely retryable events. - * - * @author Jeremy Grelle - * @since 5.2.0 - */ -@Singleton -public class AuthenticationLoggingInterceptor implements MethodInterceptor { - - private static final Logger LOG = LoggerFactory.getLogger(AuthenticationLoggingInterceptor.class); - private static final String LOGGED_AUTHENTICATION_METHOD = "getRequestMetadata"; - - /** - * Intercepts the "getRequestMetadata" call and logs any retryable errors before allowing the GCP library to continue - * its normal retry algorithm. - * - * @param context The method invocation context - * @return the result of the method invocation - */ - @Override - public @Nullable Object intercept(MethodInvocationContext context) { - if (!context.getExecutableMethod().getMethodName().equals(LOGGED_AUTHENTICATION_METHOD)) { - return context.proceed(); - } - Map> params = context.getParameters(); - params.entrySet().stream().filter(entry -> entry.getValue().getType().equals(RequestMetadataCallback.class)) - .findFirst() - .ifPresent(entry -> { - @SuppressWarnings("unchecked") MutableArgumentValue argValue = (MutableArgumentValue) entry.getValue(); - RequestMetadataCallback callback = argValue.getValue(); - argValue.setValue(new LoggingRequestMetadataCallback(callback)); - }); - return context.proceed(); - } - - /** - * A wrapper {@link RequestMetadataCallback} implementation that logs failures with a warning before proceeding with - * the original callback. - */ - private static final class LoggingRequestMetadataCallback implements RequestMetadataCallback { - - private final RequestMetadataCallback callback; - - private LoggingRequestMetadataCallback(RequestMetadataCallback callback) { - this.callback = callback; - } - - @Override - public void onSuccess(Map> metadata) { - this.callback.onSuccess(metadata); - } - - @Override - public void onFailure(Throwable ex) { - if (ex instanceof IOException) { - LOG.warn("A failure occurred while attempting to build credential metadata for a GCP API request. The GCP libraries treat this as " + - "a retryable error, but misconfigured credentials can keep it from ever succeeding.", ex); - } - this.callback.onFailure(ex); - } - } -} diff --git a/gcp-common/src/main/java/io/micronaut/gcp/credentials/GoogleCredentialsFactory.java b/gcp-common/src/main/java/io/micronaut/gcp/credentials/GoogleCredentialsFactory.java index 5eaad5bab..207b67e40 100644 --- a/gcp-common/src/main/java/io/micronaut/gcp/credentials/GoogleCredentialsFactory.java +++ b/gcp-common/src/main/java/io/micronaut/gcp/credentials/GoogleCredentialsFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2017-2020 original authors + * Copyright 2017-2024 original authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -87,7 +87,6 @@ public GoogleCredentialsFactory(@NonNull GoogleCredentialsConfiguration configur @Requires(property = GoogleCredentialsConfiguration.PREFIX + ".enabled", value = StringUtils.TRUE, defaultValue = StringUtils.TRUE) @Primary @Singleton - @LogAuthenticationFailures protected GoogleCredentials defaultGoogleCredentials() throws IOException { final List scopes = configuration.getScopes().stream() .map(URI::toString).collect(Collectors.toList()); diff --git a/gcp-common/src/main/java/io/micronaut/gcp/credentials/LogAuthenticationFailures.java b/gcp-common/src/main/java/io/micronaut/gcp/credentials/LogAuthenticationFailures.java deleted file mode 100644 index 266939f97..000000000 --- a/gcp-common/src/main/java/io/micronaut/gcp/credentials/LogAuthenticationFailures.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright 2017-2023 original authors - * - * 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 - * - * https://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 io.micronaut.gcp.credentials; - -import com.google.auth.oauth2.GoogleCredentials; -import io.micronaut.aop.Around; -import io.micronaut.context.annotation.Type; - -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -import static java.lang.annotation.RetentionPolicy.RUNTIME; - -/** - * Annotation for applying authentication failure logging AOP advice to a managed instance of - * {@link GoogleCredentials}. - * - * @author Jeremy Grelle - * @since 5.2.0 - */ -@Documented -@Retention(RUNTIME) -@Target({ElementType.METHOD, ElementType.TYPE}) -@Around(proxyTargetMode = Around.ProxyTargetConstructorMode.ALLOW) -@Type(AuthenticationLoggingInterceptor.class) -public @interface LogAuthenticationFailures { -} diff --git a/gcp-common/src/test/groovy/io/micronaut/gcp/credentials/GoogleCredentialsFactorySpec.groovy b/gcp-common/src/test/groovy/io/micronaut/gcp/credentials/GoogleCredentialsFactorySpec.groovy index b6a49e88f..bb1ad0745 100644 --- a/gcp-common/src/test/groovy/io/micronaut/gcp/credentials/GoogleCredentialsFactorySpec.groovy +++ b/gcp-common/src/test/groovy/io/micronaut/gcp/credentials/GoogleCredentialsFactorySpec.groovy @@ -1,11 +1,10 @@ package io.micronaut.gcp.credentials -import com.google.auth.RequestMetadataCallback +import com.google.api.client.util.GenericData import com.google.auth.oauth2.GoogleCredentials import com.google.auth.oauth2.ImpersonatedCredentials import com.google.auth.oauth2.ServiceAccountCredentials import com.google.auth.oauth2.UserCredentials -import com.google.common.util.concurrent.MoreExecutors import io.micronaut.context.ApplicationContext import io.micronaut.context.annotation.Requires import io.micronaut.context.exceptions.BeanInstantiationException @@ -13,6 +12,7 @@ import io.micronaut.context.exceptions.ConfigurationException import io.micronaut.context.exceptions.NoSuchBeanException import io.micronaut.core.reflect.ReflectionUtils import io.micronaut.http.HttpResponse +import io.micronaut.http.HttpStatus import io.micronaut.http.MediaType import io.micronaut.http.annotation.Controller import io.micronaut.http.annotation.Post @@ -21,12 +21,12 @@ import org.spockframework.runtime.IStandardStreamsListener import org.spockframework.runtime.StandardStreamsCapturer import spock.lang.AutoCleanup import spock.lang.Specification -import spock.util.concurrent.PollingConditions import uk.org.webcompere.systemstubs.environment.EnvironmentVariables import uk.org.webcompere.systemstubs.properties.SystemProperties import uk.org.webcompere.systemstubs.resource.Resources import java.security.PrivateKey +import java.util.concurrent.atomic.AtomicInteger import static io.micronaut.gcp.credentials.fixture.ServiceAccountCredentialsTestHelper.* @@ -37,8 +37,6 @@ class GoogleCredentialsFactorySpec extends Specification { @AutoCleanup("stop") StandardStreamsCapturer capturer = new StandardStreamsCapturer() - PollingConditions conditions = new PollingConditions(timeout: 30) - void setup() { capturer.addStandardStreamsListener(captured) capturer.start() @@ -67,6 +65,9 @@ class GoogleCredentialsFactorySpec extends Specification { then: thrown(NoSuchBeanException) + + cleanup: + ctx.close() } void "configuring both credentials location and encoded-key throws an exception"() { @@ -82,6 +83,9 @@ class GoogleCredentialsFactorySpec extends Specification { then: def ex = thrown(BeanInstantiationException) ex.getCause() instanceof ConfigurationException + + cleanup: + ctx.close() } void "default configuration without GCP SDK installed fails"() { @@ -191,7 +195,7 @@ class GoogleCredentialsFactorySpec extends Specification { then: gc != null - ImpersonatedCredentials ic = gc.$target + ImpersonatedCredentials ic = (ImpersonatedCredentials) gc UserCredentials uc = (UserCredentials) ic.getSourceCredentials() ic.getAccount() == "sa-test1@micronaut-gcp-testing.iam.gserviceaccount.com" with(uc) { @@ -232,6 +236,9 @@ class GoogleCredentialsFactorySpec extends Specification { then: matchesJsonServiceAccountCredentials(pk, gc) + + cleanup: + ctx.close() } void "service account credentials can be loaded via configured Base64-encoded key"() { @@ -247,51 +254,43 @@ class GoogleCredentialsFactorySpec extends Specification { then: matchesJsonServiceAccountCredentials(pk, gc) + + cleanup: + ctx.close() } - void "invalid credentials cause a warning to be logged when metadata is requested"(){ + void "an access token should be able to be refreshed and retrieved"() { given: PrivateKey pk = generatePrivateKey() - String encodedServiceAccountCredentials = encodeServiceCredentials(pk) + File serviceAccountCredentials = writeServiceCredentialsToTempFile(pk) + + when: EmbeddedServer gcp = ApplicationContext.run(EmbeddedServer, [ "spec.name" : "GoogleCredentialsFactorySpec", "micronaut.server.port" : 8080 ]) def ctx = ApplicationContext.run([ - (GoogleCredentialsConfiguration.PREFIX + ".encoded-key"): encodedServiceAccountCredentials + (GoogleCredentialsConfiguration.PREFIX + ".location"): serviceAccountCredentials.getPath() ]) GoogleCredentials gc = ctx.getBean(GoogleCredentials) - when: - gc.getRequestMetadata(null, MoreExecutors.directExecutor(), new RequestMetadataCallback() { - @Override - void onSuccess(Map> metadata) { - - } - - @Override - void onFailure(Throwable exception) { + then: + matchesJsonServiceAccountCredentials(pk, gc) - } - }) + when: + gc.refreshIfExpired() then: - conditions.eventually { - captured.messages.any { - it.contains("WARN") - it.contains("A failure occurred while attempting to build credential metadata for a GCP API request. The GCP libraries treat this as " + - "a retryable error, but misconfigured credentials can keep it from ever succeeding.") - } - } + gc.getAccessToken().getTokenValue() == "ThisIsAFreshToken" cleanup: - ctx.stop() - gcp.stop() + gcp.close() + ctx.close() } private void matchesJsonUserCredentials(GoogleCredentials gc) { - assert gc != null && gc.$target != null && gc.$target instanceof UserCredentials - UserCredentials uc = (UserCredentials) gc.$target + assert gc != null && gc instanceof UserCredentials + UserCredentials uc = (UserCredentials) gc assert uc.getClientId() == "client-id-1.apps.googleusercontent.com" assert uc.getClientSecret() == "client-secret-1" assert uc.getQuotaProjectId() == "micronaut-gcp-test" @@ -299,8 +298,8 @@ class GoogleCredentialsFactorySpec extends Specification { } private void matchesJsonServiceAccountCredentials(PrivateKey pk, GoogleCredentials gc) { - assert gc != null && gc.$target != null && gc.$target instanceof ServiceAccountCredentials - ServiceAccountCredentials sc = (ServiceAccountCredentials) gc.$target + assert gc != null && gc instanceof ServiceAccountCredentials + ServiceAccountCredentials sc = (ServiceAccountCredentials) gc assert sc.getAccount() == "sa-test1@micronaut-gcp-testing.iam.gserviceaccount.com" assert sc.getClientId() == "client-id-1" assert sc.getProjectId() == "micronaut-gcp-testing" @@ -313,9 +312,14 @@ class GoogleCredentialsFactorySpec extends Specification { @Controller class GoogleAuth { - @Post(value="/token", processes = MediaType.APPLICATION_FORM_URLENCODED) - HttpResponse getToken() { - return HttpResponse.unauthorized() + AtomicInteger requestCount = new AtomicInteger(1) + + @Post(value="/token", consumes = MediaType.APPLICATION_FORM_URLENCODED, produces = MediaType.APPLICATION_JSON) + HttpResponse getToken() { + if (requestCount.getAndAdd(1) == 2) { + return HttpResponse.ok(new GenericData().set("access_token", "ThisIsAFreshToken").set("expires_in", 3600)) + } + return HttpResponse.status(HttpStatus.TOO_MANY_REQUESTS) } } diff --git a/gcp-pubsub/src/test/groovy/io/micronaut/gcp/pubsub/bind/SubscriberAuthenticationFailureSpec.groovy b/gcp-pubsub/src/test/groovy/io/micronaut/gcp/pubsub/bind/SubscriberAuthenticationFailureSpec.groovy deleted file mode 100644 index 7652a284a..000000000 --- a/gcp-pubsub/src/test/groovy/io/micronaut/gcp/pubsub/bind/SubscriberAuthenticationFailureSpec.groovy +++ /dev/null @@ -1,105 +0,0 @@ -package io.micronaut.gcp.pubsub.bind - -import com.google.auth.oauth2.GoogleCredentials -import io.micronaut.context.ApplicationContext -import io.micronaut.context.annotation.Requires -import io.micronaut.core.reflect.ReflectionUtils -import io.micronaut.gcp.credentials.GoogleCredentialsConfiguration -import io.micronaut.gcp.pubsub.annotation.PubSubListener -import io.micronaut.gcp.pubsub.annotation.Subscription -import io.micronaut.http.HttpResponse -import io.micronaut.http.MediaType -import io.micronaut.http.annotation.Controller -import io.micronaut.http.annotation.Post -import io.micronaut.runtime.server.EmbeddedServer -import org.spockframework.runtime.IStandardStreamsListener -import org.spockframework.runtime.StandardStreamsCapturer -import spock.lang.AutoCleanup -import spock.lang.Specification -import spock.util.concurrent.PollingConditions - -import java.security.PrivateKey - -import static io.micronaut.gcp.credentials.fixture.ServiceAccountCredentialsTestHelper.encodeServiceCredentials -import static io.micronaut.gcp.credentials.fixture.ServiceAccountCredentialsTestHelper.generatePrivateKey - -class SubscriberAuthenticationFailureSpec extends Specification { - - ServiceAccountTestListener listener - - PollingConditions conditions = new PollingConditions(timeout: 30) - - SimpleStreamsListener captured = new SimpleStreamsListener() - - @AutoCleanup("stop") - StandardStreamsCapturer capturer = new StandardStreamsCapturer() - - void setup() { - capturer.addStandardStreamsListener(captured) - capturer.start() - } - - void "authentication failure on subscription should be logged as a warning"() { - given: - PrivateKey pk = generatePrivateKey() - String encodedServiceAccountCredentials = encodeServiceCredentials(pk) - EmbeddedServer gcp = ApplicationContext.run(EmbeddedServer, [ - "server.name" : "GoogleAuthServerTestFake", - "micronaut.server.port" : 8080 - ]) - def ctx = ApplicationContext.run([ - "spec.name" : "AuthenticationFailureSpec", - "gcp.projectId" : "micronaut-gcp-testing", - (GoogleCredentialsConfiguration.PREFIX + ".encoded-key"): encodedServiceAccountCredentials - ]) - - when: - def gc = ctx.getBean(GoogleCredentials) - listener = ctx.getBean(ServiceAccountTestListener) - - then: - gc != null - listener != null - conditions.eventually { - captured.messages.any { - it.contains("WARN") - it.contains("A failure occurred while attempting to build credential metadata for a GCP API request. The GCP libraries treat this as " + - "a retryable error, but misconfigured credentials can keep it from ever succeeding.") - } - } - - cleanup: - ReflectionUtils.getFieldValue(GoogleCredentials.class, "defaultCredentialsProvider", gc) - .ifPresent { - ReflectionUtils.setField(it.getClass(), "cachedCredentials", it, null) - } - ctx.close() - gcp.close() - } -} - -@PubSubListener -@Requires(property = "spec.name", value = "AuthenticationFailureSpec") -class ServiceAccountTestListener { - - @Subscription("micronaut-gcp-topic1-sub") - void receive(byte[] data) { - - } -} - -@Requires(property = "server.name", value = "GoogleAuthServerTestFake") -@Controller -class GoogleAuth { - - @Post(value="/token", processes = MediaType.APPLICATION_FORM_URLENCODED) - HttpResponse getToken() { - return HttpResponse.unauthorized() - } -} - -class SimpleStreamsListener implements IStandardStreamsListener { - List messages = [] - @Override void standardOut(String m) { messages << m } - @Override void standardErr(String m) { messages << m } -}