-
Notifications
You must be signed in to change notification settings - Fork 57
/
root.go
285 lines (252 loc) · 9.14 KB
/
root.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
package sso
import (
"errors"
"fmt"
"io"
"time"
"github.com/fastly/cli/pkg/auth"
"github.com/fastly/cli/pkg/cmd"
"github.com/fastly/cli/pkg/config"
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"
)
// RootCommand is the parent command for all subcommands in this package.
// It should be installed under the primary root command.
type RootCommand struct {
cmd.Base
profile string
// IMPORTANT: The following fields are public to the `profile` subcommands.
// InvokedFromProfileCreate indicates if we should create a new profile.
InvokedFromProfileCreate bool
// ProfileCreateName indicates the new profile name.
ProfileCreateName string
// ProfileDefault indicates if the affected profile should become the default.
ProfileDefault bool
// InvokedFromProfileUpdate indicates if we should update a profile.
InvokedFromProfileUpdate bool
// ProfileUpdateName indicates the profile name to update.
ProfileUpdateName string
}
// NewRootCommand returns a new command registered in the parent.
func NewRootCommand(parent cmd.Registerer, g *global.Data) *RootCommand {
var c RootCommand
c.Globals = g
// FIXME: Unhide this command once SSO is GA.
c.CmdClause = parent.Command("sso", "Single Sign-On authentication").Hidden()
c.CmdClause.Arg("profile", "Profile to authenticate (i.e. create/update a token for)").Short('p').StringVar(&c.profile)
return &c
}
// Exec implements the command interface.
func (c *RootCommand) Exec(in io.Reader, out io.Writer) error {
// 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 {
profileName, _ := c.identifyProfileAndFlow()
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.")
authorizationURL, err := c.Globals.AuthServer.AuthURL()
if err != nil {
return fsterr.RemediationError{
Inner: fmt.Errorf("failed to generate an authorization URL: %w", err),
Remediation: auth.Remediation,
}
}
text.Break(out)
text.Description(out, "We're opening the following URL in your default web browser so you may authenticate with Fastly", authorizationURL)
err = c.Globals.Opener(authorizationURL)
if err != nil {
return fmt.Errorf("failed to open your default browser: %w", err)
}
ar := <-c.Globals.AuthServer.GetResult()
if ar.Err != nil || ar.SessionToken == "" {
err := ar.Err
if ar.Err == nil {
err = errors.New("no session token")
}
return fsterr.RemediationError{
Inner: fmt.Errorf("failed to authorize: %w", err),
Remediation: auth.Remediation,
}
}
err = c.processProfiles(ar)
if err != nil {
c.Globals.ErrLog.Add(err)
return fmt.Errorf("failed to process profile data: %w", err)
}
textFn := text.Success
if c.InvokedFromProfileCreate || c.InvokedFromProfileUpdate {
textFn = text.Info
}
textFn(out, "Session token (persisted to your local configuration): %s", ar.SessionToken)
return nil
}
// ProfileFlow enumerates which profile flow to take.
type ProfileFlow uint8
const (
// ProfileNone indicates we need to create a new 'default' profile as no
// profiles currently exist.
ProfileNone ProfileFlow = iota
// ProfileCreate indicates we need to create a new profile using details
// passed in either from the `sso` or `profile create` command.
ProfileCreate
// ProfileUpdate indicates we need to update a profile using details passed in
// either from the `sso` or `profile update` command.
ProfileUpdate
)
// identifyProfileAndFlow identifies the profile and the specific workflow.
func (c *RootCommand) identifyProfileAndFlow() (profileName string, flow ProfileFlow) {
var profileOverride string
switch {
case c.Globals.Flags.Profile != "":
profileOverride = c.Globals.Flags.Profile
case c.Globals.Manifest.File.Profile != "":
profileOverride = c.Globals.Manifest.File.Profile
}
currentDefaultProfile, _ := profile.Default(c.Globals.Config.Profiles)
var newDefaultProfile string
if currentDefaultProfile == "" && len(c.Globals.Config.Profiles) > 0 {
newDefaultProfile, c.Globals.Config.Profiles = profile.SetADefault(c.Globals.Config.Profiles)
}
switch {
case profileOverride != "":
return profileOverride, ProfileUpdate
case c.profile != "":
return c.profile, ProfileUpdate
case c.InvokedFromProfileCreate && c.ProfileCreateName != "":
return c.ProfileCreateName, ProfileCreate
case c.InvokedFromProfileUpdate && c.ProfileUpdateName != "":
return c.ProfileUpdateName, ProfileUpdate
case currentDefaultProfile != "":
return currentDefaultProfile, ProfileUpdate
case newDefaultProfile != "":
return newDefaultProfile, ProfileUpdate
default:
return profile.DefaultName, ProfileCreate
}
}
// processProfiles updates the relevant profile with the returned token data.
//
// First it checks the --profile flag and the `profile` fastly.toml field.
// Second it checks to see which profile is currently the default.
// Third it identifies which profile to be modified.
// Fourth it writes the updated in-memory data back to disk.
func (c *RootCommand) processProfiles(ar auth.AuthorizationResult) error {
profileName, flow := c.identifyProfileAndFlow()
switch flow {
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 err := c.Globals.Config.Write(c.Globals.ConfigPath); err != nil {
return fmt.Errorf("failed to update config file: %w", err)
}
return nil
}
// processCreateProfile handles creating a new profile.
func (c *RootCommand) processCreateProfile(ar auth.AuthorizationResult, profileName string) {
isDefault := true
if c.InvokedFromProfileCreate {
isDefault = c.ProfileDefault
}
c.Globals.Config.Profiles = createNewProfile(profileName, 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
// their Default field set to false.
if c.ProfileDefault { // this is set by the `profile create` command.
if p, ok := profile.SetDefault(c.ProfileCreateName, c.Globals.Config.Profiles); ok {
c.Globals.Config.Profiles = p
}
}
}
// processUpdateProfile handles updating a profile.
func (c *RootCommand) processUpdateProfile(ar auth.AuthorizationResult, profileName string) error {
var isDefault bool
if p := profile.Get(profileName, c.Globals.Config.Profiles); p != nil {
isDefault = p.Default
}
if c.InvokedFromProfileUpdate {
isDefault = c.ProfileDefault
}
ps, err := editProfile(profileName, isDefault, c.Globals.Config.Profiles, ar)
if err != nil {
return err
}
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 {
now := time.Now().Unix()
if p == nil {
p = make(config.Profiles)
}
p[profileName] = &config.Profile{
AccessToken: ar.Jwt.AccessToken,
AccessTokenCreated: now,
AccessTokenTTL: ar.Jwt.ExpiresIn,
Default: makeDefault,
Email: ar.Email,
RefreshToken: ar.Jwt.RefreshToken,
RefreshTokenCreated: now,
RefreshTokenTTL: ar.Jwt.RefreshExpiresIn,
Token: ar.SessionToken,
}
return p
}
// 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) {
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.Email = ar.Email
p.RefreshToken = ar.Jwt.RefreshToken
p.RefreshTokenCreated = now
p.RefreshTokenTTL = ar.Jwt.RefreshExpiresIn
p.Token = ar.SessionToken
})
if !ok {
return ps, fsterr.RemediationError{
Inner: fmt.Errorf("failed to update '%s' profile with new token data", profileName),
Remediation: "Run `fastly sso` to retry.",
}
}
return ps, nil
}