Skip to content

Commit 5fddcef

Browse files
committed
fix(auth status): return JSON entries under hosts
Signed-off-by: Babak K. Shandiz <babakks@github.com>
1 parent 914531e commit 5fddcef

3 files changed

Lines changed: 160 additions & 224 deletions

File tree

pkg/cmd/auth/status/auth_state.go

Lines changed: 0 additions & 28 deletions
This file was deleted.

pkg/cmd/auth/status/status.go

Lines changed: 98 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -19,40 +19,56 @@ import (
1919
"github.com/spf13/cobra"
2020
)
2121

22+
type authEntryState string
23+
24+
const (
25+
authEntryStateSuccess = "success"
26+
authEntryStateTimeout = "timeout"
27+
authEntryStateError = "error"
28+
)
29+
2230
type authEntry struct {
23-
State authState `json:"state"`
24-
Error string `json:"error"`
25-
Active bool `json:"active"`
26-
Host string `json:"host"`
27-
Login string `json:"login"`
28-
TokenSource string `json:"tokenSource"`
29-
Token string `json:"token"`
30-
Scopes string `json:"scopes"`
31-
GitProtocol string `json:"gitProtocol"`
31+
State authEntryState `json:"state"`
32+
Error string `json:"error,omitempty"`
33+
Active bool `json:"active"`
34+
Host string `json:"host"`
35+
Login string `json:"login"`
36+
TokenSource string `json:"tokenSource"`
37+
Token string `json:"token,omitempty"`
38+
Scopes string `json:"scopes,omitempty"`
39+
GitProtocol string `json:"gitProtocol"`
40+
}
41+
42+
type authStatus struct {
43+
Hosts map[string][]authEntry `json:"hosts"`
44+
}
45+
46+
func newAuthStatus() *authStatus {
47+
return &authStatus{
48+
Hosts: make(map[string][]authEntry),
49+
}
50+
}
51+
52+
var authStatusFields = []string{
53+
"hosts",
3254
}
3355

34-
var authFields = []string{
35-
"state",
36-
"error",
37-
"active",
38-
"host",
39-
"login",
40-
"tokenSource",
41-
"token",
42-
"scopes",
43-
"gitProtocol",
56+
func (a authStatus) ExportData(fields []string) map[string]interface{} {
57+
return cmdutil.StructExportData(a, fields)
4458
}
4559

46-
func (e authEntry) String(cs *iostreams.ColorScheme, showToken bool) string {
60+
func (e authEntry) String(cs *iostreams.ColorScheme) string {
4761
var sb strings.Builder
48-
if e.State == authStateSuccess {
62+
63+
switch e.State {
64+
case authEntryStateSuccess:
4965
sb.WriteString(
5066
fmt.Sprintf(" %s Logged in to %s account %s (%s)\n", cs.SuccessIcon(), e.Host, cs.Bold(e.Login), e.TokenSource),
5167
)
5268
activeStr := fmt.Sprintf("%v", e.Active)
5369
sb.WriteString(fmt.Sprintf(" - Active account: %s\n", cs.Bold(activeStr)))
5470
sb.WriteString(fmt.Sprintf(" - Git operations protocol: %s\n", cs.Bold(e.GitProtocol)))
55-
sb.WriteString(fmt.Sprintf(" - Token: %s\n", cs.Bold(displayToken(e.Token, showToken))))
71+
sb.WriteString(fmt.Sprintf(" - Token: %s\n", cs.Bold(e.Token)))
5672

5773
if expectScopes(e.Token) {
5874
sb.WriteString(fmt.Sprintf(" - Token scopes: %s\n", cs.Bold(displayScopes(e.Scopes))))
@@ -68,31 +84,34 @@ func (e authEntry) String(cs *iostreams.ColorScheme, showToken bool) string {
6884
}
6985
}
7086
}
71-
return sb.String()
72-
}
7387

74-
if e.Login != "" {
75-
sb.WriteString(fmt.Sprintf(" %s %s to %s account %s (%s)\n", cs.Red("X"), e.Error, e.Host, cs.Bold(e.Login), e.TokenSource))
76-
} else {
77-
sb.WriteString(fmt.Sprintf(" %s %s to %s using token (%s)\n", cs.Red("X"), e.Error, e.Host, e.TokenSource))
78-
}
79-
activeStr := fmt.Sprintf("%v", e.Active)
80-
sb.WriteString(fmt.Sprintf(" - Active account: %s\n", cs.Bold(activeStr)))
81-
82-
if e.State == authStateError {
88+
case authEntryStateError:
89+
if e.Login != "" {
90+
sb.WriteString(fmt.Sprintf(" %s Failed to log in to %s account %s (%s)\n", cs.Red("X"), e.Host, cs.Bold(e.Login), e.TokenSource))
91+
} else {
92+
sb.WriteString(fmt.Sprintf(" %s Failed to log in to %s using token (%s)\n", cs.Red("X"), e.Host, e.TokenSource))
93+
}
94+
activeStr := fmt.Sprintf("%v", e.Active)
95+
sb.WriteString(fmt.Sprintf(" - Active account: %s\n", cs.Bold(activeStr)))
8396
sb.WriteString(fmt.Sprintf(" - The token in %s is invalid.\n", e.TokenSource))
8497
if authTokenWriteable(e.TokenSource) {
8598
loginInstructions := fmt.Sprintf("gh auth login -h %s", e.Host)
8699
logoutInstructions := fmt.Sprintf("gh auth logout -h %s -u %s", e.Host, e.Login)
87100
sb.WriteString(fmt.Sprintf(" - To re-authenticate, run: %s\n", cs.Bold(loginInstructions)))
88101
sb.WriteString(fmt.Sprintf(" - To forget about this account, run: %s\n", cs.Bold(logoutInstructions)))
89102
}
103+
104+
case authEntryStateTimeout:
105+
if e.Login != "" {
106+
sb.WriteString(fmt.Sprintf(" %s Timeout trying to log in to %s account %s (%s)\n", cs.Red("X"), e.Host, cs.Bold(e.Login), e.TokenSource))
107+
} else {
108+
sb.WriteString(fmt.Sprintf(" %s Timeout trying to log in to %s using token (%s)\n", cs.Red("X"), e.Host, e.TokenSource))
109+
}
110+
activeStr := fmt.Sprintf("%v", e.Active)
111+
sb.WriteString(fmt.Sprintf(" - Active account: %s\n", cs.Bold(activeStr)))
90112
}
91-
return sb.String()
92-
}
93113

94-
func (e authEntry) ExportData(fields []string) map[string]interface{} {
95-
return cmdutil.StructExportData(e, fields)
114+
return sb.String()
96115
}
97116

98117
type StatusOptions struct {
@@ -138,22 +157,15 @@ func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Co
138157
$ gh auth status --show-token
139158
140159
# Format authentication status as JSON
141-
$ gh auth status --json active,login,host
160+
$ gh auth status --json hosts
142161
143162
# Include plain text token in JSON output
144-
$ gh auth status --json token,login
163+
$ gh auth status --json hosts --show-token
145164
146-
# Format output as a flat JSON array
147-
$ gh auth status --json active,host,login --jq add
165+
# Format hosts as a flat JSON array
166+
$ gh auth status --json hosts --jq '.hosts | add'
148167
`),
149168
RunE: func(cmd *cobra.Command, args []string) error {
150-
if err := cmdutil.MutuallyExclusive(
151-
"`--json` and `--show-token` cannot be used together. To include the token in the JSON output, use `--json token`.",
152-
opts.Exporter != nil,
153-
opts.ShowToken,
154-
); err != nil {
155-
return err
156-
}
157169
if runF != nil {
158170
return runF(opts)
159171
}
@@ -167,7 +179,7 @@ func NewCmdStatus(f *cmdutil.Factory, runF func(*StatusOptions) error) *cobra.Co
167179
cmd.Flags().BoolVarP(&opts.Active, "active", "a", false, "Display the active account only")
168180

169181
// the json flags are intentionally not given a shorthand to avoid conflict with -t/--show-token
170-
cmdutil.AddJSONFlagsWithoutShorthand(cmd, &opts.Exporter, authFields)
182+
cmdutil.AddJSONFlagsWithoutShorthand(cmd, &opts.Exporter, authStatusFields)
171183

172184
return cmd
173185
}
@@ -183,20 +195,13 @@ func statusRun(opts *StatusOptions) error {
183195
stdout := opts.IO.Out
184196
cs := opts.IO.ColorScheme()
185197

186-
showToken := opts.ShowToken
187-
if opts.Exporter != nil && slices.Contains(opts.Exporter.Fields(), "token") {
188-
showToken = true
189-
}
190-
191-
statuses := make(map[string][]authEntry)
192-
193198
hostnames := authCfg.Hosts()
194199
if len(hostnames) == 0 {
195200
fmt.Fprintf(stderr,
196201
"You are not logged into any GitHub hosts. To log in, run: %s\n", cs.Bold("gh auth login"))
197202
if opts.Exporter != nil {
198203
// In machine-friendly mode, we always exit with no error.
199-
opts.Exporter.Write(opts.IO, struct{}{})
204+
opts.Exporter.Write(opts.IO, newAuthStatus())
200205
return nil
201206
}
202207
return cmdutil.SilentError
@@ -207,7 +212,7 @@ func statusRun(opts *StatusOptions) error {
207212
"You are not logged into any accounts on %s\n", opts.Hostname)
208213
if opts.Exporter != nil {
209214
// In machine-friendly mode, we always exit with no error.
210-
opts.Exporter.Write(opts.IO, struct{}{})
215+
opts.Exporter.Write(opts.IO, newAuthStatus())
211216
return nil
212217
}
213218
return cmdutil.SilentError
@@ -218,6 +223,9 @@ func statusRun(opts *StatusOptions) error {
218223
return err
219224
}
220225

226+
var finalErr error
227+
statuses := newAuthStatus()
228+
221229
for _, hostname := range hostnames {
222230
if opts.Hostname != "" && opts.Hostname != hostname {
223231
continue
@@ -233,15 +241,14 @@ func statusRun(opts *StatusOptions) error {
233241
active: true,
234242
gitProtocol: gitProtocol,
235243
hostname: hostname,
236-
showToken: showToken,
237244
token: activeUserToken,
238245
tokenSource: activeUserTokenSource,
239246
username: activeUser,
240247
})
241-
statuses[hostname] = append(statuses[hostname], entry)
248+
statuses.Hosts[hostname] = append(statuses.Hosts[hostname], entry)
242249

243-
if err == nil && !isValidEntry(entry) {
244-
err = cmdutil.SilentError
250+
if finalErr == nil && entry.State != authEntryStateSuccess {
251+
finalErr = cmdutil.SilentError
245252
}
246253

247254
if opts.Active {
@@ -258,33 +265,46 @@ func statusRun(opts *StatusOptions) error {
258265
active: false,
259266
gitProtocol: gitProtocol,
260267
hostname: hostname,
261-
showToken: showToken,
262268
token: token,
263269
tokenSource: tokenSource,
264270
username: username,
265271
})
266-
statuses[hostname] = append(statuses[hostname], entry)
272+
statuses.Hosts[hostname] = append(statuses.Hosts[hostname], entry)
273+
274+
if finalErr == nil && entry.State != authEntryStateSuccess {
275+
finalErr = cmdutil.SilentError
276+
}
277+
}
278+
}
267279

268-
if err == nil && !isValidEntry(entry) {
269-
err = cmdutil.SilentError
280+
if !opts.ShowToken {
281+
for _, host := range statuses.Hosts {
282+
for i := range host {
283+
if opts.Exporter != nil {
284+
// In machine-readable we just drop the token
285+
host[i].Token = ""
286+
} else {
287+
host[i].Token = maskToken(host[i].Token)
288+
}
270289
}
271290
}
272291
}
273292

274293
if opts.Exporter != nil {
294+
// In machine-friendly mode, we always exit with no error.
275295
opts.Exporter.Write(opts.IO, statuses)
276296
return nil
277297
}
278298

279299
prevEntry := false
280300
for _, hostname := range hostnames {
281-
entries, ok := statuses[hostname]
301+
entries, ok := statuses.Hosts[hostname]
282302
if !ok {
283303
continue
284304
}
285305

286306
stream := stdout
287-
if err != nil {
307+
if finalErr != nil {
288308
stream = stderr
289309
}
290310

@@ -294,26 +314,21 @@ func statusRun(opts *StatusOptions) error {
294314
prevEntry = true
295315
fmt.Fprintf(stream, "%s\n", cs.Bold(hostname))
296316
for i, entry := range entries {
297-
fmt.Fprintf(stream, "%s", entry.String(cs, showToken))
317+
fmt.Fprintf(stream, "%s", entry.String(cs))
298318
if i < len(entries)-1 {
299319
fmt.Fprint(stream, "\n")
300320
}
301321
}
302322
}
303323

304-
return err
324+
return finalErr
305325
}
306326

307-
func displayToken(token string, printRaw bool) string {
308-
if printRaw {
309-
return token
310-
}
311-
327+
func maskToken(token string) string {
312328
if idx := strings.LastIndexByte(token, '_'); idx > -1 {
313329
prefix := token[0 : idx+1]
314330
return prefix + strings.Repeat("*", len(token)-len(prefix))
315331
}
316-
317332
return strings.Repeat("*", len(token))
318333
}
319334

@@ -336,7 +351,6 @@ type buildEntryOptions struct {
336351
active bool
337352
gitProtocol string
338353
hostname string
339-
showToken bool
340354
token string
341355
tokenSource string
342356
username string
@@ -367,8 +381,8 @@ func buildEntry(httpClient *http.Client, opts buildEntryOptions) authEntry {
367381
var err error
368382
entry.Login, err = api.CurrentLoginName(apiClient, opts.hostname)
369383
if err != nil {
370-
entry.State = authStateError
371-
entry.Error = "Failed to log in"
384+
entry.State = authEntryStateError
385+
entry.Error = err.Error()
372386
return entry
373387
}
374388
}
@@ -378,25 +392,21 @@ func buildEntry(httpClient *http.Client, opts buildEntryOptions) authEntry {
378392
if err != nil {
379393
var networkError net.Error
380394
if errors.As(err, &networkError) && networkError.Timeout() {
381-
entry.State = authStateTimeout
382-
entry.Error = "Timeout trying to log in"
395+
entry.State = authEntryStateTimeout
396+
entry.Error = err.Error()
383397
return entry
384398
}
385399

386-
entry.State = authStateError
387-
entry.Error = "Failed to log in"
400+
entry.State = authEntryStateError
401+
entry.Error = err.Error()
388402
return entry
389403
}
390404
entry.Scopes = scopesHeader
391405

392-
entry.State = authStateSuccess
406+
entry.State = authEntryStateSuccess
393407
return entry
394408
}
395409

396410
func authTokenWriteable(src string) bool {
397411
return !strings.HasSuffix(src, "_TOKEN")
398412
}
399-
400-
func isValidEntry(entry authEntry) bool {
401-
return entry.State == authStateSuccess
402-
}

0 commit comments

Comments
 (0)