From 79558250d17def96759a8da544af53f63ce40478 Mon Sep 17 00:00:00 2001 From: Integralist Date: Mon, 15 Jan 2024 12:00:33 +0000 Subject: [PATCH 1/9] feat: display SSO field when listing profiles --- pkg/app/run.go | 9 ++------- pkg/auth/auth.go | 7 +++++++ pkg/commands/profile/list.go | 2 ++ 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/pkg/app/run.go b/pkg/app/run.go index 8458db719..3d0141248 100644 --- a/pkg/app/run.go +++ b/pkg/app/run.go @@ -344,7 +344,7 @@ func processToken(cmds []argparser.Command, data *global.Data) (token string, to } // User now either has an existing SSO-based token or they want to migrate. // If a long-lived token, then trigger SSO. - if longLivedToken(profileData) { + if auth.IsLongLivedToken(profileData) { return ssoAuthentication("You've not authenticated via OAuth before", cmds, data) } // Otherwise, for an existing SSO token, check its freshness. @@ -483,7 +483,7 @@ func checkAndRefreshSSOToken(profileData *config.Profile, profileName string, da // informs the user how they can use the SSO flow. It checks if the SSO // environment variable (or flag) has been set and enables the SSO flow if so. func shouldSkipSSO(_ string, profileData *config.Profile, data *global.Data) bool { - if longLivedToken(profileData) { + if auth.IsLongLivedToken(profileData) { // Skip SSO if user hasn't indicated they want to migrate. return data.Env.UseSSO != "1" && !data.Flags.SSO // FIXME: Put back messaging once SSO is GA. @@ -501,11 +501,6 @@ func shouldSkipSSO(_ string, profileData *config.Profile, data *global.Data) boo return false // don't skip SSO } -func longLivedToken(pd *config.Profile) bool { - // If user has followed SSO flow before, then these will not be zero values. - return pd.AccessToken == "" && pd.RefreshToken == "" && pd.AccessTokenCreated == 0 && pd.RefreshTokenCreated == 0 -} - // ssoAuthentication executes the `sso` command to handle authentication. func ssoAuthentication(outputMessage string, cmds []argparser.Command, data *global.Data) (token string, tokenSource lookup.Source, err error) { for _, command := range cmds { diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index 8c9b460a0..31c28358c 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -16,6 +16,7 @@ import ( "github.com/fastly/cli/pkg/api" "github.com/fastly/cli/pkg/api/undocumented" + "github.com/fastly/cli/pkg/config" fsterr "github.com/fastly/cli/pkg/errors" ) @@ -404,3 +405,9 @@ func TokenExpired(ttl int, timestamp int64) bool { ttlAgo := time.Now().Add(-d).Unix() return timestamp < ttlAgo } + +// IsLongLivedToken identifies if profile has SSO access/refresh values set. +func IsLongLivedToken(pd *config.Profile) bool { + // If user has followed SSO flow before, then these will not be zero values. + return pd.AccessToken == "" && pd.RefreshToken == "" && pd.AccessTokenCreated == 0 && pd.RefreshTokenCreated == 0 +} diff --git a/pkg/commands/profile/list.go b/pkg/commands/profile/list.go index 14deb8a7f..b97a3fa53 100644 --- a/pkg/commands/profile/list.go +++ b/pkg/commands/profile/list.go @@ -5,6 +5,7 @@ import ( "io" "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/auth" "github.com/fastly/cli/pkg/config" fsterr "github.com/fastly/cli/pkg/errors" "github.com/fastly/cli/pkg/global" @@ -74,4 +75,5 @@ func display(k string, v *config.Profile, out io.Writer, style func(a ...any) st text.Output(out, "%s: %t", style("Default"), v.Default) text.Output(out, "%s: %s", style("Email"), v.Email) text.Output(out, "%s: %s", style("Token"), v.Token) + text.Output(out, "%s: %t", style("SSO"), !auth.IsLongLivedToken(v)) } From e23ba3c846f6da3689840aa8fedd35eacff35cd8 Mon Sep 17 00:00:00 2001 From: Integralist Date: Mon, 15 Jan 2024 18:25:25 +0000 Subject: [PATCH 2/9] fix(app): override env debug value with flag --- pkg/app/run.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/app/run.go b/pkg/app/run.go index 3d0141248..7bcef9bf9 100644 --- a/pkg/app/run.go +++ b/pkg/app/run.go @@ -207,6 +207,12 @@ func Exec(data *global.Data) error { displayAPIEndpoint(apiEndpoint, endpointSource, data.Output) } + // User can set env.DebugMode env var or the --debug-mode boolean flag. + // This will prioritise the flag over the env var. + if data.Flags.Debug { + data.Env.DebugMode = "true" + } + // NOTE: Some commands need just the auth server to be running. // But not necessarily need to process an existing token. // e.g. `profile create example_sso_user --sso` From 97d876335e2f60afa5793deb29dc6e42dae0ce31 Mon Sep 17 00:00:00 2001 From: Integralist Date: Mon, 15 Jan 2024 18:25:53 +0000 Subject: [PATCH 3/9] refactor: move profile logic to global data --- pkg/app/run.go | 35 +---------------------------------- pkg/global/global.go | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 34 deletions(-) diff --git a/pkg/app/run.go b/pkg/app/run.go index 7bcef9bf9..04062a99d 100644 --- a/pkg/app/run.go +++ b/pkg/app/run.go @@ -340,7 +340,7 @@ func processToken(cmds []argparser.Command, data *global.Data) (token string, to // So we have to presume those overrides are using a long-lived token. switch tokenSource { case lookup.SourceFile: - profileName, profileData, err := getProfile(data) + profileName, profileData, err := data.Profile() if err != nil { return "", tokenSource, err } @@ -379,39 +379,6 @@ func processToken(cmds []argparser.Command, data *global.Data) (token string, to return token, tokenSource, nil } -// getProfile identifies the profile we should extract a token from. -func getProfile(data *global.Data) (string, *config.Profile, error) { - var ( - profileData *config.Profile - found bool - name, profileName string - ) - switch { - case data.Flags.Profile != "": // --profile - profileName = data.Flags.Profile - case data.Manifest.File.Profile != "": // `profile` field in fastly.toml - profileName = data.Manifest.File.Profile - default: - profileName = "default" - } - for name, profileData = range data.Config.Profiles { - if (profileName == "default" && profileData.Default) || name == profileName { - // Once we find the default profile we can update the variable to be the - // associated profile name so later on we can use that information to - // update the specific profile. - if profileName == "default" { - profileName = name - } - found = true - break - } - } - if !found { - return "", nil, fmt.Errorf("failed to locate '%s' profile", profileName) - } - return profileName, profileData, nil -} - // checkAndRefreshSSOToken refreshes the access/refresh tokens if expired. func checkAndRefreshSSOToken(profileData *config.Profile, profileName string, data *global.Data) (reauth bool, err error) { // Access Token has expired diff --git a/pkg/global/global.go b/pkg/global/global.go index 4918815a5..2336a56d8 100644 --- a/pkg/global/global.go +++ b/pkg/global/global.go @@ -1,6 +1,7 @@ package global import ( + "fmt" "io" "github.com/fastly/cli/pkg/api" @@ -88,6 +89,39 @@ type Data struct { Versioners Versioners } +// Profile identifies the current profile (if any). +func (d *Data) Profile() (string, *config.Profile, error) { + var ( + profileData *config.Profile + found bool + name, profileName string + ) + switch { + case d.Flags.Profile != "": // --profile + profileName = d.Flags.Profile + case d.Manifest.File.Profile != "": // `profile` field in fastly.toml + profileName = d.Manifest.File.Profile + default: + profileName = "default" // fallback to locating the default profile + } + for name, profileData = range d.Config.Profiles { + if (profileName == "default" && profileData.Default) || name == profileName { + // Once we find the default profile we can update the variable to be the + // associated profile name so later on we can use that information to + // update the specific profile. + if profileName == "default" { + profileName = name + } + found = true + break + } + } + if !found { + return "", nil, fmt.Errorf("failed to locate '%s' profile", profileName) + } + return profileName, profileData, nil +} + // Token yields the Fastly API token. // // Order of precedence: From 0ee2bd8839045794dc3ce49a6d91e20dc130e8e6 Mon Sep 17 00:00:00 2001 From: Integralist Date: Mon, 15 Jan 2024 18:26:19 +0000 Subject: [PATCH 4/9] fix(app): update command list that requires auth server --- pkg/app/run.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pkg/app/run.go b/pkg/app/run.go index 04062a99d..9ba83ad4d 100644 --- a/pkg/app/run.go +++ b/pkg/app/run.go @@ -611,7 +611,11 @@ func commandCollectsData(command string) bool { // commandRequiresAuthServer determines if the command to be executed is one that // requires just the authentication server to be running. func commandRequiresAuthServer(command string) bool { - return command == "profile create" + switch command { + case "profile create", "profile update": + return true + } + return false } // commandRequiresToken determines if the command to be executed is one that From 681ab21084ec90681aa6c2c44507774bfc807086 Mon Sep 17 00:00:00 2001 From: Integralist Date: Mon, 15 Jan 2024 18:26:57 +0000 Subject: [PATCH 5/9] refactor(profile): rename subcommand variable --- pkg/commands/profile/create.go | 14 +++++++------- pkg/commands/profile/update.go | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pkg/commands/profile/create.go b/pkg/commands/profile/create.go index a6720051b..8cca5fc1a 100644 --- a/pkg/commands/profile/create.go +++ b/pkg/commands/profile/create.go @@ -24,7 +24,7 @@ import ( // CreateCommand represents a Kingpin command. type CreateCommand struct { argparser.Base - authCmd *sso.RootCommand + ssoCmd *sso.RootCommand automationToken bool profile string @@ -32,10 +32,10 @@ type CreateCommand struct { } // NewCreateCommand returns a new command registered in the parent. -func NewCreateCommand(parent argparser.Registerer, g *global.Data, authCmd *sso.RootCommand) *CreateCommand { +func NewCreateCommand(parent argparser.Registerer, g *global.Data, ssoCmd *sso.RootCommand) *CreateCommand { var c CreateCommand c.Globals = g - c.authCmd = authCmd + c.ssoCmd = ssoCmd c.CmdClause = parent.Command("create", "Create user profile") c.CmdClause.Arg("profile", "Profile to create (default 'user')").Default(profile.DefaultName).Short('p').StringVar(&c.profile) c.CmdClause.Flag("automation-token", "Expected input will be an 'automation token' instead of a 'user token'").BoolVar(&c.automationToken) @@ -80,11 +80,11 @@ func (c *CreateCommand) Exec(in io.Reader, out io.Writer) (err error) { // // This is so the `sso` command will use this information to create // a new 'non-default' profile. - c.authCmd.InvokedFromProfileCreate = true - c.authCmd.ProfileCreateName = c.profile - c.authCmd.ProfileDefault = makeDefault + c.ssoCmd.InvokedFromProfileCreate = true + c.ssoCmd.ProfileCreateName = c.profile + c.ssoCmd.ProfileDefault = makeDefault - err = c.authCmd.Exec(in, out) + err = c.ssoCmd.Exec(in, out) if err != nil { return fmt.Errorf("failed to authenticate: %w", err) } diff --git a/pkg/commands/profile/update.go b/pkg/commands/profile/update.go index a74ad6528..49d362d07 100644 --- a/pkg/commands/profile/update.go +++ b/pkg/commands/profile/update.go @@ -20,7 +20,7 @@ import ( // UpdateCommand represents a Kingpin command. type UpdateCommand struct { argparser.Base - authCmd *sso.RootCommand + ssoCmd *sso.RootCommand automationToken bool profile string @@ -28,10 +28,10 @@ type UpdateCommand struct { } // NewUpdateCommand returns a usable command registered under the parent. -func NewUpdateCommand(parent argparser.Registerer, g *global.Data, authCmd *sso.RootCommand) *UpdateCommand { +func NewUpdateCommand(parent argparser.Registerer, g *global.Data, ssoCmd *sso.RootCommand) *UpdateCommand { var c UpdateCommand c.Globals = g - c.authCmd = authCmd + c.ssoCmd = ssoCmd c.CmdClause = parent.Command("update", "Update user profile") c.CmdClause.Arg("profile", "Profile to update (defaults to the currently active profile)").Short('p').StringVar(&c.profile) c.CmdClause.Flag("automation-token", "Expected input will be an 'automation token' instead of a 'user token'").BoolVar(&c.automationToken) @@ -121,13 +121,13 @@ func (c *UpdateCommand) updateToken(profileName string, p *config.Profile, in io // // This is so the `sso` command will use this information to update // the specific profile. - c.authCmd.InvokedFromProfileUpdate = true - c.authCmd.ProfileUpdateName = profileName - c.authCmd.ProfileDefault = false // set to false, as later we prompt for this + c.ssoCmd.InvokedFromProfileUpdate = true + c.ssoCmd.ProfileUpdateName = profileName + c.ssoCmd.ProfileDefault = false // set to false, as later we prompt for this // NOTE: The `sso` command already handles writing config back to disk. // So unlike `c.staticTokenFlow` (below) we don't have to do that here. - err := c.authCmd.Exec(in, out) + err := c.ssoCmd.Exec(in, out) if err != nil { return fmt.Errorf("failed to authenticate: %w", err) } From 4873301d92dc2abaa0c353d6e743bb3d071cdd90 Mon Sep 17 00:00:00 2001 From: Integralist Date: Mon, 15 Jan 2024 18:27:56 +0000 Subject: [PATCH 6/9] fix(sso): check given profile exists --- pkg/commands/sso/root.go | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/commands/sso/root.go b/pkg/commands/sso/root.go index 8aa74fe87..7bfdcf2bc 100644 --- a/pkg/commands/sso/root.go +++ b/pkg/commands/sso/root.go @@ -40,7 +40,7 @@ func NewRootCommand(parent argparser.Registerer, g *global.Data) *RootCommand { var c RootCommand c.Globals = g // FIXME: Unhide this command once SSO is GA. - c.CmdClause = parent.Command("sso", "Single Sign-On authentication").Hidden() + c.CmdClause = parent.Command("sso", "Single Sign-On authentication (defaults to current profile)").Hidden() c.CmdClause.Arg("profile", "Profile to authenticate (i.e. create/update a token for)").Short('p').StringVar(&c.profile) return &c } @@ -153,7 +153,6 @@ func (c *RootCommand) identifyProfileAndFlow() (profileName string, flow Profile } currentDefaultProfile, _ := profile.Default(c.Globals.Config.Profiles) - var newDefaultProfile string if currentDefaultProfile == "" && len(c.Globals.Config.Profiles) > 0 { newDefaultProfile, c.Globals.Config.Profiles = profile.SetADefault(c.Globals.Config.Profiles) @@ -162,7 +161,7 @@ func (c *RootCommand) identifyProfileAndFlow() (profileName string, flow Profile switch { case profileOverride != "": return profileOverride, ProfileUpdate - case c.profile != "": + case c.profile != "" && profile.Get(c.profile, c.Globals.Config.Profiles) != nil: return c.profile, ProfileUpdate case c.InvokedFromProfileCreate && c.ProfileCreateName != "": return c.ProfileCreateName, ProfileCreate @@ -186,6 +185,7 @@ func (c *RootCommand) identifyProfileAndFlow() (profileName string, flow Profile func (c *RootCommand) processProfiles(ar auth.AuthorizationResult) error { profileName, flow := c.identifyProfileAndFlow() + //nolint:exhaustive switch flow { case ProfileCreate: c.processCreateProfile(ar, profileName) @@ -230,7 +230,6 @@ func (c *RootCommand) processUpdateProfile(ar auth.AuthorizationResult, profileN if c.InvokedFromProfileUpdate { isDefault = c.ProfileDefault } - ps, err := editProfile(profileName, isDefault, c.Globals.Config.Profiles, ar) if err != nil { return err From 8a0b594cf97f8496c75e0a084835df3e1fc1a3cc Mon Sep 17 00:00:00 2001 From: Integralist Date: Tue, 16 Jan 2024 10:41:05 +0000 Subject: [PATCH 7/9] refactor(app): display well-known url in error --- pkg/app/run.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/app/run.go b/pkg/app/run.go index 9ba83ad4d..b521e25bc 100644 --- a/pkg/app/run.go +++ b/pkg/app/run.go @@ -647,7 +647,7 @@ func configureAuth(apiEndpoint string, args []string, f config.File, c api.HTTPC resp, err := c.Do(req) if err != nil { - return nil, fmt.Errorf("failed to request OpenID Connect .well-known metadata: %w", err) + return nil, fmt.Errorf("failed to request OpenID Connect .well-known metadata (%s): %w", metadataEndpoint, err) } openIDConfig, err := io.ReadAll(resp.Body) From 726198c558d02e549217c4197023cec62a18175b Mon Sep 17 00:00:00 2001 From: Integralist Date: Wed, 17 Jan 2024 11:08:56 +0000 Subject: [PATCH 8/9] feat(auth): add debug-mode support --- pkg/auth/auth.go | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index 31c28358c..563b8bbdf 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -119,10 +119,23 @@ func (s Server) GetJWT(authorizationCode string) (JWT, error) { if err != nil { return JWT{}, err } - req.Header.Add("content-type", "application/x-www-form-urlencoded") + debug, _ := strconv.ParseBool(s.DebugMode) + if debug { + rc := req.Clone(context.Background()) + rc.Header.Set("Fastly-Key", "REDACTED") + dump, _ := httputil.DumpRequest(rc, true) + fmt.Printf("GetJWT request dump:\n\n%#v\n\n", string(dump)) + } + res, err := http.DefaultClient.Do(req) + + if debug && res != nil { + dump, _ := httputil.DumpResponse(res, true) + fmt.Printf("GetJWT response dump:\n\n%#v\n\n", string(dump)) + } + if err != nil { return JWT{}, err } @@ -325,10 +338,23 @@ func (s *Server) RefreshAccessToken(refreshToken string) (JWT, error) { if err != nil { return JWT{}, err } - req.Header.Add("content-type", "application/x-www-form-urlencoded") + debug, _ := strconv.ParseBool(s.DebugMode) + if debug { + rc := req.Clone(context.Background()) + rc.Header.Set("Fastly-Key", "REDACTED") + dump, _ := httputil.DumpRequest(rc, true) + fmt.Printf("RefreshAccessToken request dump:\n\n%#v\n\n", string(dump)) + } + res, err := http.DefaultClient.Do(req) + + if debug && res != nil { + dump, _ := httputil.DumpResponse(res, true) + fmt.Printf("RefreshAccessToken response dump:\n\n%#v\n\n", string(dump)) + } + if err != nil { return JWT{}, err } From 4da30260e449204a7a9d99482d089d87b4f0f622 Mon Sep 17 00:00:00 2001 From: Integralist Date: Wed, 17 Jan 2024 11:47:36 +0000 Subject: [PATCH 9/9] fix(auth): remove unused package --- pkg/auth/auth.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/auth/auth.go b/pkg/auth/auth.go index 563b8bbdf..9e8ab0d97 100644 --- a/pkg/auth/auth.go +++ b/pkg/auth/auth.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net/http" + "net/http/httputil" "strconv" "strings" "time"