[BCID] Use GCP secrets for login

Change-Id: Id61bd077521d3497efde20327f15e34f6dddfb61
Reviewed-on: https://skia-review.googlesource.com/c/buildbot/+/556963
Commit-Queue: Eric Boren <borenet@google.com>
Reviewed-by: Joe Gregorio <jcgregorio@google.com>
diff --git a/autoroll/go/autoroll-fe/main.go b/autoroll/go/autoroll-fe/main.go
index a1cc71d..e7cf68e 100644
--- a/autoroll/go/autoroll-fe/main.go
+++ b/autoroll/go/autoroll-fe/main.go
@@ -386,9 +386,9 @@
 	login.InitWithAllow(serverURL+login.DEFAULT_OAUTH2_CALLBACK, adminAllow, editAllow, viewAllow)
 
 	// Load the OAuth2 config information.
-	_, clientID, clientSecret := login.TryLoadingFromKnownLocations()
-	if clientID == "" || clientSecret == "" {
-		sklog.Fatal("Failed to load OAuth2 configuration.")
+	_, clientID, clientSecret, err := login.TryLoadingFromK8sSecret()
+	if err != nil {
+		sklog.Fatalf("Failed to load OAuth2 configuration: %s", err)
 	}
 	gerritOauthConfig.ClientID = clientID
 	gerritOauthConfig.ClientSecret = clientSecret
diff --git a/go/login/BUILD.bazel b/go/login/BUILD.bazel
index 758aada..7ce2234 100644
--- a/go/login/BUILD.bazel
+++ b/go/login/BUILD.bazel
@@ -9,6 +9,7 @@
     deps = [
         "//go/allowed",
         "//go/httputils",
+        "//go/secret",
         "//go/skerr",
         "//go/sklog",
         "//go/util",
@@ -25,6 +26,8 @@
     embed = [":login"],
     deps = [
         "//go/deepequal/assertdeep",
+        "//go/secret",
+        "//go/secret/mocks",
         "//go/testutils/unittest",
         "@com_github_stretchr_testify//assert",
         "@com_github_stretchr_testify//require",
diff --git a/go/login/login.go b/go/login/login.go
index 7fd2369..365b6b8 100644
--- a/go/login/login.go
+++ b/go/login/login.go
@@ -43,6 +43,7 @@
 	"github.com/gorilla/securecookie"
 	"go.skia.org/infra/go/allowed"
 	"go.skia.org/infra/go/httputils"
+	"go.skia.org/infra/go/secret"
 	"go.skia.org/infra/go/skerr"
 	"go.skia.org/infra/go/sklog"
 	"go.skia.org/infra/go/util"
@@ -81,6 +82,12 @@
 
 	// DEFAULT_CLIENT_SECRET_FILE is the default path to the file used for OAuth2 login.
 	DEFAULT_CLIENT_SECRET_FILE = "client_secret.json"
+
+	// LoginSecretProject is the GCP project containing the login secrets.
+	LoginSecretProject = "skia-infra-public"
+
+	// LoginSecretName is the name of the GCP secret for login.
+	LoginSecretName = "login-oauth2-secrets"
 )
 
 var (
@@ -179,33 +186,41 @@
 // Init must be called before any other login methods.
 //
 // The function first tries to load the cookie salt, client id, and client
-// secret from GCE project level metadata. If that fails it looks for a
+// secret from a file provided by Kubernetes secrets. If that fails, it tries to
+// load them from GCP secret manager, and if that also fails it looks for a
 // "client_secret.json" file in the current directory to extract the client id
-// and client secret from. If both of those fail then it returns an error.
+// and client secret from. If all three of those fail then it returns an error.
 //
 // The authAllowList is the space separated list of domains and email addresses
 // that are allowed to log in.
 func Init(redirectURL string, authAllowList string, clientSecretFile string) error {
-	cookieSalt, clientID, clientSecret := TryLoadingFromKnownLocations()
-	if clientID == "" {
-		if clientSecretFile == "" {
-			clientSecretFile = DEFAULT_CLIENT_SECRET_FILE
-		}
-
-		b, err := ioutil.ReadFile(clientSecretFile)
-		if err != nil {
-			return skerr.Fmt("Failed to read from metadata and from %s. Got error: %s", clientSecretFile, err)
-		}
-		config, err := google.ConfigFromJSON(b)
-		if err != nil {
-			return skerr.Fmt("Failed to read from metadata and decode %s. Got error: %s", clientSecretFile, err)
-		}
-		sklog.Infof("Successfully read client secret from %s", clientSecretFile)
-		clientID = config.ClientID
-		clientSecret = config.ClientSecret
+	// Kubernetes secret.
+	cookieSalt, clientID, clientSecret, err1 := TryLoadingFromK8sSecret()
+	if err1 == nil {
+		initLogin(clientID, clientSecret, redirectURL, cookieSalt, DEFAULT_SCOPE, authAllowList)
+		return nil
 	}
-	initLogin(clientID, clientSecret, redirectURL, cookieSalt, DEFAULT_SCOPE, authAllowList)
-	return nil
+
+	// GCP secret.
+	ctx := context.TODO()
+	secretClient, err2 := secret.NewClient(ctx)
+	if err2 == nil {
+		cookieSalt, clientID, clientSecret, err2 := TryLoadingFromGCPSecret(ctx, secretClient)
+		if err2 == nil {
+			initLogin(clientID, clientSecret, redirectURL, cookieSalt, DEFAULT_SCOPE, authAllowList)
+			return nil
+		}
+	} else {
+		err2 = skerr.Wrapf(err2, "failed loading login secrets from GCP secret manager; failed to create client")
+	}
+
+	// Local file.
+	cookieSalt, clientID, clientSecret, err3 := TryLoadingFromFile(clientSecretFile)
+	if err3 == nil {
+		initLogin(clientID, clientSecret, redirectURL, cookieSalt, DEFAULT_SCOPE, authAllowList)
+		return nil
+	}
+	return skerr.Fmt("Failed loading from metadata, GCP secrets, and from %s: %s | %s | %s", clientSecretFile, err1, err2, err3)
 }
 
 // initLogin sets the params.  It should only be called directly for testing purposes.
@@ -837,12 +852,12 @@
 	ClientSecret string `json:"client_secret"`
 }
 
-// TryLoadingFromKnownLocations tries to load the cookie salt, client id, and
-// client secret from a file in a known location. If it fails then it returns
-// the salt it was passed and the client id and secret are the empty string.
+// TryLoadingFromK8sSecret tries to load the cookie salt, client id, and client
+// secret from a file in a known location which is created from a Kubernetes
+// secret.
 //
 // Returns salt, clientID, clientSecret.
-func TryLoadingFromKnownLocations() (string, string, string) {
+func TryLoadingFromK8sSecret() (string, string, string, error) {
 	cookieSalt := ""
 	clientID := ""
 	clientSecret := ""
@@ -856,13 +871,49 @@
 		clientSecret = info.ClientSecret
 		return nil
 	})
-
-	if err == nil {
-		sklog.Infof("Successfully loaded login secrets from file %s.", LOGIN_CONFIG_FILE)
-		return cookieSalt, clientID, clientSecret
+	if err != nil {
+		return "", "", "", skerr.Wrapf(err, "failed loading login secrets from %s", LOGIN_CONFIG_FILE)
 	}
-	sklog.Infof("Failed to load login secrets from file %s. Got error: %s", LOGIN_CONFIG_FILE, err)
-	return DEFAULT_COOKIE_SALT, "", ""
+	sklog.Infof("Successfully loaded login secrets from file %s.", LOGIN_CONFIG_FILE)
+	return cookieSalt, clientID, clientSecret, nil
+}
+
+// TryLoadingFromFile tries to load the client id and client secret from the
+// given file.  If not specified, it tries to load from client_secret.json in
+// the current working directory.
+//
+// Returns DEFAULT_COOKIE_SALT, clientID, clientSecret.
+func TryLoadingFromFile(clientSecretFile string) (string, string, string, error) {
+	if clientSecretFile == "" {
+		clientSecretFile = DEFAULT_CLIENT_SECRET_FILE
+	}
+	b, err := ioutil.ReadFile(clientSecretFile)
+	if err != nil {
+		return "", "", "", skerr.Wrapf(err, "failed loading login secrets from %s", clientSecretFile)
+	}
+	config, err := google.ConfigFromJSON(b)
+	if err != nil {
+		return "", "", "", skerr.Wrapf(err, "failed decoding login secrets from %s", clientSecretFile)
+	}
+	sklog.Infof("Successfully read client secret from %s", clientSecretFile)
+	return DEFAULT_COOKIE_SALT, config.ClientID, config.ClientSecret, nil
+}
+
+// TryLoadingFromGCPSecret tries to load the cookie salt, client id, and client
+// secret from GCP secrets.  If it fails, it returns the default cookie salt and
+// the client id and secret are the empty string.
+//
+// Returns salt, clientID, clientSecret.
+func TryLoadingFromGCPSecret(ctx context.Context, secretClient secret.Client) (string, string, string, error) {
+	contents, err := secretClient.Get(ctx, LoginSecretProject, LoginSecretName, secret.VersionLatest)
+	if err != nil {
+		return "", "", "", skerr.Wrapf(err, "failed loading login secrets from GCP secret manager; failed to retrieve secret %q", LoginSecretName)
+	}
+	var info loginInfo
+	if err := json.Unmarshal([]byte(contents), &info); err != nil {
+		return "", "", "", skerr.Wrapf(err, "successfully retrieved login secret from GCP secrets but failed to decode it as JSON")
+	}
+	return info.Salt, info.ClientID, info.ClientSecret, nil
 }
 
 // ViaBearerToken tries to load an OAuth 2.0 Bearer token from from the request
diff --git a/go/login/login_test.go b/go/login/login_test.go
index 8ff371b..43ffeaa 100644
--- a/go/login/login_test.go
+++ b/go/login/login_test.go
@@ -10,6 +10,8 @@
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"go.skia.org/infra/go/deepequal/assertdeep"
+	"go.skia.org/infra/go/secret"
+	"go.skia.org/infra/go/secret/mocks"
 	"go.skia.org/infra/go/testutils/unittest"
 )
 
@@ -261,3 +263,21 @@
 		})
 	})
 }
+
+func TestTryLoadingFromGCPSecret_Success(t *testing.T) {
+	unittest.SmallTest(t)
+
+	ctx := context.Background()
+	client := &mocks.Client{}
+	secretValue := `{
+  "salt": "fake-salt",
+  "client_id": "fake-client-id",
+  "client_secret": "fake-client-secret"
+}`
+	client.On("Get", ctx, LoginSecretProject, LoginSecretName, secret.VersionLatest).Return(secretValue, nil)
+	cookieSalt, clientID, clientSecret, err := TryLoadingFromGCPSecret(ctx, client)
+	require.NoError(t, err)
+	require.Equal(t, "fake-salt", cookieSalt)
+	require.Equal(t, "fake-client-id", clientID)
+	require.Equal(t, "fake-client-secret", clientSecret)
+}