cmd/checker: for GCP, check scopes

GCP service accounts can be granted different permissions ("scopes").
Without the correct scope, requests be denied with status 403.

With this change, gerrit-linter will provide a better error message
if scopes are misconfigured.

Change-Id: I74390f4f89b7bf3bc6cec54a6527179a764add7f
diff --git a/cmd/checker/gcp.go b/cmd/checker/gcp.go
index 0597a2e..ad8f81b 100644
--- a/cmd/checker/gcp.go
+++ b/cmd/checker/gcp.go
@@ -21,6 +21,7 @@
 	"io/ioutil"
 	"log"
 	"net/http"
+	"strings"
 	"sync"
 	"time"
 
@@ -56,12 +57,43 @@
 	return nil
 }
 
-// fetch gets the token from the metadata server.
-func (tc *tokenCache) fetch() (*gcpToken, error) {
-	u := fmt.Sprintf("http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/%s/token",
-		tc.account)
+// The name of the scope that is necessary to access googlesource.com
+// gerrit instances.
+const gerritScope = "https://www.googleapis.com/auth/gerritcodereview"
 
-	req, err := http.NewRequest("GET", u, nil)
+// scopeURL returns the URL where GCP serves scopes for an account.
+func scopeURL(account string) string {
+	return fmt.Sprintf("http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/%s/scopes",
+		account)
+}
+
+// tokenURL returns the URL where GCP serves tokens for an account.
+func tokenURL(account string) string {
+	return fmt.Sprintf("http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/%s/token",
+		account)
+}
+
+// fetchScopes returns the scopes for the configured service account.
+func (tc *tokenCache) fetchScopes() ([]string, error) {
+	req, err := http.NewRequest("GET", scopeURL(tc.account), nil)
+	if err != nil {
+		return nil, err
+	}
+	req.Header.Set("Metadata-Flavor", "Google")
+
+	resp, err := http.DefaultClient.Do(req.WithContext(context.Background()))
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	all, _ := ioutil.ReadAll(resp.Body)
+
+	return strings.Split(strings.TrimSpace(string(all)), "\n"), nil
+}
+
+// fetch gets the token from the metadata server.
+func (tc *tokenCache) fetchToken() (*gcpToken, error) {
+	req, err := http.NewRequest("GET", tokenURL(tc.account), nil)
 	if err != nil {
 		return nil, err
 	}
@@ -75,7 +107,7 @@
 	all, _ := ioutil.ReadAll(resp.Body)
 
 	if resp.StatusCode != http.StatusOK {
-		return nil, fmt.Errorf("%s failed (%d): %s", req, resp.StatusCode, string(all))
+		return nil, fmt.Errorf("%v failed (%d): %s", req, resp.StatusCode, string(all))
 	}
 
 	tok := &gcpToken{}
@@ -87,13 +119,30 @@
 }
 
 // NewGCPServiceAccount returns a Authenticator that will use GCP
-// bearer-tokens. The tokens are refreshed automatically.
+// bearer-tokens to authenticate against a googlesource.com Gerrit
+// instance. The tokens are refreshed automatically.
 func NewGCPServiceAccount(account string) (gerrit.Authenticator, error) {
 	tc := tokenCache{
 		account: account,
 	}
 
-	tc.current, err := tc.fetch()
+	scopes, err := tc.fetchScopes()
+	if err != nil {
+		return nil, err
+	}
+
+	found := false
+	for _, s := range scopes {
+		if s == gerritScope {
+			found = true
+			break
+		}
+	}
+	if !found {
+		return nil, fmt.Errorf("missing scope %q, got %q", gerritScope, scopes)
+	}
+
+	tc.current, err = tc.fetchToken()
 	if err != nil {
 		return nil, err
 	}
@@ -108,7 +157,7 @@
 	delaySecs := tc.current.ExpiresIn - 1
 	for {
 		time.Sleep(time.Duration(delaySecs) * time.Second)
-		tok, err := tc.fetch()
+		tok, err := tc.fetchToken()
 		if err != nil {
 			log.Printf("fetching token failed:  %s", err)
 			delaySecs = 2