Index: impl/prod/devserver.go |
diff --git a/impl/prod/devserver.go b/impl/prod/devserver.go |
new file mode 100644 |
index 0000000000000000000000000000000000000000..ef91dc2c4978019d1b7ef5f6c6b5bb195e918195 |
--- /dev/null |
+++ b/impl/prod/devserver.go |
@@ -0,0 +1,89 @@ |
+// Copyright 2016 The LUCI Authors. All rights reserved. |
+// Use of this source code is governed under the Apache License, Version 2.0 |
+// that can be found in the LICENSE file. |
+ |
+package prod |
+ |
+import ( |
+ "encoding/json" |
+ "fmt" |
+ "io/ioutil" |
+ "net/http" |
+ "net/url" |
+ "sync" |
+ |
+ "golang.org/x/net/context" |
+ |
+ "google.golang.org/appengine" |
+ "google.golang.org/appengine/log" |
+ "google.golang.org/appengine/urlfetch" |
+) |
+ |
+var devAccountCache struct { |
+ once sync.Once |
+ account string |
+ err error |
+} |
+ |
+// developerAccount is used on dev server to get account name matching |
+// the OAuth token produced by AccessToken. |
+// |
+// On dev server ServiceAccount returns empty string, but AccessToken(...) works |
+// and returns developer's token (the one configured with "gcloud auth"). We can |
+// use it to get the matching account name. |
+func developerAccount(gaeCtx context.Context) (string, error) { |
+ if !appengine.IsDevAppServer() { |
+ panic("developerAccount must not be used outside of devserver") |
+ } |
+ devAccountCache.once.Do(func() { |
+ devAccountCache.account, devAccountCache.err = fetchDevAccount(gaeCtx) |
+ if devAccountCache.err == nil { |
+ log.Debugf(gaeCtx, "Devserver is running as %q", devAccountCache.account) |
+ } else { |
+ log.Errorf(gaeCtx, "Failed to fetch account name associated with AccessToken - %s", devAccountCache.err) |
+ } |
+ }) |
+ return devAccountCache.account, devAccountCache.err |
+} |
+ |
+// fetchDevAccount grabs an access token and calls Google API to get associated |
+// email. |
+func fetchDevAccount(gaeCtx context.Context) (string, error) { |
+ // Grab the developer's token from devserver. |
+ tok, _, err := appengine.AccessToken(gaeCtx, "https://www.googleapis.com/auth/userinfo.email") |
+ if err != nil { |
+ return "", err |
+ } |
+ |
+ // Fetch the info dict associated with the token. |
+ client := http.Client{ |
+ Transport: &urlfetch.Transport{Context: gaeCtx}, |
+ } |
+ resp, err := client.Get("https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=" + url.QueryEscape(tok)) |
+ if err != nil { |
+ return "", err |
+ } |
+ defer func() { |
+ ioutil.ReadAll(resp.Body) |
+ resp.Body.Close() |
+ }() |
+ if resp.StatusCode >= 500 { |
+ return "", fmt.Errorf("devserver: transient error when validating token (HTTP %d)", resp.StatusCode) |
+ } |
+ |
+ // There's more stuff in the reply, we don't need it. |
+ var tokenInfo struct { |
+ Email string `json:"email"` |
+ Error string `json:"error_description"` |
+ } |
+ if err := json.NewDecoder(resp.Body).Decode(&tokenInfo); err != nil { |
+ return "", fmt.Errorf("devserver: failed to deserialize token info JSON - %s", err) |
+ } |
+ switch { |
+ case tokenInfo.Error != "": |
+ return "", fmt.Errorf("devserver: bad token - %s", tokenInfo.Error) |
+ case tokenInfo.Email == "": |
+ return "", fmt.Errorf("devserver: token is not associated with an email") |
+ } |
+ return tokenInfo.Email, nil |
+} |