go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/gae/impl/prod/devserver.go (about)

     1  // Copyright 2016 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package prod
    16  
    17  import (
    18  	"context"
    19  	"encoding/json"
    20  	"fmt"
    21  	"io"
    22  	"net/http"
    23  	"net/url"
    24  	"sync"
    25  
    26  	"google.golang.org/appengine"
    27  	"google.golang.org/appengine/log"
    28  	"google.golang.org/appengine/urlfetch"
    29  )
    30  
    31  var devAccountCache struct {
    32  	once    sync.Once
    33  	account string
    34  	err     error
    35  }
    36  
    37  // developerAccount is used on dev server to get account name matching
    38  // the OAuth token produced by AccessToken.
    39  //
    40  // On dev server ServiceAccount returns empty string, but AccessToken(...) works
    41  // and returns developer's token (the one configured with "gcloud auth"). We can
    42  // use it to get the matching account name.
    43  func developerAccount(gaeCtx context.Context) (string, error) {
    44  	if !appengine.IsDevAppServer() {
    45  		panic("developerAccount must not be used outside of devserver")
    46  	}
    47  	devAccountCache.once.Do(func() {
    48  		devAccountCache.account, devAccountCache.err = fetchDevAccount(gaeCtx)
    49  		if devAccountCache.err == nil {
    50  			log.Debugf(gaeCtx, "Devserver is running as %q", devAccountCache.account)
    51  		} else {
    52  			log.Errorf(gaeCtx, "Failed to fetch account name associated with AccessToken - %s", devAccountCache.err)
    53  		}
    54  	})
    55  	return devAccountCache.account, devAccountCache.err
    56  }
    57  
    58  // fetchDevAccount grabs an access token and calls Google API to get associated
    59  // email.
    60  func fetchDevAccount(gaeCtx context.Context) (string, error) {
    61  	// Grab the developer's token from devserver.
    62  	tok, _, err := appengine.AccessToken(gaeCtx, "https://www.googleapis.com/auth/userinfo.email")
    63  	if err != nil {
    64  		return "", err
    65  	}
    66  
    67  	// Fetch the info dict associated with the token.
    68  	client := http.Client{
    69  		Transport: &urlfetch.Transport{Context: gaeCtx},
    70  	}
    71  	resp, err := client.Get("https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=" + url.QueryEscape(tok))
    72  	if err != nil {
    73  		return "", err
    74  	}
    75  	defer func() {
    76  		io.ReadAll(resp.Body)
    77  		resp.Body.Close()
    78  	}()
    79  	if resp.StatusCode >= 500 {
    80  		return "", fmt.Errorf("devserver: transient error when validating token (HTTP %d)", resp.StatusCode)
    81  	}
    82  
    83  	// There's more stuff in the reply, we don't need it.
    84  	var tokenInfo struct {
    85  		Email string `json:"email"`
    86  		Error string `json:"error_description"`
    87  	}
    88  	if err := json.NewDecoder(resp.Body).Decode(&tokenInfo); err != nil {
    89  		return "", fmt.Errorf("devserver: failed to deserialize token info JSON - %s", err)
    90  	}
    91  	switch {
    92  	case tokenInfo.Error != "":
    93  		return "", fmt.Errorf("devserver: bad token - %s", tokenInfo.Error)
    94  	case tokenInfo.Email == "":
    95  		return "", fmt.Errorf("devserver: token is not associated with an email")
    96  	}
    97  	return tokenInfo.Email, nil
    98  }