diff --git a/README.md b/README.md index f6ba8cf..108abe5 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,10 @@ go install github.com/linuxfoundation/lfx-cli/cmd/lfx@latest # Log in via the Auth0 Device Code flow. lfx auth login +# Log in against a non-production environment (aliases accepted). +lfx auth login --env staging # also: stage, stg +lfx auth login --env dev # also: develop, development + # Show the current authentication status. lfx auth status diff --git a/go.mod b/go.mod index 79ad9f7..b62eed4 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/99designs/keyring v1.2.2 github.com/tidwall/gjson v1.19.0 github.com/urfave/cli-docs/v3 v3.1.0 - github.com/urfave/cli/v3 v3.11.0 + github.com/urfave/cli/v3 v3.13.0 golang.org/x/oauth2 v0.36.0 ) @@ -16,13 +16,13 @@ require ( github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/danieljoos/wincred v1.2.3 // indirect - github.com/dvsekhvalnov/jose2go v1.10.0 // indirect + github.com/dvsekhvalnov/jose2go v1.11.0 // indirect github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect github.com/mtibben/percent v0.2.1 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/tidwall/match v1.1.1 // indirect - github.com/tidwall/pretty v1.2.0 // indirect + github.com/tidwall/match v1.2.0 // indirect + github.com/tidwall/pretty v1.2.1 // indirect golang.org/x/sys v0.47.0 // indirect golang.org/x/term v0.45.0 // indirect ) diff --git a/go.sum b/go.sum index c0e834f..9cc8e0b 100644 --- a/go.sum +++ b/go.sum @@ -6,10 +6,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3 github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/danieljoos/wincred v1.2.3 h1:v7dZC2x32Ut3nEfRH+vhoZGvN72+dQ/snVXo/vMFLdQ= github.com/danieljoos/wincred v1.2.3/go.mod h1:6qqX0WNrS4RzPZ1tnroDzq9kY3fu1KwE7MRLQK4X0bs= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dvsekhvalnov/jose2go v1.10.0 h1:5RmEnUoQBMBURnk346hX3dKqG60Jkf9qkp6dkLsFx60= -github.com/dvsekhvalnov/jose2go v1.10.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= +github.com/dvsekhvalnov/jose2go v1.11.0 h1:9pnsV1/CLWFYSxMilqtiY1Fb4JZOxfDk/q0/CQiUMYw= +github.com/dvsekhvalnov/jose2go v1.11.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= @@ -21,24 +19,24 @@ github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= -github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= -github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE= +github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg= github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU= github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc= -github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= -github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= -github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= -github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= +github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/urfave/cli-docs/v3 v3.1.0 h1:Sa5xm19IpE5gpm6tZzXdfjdFxn67PnEsE4dpXF7vsKw= github.com/urfave/cli-docs/v3 v3.1.0/go.mod h1:59d+5Hz1h6GSGJ10cvcEkbIe3j233t4XDqI72UIx7to= -github.com/urfave/cli/v3 v3.11.0 h1:P/euJp99kb9p0tlVY+iYTLYYTAQlfl0hR2gUO1Img1Q= -github.com/urfave/cli/v3 v3.11.0/go.mod h1:ysVLtOEmg2tOy6PknnYVhDoouyC/6N42TMeoMzskhso= +github.com/urfave/cli/v3 v3.13.0 h1:Dr6jqMfIyyFsRVn7Nz5mqLsMY+ZMpfh3a0aMs+umPVY= +github.com/urfave/cli/v3 v3.13.0/go.mod h1:vXn6HxPNccJSzQr2QvwVncOKrgYGIHU0HY5h8B2nQj4= +go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw= +go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= @@ -47,5 +45,3 @@ golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0= golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/commands/api.go b/internal/commands/api.go index 58aa4c3..84d2ece 100644 --- a/internal/commands/api.go +++ b/internal/commands/api.go @@ -82,7 +82,7 @@ func NewAPICommand() *cli.Command { }, &cli.StringFlag{ Name: apiHostnameFlagName, - Usage: "Override the LFX API base URL (advanced; independent of the IdP domain). Requires a development-environment login (`lfx auth login --env=development`).", + Usage: "Override the LFX API base URL (advanced; independent of the IdP domain). Requires a development-environment login ('lfx auth login --env=development').", }, }, Action: runAPI, diff --git a/internal/commands/auth.go b/internal/commands/auth.go index e407b84..7a897e2 100644 --- a/internal/commands/auth.go +++ b/internal/commands/auth.go @@ -55,7 +55,7 @@ var CredentialStoreFlags = []cli.Flag{ }, &cli.StringFlag{ Name: backendFlagName, - Usage: "Pin credential storage to a specific system backend (see `lfx auth backends`); mutually exclusive with --insecure-storage", + Usage: "Pin credential storage to a specific system backend (see 'lfx auth backends'); mutually exclusive with --insecure-storage", }, } @@ -84,6 +84,7 @@ func NewAuthCommand() *cli.Command { newAuthStatusCommand(), newAuthLogoutCommand(), newAuthBackendsCommand(), + newAuthEnvironmentsCommand(), }, } } @@ -116,12 +117,12 @@ func newAuthLoginCommand() *cli.Command { }, &cli.StringFlag{ Name: envFlagName, - Usage: "Target environment: prod, staging, or development", - Value: string(envProd), + Usage: "Target environment; see 'lfx auth environments' for accepted values", + Value: string(envProduction), }, &cli.StringFlag{ Name: audienceFlagName, - Usage: "Auth0 API audience to request tokens for (defaults to the selected environment's LFX API audience)", + Usage: "Auth0 API audience to request tokens for; defaults to --env's audience (see 'lfx auth environments')", }, }, Action: runAuthLogin, @@ -134,7 +135,7 @@ func runAuthLogin(ctx context.Context, cmd *cli.Command) error { return err } - env := authEnvironment(cmd.String(envFlagName)) + env := normalizeEnvironment(cmd.String(envFlagName)) domain, clientID, err := resolveEnvironment(env) if err != nil { return err @@ -336,7 +337,7 @@ func loadDeviceStateForBackend(store credstore.Store, cmd *cli.Command) (state c ) } - domain, clientID, err = resolveEnvironment(authEnvironment(state.Environment)) + domain, clientID, err = resolveEnvironment(normalizeEnvironment(state.Environment)) if err != nil { return credstore.DeviceState{}, "", "", err } @@ -488,7 +489,7 @@ func resolveAccessToken(ctx context.Context, cmd *cli.Command) (token, audience } if creds.ValidAccessToken() { - return creds.AccessToken, state.Audience, authEnvironment(state.Environment), nil + return creds.AccessToken, state.Audience, normalizeEnvironment(state.Environment), nil } if creds.RefreshToken == "" { @@ -527,7 +528,7 @@ func resolveAccessToken(ctx context.Context, cmd *cli.Command) (token, audience return "", "", "", fmt.Errorf("save refreshed credentials: %w", err) } - return refreshed.AccessToken, state.Audience, authEnvironment(state.Environment), nil + return refreshed.AccessToken, state.Audience, normalizeEnvironment(state.Environment), nil } func newAuthStatusCommand() *cli.Command { @@ -563,7 +564,7 @@ func newAuthStatusCommand() *cli.Command { ) } if state.Environment != "" { - fmt.Printf(" %-22s %s\n", "Environment:", state.Environment) + fmt.Printf(" %-22s %s\n", "Environment:", normalizeEnvironment(state.Environment)) } if state.IDPDomain != "" { fmt.Printf(" %-22s %s\n", "IdP domain:", state.IDPDomain) @@ -680,6 +681,31 @@ func newAuthBackendsCommand() *cli.Command { } } +// newAuthEnvironmentsCommand builds `lfx auth environments`, which lists +// every canonical --env value accepted by `lfx auth login`, along with +// its aliases and default API audience. Kept out of --env's own usage +// text (and out of the main help listing, aside from the command itself) +// to keep that terser; full detail lives here for anyone who needs to +// look it up. +func newAuthEnvironmentsCommand() *cli.Command { + return &cli.Command{ + Name: "environments", + Usage: "List --env values accepted by `lfx auth login`, including aliases and default audiences", + Action: func(_ context.Context, _ *cli.Command) error { + fmt.Println("Accepted --env values:") + for _, env := range []authEnvironment{envProduction, envStaging, envDevelopment} { + line := " " + string(env) + if aliases := environmentAliases(env); len(aliases) > 0 { + line += fmt.Sprintf(" (aliases: %s)", strings.Join(aliases, ", ")) + } + fmt.Println(line) + fmt.Printf(" default audience: %s\n", defaultAudiences[env]) + } + return nil + }, + } +} + // lfidClaimsNamespace prefixes the custom LFID claims Auth0 adds to the ID // token, namely username. Distinct from the shorter "http://lfx.dev/claims" // LFX claims namespace used elsewhere. diff --git a/internal/commands/auth_test.go b/internal/commands/auth_test.go index 832fa99..f92b64a 100644 --- a/internal/commands/auth_test.go +++ b/internal/commands/auth_test.go @@ -50,8 +50,8 @@ func TestDefaultAudienceForEnvironment(t *testing.T) { want string }{ { - name: "prod", - env: envProd, + name: "production", + env: envProduction, want: "https://lfx-api.v2.cluster.lfx.dev/", }, { @@ -177,6 +177,34 @@ func TestLoadDeviceStateForBackendDomainMismatch(t *testing.T) { }) } +func TestLoadDeviceStateForBackendLegacyProdEnvironment(t *testing.T) { + // state.json files written before "prod" was renamed to "production" + // persist the old name; loadDeviceStateForBackend must still resolve + // them via normalizeEnvironment rather than erroring or requiring a + // fresh `lfx auth login`. + store := newInsecureStore(t) + if err := store.SaveDeviceState(credstore.DeviceState{ + IDPDomain: "sso.linuxfoundation.org", + Environment: "prod", + Insecure: true, + }); err != nil { + t.Fatalf("SaveDeviceState: %v", err) + } + + newTestCommand(t, []string{"--insecure-storage"}, func(cmd *cli.Command) { + state, domain, _, err := loadDeviceStateForBackend(store, cmd) + if err != nil { + t.Fatalf("loadDeviceStateForBackend: %v", err) + } + if domain != "sso.linuxfoundation.org" { + t.Errorf("domain = %q, want sso.linuxfoundation.org", domain) + } + if state.Environment != "prod" { + t.Errorf("state.Environment = %q, want the raw persisted value %q", state.Environment, "prod") + } + }) +} + func TestLoadDeviceStateForBackendKeyringBackendMismatch(t *testing.T) { store := newInsecureStore(t) if err := store.SaveDeviceState(credstore.DeviceState{ @@ -343,3 +371,67 @@ func TestStateMismatchReasonBackendPinMismatch(t *testing.T) { } }) } + +func TestNormalizeEnvironment(t *testing.T) { + tests := []struct { + input string + want authEnvironment + }{ + // Canonical names pass through unchanged. + {"production", envProduction}, + {"staging", envStaging}, + {"development", envDevelopment}, + // Aliases. + {"prod", envProduction}, + {"stage", envStaging}, + {"stg", envStaging}, + {"develop", envDevelopment}, + {"dev", envDevelopment}, + // Unrecognized inputs are returned as-is (error surfaces in resolveEnvironment). + {"unknown", authEnvironment("unknown")}, + } + for _, tc := range tests { + t.Run(tc.input, func(t *testing.T) { + if got := normalizeEnvironment(tc.input); got != tc.want { + t.Errorf("normalizeEnvironment(%q) = %q, want %q", tc.input, got, tc.want) + } + }) + } +} + +func TestResolveEnvironmentAliases(t *testing.T) { + tests := []struct { + input string + wantDomain string + }{ + {"prod", "sso.linuxfoundation.org"}, + {"stage", "linuxfoundation-staging.auth0.com"}, + {"stg", "linuxfoundation-staging.auth0.com"}, + {"dev", "linuxfoundation-dev.auth0.com"}, + } + for _, tc := range tests { + t.Run(tc.input, func(t *testing.T) { + env := normalizeEnvironment(tc.input) + domain, clientID, err := resolveEnvironment(env) + if err != nil { + t.Fatalf("resolveEnvironment(%q): %v", tc.input, err) + } + if domain != tc.wantDomain { + t.Errorf("domain = %q, want %q", domain, tc.wantDomain) + } + if clientID == "" { + t.Error("clientID = \"\", want a non-empty compiled-in client ID") + } + }) + } +} + +func TestResolveEnvironmentUnknown(t *testing.T) { + _, _, err := resolveEnvironment(authEnvironment("bogus")) + if err == nil { + t.Fatal("resolveEnvironment(\"bogus\"): got nil error, want errInvalidEnvironment") + } + if !errors.Is(err, errInvalidEnvironment) { + t.Errorf("resolveEnvironment(\"bogus\") error = %v, want wrapping errInvalidEnvironment", err) + } +} diff --git a/internal/commands/environment.go b/internal/commands/environment.go index ea82f0c..3d0e7cd 100644 --- a/internal/commands/environment.go +++ b/internal/commands/environment.go @@ -7,6 +7,7 @@ package commands import ( "errors" "fmt" + "sort" ) // authEnvironment identifies which LFX Auth0 tenant/IdP a login targets, @@ -18,7 +19,7 @@ type authEnvironment string // refresh_token grants). CIMD was evaluated and abandoned for this flow: // Auth0 silently drops CIMD client registration for the device_code grant. const ( - envProd authEnvironment = "prod" + envProduction authEnvironment = "production" envStaging authEnvironment = "staging" envDevelopment authEnvironment = "development" ) @@ -41,7 +42,7 @@ const ( // a development-issued token claiming the prod audience would still be // rejected by prod as untrusted. var authDomains = map[authEnvironment]string{ - envProd: "sso.linuxfoundation.org", + envProduction: "sso.linuxfoundation.org", envStaging: "linuxfoundation-staging.auth0.com", envDevelopment: "linuxfoundation-dev.auth0.com", } @@ -51,7 +52,7 @@ var authDomains = map[authEnvironment]string{ // grant. // cspell:disable -- opaque, randomly-generated Auth0 client IDs, not words. var authClientIDs = map[authEnvironment]string{ - envProd: "kkCpM0c9zJ0vNZZDDOGqcyzocOBircOn", + envProduction: "kkCpM0c9zJ0vNZZDDOGqcyzocOBircOn", envStaging: "9XzXgDfAB9O7IoHqhBj5mg4VLvdBM8ci", envDevelopment: "0TN1OElqQY146vLEPdV5qfejRKpc9IAZ", } @@ -62,7 +63,7 @@ var authClientIDs = map[authEnvironment]string{ // audience used when `lfx auth login` is run without an explicit // `--audience` override. var defaultAudiences = map[authEnvironment]string{ - envProd: "https://lfx-api.v2.cluster.lfx.dev/", + envProduction: "https://lfx-api.v2.cluster.lfx.dev/", envStaging: "https://lfx-api.staging.v2.cluster.linuxfound.info/", envDevelopment: "https://lfx-api.dev.v2.cluster.linuxfound.info/", } @@ -71,11 +72,44 @@ var defaultAudiences = map[authEnvironment]string{ // unrecognized authEnvironment value. var errInvalidEnvironment = errors.New("invalid environment") +// envAliases maps accepted short-forms to the canonical authEnvironment +// constant. Only aliases are listed here; canonical names are valid +// inputs to resolveEnvironment on their own via the authDomains lookup, +// so they don't need a duplicate entry. +var envAliases = map[string]authEnvironment{ + "prod": envProduction, + "stage": envStaging, + "stg": envStaging, + "develop": envDevelopment, + "dev": envDevelopment, +} + +// normalizeEnvironment maps an input string (canonical name or alias) to +// the canonical authEnvironment value. Unrecognized inputs are returned +// as-is so that resolveEnvironment can produce a single, consistent error. +// +// This same lookup is also applied to authEnvironment values loaded from +// a previously persisted credstore.DeviceState.Environment (see auth.go's +// loadDeviceStateForBackend, resolveAccessToken, and the `auth status` +// display): "prod" was the canonical production name before it was +// renamed to "production", and envAliases still maps it to envProduction, +// so state.json files written before that rename keep resolving +// correctly without requiring a fresh `lfx auth login`. +func normalizeEnvironment(input string) authEnvironment { + if canonical, ok := envAliases[input]; ok { + return canonical + } + return authEnvironment(input) +} + // resolveEnvironment returns the IdP domain and client ID for env. func resolveEnvironment(env authEnvironment) (domain, clientID string, err error) { domain, ok := authDomains[env] if !ok { - return "", "", fmt.Errorf("%w: %q (must be one of prod, staging, development)", errInvalidEnvironment, env) + return "", "", fmt.Errorf( + "%w: %q; see `lfx auth environments` for accepted values", + errInvalidEnvironment, env, + ) } return domain, authClientIDs[env], nil } @@ -85,7 +119,24 @@ func resolveEnvironment(env authEnvironment) (domain, clientID string, err error func defaultAudienceForEnvironment(env authEnvironment) (string, error) { audience, ok := defaultAudiences[env] if !ok { - return "", fmt.Errorf("%w: %q (must be one of prod, staging, development)", errInvalidEnvironment, env) + return "", fmt.Errorf( + "%w: %q; see `lfx auth environments` for accepted values", + errInvalidEnvironment, env, + ) } return audience, nil } + +// environmentAliases returns the aliases accepted for env's canonical +// name, sorted for stable, readable output (e.g. from `lfx auth +// environments`). +func environmentAliases(env authEnvironment) []string { + var aliases []string + for alias, canonical := range envAliases { + if canonical == env { + aliases = append(aliases, alias) + } + } + sort.Strings(aliases) + return aliases +}