fix(auth): tolerate unreadable file keyring tokens
This commit is contained in:
parent
77a16d10ef
commit
0a5d06e98b
@ -24,6 +24,7 @@
|
||||
|
||||
### Fixed
|
||||
- Backup: split Gmail checkpoint commits by row count and plaintext byte size so large messages stay below GitHub's blob limit.
|
||||
- Auth: keep `gog auth list` and `gog auth tokens list` useful when one file-keyring token cannot be decrypted; unreadable entries are now reported instead of aborting the whole listing. (#377)
|
||||
- Auth: time out Linux D-Bus keyring write operations and report when OAuth completed but saving the refresh token failed, so manual auth no longer looks like a stuck paste when token persistence is blocked. (#130)
|
||||
- Install docs: document Windows release ZIP/PATH setup and clarify that source builds require the Go version declared in `go.mod`, not Ubuntu 24.04's Go 1.22 package. (#157, #135)
|
||||
- CI: pin GitHub Actions workflow dependencies to immutable commit SHAs. (#288)
|
||||
|
||||
@ -219,6 +219,8 @@ Verify tokens are usable (helps spot revoked/expired tokens):
|
||||
gog auth list --check
|
||||
```
|
||||
|
||||
If one file-keyring token cannot be decrypted, `gog auth list` still reports the readable accounts and prints the unreadable account with an error. Use `gog auth tokens list` to show token keys without decrypting them, then `gog auth tokens delete <email>` after confirming the affected account.
|
||||
|
||||
Diagnose keyring/password drift and refresh-token failures:
|
||||
|
||||
```bash
|
||||
|
||||
@ -102,8 +102,9 @@ Implementation: `internal/config/*`.
|
||||
|
||||
Current minimal management commands (implemented):
|
||||
|
||||
- `gog auth tokens list` (keys only)
|
||||
- `gog auth tokens list` (keys only; does not decrypt token payloads)
|
||||
- `gog auth tokens delete <email>`
|
||||
- `gog auth list` reports unreadable token entries instead of failing the whole listing, so one bad file-keyring entry does not hide other accounts.
|
||||
|
||||
Implementation: `internal/secrets/store.go`.
|
||||
|
||||
|
||||
@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@ -121,7 +120,7 @@ func (c *AuthListCmd) Run(ctx context.Context, _ *RootFlags) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tokens, err := store.ListTokens()
|
||||
tokens, tokenReadErrors, err := listAuthTokensWithFallback(store)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -131,180 +130,18 @@ func (c *AuthListCmd) Run(ctx context.Context, _ *RootFlags) error {
|
||||
return err
|
||||
}
|
||||
|
||||
sort.Slice(tokens, func(i, j int) bool { return tokens[i].Email < tokens[j].Email })
|
||||
|
||||
type tokenByEmail struct {
|
||||
tok secrets.Token
|
||||
ok bool
|
||||
}
|
||||
tokMap := make(map[string]tokenByEmail, len(tokens))
|
||||
for _, t := range tokens {
|
||||
email := normalizeEmail(t.Email)
|
||||
if email == "" {
|
||||
continue
|
||||
}
|
||||
tokMap[email] = tokenByEmail{tok: t, ok: true}
|
||||
}
|
||||
|
||||
type entry struct {
|
||||
Email string
|
||||
Token *secrets.Token
|
||||
SA bool
|
||||
}
|
||||
entries := make([]entry, 0, len(tokens)+len(serviceAccountEmails))
|
||||
seen := make(map[string]struct{})
|
||||
for _, email := range serviceAccountEmails {
|
||||
email = normalizeEmail(email)
|
||||
if email == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[email]; ok {
|
||||
continue
|
||||
}
|
||||
seen[email] = struct{}{}
|
||||
te := tokMap[email]
|
||||
var tok *secrets.Token
|
||||
if te.ok {
|
||||
t := te.tok
|
||||
tok = &t
|
||||
}
|
||||
entries = append(entries, entry{Email: email, Token: tok, SA: true})
|
||||
}
|
||||
for _, t := range tokens {
|
||||
email := normalizeEmail(t.Email)
|
||||
if email == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[email]; ok {
|
||||
continue
|
||||
}
|
||||
seen[email] = struct{}{}
|
||||
t2 := t
|
||||
entries = append(entries, entry{Email: email, Token: &t2, SA: false})
|
||||
}
|
||||
sort.Slice(entries, func(i, j int) bool { return entries[i].Email < entries[j].Email })
|
||||
entries := buildAuthListEntries(tokens, tokenReadErrors, serviceAccountEmails)
|
||||
|
||||
if outfmt.IsJSON(ctx) {
|
||||
type item struct {
|
||||
Email string `json:"email"`
|
||||
Subject string `json:"subject,omitempty"`
|
||||
Client string `json:"client,omitempty"`
|
||||
Services []string `json:"services,omitempty"`
|
||||
Scopes []string `json:"scopes,omitempty"`
|
||||
CreatedAt string `json:"created_at,omitempty"`
|
||||
Auth string `json:"auth"`
|
||||
Valid *bool `json:"valid,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
out := make([]item, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
auth := authTypeOAuth
|
||||
if e.SA {
|
||||
auth = authTypeServiceAccount
|
||||
}
|
||||
if e.Token != nil && e.SA {
|
||||
auth = authTypeOAuthServiceAccount
|
||||
}
|
||||
|
||||
created := ""
|
||||
services := []string(nil)
|
||||
scopes := []string(nil)
|
||||
|
||||
if e.Token != nil {
|
||||
if !e.Token.CreatedAt.IsZero() {
|
||||
created = e.Token.CreatedAt.UTC().Format("2006-01-02T15:04:05Z07:00")
|
||||
}
|
||||
services = e.Token.Services
|
||||
scopes = e.Token.Scopes
|
||||
} else if e.SA {
|
||||
if _, mtime, ok := bestServiceAccountPathAndMtime(e.Email); ok {
|
||||
created = mtime.UTC().Format("2006-01-02T15:04:05Z07:00")
|
||||
}
|
||||
services = []string{"service-account"}
|
||||
}
|
||||
|
||||
it := item{
|
||||
Email: e.Email,
|
||||
Client: "",
|
||||
Services: services,
|
||||
Scopes: scopes,
|
||||
CreatedAt: created,
|
||||
Auth: auth,
|
||||
}
|
||||
if e.Token != nil {
|
||||
it.Client = e.Token.Client
|
||||
it.Subject = e.Token.Subject
|
||||
}
|
||||
if c.Check {
|
||||
if e.Token == nil {
|
||||
valid := true
|
||||
it.Valid = &valid
|
||||
it.Error = "service account (not checked)"
|
||||
} else {
|
||||
err := checkRefreshToken(ctx, e.Token.Client, e.Token.RefreshToken, e.Token.Scopes, c.Timeout)
|
||||
valid := err == nil
|
||||
it.Valid = &valid
|
||||
if err != nil {
|
||||
it.Error = err.Error()
|
||||
}
|
||||
}
|
||||
}
|
||||
out = append(out, it)
|
||||
}
|
||||
return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{"accounts": out})
|
||||
return c.writeAuthListJSON(ctx, entries)
|
||||
}
|
||||
|
||||
if len(entries) == 0 {
|
||||
u.Err().Println("No tokens stored")
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, e := range entries {
|
||||
auth := authTypeOAuth
|
||||
if e.SA {
|
||||
auth = authTypeServiceAccount
|
||||
}
|
||||
if e.Token != nil && e.SA {
|
||||
auth = authTypeOAuthServiceAccount
|
||||
}
|
||||
|
||||
client := ""
|
||||
if e.Token != nil {
|
||||
client = e.Token.Client
|
||||
}
|
||||
created := ""
|
||||
servicesCSV := ""
|
||||
|
||||
if e.Token != nil {
|
||||
if !e.Token.CreatedAt.IsZero() {
|
||||
created = e.Token.CreatedAt.UTC().Format("2006-01-02T15:04:05Z07:00")
|
||||
}
|
||||
servicesCSV = strings.Join(e.Token.Services, ",")
|
||||
} else if e.SA {
|
||||
if _, mtime, ok := bestServiceAccountPathAndMtime(e.Email); ok {
|
||||
created = mtime.UTC().Format("2006-01-02T15:04:05Z07:00")
|
||||
}
|
||||
servicesCSV = "service-account"
|
||||
}
|
||||
|
||||
if c.Check {
|
||||
if e.Token == nil {
|
||||
u.Out().Printf("%s\t%s\t%s\t%s\t%t\t%s\t%s", e.Email, client, servicesCSV, created, true, "service account (not checked)", auth)
|
||||
continue
|
||||
}
|
||||
|
||||
err := checkRefreshToken(ctx, e.Token.Client, e.Token.RefreshToken, e.Token.Scopes, c.Timeout)
|
||||
valid := err == nil
|
||||
msg := ""
|
||||
if err != nil {
|
||||
msg = err.Error()
|
||||
}
|
||||
u.Out().Printf("%s\t%s\t%s\t%s\t%t\t%s\t%s", e.Email, client, servicesCSV, created, valid, msg, auth)
|
||||
continue
|
||||
}
|
||||
|
||||
u.Out().Printf("%s\t%s\t%s\t%s\t%s", e.Email, client, servicesCSV, created, auth)
|
||||
}
|
||||
return nil
|
||||
return c.writeAuthListText(ctx, u, entries)
|
||||
}
|
||||
|
||||
func bestServiceAccountPathAndMtime(email string) (string, time.Time, bool) {
|
||||
|
||||
@ -212,6 +212,30 @@ func TestAuthTokensList_FiltersNonTokenKeys(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthTokensList_ListsKeysWithoutDecrypting(t *testing.T) {
|
||||
origOpen := openSecretsStore
|
||||
t.Cleanup(func() { openSecretsStore = origOpen })
|
||||
|
||||
openSecretsStore = func() (secrets.Store, error) {
|
||||
return &errorTokenStore{
|
||||
keys: []string{secrets.TokenKey(config.DefaultClientName, "a@b.com")},
|
||||
err: errors.New("read token: aes.KeyUnwrap(): integrity check failed"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"auth", "tokens", "list"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if strings.TrimSpace(out) != "token:default:a@b.com" {
|
||||
t.Fatalf("unexpected output: %q", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthStatus_JSON(t *testing.T) {
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
|
||||
@ -301,7 +325,7 @@ func (s *errorTokenStore) GetToken(string, string) (secrets.Token, error) {
|
||||
|
||||
func (s *errorTokenStore) DeleteToken(string, string) error { return nil }
|
||||
|
||||
func (s *errorTokenStore) ListTokens() ([]secrets.Token, error) { return nil, nil }
|
||||
func (s *errorTokenStore) ListTokens() ([]secrets.Token, error) { return nil, s.err }
|
||||
|
||||
func (s *errorTokenStore) GetDefaultAccount(string) (string, error) { return "", nil }
|
||||
|
||||
@ -356,6 +380,52 @@ func TestAuthDoctor_JSON_ClassifiesFileKeyringIntegrity(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthList_JSON_ReportsUnreadableToken(t *testing.T) {
|
||||
origOpen := openSecretsStore
|
||||
t.Cleanup(func() { openSecretsStore = origOpen })
|
||||
|
||||
t.Setenv("HOME", t.TempDir())
|
||||
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
|
||||
|
||||
openSecretsStore = func() (secrets.Store, error) {
|
||||
return &errorTokenStore{
|
||||
keys: []string{secrets.TokenKey(config.DefaultClientName, "a@b.com")},
|
||||
err: errors.New("read token: aes.KeyUnwrap(): integrity check failed"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
out := captureStdout(t, func() {
|
||||
_ = captureStderr(t, func() {
|
||||
if err := Execute([]string{"--json", "auth", "list"}); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
var payload struct {
|
||||
Accounts []struct {
|
||||
Email string `json:"email"`
|
||||
Client string `json:"client"`
|
||||
Auth string `json:"auth"`
|
||||
Error string `json:"error"`
|
||||
Hint string `json:"hint"`
|
||||
} `json:"accounts"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(out), &payload); err != nil {
|
||||
t.Fatalf("json parse: %v\nout=%q", err, out)
|
||||
}
|
||||
if len(payload.Accounts) != 1 {
|
||||
t.Fatalf("accounts=%#v, want one unreadable token row", payload.Accounts)
|
||||
}
|
||||
account := payload.Accounts[0]
|
||||
if account.Email != "a@b.com" || account.Client != config.DefaultClientName || account.Auth != authTypeOAuth {
|
||||
t.Fatalf("unexpected account row: %#v", account)
|
||||
}
|
||||
if !strings.Contains(account.Error, "integrity check failed") || !strings.Contains(account.Hint, "password mismatch") {
|
||||
t.Fatalf("missing classified unreadable-token details: %#v", account)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthDoctor_JSON_CheckClassifiesInvalidRAPT(t *testing.T) {
|
||||
origOpen := openSecretsStore
|
||||
origCheck := checkRefreshToken
|
||||
|
||||
304
internal/cmd/auth_list_helpers.go
Normal file
304
internal/cmd/auth_list_helpers.go
Normal file
@ -0,0 +1,304 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/steipete/gogcli/internal/outfmt"
|
||||
"github.com/steipete/gogcli/internal/secrets"
|
||||
"github.com/steipete/gogcli/internal/ui"
|
||||
)
|
||||
|
||||
type authTokenReadError struct {
|
||||
Client string
|
||||
Email string
|
||||
Err error
|
||||
}
|
||||
|
||||
type authListEntry struct {
|
||||
Email string
|
||||
Client string
|
||||
Token *secrets.Token
|
||||
SA bool
|
||||
ReadErr error
|
||||
ReadHint string
|
||||
}
|
||||
|
||||
type authListJSONItem struct {
|
||||
Email string `json:"email"`
|
||||
Subject string `json:"subject,omitempty"`
|
||||
Client string `json:"client,omitempty"`
|
||||
Services []string `json:"services,omitempty"`
|
||||
Scopes []string `json:"scopes,omitempty"`
|
||||
CreatedAt string `json:"created_at,omitempty"`
|
||||
Auth string `json:"auth"`
|
||||
Valid *bool `json:"valid,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Hint string `json:"hint,omitempty"`
|
||||
}
|
||||
|
||||
func listAuthTokensWithFallback(store secrets.Store) ([]secrets.Token, []authTokenReadError, error) {
|
||||
tokens, err := store.ListTokens()
|
||||
if err == nil {
|
||||
return tokens, nil, nil
|
||||
}
|
||||
|
||||
return readableTokens(store)
|
||||
}
|
||||
|
||||
type tokenByEmail struct {
|
||||
tok secrets.Token
|
||||
ok bool
|
||||
}
|
||||
|
||||
func buildAuthListEntries(tokens []secrets.Token, tokenReadErrors []authTokenReadError, serviceAccountEmails []string) []authListEntry {
|
||||
sort.Slice(tokens, func(i, j int) bool { return tokens[i].Email < tokens[j].Email })
|
||||
|
||||
tokMap := make(map[string]tokenByEmail, len(tokens))
|
||||
for _, t := range tokens {
|
||||
email := normalizeEmail(t.Email)
|
||||
if email == "" {
|
||||
continue
|
||||
}
|
||||
tokMap[email] = tokenByEmail{tok: t, ok: true}
|
||||
}
|
||||
|
||||
readErrMap := make(map[string]authTokenReadError, len(tokenReadErrors))
|
||||
for _, readErr := range tokenReadErrors {
|
||||
email := normalizeEmail(readErr.Email)
|
||||
if email == "" {
|
||||
continue
|
||||
}
|
||||
readErrMap[email] = readErr
|
||||
}
|
||||
|
||||
entries := make([]authListEntry, 0, len(tokens)+len(serviceAccountEmails)+len(tokenReadErrors))
|
||||
seen := make(map[string]struct{})
|
||||
for _, email := range serviceAccountEmails {
|
||||
email = normalizeEmail(email)
|
||||
if email == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[email]; ok {
|
||||
continue
|
||||
}
|
||||
seen[email] = struct{}{}
|
||||
entries = append(entries, authListEntryForServiceAccount(email, tokMap[email], readErrMap[email]))
|
||||
}
|
||||
for _, t := range tokens {
|
||||
email := normalizeEmail(t.Email)
|
||||
if email == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[email]; ok {
|
||||
continue
|
||||
}
|
||||
seen[email] = struct{}{}
|
||||
tok := t
|
||||
entries = append(entries, authListEntry{Email: email, Token: &tok})
|
||||
}
|
||||
for _, readErr := range tokenReadErrors {
|
||||
email := normalizeEmail(readErr.Email)
|
||||
if email == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[email]; ok {
|
||||
continue
|
||||
}
|
||||
seen[email] = struct{}{}
|
||||
entries = append(entries, authListEntryForReadError(email, readErr))
|
||||
}
|
||||
|
||||
sort.Slice(entries, func(i, j int) bool { return entries[i].Email < entries[j].Email })
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
func authListEntryForServiceAccount(email string, te tokenByEmail, readErr authTokenReadError) authListEntry {
|
||||
var tok *secrets.Token
|
||||
if te.ok {
|
||||
t := te.tok
|
||||
tok = &t
|
||||
}
|
||||
entry := authListEntry{Email: email, Client: readErr.Client, Token: tok, SA: true}
|
||||
if readErr.Err != nil {
|
||||
entry.ReadErr = readErr.Err
|
||||
_, entry.ReadHint = classifyAuthDoctorError(readErr.Err)
|
||||
}
|
||||
|
||||
return entry
|
||||
}
|
||||
|
||||
func authListEntryForReadError(email string, readErr authTokenReadError) authListEntry {
|
||||
entry := authListEntry{Email: email, Client: readErr.Client, ReadErr: readErr.Err}
|
||||
_, entry.ReadHint = classifyAuthDoctorError(readErr.Err)
|
||||
|
||||
return entry
|
||||
}
|
||||
|
||||
func (e authListEntry) authType() string {
|
||||
if e.SA && (e.Token != nil || e.ReadErr != nil) {
|
||||
return authTypeOAuthServiceAccount
|
||||
}
|
||||
if e.SA {
|
||||
return authTypeServiceAccount
|
||||
}
|
||||
|
||||
return authTypeOAuth
|
||||
}
|
||||
|
||||
func (e authListEntry) details() (client string, created string, services []string, scopes []string) {
|
||||
client = e.Client
|
||||
if e.Token != nil {
|
||||
if !e.Token.CreatedAt.IsZero() {
|
||||
created = e.Token.CreatedAt.UTC().Format("2006-01-02T15:04:05Z07:00")
|
||||
}
|
||||
return e.Token.Client, created, e.Token.Services, e.Token.Scopes
|
||||
}
|
||||
if e.SA {
|
||||
if _, mtime, ok := bestServiceAccountPathAndMtime(e.Email); ok {
|
||||
created = mtime.UTC().Format("2006-01-02T15:04:05Z07:00")
|
||||
}
|
||||
return client, created, []string{"service-account"}, nil
|
||||
}
|
||||
|
||||
return client, created, nil, nil
|
||||
}
|
||||
|
||||
func (c *AuthListCmd) writeAuthListJSON(ctx context.Context, entries []authListEntry) error {
|
||||
out := make([]authListJSONItem, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
client, created, services, scopes := e.details()
|
||||
it := authListJSONItem{
|
||||
Email: e.Email,
|
||||
Client: client,
|
||||
Services: services,
|
||||
Scopes: scopes,
|
||||
CreatedAt: created,
|
||||
Auth: e.authType(),
|
||||
}
|
||||
if e.Token != nil {
|
||||
it.Subject = e.Token.Subject
|
||||
}
|
||||
if e.ReadErr != nil {
|
||||
it.Error = e.ReadErr.Error()
|
||||
it.Hint = e.ReadHint
|
||||
}
|
||||
if c.Check {
|
||||
c.annotateAuthListCheck(ctx, e, &it)
|
||||
}
|
||||
out = append(out, it)
|
||||
}
|
||||
|
||||
return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{"accounts": out})
|
||||
}
|
||||
|
||||
func (c *AuthListCmd) annotateAuthListCheck(ctx context.Context, e authListEntry, it *authListJSONItem) {
|
||||
switch {
|
||||
case e.ReadErr != nil:
|
||||
valid := false
|
||||
it.Valid = &valid
|
||||
case e.Token == nil:
|
||||
valid := true
|
||||
it.Valid = &valid
|
||||
it.Error = "service account (not checked)"
|
||||
default:
|
||||
err := checkRefreshToken(ctx, e.Token.Client, e.Token.RefreshToken, e.Token.Scopes, c.Timeout)
|
||||
valid := err == nil
|
||||
it.Valid = &valid
|
||||
if err != nil {
|
||||
it.Error = err.Error()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *AuthListCmd) writeAuthListText(ctx context.Context, u *ui.UI, entries []authListEntry) error {
|
||||
for _, e := range entries {
|
||||
writeAuthListReadWarning(u, e)
|
||||
|
||||
client, created, services, _ := e.details()
|
||||
servicesCSV := strings.Join(services, ",")
|
||||
if c.Check {
|
||||
c.writeAuthListCheckRow(ctx, u, e, client, servicesCSV, created)
|
||||
continue
|
||||
}
|
||||
|
||||
u.Out().Printf("%s\t%s\t%s\t%s\t%s", e.Email, client, servicesCSV, created, e.authType())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeAuthListReadWarning(u *ui.UI, e authListEntry) {
|
||||
if e.ReadErr == nil {
|
||||
return
|
||||
}
|
||||
u.Err().Printf("WARN\t%s\t%s", e.Email, e.ReadErr.Error())
|
||||
if e.ReadHint != "" {
|
||||
u.Err().Printf("hint\t%s\t%s", e.Email, e.ReadHint)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *AuthListCmd) writeAuthListCheckRow(ctx context.Context, u *ui.UI, e authListEntry, client string, servicesCSV string, created string) {
|
||||
switch {
|
||||
case e.ReadErr != nil:
|
||||
u.Out().Printf("%s\t%s\t%s\t%s\t%t\t%s\t%s", e.Email, client, servicesCSV, created, false, e.ReadErr.Error(), e.authType())
|
||||
case e.Token == nil:
|
||||
u.Out().Printf("%s\t%s\t%s\t%s\t%t\t%s\t%s", e.Email, client, servicesCSV, created, true, "service account (not checked)", e.authType())
|
||||
default:
|
||||
err := checkRefreshToken(ctx, e.Token.Client, e.Token.RefreshToken, e.Token.Scopes, c.Timeout)
|
||||
valid := err == nil
|
||||
msg := ""
|
||||
if err != nil {
|
||||
msg = err.Error()
|
||||
}
|
||||
u.Out().Printf("%s\t%s\t%s\t%s\t%t\t%s\t%s", e.Email, client, servicesCSV, created, valid, msg, e.authType())
|
||||
}
|
||||
}
|
||||
|
||||
func readableTokens(store secrets.Store) ([]secrets.Token, []authTokenReadError, error) {
|
||||
keys, err := store.Keys()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("list tokens: %w", err)
|
||||
}
|
||||
|
||||
out := make([]secrets.Token, 0)
|
||||
readErrors := make([]authTokenReadError, 0)
|
||||
seen := make(map[string]struct{})
|
||||
|
||||
for _, key := range keys {
|
||||
client, email, ok := secrets.ParseTokenKey(key)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
keyID := client + "\n" + email
|
||||
if _, ok := seen[keyID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[keyID] = struct{}{}
|
||||
|
||||
tok, err := store.GetToken(client, email)
|
||||
if err != nil {
|
||||
readErrors = append(readErrors, authTokenReadError{
|
||||
Client: client,
|
||||
Email: email,
|
||||
Err: fmt.Errorf("read token for %s: %w", email, err),
|
||||
})
|
||||
continue
|
||||
}
|
||||
|
||||
if tok.Subject != "" {
|
||||
subjectID := tok.Client + "\nsub:" + tok.Subject
|
||||
if _, ok := seen[subjectID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[subjectID] = struct{}{}
|
||||
}
|
||||
out = append(out, tok)
|
||||
}
|
||||
|
||||
return out, readErrors, nil
|
||||
}
|
||||
@ -32,18 +32,10 @@ func (c *AuthTokensListCmd) Run(ctx context.Context, _ *RootFlags) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tokens, err := store.ListTokens()
|
||||
filtered, err := storedTokenKeys(store)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filtered := make([]string, 0, len(tokens))
|
||||
for _, tok := range tokens {
|
||||
if strings.TrimSpace(tok.Email) == "" {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, secrets.TokenKey(tok.Client, tok.Email))
|
||||
}
|
||||
sort.Strings(filtered)
|
||||
|
||||
if len(filtered) == 0 {
|
||||
if outfmt.IsJSON(ctx) {
|
||||
@ -61,6 +53,31 @@ func (c *AuthTokensListCmd) Run(ctx context.Context, _ *RootFlags) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func storedTokenKeys(store secrets.Store) ([]string, error) {
|
||||
keys, err := store.Keys()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filtered := make([]string, 0, len(keys))
|
||||
seen := make(map[string]struct{}, len(keys))
|
||||
for _, key := range keys {
|
||||
client, email, ok := secrets.ParseTokenKey(key)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
tokenKey := secrets.TokenKey(client, email)
|
||||
if _, ok := seen[tokenKey]; ok {
|
||||
continue
|
||||
}
|
||||
seen[tokenKey] = struct{}{}
|
||||
filtered = append(filtered, tokenKey)
|
||||
}
|
||||
sort.Strings(filtered)
|
||||
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
type AuthTokensDeleteCmd struct {
|
||||
Email string `arg:"" name:"email" help:"Email"`
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user