Skip to content

Commit

Permalink
fix: Handling broken token issues (#1301)
Browse files Browse the repository at this point in the history
* fix: Handling bad token issues

* add tests
  • Loading branch information
piotradamczyk5 authored Nov 9, 2020
1 parent e97a025 commit ab55fbc
Show file tree
Hide file tree
Showing 12 changed files with 173 additions and 74 deletions.
7 changes: 3 additions & 4 deletions test_runner/src/main/kotlin/ftl/args/ArgsHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,14 @@ import com.google.cloud.storage.BucketInfo
import com.google.cloud.storage.Storage
import com.google.cloud.storage.StorageClass
import com.google.cloud.storage.StorageOptions
import ftl.args.ArgsHelper.convertToWindowsPath
import ftl.args.IArgs.Companion.AVAILABLE_PHYSICAL_SHARD_COUNT_RANGE
import ftl.args.yml.YamlObjectMapper
import ftl.config.FtlConstants
import ftl.config.FtlConstants.GCS_PREFIX
import ftl.config.FtlConstants.JSON_FACTORY
import ftl.config.FtlConstants.defaultCredentialPath
import ftl.config.defaultCredentialPath
import ftl.config.FtlConstants.isWindows
import ftl.config.FtlConstants.useMock
import ftl.config.credential
import ftl.gc.GcStorage
import ftl.gc.GcToolResults
import ftl.reports.xml.model.JUnitTestResult
Expand Down Expand Up @@ -146,7 +145,7 @@ object ArgsHelper {
if (bucket.startsWith("test-lab-")) return bucket

val storage = StorageOptions.newBuilder()
.setCredentials(FtlConstants.credential)
.setCredentials(credential)
.setProjectId(projectId)
.build().service
val bucketLabel = mapOf("flank" to "")
Expand Down
3 changes: 2 additions & 1 deletion test_runner/src/main/kotlin/ftl/args/ValidateCommonArgs.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package ftl.args

import ftl.config.Device
import ftl.config.FtlConstants
import ftl.config.defaultCredentialPath
import ftl.gc.GcStorage
import ftl.reports.FullJUnitReport
import ftl.reports.JUnitReport
Expand Down Expand Up @@ -30,7 +31,7 @@ private fun List<Device>.throwIfAnyMisspelt() =
private fun CommonArgs.assertProjectId() {
if (project.isBlank()) throw FlankConfigurationError(
"The project is not set. Define GOOGLE_CLOUD_PROJECT, set project in flank.yml\n" +
"or save service account credential to ${FtlConstants.defaultCredentialPath}\n" +
"or save service account credential to $defaultCredentialPath\n" +
" See https://github.com/GoogleCloudPlatform/google-cloud-java#specifying-a-project-id"
)
}
Expand Down
52 changes: 52 additions & 0 deletions test_runner/src/main/kotlin/ftl/config/Credentials.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package ftl.config

import com.google.api.client.http.GoogleApiLogger
import com.google.api.client.http.HttpRequestInitializer
import com.google.auth.oauth2.AccessToken
import com.google.auth.oauth2.GoogleCredentials
import com.google.auth.oauth2.ServiceAccountCredentials
import ftl.config.FtlConstants.userHome
import ftl.gc.UserAuth
import ftl.http.HttpTimeoutIncrease
import ftl.run.exception.FlankGeneralError
import java.io.IOException
import java.nio.file.Path
import java.nio.file.Paths
import java.util.Date

val defaultCredentialPath: Path by lazy {
Paths.get(userHome, ".config/gcloud/application_default_credentials.json")
}

val credential: GoogleCredentials by lazy {
when {
FtlConstants.useMock -> GoogleCredentials.create(AccessToken("mock", Date()))
UserAuth.exists() -> UserAuth.load()
else -> runCatching {
GoogleApiLogger.silenceComputeEngine()
ServiceAccountCredentials.getApplicationDefault()
}.getOrElse {
if (FtlConstants.isWindows) loadGoogleAccountCredentials()
else throw FlankGeneralError("Error: Failed to read service account credential.\n${it.message}")
}
}
}

private fun loadGoogleAccountCredentials(): GoogleCredentials = try {
GoogleCredentials.fromStream(defaultCredentialPath.toFile().inputStream())
} catch (e: IOException) {
throw FlankGeneralError("Error: Failed to read service account credential.\n${e.message}")
}

val httpCredential: HttpRequestInitializer by lazy {
if (FtlConstants.useMock) {
HttpRequestInitializer {}
} else {
// Authenticate with https://github.com/googleapis/google-auth-library-java
// Scope is required.
// https://developers.google.com/identity/protocols/googlescopes
// https://developers.google.com/identity/protocols/application-default-credentials
// https://cloud.google.com/sdk/gcloud/reference/alpha/compute/instances/set-scopes
HttpTimeoutIncrease(credential.createScoped(listOf("https://www.googleapis.com/auth/cloud-platform")))
}
}
53 changes: 4 additions & 49 deletions test_runner/src/main/kotlin/ftl/config/FtlConstants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,25 @@ package ftl.config
import com.bugsnag.Bugsnag
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport
import com.google.api.client.googleapis.util.Utils
import com.google.api.client.http.GoogleApiLogger
import com.google.api.client.http.HttpRequestInitializer
import com.google.api.client.http.javanet.NetHttpTransport
import com.google.api.client.json.JsonFactory
import com.google.auth.oauth2.AccessToken
import com.google.auth.oauth2.GoogleCredentials
import com.google.auth.oauth2.ServiceAccountCredentials
import ftl.args.AndroidArgs
import ftl.args.IArgs
import ftl.args.IosArgs
import ftl.gc.UserAuth
import ftl.http.HttpTimeoutIncrease
import ftl.util.BugsnagInitHelper.initBugsnag
import ftl.run.exception.FlankConfigurationError
import ftl.run.exception.FlankGeneralError
import ftl.util.readRevision
import java.io.IOException
import java.nio.file.Path
import java.nio.file.Paths
import java.util.Date

object FtlConstants {
var useMock = false

private val osName = System.getProperty("os.name")?.toLowerCase() ?: ""

val userHome: String by lazy {
if (isWindows) System.getenv("HOMEPATH") else System.getProperty("user.home")
}

val isMacOS: Boolean by lazy {
val isMacOS = osName.indexOf("mac") >= 0
println("isMacOS = $isMacOS ($osName)")
Expand Down Expand Up @@ -68,44 +61,6 @@ object FtlConstants {
}
}

val defaultCredentialPath: Path by lazy {
val homePath = if (isWindows) System.getenv("HOMEPATH") else System.getProperty("user.home")
Paths.get(homePath, ".config/gcloud/application_default_credentials.json")
}

val credential: GoogleCredentials by lazy {
when {
useMock -> GoogleCredentials.create(AccessToken("mock", Date()))
UserAuth.exists() -> UserAuth.load()
else -> runCatching {
GoogleApiLogger.silenceComputeEngine()
ServiceAccountCredentials.getApplicationDefault()
}.getOrElse {
if (isWindows) loadGoogleAccountCredentials()
else throw FlankGeneralError("Error: Failed to read service account credential.\n${it.message}")
}
}
}

private fun loadGoogleAccountCredentials(): GoogleCredentials = try {
GoogleCredentials.fromStream(defaultCredentialPath.toFile().inputStream())
} catch (e: IOException) {
throw FlankGeneralError("Error: Failed to read service account credential.\n${e.message}")
}

val httpCredential: HttpRequestInitializer by lazy {
if (useMock) {
HttpRequestInitializer {}
} else {
// Authenticate with https://github.com/googleapis/google-auth-library-java
// Scope is required.
// https://developers.google.com/identity/protocols/googlescopes
// https://developers.google.com/identity/protocols/application-default-credentials
// https://cloud.google.com/sdk/gcloud/reference/alpha/compute/instances/set-scopes
HttpTimeoutIncrease(credential.createScoped(listOf("https://www.googleapis.com/auth/cloud-platform")))
}
}

fun configFileName(args: IArgs): String {
return when (args) {
is IosArgs -> defaultIosConfig
Expand Down
4 changes: 2 additions & 2 deletions test_runner/src/main/kotlin/ftl/gc/GcStorage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import ftl.args.IArgs
import ftl.args.IosArgs
import ftl.config.FtlConstants
import ftl.config.FtlConstants.GCS_PREFIX
import ftl.gc.GcStorage.dropLeadingSlash
import ftl.config.credential
import ftl.reports.xml.model.JUnitTestResult
import ftl.reports.xml.parseAllSuitesXml
import ftl.reports.xml.xmlToString
Expand All @@ -34,7 +34,7 @@ object GcStorage {
val storageOptions: StorageOptions by lazy {
val builder = StorageOptions.newBuilder()
if (FtlConstants.useMock) builder.setHost(FtlConstants.localhost)
builder.setCredentials(FtlConstants.credential)
builder.setCredentials(credential)

// The oauth lib for user auth needs to be replaced
// https://github.com/Flank/flank/issues/464#issuecomment-455227703
Expand Down
2 changes: 1 addition & 1 deletion test_runner/src/main/kotlin/ftl/gc/GcTesting.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import com.google.api.services.testing.Testing
import ftl.config.FtlConstants
import ftl.config.FtlConstants.JSON_FACTORY
import ftl.config.FtlConstants.applicationName
import ftl.config.FtlConstants.httpCredential
import ftl.config.httpCredential
import ftl.config.FtlConstants.httpTransport
import ftl.http.executeWithRetry

Expand Down
4 changes: 3 additions & 1 deletion test_runner/src/main/kotlin/ftl/gc/GcToolResults.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,11 @@ import ftl.args.IArgs
import ftl.config.FtlConstants
import ftl.config.FtlConstants.JSON_FACTORY
import ftl.config.FtlConstants.applicationName
import ftl.config.FtlConstants.httpCredential
import ftl.config.httpCredential
import ftl.config.FtlConstants.httpTransport
import ftl.http.executeWithRetry
import ftl.run.exception.FTLProjectError
import ftl.run.exception.FailureToken
import ftl.run.exception.FlankGeneralError
import ftl.run.exception.PermissionDenied
import ftl.run.exception.ProjectNotFound
Expand Down Expand Up @@ -129,6 +130,7 @@ object GcToolResults {
when (ftlProjectError) {
is PermissionDenied -> throw FlankGeneralError(permissionDeniedErrorMessage(projectId, ftlProjectError.message))
is ProjectNotFound -> throw FlankGeneralError(projectNotFoundErrorMessage(projectId, ftlProjectError.message))
is FailureToken -> UserAuth.throwAuthenticationError()
}
}

Expand Down
37 changes: 25 additions & 12 deletions test_runner/src/main/kotlin/ftl/gc/UserAuth.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import com.google.auth.oauth2.MemoryTokensStorage
import com.google.auth.oauth2.UserAuthorizer
import com.google.auth.oauth2.UserCredentials
import ftl.config.FtlConstants
import ftl.config.FtlConstants.userHome
import ftl.run.exception.FlankGeneralError
import io.ktor.application.call
import io.ktor.response.respondText
import io.ktor.routing.get
Expand All @@ -13,26 +15,39 @@ import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import java.io.File
import java.io.FileInputStream
import java.io.FileOutputStream
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.net.URI
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.Paths

class UserAuth {

companion object {
private val home = System.getProperty("user.home")!!
private val dotFlank = Paths.get(home, ".flank/")
val userToken: Path = Paths.get(dotFlank.toString(), "UserToken")

fun exists() = userToken.toFile().exists()
fun load(): UserCredentials {
return ObjectInputStream(FileInputStream(userToken.toFile())).use {
it.readObject() as UserCredentials
}
private val dotFlank = Paths.get(userHome, ".flank")
val userToken: Path = if (FtlConstants.useMock) File.createTempFile("test_", ".Token").toPath()
else Paths.get(dotFlank.toString(), "UserToken")

fun exists() = Files.exists(userToken)
fun load(): UserCredentials = readCredentialsOrThrow()

private fun readCredentialsOrThrow(): UserCredentials = runCatching {
ObjectInputStream(FileInputStream(userToken.toFile())).readObject() as UserCredentials
}.getOrElse {
throwAuthenticationError()
}

fun throwAuthenticationError(): Nothing {
Files.delete(userToken)
throw FlankGeneralError(
"Could not load user authentication, please\n" +
" - login again using command: flank auth login\n" +
" - or try again to use The Application Default Credentials variable to login"
)
}
}

Expand Down Expand Up @@ -96,9 +111,7 @@ class UserAuth {
val userCredential = authorizer.getCredentials(userId)

dotFlank.toFile().mkdirs()
ObjectOutputStream(FileOutputStream(userToken.toFile())).use {
it.writeObject(userCredential)
}
ObjectOutputStream(FileOutputStream(userToken.toFile())).writeObject(userCredential)

println()
println("User token saved to $userToken")
Expand Down
6 changes: 6 additions & 0 deletions test_runner/src/main/kotlin/ftl/http/ExecuteWithRetry.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package ftl.http
import com.google.api.client.googleapis.services.json.AbstractGoogleJsonClientRequest
import com.google.api.client.http.HttpResponseException
import ftl.config.FtlConstants
import ftl.run.exception.FailureToken
import ftl.run.exception.PermissionDenied
import ftl.run.exception.ProjectNotFound
import kotlinx.coroutines.delay
Expand All @@ -29,6 +30,7 @@ private inline fun <T> withRetry(crossinline block: () -> T): T = runBlocking {
if (err is HttpResponseException) {
// we want to handle some FTL errors with special care
when (err.statusCode) {
400 -> if (err.containsBadTokenMessage()) throw FailureToken(err) else return@repeat
429 -> return@repeat
403 -> throw PermissionDenied(err)
404 -> throw ProjectNotFound(err)
Expand All @@ -41,3 +43,7 @@ private inline fun <T> withRetry(crossinline block: () -> T): T = runBlocking {
}

private class FlankGoogleApiError(exception: Throwable) : Error(exception)

private fun HttpResponseException.containsBadTokenMessage() = message?.let {
it.contains("\"error\": \"invalid_grant\"") && it.contains("https://oauth2.googleapis.com/token")
} ?: false
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ class FlankGeneralError : FlankException {
sealed class FTLProjectError(exc: IOException) : FlankException("Caused by: $exc")
class PermissionDenied(exc: IOException) : FTLProjectError(exc)
class ProjectNotFound(exc: IOException) : FTLProjectError(exc)
class FailureToken(exc: IOException) : FTLProjectError(exc)

/**
* The test environment for this test execution is not supported because of incompatible test dimensions. This error might occur if the selected Android API level is not supported by the selected device type.
Expand Down
Loading

0 comments on commit ab55fbc

Please sign in to comment.