| 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
|
| +}
|
|
|