-
Notifications
You must be signed in to change notification settings - Fork 11
/
auth_login.go
417 lines (351 loc) · 12.3 KB
/
auth_login.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
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
package auth
import (
"context"
"fmt"
"net/url"
"sort"
"strings"
"time"
"github.com/AlecAivazis/survey/v2"
"github.com/MakeNowJust/heredoc"
"github.com/pkg/browser"
"github.com/spf13/cobra"
"github.com/axiomhq/cli/internal/client"
"github.com/axiomhq/cli/internal/client/auth"
"github.com/axiomhq/cli/internal/cmdutil"
"github.com/axiomhq/cli/internal/config"
"github.com/axiomhq/cli/pkg/surveyext"
)
const oAuth2ClientID = "13c885a8-f46a-4424-82d2-883cf7ccfe49"
type loginOptions struct {
*cmdutil.Factory
// AutoLogin specifies if the CLI redirects to the Axiom UI for
// authentication.
AutoLogin bool
// Alias of the deployment for future reference. If not supplied as a flag,
// which is optional, the user will be asked for it.
Alias string
// Token of the user who wants to authenticate against the deployment. The
// user will be asked for it unless the session has no TTY attached, in
// which case the token is read from stdin.
Token string
// OrganizationID of the organization the supplied token is valid for. If
// not supplied as a flag, which is optional, the user will be asked for it.
OrganizationID string
// Force the creation and skip the confirmation prompt.
Force bool
// Alternate deployment support:
// Base URL of the deployment to authenticate with. Defaults to the Axiom
// URL. Can be overwritten by a hidden flag.
URL string
// Parsed from the URL.
appURL string
apiURL string
loginURL string
}
// NewLoginCmd creates ans returns the login command.
func NewLoginCmd(f *cmdutil.Factory) *cobra.Command {
opts := &loginOptions{
Factory: f,
}
cmd := &cobra.Command{
Use: "login [(-a|--alias) <alias>] [(-o|--org-id) <organization-id>] [-f|--force]",
Short: "Login to Axiom",
DisableFlagsInUseLine: true,
Example: heredoc.Doc(`
# Interactively authenticate against Axiom:
$ axiom auth login
# Provide parameters on the command-line:
$ echo $AXIOM_TOKEN | axiom auth login --alias="axiom-mycompany" --org-id="fancy-horse-1234" -f
`),
PreRunE: func(cmd *cobra.Command, _ []string) (err error) {
// Get specific URLs.
if opts.appURL, err = client.GetAppURL(opts.URL); err != nil {
return err
} else if opts.apiURL, err = client.GetAPIURL(opts.URL); err != nil {
return err
} else if opts.loginURL, err = client.GetLoginURL(opts.URL); err != nil {
return err
}
if !opts.IO.IsStdinTTY() || opts.AutoLogin {
return nil
}
return completeLogin(cmd.Context(), opts)
},
RunE: func(cmd *cobra.Command, _ []string) error {
if opts.IO.IsStdinTTY() && opts.AutoLogin {
return autoLogin(cmd.Context(), opts)
}
return runLogin(cmd.Context(), opts)
},
}
cmd.Flags().BoolVar(&opts.AutoLogin, "auto-login", true, "Login through the Axiom UI")
cmd.Flags().StringVarP(&opts.Alias, "alias", "a", "", "Alias of the deployment")
cmd.Flags().StringVarP(&opts.OrganizationID, "org-id", "o", "", "Organization ID")
cmd.Flags().BoolVarP(&opts.Force, "force", "f", false, "Skip the confirmation prompt")
cmd.Flags().StringVarP(&opts.URL, "url", "u", client.BaseURL, "Url of the deployment")
_ = cmd.RegisterFlagCompletionFunc("auto-login", cmdutil.NoCompletion)
_ = cmd.RegisterFlagCompletionFunc("alias", cmdutil.NoCompletion)
_ = cmd.RegisterFlagCompletionFunc("org-id", cmdutil.NoCompletion)
_ = cmd.RegisterFlagCompletionFunc("force", cmdutil.NoCompletion)
_ = cmd.RegisterFlagCompletionFunc("url", cmdutil.NoCompletion)
if !opts.IO.IsStdinTTY() {
_ = cmd.MarkFlagRequired("alias")
_ = cmd.MarkFlagRequired("org-id")
}
_ = cmd.Flags().MarkHidden("url")
return cmd
}
func completeLogin(ctx context.Context, opts *loginOptions) error {
// Suggest this URL to the user for creating a personal token.
u, err := url.ParseRequestURI(opts.appURL)
if err != nil {
return err
}
u.Path = "/profile"
// 1. Wheather to open the browser or not.
askTokenMsg := "What is your personal access token?"
if ok, err := surveyext.AskConfirm("You need to retrieve a personal access token from your profile page. Should I open that page in your default browser?",
true, opts.IO.SurveyIO()); err != nil {
return err
} else if !ok {
askTokenMsg = fmt.Sprintf("What is your personal access token (create one over at %s)?", u.String())
} else if err = browser.OpenURL(u.String()); err != nil {
return err
}
// 2. The token to use.
if err := survey.AskOne(&survey.Password{
Message: askTokenMsg,
}, &opts.Token, survey.WithValidator(survey.ComposeValidators(
survey.Required,
surveyext.ValidateToken,
)), opts.IO.SurveyIO()); err != nil {
return err
}
// 3. Try to authenticate and fetch the organizations available to the user.
// If only one organization is available, that one is selected by default,
// without asking the user for it.
if opts.OrganizationID == "" {
axiomClient, err := client.New(ctx, opts.apiURL, opts.Token, "axiom", opts.Config.Insecure)
if err != nil {
return err
}
if organizations, err := axiomClient.Organizations.List(ctx); err != nil {
return err
} else if len(organizations) == 1 {
opts.OrganizationID = organizations[0].ID
} else {
sort.Slice(organizations, func(i, j int) bool {
return strings.ToLower(organizations[i].Name) < strings.ToLower(organizations[j].Name)
})
organizationNames := make([]string, len(organizations))
for i, organization := range organizations {
organizationNames[i] = organization.Name
}
var organizationName string
if err := survey.AskOne(&survey.Select{
Message: "Which organization to use?",
Options: organizationNames,
Default: organizationNames[0],
Description: func(_ string, idx int) string {
return organizations[idx].ID
},
}, &organizationName, opts.IO.SurveyIO()); err != nil {
return err
}
for i, organization := range organizations {
if organization.Name == organizationName {
opts.OrganizationID = organizations[i].ID
break
}
}
}
}
// Make a useful suggestion for the alias to use (subdomain) but omit the
// sugesstion if a deployment with that alias is already configured. Cut the
// port, if present.
hostRef := firstSubDomain(opts.URL)
if _, ok := opts.Config.Deployments[hostRef]; ok {
hostRef = ""
}
// Just use "axiom" as the alias if this is their first deployment and they
// are authenticating against Axiom App.
if hostRef == "axiom" {
opts.Alias = hostRef
}
// 4. Ask for an alias to use.
if opts.Alias == "" {
if err := survey.AskOne(&survey.Input{
Message: "Under which name should the deployment be referenced in the future?",
Default: hostRef,
}, &opts.Alias, survey.WithValidator(survey.ComposeValidators(
survey.Required,
survey.MinLength(3),
surveyext.NotIn(opts.Config.DeploymentAliases()),
)), opts.IO.SurveyIO()); err != nil {
return err
}
}
return nil
}
func autoLogin(ctx context.Context, opts *loginOptions) error {
// 1. Wheather to open the browser or not. But the URL to open and have the
// user login is presented nonetheless.
stop := func() {}
loginFunc := func(_ context.Context, loginURL string) error {
if ok, err := surveyext.AskConfirm("You need to login to Axiom. Should I open your default browser?",
true, opts.IO.SurveyIO()); err != nil {
return err
} else if !ok {
fmt.Fprintf(opts.IO.ErrOut(), "Please open %s in your browser, manually.\n", loginURL)
} else if err = browser.OpenURL(loginURL); err != nil {
return err
}
fmt.Fprintln(opts.IO.ErrOut(), "Waiting for authentication...")
stop = opts.IO.StartActivityIndicator()
return nil
}
defer stop()
// Wait five minutes before timing out.
authContext, authCancel := context.WithTimeout(ctx, time.Minute*5)
defer authCancel()
var err error
if opts.Token, err = auth.Login(authContext, oAuth2ClientID, opts.loginURL, loginFunc); err != nil {
return err
}
// 2. Try to authenticate and fetch the organizations available to the user.
// If only one organization is available, that one is selected by default,
// without asking the user for it.
if opts.OrganizationID == "" {
axiomClient, err := client.New(ctx, opts.apiURL, opts.Token, "axiom", opts.Config.Insecure)
if err != nil {
return err
}
if organizations, err := axiomClient.Organizations.List(ctx); err != nil {
return err
} else if len(organizations) == 1 {
opts.OrganizationID = organizations[0].ID
} else {
sort.Slice(organizations, func(i, j int) bool {
return strings.ToLower(organizations[i].Name) < strings.ToLower(organizations[j].Name)
})
organizationNames := make([]string, len(organizations))
for i, organization := range organizations {
organizationNames[i] = organization.Name
}
stop()
var organizationName string
if err := survey.AskOne(&survey.Select{
Message: "Which organization to use?",
Options: organizationNames,
Default: organizationNames[0],
Description: func(_ string, idx int) string {
return organizations[idx].ID
},
}, &organizationName, opts.IO.SurveyIO()); err != nil {
return err
}
for i, organization := range organizations {
if organization.Name == organizationName {
opts.OrganizationID = organizations[i].ID
break
}
}
}
}
stop()
// Make a useful suggestion for the alias to use (subdomain) but omit the
// sugesstion if a deployment with that alias is already configured. Cut the
// port, if present.
hostRef := firstSubDomain(opts.URL)
if _, ok := opts.Config.Deployments[hostRef]; ok {
hostRef = ""
}
// Just use "axiom" as the alias if this is their first deployment and they
// are authenticating against Axiom App.
if hostRef == "axiom" {
opts.Alias = hostRef
}
// 3. Ask for an alias to use.
if opts.Alias == "" {
if err := survey.AskOne(&survey.Input{
Message: "Under which name should the deployment be referenced in the future?",
Default: hostRef,
}, &opts.Alias, survey.WithValidator(survey.ComposeValidators(
survey.Required,
survey.MinLength(3),
surveyext.NotIn(opts.Config.DeploymentAliases()),
)), opts.IO.SurveyIO()); err != nil {
return err
}
}
// 5. Try to login with the retrieved credentials.
return runLogin(ctx, opts)
}
func runLogin(ctx context.Context, opts *loginOptions) error {
// Read token from stdin, if no TTY is attached.
if !opts.IO.IsStdinTTY() {
var err error
if opts.Token, err = readTokenFromStdIn(opts.IO.In()); err != nil {
return err
}
}
// If a deployment with the alias exists in the config, we ask the user if
// he wants to overwrite it, if "--force" is not set. When no TTY is
// attached, we abort and return, not overwritting anything.
if _, ok := opts.Config.Deployments[opts.Alias]; ok && !opts.Force {
if !opts.IO.IsStdinTTY() {
return fmt.Errorf("deployment with alias %q already configured, overwrite with '-f|--force' flag", opts.Alias)
}
msg := fmt.Sprintf("Deployment with alias %q already configured! Overwrite?", opts.Alias)
if overwrite, err := surveyext.AskConfirm(msg, false, opts.IO.SurveyIO()); err != nil {
return err
} else if !overwrite {
return cmdutil.ErrSilent
}
}
axiomClient, err := client.New(ctx, opts.apiURL, opts.Token, opts.OrganizationID, opts.Config.Insecure)
if err != nil {
return err
}
stop := opts.IO.StartActivityIndicator()
defer stop()
user, err := axiomClient.Users.Current(ctx)
if err != nil {
return err
}
stop()
if opts.IO.IsStderrTTY() {
cs := opts.IO.ColorScheme()
if client.IsPersonalToken(opts.Token) {
organization, err := axiomClient.Organizations.Get(ctx, opts.OrganizationID)
if err != nil {
return err
}
fmt.Fprintf(opts.IO.ErrOut(), "%s Logged in to organization %s as %s\n",
cs.SuccessIcon(), cs.Bold(organization.Name), cs.Bold(user.Name))
} else {
fmt.Fprintf(opts.IO.ErrOut(), "%s Logged in to organization %s %s\n",
cs.SuccessIcon(), cs.Bold(opts.OrganizationID), cs.Red(cs.Bold("(ingestion/query only!)")))
}
}
opts.Config.ActiveDeployment = opts.Alias
opts.Config.Deployments[opts.Alias] = config.Deployment{
URL: opts.apiURL,
Token: opts.Token,
OrganizationID: opts.OrganizationID,
}
return opts.Config.Write()
}
func firstSubDomain(s string) string {
u, err := url.ParseRequestURI(s)
if err != nil {
return ""
}
var hostRef string
hostRefParts := strings.Split(u.Hostname(), ".")
if len(hostRefParts) > 0 {
hostRef = hostRefParts[0]
}
return strings.TrimLeft(hostRef, u.Scheme)
}