package main import ( "encoding/json" "fmt" "os" "strings" "github.com/spf13/cobra" ) // Credentials represents docker credentials (Docker credential helper protocol) type Credentials struct { ServerURL string `json:"ServerURL,omitempty"` Username string `json:"Username,omitempty"` Secret string `json:"Secret,omitempty"` } func newGetCmd() *cobra.Command { return &cobra.Command{ Use: "get", Short: "Get credentials for a registry (Docker protocol)", Hidden: true, RunE: runGet, } } func newStoreCmd() *cobra.Command { return &cobra.Command{ Use: "store", Short: "Store credentials (Docker protocol)", Hidden: true, RunE: runStore, } } func newEraseCmd() *cobra.Command { return &cobra.Command{ Use: "erase", Short: "Erase credentials (Docker protocol)", Hidden: true, RunE: runErase, } } func newListCmd() *cobra.Command { return &cobra.Command{ Use: "list", Short: "List all credentials (Docker protocol extension)", Hidden: true, RunE: runList, } } func runGet(cmd *cobra.Command, args []string) error { // If stdin is a terminal, the user ran this directly (not Docker calling us) if isTerminal(os.Stdin) { fmt.Fprintf(os.Stderr, "The 'get' command is part of the Docker credential helper protocol.\n") fmt.Fprintf(os.Stderr, "It should not be run directly.\n\n") fmt.Fprintf(os.Stderr, "To authenticate with a registry, run:\n") fmt.Fprintf(os.Stderr, " docker-credential-atcr login\n\n") fmt.Fprintf(os.Stderr, "To check your accounts:\n") fmt.Fprintf(os.Stderr, " docker-credential-atcr status\n") return fmt.Errorf("not a pipe") } // Docker sends the server URL as a plain string on stdin (not JSON) var serverURL string if _, err := fmt.Fscanln(os.Stdin, &serverURL); err != nil { return fmt.Errorf("reading server URL: %w", err) } appViewURL := buildAppViewURL(serverURL) cfg, err := loadConfig() if err != nil { fmt.Fprintf(os.Stderr, "Warning: config load error: %v\n", err) } acct, err := cfg.resolveAccount(appViewURL, serverURL) if err != nil { return err } // Validate credentials result := validateCredentials(appViewURL, acct.Handle, acct.DeviceSecret) if !result.Valid { if result.OAuthSessionExpired { loginURL := result.LoginURL if loginURL == "" { loginURL = appViewURL + "/auth/oauth/login" } fmt.Fprintf(os.Stderr, "OAuth session expired for %s.\n", acct.Handle) fmt.Fprintf(os.Stderr, "Please visit: %s\n", loginURL) fmt.Fprintf(os.Stderr, "Then retry your docker command.\n") return fmt.Errorf("oauth session expired") } // Generic auth failure — remove the bad account fmt.Fprintf(os.Stderr, "Credentials for %s are invalid.\n", acct.Handle) fmt.Fprintf(os.Stderr, "Run: docker-credential-atcr login\n") cfg.removeAccount(appViewURL, acct.Handle) cfg.save() //nolint:errcheck return fmt.Errorf("invalid credentials") } // Check for updates (cached, non-blocking) checkAndNotifyUpdate() // Return credentials for Docker creds := Credentials{ ServerURL: serverURL, Username: acct.Handle, Secret: acct.DeviceSecret, } return json.NewEncoder(os.Stdout).Encode(creds) } func runStore(cmd *cobra.Command, args []string) error { var creds Credentials if err := json.NewDecoder(os.Stdin).Decode(&creds); err != nil { return fmt.Errorf("decoding credentials: %w", err) } // Only store if the secret looks like a device secret if !strings.HasPrefix(creds.Secret, "atcr_device_") { // Not our device secret — ignore (e.g., docker login with app-password) return nil } appViewURL := buildAppViewURL(creds.ServerURL) cfg, err := loadConfig() if err != nil { fmt.Fprintf(os.Stderr, "Warning: config load error: %v\n", err) } cfg.addAccount(appViewURL, &Account{ Handle: creds.Username, DeviceSecret: creds.Secret, }) return cfg.save() } func runErase(cmd *cobra.Command, args []string) error { var serverURL string if _, err := fmt.Fscanln(os.Stdin, &serverURL); err != nil { return fmt.Errorf("reading server URL: %w", err) } appViewURL := buildAppViewURL(serverURL) cfg, err := loadConfig() if err != nil { return nil // No config, nothing to erase } reg := cfg.findRegistry(appViewURL) if reg == nil { return nil } // Erase the active account (or sole account) handle := reg.Active if handle == "" && len(reg.Accounts) == 1 { for h := range reg.Accounts { handle = h } } if handle == "" { return nil } cfg.removeAccount(appViewURL, handle) return cfg.save() } func runList(cmd *cobra.Command, args []string) error { cfg, err := loadConfig() if err != nil { // Return empty object fmt.Println("{}") return nil } // Docker list protocol: {"ServerURL": "Username", ...} result := make(map[string]string) for url, reg := range cfg.Registries { // Strip scheme for Docker compatibility host := strings.TrimPrefix(url, "https://") host = strings.TrimPrefix(host, "http://") for _, acct := range reg.Accounts { result[host] = acct.Handle } } return json.NewEncoder(os.Stdout).Encode(result) } // checkAndNotifyUpdate checks for updates in the background and notifies the user func checkAndNotifyUpdate() { cache := loadUpdateCheckCache() if cache != nil && cache.Current == version { // Cache is fresh and for current version if isNewerVersion(cache.Latest, version) { fmt.Fprintf(os.Stderr, "\nUpdate available: %s (current: %s)\n", cache.Latest, version) fmt.Fprintf(os.Stderr, "Run: docker-credential-atcr update\n\n") } // Check if cache is still fresh (24h) if cache.CheckedAt.Add(updateCheckCacheTTL).After(timeNow()) { return } } latest, err := fetchLatestVersion() if err != nil { return // Silently fail } saveUpdateCheckCache(&UpdateCheckCache{ CheckedAt: timeNow(), Latest: latest, Current: version, }) if isNewerVersion(latest, version) { fmt.Fprintf(os.Stderr, "\nUpdate available: %s (current: %s)\n", latest, version) fmt.Fprintf(os.Stderr, "Run: docker-credential-atcr update\n\n") } }