Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Retry reading default GCP credentials in the expired case #34

Merged
merged 1 commit into from
Jul 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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