| Index: go/src/infra/libs/auth/auth.go
|
| diff --git a/go/src/infra/libs/auth/auth.go b/go/src/infra/libs/auth/auth.go
|
| deleted file mode 100644
|
| index 9082eb14e69c1feba6fbe714127ad01d439d6567..0000000000000000000000000000000000000000
|
| --- a/go/src/infra/libs/auth/auth.go
|
| +++ /dev/null
|
| @@ -1,602 +0,0 @@
|
| -// Copyright 2014 The Chromium Authors. All rights reserved.
|
| -// Use of this source code is governed by a BSD-style license that can be
|
| -// found in the LICENSE file.
|
| -
|
| -/*
|
| -Package auth defines an opinionated wrapper around OAuth2.
|
| -
|
| -It hides configurability of base oauth2 library and instead makes a predefined
|
| -set of choices regarding where the credentials should be stored and how OAuth2
|
| -should be used. It makes authentication flows look more uniform across tools
|
| -that use infra.libs.auth and allow credentials reuse across multiple binaries.
|
| -
|
| -Also it knows about various environments Chrome Infra tools are running under
|
| -(GCE, Chrome Infra Golo, GAE, developers' machine) and switches default
|
| -authentication scheme accordingly (e.g. on GCE machine the default is to use
|
| -GCE metadata server).
|
| -
|
| -All tools that use infra.libs.auth share same credentials by default, meaning
|
| -a user needs to authenticate only once to use them all. Credentials are cached
|
| -in ~/.config/chrome_infra/auth/* and reused by all processes running under
|
| -the same user account.
|
| -*/
|
| -package auth
|
| -
|
| -import (
|
| - "crypto/sha1"
|
| - "encoding/hex"
|
| - "errors"
|
| - "fmt"
|
| - "io/ioutil"
|
| - "net/http"
|
| - "os"
|
| - "os/user"
|
| - "path/filepath"
|
| - "sort"
|
| - "sync"
|
| -
|
| - "golang.org/x/net/context"
|
| - "google.golang.org/cloud/compute/metadata"
|
| -
|
| - "infra/libs/auth/internal"
|
| - "infra/libs/logging"
|
| -)
|
| -
|
| -var (
|
| - // ErrLoginRequired is returned by Transport() in case long term credentials
|
| - // are not cached and the user must go through interactive login.
|
| - ErrLoginRequired = errors.New("Interactive login is required")
|
| -
|
| - // ErrInsufficientAccess is returned by Login() or Transport() if access_token
|
| - // can't be minted for given OAuth scopes. For example if GCE instance wasn't
|
| - // granted access to requested scopes when it was created.
|
| - ErrInsufficientAccess = internal.ErrInsufficientAccess
|
| -)
|
| -
|
| -// Known Google API OAuth scopes.
|
| -const (
|
| - OAuthScopeEmail = "https://www.googleapis.com/auth/userinfo.email"
|
| -)
|
| -
|
| -// Method defines a method to use to obtain OAuth access_token.
|
| -type Method string
|
| -
|
| -// Supported authentication methods.
|
| -const (
|
| - // AutoSelectMethod can be used to allow the library to pick a method most
|
| - // appropriate for current execution environment. It will search for a private
|
| - // key for a service account, then (if running on GCE) will try to query GCE
|
| - // metadata server, and only then pick UserCredentialsMethod that requires
|
| - // interaction with a user.
|
| - AutoSelectMethod Method = ""
|
| -
|
| - // UserCredentialsMethod is used for interactive OAuth 3-legged login flow.
|
| - UserCredentialsMethod Method = "UserCredentialsMethod"
|
| -
|
| - // ServiceAccountMethod is used to authenticate as a service account using
|
| - // a private key.
|
| - ServiceAccountMethod Method = "ServiceAccountMethod"
|
| -
|
| - // GCEMetadataMethod is used on Compute Engine to use tokens provided by
|
| - // Metadata server. See https://cloud.google.com/compute/docs/authentication
|
| - GCEMetadataMethod Method = "GCEMetadataMethod"
|
| -)
|
| -
|
| -// LoginMode is used as enum in AuthenticatedClient function.
|
| -type LoginMode string
|
| -
|
| -const (
|
| - // InteractiveLogin is passed to AuthenticatedClient to forcefully rerun full
|
| - // login flow and cache resulting tokens. Used by 'login' CLI command.
|
| - InteractiveLogin LoginMode = "InteractiveLogin"
|
| -
|
| - // SilentLogin is passed to AuthenticatedClient if authentication must be used
|
| - // and it is NOT OK to run interactive login flow to get the tokens. The call
|
| - // will fail with ErrLoginRequired error if there's no cached tokens. Should
|
| - // normally be used by all CLI tools that need to use authentication.
|
| - SilentLogin LoginMode = "SilentLogin"
|
| -
|
| - // OptionalLogin is passed to AuthenticatedClient if it is OK not to use
|
| - // authentication if there are no cached credentials. Interactive login will
|
| - // never be called, default unauthenticated client will be returned instead.
|
| - // Should be used by CLI tools where authentication is optional.
|
| - OptionalLogin LoginMode = "OptionalLogin"
|
| -)
|
| -
|
| -// Options are used by NewAuthenticator call. All fields are optional and have
|
| -// sane default values.
|
| -type Options struct {
|
| - // Method defaults to AutoSelectMethod.
|
| - Method Method
|
| - // Scopes is a list of OAuth scopes to request, defaults to [OAuthScopeEmail].
|
| - Scopes []string
|
| -
|
| - // ClientID is OAuth client_id to use with UserCredentialsMethod.
|
| - // Default: provided by DefaultClient().
|
| - ClientID string
|
| - // ClientID is OAuth client_secret to use with UserCredentialsMethod.
|
| - // Default: provided by DefaultClient().
|
| - ClientSecret string
|
| -
|
| - // ServiceAccountJSONPath is a path to a JSON blob with a private key to use
|
| - // with ServiceAccountMethod. See the "Credentials" page under "APIs & Auth"
|
| - // for your project at Cloud Console.
|
| - // Default: ~/.config/chrome_infra/auth/service_account.json.
|
| - ServiceAccountJSONPath string
|
| -
|
| - // GCEAccountName is an account name to query to fetch token for from metadata
|
| - // server when GCEMetadataMethod is used. If given account wasn't granted
|
| - // required set of scopes during instance creation time, Transport() call
|
| - // fails with ErrInsufficientAccess.
|
| - // Default: "default" account.
|
| - GCEAccountName string
|
| -
|
| - // Context carries the underlying HTTP transport to use. If context is not
|
| - // provided or doesn't contain the transport, http.DefaultTransport will be
|
| - // used. Context will also be used to grab a logger if passed Logger is nil.
|
| - Context context.Context
|
| -
|
| - // Logger is used to write log messages. If nil, extract it from the context.
|
| - Logger logging.Logger
|
| -}
|
| -
|
| -// Authenticator is a factory for http.RoundTripper objects that know how to use
|
| -// cached OAuth credentials. Authenticator also knows how to run interactive
|
| -// login flow, if required.
|
| -type Authenticator interface {
|
| - // Transport returns http.RoundTripper that adds authentication details to
|
| - // each request. An interactive authentication flow (if required) must be
|
| - // complete before making a transport, otherwise ErrLoginRequired is returned.
|
| - // Returned transport object can be safely reused across many http.Client's.
|
| - Transport() (http.RoundTripper, error)
|
| -
|
| - // Login perform an interaction with the user to get a long term refresh token
|
| - // and cache it. Blocks for user input, can use stdin. Returns ErrNoTerminal
|
| - // if interaction with a user is required, but the process is not running
|
| - // under a terminal. It overwrites currently cached credentials, if any.
|
| - Login() error
|
| -
|
| - // PurgeCredentialsCache removes cached tokens.
|
| - PurgeCredentialsCache() error
|
| -}
|
| -
|
| -// NewAuthenticator returns a new instance of Authenticator given its options.
|
| -func NewAuthenticator(opts Options) Authenticator {
|
| - // Add default scope, sort scopes.
|
| - if len(opts.Scopes) == 0 {
|
| - opts.Scopes = []string{OAuthScopeEmail}
|
| - }
|
| - tmp := make([]string, len(opts.Scopes))
|
| - copy(tmp, opts.Scopes)
|
| - sort.Strings(tmp)
|
| - opts.Scopes = tmp
|
| -
|
| - // Fill in blanks with default values.
|
| - if opts.ClientID == "" || opts.ClientSecret == "" {
|
| - opts.ClientID, opts.ClientSecret = DefaultClient()
|
| - }
|
| - if opts.ServiceAccountJSONPath == "" {
|
| - opts.ServiceAccountJSONPath = filepath.Join(SecretsDir(), "service_account.json")
|
| - }
|
| - if opts.GCEAccountName == "" {
|
| - opts.GCEAccountName = "default"
|
| - }
|
| - if opts.Context == nil {
|
| - opts.Context = context.Background()
|
| - }
|
| - if opts.Logger == nil {
|
| - opts.Logger = logging.Get(opts.Context)
|
| - }
|
| -
|
| - // See ensureInitialized for the rest of the initialization.
|
| - auth := &authenticatorImpl{opts: &opts, log: opts.Logger}
|
| - auth.transport = &authTransport{
|
| - parent: auth,
|
| - base: internal.TransportFromContext(opts.Context),
|
| - log: opts.Logger,
|
| - }
|
| - return auth
|
| -}
|
| -
|
| -// AuthenticatedClient performs login (if requested) and returns http.Client.
|
| -// See documentation for 'mode' for more details.
|
| -func AuthenticatedClient(mode LoginMode, auth Authenticator) (*http.Client, error) {
|
| - if mode == InteractiveLogin {
|
| - if err := auth.PurgeCredentialsCache(); err != nil {
|
| - return nil, err
|
| - }
|
| - }
|
| - transport, err := auth.Transport()
|
| - if err == nil {
|
| - return &http.Client{Transport: transport}, nil
|
| - }
|
| - if err != ErrLoginRequired || mode == SilentLogin {
|
| - return nil, err
|
| - }
|
| - if mode == OptionalLogin {
|
| - return http.DefaultClient, nil
|
| - }
|
| - if mode != InteractiveLogin {
|
| - return nil, fmt.Errorf("Invalid mode argument: %s", mode)
|
| - }
|
| - if err = auth.Login(); err != nil {
|
| - return nil, err
|
| - }
|
| - if transport, err = auth.Transport(); err != nil {
|
| - return nil, err
|
| - }
|
| - return &http.Client{Transport: transport}, nil
|
| -}
|
| -
|
| -////////////////////////////////////////////////////////////////////////////////
|
| -// Authenticator implementation.
|
| -
|
| -type authenticatorImpl struct {
|
| - // Immutable members.
|
| - opts *Options
|
| - transport http.RoundTripper
|
| - log logging.Logger
|
| -
|
| - // Mutable members.
|
| - lock sync.Mutex
|
| - cache *tokenCache
|
| - provider internal.TokenProvider
|
| - err error
|
| - token internal.Token
|
| -}
|
| -
|
| -func (a *authenticatorImpl) Transport() (http.RoundTripper, error) {
|
| - a.lock.Lock()
|
| - defer a.lock.Unlock()
|
| -
|
| - err := a.ensureInitialized()
|
| - if err != nil {
|
| - return nil, err
|
| - }
|
| -
|
| - // No cached token and token provider requires interaction with a user: need
|
| - // to login. Only non-interactive token providers are allowed to mint tokens
|
| - // on the fly, see refreshToken.
|
| - if a.token == nil && a.provider.RequiresInteraction() {
|
| - return nil, ErrLoginRequired
|
| - }
|
| - return a.transport, nil
|
| -}
|
| -
|
| -func (a *authenticatorImpl) Login() error {
|
| - a.lock.Lock()
|
| - defer a.lock.Unlock()
|
| -
|
| - err := a.ensureInitialized()
|
| - if err != nil {
|
| - return err
|
| - }
|
| - if !a.provider.RequiresInteraction() {
|
| - return nil
|
| - }
|
| -
|
| - // Create initial token. This may require interaction with a user.
|
| - a.token, err = a.provider.MintToken()
|
| - if err != nil {
|
| - return err
|
| - }
|
| -
|
| - // Store the initial token in the cache. Don't abort if it fails, the token
|
| - // is still usable from the memory.
|
| - if err = a.cacheToken(a.token); err != nil {
|
| - a.log.Warningf("auth: failed to write token to cache: %v", err)
|
| - }
|
| -
|
| - return nil
|
| -}
|
| -
|
| -func (a *authenticatorImpl) PurgeCredentialsCache() error {
|
| - a.lock.Lock()
|
| - defer a.lock.Unlock()
|
| -
|
| - if err := a.ensureInitialized(); err != nil {
|
| - return err
|
| - }
|
| - if err := a.cache.clear(); err != nil {
|
| - return err
|
| - }
|
| - a.token = nil
|
| - return nil
|
| -}
|
| -
|
| -////////////////////////////////////////////////////////////////////////////////
|
| -// Authenticator private methods.
|
| -
|
| -// ensureInitialized is supposed to be called under the lock.
|
| -func (a *authenticatorImpl) ensureInitialized() error {
|
| - if a.err != nil || a.provider != nil {
|
| - return a.err
|
| - }
|
| -
|
| - // selectDefaultMethod may do heavy calls, call it lazily here rather than in
|
| - // NewAuthenticator.
|
| - if a.opts.Method == AutoSelectMethod {
|
| - a.opts.Method = selectDefaultMethod(a.opts)
|
| - }
|
| - a.log.Infof("auth: using %s", a.opts.Method)
|
| - a.provider, a.err = makeTokenProvider(a.opts)
|
| - if a.err != nil {
|
| - return a.err
|
| - }
|
| -
|
| - // Setup the cache only when Method is known, cache filename depends on it.
|
| - a.cache = &tokenCache{
|
| - path: filepath.Join(SecretsDir(), cacheFileName(a.opts)+".tok"),
|
| - log: a.log,
|
| - }
|
| -
|
| - // Broken token cache is not a fatal error. So just log it and forget, a new
|
| - // token will be minted.
|
| - var err error
|
| - a.token, err = a.readTokenCache()
|
| - if err != nil {
|
| - a.log.Warningf("auth: failed to read token from cache: %v", err)
|
| - }
|
| - return nil
|
| -}
|
| -
|
| -// readTokenCache may be called with a.lock held or not held. It works either way.
|
| -func (a *authenticatorImpl) readTokenCache() (internal.Token, error) {
|
| - // 'read' returns (nil, nil) if cache is empty.
|
| - buf, err := a.cache.read()
|
| - if err != nil || buf == nil {
|
| - return nil, err
|
| - }
|
| - token, err := a.provider.UnmarshalToken(buf)
|
| - if err != nil {
|
| - return nil, err
|
| - }
|
| - return token, nil
|
| -}
|
| -
|
| -// cacheToken may be called with a.lock held or not held. It works either way.
|
| -func (a *authenticatorImpl) cacheToken(tok internal.Token) error {
|
| - buf, err := a.provider.MarshalToken(tok)
|
| - if err != nil {
|
| - return err
|
| - }
|
| - return a.cache.write(buf)
|
| -}
|
| -
|
| -// currentToken lock a.lock inside. It MUST NOT be called when a.lock is held.
|
| -func (a *authenticatorImpl) currentToken() internal.Token {
|
| - // TODO(vadimsh): Test with go test -race. The lock may be unnecessary.
|
| - a.lock.Lock()
|
| - defer a.lock.Unlock()
|
| - return a.token
|
| -}
|
| -
|
| -// refreshToken compares current token to 'prev' and launches token refresh
|
| -// procedure if they still match. Returns a refreshed token (if a refresh
|
| -// procedure happened) or the current token (i.e. if it's different from prev).
|
| -// Acts as "Compare-And-Swap" where "Swap" is a token refresh procedure.
|
| -func (a *authenticatorImpl) refreshToken(prev internal.Token) (internal.Token, error) {
|
| - // Refresh the token under the lock.
|
| - tok, cache, err := func() (internal.Token, bool, error) {
|
| - a.lock.Lock()
|
| - defer a.lock.Unlock()
|
| -
|
| - // Some other goroutine already updated the token, just return the token.
|
| - if a.token != nil && !a.token.Equals(prev) {
|
| - return a.token, false, nil
|
| - }
|
| -
|
| - // Rescan the cache. Maybe some other process updated the token.
|
| - cached, err := a.readTokenCache()
|
| - if err == nil && cached != nil && !cached.Equals(prev) && !cached.Expired() {
|
| - a.log.Infof("auth: some other process put refreshed token in the cache")
|
| - a.token = cached
|
| - return a.token, false, nil
|
| - }
|
| -
|
| - // Mint a new token or refresh the existing one.
|
| - if a.token == nil {
|
| - // Can't do user interaction outside of Login.
|
| - if a.provider.RequiresInteraction() {
|
| - return nil, false, ErrLoginRequired
|
| - }
|
| - a.log.Infof("auth: minting a new token")
|
| - a.token, err = a.provider.MintToken()
|
| - if err != nil {
|
| - a.log.Warningf("auth: failed to mint a token: %v", err)
|
| - return nil, false, err
|
| - }
|
| - } else {
|
| - a.log.Infof("auth: refreshing the token")
|
| - a.token, err = a.provider.RefreshToken(a.token)
|
| - if err != nil {
|
| - a.log.Warningf("auth: failed to refresh the token: %v", err)
|
| - return nil, false, err
|
| - }
|
| - }
|
| - return a.token, true, nil
|
| - }()
|
| -
|
| - if err != nil {
|
| - return nil, err
|
| - }
|
| -
|
| - // Store the new token in the cache outside the lock, no need for callers to
|
| - // wait for this. Do not die if failed, token is still usable from the memory.
|
| - if cache {
|
| - if err = a.cacheToken(tok); err != nil {
|
| - a.log.Warningf("auth: failed to write refreshed token to the cache: %v", err)
|
| - }
|
| - }
|
| - return tok, nil
|
| -}
|
| -
|
| -////////////////////////////////////////////////////////////////////////////////
|
| -// authTransport implementation.
|
| -
|
| -// TODO(vadimsh): Support CancelRequest if underlying transport supports it.
|
| -// It's tricky. http.Client uses type cast to figure out whether transport
|
| -// supports request cancellation or not. So new authTransportWithCancelation
|
| -// should be used when parent transport provides CancelRequest. Also
|
| -// authTransport should keep a mapping between original http.Request objects
|
| -// and ones with access tokens attached (to know what to pass to
|
| -// parent transport CancelRequest).
|
| -
|
| -type authTransport struct {
|
| - parent *authenticatorImpl
|
| - base http.RoundTripper
|
| - log logging.Logger
|
| -}
|
| -
|
| -// RoundTrip appends authorization details to the request.
|
| -func (t *authTransport) RoundTrip(req *http.Request) (resp *http.Response, err error) {
|
| - tok := t.parent.currentToken()
|
| - if tok == nil || tok.Expired() {
|
| - tok, err = t.parent.refreshToken(tok)
|
| - if err != nil {
|
| - return
|
| - }
|
| - if tok == nil || tok.Expired() {
|
| - err = fmt.Errorf("auth: failed to refresh the token")
|
| - return
|
| - }
|
| - }
|
| - clone := *req
|
| - clone.Header = make(http.Header)
|
| - for k, v := range req.Header {
|
| - clone.Header[k] = v
|
| - }
|
| - for k, v := range tok.RequestHeaders() {
|
| - clone.Header.Set(k, v)
|
| - }
|
| - return t.base.RoundTrip(&clone)
|
| -}
|
| -
|
| -////////////////////////////////////////////////////////////////////////////////
|
| -// tokenCache implementation.
|
| -
|
| -type tokenCache struct {
|
| - path string
|
| - log logging.Logger
|
| - lock sync.Mutex
|
| -}
|
| -
|
| -func (c *tokenCache) read() (buf []byte, err error) {
|
| - c.lock.Lock()
|
| - defer c.lock.Unlock()
|
| - c.log.Infof("auth: reading token from %s", c.path)
|
| - buf, err = ioutil.ReadFile(c.path)
|
| - if err != nil && os.IsNotExist(err) {
|
| - err = nil
|
| - }
|
| - return
|
| -}
|
| -
|
| -func (c *tokenCache) write(buf []byte) error {
|
| - c.lock.Lock()
|
| - defer c.lock.Unlock()
|
| - c.log.Infof("auth: writing token to %s", c.path)
|
| - err := os.MkdirAll(filepath.Dir(c.path), 0700)
|
| - if err != nil {
|
| - return err
|
| - }
|
| - // TODO(vadimsh): Make it atomic across multiple processes.
|
| - return ioutil.WriteFile(c.path, buf, 0600)
|
| -}
|
| -
|
| -func (c *tokenCache) clear() error {
|
| - c.lock.Lock()
|
| - defer c.lock.Unlock()
|
| - err := os.Remove(c.path)
|
| - if err != nil && !os.IsNotExist(err) {
|
| - return err
|
| - }
|
| - return nil
|
| -}
|
| -
|
| -////////////////////////////////////////////////////////////////////////////////
|
| -// Utility functions.
|
| -
|
| -func cacheFileName(opts *Options) string {
|
| - // Construct a name of cache file from data that identifies requested
|
| - // credential, to allow multiple differently configured instances of
|
| - // Authenticator to coexist.
|
| - sum := sha1.New()
|
| - sum.Write([]byte(opts.Method))
|
| - sum.Write([]byte{0})
|
| - sum.Write([]byte(opts.ClientID))
|
| - sum.Write([]byte{0})
|
| - sum.Write([]byte(opts.ClientSecret))
|
| - sum.Write([]byte{0})
|
| - for _, scope := range opts.Scopes {
|
| - sum.Write([]byte(scope))
|
| - sum.Write([]byte{0})
|
| - }
|
| - sum.Write([]byte(opts.GCEAccountName))
|
| - return hex.EncodeToString(sum.Sum(nil))[:16]
|
| -}
|
| -
|
| -// selectDefaultMethod is mocked in tests.
|
| -var selectDefaultMethod = func(opts *Options) Method {
|
| - if opts.ServiceAccountJSONPath != "" {
|
| - info, _ := os.Stat(opts.ServiceAccountJSONPath)
|
| - if info != nil && info.Mode().IsRegular() {
|
| - return ServiceAccountMethod
|
| - }
|
| - }
|
| - if metadata.OnGCE() {
|
| - return GCEMetadataMethod
|
| - }
|
| - return UserCredentialsMethod
|
| -}
|
| -
|
| -// secretsDir is mocked in tests. Called by publicly visible SecretsDir().
|
| -var secretsDir = func() string {
|
| - usr, err := user.Current()
|
| - if err != nil {
|
| - panic(err.Error())
|
| - }
|
| - // TODO(vadimsh): On Windows use SHGetFolderPath with CSIDL_LOCAL_APPDATA to
|
| - // locate a directory to store app files.
|
| - return filepath.Join(usr.HomeDir, ".config", "chrome_infra", "auth")
|
| -}
|
| -
|
| -// makeTokenProvider is mocked in tests. Called by ensureInitialized.
|
| -var makeTokenProvider = func(opts *Options) (internal.TokenProvider, error) {
|
| - switch opts.Method {
|
| - case UserCredentialsMethod:
|
| - return internal.NewUserAuthTokenProvider(
|
| - opts.Context,
|
| - opts.ClientID,
|
| - opts.ClientSecret,
|
| - opts.Scopes)
|
| - case ServiceAccountMethod:
|
| - return internal.NewServiceAccountTokenProvider(
|
| - opts.Context,
|
| - opts.ServiceAccountJSONPath,
|
| - opts.Scopes)
|
| - case GCEMetadataMethod:
|
| - return internal.NewGCETokenProvider(
|
| - opts.GCEAccountName,
|
| - opts.Scopes)
|
| - default:
|
| - return nil, fmt.Errorf("Unrecognized authentication method: %s", opts.Method)
|
| - }
|
| -}
|
| -
|
| -// DefaultClient returns OAuth client_id and client_secret to use for 3 legged
|
| -// OAuth flow. Note that client_secret is not really a secret since it's
|
| -// hardcoded into the source code (and binaries). It's totally fine, as long
|
| -// as it's callback URI is configured to be 'localhost'. If someone decides to
|
| -// reuse such client_secret they have to run something on user's local machine
|
| -// to get the refresh_token.
|
| -func DefaultClient() (clientID string, clientSecret string) {
|
| - clientID = "446450136466-2hr92jrq8e6i4tnsa56b52vacp7t3936.apps.googleusercontent.com"
|
| - clientSecret = "uBfbay2KCy9t4QveJ-dOqHtp"
|
| - return
|
| -}
|
| -
|
| -// SecretsDir returns an absolute path to a directory to keep secret files in.
|
| -func SecretsDir() string {
|
| - return secretsDir()
|
| -}
|
|
|