Skip to content

Commit

Permalink
flank auth login (#449)
Browse files Browse the repository at this point in the history
  • Loading branch information
bootstraponline authored Jan 10, 2019
1 parent 55c2d53 commit f710c18
Show file tree
Hide file tree
Showing 14 changed files with 257 additions and 11 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,11 @@ EOF
[fladle]: https://github.com/runningcode/fladle
### Authenticate with a Google account
Run `flank auth login`. Flank will save the credential to `~/.flank`. Google account authentication allows each person
to have a unique non-shared credential. A service account is still recommended for CI.
### Authenticate with a service account
Follow the [test lab docs](https://firebase.google.com/docs/test-lab/android/continuous) to create a service account.
Expand Down
1 change: 1 addition & 0 deletions release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

- Fix create Gcs bucket [#444](https://github.com/TestArmada/flank/pull/444)
- Add `files-to-download` to Android and iOS. Specify a list of regular expressions to download files from the Google Cloud Storage bucket. [#441](https://github.com/TestArmada/flank/pull/441)
- Add `flank auth login` to authorize with a user account instead of a service account. [#446](https://github.com/TestArmada/flank/pull/436)

## v4.1.1

Expand Down
3 changes: 3 additions & 0 deletions test_runner/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ dependencies {
compile("com.fasterxml.jackson.dataformat:jackson-dataformat-xml:${Versions.JACKSON}")
compile("com.fasterxml.woodstox:woodstox-core:5.1.0")

// https://github.com/googleapis/google-oauth-java-client
compile("com.google.oauth-client:google-oauth-client-jetty:1.23.0")

// https://github.com/jhy/jsoup/releases
testImplementation("org.jsoup:jsoup:1.11.3")
testImplementation(Libs.JUNIT)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.google.api.client.http

import com.google.auth.oauth2.ComputeEngineCredentials
import java.util.logging.Level
import java.util.logging.Logger
import java.util.logging.SimpleFormatter
import java.util.logging.StreamHandler

// Used to enable cURL logging of the Java client API requests.
//
// Compare with gcloud sdk traffic by using the --log-http flag
//
// gcloud alpha firebase test ios models list --log-http
//
object GoogleApiLogger {
fun logAllToStdout() {
val logger = HttpTransport.LOGGER
logger.level = Level.ALL

val handler = StreamHandler(System.out, SimpleFormatter())
handler.level = Level.ALL
logger.addHandler(handler)
}

fun silenceComputeEngine() {
// Silence info log about "Failed to detect whether we are running on Google Compute Engine."
Logger.getLogger(ComputeEngineCredentials::class.java.name).level = Level.OFF
}
}
8 changes: 7 additions & 1 deletion test_runner/src/main/kotlin/ftl/Main.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package ftl

import ftl.cli.AuthCommand
import ftl.cli.FirebaseCommand
import ftl.cli.firebase.CancelCommand
import ftl.cli.firebase.RefreshCommand
Expand All @@ -16,7 +17,8 @@ import picocli.CommandLine
IosCommand::class,
AndroidCommand::class,
RefreshCommand::class,
CancelCommand::class
CancelCommand::class,
AuthCommand::class
]
)
class Main : Runnable {
Expand All @@ -36,6 +38,10 @@ class Main : Runnable {
private var printVersion = false

companion object {
init {
// GoogleApiLogger.logAllToStdout()
}

@JvmStatic
fun main(args: Array<String>) {
// val args = arrayOf("firebase", "test", "android", "run") // for debugging. run test from IntelliJ IDEA
Expand Down
2 changes: 1 addition & 1 deletion test_runner/src/main/kotlin/ftl/args/yml/GcloudYml.kt
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class GcloudYmlParams(

init {
assertNotEmpty(
project, "project is not set. Define GOOGLE_CLOUD_PROJECT, set project in flank.yml\n" +
project, "The project is not set. Define GOOGLE_CLOUD_PROJECT, set project in flank.yml\n" +
"or save service account credential to $defaultCredentialPath\n" +
" See https://github.com/GoogleCloudPlatform/google-cloud-java#specifying-a-project-id"
)
Expand Down
19 changes: 19 additions & 0 deletions test_runner/src/main/kotlin/ftl/cli/AuthCommand.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package ftl.cli

import ftl.cli.auth.LoginCommand
import picocli.CommandLine
import picocli.CommandLine.Command

@Command(
name = "auth",
synopsisHeading = "%n",
header = ["Manage oauth2 credentials for Google Cloud"],
subcommands = [
LoginCommand::class
]
)
class AuthCommand : Runnable {
override fun run() {
CommandLine.usage(AuthCommand(), System.out)
}
}
29 changes: 29 additions & 0 deletions test_runner/src/main/kotlin/ftl/cli/auth/LoginCommand.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package ftl.cli.auth

import ftl.gc.GcAuth
import picocli.CommandLine

@CommandLine.Command(
name = "login",
sortOptions = false,
headerHeading = "",
synopsisHeading = "%n",
descriptionHeading = "%n@|bold,underline Description:|@%n%n",
parameterListHeading = "%n@|bold,underline Parameters:|@%n",
optionListHeading = "%n@|bold,underline Options:|@%n",
header = ["Obtains access credentials for your user account via a web-based authorization flow."],
description = ["""Authenticates using your user account. For CI, a service account is recommended."""]
)
class LoginCommand : Runnable {
override fun run() {
GcAuth.authorizeUser()
println("Saving credential to ${GcAuth.CRED.absolutePath}")
}

@CommandLine.Option(
names = ["-h", "--help"],
usageHelp = true,
description = ["Prints this help message"]
)
var usageHelpRequested: Boolean = false
}
25 changes: 19 additions & 6 deletions test_runner/src/main/kotlin/ftl/config/FtlConstants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import com.google.api.client.googleapis.util.Utils
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 ftl.args.AndroidArgs
import ftl.args.IArgs
import ftl.args.IosArgs
import ftl.gc.GcAuth
import ftl.http.TimeoutHttpRequestInitializer
import java.nio.file.Path
import java.nio.file.Paths
Expand Down Expand Up @@ -45,27 +48,37 @@ object FtlConstants {
Paths.get(System.getProperty("user.home"), ".config/gcloud/application_default_credentials.json")
}

val credential: HttpRequestInitializer by lazy {
private val credentialObj: GoogleCredential by lazy {
try {
// 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
val credential = if (useMock) {
return@lazy if (useMock) {
MockGoogleCredential.Builder()
.setTransport(MockGoogleCredential.newMockHttpTransportWithSampleTokenResponse())
.build()
} else {
GoogleCredential.getApplicationDefault()
.createScoped(listOf("https://www.googleapis.com/auth/cloud-platform"))
if (GcAuth.hasUserAuth()) {
GcAuth.authorizeUser()
} else {
GoogleCredential.getApplicationDefault()
.createScoped(listOf("https://www.googleapis.com/auth/cloud-platform"))
}
}

return@lazy TimeoutHttpRequestInitializer(credential)
} catch (e: Exception) {
throw RuntimeException(e)
}
}

val credential: HttpRequestInitializer by lazy {
return@lazy TimeoutHttpRequestInitializer(credentialObj)
}

val googleCredentials: GoogleCredentials by lazy {
GoogleCredentials.create(AccessToken(credentialObj.accessToken, null))
}

const val localResultsDir = "results"

fun configFileName(args: IArgs): String {
Expand Down
59 changes: 59 additions & 0 deletions test_runner/src/main/kotlin/ftl/gc/GcAuth.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package ftl.gc

import com.google.api.client.auth.oauth2.Credential
import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp
import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport
import com.google.api.client.json.jackson2.JacksonFactory
import com.google.api.client.util.store.FileDataStoreFactory
import ftl.config.FtlConstants
import java.io.File
import java.io.IOException

// https://github.com/googleapis/google-oauth-java-client
// GoogleAuthorizationCodeFlow usage based on https://developers.google.com/sheets/api/quickstart/java
object GcAuth {
private val HOME = System.getProperty("user.home")
private val CRED_FOLDER = File(HOME, ".flank/")
val CRED = File(CRED_FOLDER, "StoredCredential")

private val JSON_FACTORY = JacksonFactory.getDefaultInstance()
private var HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport()
private var DATA_STORE_FACTORY = FileDataStoreFactory(CRED_FOLDER)

// https://github.com/bootstraponline/gcloud_cli/blob/40521a6e297830b9f652a9ab4d8002e309b4353a/google-cloud-sdk/platform/gsutil/gslib/utils/system_util.py#L177
private const val CLIENT_ID = "32555940559.apps.googleusercontent.com"
private const val CLIENT_SECRET = "ZmssLNjJy2998hD4CTg2ejr2"

fun hasUserAuth(): Boolean {
return CRED.exists()
}

private fun Credential.toGoogleCredential(): GoogleCredential {
return GoogleCredential.Builder()
.setTransport(HTTP_TRANSPORT)
.setJsonFactory(JSON_FACTORY)
.setClientSecrets(CLIENT_ID, CLIENT_SECRET)
.build()
.setAccessToken(this.accessToken)
}

@Throws(IOException::class)
fun authorizeUser(): GoogleCredential {
if (FtlConstants.useMock) return GoogleCredential()
// https://github.com/bootstraponline/gcloud_cli/blob/e4b5e01610abad2e31d8a6edb20b17b2f84c5395/google-cloud-sdk/lib/googlecloudsdk/core/config.py#L167
val scopes = listOf("https://www.googleapis.com/auth/cloud-platform")

val flow = GoogleAuthorizationCodeFlow.Builder(HTTP_TRANSPORT, JSON_FACTORY, CLIENT_ID, CLIENT_SECRET, scopes)
.setDataStoreFactory(DATA_STORE_FACTORY)
.setAccessType("offline")
.build()

val authCode = AuthorizationCodeInstalledApp(flow, LocalServerReceiver())
val dataStoreKey = "default"

return authCode.authorize(dataStoreKey).toGoogleCredential()
}
}
5 changes: 5 additions & 0 deletions test_runner/src/main/kotlin/ftl/gc/GcStorage.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package ftl.gc

import com.google.api.client.http.GoogleApiLogger
import com.google.cloud.storage.BlobInfo
import com.google.cloud.storage.Storage
import com.google.cloud.storage.StorageOptions
Expand All @@ -26,10 +27,14 @@ object GcStorage {
val storageOptions: StorageOptions by lazy {
val builder = StorageOptions.newBuilder()
if (FtlConstants.useMock) builder.setHost(FtlConstants.localhost)

builder.setCredentials(FtlConstants.googleCredentials).build().service

builder.build()
}

val storage: Storage by lazy {
GoogleApiLogger.silenceComputeEngine()
if (FtlConstants.useMock) {
LocalStorageHelper.getOptions().service
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package ftl.http

import com.google.api.client.googleapis.auth.oauth2.GoogleCredential
import com.google.api.client.auth.oauth2.Credential
import com.google.api.client.http.HttpRequest
import com.google.api.client.http.HttpRequestInitializer

class TimeoutHttpRequestInitializer(private val googleCredential: GoogleCredential) : HttpRequestInitializer {
class TimeoutHttpRequestInitializer(private val credential: Credential) : HttpRequestInitializer {
override fun initialize(request: HttpRequest?) {
googleCredential.initialize(request)
credential.initialize(request)
// timeout in milliseconds. wait 60s instead of default 20s
request?.connectTimeout = 60 * 1000
request?.readTimeout = 60 * 1000
Expand Down
27 changes: 27 additions & 0 deletions test_runner/src/test/kotlin/ftl/cli/AuthCommandTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package ftl.cli

import com.google.common.truth.Truth.assertThat
import ftl.test.util.FlankTestRunner
import org.junit.Rule
import org.junit.Test
import org.junit.contrib.java.lang.system.SystemOutRule
import org.junit.runner.RunWith

@RunWith(FlankTestRunner::class)
class AuthCommandTest {
@Rule
@JvmField
val systemOutRule: SystemOutRule = SystemOutRule().enableLog().muteForSuccessfulTests()

@Test
fun firebaseCommandPrintsHelp() {
AuthCommand().run()
val output = systemOutRule.log
assertThat(output).startsWith(
"Manage oauth2 credentials for Google Cloud\n\n" +
"auth [COMMAND]\n" +
"Commands:\n" +
" login"
)
}
}
50 changes: 50 additions & 0 deletions test_runner/src/test/kotlin/ftl/cli/auth/LoginCommandTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package ftl.cli.auth

import com.google.common.truth.Truth
import com.google.common.truth.Truth.assertThat
import ftl.test.util.FlankTestRunner
import org.junit.Rule
import org.junit.Test
import org.junit.contrib.java.lang.system.ExpectedSystemExit
import org.junit.contrib.java.lang.system.SystemOutRule
import org.junit.runner.RunWith
import picocli.CommandLine

@RunWith(FlankTestRunner::class)
class LoginCommandTest {
@Rule
@JvmField
val systemOutRule: SystemOutRule = SystemOutRule().enableLog().muteForSuccessfulTests()

@get:Rule
val exit = ExpectedSystemExit.none()

@Test
fun cancelCommandPrintsHelp() {
val command = LoginCommand()
assertThat(command.usageHelpRequested).isFalse()
CommandLine.run<Runnable>(command, System.out, "-h")

val output = systemOutRule.log
Truth.assertThat(output).startsWith(
"""Obtains access credentials for your user account via a web-based authorization
flow.
login [-h]""".trimIndent())

assertThat(command.usageHelpRequested).isTrue()
}

@Test
fun commandRuns() {
LoginCommand().run()
}

@Test
fun cancelCommandOptions() {
val cmd = LoginCommand()
assertThat(cmd.usageHelpRequested).isFalse()
cmd.usageHelpRequested = true
assertThat(cmd.usageHelpRequested).isTrue()
}
}

0 comments on commit f710c18

Please sign in to comment.