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 }