feat: add commands for managing GPG keys
This commit is contained in:
parent
75abeb13a8
commit
dad2d52571
6 changed files with 356 additions and 0 deletions
100
pkg/cmd/gpg-key/add/add.go
Normal file
100
pkg/cmd/gpg-key/add/add.go
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
package add
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type AddOptions struct {
|
||||
IO *iostreams.IOStreams
|
||||
Config func() (config.Config, error)
|
||||
HTTPClient func() (*http.Client, error)
|
||||
|
||||
KeyFile string
|
||||
}
|
||||
|
||||
func NewCmdAdd(f *cmdutil.Factory, runF func(*AddOptions) error) *cobra.Command {
|
||||
opts := &AddOptions{
|
||||
HTTPClient: f.HttpClient,
|
||||
Config: f.Config,
|
||||
IO: f.IOStreams,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "add [<key-file>]",
|
||||
Short: "Add a GPG key to your GitHub account",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 0 {
|
||||
if opts.IO.IsStdoutTTY() && opts.IO.IsStdinTTY() {
|
||||
return &cmdutil.FlagError{Err: errors.New("GPG public key file missing")}
|
||||
}
|
||||
opts.KeyFile = "-"
|
||||
} else {
|
||||
opts.KeyFile = args[0]
|
||||
}
|
||||
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
return runAdd(opts)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func runAdd(opts *AddOptions) error {
|
||||
httpClient, err := opts.HTTPClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var keyReader io.Reader
|
||||
if opts.KeyFile == "-" {
|
||||
keyReader = opts.IO.In
|
||||
defer opts.IO.In.Close()
|
||||
} else {
|
||||
f, err := os.Open(opts.KeyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
keyReader = f
|
||||
}
|
||||
|
||||
cfg, err := opts.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hostname, err := cfg.DefaultHost()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = GPGKeyUpload(httpClient, hostname, keyReader)
|
||||
if err != nil {
|
||||
if errors.Is(err, scopesError) {
|
||||
cs := opts.IO.ColorScheme()
|
||||
fmt.Fprint(opts.IO.ErrOut, "Error: insufficient OAuth scopes to list GPG keys\n")
|
||||
fmt.Fprintf(opts.IO.ErrOut, "Run the following to grant scopes: %s\n", cs.Bold("gh auth refresh -s write:gpg_key"))
|
||||
return cmdutil.SilentError
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if opts.IO.IsStdoutTTY() {
|
||||
cs := opts.IO.ColorScheme()
|
||||
fmt.Fprintf(opts.IO.ErrOut, "%s GPG public key added to your account\n", cs.SuccessIcon())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
67
pkg/cmd/gpg-key/add/http.go
Normal file
67
pkg/cmd/gpg-key/add/http.go
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
package add
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/ghinstance"
|
||||
)
|
||||
|
||||
var scopesError = errors.New("insufficient OAuth scopes")
|
||||
|
||||
func GPGKeyUpload(httpClient *http.Client, hostname string, keyFile io.Reader) error {
|
||||
url := ghinstance.RESTPrefix(hostname) + "user/gpg_keys"
|
||||
|
||||
keyBytes, err := ioutil.ReadAll(keyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
payload := map[string]string{
|
||||
"armored_public_key": string(keyBytes),
|
||||
}
|
||||
|
||||
payloadBytes, err := json.Marshal(payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", url, bytes.NewBuffer(payloadBytes))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 404 {
|
||||
return scopesError
|
||||
} else if resp.StatusCode > 299 {
|
||||
var httpError api.HTTPError
|
||||
err := api.HandleHTTPError(resp)
|
||||
if errors.As(err, &httpError) && isDuplicateError(&httpError) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(ioutil.Discard, resp.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isDuplicateError(err *api.HTTPError) bool {
|
||||
return err.StatusCode == 422 && len(err.Errors) == 1 &&
|
||||
err.Errors[0].Field == "key" && err.Errors[0].Message == "key is already in use"
|
||||
}
|
||||
21
pkg/cmd/gpg-key/gpg-key.go
Normal file
21
pkg/cmd/gpg-key/gpg-key.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package key
|
||||
|
||||
import (
|
||||
cmdAdd "github.com/cli/cli/pkg/cmd/gpg-key/add"
|
||||
cmdList "github.com/cli/cli/pkg/cmd/gpg-key/list"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewCmdGPGKey(f *cmdutil.Factory) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "gpg-key <command>",
|
||||
Short: "Manage GPG keys",
|
||||
Long: "Manage GPG keys registered with your GitHub account",
|
||||
}
|
||||
|
||||
cmd.AddCommand(cmdList.NewCmdList(f, nil))
|
||||
cmd.AddCommand(cmdAdd.NewCmdAdd(f, nil))
|
||||
|
||||
return cmd
|
||||
}
|
||||
58
pkg/cmd/gpg-key/list/http.go
Normal file
58
pkg/cmd/gpg-key/list/http.go
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
package list
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/api"
|
||||
"github.com/cli/cli/internal/ghinstance"
|
||||
)
|
||||
|
||||
var scopesError = errors.New("insufficient OAuth scopes")
|
||||
|
||||
type gpgKey struct {
|
||||
KeyId string `json:"key_id"`
|
||||
PublicKey string `json:"public_key"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
}
|
||||
|
||||
func userKeys(httpClient *http.Client, host, userHandle string) ([]gpgKey, error) {
|
||||
resource := "user/gpg_keys"
|
||||
if userHandle != "" {
|
||||
resource = fmt.Sprintf("users/%s/gpg_keys", userHandle)
|
||||
}
|
||||
url := fmt.Sprintf("%s%s?per_page=%d", ghinstance.RESTPrefix(host), resource, 100)
|
||||
req, err := http.NewRequest("GET", url, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == 404 {
|
||||
return nil, scopesError
|
||||
} else if resp.StatusCode > 299 {
|
||||
return nil, api.HandleHTTPError(resp)
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var keys []gpgKey
|
||||
err = json.Unmarshal(b, &keys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return keys, nil
|
||||
}
|
||||
108
pkg/cmd/gpg-key/list/list.go
Normal file
108
pkg/cmd/gpg-key/list/list.go
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
package list
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/cli/cli/internal/config"
|
||||
"github.com/cli/cli/pkg/cmdutil"
|
||||
"github.com/cli/cli/pkg/iostreams"
|
||||
"github.com/cli/cli/utils"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type ListOptions struct {
|
||||
IO *iostreams.IOStreams
|
||||
Config func() (config.Config, error)
|
||||
HTTPClient func() (*http.Client, error)
|
||||
}
|
||||
|
||||
func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command {
|
||||
opts := &ListOptions{
|
||||
IO: f.IOStreams,
|
||||
Config: f.Config,
|
||||
HTTPClient: f.HttpClient,
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "Lists GPG keys in your GitHub account",
|
||||
Args: cobra.ExactArgs(0),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if runF != nil {
|
||||
return runF(opts)
|
||||
}
|
||||
return listRun(opts)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func listRun(opts *ListOptions) error {
|
||||
apiClient, err := opts.HTTPClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg, err := opts.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
host, err := cfg.DefaultHost()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
gpgKeys, err := userKeys(apiClient, host, "")
|
||||
if err != nil {
|
||||
if errors.Is(err, scopesError) {
|
||||
cs := opts.IO.ColorScheme()
|
||||
fmt.Fprint(opts.IO.ErrOut, "Error: insufficient OAuth scopes to list GPG keys\n")
|
||||
fmt.Fprintf(opts.IO.ErrOut, "Run the following to grant scopes: %s\n", cs.Bold("gh auth refresh -s read:gpg_key"))
|
||||
return cmdutil.SilentError
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if len(gpgKeys) == 0 {
|
||||
fmt.Fprintln(opts.IO.ErrOut, "No GPG keys present in GitHub account.")
|
||||
return cmdutil.SilentError
|
||||
}
|
||||
|
||||
t := utils.NewTablePrinter(opts.IO)
|
||||
cs := opts.IO.ColorScheme()
|
||||
|
||||
for _, gpgKey := range gpgKeys {
|
||||
t.AddField(gpgKey.KeyId, nil, nil)
|
||||
|
||||
expiresAt := gpgKey.ExpiresAt.Format(time.RFC3339)
|
||||
if gpgKey.ExpiresAt.IsZero() {
|
||||
expiresAt = "Never"
|
||||
}
|
||||
t.AddField(expiresAt, nil, cs.Gray)
|
||||
|
||||
t.AddField(gpgKey.PublicKey, truncateMiddle, nil)
|
||||
t.EndRow()
|
||||
}
|
||||
|
||||
return t.Render()
|
||||
}
|
||||
|
||||
func truncateMiddle(maxWidth int, t string) string {
|
||||
if len(t) <= maxWidth {
|
||||
return t
|
||||
}
|
||||
|
||||
ellipsis := "..."
|
||||
if maxWidth < len(ellipsis)+2 {
|
||||
return t[0:maxWidth]
|
||||
}
|
||||
|
||||
halfWidth := (maxWidth - len(ellipsis)) / 2
|
||||
remainder := (maxWidth - len(ellipsis)) % 2
|
||||
return t[0:halfWidth+remainder] + ellipsis + t[len(t)-halfWidth:]
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ import (
|
|||
extensionsCmd "github.com/cli/cli/pkg/cmd/extensions"
|
||||
"github.com/cli/cli/pkg/cmd/factory"
|
||||
gistCmd "github.com/cli/cli/pkg/cmd/gist"
|
||||
gpgKeyCmd "github.com/cli/cli/pkg/cmd/gpg-key"
|
||||
issueCmd "github.com/cli/cli/pkg/cmd/issue"
|
||||
prCmd "github.com/cli/cli/pkg/cmd/pr"
|
||||
releaseCmd "github.com/cli/cli/pkg/cmd/release"
|
||||
|
|
@ -80,6 +81,7 @@ func NewCmdRoot(f *cmdutil.Factory, version, buildDate string) *cobra.Command {
|
|||
cmd.AddCommand(configCmd.NewCmdConfig(f))
|
||||
cmd.AddCommand(creditsCmd.NewCmdCredits(f, nil))
|
||||
cmd.AddCommand(gistCmd.NewCmdGist(f))
|
||||
cmd.AddCommand(gpgKeyCmd.NewCmdGPGKey(f))
|
||||
cmd.AddCommand(completionCmd.NewCmdCompletion(f.IOStreams))
|
||||
cmd.AddCommand(extensionsCmd.NewCmdExtensions(f.IOStreams))
|
||||
cmd.AddCommand(secretCmd.NewCmdSecret(f))
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue