From ea6f1db87177d9d79673c91f6f67fe1452ca9435 Mon Sep 17 00:00:00 2001 From: Mark McDonnell Date: Thu, 13 Jun 2024 19:47:16 +0100 Subject: [PATCH 01/15] fix(sso): re-auth on profile switch + support MAUA (#1226) * fix(sso): add select_account for MAUA (multiple account user auth) * fix(sso): re-auth * refactor(sso): only set hints if non-empty string * refactor(sso): only set select_account for profile create * refactor(sso): update code and tests to use /current_customer endpoint * refactor(sso): update error message to reflect new API call Co-authored-by: Patrick Hamann * fix(sso): remove unnecessary header * doc(sso): correct CurrentCustomerResponse annotation * doc(testutil): correct CurrentCustomerClient/CurrentCustomerResponse annotation * fix(profile/list): use corrert format type for output * fix(profile/delete): spacing around success output * fix(profile/list): spacing around success output * fix(profile/switch): make sure auth server is running --------- Co-authored-by: Patrick Hamann --- pkg/app/run.go | 2 +- pkg/commands/commands.go | 2 +- pkg/commands/profile/delete.go | 3 + pkg/commands/profile/list.go | 7 ++ pkg/commands/profile/profile_test.go | 4 + pkg/commands/profile/switch.go | 39 ++++++-- pkg/commands/sso/root.go | 139 +++++++++++++++++++++++++-- pkg/commands/sso/sso_test.go | 56 +++++++++-- pkg/config/config.go | 4 + pkg/config/config_test.go | 2 + pkg/testutil/api.go | 17 ++++ 11 files changed, 253 insertions(+), 22 deletions(-) diff --git a/pkg/app/run.go b/pkg/app/run.go index d6b4635d9..da091efa9 100644 --- a/pkg/app/run.go +++ b/pkg/app/run.go @@ -622,7 +622,7 @@ func commandCollectsData(command string) bool { // requires just the authentication server to be running. func commandRequiresAuthServer(command string) bool { switch command { - case "profile create", "profile update": + case "profile create", "profile switch", "profile update": return true } return false diff --git a/pkg/commands/commands.go b/pkg/commands/commands.go index ab305f5c9..2514e9437 100644 --- a/pkg/commands/commands.go +++ b/pkg/commands/commands.go @@ -354,7 +354,7 @@ func Define( profileCreate := profile.NewCreateCommand(profileCmdRoot.CmdClause, data, ssoCmdRoot) profileDelete := profile.NewDeleteCommand(profileCmdRoot.CmdClause, data) profileList := profile.NewListCommand(profileCmdRoot.CmdClause, data) - profileSwitch := profile.NewSwitchCommand(profileCmdRoot.CmdClause, data) + profileSwitch := profile.NewSwitchCommand(profileCmdRoot.CmdClause, data, ssoCmdRoot) profileToken := profile.NewTokenCommand(profileCmdRoot.CmdClause, data) profileUpdate := profile.NewUpdateCommand(profileCmdRoot.CmdClause, data, ssoCmdRoot) purgeCmdRoot := purge.NewRootCommand(app, data) diff --git a/pkg/commands/profile/delete.go b/pkg/commands/profile/delete.go index 68043eec2..e2aecc2e2 100644 --- a/pkg/commands/profile/delete.go +++ b/pkg/commands/profile/delete.go @@ -32,6 +32,9 @@ func (c *DeleteCommand) Exec(_ io.Reader, out io.Writer) error { if err := c.Globals.Config.Write(c.Globals.ConfigPath); err != nil { return err } + if c.Globals.Verbose() { + text.Break(out) + } text.Success(out, "Profile '%s' deleted", c.profile) if _, p := profile.Default(c.Globals.Config.Profiles); p == nil && len(c.Globals.Config.Profiles) > 0 { diff --git a/pkg/commands/profile/list.go b/pkg/commands/profile/list.go index b97a3fa53..b771fcc6b 100644 --- a/pkg/commands/profile/list.go +++ b/pkg/commands/profile/list.go @@ -56,6 +56,9 @@ func (c *ListCommand) Exec(_ io.Reader, out io.Writer) error { if p == nil { text.Warning(out, profile.NoDefaults) } else { + if c.Globals.Verbose() { + text.Break(out) + } text.Info(out, "Default profile highlighted in red.\n\n") display(name, p, out, text.BoldRed) } @@ -76,4 +79,8 @@ func display(k string, v *config.Profile, out io.Writer, style func(a ...any) st 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)) + if !auth.IsLongLivedToken(v) { + text.Output(out, "%s: %s", style("Customer ID"), v.CustomerID) + text.Output(out, "%s: %s", style("Customer Name"), v.CustomerName) + } } diff --git a/pkg/commands/profile/profile_test.go b/pkg/commands/profile/profile_test.go index f91d882b1..3acfef824 100644 --- a/pkg/commands/profile/profile_test.go +++ b/pkg/commands/profile/profile_test.go @@ -437,6 +437,8 @@ func TestProfileList(t *testing.T) { "access_token": "", "access_token_created": 0, "access_token_ttl": 0, + "customer_id": "", + "customer_name": "", "default": false, "email": "bar@example.com", "refresh_token": "", @@ -448,6 +450,8 @@ func TestProfileList(t *testing.T) { "access_token": "", "access_token_created": 0, "access_token_ttl": 0, + "customer_id": "", + "customer_name": "", "default": false, "email": "foo@example.com", "refresh_token": "", diff --git a/pkg/commands/profile/switch.go b/pkg/commands/profile/switch.go index 85bbaab80..1d104b426 100644 --- a/pkg/commands/profile/switch.go +++ b/pkg/commands/profile/switch.go @@ -6,6 +6,7 @@ import ( "io" "github.com/fastly/cli/pkg/argparser" + "github.com/fastly/cli/pkg/commands/sso" "github.com/fastly/cli/pkg/global" "github.com/fastly/cli/pkg/profile" "github.com/fastly/cli/pkg/text" @@ -16,32 +17,58 @@ type SwitchCommand struct { argparser.Base profile string + ssoCmd *sso.RootCommand } // NewSwitchCommand returns a usable command registered under the parent. -func NewSwitchCommand(parent argparser.Registerer, g *global.Data) *SwitchCommand { +func NewSwitchCommand(parent argparser.Registerer, g *global.Data, ssoCmd *sso.RootCommand) *SwitchCommand { var c SwitchCommand c.Globals = g + c.ssoCmd = ssoCmd c.CmdClause = parent.Command("switch", "Switch user profile") c.CmdClause.Arg("profile", "Profile to switch to").Short('p').Required().StringVar(&c.profile) return &c } // Exec invokes the application logic for the command. -func (c *SwitchCommand) Exec(_ io.Reader, out io.Writer) error { - var ok bool +func (c *SwitchCommand) Exec(in io.Reader, out io.Writer) error { + // We get the named profile to check if it's an SSO-based profile. + // If we're switching to an SSO-based profile, then we need to re-auth. + p := profile.Get(c.profile, c.Globals.Config.Profiles) + if p == nil { + err := fmt.Errorf(profile.DoesNotExist, c.profile) + c.Globals.ErrLog.Add(err) + return err + } + if isSSOToken(p) { + // IMPORTANT: We need to set profile fields for `sso` command. + // + // This is so the `sso` command will use this information to trigger the + // correct authentication flow. + c.ssoCmd.InvokedFromProfileSwitch = true + c.ssoCmd.ProfileSwitchName = c.profile + c.ssoCmd.ProfileSwitchEmail = p.Email + c.ssoCmd.ProfileSwitchCustomerID = p.CustomerID + c.ssoCmd.ProfileDefault = true + + err := c.ssoCmd.Exec(in, out) + if err != nil { + return fmt.Errorf("failed to authenticate: %w", err) + } + text.Success(out, "\nProfile switched to '%s'", c.profile) + return nil + } // We call SetDefault for its side effect of resetting all other profiles to have // their Default field set to false. - p, ok := profile.SetDefault(c.profile, c.Globals.Config.Profiles) + ps, ok := profile.SetDefault(c.profile, c.Globals.Config.Profiles) if !ok { msg := fmt.Sprintf(profile.DoesNotExist, c.profile) err := errors.New(msg) c.Globals.ErrLog.Add(err) return err } - - c.Globals.Config.Profiles = p + c.Globals.Config.Profiles = ps if err := c.Globals.Config.Write(c.Globals.ConfigPath); err != nil { c.Globals.ErrLog.Add(err) diff --git a/pkg/commands/sso/root.go b/pkg/commands/sso/root.go index 244de3a63..2134cb436 100644 --- a/pkg/commands/sso/root.go +++ b/pkg/commands/sso/root.go @@ -1,11 +1,15 @@ package sso import ( + "encoding/json" "errors" "fmt" "io" + "net/http" + "strconv" "time" + "github.com/fastly/cli/pkg/api/undocumented" "github.com/fastly/cli/pkg/argparser" "github.com/fastly/cli/pkg/auth" "github.com/fastly/cli/pkg/config" @@ -13,6 +17,7 @@ import ( "github.com/fastly/cli/pkg/global" "github.com/fastly/cli/pkg/profile" "github.com/fastly/cli/pkg/text" + "github.com/fastly/cli/pkg/useragent" ) // ForceReAuth indicates we want to force a re-auth of the user's session. @@ -25,6 +30,10 @@ type RootCommand struct { argparser.Base profile string + // The following fields are populated once authentication is complete. + customerID string + customerName string + // IMPORTANT: The following fields are public to the `profile` subcommands. // InvokedFromProfileCreate indicates if we should create a new profile. @@ -37,6 +46,14 @@ type RootCommand struct { InvokedFromProfileUpdate bool // ProfileUpdateName indicates the profile name to update. ProfileUpdateName string + // InvokedFromProfileSwitch indicates if we should switch a profile. + InvokedFromProfileSwitch bool + // ProfileSwitchName indicates the profile name to switch to. + ProfileSwitchName string + // ProfileSwitchEmail indicates the profile email to reference in auth URL. + ProfileSwitchEmail string + // ProfileSwitchCustomerID indicates the customer ID to reference in auth URL. + ProfileSwitchCustomerID string } // NewRootCommand returns a new command registered in the parent. @@ -91,8 +108,17 @@ func (c *RootCommand) Exec(in io.Reader, out io.Writer) error { // For creating/updating a profile we set `prompt` because we want to ensure // that another session (from a different profile) doesn't cause unexpected // errors for the user flow. This forces a re-auth. - if c.InvokedFromProfileCreate || c.InvokedFromProfileUpdate || ForceReAuth { + if c.InvokedFromProfileCreate || ForceReAuth { + c.Globals.AuthServer.SetParam("prompt", "login select_account") + } + if c.InvokedFromProfileUpdate || c.InvokedFromProfileSwitch { c.Globals.AuthServer.SetParam("prompt", "login") + if c.ProfileSwitchEmail != "" { + c.Globals.AuthServer.SetParam("login_hint", c.ProfileSwitchEmail) + } + if c.ProfileSwitchCustomerID != "" { + c.Globals.AuthServer.SetParam("account_hint", c.ProfileSwitchCustomerID) + } } authorizationURL, err := c.Globals.AuthServer.AuthURL() @@ -123,6 +149,11 @@ func (c *RootCommand) Exec(in io.Reader, out io.Writer) error { } } + err = c.processCustomer(ar) + if err != nil { + return fmt.Errorf("failed to use session token to get customer data: %w", err) + } + err = c.processProfiles(ar) if err != nil { c.Globals.ErrLog.Add(err) @@ -130,7 +161,7 @@ func (c *RootCommand) Exec(in io.Reader, out io.Writer) error { } textFn := text.Success - if c.InvokedFromProfileCreate || c.InvokedFromProfileUpdate { + if c.InvokedFromProfileCreate || c.InvokedFromProfileUpdate || c.InvokedFromProfileSwitch { textFn = text.Info } textFn(out, "Session token (persisted to your local configuration): %s", ar.SessionToken) @@ -152,6 +183,10 @@ const ( // ProfileUpdate indicates we need to update a profile using details passed in // either from the `sso` or `profile update` command. ProfileUpdate + + // ProfileSwitch indicates we need to re-authenticate and switch profiles. + // Triggered by user invoking `fastly profile switch` with an SSO-based profile. + ProfileSwitch ) // identifyProfileAndFlow identifies the profile and the specific workflow. @@ -179,6 +214,8 @@ func (c *RootCommand) identifyProfileAndFlow() (profileName string, flow Profile return c.ProfileCreateName, ProfileCreate case c.InvokedFromProfileUpdate && c.ProfileUpdateName != "": return c.ProfileUpdateName, ProfileUpdate + case c.InvokedFromProfileSwitch && c.ProfileSwitchName != "": + return c.ProfileSwitchName, ProfileSwitch case currentDefaultProfile != "": return currentDefaultProfile, ProfileUpdate case newDefaultProfile != "": @@ -188,6 +225,52 @@ func (c *RootCommand) identifyProfileAndFlow() (profileName string, flow Profile } } +func (c *RootCommand) processCustomer(ar auth.AuthorizationResult) error { + debugMode, _ := strconv.ParseBool(c.Globals.Env.DebugMode) + apiEndpoint, _ := c.Globals.APIEndpoint() + // NOTE: The endpoint is documented but not implemented in go-fastly. + data, err := undocumented.Call(undocumented.CallOptions{ + APIEndpoint: apiEndpoint, + HTTPClient: c.Globals.HTTPClient, + HTTPHeaders: []undocumented.HTTPHeader{ + { + Key: "Accept", + Value: "application/json", + }, + { + Key: "User-Agent", + Value: useragent.Name, + }, + }, + Method: http.MethodGet, + Path: "/current_customer", + Token: ar.SessionToken, + Debug: debugMode, + }) + if err != nil { + c.Globals.ErrLog.Add(err) + return fmt.Errorf("error executing current_customer API request: %w", err) + } + + var response CurrentCustomerResponse + if err := json.Unmarshal(data, &response); err != nil { + c.Globals.ErrLog.Add(err) + return fmt.Errorf("error decoding current_customer API response: %w", err) + } + + c.customerID = response.ID + c.customerName = response.Name + + return nil +} + +// CurrentCustomerResponse models the Fastly API response for the +// /current_customer endpoint. +type CurrentCustomerResponse struct { + ID string `json:"id"` + Name string `json:"name"` +} + // processProfiles updates the relevant profile with the returned token data. // // First it checks the --profile flag and the `profile` fastly.toml field. @@ -206,6 +289,11 @@ func (c *RootCommand) processProfiles(ar auth.AuthorizationResult) error { if err != nil { return fmt.Errorf("failed to update profile: %w", err) } + case ProfileSwitch: + err := c.processSwitchProfile(ar, profileName) + if err != nil { + return fmt.Errorf("failed to switch profile: %w", err) + } } if err := c.Globals.Config.Write(c.Globals.ConfigPath); err != nil { @@ -221,7 +309,14 @@ func (c *RootCommand) processCreateProfile(ar auth.AuthorizationResult, profileN isDefault = c.ProfileDefault } - c.Globals.Config.Profiles = createNewProfile(profileName, isDefault, c.Globals.Config.Profiles, ar) + c.Globals.Config.Profiles = createNewProfile( + profileName, + c.customerID, + c.customerName, + isDefault, + c.Globals.Config.Profiles, + ar, + ) // If the user wants the newly created profile to be their new default, then // we'll call Set for its side effect of resetting all other profiles to have @@ -242,17 +337,45 @@ func (c *RootCommand) processUpdateProfile(ar auth.AuthorizationResult, profileN if c.InvokedFromProfileUpdate { isDefault = c.ProfileDefault } - ps, err := editProfile(profileName, isDefault, c.Globals.Config.Profiles, ar) + ps, err := editProfile( + profileName, + c.customerID, + c.customerName, + isDefault, + c.Globals.Config.Profiles, + ar, + ) + if err != nil { + return err + } + c.Globals.Config.Profiles = ps + return nil +} + +// processSwitchProfile handles updating a profile. +func (c *RootCommand) processSwitchProfile(ar auth.AuthorizationResult, profileName string) error { + ps, err := editProfile( + profileName, + c.customerID, + c.customerName, + c.ProfileDefault, + c.Globals.Config.Profiles, + ar, + ) if err != nil { return err } + ps, ok := profile.SetDefault(profileName, ps) + if !ok { + return fmt.Errorf("failed to set '%s' to be the default profile", profileName) + } c.Globals.Config.Profiles = ps return nil } // IMPORTANT: Mutates the config.Profiles map type. // We need to return the modified type so it can be safely reassigned. -func createNewProfile(profileName string, makeDefault bool, p config.Profiles, ar auth.AuthorizationResult) config.Profiles { +func createNewProfile(profileName, customerID, customerName string, makeDefault bool, p config.Profiles, ar auth.AuthorizationResult) config.Profiles { now := time.Now().Unix() if p == nil { p = make(config.Profiles) @@ -261,6 +384,8 @@ func createNewProfile(profileName string, makeDefault bool, p config.Profiles, a AccessToken: ar.Jwt.AccessToken, AccessTokenCreated: now, AccessTokenTTL: ar.Jwt.ExpiresIn, + CustomerID: customerID, + CustomerName: customerName, Default: makeDefault, Email: ar.Email, RefreshToken: ar.Jwt.RefreshToken, @@ -276,13 +401,15 @@ func createNewProfile(profileName string, makeDefault bool, p config.Profiles, a // // IMPORTANT: Mutates the config.Profiles map type. // We need to return the modified type so it can be safely reassigned. -func editProfile(profileName string, makeDefault bool, p config.Profiles, ar auth.AuthorizationResult) (config.Profiles, error) { +func editProfile(profileName, customerID, customerName string, makeDefault bool, p config.Profiles, ar auth.AuthorizationResult) (config.Profiles, error) { ps, ok := profile.Edit(profileName, p, func(p *config.Profile) { now := time.Now().Unix() p.Default = makeDefault p.AccessToken = ar.Jwt.AccessToken p.AccessTokenCreated = now p.AccessTokenTTL = ar.Jwt.ExpiresIn + p.CustomerID = customerID + p.CustomerName = customerName p.Email = ar.Email p.RefreshToken = ar.Jwt.RefreshToken p.RefreshTokenCreated = now diff --git a/pkg/commands/sso/sso_test.go b/pkg/commands/sso/sso_test.go index 2c848d91c..b3ab720ea 100644 --- a/pkg/commands/sso/sso_test.go +++ b/pkg/commands/sso/sso_test.go @@ -9,6 +9,8 @@ import ( "testing" "time" + "github.com/fastly/go-fastly/v9/fastly" + "github.com/fastly/cli/pkg/api" "github.com/fastly/cli/pkg/app" "github.com/fastly/cli/pkg/auth" @@ -90,6 +92,7 @@ func TestSSO(t *testing.T) { "Session token (persisted to your local configuration): 123", }, }, + HTTPClient: testutil.CurrentCustomerClient(testutil.CurrentCustomerResponse), AuthResult: &auth.AuthorizationResult{ SessionToken: "123", }, @@ -110,6 +113,7 @@ func TestSSO(t *testing.T) { "Session token (persisted to your local configuration): 123", }, }, + HTTPClient: testutil.CurrentCustomerClient(testutil.CurrentCustomerResponse), AuthResult: &auth.AuthorizationResult{ SessionToken: "123", }, @@ -138,11 +142,29 @@ func TestSSO(t *testing.T) { // Otherwise no OAuth flow is happening here. { TestScenario: testutil.TestScenario{ - Args: args("whoami"), + API: mock.API{ + AllDatacentersFn: func() ([]fastly.Datacenter, error) { + return []fastly.Datacenter{ + { + Name: fastly.ToPointer("Foobar"), + Code: fastly.ToPointer("FBR"), + Group: fastly.ToPointer("Bar"), + Shield: fastly.ToPointer("Baz"), + Coordinates: &fastly.Coordinates{ + Latitude: fastly.ToPointer(float64(1)), + Longtitude: fastly.ToPointer(float64(2)), + X: fastly.ToPointer(float64(3)), + Y: fastly.ToPointer(float64(4)), + }, + }, + }, nil + }, + }, + Args: args("pops"), WantOutputs: []string{ // FIXME: Put back messaging once SSO is GA. // "is not a Fastly SSO (Single Sign-On) generated token", - "Alice Programmer ", + "{Latitude:1 Longtitude:2 X:3 Y:4}", }, }, ConfigFile: &config.File{ @@ -157,7 +179,7 @@ func TestSSO(t *testing.T) { ExpectedConfigProfile: &config.Profile{ Token: "mock-token", }, - HTTPClient: testutil.WhoamiVerifyClient(testutil.WhoamiBasicResponse), + HTTPClient: testutil.CurrentCustomerClient(testutil.CurrentCustomerResponse), }, // 7. Success processing `whoami` command. // We set an SSO token that has expired. @@ -168,7 +190,7 @@ func TestSSO(t *testing.T) { TestScenario: testutil.TestScenario{ Args: args("whoami"), WantOutput: "Your access token has expired and so has your refresh token.", - DontWantOutput: "Alice Programmer ", + DontWantOutput: "{Latitude:1 Longtitude:2 X:3 Y:4}", }, ConfigFile: &config.File{ Profiles: config.Profiles{ @@ -183,19 +205,37 @@ func TestSSO(t *testing.T) { ExpectedConfigProfile: &config.Profile{ Token: "mock-token", }, - HTTPClient: testutil.WhoamiVerifyClient(testutil.WhoamiBasicResponse), + HTTPClient: testutil.CurrentCustomerClient(testutil.CurrentCustomerResponse), }, // 8. Success processing OAuth flow via `whoami` command // We set an SSO token that has expired. // This allows us to validate the output messages. { TestScenario: testutil.TestScenario{ - Args: args("whoami"), + API: mock.API{ + AllDatacentersFn: func() ([]fastly.Datacenter, error) { + return []fastly.Datacenter{ + { + Name: fastly.ToPointer("Foobar"), + Code: fastly.ToPointer("FBR"), + Group: fastly.ToPointer("Bar"), + Shield: fastly.ToPointer("Baz"), + Coordinates: &fastly.Coordinates{ + Latitude: fastly.ToPointer(float64(1)), + Longtitude: fastly.ToPointer(float64(2)), + X: fastly.ToPointer(float64(3)), + Y: fastly.ToPointer(float64(4)), + }, + }, + }, nil + }, + }, + Args: args("pops"), WantOutputs: []string{ "Your access token has expired and so has your refresh token.", "Starting a local server to handle the authentication flow.", "Session token (persisted to your local configuration): 123", - "Alice Programmer ", + "{Latitude:1 Longtitude:2 X:3 Y:4}", }, }, AuthResult: &auth.AuthorizationResult{ @@ -214,7 +254,7 @@ func TestSSO(t *testing.T) { ExpectedConfigProfile: &config.Profile{ Token: "123", }, - HTTPClient: testutil.WhoamiVerifyClient(testutil.WhoamiBasicResponse), + HTTPClient: testutil.CurrentCustomerClient(testutil.CurrentCustomerResponse), Stdin: []string{ "Y", // when prompted to open a web browser to start authentication }, diff --git a/pkg/config/config.go b/pkg/config/config.go index 5cbfa8215..6689066de 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -148,6 +148,10 @@ type Profile struct { AccessTokenCreated int64 `toml:"access_token_created" json:"access_token_created"` // AccessTokenTTL indicates when the access token needs to be replaced. AccessTokenTTL int `toml:"access_token_ttl" json:"access_token_ttl"` + // CustomerID is the customer ID associated with the profile. + CustomerID string `toml:"customer_id" json:"customer_id"` + // CustomerName is the customer name associated with the profile. + CustomerName string `toml:"customer_name" json:"customer_name"` // Default indicates if the profile is the default profile to use. Default bool `toml:"default" json:"default"` // Email is the email address associated with the token. diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 20ab22858..c2667c3f5 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -233,6 +233,8 @@ func TestUseStatic(t *testing.T) { access_token = "" access_token_created = 0 access_token_ttl = 0 +customer_id = "" +customer_name = "" default = true email = "testing@fastly.com" refresh_token = "" diff --git a/pkg/testutil/api.go b/pkg/testutil/api.go index 5ead2bd0d..eeae2a7b1 100644 --- a/pkg/testutil/api.go +++ b/pkg/testutil/api.go @@ -8,6 +8,7 @@ import ( "github.com/fastly/go-fastly/v9/fastly" + "github.com/fastly/cli/pkg/commands/sso" "github.com/fastly/cli/pkg/commands/whoami" ) @@ -100,3 +101,19 @@ var WhoamiBasicResponse = whoami.VerifyResponse{ Scope: "global", }, } + +// CurrentCustomerClient is used by `sso` tests. +type CurrentCustomerClient sso.CurrentCustomerResponse + +// Do executes the HTTP request. +func (c CurrentCustomerClient) Do(*http.Request) (*http.Response, error) { + rec := httptest.NewRecorder() + _ = json.NewEncoder(rec).Encode(sso.CurrentCustomerResponse(c)) + return rec.Result(), nil +} + +// CurrentCustomerResponse is used by `sso` tests. +var CurrentCustomerResponse = sso.CurrentCustomerResponse{ + ID: "abc", + Name: "Computer Company", +} From 3cd12f5b9b376adc48c20ff5d12f1eb4968a1208 Mon Sep 17 00:00:00 2001 From: Mark McDonnell Date: Thu, 13 Jun 2024 19:49:47 +0100 Subject: [PATCH 02/15] v10.12.2 (#1229) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46e58ec43..29ceb5dfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG +## [v10.12.2](https://github.com/fastly/cli/releases/tag/v10.12.2) (2024-06-13) + +**Bug fixes:** + +- fix(sso): re-auth on profile switch + support MAUA [#1226](https://github.com/fastly/cli/pull/1226) + ## [v10.12.1](https://github.com/fastly/cli/releases/tag/v10.12.1) (2024-06-10) **Enhancements:** From 2008cf44ba4ccf230b3b8f9d56ce5cd9aa13fdd8 Mon Sep 17 00:00:00 2001 From: Mark McDonnell Date: Fri, 14 Jun 2024 10:27:29 +0100 Subject: [PATCH 03/15] fix(sso): correct the behaviour for direct sso invocation (#1230) --- pkg/commands/sso/root.go | 39 +++++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/pkg/commands/sso/root.go b/pkg/commands/sso/root.go index 2134cb436..09616b184 100644 --- a/pkg/commands/sso/root.go +++ b/pkg/commands/sso/root.go @@ -54,6 +54,8 @@ type RootCommand struct { ProfileSwitchEmail string // ProfileSwitchCustomerID indicates the customer ID to reference in auth URL. ProfileSwitchCustomerID string + // InvokedFromSSO is an override for anyone using the `fastly sso` directly. + InvokedFromSSO bool } // NewRootCommand returns a new command registered in the parent. @@ -108,10 +110,10 @@ func (c *RootCommand) Exec(in io.Reader, out io.Writer) error { // For creating/updating a profile we set `prompt` because we want to ensure // that another session (from a different profile) doesn't cause unexpected // errors for the user flow. This forces a re-auth. - if c.InvokedFromProfileCreate || ForceReAuth { + switch { + case c.InvokedFromProfileCreate || ForceReAuth: c.Globals.AuthServer.SetParam("prompt", "login select_account") - } - if c.InvokedFromProfileUpdate || c.InvokedFromProfileSwitch { + case c.InvokedFromProfileUpdate || c.InvokedFromProfileSwitch: c.Globals.AuthServer.SetParam("prompt", "login") if c.ProfileSwitchEmail != "" { c.Globals.AuthServer.SetParam("login_hint", c.ProfileSwitchEmail) @@ -119,6 +121,25 @@ func (c *RootCommand) Exec(in io.Reader, out io.Writer) error { if c.ProfileSwitchCustomerID != "" { c.Globals.AuthServer.SetParam("account_hint", c.ProfileSwitchCustomerID) } + default: + // Handle `fastly sso` being invoked directly. + p := profile.Get(profileName, c.Globals.Config.Profiles) + if p == nil { + err := fmt.Errorf(profile.DoesNotExist, profileName) + c.Globals.ErrLog.Add(err) + return err + } + c.Globals.AuthServer.SetParam("prompt", "login") + c.Globals.AuthServer.SetParam("login_hint", p.Email) + c.Globals.AuthServer.SetParam("account_hint", p.CustomerID) + // IMPORTANT: We must make the specified profile the default. + // If we don't, then the next command run will fail to check the token. + // Because the current profile will not be the active session. + // And we don't want to have to force the user to have re-auth again. + // Really, `fastly sso` should be a hidden command. + // And all users should use the `fastly profile ...` subcommands. + c.ProfileDefault = true + c.InvokedFromSSO = true } authorizationURL, err := c.Globals.AuthServer.AuthURL() @@ -285,10 +306,16 @@ func (c *RootCommand) processProfiles(ar auth.AuthorizationResult) error { case ProfileCreate: c.processCreateProfile(ar, profileName) case ProfileUpdate: - err := c.processUpdateProfile(ar, profileName) - if err != nil { - return fmt.Errorf("failed to update profile: %w", err) + // If a user calls `fastly sso` directly then they can be incorrectly + // identified as needing the ProfileUpdate flow. So we check for that and + // fallthrough to the ProfileSwitch otherwise. + if !c.InvokedFromSSO { + err := c.processUpdateProfile(ar, profileName) + if err != nil { + return fmt.Errorf("failed to update profile: %w", err) + } } + fallthrough case ProfileSwitch: err := c.processSwitchProfile(ar, profileName) if err != nil { From aac31d1fe18e5666a8adf1ba63444c950ccbcbea Mon Sep 17 00:00:00 2001 From: Mark McDonnell Date: Fri, 14 Jun 2024 10:56:31 +0100 Subject: [PATCH 04/15] fix(compute/deploy): dereference service number pointer (#1231) * fix(compute/deploy): dereference service number pointer * fix(compute/deploy): fix spacing * fix(profile/switch): return remediation --- pkg/commands/compute/deploy.go | 10 ++++------ pkg/commands/profile/switch.go | 15 ++++++++++----- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/pkg/commands/compute/deploy.go b/pkg/commands/compute/deploy.go index 8ee755c8a..8a325f7b5 100644 --- a/pkg/commands/compute/deploy.go +++ b/pkg/commands/compute/deploy.go @@ -160,9 +160,7 @@ func (c *DeployCommand) Exec(in io.Reader, out io.Writer) (err error) { if err != nil { return err } - if !c.Globals.Flags.NonInteractive { - text.Break(out) - } + text.Break(out) fnActivateTrial, serviceID, err := c.Setup(out) if err != nil { @@ -204,7 +202,7 @@ func (c *DeployCommand) Exec(in io.Reader, out io.Writer) (err error) { serviceVersion, err = c.ExistingServiceVersion(serviceID, out) if err != nil { if errors.Is(err, ErrPackageUnchanged) { - text.Info(out, "Skipping package deployment, local and service version are identical. (service %s, version %d) ", serviceID, serviceVersion.Number) + text.Info(out, "Skipping package deployment, local and service version are identical. (service %s, version %d) ", serviceID, fastly.ToValue(serviceVersion.Number)) return nil } return err @@ -215,6 +213,7 @@ func (c *DeployCommand) Exec(in io.Reader, out io.Writer) (err error) { } var sr ServiceResources + serviceVersionNumber := fastly.ToValue(serviceVersion.Number) // NOTE: A 'domain' resource isn't strictly part of the [setup] config. // It's part of the implementation so that we can utilise the same interface. @@ -226,12 +225,11 @@ func (c *DeployCommand) Exec(in io.Reader, out io.Writer) (err error) { PackageDomain: c.Domain, RetryLimit: 5, ServiceID: serviceID, - ServiceVersion: fastly.ToValue(serviceVersion.Number), + ServiceVersion: serviceVersionNumber, Stdin: in, Stdout: out, Verbose: c.Globals.Verbose(), } - serviceVersionNumber := fastly.ToValue(serviceVersion.Number) if err = sr.domains.Validate(); err != nil { errLogService(c.Globals.ErrLog, err, serviceID, serviceVersionNumber) return fmt.Errorf("error configuring service domains: %w", err) diff --git a/pkg/commands/profile/switch.go b/pkg/commands/profile/switch.go index 1d104b426..ce7cb37d6 100644 --- a/pkg/commands/profile/switch.go +++ b/pkg/commands/profile/switch.go @@ -1,12 +1,12 @@ package profile import ( - "errors" "fmt" "io" "github.com/fastly/cli/pkg/argparser" "github.com/fastly/cli/pkg/commands/sso" + fsterr "github.com/fastly/cli/pkg/errors" "github.com/fastly/cli/pkg/global" "github.com/fastly/cli/pkg/profile" "github.com/fastly/cli/pkg/text" @@ -38,7 +38,10 @@ func (c *SwitchCommand) Exec(in io.Reader, out io.Writer) error { if p == nil { err := fmt.Errorf(profile.DoesNotExist, c.profile) c.Globals.ErrLog.Add(err) - return err + return fsterr.RemediationError{ + Inner: err, + Remediation: fsterr.ProfileRemediation, + } } if isSSOToken(p) { // IMPORTANT: We need to set profile fields for `sso` command. @@ -63,10 +66,12 @@ func (c *SwitchCommand) Exec(in io.Reader, out io.Writer) error { // their Default field set to false. ps, ok := profile.SetDefault(c.profile, c.Globals.Config.Profiles) if !ok { - msg := fmt.Sprintf(profile.DoesNotExist, c.profile) - err := errors.New(msg) + err := fmt.Errorf(profile.DoesNotExist, c.profile) c.Globals.ErrLog.Add(err) - return err + return fsterr.RemediationError{ + Inner: err, + Remediation: fsterr.ProfileRemediation, + } } c.Globals.Config.Profiles = ps From 0f3842725595d1f4e7eee52bd2518896f3d53023 Mon Sep 17 00:00:00 2001 From: Mark McDonnell Date: Fri, 14 Jun 2024 11:42:05 +0100 Subject: [PATCH 05/15] fix(sso): update output to reflect default profile behaviour (#1232) --- pkg/commands/sso/root.go | 82 ++++++++++++++++++++---------------- pkg/commands/sso/sso_test.go | 4 +- 2 files changed, 48 insertions(+), 38 deletions(-) diff --git a/pkg/commands/sso/root.go b/pkg/commands/sso/root.go index 09616b184..1f599d6bb 100644 --- a/pkg/commands/sso/root.go +++ b/pkg/commands/sso/root.go @@ -72,41 +72,6 @@ func NewRootCommand(parent argparser.Registerer, g *global.Data) *RootCommand { func (c *RootCommand) Exec(in io.Reader, out io.Writer) error { profileName, _ := c.identifyProfileAndFlow() - // We need to prompt the user, so they know we're about to open their web - // browser, but we also need to handle the scenario where the `sso` command is - // invoked indirectly via ../../app/run.go as that package will have its own - // (similar) prompt before invoking this command. So to avoid a double prompt, - // the app package will set `SkipAuthPrompt: true`. - if !c.Globals.SkipAuthPrompt && !c.Globals.Flags.AutoYes && !c.Globals.Flags.NonInteractive { - msg := fmt.Sprintf("We're going to authenticate the '%s' profile", profileName) - text.Important(out, "%s. We need to open your browser to authenticate you.", msg) - text.Break(out) - cont, err := text.AskYesNo(out, text.BoldYellow("Do you want to continue? [y/N]: "), in) - text.Break(out) - if err != nil { - return err - } - if !cont { - return fsterr.SkipExitError{ - Skip: true, - Err: fsterr.ErrDontContinue, - } - } - } - - var serverErr error - go func() { - err := c.Globals.AuthServer.Start() - if err != nil { - serverErr = err - } - }() - if serverErr != nil { - return serverErr - } - - text.Info(out, "Starting a local server to handle the authentication flow.") - // For creating/updating a profile we set `prompt` because we want to ensure // that another session (from a different profile) doesn't cause unexpected // errors for the user flow. This forces a re-auth. @@ -122,12 +87,18 @@ func (c *RootCommand) Exec(in io.Reader, out io.Writer) error { c.Globals.AuthServer.SetParam("account_hint", c.ProfileSwitchCustomerID) } default: + if c.profile != "" { + profileName = c.profile + } // Handle `fastly sso` being invoked directly. p := profile.Get(profileName, c.Globals.Config.Profiles) if p == nil { err := fmt.Errorf(profile.DoesNotExist, profileName) c.Globals.ErrLog.Add(err) - return err + return fsterr.RemediationError{ + Inner: err, + Remediation: fsterr.ProfileRemediation, + } } c.Globals.AuthServer.SetParam("prompt", "login") c.Globals.AuthServer.SetParam("login_hint", p.Email) @@ -142,6 +113,45 @@ func (c *RootCommand) Exec(in io.Reader, out io.Writer) error { c.InvokedFromSSO = true } + // We need to prompt the user, so they know we're about to open their web + // browser, but we also need to handle the scenario where the `sso` command is + // invoked indirectly via ../../app/run.go as that package will have its own + // (similar) prompt before invoking this command. So to avoid a double prompt, + // the app package will set `SkipAuthPrompt: true`. + if !c.Globals.SkipAuthPrompt && !c.Globals.Flags.AutoYes && !c.Globals.Flags.NonInteractive { + var defaultMsg string + if c.InvokedFromSSO { + defaultMsg = " and make it the default" + } + msg := fmt.Sprintf("We're going to authenticate the '%s' profile%s", profileName, defaultMsg) + text.Important(out, "%s. We need to open your browser to authenticate you.", msg) + text.Break(out) + cont, err := text.AskYesNo(out, text.BoldYellow("Do you want to continue? [y/N]: "), in) + text.Break(out) + if err != nil { + return err + } + if !cont { + return fsterr.SkipExitError{ + Skip: true, + Err: fsterr.ErrDontContinue, + } + } + } + + var serverErr error + go func() { + err := c.Globals.AuthServer.Start() + if err != nil { + serverErr = err + } + }() + if serverErr != nil { + return serverErr + } + + text.Info(out, "Starting a local server to handle the authentication flow.") + authorizationURL, err := c.Globals.AuthServer.AuthURL() if err != nil { return fsterr.RemediationError{ diff --git a/pkg/commands/sso/sso_test.go b/pkg/commands/sso/sso_test.go index b3ab720ea..d62658b06 100644 --- a/pkg/commands/sso/sso_test.go +++ b/pkg/commands/sso/sso_test.go @@ -87,7 +87,7 @@ func TestSSO(t *testing.T) { TestScenario: testutil.TestScenario{ Args: args("sso"), WantOutputs: []string{ - "We're going to authenticate the 'user' profile.", + "We're going to authenticate the 'user' profile", "We need to open your browser to authenticate you.", "Session token (persisted to your local configuration): 123", }, @@ -108,7 +108,7 @@ func TestSSO(t *testing.T) { TestScenario: testutil.TestScenario{ Args: args("sso test_user"), WantOutputs: []string{ - "We're going to authenticate the 'test_user' profile.", + "We're going to authenticate the 'test_user' profile", "We need to open your browser to authenticate you.", "Session token (persisted to your local configuration): 123", }, From f0fe32f4c161a324024f7c040ff20961fb364df7 Mon Sep 17 00:00:00 2001 From: Mark McDonnell Date: Fri, 14 Jun 2024 11:45:29 +0100 Subject: [PATCH 06/15] v10.12.3 (#1233) --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29ceb5dfc..bba1cd981 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ # CHANGELOG +## [v10.12.3](https://github.com/fastly/cli/releases/tag/v10.12.3) (2024-06-14) + +**Bug fixes:** + +- fix(sso): correct the behaviour for direct sso invocation [#1230](https://github.com/fastly/cli/pull/1230) +- fix(compute/deploy): dereference service number pointer [#1231](https://github.com/fastly/cli/pull/1231) +- fix(sso): update output to reflect default profile behaviour [#1232](https://github.com/fastly/cli/pull/1232) + ## [v10.12.2](https://github.com/fastly/cli/releases/tag/v10.12.2) (2024-06-13) **Bug fixes:** From 4cf69de09f1040002bee83ae349abb6ea3c5a8b5 Mon Sep 17 00:00:00 2001 From: Joe Shaw Date: Mon, 17 Jun 2024 11:57:22 -0400 Subject: [PATCH 07/15] pkg/errors: fix error message for missing KV store entry key flag (#1234) --- pkg/errors/errors.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/errors/errors.go b/pkg/errors/errors.go index 091005bac..fa9168aee 100644 --- a/pkg/errors/errors.go +++ b/pkg/errors/errors.go @@ -147,7 +147,7 @@ var ErrNoSTDINData = RemediationError{ // ErrInvalidKVCombo means the user omitted either the key or value flag. var ErrInvalidKVCombo = RemediationError{ - Inner: fmt.Errorf("--key-name and --value are required"), + Inner: fmt.Errorf("--key and --value are required"), Remediation: "Please add both flags or alternatively use either --stdin or --file.", } From fb04327eb8c84bd29326decef94e389f155278d1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jun 2024 13:01:22 -0400 Subject: [PATCH 08/15] build(deps): bump golang.org/x/text from 0.15.0 to 0.16.0 (#1222) Bumps [golang.org/x/text](https://github.com/golang/text) from 0.15.0 to 0.16.0. - [Release notes](https://github.com/golang/text/releases) - [Commits](https://github.com/golang/text/compare/v0.15.0...v0.16.0) --- updated-dependencies: - dependency-name: golang.org/x/text dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 656d327e5..63919ba57 100644 --- a/go.mod +++ b/go.mod @@ -74,7 +74,7 @@ require ( golang.org/x/net v0.24.0 // indirect golang.org/x/oauth2 v0.19.0 // indirect golang.org/x/sync v0.7.0 // indirect - golang.org/x/text v0.15.0 + golang.org/x/text v0.16.0 gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index bd9d36834..86d24b643 100644 --- a/go.sum +++ b/go.sum @@ -204,8 +204,8 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= From 9785bedd4fb1073621db466ddbcfdc32836fc10b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jun 2024 13:01:49 -0400 Subject: [PATCH 09/15] build(deps): bump golang.org/x/mod from 0.17.0 to 0.18.0 (#1223) Bumps [golang.org/x/mod](https://github.com/golang/mod) from 0.17.0 to 0.18.0. - [Commits](https://github.com/golang/mod/compare/v0.17.0...v0.18.0) --- updated-dependencies: - dependency-name: golang.org/x/mod dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 63919ba57..e8ce096f3 100644 --- a/go.mod +++ b/go.mod @@ -37,7 +37,7 @@ require ( github.com/theckman/yacspin v0.13.12 golang.org/x/crypto v0.23.0 golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 - golang.org/x/mod v0.17.0 + golang.org/x/mod v0.18.0 ) require ( diff --git a/go.sum b/go.sum index 86d24b643..597763daa 100644 --- a/go.sum +++ b/go.sum @@ -158,8 +158,8 @@ golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 h1:ESSUROHIBHg7USnszlcdmjBEw golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= -golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= +golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= From e7e957bd385161869d6ea128fcc39cdfe8553792 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jun 2024 13:02:05 -0400 Subject: [PATCH 10/15] build(deps): bump golang.org/x/term from 0.20.0 to 0.21.0 (#1224) Bumps [golang.org/x/term](https://github.com/golang/term) from 0.20.0 to 0.21.0. - [Commits](https://github.com/golang/term/compare/v0.20.0...v0.21.0) --- updated-dependencies: - dependency-name: golang.org/x/term dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index e8ce096f3..7f1ab3966 100644 --- a/go.mod +++ b/go.mod @@ -22,8 +22,8 @@ require ( github.com/pierrec/lz4 v2.6.1+incompatible // indirect github.com/segmentio/textio v1.2.0 github.com/tomnomnom/linkheader v0.0.0-20180905144013-02ca5825eb80 - golang.org/x/sys v0.20.0 // indirect - golang.org/x/term v0.20.0 + golang.org/x/sys v0.21.0 // indirect + golang.org/x/term v0.21.0 ) require ( diff --git a/go.sum b/go.sum index 597763daa..1f765ef0b 100644 --- a/go.sum +++ b/go.sum @@ -189,15 +189,15 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= From 1ecff74abd1930c9a8acd0e7cf2d121ee361bb32 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jun 2024 13:02:27 -0400 Subject: [PATCH 11/15] build(deps): bump golang.org/x/crypto from 0.23.0 to 0.24.0 (#1225) Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.23.0 to 0.24.0. - [Commits](https://github.com/golang/crypto/compare/v0.23.0...v0.24.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 7f1ab3966..9fa1ad499 100644 --- a/go.mod +++ b/go.mod @@ -35,7 +35,7 @@ require ( github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 github.com/theckman/yacspin v0.13.12 - golang.org/x/crypto v0.23.0 + golang.org/x/crypto v0.24.0 golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 golang.org/x/mod v0.18.0 ) diff --git a/go.sum b/go.sum index 1f765ef0b..7441640c0 100644 --- a/go.sum +++ b/go.sum @@ -152,8 +152,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8 h1:ESSUROHIBHg7USnszlcdmjBEwdMj9VUvU+OPk4yl2mc= golang.org/x/exp v0.0.0-20240409090435-93d18d7e34b8/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= From 582a646c1809ee7353a8e1ad3fe4cb96779917d7 Mon Sep 17 00:00:00 2001 From: Dora Militaru Date: Mon, 24 Jun 2024 18:03:16 +0100 Subject: [PATCH 12/15] chore(documentation): add example format for the kv-store-entry create --file contents (#1228) * Update documentation for kv-store-entry create --file arg * Update pkg/commands/kvstoreentry/create.go Co-authored-by: Cameron Walters (cee-dub) --------- Co-authored-by: Mark McDonnell Co-authored-by: Cameron Walters (cee-dub) --- pkg/commands/kvstoreentry/create.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/commands/kvstoreentry/create.go b/pkg/commands/kvstoreentry/create.go index b01b3978e..0d422922e 100644 --- a/pkg/commands/kvstoreentry/create.go +++ b/pkg/commands/kvstoreentry/create.go @@ -37,7 +37,7 @@ func NewCreateCommand(parent argparser.Registerer, g *global.Data) *CreateComman c.CmdClause.Flag("dir", "Path to a directory containing individual files where the filename is the key and the file contents is the value").StringVar(&c.dirPath) c.CmdClause.Flag("dir-allow-hidden", "Allow hidden files (e.g. dot files) to be included (skipped by default)").BoolVar(&c.dirAllowHidden) c.CmdClause.Flag("dir-concurrency", "Limit the number of concurrent network resources allocated").Default("50").IntVar(&c.dirConcurrency) - c.CmdClause.Flag("file", "Path to a file containing individual JSON objects separated by new-line delimiter").StringVar(&c.filePath) + c.CmdClause.Flag("file", `Path to a file containing individual JSON objects (e.g., {"key":"...","value":"base64_encoded_value"}) separated by new-line delimiter`).StringVar(&c.filePath) c.RegisterFlagBool(c.JSONFlag()) // --json c.CmdClause.Flag("key", "Key name").Short('k').StringVar(&c.Input.Key) c.CmdClause.Flag("stdin", "Read new-line separated JSON stream via STDIN").BoolVar(&c.stdin) From 20976a7ab77ceaf2fbb6710dfa7ef5cb50d1a0ef Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jun 2024 13:18:42 -0400 Subject: [PATCH 13/15] build(deps): bump github.com/fastly/go-fastly/v9 from 9.5.0 to 9.7.0 (#1235) Bumps [github.com/fastly/go-fastly/v9](https://github.com/fastly/go-fastly) from 9.5.0 to 9.7.0. - [Release notes](https://github.com/fastly/go-fastly/releases) - [Changelog](https://github.com/fastly/go-fastly/blob/main/CHANGELOG.md) - [Commits](https://github.com/fastly/go-fastly/compare/v9.5.0...v9.7.0) --- updated-dependencies: - dependency-name: github.com/fastly/go-fastly/v9 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 9fa1ad499..3e49f6e36 100644 --- a/go.mod +++ b/go.mod @@ -27,7 +27,7 @@ require ( ) require ( - github.com/fastly/go-fastly/v9 v9.5.0 + github.com/fastly/go-fastly/v9 v9.7.0 github.com/hashicorp/cap v0.6.0 github.com/kennygrant/sanitize v1.2.4 github.com/mholt/archiver v3.1.1+incompatible diff --git a/go.sum b/go.sum index 7441640c0..ac60cf570 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,8 @@ github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj6 github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2 h1:S6Dco8FtAhEI/qkg/00H6RdEGC+MCy5GPiQ+xweNRFE= github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2/go.mod h1:8AuBTZBRSFqEYBPYULd+NN474/zZBLP+6WeT5S9xlAc= -github.com/fastly/go-fastly/v9 v9.5.0 h1:FOZtOA7Dn9DgKQM2hOixP2oKF/49bA5Gf0MpH1eRVUI= -github.com/fastly/go-fastly/v9 v9.5.0/go.mod h1:5w2jgJBZqQEebOwM/rRg7wutAcpDTziiMYWb/6qdM7U= +github.com/fastly/go-fastly/v9 v9.7.0 h1:RIHKQcsUT6n5kZHeDES47FsUWtoJm69/40tk5P8fi8Q= +github.com/fastly/go-fastly/v9 v9.7.0/go.mod h1:5w2jgJBZqQEebOwM/rRg7wutAcpDTziiMYWb/6qdM7U= github.com/fastly/kingpin v2.1.12-0.20191105091915-95d230a53780+incompatible h1:FhrXlfhgGCS+uc6YwyiFUt04alnjpoX7vgDKJxS6Qbk= github.com/fastly/kingpin v2.1.12-0.20191105091915-95d230a53780+incompatible/go.mod h1:U8UynVoU1SQaqD2I4ZqgYd5lx3A1ipQYn4aSt2Y5h6c= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= From d728a934101fd26853427aeb5a6df745ec640696 Mon Sep 17 00:00:00 2001 From: Ryan Doherty <111927399+rdoherty-fastly@users.noreply.github.com> Date: Mon, 24 Jun 2024 10:25:34 -0700 Subject: [PATCH 14/15] feat(tls): add optional `--key-path` parameter to `tls-custom private-key create` command (#1215) This option is mutually exclusive with --key. Co-authored-by: Kevin P. Fleming --- pkg/commands/tls/custom/privatekey/create.go | 47 ++++++++++++++++--- .../tls/custom/privatekey/privatekey_test.go | 32 ++++++++++++- .../custom/privatekey/testdata/testkey.pem | 1 + 3 files changed, 71 insertions(+), 9 deletions(-) create mode 100644 pkg/commands/tls/custom/privatekey/testdata/testkey.pem diff --git a/pkg/commands/tls/custom/privatekey/create.go b/pkg/commands/tls/custom/privatekey/create.go index 5314aebe4..d976deb1b 100644 --- a/pkg/commands/tls/custom/privatekey/create.go +++ b/pkg/commands/tls/custom/privatekey/create.go @@ -1,7 +1,10 @@ package privatekey import ( + "fmt" "io" + "os" + "path/filepath" "github.com/fastly/go-fastly/v9/fastly" @@ -17,7 +20,8 @@ func NewCreateCommand(parent argparser.Registerer, g *global.Data) *CreateComman c.Globals = g // Required. - c.CmdClause.Flag("key", "The contents of the private key. Must be a PEM-formatted key").Required().StringVar(&c.key) + c.CmdClause.Flag("key", "The contents of the private key. Must be a PEM-formatted key, mutually exclusive with --key-path").StringVar(&c.key) + c.CmdClause.Flag("key-path", "Filepath to a PEM-formatted key, mutually exclusive with --key").StringVar(&c.keyPath) c.CmdClause.Flag("name", "A customizable name for your private key").Required().StringVar(&c.name) return &c @@ -27,13 +31,17 @@ func NewCreateCommand(parent argparser.Registerer, g *global.Data) *CreateComman type CreateCommand struct { argparser.Base - key string - name string + key string + keyPath string + name string } // Exec invokes the application logic for the command. func (c *CreateCommand) Exec(_ io.Reader, out io.Writer) error { - input := c.constructInput() + input, err := c.constructInput() + if err != nil { + return err + } r, err := c.Globals.APIClient.CreatePrivateKey(input) if err != nil { @@ -48,11 +56,36 @@ func (c *CreateCommand) Exec(_ io.Reader, out io.Writer) error { } // constructInput transforms values parsed from CLI flags into an object to be used by the API client library. -func (c *CreateCommand) constructInput() *fastly.CreatePrivateKeyInput { +func (c *CreateCommand) constructInput() (*fastly.CreatePrivateKeyInput, error) { var input fastly.CreatePrivateKeyInput - input.Key = c.key + if c.keyPath == "" && c.key == "" { + return nil, fmt.Errorf("neither --key-path or --key provided, one must be provided") + } + + if c.keyPath != "" && c.key != "" { + return nil, fmt.Errorf("--key-path and --key provided, only one can be specified") + } + + if c.key != "" { + input.Key = c.key + } + + if c.keyPath != "" { + path, err := filepath.Abs(c.keyPath) + if err != nil { + return nil, fmt.Errorf("error parsing key-path '%s': %q", c.keyPath, err) + } + + data, err := os.ReadFile(path) // #nosec + if err != nil { + return nil, fmt.Errorf("error reading key-path '%s': %q", c.keyPath, err) + } + + input.Key = string(data) + } + input.Name = c.name - return &input + return &input, nil } diff --git a/pkg/commands/tls/custom/privatekey/privatekey_test.go b/pkg/commands/tls/custom/privatekey/privatekey_test.go index 91043c7b0..209f500f0 100644 --- a/pkg/commands/tls/custom/privatekey/privatekey_test.go +++ b/pkg/commands/tls/custom/privatekey/privatekey_test.go @@ -23,12 +23,18 @@ const ( ) func TestTLSCustomPrivateKeyCreate(t *testing.T) { + var content string args := testutil.Args scenarios := []testutil.TestScenario{ { - Name: "validate missing --key flag", + Name: "validate missing --key and --key-path flags", Args: args("tls-custom private-key create --name example"), - WantError: "required flag --key not provided", + WantError: "neither --key-path or --key provided, one must be provided", + }, + { + Name: "validate using both --key and --key-path flags", + Args: args("tls-custom private-key create --name example --key example --key-path foobar"), + WantError: "--key-path and --key provided, only one can be specified", }, { Name: "validate missing --name flag", @@ -39,6 +45,7 @@ func TestTLSCustomPrivateKeyCreate(t *testing.T) { Name: validateAPIError, API: mock.API{ CreatePrivateKeyFn: func(i *fastly.CreatePrivateKeyInput) (*fastly.PrivateKey, error) { + content = i.Key return nil, testutil.Err }, }, @@ -49,6 +56,7 @@ func TestTLSCustomPrivateKeyCreate(t *testing.T) { Name: validateAPISuccess, API: mock.API{ CreatePrivateKeyFn: func(i *fastly.CreatePrivateKeyInput) (*fastly.PrivateKey, error) { + content = i.Key return &fastly.PrivateKey{ ID: mockResponseID, Name: i.Name, @@ -58,6 +66,25 @@ func TestTLSCustomPrivateKeyCreate(t *testing.T) { Args: args("tls-custom private-key create --key example --name example"), WantOutput: "Created TLS Private Key 'example'", }, + { + Name: "validate custom key is submitted", + API: mock.API{ + CreatePrivateKeyFn: func(i *fastly.CreatePrivateKeyInput) (*fastly.PrivateKey, error) { + content = i.Key + return &fastly.PrivateKey{ + ID: mockResponseID, + Name: i.Name, + }, nil + }, + }, + Args: args("tls-custom private-key create --name example --key-path ./testdata/testkey.pem"), + WantOutput: "Created TLS Private Key 'example'", + }, + { + Name: "validate invalid --key-path arg", + Args: args("tls-custom private-key create --name example --key-path ............"), + WantError: "error reading key-path", + }, } for testcaseIdx := range scenarios { @@ -72,6 +99,7 @@ func TestTLSCustomPrivateKeyCreate(t *testing.T) { err := app.Run(testcase.Args, nil) testutil.AssertErrorContains(t, err, testcase.WantError) testutil.AssertStringContains(t, stdout.String(), testcase.WantOutput) + testutil.AssertPathContentFlag("key-path", testcase.WantError, testcase.Args, "testkey.pem", content, t) }) } } diff --git a/pkg/commands/tls/custom/privatekey/testdata/testkey.pem b/pkg/commands/tls/custom/privatekey/testdata/testkey.pem new file mode 100644 index 000000000..f3a59afab --- /dev/null +++ b/pkg/commands/tls/custom/privatekey/testdata/testkey.pem @@ -0,0 +1 @@ +this is a test key \ No newline at end of file From a34f38a90c3aba2ce317e9367796469cea99505a Mon Sep 17 00:00:00 2001 From: "Kevin P. Fleming" Date: Tue, 25 Jun 2024 10:47:58 -0400 Subject: [PATCH 15/15] Upgrade to goreleaser version 2. (#1236) --- .github/dependencies.txt | 2 +- .github/workflows/tag_release.yml | 2 +- .goreleaser.yml | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/dependencies.txt b/.github/dependencies.txt index 3bb60ebf5..dec6e2f7b 100644 --- a/.github/dependencies.txt +++ b/.github/dependencies.txt @@ -1,4 +1,4 @@ -github.com/goreleaser/goreleaser@latest +github.com/goreleaser/goreleaser/v2@latest github.com/mgechev/revive@latest github.com/securego/gosec/v2/cmd/gosec@latest honnef.co/go/tools/cmd/staticcheck@2023.1.7 diff --git a/.github/workflows/tag_release.yml b/.github/workflows/tag_release.yml index 6fc320f38..2f6a84cf9 100644 --- a/.github/workflows/tag_release.yml +++ b/.github/workflows/tag_release.yml @@ -39,7 +39,7 @@ jobs: with: # goreleaser version (NOT goreleaser-action version) # update inline with the Makefile - version: latest + version: v2@latest args: release --clean env: AUR_KEY: '${{ github.workspace }}/aur_key' diff --git a/.goreleaser.yml b/.goreleaser.yml index d627ccd65..544360549 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -1,5 +1,6 @@ # https://goreleaser.com/customization/project/ project_name: fastly +version: 2 # https://goreleaser.com/customization/release/ release: