go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/auth/authctx/context.go (about)

     1  // Copyright 2019 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 authctx allows to run subprocesses in an environment with ambient
    16  // auth.
    17  //
    18  // Supports setting up an auth context for LUCI tools, gsutil and gcloud, Git,
    19  // Docker and Firebase.
    20  //
    21  // Git auth depends on presence of Git wrapper and git-credential-luci in PATH.
    22  // Docker auth depends on presence of docker-credential-luci in PATH.
    23  package authctx
    24  
    25  import (
    26  	"context"
    27  	"encoding/json"
    28  	"fmt"
    29  	"io/ioutil"
    30  	"net"
    31  	"os"
    32  	"path/filepath"
    33  	"runtime"
    34  	"strings"
    35  	"sync"
    36  
    37  	"go.chromium.org/luci/common/errors"
    38  	"go.chromium.org/luci/common/logging"
    39  	"go.chromium.org/luci/common/system/environ"
    40  
    41  	"go.chromium.org/luci/lucictx"
    42  
    43  	"go.chromium.org/luci/auth"
    44  	"go.chromium.org/luci/auth/integration/devshell"
    45  	"go.chromium.org/luci/auth/integration/firebase"
    46  	"go.chromium.org/luci/auth/integration/gcemeta"
    47  	"go.chromium.org/luci/auth/integration/gsutil"
    48  	"go.chromium.org/luci/auth/integration/localauth"
    49  )
    50  
    51  // Context knows how to prepare an environment with ambient authentication for
    52  // various tools: LUCI, gsutil, Docker, Git, Firebase.
    53  //
    54  // 'Launch' launches a bunch of local HTTP servers and writes a bunch of
    55  // configuration files that point to these servers. 'Export' then exposes
    56  // location of these configuration files to subprocesses, so they can discover
    57  // local HTTP servers and use them to mint tokens.
    58  type Context struct {
    59  	// ID is used in logs, filenames and in LUCI_CONTEXT (if we launch a new one).
    60  	//
    61  	// Usually a logical account name associated with this context, e.g. "task" or
    62  	// "system".
    63  	ID string
    64  
    65  	// Options define how to build the root authenticator.
    66  	//
    67  	// This authenticator (perhaps indirectly through LUCI_CONTEXT created in
    68  	// 'Launch') will be used by all other auth helpers to grab access tokens.
    69  	//
    70  	// If Options.Method is LUCIContextMethod, indicating there's some existing
    71  	// LUCI_CONTEXT with "local_auth" section we should use, and service account
    72  	// impersonation is not requested (Options.ActAsServiceAccount == "") the
    73  	// existing LUCI_CONTEXT is reused. Otherwise launches a new local_auth server
    74  	// (that uses given auth options to mint tokens) and puts its location into
    75  	// the new LUCI_CONTEXT. Either way, subprocesses launched with an environment
    76  	// modified by 'Export' will see a functional LUCI_CONTEXT.
    77  	//
    78  	// When reusing an existing LUCI_CONTEXT, subprocesses inherit all OAuth
    79  	// scopes permissible there.
    80  	Options auth.Options
    81  
    82  	// ExposeSystemAccount indicates if this authentication context should also
    83  	// expose non-default "system" logical LUCI account (using the same
    84  	// credentials as the default account).
    85  	//
    86  	// This is an advanced feature used to emulate Swarming environment.
    87  	ExposeSystemAccount bool
    88  
    89  	// EnableGitAuth enables authentication for Git subprocesses.
    90  	//
    91  	// Assumes 'git' binary is actually gitwrapper and that 'git-credential-luci'
    92  	// binary is in PATH.
    93  	//
    94  	// Requires "https://www.googleapis.com/auth/gerritcodereview" OAuth scope.
    95  	EnableGitAuth bool
    96  
    97  	// EnableDockerAuth enables authentication for Docker.
    98  	//
    99  	// Assumes 'docker-credential-luci' is in PATH.
   100  	//
   101  	// Requires Google Storage OAuth scopes. See GCR docs for more info.
   102  	EnableDockerAuth bool
   103  
   104  	// EnableDevShell enables DevShell server and gsutil auth shim.
   105  	//
   106  	// They are used to make gsutil and gcloud use LUCI authentication.
   107  	//
   108  	// On Windows only gsutil auth shim is enabled, since enabling DevShell there
   109  	// triggers bugs in gsutil. See https://crbug.com/788058#c14.
   110  	//
   111  	// Requires Google Storage OAuth scopes. See GS docs for more info.
   112  	//
   113  	// TODO(vadimsh): Delete this method if EnableGCEEmulation works everywhere.
   114  	EnableDevShell bool
   115  
   116  	// EnableGCEEmulation enables emulation of GCE instance environment.
   117  	//
   118  	// Overrides EnableDevShell if used. Will likely completely replace
   119  	// EnableDevShell in the near future.
   120  	//
   121  	// It does multiple things by setting environment variables and writing config
   122  	// files:
   123  	//   * Creates new empty CLOUDSDK_CONFIG directory, to make sure we don't
   124  	//     reuse existing gcloud cache.
   125  	//   * Creates new BOTO_CONFIG, telling gsutil to use new empty state dir.
   126  	//   * Launches a local server that imitates GCE metadata server.
   127  	//   * Tells gcloud, gsutil and various Go and Python libraries to use this
   128  	//     server by setting env vars like GCE_METADATA_HOST (and a bunch more).
   129  	//
   130  	// This tricks gcloud, gsutil and various Go and Python libraries that use
   131  	// Application Default Credentials into believing they run on GCE so that
   132  	// they request OAuth2 tokens via GCE metadata server (which is implemented by
   133  	// us).
   134  	//
   135  	// This is not a foolproof way: nothing prevents clients from ignoring env
   136  	// vars and hitting metadata.google.internal directly. But most clients
   137  	// respect env vars we set.
   138  	EnableGCEEmulation bool
   139  
   140  	// EnableFirebaseAuth enables Firebase auth shim.
   141  	//
   142  	// It is used to make Firebase use LUCI authentication.
   143  	//
   144  	// Requires "https://www.googleapis.com/auth/firebase" OAuth scope.
   145  	EnableFirebaseAuth bool
   146  
   147  	// KnownGerritHosts is list of Gerrit hosts to force git authentication for.
   148  	//
   149  	// By default public hosts are accessed anonymously, and the anonymous access
   150  	// has very low quota. Context needs to know all such hostnames in advance to
   151  	// be able to force authenticated access to them.
   152  	KnownGerritHosts []string
   153  
   154  	localAuth     *lucictx.LocalAuth  // non-nil when running localauth.Server
   155  	tmpDir        string              // non empty if we created a new temp dir
   156  	authenticator *auth.Authenticator // used by in-process helpers
   157  	anonymous     bool                // true if not associated with any account
   158  	email         string              // an account email or "" for anonymous
   159  	luciSrv       *localauth.Server   // non-nil if we launched a LUCI_CONTEXT subcontext
   160  
   161  	gitHome string // custom HOME for git or "" if not using git auth
   162  
   163  	dockerConfig string // location for Docker configuration files
   164  	dockerTmpDir string // location for Docker temporary files
   165  
   166  	gsutilSrv *gsutil.Server // gsutil auth shim server
   167  
   168  	// Note: these fields are used in both EnableGCEEmulation and EnableDevShell
   169  	// modes.
   170  	gsutilState string // path to a context-managed state directory
   171  	gsutilBoto  string // path to a generated .boto file
   172  
   173  	devShellSrv  *devshell.Server // DevShell server instance
   174  	devShellAddr *net.TCPAddr     // address local DevShell instance is listening on
   175  
   176  	gcemetaSrv    *gcemeta.Server // fake GCE metadata server
   177  	gcemetaAddr   string          // "host:port" address of the fake metadata server
   178  	gcloudConfDir string          // directory with gcloud configs
   179  
   180  	firebaseSrv      *firebase.Server // firebase auth shim server
   181  	firebaseTokenURL string           // URL to get firebase auth token from
   182  }
   183  
   184  // Launch launches this auth context. It must be called before any other method.
   185  //
   186  // It launches various local server and prepares various configs, by putting
   187  // them into tempDir which may be "" to use some new ioutil.TempDir.
   188  //
   189  // The given context.Context is used for logging and to pick up the initial
   190  // ambient authentication (per auth.NewAuthenticator contract, see its docs).
   191  //
   192  // To run a subprocess within this new auth context use 'Export' to modify
   193  // an environ for a new process.
   194  func (ac *Context) Launch(ctx context.Context, tempDir string) (err error) {
   195  	// EnableGCEEmulation provides a superset of EnableDevShell features. No need
   196  	// to have both enabled at the same time (they also conflict with each other).
   197  	if ac.EnableGCEEmulation {
   198  		ac.EnableDevShell = false
   199  	}
   200  
   201  	defer func() {
   202  		if err != nil {
   203  			ac.Close(ctx)
   204  		}
   205  	}()
   206  
   207  	if tempDir == "" {
   208  		ac.tmpDir, err = ioutil.TempDir("", "luci")
   209  		if err != nil {
   210  			return errors.Annotate(err, "failed to create a temp directory").Err()
   211  		}
   212  		tempDir = ac.tmpDir
   213  	}
   214  
   215  	// Make a generator that will be used to generate tokens for subprocesses
   216  	// that request them via LUCI_CONTEXT protocol or via GCE metadata emulation.
   217  	tokens := auth.NewTokenGenerator(ctx, ac.Options)
   218  	opts := tokens.Options() // normalized, with opts.Method populated
   219  
   220  	// Construct the OAuth2 authenticator to be used directly by the helpers
   221  	// hosted in the current process (devshell, gsutil, firebase) using scopes
   222  	// passed via ac.Options. Out-of-process helpers (git, docker) will use
   223  	// `tokens` via the LUCI_CONTEXT protocol or GCE metadata server emulation.
   224  	ac.authenticator, err = tokens.Authenticator(opts.Scopes, "")
   225  	if err != nil {
   226  		return errors.Annotate(err, "failed to construct authenticator for %q account", ac.ID).Err()
   227  	}
   228  
   229  	// Figure out what email is associated with this account (if any).
   230  	ac.email, err = ac.authenticator.GetEmail()
   231  	switch {
   232  	case err == auth.ErrLoginRequired:
   233  		// This context is not associated with any account. This happens when
   234  		// running Swarming tasks without service account specified or running
   235  		// locally without doing 'luci-auth login' first.
   236  		ac.anonymous = true
   237  	case err != nil:
   238  		return errors.Annotate(err, "failed to get email of %q account", ac.ID).Err()
   239  	}
   240  
   241  	// Check whether we are allowed to inherit the existing LUCI_CONTEXT. We do it
   242  	// if 'opts' indicate to use LUCI_CONTEXT and do NOT use impersonation. When
   243  	// impersonating, we must launch a new auth server to actually perform it
   244  	// there.
   245  	//
   246  	// If we can't reuse the existing LUCI_CONTEXT, launch a new one (deriving
   247  	// a new context.Context with it).
   248  	//
   249  	// If there's no auth credentials at all, do not launch any LUCI_CONTEXT (it
   250  	// is impossible without credentials). Subprocesses will discover lack of
   251  	// ambient credentials on their own and fail (or proceed) appropriately.
   252  	canInherit := opts.Method == auth.LUCIContextMethod && opts.ActAsServiceAccount == "" && !ac.ExposeSystemAccount
   253  	if !canInherit && !ac.anonymous {
   254  		if ac.luciSrv, ac.localAuth, err = launchSrv(ctx, tokens, ac.ID, ac.ExposeSystemAccount); err != nil {
   255  			return errors.Annotate(err, "failed to launch local auth server for %q account", ac.ID).Err()
   256  		}
   257  	}
   258  
   259  	// Now setup various credential helpers (they all mutate 'ac' and return
   260  	// annotated errors).
   261  	if ac.EnableGitAuth {
   262  		if err := ac.setupGitAuth(tempDir); err != nil {
   263  			return err
   264  		}
   265  	}
   266  	if ac.EnableDockerAuth {
   267  		if err := ac.setupDockerAuth(tempDir); err != nil {
   268  			return err
   269  		}
   270  	}
   271  	if ac.EnableDevShell && !ac.anonymous {
   272  		if err := ac.setupDevShellAuth(ctx, tempDir); err != nil {
   273  			return err
   274  		}
   275  	}
   276  	if ac.EnableGCEEmulation {
   277  		if err := ac.setupGCEEmulationAuth(ctx, tokens, tempDir); err != nil {
   278  			return err
   279  		}
   280  	}
   281  	if ac.EnableFirebaseAuth && !ac.anonymous {
   282  		if err := ac.setupFirebaseAuth(ctx); err != nil {
   283  			return err
   284  		}
   285  	}
   286  
   287  	return nil
   288  }
   289  
   290  // Close stops this context, cleaning up after it.
   291  //
   292  // The given context.Context is used for deadlines and for logging.
   293  //
   294  // The auth context is not usable after this call. Logs errors inside (there's
   295  // nothing caller can do about them anyway).
   296  func (ac *Context) Close(ctx context.Context) {
   297  	// Stop all the servers in parallel.
   298  	wg := sync.WaitGroup{}
   299  	stop := func(what string, srv interface{ Stop(context.Context) error }) {
   300  		wg.Add(1)
   301  		go func() {
   302  			defer wg.Done()
   303  			if err := srv.Stop(ctx); err != nil {
   304  				logging.Errorf(ctx, "Failed to stop %s server for %q account: %s", what, ac.ID, err)
   305  			}
   306  		}()
   307  	}
   308  	// Note: can't move != nil check into stop(...) because 'srv' becomes
   309  	// a "typed nil interface", which is not nil itself.
   310  	if ac.luciSrv != nil {
   311  		stop("local auth", ac.luciSrv)
   312  	}
   313  	if ac.gsutilSrv != nil {
   314  		stop("gsutil shim", ac.gsutilSrv)
   315  	}
   316  	if ac.devShellSrv != nil {
   317  		stop("devshell", ac.devShellSrv)
   318  	}
   319  	if ac.gcemetaSrv != nil {
   320  		stop("fake GCE metadata server", ac.gcemetaSrv)
   321  	}
   322  	if ac.firebaseSrv != nil {
   323  		stop("firebase shim", ac.firebaseSrv)
   324  	}
   325  	wg.Wait()
   326  
   327  	// Cleanup the rest of the garbage.
   328  	cleanup := func(what, where string) {
   329  		if where != "" {
   330  			if err := os.RemoveAll(where); err != nil {
   331  				logging.Errorf(ctx, "Failed to clean up %s for %q account at [%s]: %s", what, ac.ID, where, err)
   332  			}
   333  		}
   334  	}
   335  	cleanup("git HOME", ac.gitHome)
   336  	cleanup("gsutil state", ac.gsutilState)
   337  	cleanup("gcloud config dir", ac.gcloudConfDir)
   338  	cleanup("docker configs", ac.dockerConfig)
   339  	cleanup("docker temp dir", ac.dockerTmpDir)
   340  	cleanup("created temp dir", ac.tmpDir)
   341  
   342  	// And finally reset the state as if nothing happened.
   343  	ac.localAuth = nil
   344  	ac.tmpDir = ""
   345  	ac.authenticator = nil
   346  	ac.anonymous = false
   347  	ac.email = ""
   348  	ac.luciSrv = nil
   349  	ac.gitHome = ""
   350  	ac.dockerConfig = ""
   351  	ac.dockerTmpDir = ""
   352  	ac.gsutilSrv = nil
   353  	ac.gsutilState = ""
   354  	ac.gsutilBoto = ""
   355  	ac.devShellSrv = nil
   356  	ac.devShellAddr = nil
   357  	ac.gcemetaSrv = nil
   358  	ac.gcemetaAddr = ""
   359  	ac.gcloudConfDir = ""
   360  	ac.firebaseSrv = nil
   361  	ac.firebaseTokenURL = ""
   362  }
   363  
   364  // Authenticator returns an authenticator used by this context.
   365  //
   366  // It is the one constructed from Options. It is safe to use it directly.
   367  func (ac *Context) Authenticator() *auth.Authenticator {
   368  	return ac.authenticator
   369  }
   370  
   371  // Export exports details of this context into the environment, so it can
   372  // be inherited by subprocesses that support it.
   373  //
   374  // It does two inter-dependent things:
   375  //  1. Updates LUCI_CONTEXT in 'ctx' so that LUCI tools can use the local
   376  //     token server.
   377  //  2. Mutates 'env' so that various third party tools can also use local
   378  //     tokens.
   379  //
   380  // To successfully launch a subprocess, LUCI_CONTEXT in returned context.Context
   381  // *must* be exported into 'env' (e.g. via lucictx.Export(...) followed by
   382  // SetInEnviron).
   383  func (ac *Context) Export(ctx context.Context, env environ.Env) context.Context {
   384  	// Mutate LUCI_CONTEXT to use localauth.Server{...} launched by us (if any).
   385  	ctx = ac.SetLocalAuth(ctx)
   386  
   387  	if ac.EnableGitAuth {
   388  		env.Set("GIT_TERMINAL_PROMPT", "0")           // no interactive prompts
   389  		env.Set("INFRA_GIT_WRAPPER_HOME", ac.gitHome) // tell gitwrapper about the new HOME
   390  	}
   391  
   392  	if ac.EnableDockerAuth {
   393  		env.Set("DOCKER_CONFIG", ac.dockerConfig)
   394  		env.Set("DOCKER_TMPDIR", ac.dockerTmpDir)
   395  	}
   396  
   397  	if ac.EnableDevShell && !ac.anonymous {
   398  		if ac.devShellAddr != nil {
   399  			env.Set(devshell.EnvKey, fmt.Sprintf("%d", ac.devShellAddr.Port))
   400  		} else {
   401  			// See https://crbug.com/788058#c14.
   402  			logging.Warningf(ctx, "Disabling devshell auth for account %q", ac.ID)
   403  		}
   404  	}
   405  
   406  	if ac.EnableGCEEmulation {
   407  		env.Set("CLOUDSDK_CONFIG", ac.gcloudConfDir)
   408  		if !ac.anonymous {
   409  			// Used by google.auth.compute_engine Python library to grab tokens.
   410  			env.Set("GCE_METADATA_ROOT", ac.gcemetaAddr)
   411  			// Used by google.auth.compute_engine Python library to "ping" metadata srv.
   412  			env.Set("GCE_METADATA_IP", ac.gcemetaAddr)
   413  			// Used by cloud.google.com/go/compute/metadata Go library.
   414  			env.Set("GCE_METADATA_HOST", ac.gcemetaAddr)
   415  		}
   416  	}
   417  
   418  	// Prepare .boto configs if faking Cloud in some way. Do it even if running
   419  	// anonymously, since in this case we want to switch gsutil to run in
   420  	// anonymous mode as well (by forbidding it to use default ~/.boto that may
   421  	// have some credential in it).
   422  	if ac.EnableDevShell || ac.EnableGCEEmulation {
   423  		// Note: gsutilBoto may be empty here if running anonymously in DevShell
   424  		// mode. This is fine, it tells gsutil not to use default ~/.boto.
   425  		env.Set("BOTO_CONFIG", ac.gsutilBoto)
   426  		env.Remove("BOTO_PATH")
   427  	}
   428  
   429  	if ac.EnableFirebaseAuth && !ac.anonymous {
   430  		// This env var is supposed to contain a refresh token. Its presence
   431  		// switches Firebase into "CI mode" where it doesn't try to grab credentials
   432  		// from disk or via gcloud. The actual value doesn't matter, since we
   433  		// replace the endpoint that consumes this token below.
   434  		env.Set("FIREBASE_TOKEN", "ignored-non-empty-value")
   435  		// Instruct Firebase to use the local server for "refreshing" the token.
   436  		// Usually this is "https://www.googleapis.com" and it takes a refresh token
   437  		// and returns an access token. We replace it with a local version that
   438  		// just returns task account access tokens.
   439  		env.Set("FIREBASE_TOKEN_URL", ac.firebaseTokenURL)
   440  	}
   441  
   442  	return ctx
   443  }
   444  
   445  // SetLocalAuth updates `local_auth` section of LUCI_CONTEXT.
   446  //
   447  // Note that this would allow LUCI libraries to use this auth context, but
   448  // other software (gsutil, gcloud, firebase etc) will not see it. They need
   449  // various environment variables to be exported first. Use Export for that.
   450  func (ac *Context) SetLocalAuth(ctx context.Context) context.Context {
   451  	if ac.localAuth != nil {
   452  		return lucictx.SetLocalAuth(ctx, ac.localAuth)
   453  	}
   454  	return ctx
   455  }
   456  
   457  // Report logs the service account email used by this auth context.
   458  func (ac *Context) Report(ctx context.Context) {
   459  	account := ac.email
   460  	if ac.anonymous {
   461  		account = "anonymous"
   462  	}
   463  	logging.Infof(ctx,
   464  		"%q account is %s (git_auth: %v, devshell: %v, emulate_gce:%v, docker:%v, firebase: %v)",
   465  		ac.ID, account, ac.EnableGitAuth, ac.EnableDevShell, ac.EnableGCEEmulation,
   466  		ac.EnableDockerAuth, ac.EnableFirebaseAuth)
   467  }
   468  
   469  ////////////////////////////////////////////////////////////////////////////////
   470  
   471  // launchSrv launches new localauth.Server that serves LUCI_CONTEXT protocol.
   472  //
   473  // Returns the server itself (so it can be stopped) and also LocalAuth section
   474  // that can be put into LUCI_CONTEXT to make subprocesses use the server.
   475  func launchSrv(ctx context.Context, tokens *auth.TokenGenerator, accID string, exposeSystemAccount bool) (*localauth.Server, *lucictx.LocalAuth, error) {
   476  	generators := make(map[string]localauth.TokenGenerator, 2)
   477  	generators[accID] = tokens
   478  	if exposeSystemAccount {
   479  		generators["system"] = tokens
   480  	}
   481  	srv := &localauth.Server{
   482  		TokenGenerators:  generators,
   483  		DefaultAccountID: accID,
   484  	}
   485  	la, err := srv.Start(ctx)
   486  	return srv, la, err
   487  }
   488  
   489  func (ac *Context) setupGitAuth(tempDir string) error {
   490  	ac.gitHome = filepath.Join(tempDir, "git-home-"+ac.ID)
   491  	if err := os.Mkdir(ac.gitHome, 0700); err != nil {
   492  		return errors.Annotate(err, "failed to create git HOME for %q account at %s", ac.ID, ac.gitHome).Err()
   493  	}
   494  	if err := ac.writeGitConfig(); err != nil {
   495  		return errors.Annotate(err, "failed to setup .gitconfig for %q account", ac.ID).Err()
   496  	}
   497  	return nil
   498  }
   499  
   500  func (ac *Context) writeGitConfig() error {
   501  	var cfg gitConfig
   502  	if !ac.anonymous {
   503  		cfg = gitConfig{
   504  			IsWindows:           runtime.GOOS == "windows",
   505  			UserEmail:           ac.email,
   506  			UserName:            strings.Split(ac.email, "@")[0],
   507  			UseCredentialHelper: true,
   508  			KnownGerritHosts:    ac.KnownGerritHosts,
   509  		}
   510  	} else {
   511  		cfg = gitConfig{
   512  			IsWindows:           runtime.GOOS == "windows",
   513  			UserEmail:           "anonymous@example.com", // otherwise git doesn't work
   514  			UserName:            "anonymous",
   515  			UseCredentialHelper: false, // fetch will be anonymous, push will fail
   516  			KnownGerritHosts:    nil,   // don't force non-anonymous fetch for public hosts
   517  		}
   518  	}
   519  	return cfg.Write(filepath.Join(ac.gitHome, ".gitconfig"))
   520  }
   521  
   522  func (ac *Context) setupDockerAuth(tempDir string) error {
   523  	ac.dockerConfig = filepath.Join(tempDir, "docker-cfg-"+ac.ID)
   524  	if err := os.Mkdir(ac.dockerConfig, 0700); err != nil {
   525  		return errors.Annotate(err, "failed to create Docker configuration directory for %q account at %s", ac.ID, ac.dockerConfig).Err()
   526  	}
   527  	if err := ac.writeDockerConfig(); err != nil {
   528  		return errors.Annotate(err, "failed to create config.json for %q account", ac.ID).Err()
   529  	}
   530  	ac.dockerTmpDir = filepath.Join(tempDir, "docker-tmp-"+ac.ID)
   531  	if err := os.Mkdir(ac.dockerTmpDir, 0700); err != nil {
   532  		return errors.Annotate(err, "failed to create Docker temporary directory for %q account at %s", ac.ID, ac.dockerTmpDir).Err()
   533  	}
   534  	return nil
   535  }
   536  
   537  func (ac *Context) writeDockerConfig() error {
   538  	f, err := os.Create(filepath.Join(ac.dockerConfig, "config.json"))
   539  	if err != nil {
   540  		return err
   541  	}
   542  	defer f.Close()
   543  	config := map[string]map[string]string{
   544  		"credHelpers": {
   545  			"us.gcr.io":                  "luci",
   546  			"staging-k8s.gcr.io":         "luci",
   547  			"asia.gcr.io":                "luci",
   548  			"gcr.io":                     "luci",
   549  			"marketplace.gcr.io":         "luci",
   550  			"eu.gcr.io":                  "luci",
   551  			"us-central1-docker.pkg.dev": "luci",
   552  		},
   553  	}
   554  	if err := json.NewEncoder(f).Encode(&config); err != nil {
   555  		return errors.Annotate(err, "cannot encode configuration").Err()
   556  	}
   557  	return f.Close()
   558  }
   559  
   560  func (ac *Context) setupDevShellAuth(ctx context.Context, tempDir string) error {
   561  	source, err := ac.authenticator.TokenSource()
   562  	if err != nil {
   563  		return errors.Annotate(err, "failed to get token source for %q account", ac.ID).Err()
   564  	}
   565  
   566  	// The directory for .boto and gsutil credentials cache (including access
   567  	// tokens).
   568  	ac.gsutilState = filepath.Join(tempDir, "gsutil-"+ac.ID)
   569  	if err := os.Mkdir(ac.gsutilState, 0700); err != nil {
   570  		return errors.Annotate(err, "failed to create gsutil state dir for %q account at %s", ac.ID, ac.gsutilState).Err()
   571  	}
   572  
   573  	// Launch gsutil auth shim server. It will put a specially constructed .boto
   574  	// into gsutilState dir (and return path to it).
   575  	ac.gsutilSrv = &gsutil.Server{
   576  		Source:   source,
   577  		StateDir: ac.gsutilState,
   578  	}
   579  	if ac.gsutilBoto, err = ac.gsutilSrv.Start(ctx); err != nil {
   580  		return errors.Annotate(err, "failed to start gsutil auth shim server for %q account", ac.ID).Err()
   581  	}
   582  
   583  	// Presence of DevShell env var breaks gsutil on Windows. Luckily, we rarely
   584  	// need to use gcloud in Windows, and gsutil (which we do use on Windows
   585  	// extensively) is covered by gsutil auth shim server setup above.
   586  	if runtime.GOOS != "windows" {
   587  		ac.devShellSrv = &devshell.Server{
   588  			Source: source,
   589  			Email:  ac.email,
   590  		}
   591  		if ac.devShellAddr, err = ac.devShellSrv.Start(ctx); err != nil {
   592  			return errors.Annotate(err, "failed to start the DevShell server").Err()
   593  		}
   594  	}
   595  
   596  	return nil
   597  }
   598  
   599  func (ac *Context) setupGCEEmulationAuth(ctx context.Context, tokens *auth.TokenGenerator, tempDir string) error {
   600  	// Launch the fake GCE metadata server.
   601  	botoGCEAccount := ""
   602  	if !ac.anonymous {
   603  		ac.gcemetaSrv = &gcemeta.Server{
   604  			Generator:        tokens,
   605  			Email:            ac.email,
   606  			Scopes:           tokens.Options().Scopes,
   607  			MinTokenLifetime: tokens.Options().MinTokenLifetime,
   608  		}
   609  		var err error
   610  		if ac.gcemetaAddr, err = ac.gcemetaSrv.Start(ctx); err != nil {
   611  			return errors.Annotate(err, "failed to start fake GCE metadata server for %q account", ac.ID).Err()
   612  		}
   613  		botoGCEAccount = "default" // switch .boto to use GCE auth
   614  	}
   615  
   616  	// Prepare clean gcloud config, otherwise gcloud will reuse cached "is on GCE"
   617  	// value from ~/.config/gcloud/gce and will not bother contacting the fake GCE
   618  	// metadata server on non-GCE machines. Additionally in anonymous mode we
   619  	// want to avoid using any cached credentials (also stored in the default
   620  	// ~/.config/gcloud/...).
   621  	ac.gcloudConfDir = filepath.Join(tempDir, "gcloud-"+ac.ID)
   622  	if err := os.Mkdir(ac.gcloudConfDir, 0700); err != nil {
   623  		return errors.Annotate(err, "failed to create gcloud config dir for %q account at %s", ac.ID, ac.gcloudConfDir).Err()
   624  	}
   625  
   626  	// The directory for .boto and gsutil credentials cache. We need to replace it
   627  	// to tell gsutil NOT to use whatever tokens it had cached in the default
   628  	// ~/.gsutil/... state dir.
   629  	var err error
   630  	ac.gsutilState = filepath.Join(tempDir, "gsutil-"+ac.ID)
   631  	ac.gsutilBoto, err = gsutil.PrepareStateDir(&gsutil.Boto{
   632  		StateDir:          ac.gsutilState,
   633  		GCEServiceAccount: botoGCEAccount, // may be "" in anonymous mode
   634  	})
   635  	return errors.Annotate(err, "failed to setup .boto for %q account", ac.ID).Err()
   636  }
   637  
   638  func (ac *Context) setupFirebaseAuth(ctx context.Context) error {
   639  	source, err := ac.authenticator.TokenSource()
   640  	if err != nil {
   641  		return errors.Annotate(err, "failed to get token source for %q account", ac.ID).Err()
   642  	}
   643  	// Launch firebase auth shim server. It will provide an URL from which we'll
   644  	// fetch an auth token.
   645  	ac.firebaseSrv = &firebase.Server{
   646  		Source: source,
   647  	}
   648  	if ac.firebaseTokenURL, err = ac.firebaseSrv.Start(ctx); err != nil {
   649  		return errors.Annotate(err, "failed to start firebase auth shim server for %q account", ac.ID).Err()
   650  	}
   651  	return nil
   652  }