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 629bfe5ca..6ffcc7a3d 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: diff --git a/CHANGELOG.md b/CHANGELOG.md index 46e58ec43..bba1cd981 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,19 @@ # 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:** + +- 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:** diff --git a/go.mod b/go.mod index 656d327e5..3e49f6e36 100644 --- a/go.mod +++ b/go.mod @@ -22,12 +22,12 @@ 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 ( - 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 @@ -35,9 +35,9 @@ 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.17.0 + golang.org/x/mod v0.18.0 ) require ( @@ -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..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= @@ -152,14 +152,14 @@ 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= 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= @@ -189,23 +189,23 @@ 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= 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= 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/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/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) 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..ce7cb37d6 100644 --- a/pkg/commands/profile/switch.go +++ b/pkg/commands/profile/switch.go @@ -1,11 +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" @@ -16,32 +17,63 @@ 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 fsterr.RemediationError{ + Inner: err, + Remediation: fsterr.ProfileRemediation, + } + } + 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) + 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 = 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..1f599d6bb 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,16 @@ 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 + // InvokedFromSSO is an override for anyone using the `fastly sso` directly. + InvokedFromSSO bool } // NewRootCommand returns a new command registered in the parent. @@ -53,13 +72,58 @@ func NewRootCommand(parent argparser.Registerer, g *global.Data) *RootCommand { func (c *RootCommand) Exec(in io.Reader, out io.Writer) error { profileName, _ := c.identifyProfileAndFlow() + // 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. + switch { + case c.InvokedFromProfileCreate || ForceReAuth: + c.Globals.AuthServer.SetParam("prompt", "login select_account") + case 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) + } + 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 fsterr.RemediationError{ + Inner: err, + Remediation: fsterr.ProfileRemediation, + } + } + 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 + } + // 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) + 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) @@ -88,13 +152,6 @@ func (c *RootCommand) Exec(in io.Reader, out io.Writer) error { 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. - if c.InvokedFromProfileCreate || c.InvokedFromProfileUpdate || ForceReAuth { - c.Globals.AuthServer.SetParam("prompt", "login") - } - authorizationURL, err := c.Globals.AuthServer.AuthURL() if err != nil { return fsterr.RemediationError{ @@ -123,6 +180,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 +192,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 +214,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 +245,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 +256,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. @@ -202,9 +316,20 @@ func (c *RootCommand) processProfiles(ar auth.AuthorizationResult) error { case ProfileCreate: c.processCreateProfile(ar, profileName) case ProfileUpdate: - err := c.processUpdateProfile(ar, profileName) + // 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 { - return fmt.Errorf("failed to update profile: %w", err) + return fmt.Errorf("failed to switch profile: %w", err) } } @@ -221,7 +346,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 +374,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 +421,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 +438,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..d62658b06 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" @@ -85,11 +87,12 @@ 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", }, }, + HTTPClient: testutil.CurrentCustomerClient(testutil.CurrentCustomerResponse), AuthResult: &auth.AuthorizationResult{ SessionToken: "123", }, @@ -105,11 +108,12 @@ 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", }, }, + 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/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 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/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.", } 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", +}