diff --git a/gcpbuildcache/build.gradle.kts b/gcpbuildcache/build.gradle.kts index 06e2796..34e0e35 100644 --- a/gcpbuildcache/build.gradle.kts +++ b/gcpbuildcache/build.gradle.kts @@ -50,7 +50,7 @@ gradlePlugin { } group = "androidx.build.gradle.gcpbuildcache" -version = "1.0.0-beta03" +version = "1.0.0-beta04" testing { suites { diff --git a/gcpbuildcache/src/main/kotlin/androidx/build/gradle/gcpbuildcache/GcpBuildCache.kt b/gcpbuildcache/src/main/kotlin/androidx/build/gradle/gcpbuildcache/GcpBuildCache.kt index 6a2b003..0085408 100644 --- a/gcpbuildcache/src/main/kotlin/androidx/build/gradle/gcpbuildcache/GcpBuildCache.kt +++ b/gcpbuildcache/src/main/kotlin/androidx/build/gradle/gcpbuildcache/GcpBuildCache.kt @@ -34,4 +34,8 @@ abstract class GcpBuildCache : RemoteGradleBuildCache() { * The type of credentials to use to connect to the Google Cloud Platform project instance. */ override var credentials: GcpCredentials = ApplicationDefaultGcpCredentials + + var messageOnAuthenticationFailure: String = """Your GCP Credentials have expired. + Please regenerate credentials following the steps below and try again: + gcloud auth application-default login""".trimIndent() } diff --git a/gcpbuildcache/src/main/kotlin/androidx/build/gradle/gcpbuildcache/GcpBuildCacheService.kt b/gcpbuildcache/src/main/kotlin/androidx/build/gradle/gcpbuildcache/GcpBuildCacheService.kt index 3875630..879f0e5 100644 --- a/gcpbuildcache/src/main/kotlin/androidx/build/gradle/gcpbuildcache/GcpBuildCacheService.kt +++ b/gcpbuildcache/src/main/kotlin/androidx/build/gradle/gcpbuildcache/GcpBuildCacheService.kt @@ -38,6 +38,7 @@ internal class GcpBuildCacheService( private val projectId: String, private val bucketName: String, gcpCredentials: GcpCredentials, + messageOnAuthenticationFailure: String, isPush: Boolean, isEnabled: Boolean, inTestMode: Boolean = false @@ -47,7 +48,7 @@ internal class GcpBuildCacheService( // Use an implementation backed by the File System when in test mode. FileSystemStorageService(bucketName, isPush, isEnabled) } else { - GcpStorageService(projectId, bucketName, gcpCredentials, isPush, isEnabled) + GcpStorageService(projectId, bucketName, gcpCredentials, messageOnAuthenticationFailure, isPush, isEnabled) } override fun close() { diff --git a/gcpbuildcache/src/main/kotlin/androidx/build/gradle/gcpbuildcache/GcpBuildCacheServiceFactory.kt b/gcpbuildcache/src/main/kotlin/androidx/build/gradle/gcpbuildcache/GcpBuildCacheServiceFactory.kt index 30e2c44..846367e 100644 --- a/gcpbuildcache/src/main/kotlin/androidx/build/gradle/gcpbuildcache/GcpBuildCacheServiceFactory.kt +++ b/gcpbuildcache/src/main/kotlin/androidx/build/gradle/gcpbuildcache/GcpBuildCacheServiceFactory.kt @@ -43,6 +43,7 @@ class GcpBuildCacheServiceFactory : BuildCacheServiceFactory { buildCache.projectId, buildCache.bucketName, buildCache.credentials, + buildCache.messageOnAuthenticationFailure, buildCache.isPush, buildCache.isEnabled ) diff --git a/gcpbuildcache/src/main/kotlin/androidx/build/gradle/gcpbuildcache/GcpStorageService.kt b/gcpbuildcache/src/main/kotlin/androidx/build/gradle/gcpbuildcache/GcpStorageService.kt index 58a8a90..cb701d3 100644 --- a/gcpbuildcache/src/main/kotlin/androidx/build/gradle/gcpbuildcache/GcpStorageService.kt +++ b/gcpbuildcache/src/main/kotlin/androidx/build/gradle/gcpbuildcache/GcpStorageService.kt @@ -36,12 +36,15 @@ internal class GcpStorageService( private val projectId: String, override val bucketName: String, gcpCredentials: GcpCredentials, + messageOnAuthenticationFailure: String, override val isPush: Boolean, override val isEnabled: Boolean, private val sizeThreshold: Long = BLOB_SIZE_THRESHOLD ) : StorageService { - private val storageOptions by lazy { storageOptions(projectId, gcpCredentials, isPush) } + private val storageOptions by lazy { + storageOptions(projectId, gcpCredentials, messageOnAuthenticationFailure, isPush) + } override fun load(cacheKey: String): InputStream? { if (!isEnabled) { @@ -153,9 +156,14 @@ internal class GcpStorageService( private fun storageOptions( projectId: String, gcpCredentials: GcpCredentials, + messageOnAuthenticationFailure: String, isPushSupported: Boolean ): StorageOptions? { - val credentials = credentials(gcpCredentials, isPushSupported) ?: return null + val credentials = credentials( + gcpCredentials, + messageOnAuthenticationFailure, + isPushSupported + ) ?: return null val retrySettings = RetrySettings.newBuilder() retrySettings.maxAttempts = 3 return StorageOptions.newBuilder().setCredentials(credentials) @@ -165,8 +173,66 @@ internal class GcpStorageService( .build() } + /** + * Attempts to use reflection to clear the cached credentials inside the Google authentication library. + */ + private fun clearCachedDefaultCredentials() { + try { + val field = GoogleCredentials::class.java.getDeclaredField("defaultCredentialsProvider") + field.isAccessible = true + val defaultCredentialsProvider = field.get(null) + val cachedCredentials = field.type.getDeclaredField("cachedCredentials") + cachedCredentials.isAccessible = true + cachedCredentials.set(defaultCredentialsProvider, null) + } catch (exception: Exception) { + // unable to clear the credentials, oh well. + } + } + + private fun defaultApplicationGcpCredentials( + scopes: List, + messageOnAuthenticationFailure: String, + forceClearCache: Boolean + ): GoogleCredentials { + if (forceClearCache) clearCachedDefaultCredentials() + val credentials = GoogleCredentials.getApplicationDefault().createScoped(scopes) + + try { + // If the credentials have expired, + // reauth is required by the user to be able to generate or refresh access token; + // Refreshing the access token here helps us to provide a useful error message to the user + // in case the credentials have expired + credentials.refreshIfExpired() + } catch (e: Exception) { + if (forceClearCache) { + throw Exception(messageOnAuthenticationFailure) + } else { + return defaultApplicationGcpCredentials( + scopes, + messageOnAuthenticationFailure, + forceClearCache = true + ) + } + } + val tokenService = TokenInfoService.tokenService() + val tokenInfoResponse = tokenService.tokenInfo(credentials.accessToken.tokenValue).execute() + if (!tokenInfoResponse.isSuccessful) { + if (forceClearCache) { + throw Exception(messageOnAuthenticationFailure) + } else { + return defaultApplicationGcpCredentials( + scopes, + messageOnAuthenticationFailure, + forceClearCache = true + ) + } + } + return credentials + } + private fun credentials( gcpCredentials: GcpCredentials, + messageOnAuthenticationFailure: String, isPushSupported: Boolean ): GoogleCredentials? { val scopes = mutableListOf( @@ -177,26 +243,7 @@ internal class GcpStorageService( } return when (gcpCredentials) { is ApplicationDefaultGcpCredentials -> { - val credentials = GoogleCredentials.getApplicationDefault().createScoped(scopes) - try { - // If the credentials have expired, - // reauth is required by the user to be able to generate or refresh access token; - // Refreshing the access token here helps us to provide a useful error message to the user - // in case the credentials have expired - credentials.refreshIfExpired() - } catch (e: Exception) { - throw Exception(""" - "Your GCP Credentials have expired. - Please regenerate credentials and try again. - """.trimIndent() - ) - } - val tokenService = TokenInfoService.tokenService() - val tokenInfoResponse = tokenService.tokenInfo(credentials.accessToken.tokenValue).execute() - if(!tokenInfoResponse.isSuccessful) { - throw GradleException(tokenInfoResponse.errorBody().toString()) - } - credentials + defaultApplicationGcpCredentials(scopes, messageOnAuthenticationFailure, forceClearCache = false) } is ExportedKeyGcpCredentials -> { val contents = gcpCredentials.credentials.invoke() diff --git a/gcpbuildcache/src/test/kotlin/androidx/build/gradle/gcpbuildcache/GcpStorageServiceTest.kt b/gcpbuildcache/src/test/kotlin/androidx/build/gradle/gcpbuildcache/GcpStorageServiceTest.kt index 18af221..0f14b9e 100644 --- a/gcpbuildcache/src/test/kotlin/androidx/build/gradle/gcpbuildcache/GcpStorageServiceTest.kt +++ b/gcpbuildcache/src/test/kotlin/androidx/build/gradle/gcpbuildcache/GcpStorageServiceTest.kt @@ -34,6 +34,7 @@ class GcpStorageServiceTest { projectId = PROJECT_ID, bucketName = BUCKET_NAME, gcpCredentials = ExportedKeyGcpCredentials(File(serviceAccountPath!!)), + messageOnAuthenticationFailure = "Please re-authenticate", isPush = true, isEnabled = true, sizeThreshold = 0L @@ -54,6 +55,7 @@ class GcpStorageServiceTest { projectId = PROJECT_ID, bucketName = BUCKET_NAME, gcpCredentials = ExportedKeyGcpCredentials(File(serviceAccountPath!!)), + messageOnAuthenticationFailure = "Please re-authenticate", isPush = true, isEnabled = true, sizeThreshold = 0L @@ -77,6 +79,7 @@ class GcpStorageServiceTest { projectId = PROJECT_ID, bucketName = BUCKET_NAME, gcpCredentials = ExportedKeyGcpCredentials(File(serviceAccountPath!!)), + messageOnAuthenticationFailure = "Please re-authenticate", isPush = false, isEnabled = true, sizeThreshold = 0L @@ -96,6 +99,7 @@ class GcpStorageServiceTest { projectId = PROJECT_ID, bucketName = BUCKET_NAME, gcpCredentials = ExportedKeyGcpCredentials(File(serviceAccountPath!!)), + messageOnAuthenticationFailure = "Please re-authenticate", isPush = true, isEnabled = true, sizeThreshold = 0L @@ -104,6 +108,7 @@ class GcpStorageServiceTest { projectId = PROJECT_ID, bucketName = BUCKET_NAME, gcpCredentials = ExportedKeyGcpCredentials(File(serviceAccountPath)), + messageOnAuthenticationFailure = "Please re-authenticate", isPush = false, isEnabled = true, sizeThreshold = 0L @@ -129,6 +134,7 @@ class GcpStorageServiceTest { projectId = PROJECT_ID, bucketName = BUCKET_NAME, gcpCredentials = ExportedKeyGcpCredentials(File(serviceAccountPath!!)), + messageOnAuthenticationFailure = "Please re-authenticate", isPush = true, isEnabled = false, sizeThreshold = 0L