From 758b44b9c035ddaf44bf219a449f633289803945 Mon Sep 17 00:00:00 2001
From: Peter Schultz <peter.schultz@classmarkets.com>
Date: Tue, 21 Jul 2020 19:24:30 +0200
Subject: [PATCH] gs: support authentication with access token

In the Google Cloud Storage backend, support specifying access tokens
directly, as an alternative to a credentials file. This is useful when
restic is used non-interactively by some other program that is already
authenticated and eliminates the need to store long lived credentials.

The access token is specified in the GOOGLE_ACCESS_TOKEN environment
variable and takes precedence over GOOGLE_APPLICATION_CREDENTIALS.
---
 changelog/unreleased/pull-2849   |  7 +++++++
 doc/030_preparing_a_new_repo.rst | 12 ++++++++++++
 internal/backend/gs/gs.go        | 20 +++++++++++++++-----
 internal/backend/gs/gs_test.go   | 10 ++++++++--
 4 files changed, 42 insertions(+), 7 deletions(-)
 create mode 100644 changelog/unreleased/pull-2849

diff --git a/changelog/unreleased/pull-2849 b/changelog/unreleased/pull-2849
new file mode 100644
index 00000000..fde8d411
--- /dev/null
+++ b/changelog/unreleased/pull-2849
@@ -0,0 +1,7 @@
+Enhancement: Authenticate to Google Cloud Storage with access token
+
+When using the GCS backend, it is now possible to authenticate with OAuth2
+access tokens instead of a credentials file by setting the GOOGLE_ACCESS_TOKEN
+environment variable.
+
+https://github.com/restic/restic/pull/2849
diff --git a/doc/030_preparing_a_new_repo.rst b/doc/030_preparing_a_new_repo.rst
index 78096426..60c2e24b 100644
--- a/doc/030_preparing_a_new_repo.rst
+++ b/doc/030_preparing_a_new_repo.rst
@@ -458,6 +458,18 @@ which means if you're running in Google Container Engine or are otherwise
 located on an instance with default service accounts then these should work out of 
 the box.
 
+Alternatively, you can specify an existing access token directly:
+
+.. code-block:: console
+
+    $ export GOOGLE_ACCESS_TOKEN=ya29.a0AfH6SMC78...
+
+If ``GOOGLE_ACCESS_TOKEN`` is set all other authentication mechanisms are
+disabled. The access token must have at least the
+``https://www.googleapis.com/auth/devstorage.read_write`` scope. Keep in mind
+that access tokens are short-lived (usually one hour), so they are not suitable
+if creating a backup takes longer than that, for instance.
+
 Once authenticated, you can use the ``gs:`` backend type to create a new
 repository in the bucket ``foo`` at the root path:
 
diff --git a/internal/backend/gs/gs.go b/internal/backend/gs/gs.go
index feea05d0..9088f116 100644
--- a/internal/backend/gs/gs.go
+++ b/internal/backend/gs/gs.go
@@ -47,15 +47,25 @@ func getStorageService(rt http.RoundTripper) (*storage.Service, error) {
 		Transport: rt,
 	}
 
-	// create a now context with the HTTP client stored at the oauth2.HTTPClient key
+	// create a new context with the HTTP client stored at the oauth2.HTTPClient key
 	ctx := context.WithValue(context.Background(), oauth2.HTTPClient, httpClient)
 
-	// use this context
-	client, err := google.DefaultClient(ctx, storage.DevstorageReadWriteScope)
-	if err != nil {
-		return nil, err
+	var ts oauth2.TokenSource
+	if token := os.Getenv("GOOGLE_ACCESS_TOKEN"); token != "" {
+		ts = oauth2.StaticTokenSource(&oauth2.Token{
+			AccessToken: token,
+			TokenType:   "Bearer",
+		})
+	} else {
+		var err error
+		ts, err = google.DefaultTokenSource(ctx, storage.DevstorageReadWriteScope)
+		if err != nil {
+			return nil, err
+		}
 	}
 
+	client := oauth2.NewClient(ctx, ts)
+
 	service, err := storage.New(client)
 	if err != nil {
 		return nil, err
diff --git a/internal/backend/gs/gs_test.go b/internal/backend/gs/gs_test.go
index 27ff809f..d7bf1422 100644
--- a/internal/backend/gs/gs_test.go
+++ b/internal/backend/gs/gs_test.go
@@ -87,7 +87,6 @@ func TestBackendGS(t *testing.T) {
 	}()
 
 	vars := []string{
-		"GOOGLE_APPLICATION_CREDENTIALS",
 		"RESTIC_TEST_GS_PROJECT_ID",
 		"RESTIC_TEST_GS_REPOSITORY",
 	}
@@ -98,6 +97,10 @@ func TestBackendGS(t *testing.T) {
 			return
 		}
 	}
+	if os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")+os.Getenv("GOOGLE_ACCESS_TOKEN") == "" {
+		t.Skipf("environment variable GOOGLE_APPLICATION_CREDENTIALS not set, nor GOOGLE_ACCESS_TOKEN")
+		return
+	}
 
 	t.Logf("run tests")
 	newGSTestSuite(t).RunTests(t)
@@ -105,7 +108,6 @@ func TestBackendGS(t *testing.T) {
 
 func BenchmarkBackendGS(t *testing.B) {
 	vars := []string{
-		"GOOGLE_APPLICATION_CREDENTIALS",
 		"RESTIC_TEST_GS_PROJECT_ID",
 		"RESTIC_TEST_GS_REPOSITORY",
 	}
@@ -116,6 +118,10 @@ func BenchmarkBackendGS(t *testing.B) {
 			return
 		}
 	}
+	if os.Getenv("GOOGLE_APPLICATION_CREDENTIALS")+os.Getenv("GOOGLE_ACCESS_TOKEN") == "" {
+		t.Skipf("environment variable GOOGLE_APPLICATION_CREDENTIALS not set, nor GOOGLE_ACCESS_TOKEN")
+		return
+	}
 
 	t.Logf("run tests")
 	newGSTestSuite(t).RunBenchmarks(t)