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

     1  // Copyright 2015 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 authcli implements authentication related flags parsing and CLI
    16  // subcommands.
    17  //
    18  // It can be used from CLI tools that want customize authentication
    19  // configuration from the command line.
    20  //
    21  // Minimal example of using flags parsing:
    22  //
    23  //	authFlags := authcli.Flags{}
    24  //	defaults := ... // prepare default auth.Options
    25  //	authFlags.Register(flag.CommandLine, defaults)
    26  //	flag.Parse()
    27  //	opts, err := authFlags.Options()
    28  //	if err != nil {
    29  //	  // handle error
    30  //	}
    31  //	authenticator := auth.NewAuthenticator(ctx, auth.SilentLogin, opts)
    32  //	httpClient, err := authenticator.Client()
    33  //	if err != nil {
    34  //	  // handle error
    35  //	}
    36  //
    37  // This assumes that either a service account credentials are used (passed via
    38  // -service-account-json), or the user has previously ran "login" subcommand and
    39  // their refresh token is already cached. In any case, there will be no
    40  // interaction with the user (this is what auth.SilentLogin means): if there
    41  // are no cached token, authenticator.Client will return auth.ErrLoginRequired.
    42  //
    43  // Interaction with the user happens only in "login" subcommand. This subcommand
    44  // (as well as a bunch of other related commands) can be added to any
    45  // subcommands.Application.
    46  //
    47  // While it will work with any subcommand.Application, it uses
    48  // luci-go/common/cli.GetContext() to grab a context for logging, so callers
    49  // should prefer using cli.Application for hosting auth subcommands and making
    50  // the context. This ensures consistent logging style between all subcommands
    51  // of a CLI application:
    52  //
    53  //	import (
    54  //	  ...
    55  //	  "go.chromium.org/luci/client/authcli"
    56  //	  "go.chromium.org/luci/common/cli"
    57  //	)
    58  //
    59  //	func GetApplication(defaultAuthOpts auth.Options) *cli.Application {
    60  //	  return &cli.Application{
    61  //	    Name:  "app_name",
    62  //
    63  //	    Context: func(ctx context.Context) context.Context {
    64  //	      ... configure logging, etc. ...
    65  //	      return ctx
    66  //	    },
    67  //
    68  //	    Commands: []*subcommands.Command{
    69  //	      authcli.SubcommandInfo(defaultAuthOpts, "auth-info", false),
    70  //	      authcli.SubcommandLogin(defaultAuthOpts, "auth-login", false),
    71  //	      authcli.SubcommandLogout(defaultAuthOpts, "auth-logout", false),
    72  //	      ...
    73  //	    },
    74  //	  }
    75  //	}
    76  //
    77  //	func main() {
    78  //	  defaultAuthOpts := ...
    79  //	  app := GetApplication(defaultAuthOpts)
    80  //	  os.Exit(subcommands.Run(app, nil))
    81  //	}
    82  package authcli
    83  
    84  import (
    85  	"context"
    86  	"encoding/json"
    87  	"flag"
    88  	"fmt"
    89  	"os"
    90  	"os/exec"
    91  	"os/signal"
    92  	"sort"
    93  	"strings"
    94  	"time"
    95  
    96  	"github.com/maruel/subcommands"
    97  
    98  	"go.chromium.org/luci/auth"
    99  	"go.chromium.org/luci/auth/authctx"
   100  	"go.chromium.org/luci/auth/internal"
   101  	"go.chromium.org/luci/common/cli"
   102  	"go.chromium.org/luci/common/gcloud/googleoauth"
   103  	"go.chromium.org/luci/common/logging"
   104  	"go.chromium.org/luci/common/system/environ"
   105  	"go.chromium.org/luci/common/system/exitcode"
   106  	"go.chromium.org/luci/common/system/signals"
   107  	"go.chromium.org/luci/lucictx"
   108  )
   109  
   110  // CommandParams specifies various parameters for a subcommand.
   111  type CommandParams struct {
   112  	Name     string // name of the subcommand
   113  	Advanced bool   // treat this as an 'advanced' subcommand
   114  
   115  	AuthOptions auth.Options // default auth options
   116  
   117  	// UseScopeFlags specifies whether scope-related flags must be registered.
   118  	//
   119  	// This is primarily used by `luci-auth` executable.
   120  	//
   121  	// UseScopeFlags is *not needed* for command line tools that call a fixed
   122  	// number of backends. Just add all necessary scopes to AuthOptions.Scopes,
   123  	// no need to expose a flag.
   124  	UseScopeFlags bool
   125  
   126  	// UseIDTokenFlags specifies whether to register flags related to ID tokens.
   127  	//
   128  	// This is primarily used by `luci-auth` executable.
   129  	UseIDTokenFlags bool
   130  }
   131  
   132  // Flags defines command line flags related to authentication.
   133  type Flags struct {
   134  	defaults           auth.Options
   135  	serviceAccountJSON string // value of -service-account-json
   136  
   137  	hasScopeFlags bool   // true if registered -scopes (and related) flags
   138  	scopes        string // value of -scopes
   139  	scopesIAM     bool   // value of -scopes-iam
   140  	scopesContext bool   // value of -scopes-context
   141  
   142  	hasIDTokenFlags bool   // true if registered -use-id-token flag
   143  	useIDToken      bool   // value of -use-id-token
   144  	audience        string // value of -audience
   145  }
   146  
   147  // Register adds auth related flags to a FlagSet.
   148  func (fl *Flags) Register(f *flag.FlagSet, defaults auth.Options) {
   149  	fl.defaults = defaults
   150  	if len(fl.defaults.Scopes) == 0 {
   151  		fl.defaults.Scopes = append([]string(nil), scopesDefault...)
   152  	}
   153  	f.StringVar(&fl.serviceAccountJSON, "service-account-json", fl.defaults.ServiceAccountJSONPath,
   154  		fmt.Sprintf("Path to JSON file with service account credentials to use. Or specify %q to use GCE's default service account.", auth.GCEServiceAccount))
   155  }
   156  
   157  // registerScopesFlags adds scope-related flags.
   158  func (fl *Flags) registerScopesFlags(f *flag.FlagSet) {
   159  	fl.hasScopeFlags = true
   160  	f.StringVar(&fl.scopes, "scopes", strings.Join(fl.defaults.Scopes, " "),
   161  		"Space-separated list of OAuth 2.0 scopes to use.")
   162  	f.BoolVar(&fl.scopesIAM, "scopes-iam", false,
   163  		"When set, use scopes needed to impersonate accounts via Cloud IAM. Overrides -scopes when present.")
   164  	f.BoolVar(&fl.scopesContext, "scopes-context", false,
   165  		"When set, use scopes needed to run `context` subcommand. Overrides -scopes when present.")
   166  }
   167  
   168  // RegisterIDTokenFlags adds flags related to ID tokens.
   169  func (fl *Flags) RegisterIDTokenFlags(f *flag.FlagSet) {
   170  	fl.hasIDTokenFlags = true
   171  	f.BoolVar(&fl.useIDToken, "use-id-token", false,
   172  		"When set, use ID tokens instead of OAuth2 access tokens. Some backends may require them.")
   173  	f.StringVar(&fl.audience, "audience", fl.defaults.Audience,
   174  		"An audience to put into ID tokens. Ignored when not using ID tokens.")
   175  }
   176  
   177  // Options returns auth.Options populated based on parsed command line flags.
   178  func (fl *Flags) Options() (auth.Options, error) {
   179  	opts := fl.defaults
   180  	opts.ServiceAccountJSONPath = fl.serviceAccountJSON
   181  
   182  	if fl.hasScopeFlags {
   183  		if fl.scopesIAM && fl.scopesContext {
   184  			return auth.Options{}, fmt.Errorf("-scopes-iam and -scopes-context can't be used together")
   185  		}
   186  		switch {
   187  		case fl.scopesIAM:
   188  			opts.Scopes = append([]string(nil), scopesIAM...)
   189  		case fl.scopesContext:
   190  			opts.Scopes = append([]string(nil), scopesContext...)
   191  		default:
   192  			opts.Scopes = strings.Split(fl.scopes, " ")
   193  		}
   194  		sort.Strings(opts.Scopes)
   195  	}
   196  
   197  	if fl.hasIDTokenFlags {
   198  		opts.UseIDTokens = fl.useIDToken
   199  		opts.Audience = fl.audience
   200  	}
   201  
   202  	return opts, nil
   203  }
   204  
   205  // Process exit codes for subcommands.
   206  const (
   207  	ExitCodeSuccess = iota
   208  	ExitCodeNoValidToken
   209  	ExitCodeInvalidInput
   210  	ExitCodeInternalError
   211  	ExitCodeBadLogin
   212  )
   213  
   214  // List of scopes requested by `luci-auth login` by default.
   215  var scopesDefault = []string{
   216  	auth.OAuthScopeEmail,
   217  }
   218  
   219  // List of scopes needed to impersonate accounts via Cloud IAM.
   220  var scopesIAM = []string{
   221  	auth.OAuthScopeIAM,
   222  }
   223  
   224  // List of scopes needed to run `luci-auth context`. It correlates with a list
   225  // of requested features in authctx.Context{...} construction in contextRun.
   226  var scopesContext = []string{
   227  	"https://www.googleapis.com/auth/cloud-platform",
   228  	"https://www.googleapis.com/auth/firebase",
   229  	"https://www.googleapis.com/auth/gerritcodereview",
   230  	"https://www.googleapis.com/auth/userinfo.email",
   231  }
   232  
   233  type commandRunBase struct {
   234  	subcommands.CommandRunBase
   235  	flags   Flags
   236  	params  CommandParams
   237  	verbose bool
   238  }
   239  
   240  func (c *commandRunBase) ModifyContext(ctx context.Context) context.Context {
   241  	if c.verbose {
   242  		ctx = logging.SetLevel(ctx, logging.Debug)
   243  	}
   244  	return ctx
   245  }
   246  
   247  func (c *commandRunBase) registerBaseFlags(params CommandParams) {
   248  	c.params = params
   249  	c.flags.Register(&c.Flags, c.params.AuthOptions)
   250  	c.Flags.BoolVar(&c.verbose, "verbose", false, "More verbose logging.")
   251  	if c.params.UseScopeFlags {
   252  		c.flags.registerScopesFlags(&c.Flags)
   253  	}
   254  	if c.params.UseIDTokenFlags {
   255  		c.flags.RegisterIDTokenFlags(&c.Flags)
   256  	}
   257  }
   258  
   259  // askToLogin emits to stderr an instruction to login.
   260  func (c *commandRunBase) askToLogin(opts auth.Options, forContext bool) {
   261  	var loginFlags []string
   262  
   263  	if forContext {
   264  		switch {
   265  		case opts.ActAsServiceAccount != "" && opts.ActViaLUCIRealm != "":
   266  			// When acting via LUCI the default `luci-auth login` is sufficient to
   267  			// get necessary tokens, since we need only userinfo.email scope.
   268  		case opts.ActAsServiceAccount != "":
   269  			// When acting via IAM need an IAM-scoped token.
   270  			loginFlags = []string{"-scopes-iam"}
   271  		default:
   272  			// When not acting, need all scopes used by `luci-auth context`.
   273  			loginFlags = []string{"-scopes-context"}
   274  		}
   275  	} else {
   276  		// Ask for custom scopes only if they were actually requested. Use our
   277  		// neat aliases when possible.
   278  		switch {
   279  		case isSameScopes(opts.Scopes, scopesIAM):
   280  			loginFlags = []string{"-scopes-iam"}
   281  		case isSameScopes(opts.Scopes, scopesContext):
   282  			loginFlags = []string{"-scopes-context"}
   283  		case !isSameScopes(opts.Scopes, c.flags.defaults.Scopes):
   284  			loginFlags = []string{"-scopes", fmt.Sprintf("%q", strings.Join(opts.Scopes, " "))}
   285  		}
   286  	}
   287  
   288  	fmt.Fprintf(os.Stderr, "Not logged in.\n\nLogin by running:\n")
   289  	fmt.Fprintf(os.Stderr, "   $ luci-auth login")
   290  	if len(loginFlags) != 0 {
   291  		fmt.Fprintf(os.Stderr, " %s", strings.Join(loginFlags, " "))
   292  	}
   293  	fmt.Fprintf(os.Stderr, "\n")
   294  }
   295  
   296  func isSameScopes(a, b []string) bool {
   297  	if len(a) != len(b) {
   298  		return false
   299  	}
   300  	for i := range a {
   301  		if a[i] != b[i] {
   302  			return false
   303  		}
   304  	}
   305  	return true
   306  }
   307  
   308  ////////////////////////////////////////////////////////////////////////////////
   309  
   310  // SubcommandLogin returns subcommands.Command that can be used to perform
   311  // interactive login.
   312  func SubcommandLogin(opts auth.Options, name string, advanced bool) *subcommands.Command {
   313  	return SubcommandLoginWithParams(CommandParams{
   314  		Name:        name,
   315  		Advanced:    advanced,
   316  		AuthOptions: opts,
   317  	})
   318  }
   319  
   320  // SubcommandLoginWithParams returns subcommands.Command that can be used to
   321  // perform interactive login.
   322  func SubcommandLoginWithParams(params CommandParams) *subcommands.Command {
   323  	return &subcommands.Command{
   324  		Advanced:  params.Advanced,
   325  		UsageLine: params.Name,
   326  		ShortDesc: "performs interactive login flow",
   327  		LongDesc:  "Performs interactive login flow and caches obtained credentials",
   328  		CommandRun: func() subcommands.CommandRun {
   329  			c := &loginRun{}
   330  			c.registerBaseFlags(params)
   331  			return c
   332  		},
   333  	}
   334  }
   335  
   336  type loginRun struct {
   337  	commandRunBase
   338  }
   339  
   340  func (c *loginRun) Run(a subcommands.Application, _ []string, env subcommands.Env) int {
   341  	opts, err := c.flags.Options()
   342  	if err != nil {
   343  		fmt.Fprintln(os.Stderr, err)
   344  		return ExitCodeInvalidInput
   345  	}
   346  	ctx := cli.GetContext(a, c, env)
   347  	authenticator := auth.NewAuthenticator(ctx, auth.InteractiveLogin, opts)
   348  	if err := authenticator.Login(); err != nil {
   349  		fmt.Fprintf(os.Stderr, "Login failed: %s\n", err)
   350  		return ExitCodeBadLogin
   351  	}
   352  	return checkToken(ctx, &opts, authenticator)
   353  }
   354  
   355  ////////////////////////////////////////////////////////////////////////////////
   356  
   357  // SubcommandLogout returns subcommands.Command that can be used to purge cached
   358  // credentials.
   359  func SubcommandLogout(opts auth.Options, name string, advanced bool) *subcommands.Command {
   360  	return SubcommandLogoutWithParams(CommandParams{
   361  		Name:        name,
   362  		Advanced:    advanced,
   363  		AuthOptions: opts,
   364  	})
   365  }
   366  
   367  // SubcommandLogoutWithParams returns subcommands.Command that can be used to purge cached
   368  // credentials.
   369  func SubcommandLogoutWithParams(params CommandParams) *subcommands.Command {
   370  	return &subcommands.Command{
   371  		Advanced:  params.Advanced,
   372  		UsageLine: params.Name,
   373  		ShortDesc: "removes cached credentials",
   374  		LongDesc:  "Removes cached credentials from the disk",
   375  		CommandRun: func() subcommands.CommandRun {
   376  			c := &logoutRun{}
   377  			c.registerBaseFlags(params)
   378  			return c
   379  		},
   380  	}
   381  }
   382  
   383  type logoutRun struct {
   384  	commandRunBase
   385  }
   386  
   387  func (c *logoutRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
   388  	opts, err := c.flags.Options()
   389  	if err != nil {
   390  		fmt.Fprintln(os.Stderr, err)
   391  		return ExitCodeInvalidInput
   392  	}
   393  	ctx := cli.GetContext(a, c, env)
   394  	err = auth.NewAuthenticator(ctx, auth.SilentLogin, opts).PurgeCredentialsCache()
   395  	if err != nil {
   396  		fmt.Fprintln(os.Stderr, err)
   397  		return ExitCodeInternalError
   398  	}
   399  	return ExitCodeSuccess
   400  }
   401  
   402  ////////////////////////////////////////////////////////////////////////////////
   403  
   404  // SubcommandInfo returns subcommand.Command that can be used to print current
   405  // cached credentials.
   406  func SubcommandInfo(opts auth.Options, name string, advanced bool) *subcommands.Command {
   407  	return SubcommandInfoWithParams(CommandParams{
   408  		Name:        name,
   409  		Advanced:    advanced,
   410  		AuthOptions: opts,
   411  	})
   412  }
   413  
   414  // SubcommandInfoWithParams returns subcommand.Command that can be used to print
   415  // current cached credentials.
   416  func SubcommandInfoWithParams(params CommandParams) *subcommands.Command {
   417  	return &subcommands.Command{
   418  		Advanced:  params.Advanced,
   419  		UsageLine: params.Name,
   420  		ShortDesc: "prints an email address associated with currently cached token",
   421  		LongDesc:  "Prints an email address associated with currently cached token",
   422  		CommandRun: func() subcommands.CommandRun {
   423  			c := &infoRun{}
   424  			c.registerBaseFlags(params)
   425  			return c
   426  		},
   427  	}
   428  }
   429  
   430  type infoRun struct {
   431  	commandRunBase
   432  }
   433  
   434  func (c *infoRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
   435  	opts, err := c.flags.Options()
   436  	if err != nil {
   437  		fmt.Fprintln(os.Stderr, err)
   438  		return ExitCodeInvalidInput
   439  	}
   440  	ctx := cli.GetContext(a, c, env)
   441  	authenticator := auth.NewAuthenticator(ctx, auth.SilentLogin, opts)
   442  	switch _, err := authenticator.Client(); {
   443  	case err == auth.ErrLoginRequired:
   444  		fmt.Fprintln(os.Stderr, "Not logged in.")
   445  		return ExitCodeNoValidToken
   446  	case err != nil:
   447  		fmt.Fprintln(os.Stderr, err)
   448  		return ExitCodeInternalError
   449  	}
   450  	return checkToken(ctx, &opts, authenticator)
   451  }
   452  
   453  ////////////////////////////////////////////////////////////////////////////////
   454  
   455  // SubcommandToken returns subcommand.Command that can be used to print current
   456  // access token.
   457  func SubcommandToken(opts auth.Options, name string) *subcommands.Command {
   458  	return SubcommandTokenWithParams(CommandParams{
   459  		Name:        name,
   460  		AuthOptions: opts,
   461  	})
   462  }
   463  
   464  // SubcommandTokenWithParams returns subcommand.Command that can be used to
   465  // print current access token.
   466  func SubcommandTokenWithParams(params CommandParams) *subcommands.Command {
   467  	return &subcommands.Command{
   468  		Advanced:  params.Advanced,
   469  		UsageLine: params.Name,
   470  		ShortDesc: "prints an access or ID token",
   471  		LongDesc:  "Refreshes the token (if necessary) and prints it or writes it to a JSON file.",
   472  		CommandRun: func() subcommands.CommandRun {
   473  			c := &tokenRun{}
   474  			c.registerBaseFlags(params)
   475  			c.Flags.DurationVar(
   476  				&c.lifetime, "lifetime", time.Minute,
   477  				"The returned token will live for at least that long. Depending on\n"+
   478  					"what exact token provider is used internally, large values may not\n"+
   479  					"work. Avoid using this parameter unless really necessary.\n"+
   480  					"The maximum acceptable value is 30m.",
   481  			)
   482  			c.Flags.StringVar(
   483  				&c.jsonOutput, "json-output", "",
   484  				`Path to a JSON file to write {"token": "...", expiry: <unix_ts>} into.`+
   485  					"\nUse \"-\" for standard output.")
   486  			return c
   487  		},
   488  	}
   489  }
   490  
   491  type tokenRun struct {
   492  	commandRunBase
   493  	lifetime   time.Duration
   494  	jsonOutput string
   495  }
   496  
   497  func (c *tokenRun) Run(a subcommands.Application, args []string, env subcommands.Env) (exitCode int) {
   498  	opts, err := c.flags.Options()
   499  	if err != nil {
   500  		fmt.Fprintln(os.Stderr, err)
   501  		return ExitCodeInvalidInput
   502  	}
   503  	if c.lifetime > 30*time.Minute {
   504  		fmt.Fprintf(os.Stderr, "Requested -lifetime (%s) must not exceed 30m.\n", c.lifetime)
   505  		return ExitCodeInvalidInput
   506  	}
   507  
   508  	ctx := cli.GetContext(a, c, env)
   509  	authenticator := auth.NewAuthenticator(ctx, auth.SilentLogin, opts)
   510  	token, err := authenticator.GetAccessToken(c.lifetime)
   511  	if err != nil {
   512  		if err == auth.ErrLoginRequired {
   513  			c.askToLogin(opts, false)
   514  		} else {
   515  			fmt.Fprintln(os.Stderr, err)
   516  		}
   517  		return ExitCodeNoValidToken
   518  	}
   519  	if token.AccessToken == "" {
   520  		return ExitCodeNoValidToken
   521  	}
   522  
   523  	if c.jsonOutput == "" {
   524  		fmt.Println(token.AccessToken)
   525  	} else {
   526  		out := os.Stdout
   527  		if c.jsonOutput != "-" {
   528  			out, err = os.Create(c.jsonOutput)
   529  			if err != nil {
   530  				fmt.Fprintln(os.Stderr, err)
   531  				return ExitCodeInvalidInput
   532  			}
   533  			defer func() {
   534  				if err := out.Close(); err != nil {
   535  					fmt.Fprintln(os.Stderr, err)
   536  					exitCode = ExitCodeInternalError
   537  				}
   538  			}()
   539  		}
   540  		data := struct {
   541  			Token  string `json:"token"`
   542  			Expiry int64  `json:"expiry"`
   543  		}{token.AccessToken, token.Expiry.Unix()}
   544  		if err = json.NewEncoder(out).Encode(data); err != nil {
   545  			fmt.Fprintln(os.Stderr, err)
   546  			return ExitCodeInternalError
   547  		}
   548  	}
   549  	return ExitCodeSuccess
   550  }
   551  
   552  ////////////////////////////////////////////////////////////////////////////////
   553  
   554  // SubcommandContext returns subcommand.Command that can be used to setup new
   555  // LUCI authentication context for a process tree.
   556  //
   557  // This is an advanced command and shouldn't be usually embedded into binaries.
   558  // It is primarily used by 'luci-auth' program. It exists to simplify
   559  // development and debugging of programs that rely on LUCI authentication
   560  // context.
   561  func SubcommandContext(opts auth.Options, name string) *subcommands.Command {
   562  	return SubcommandContextWithParams(CommandParams{
   563  		Name:        name,
   564  		AuthOptions: opts,
   565  	})
   566  }
   567  
   568  // SubcommandContextWithParams returns subcommand.Command that can be used to
   569  // setup new LUCI authentication context for a process tree.
   570  func SubcommandContextWithParams(params CommandParams) *subcommands.Command {
   571  	params.AuthOptions.Scopes = append([]string(nil), scopesContext...)
   572  	return &subcommands.Command{
   573  		Advanced:  params.Advanced,
   574  		UsageLine: fmt.Sprintf("%s [flags] [--] <bin> [args]", params.Name),
   575  		ShortDesc: "sets up new LUCI local auth context and launches a process in it",
   576  		LongDesc:  "Starts local RPC auth server, prepares LUCI_CONTEXT, launches a process in this environment.",
   577  		CommandRun: func() subcommands.CommandRun {
   578  			c := &contextRun{}
   579  			c.registerBaseFlags(params)
   580  			c.Flags.StringVar(
   581  				&c.actAs, "act-as-service-account", "",
   582  				"Act as a given service account (via Cloud IAM or via LUCI Token Server).")
   583  			c.Flags.StringVar(
   584  				&c.actViaRealm, "act-via-realm", params.AuthOptions.ActViaLUCIRealm,
   585  				"When used together with -act-as-service-account enables account\n"+
   586  					"impersonation through LUCI Token Server using LUCI Realms for ACLs.\n"+
   587  					"Must have form `<project>:<realm>`. If unset, the impersonation will\n"+
   588  					"be done through Cloud IAM instead bypassing LUCI.")
   589  			c.Flags.StringVar(
   590  				&c.tokenServerHost, "token-server-host", params.AuthOptions.TokenServerHost,
   591  				"The LUCI Token Server hostname to use when using -act-via-realm.")
   592  			c.Flags.BoolVar(
   593  				&c.exposeSystemAccount, "expose-system-account", false,
   594  				`Exposes non-default "system" LUCI logical account to emulate Swarming environment.`)
   595  			c.Flags.BoolVar(
   596  				&c.disableGitAuth, "disable-git-auth", false,
   597  				"Toggles whether to attempt configuration of the git credentials environment\n"+
   598  					"for the subprocess.")
   599  			return c
   600  		},
   601  	}
   602  }
   603  
   604  type contextRun struct {
   605  	commandRunBase
   606  
   607  	actAs               string
   608  	actViaRealm         string
   609  	tokenServerHost     string
   610  	exposeSystemAccount bool
   611  	disableGitAuth      bool
   612  }
   613  
   614  func (c *contextRun) Run(a subcommands.Application, args []string, env subcommands.Env) int {
   615  	ctx := cli.GetContext(a, c, env)
   616  
   617  	opts, err := c.flags.Options()
   618  	if err != nil {
   619  		fmt.Fprintln(os.Stderr, err)
   620  		return ExitCodeInvalidInput
   621  	}
   622  	opts.ActAsServiceAccount = c.actAs
   623  	opts.ActViaLUCIRealm = c.actViaRealm
   624  	opts.TokenServerHost = c.tokenServerHost
   625  
   626  	// 'args' specify a subcommand to run.
   627  	if len(args) == 0 {
   628  		fmt.Fprintf(os.Stderr, "Specify a command to run:\n  %s context [flags] [--] <bin> [args]\n", os.Args[0])
   629  		return ExitCodeInvalidInput
   630  	}
   631  
   632  	// Start watching for interrupts as soon as possible (in particular before
   633  	// any heavy setup calls).
   634  	interrupts := make(chan os.Signal, 1)
   635  	signal.Notify(interrupts, signals.Interrupts()...)
   636  	defer func() {
   637  		signal.Stop(interrupts)
   638  		close(interrupts)
   639  	}()
   640  
   641  	// Create an authenticator for requested options to make sure we have required
   642  	// refresh tokens (if any), asking the user to login if not.
   643  	if opts.Method == auth.AutoSelectMethod {
   644  		opts.Method = auth.SelectBestMethod(ctx, opts)
   645  	}
   646  	authenticator := auth.NewAuthenticator(ctx, auth.SilentLogin, opts)
   647  	if err = authenticator.CheckLoginRequired(); err != nil {
   648  		if err == auth.ErrLoginRequired {
   649  			c.askToLogin(opts, true)
   650  		} else {
   651  			fmt.Fprintln(os.Stderr, err)
   652  		}
   653  		return ExitCodeNoValidToken
   654  	}
   655  
   656  	// Now that there exists a cached token for requested options, we can launch
   657  	// an auth context with all bells and whistles. If you enable or disable
   658  	// a feature here, make sure to adjust scopesContext as well.
   659  	authCtx := authctx.Context{
   660  		ID:                  "luci-auth",
   661  		Options:             opts,
   662  		ExposeSystemAccount: c.exposeSystemAccount,
   663  		EnableGitAuth:       !c.disableGitAuth,
   664  		EnableDockerAuth:    true,
   665  		EnableGCEEmulation:  true,
   666  		EnableFirebaseAuth:  true,
   667  	}
   668  	if err = authCtx.Launch(ctx, ""); err != nil {
   669  		fmt.Fprintln(os.Stderr, err)
   670  		return ExitCodeInternalError
   671  	}
   672  	defer authCtx.Close(ctx) // logs errors inside
   673  
   674  	// Prepare a modified environ for the subcommand.
   675  	cmdEnv := environ.System()
   676  	exported, err := lucictx.Export(authCtx.Export(ctx, cmdEnv))
   677  	if err != nil {
   678  		fmt.Fprintln(os.Stderr, err)
   679  		return ExitCodeInternalError
   680  	}
   681  	defer exported.Close()
   682  	exported.SetInEnviron(cmdEnv)
   683  
   684  	// Prepare the subcommand.
   685  	logging.Debugf(ctx, "Running %q", args)
   686  	cmd := exec.Command(args[0], args[1:]...)
   687  	cmd.Env = cmdEnv.Sorted()
   688  	cmd.Stdin = os.Stdin
   689  	cmd.Stdout = os.Stdout
   690  	cmd.Stderr = os.Stderr
   691  
   692  	// Rig it to die violently if the luci-auth unexpectedly dies. This works only
   693  	// on Linux. See pdeath_linux.go and pdeath_notlinux.go.
   694  	setPdeathsig(cmd)
   695  
   696  	// Launch.
   697  	if err = cmd.Start(); err != nil {
   698  		fmt.Fprintln(os.Stderr, err)
   699  		return ExitCodeInvalidInput
   700  	}
   701  
   702  	// Forward interrupts to the child process. See terminate_windows.go and
   703  	// terminate_notwindows.go.
   704  	go func() {
   705  		for sig := range interrupts {
   706  			if err := terminateProcess(cmd.Process, sig); err != nil {
   707  				logging.Errorf(ctx, "Failed to send %q to the child process: %s", sig, err)
   708  			}
   709  		}
   710  	}()
   711  
   712  	if err = cmd.Wait(); err == nil {
   713  		return 0
   714  	}
   715  	if code, hasCode := exitcode.Get(err); hasCode {
   716  		return code
   717  	}
   718  	return ExitCodeInternalError
   719  }
   720  
   721  ////////////////////////////////////////////////////////////////////////////////
   722  
   723  // checkToken prints information about the token carried by the authenticator.
   724  //
   725  // Prints errors to stderr and returns corresponding process exit code.
   726  func checkToken(ctx context.Context, opts *auth.Options, a *auth.Authenticator) int {
   727  	// Grab the active token.
   728  	tok, err := a.GetAccessToken(time.Minute)
   729  	if err != nil {
   730  		fmt.Fprintf(os.Stderr, "Can't grab an access token: %s\n", err)
   731  		return ExitCodeNoValidToken
   732  	}
   733  
   734  	if opts.UseIDTokens {
   735  		// When using ID tokens, decode the claims and show some interesting ones.
   736  		claims, err := internal.ParseIDTokenClaims(tok.AccessToken)
   737  		if err != nil {
   738  			fmt.Fprintf(os.Stderr, "Failed to decode ID token: %s\n", err)
   739  			return ExitCodeNoValidToken
   740  		}
   741  		fmt.Printf("Logged in as %s.\n\n", claims.Email)
   742  		fmt.Printf("ID token details:\n")
   743  		fmt.Printf("  Issuer: %s\n", claims.Iss)
   744  		fmt.Printf("  Subject: %s\n", claims.Sub)
   745  		fmt.Printf("  Audience: %s\n", claims.Aud)
   746  	} else {
   747  		// When using access tokens, ask the Google endpoint for details of the
   748  		// token.
   749  		info, err := googleoauth.GetTokenInfo(ctx, googleoauth.TokenInfoParams{
   750  			AccessToken: tok.AccessToken,
   751  		})
   752  		if err != nil {
   753  			fmt.Fprintf(os.Stderr, "Failed to call token info endpoint: %s\n", err)
   754  			if err == googleoauth.ErrBadToken {
   755  				return ExitCodeNoValidToken
   756  			}
   757  			return ExitCodeInternalError
   758  		}
   759  		if info.Email != "" {
   760  			fmt.Printf("Logged in as %s.\n\n", info.Email)
   761  		} else if info.Sub != "" {
   762  			fmt.Printf("Logged in as uid %q.\n\n", info.Sub)
   763  		}
   764  		fmt.Printf("OAuth token details:\n")
   765  		fmt.Printf("  Client ID: %s\n", info.Aud)
   766  		fmt.Printf("  Scopes:\n")
   767  		for _, scope := range strings.Split(info.Scope, " ") {
   768  			fmt.Printf("    %s\n", scope)
   769  		}
   770  	}
   771  
   772  	return ExitCodeSuccess
   773  }