Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(72)

Unified Diff: go/src/infra/libs/auth/auth.go

Issue 1153883002: go: infra/libs/* now live in luci-go. (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: move the rest too Created 5 years, 7 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « go/src/infra/gae/libs/meta/eg.go ('k') | go/src/infra/libs/auth/auth.infra_testing » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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()
-}
« no previous file with comments | « go/src/infra/gae/libs/meta/eg.go ('k') | go/src/infra/libs/auth/auth.infra_testing » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698