Skip to content

Commit bfcf92c

Browse files
authored
fix: update list of commands that require auth server (#1120)
* feat: display SSO field when listing profiles * fix(app): override env debug value with flag * refactor: move profile logic to global data * fix(app): update command list that requires auth server * refactor(profile): rename subcommand variable * fix(sso): check given profile exists * refactor(app): display well-known url in error * feat(auth): add debug-mode support * fix(auth): remove unused package
1 parent a1a5d99 commit bfcf92c

File tree

7 files changed

+104
-63
lines changed

7 files changed

+104
-63
lines changed

pkg/app/run.go

+15-43
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,12 @@ func Exec(data *global.Data) error {
207207
displayAPIEndpoint(apiEndpoint, endpointSource, data.Output)
208208
}
209209

210+
// User can set env.DebugMode env var or the --debug-mode boolean flag.
211+
// This will prioritise the flag over the env var.
212+
if data.Flags.Debug {
213+
data.Env.DebugMode = "true"
214+
}
215+
210216
// NOTE: Some commands need just the auth server to be running.
211217
// But not necessarily need to process an existing token.
212218
// e.g. `profile create example_sso_user --sso`
@@ -334,7 +340,7 @@ func processToken(cmds []argparser.Command, data *global.Data) (token string, to
334340
// So we have to presume those overrides are using a long-lived token.
335341
switch tokenSource {
336342
case lookup.SourceFile:
337-
profileName, profileData, err := getProfile(data)
343+
profileName, profileData, err := data.Profile()
338344
if err != nil {
339345
return "", tokenSource, err
340346
}
@@ -344,7 +350,7 @@ func processToken(cmds []argparser.Command, data *global.Data) (token string, to
344350
}
345351
// User now either has an existing SSO-based token or they want to migrate.
346352
// If a long-lived token, then trigger SSO.
347-
if longLivedToken(profileData) {
353+
if auth.IsLongLivedToken(profileData) {
348354
return ssoAuthentication("You've not authenticated via OAuth before", cmds, data)
349355
}
350356
// Otherwise, for an existing SSO token, check its freshness.
@@ -373,39 +379,6 @@ func processToken(cmds []argparser.Command, data *global.Data) (token string, to
373379
return token, tokenSource, nil
374380
}
375381

376-
// getProfile identifies the profile we should extract a token from.
377-
func getProfile(data *global.Data) (string, *config.Profile, error) {
378-
var (
379-
profileData *config.Profile
380-
found bool
381-
name, profileName string
382-
)
383-
switch {
384-
case data.Flags.Profile != "": // --profile
385-
profileName = data.Flags.Profile
386-
case data.Manifest.File.Profile != "": // `profile` field in fastly.toml
387-
profileName = data.Manifest.File.Profile
388-
default:
389-
profileName = "default"
390-
}
391-
for name, profileData = range data.Config.Profiles {
392-
if (profileName == "default" && profileData.Default) || name == profileName {
393-
// Once we find the default profile we can update the variable to be the
394-
// associated profile name so later on we can use that information to
395-
// update the specific profile.
396-
if profileName == "default" {
397-
profileName = name
398-
}
399-
found = true
400-
break
401-
}
402-
}
403-
if !found {
404-
return "", nil, fmt.Errorf("failed to locate '%s' profile", profileName)
405-
}
406-
return profileName, profileData, nil
407-
}
408-
409382
// checkAndRefreshSSOToken refreshes the access/refresh tokens if expired.
410383
func checkAndRefreshSSOToken(profileData *config.Profile, profileName string, data *global.Data) (reauth bool, err error) {
411384
// Access Token has expired
@@ -483,7 +456,7 @@ func checkAndRefreshSSOToken(profileData *config.Profile, profileName string, da
483456
// informs the user how they can use the SSO flow. It checks if the SSO
484457
// environment variable (or flag) has been set and enables the SSO flow if so.
485458
func shouldSkipSSO(_ string, profileData *config.Profile, data *global.Data) bool {
486-
if longLivedToken(profileData) {
459+
if auth.IsLongLivedToken(profileData) {
487460
// Skip SSO if user hasn't indicated they want to migrate.
488461
return data.Env.UseSSO != "1" && !data.Flags.SSO
489462
// FIXME: Put back messaging once SSO is GA.
@@ -501,11 +474,6 @@ func shouldSkipSSO(_ string, profileData *config.Profile, data *global.Data) boo
501474
return false // don't skip SSO
502475
}
503476

504-
func longLivedToken(pd *config.Profile) bool {
505-
// If user has followed SSO flow before, then these will not be zero values.
506-
return pd.AccessToken == "" && pd.RefreshToken == "" && pd.AccessTokenCreated == 0 && pd.RefreshTokenCreated == 0
507-
}
508-
509477
// ssoAuthentication executes the `sso` command to handle authentication.
510478
func ssoAuthentication(outputMessage string, cmds []argparser.Command, data *global.Data) (token string, tokenSource lookup.Source, err error) {
511479
for _, command := range cmds {
@@ -643,7 +611,11 @@ func commandCollectsData(command string) bool {
643611
// commandRequiresAuthServer determines if the command to be executed is one that
644612
// requires just the authentication server to be running.
645613
func commandRequiresAuthServer(command string) bool {
646-
return command == "profile create"
614+
switch command {
615+
case "profile create", "profile update":
616+
return true
617+
}
618+
return false
647619
}
648620

649621
// commandRequiresToken determines if the command to be executed is one that
@@ -675,7 +647,7 @@ func configureAuth(apiEndpoint string, args []string, f config.File, c api.HTTPC
675647

676648
resp, err := c.Do(req)
677649
if err != nil {
678-
return nil, fmt.Errorf("failed to request OpenID Connect .well-known metadata: %w", err)
650+
return nil, fmt.Errorf("failed to request OpenID Connect .well-known metadata (%s): %w", metadataEndpoint, err)
679651
}
680652

681653
openIDConfig, err := io.ReadAll(resp.Body)

pkg/auth/auth.go

+36-2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"io"
99
"net/http"
10+
"net/http/httputil"
1011
"strconv"
1112
"strings"
1213
"time"
@@ -16,6 +17,7 @@ import (
1617

1718
"github.com/fastly/cli/pkg/api"
1819
"github.com/fastly/cli/pkg/api/undocumented"
20+
"github.com/fastly/cli/pkg/config"
1921
fsterr "github.com/fastly/cli/pkg/errors"
2022
)
2123

@@ -118,10 +120,23 @@ func (s Server) GetJWT(authorizationCode string) (JWT, error) {
118120
if err != nil {
119121
return JWT{}, err
120122
}
121-
122123
req.Header.Add("content-type", "application/x-www-form-urlencoded")
123124

125+
debug, _ := strconv.ParseBool(s.DebugMode)
126+
if debug {
127+
rc := req.Clone(context.Background())
128+
rc.Header.Set("Fastly-Key", "REDACTED")
129+
dump, _ := httputil.DumpRequest(rc, true)
130+
fmt.Printf("GetJWT request dump:\n\n%#v\n\n", string(dump))
131+
}
132+
124133
res, err := http.DefaultClient.Do(req)
134+
135+
if debug && res != nil {
136+
dump, _ := httputil.DumpResponse(res, true)
137+
fmt.Printf("GetJWT response dump:\n\n%#v\n\n", string(dump))
138+
}
139+
125140
if err != nil {
126141
return JWT{}, err
127142
}
@@ -324,10 +339,23 @@ func (s *Server) RefreshAccessToken(refreshToken string) (JWT, error) {
324339
if err != nil {
325340
return JWT{}, err
326341
}
327-
328342
req.Header.Add("content-type", "application/x-www-form-urlencoded")
329343

344+
debug, _ := strconv.ParseBool(s.DebugMode)
345+
if debug {
346+
rc := req.Clone(context.Background())
347+
rc.Header.Set("Fastly-Key", "REDACTED")
348+
dump, _ := httputil.DumpRequest(rc, true)
349+
fmt.Printf("RefreshAccessToken request dump:\n\n%#v\n\n", string(dump))
350+
}
351+
330352
res, err := http.DefaultClient.Do(req)
353+
354+
if debug && res != nil {
355+
dump, _ := httputil.DumpResponse(res, true)
356+
fmt.Printf("RefreshAccessToken response dump:\n\n%#v\n\n", string(dump))
357+
}
358+
331359
if err != nil {
332360
return JWT{}, err
333361
}
@@ -404,3 +432,9 @@ func TokenExpired(ttl int, timestamp int64) bool {
404432
ttlAgo := time.Now().Add(-d).Unix()
405433
return timestamp < ttlAgo
406434
}
435+
436+
// IsLongLivedToken identifies if profile has SSO access/refresh values set.
437+
func IsLongLivedToken(pd *config.Profile) bool {
438+
// If user has followed SSO flow before, then these will not be zero values.
439+
return pd.AccessToken == "" && pd.RefreshToken == "" && pd.AccessTokenCreated == 0 && pd.RefreshTokenCreated == 0
440+
}

pkg/commands/profile/create.go

+7-7
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,18 @@ import (
2424
// CreateCommand represents a Kingpin command.
2525
type CreateCommand struct {
2626
argparser.Base
27-
authCmd *sso.RootCommand
27+
ssoCmd *sso.RootCommand
2828

2929
automationToken bool
3030
profile string
3131
sso bool
3232
}
3333

3434
// NewCreateCommand returns a new command registered in the parent.
35-
func NewCreateCommand(parent argparser.Registerer, g *global.Data, authCmd *sso.RootCommand) *CreateCommand {
35+
func NewCreateCommand(parent argparser.Registerer, g *global.Data, ssoCmd *sso.RootCommand) *CreateCommand {
3636
var c CreateCommand
3737
c.Globals = g
38-
c.authCmd = authCmd
38+
c.ssoCmd = ssoCmd
3939
c.CmdClause = parent.Command("create", "Create user profile")
4040
c.CmdClause.Arg("profile", "Profile to create (default 'user')").Default(profile.DefaultName).Short('p').StringVar(&c.profile)
4141
c.CmdClause.Flag("automation-token", "Expected input will be an 'automation token' instead of a 'user token'").BoolVar(&c.automationToken)
@@ -80,11 +80,11 @@ func (c *CreateCommand) Exec(in io.Reader, out io.Writer) (err error) {
8080
//
8181
// This is so the `sso` command will use this information to create
8282
// a new 'non-default' profile.
83-
c.authCmd.InvokedFromProfileCreate = true
84-
c.authCmd.ProfileCreateName = c.profile
85-
c.authCmd.ProfileDefault = makeDefault
83+
c.ssoCmd.InvokedFromProfileCreate = true
84+
c.ssoCmd.ProfileCreateName = c.profile
85+
c.ssoCmd.ProfileDefault = makeDefault
8686

87-
err = c.authCmd.Exec(in, out)
87+
err = c.ssoCmd.Exec(in, out)
8888
if err != nil {
8989
return fmt.Errorf("failed to authenticate: %w", err)
9090
}

pkg/commands/profile/list.go

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"io"
66

77
"github.com/fastly/cli/pkg/argparser"
8+
"github.com/fastly/cli/pkg/auth"
89
"github.com/fastly/cli/pkg/config"
910
fsterr "github.com/fastly/cli/pkg/errors"
1011
"github.com/fastly/cli/pkg/global"
@@ -74,4 +75,5 @@ func display(k string, v *config.Profile, out io.Writer, style func(a ...any) st
7475
text.Output(out, "%s: %t", style("Default"), v.Default)
7576
text.Output(out, "%s: %s", style("Email"), v.Email)
7677
text.Output(out, "%s: %s", style("Token"), v.Token)
78+
text.Output(out, "%s: %t", style("SSO"), !auth.IsLongLivedToken(v))
7779
}

pkg/commands/profile/update.go

+7-7
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,18 @@ import (
2020
// UpdateCommand represents a Kingpin command.
2121
type UpdateCommand struct {
2222
argparser.Base
23-
authCmd *sso.RootCommand
23+
ssoCmd *sso.RootCommand
2424

2525
automationToken bool
2626
profile string
2727
sso bool
2828
}
2929

3030
// NewUpdateCommand returns a usable command registered under the parent.
31-
func NewUpdateCommand(parent argparser.Registerer, g *global.Data, authCmd *sso.RootCommand) *UpdateCommand {
31+
func NewUpdateCommand(parent argparser.Registerer, g *global.Data, ssoCmd *sso.RootCommand) *UpdateCommand {
3232
var c UpdateCommand
3333
c.Globals = g
34-
c.authCmd = authCmd
34+
c.ssoCmd = ssoCmd
3535
c.CmdClause = parent.Command("update", "Update user profile")
3636
c.CmdClause.Arg("profile", "Profile to update (defaults to the currently active profile)").Short('p').StringVar(&c.profile)
3737
c.CmdClause.Flag("automation-token", "Expected input will be an 'automation token' instead of a 'user token'").BoolVar(&c.automationToken)
@@ -121,13 +121,13 @@ func (c *UpdateCommand) updateToken(profileName string, p *config.Profile, in io
121121
//
122122
// This is so the `sso` command will use this information to update
123123
// the specific profile.
124-
c.authCmd.InvokedFromProfileUpdate = true
125-
c.authCmd.ProfileUpdateName = profileName
126-
c.authCmd.ProfileDefault = false // set to false, as later we prompt for this
124+
c.ssoCmd.InvokedFromProfileUpdate = true
125+
c.ssoCmd.ProfileUpdateName = profileName
126+
c.ssoCmd.ProfileDefault = false // set to false, as later we prompt for this
127127

128128
// NOTE: The `sso` command already handles writing config back to disk.
129129
// So unlike `c.staticTokenFlow` (below) we don't have to do that here.
130-
err := c.authCmd.Exec(in, out)
130+
err := c.ssoCmd.Exec(in, out)
131131
if err != nil {
132132
return fmt.Errorf("failed to authenticate: %w", err)
133133
}

pkg/commands/sso/root.go

+3-4
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ func NewRootCommand(parent argparser.Registerer, g *global.Data) *RootCommand {
4040
var c RootCommand
4141
c.Globals = g
4242
// FIXME: Unhide this command once SSO is GA.
43-
c.CmdClause = parent.Command("sso", "Single Sign-On authentication").Hidden()
43+
c.CmdClause = parent.Command("sso", "Single Sign-On authentication (defaults to current profile)").Hidden()
4444
c.CmdClause.Arg("profile", "Profile to authenticate (i.e. create/update a token for)").Short('p').StringVar(&c.profile)
4545
return &c
4646
}
@@ -153,7 +153,6 @@ func (c *RootCommand) identifyProfileAndFlow() (profileName string, flow Profile
153153
}
154154

155155
currentDefaultProfile, _ := profile.Default(c.Globals.Config.Profiles)
156-
157156
var newDefaultProfile string
158157
if currentDefaultProfile == "" && len(c.Globals.Config.Profiles) > 0 {
159158
newDefaultProfile, c.Globals.Config.Profiles = profile.SetADefault(c.Globals.Config.Profiles)
@@ -162,7 +161,7 @@ func (c *RootCommand) identifyProfileAndFlow() (profileName string, flow Profile
162161
switch {
163162
case profileOverride != "":
164163
return profileOverride, ProfileUpdate
165-
case c.profile != "":
164+
case c.profile != "" && profile.Get(c.profile, c.Globals.Config.Profiles) != nil:
166165
return c.profile, ProfileUpdate
167166
case c.InvokedFromProfileCreate && c.ProfileCreateName != "":
168167
return c.ProfileCreateName, ProfileCreate
@@ -186,6 +185,7 @@ func (c *RootCommand) identifyProfileAndFlow() (profileName string, flow Profile
186185
func (c *RootCommand) processProfiles(ar auth.AuthorizationResult) error {
187186
profileName, flow := c.identifyProfileAndFlow()
188187

188+
//nolint:exhaustive
189189
switch flow {
190190
case ProfileCreate:
191191
c.processCreateProfile(ar, profileName)
@@ -230,7 +230,6 @@ func (c *RootCommand) processUpdateProfile(ar auth.AuthorizationResult, profileN
230230
if c.InvokedFromProfileUpdate {
231231
isDefault = c.ProfileDefault
232232
}
233-
234233
ps, err := editProfile(profileName, isDefault, c.Globals.Config.Profiles, ar)
235234
if err != nil {
236235
return err

pkg/global/global.go

+34
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package global
22

33
import (
4+
"fmt"
45
"io"
56

67
"github.com/fastly/cli/pkg/api"
@@ -88,6 +89,39 @@ type Data struct {
8889
Versioners Versioners
8990
}
9091

92+
// Profile identifies the current profile (if any).
93+
func (d *Data) Profile() (string, *config.Profile, error) {
94+
var (
95+
profileData *config.Profile
96+
found bool
97+
name, profileName string
98+
)
99+
switch {
100+
case d.Flags.Profile != "": // --profile
101+
profileName = d.Flags.Profile
102+
case d.Manifest.File.Profile != "": // `profile` field in fastly.toml
103+
profileName = d.Manifest.File.Profile
104+
default:
105+
profileName = "default" // fallback to locating the default profile
106+
}
107+
for name, profileData = range d.Config.Profiles {
108+
if (profileName == "default" && profileData.Default) || name == profileName {
109+
// Once we find the default profile we can update the variable to be the
110+
// associated profile name so later on we can use that information to
111+
// update the specific profile.
112+
if profileName == "default" {
113+
profileName = name
114+
}
115+
found = true
116+
break
117+
}
118+
}
119+
if !found {
120+
return "", nil, fmt.Errorf("failed to locate '%s' profile", profileName)
121+
}
122+
return profileName, profileData, nil
123+
}
124+
91125
// Token yields the Fastly API token.
92126
//
93127
// Order of precedence:

0 commit comments

Comments
 (0)