Skip to content

Commit

Permalink
Retry reading default GCP credentials in the expired case
Browse files Browse the repository at this point in the history
Currently, Google Auth library caches default credentials indefinitely
in a static field. This means if user's credentials expire, they run
gcloud auth command, then try runnning Gradle again the old credentials
are used. This change adds a reflection based mechanism to clear default
credentials cache and retrying one additional time to get new
credentials.

Additionally, allow specifying the message on authentication failure.
  • Loading branch information
liutikas committed Jul 26, 2023
1 parent 8f20c58 commit 7d69a41
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 24 deletions.
2 changes: 1 addition & 1 deletion gcpbuildcache/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ gradlePlugin {
}

group = "androidx.build.gradle.gcpbuildcache"
version = "1.0.0-beta03"
version = "1.0.0-beta04"

testing {
suites {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ class GcpBuildCacheServiceFactory : BuildCacheServiceFactory<GcpBuildCache> {
buildCache.projectId,
buildCache.bucketName,
buildCache.credentials,
buildCache.messageOnAuthenticationFailure,
buildCache.isPush,
buildCache.isEnabled
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
Expand All @@ -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<String>,
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(
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down

0 comments on commit 7d69a41

Please sign in to comment.