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 }