blob: 0597a2e08239c51be83c47b44b1f1c16c0ca05c4 [file] [log] [blame]
// Copyright 2020 Google Ltd. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package main
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"log"
"net/http"
"sync"
"time"
"github.com/google/gerrit-linter/gerrit"
)
// The token as it comes from metadata service.
type gcpToken struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
TokenType string `json:"token_type"`
}
// tokenCache fetches a bearer token from the GCP metadata service,
// and refreshes it before it expires.
type tokenCache struct {
account string
mu sync.Mutex
current *gcpToken
}
// Implement the Authenticator interface.
func (tc *tokenCache) Authenticate(req *http.Request) error {
tc.mu.Lock()
defer tc.mu.Unlock()
if tc.current == nil {
return fmt.Errorf("no token")
}
req.Header.Set("Authorization", "Bearer "+tc.current.AccessToken)
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)
req, err := http.NewRequest("GET", u, 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)
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("%s failed (%d): %s", req, resp.StatusCode, string(all))
}
tok := &gcpToken{}
if err := json.Unmarshal(all, tok); err != nil {
return nil, fmt.Errorf("can't unmarshal %s: %v", string(all), err)
}
return tok, nil
}
// NewGCPServiceAccount returns a Authenticator that will use GCP
// bearer-tokens. The tokens are refreshed automatically.
func NewGCPServiceAccount(account string) (gerrit.Authenticator, error) {
tc := tokenCache{
account: account,
}
tc.current, err := tc.fetch()
if err != nil {
return nil, err
}
go tc.loop()
return &tc, nil
}
// loop refreshes the token periodically.
func (tc *tokenCache) loop() {
delaySecs := tc.current.ExpiresIn - 1
for {
time.Sleep(time.Duration(delaySecs) * time.Second)
tok, err := tc.fetch()
if err != nil {
log.Printf("fetching token failed: %s", err)
delaySecs = 2
} else {
delaySecs = tok.ExpiresIn - 1
}
tc.mu.Lock()
tc.current = tok
tc.mu.Unlock()
}
}