|
| 1 | +package create |
| 2 | + |
| 3 | +import ( |
| 4 | + "encoding/base64" |
| 5 | + "errors" |
| 6 | + "fmt" |
| 7 | + "io" |
| 8 | + "io/ioutil" |
| 9 | + "net/http" |
| 10 | + "strings" |
| 11 | + |
| 12 | + "github.com/MakeNowJust/heredoc" |
| 13 | + "github.com/cli/cli/api" |
| 14 | + "github.com/cli/cli/internal/ghinstance" |
| 15 | + "github.com/cli/cli/internal/ghrepo" |
| 16 | + "github.com/cli/cli/pkg/cmd/secret/shared" |
| 17 | + "github.com/cli/cli/pkg/cmdutil" |
| 18 | + "github.com/cli/cli/pkg/iostreams" |
| 19 | + "github.com/spf13/cobra" |
| 20 | + "golang.org/x/crypto/nacl/box" |
| 21 | +) |
| 22 | + |
| 23 | +type CreateOptions struct { |
| 24 | + HttpClient func() (*http.Client, error) |
| 25 | + IO *iostreams.IOStreams |
| 26 | + BaseRepo func() (ghrepo.Interface, error) |
| 27 | + |
| 28 | + RandomOverride io.Reader |
| 29 | + |
| 30 | + SecretName string |
| 31 | + OrgName string |
| 32 | + Body string |
| 33 | + Visibility string |
| 34 | + RepositoryNames []string |
| 35 | +} |
| 36 | + |
| 37 | +func NewCmdCreate(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Command { |
| 38 | + opts := &CreateOptions{ |
| 39 | + IO: f.IOStreams, |
| 40 | + HttpClient: f.HttpClient, |
| 41 | + } |
| 42 | + |
| 43 | + cmd := &cobra.Command{ |
| 44 | + Use: "create <secret name>", |
| 45 | + Short: "Create secrets", |
| 46 | + Long: "Locally encrypt a new secret and send it to GitHub for storage.", |
| 47 | + Example: heredoc.Doc(` |
| 48 | + $ cat SECRET.txt | gh secret create NEW_SECRET |
| 49 | + $ gh secret create NEW_SECRET -b"some literal value" |
| 50 | + $ gh secret create NEW_SECRET -b"@file.json" |
| 51 | + $ gh secret create ORG_SECRET --org |
| 52 | + $ gh secret create ORG_SECRET --org=anotherOrg --visibility=selected -r="repo1,repo2,repo3" |
| 53 | + $ gh secret create ORG_SECRET --org=anotherOrg --visibility="all" |
| 54 | +`), |
| 55 | + Args: func(cmd *cobra.Command, args []string) error { |
| 56 | + if len(args) != 1 { |
| 57 | + return &cmdutil.FlagError{Err: errors.New("must pass single secret name")} |
| 58 | + } |
| 59 | + if !cmd.Flags().Changed("body") && opts.IO.IsStdinTTY() { |
| 60 | + return &cmdutil.FlagError{Err: errors.New("no --body specified but nothing on STIDN")} |
| 61 | + } |
| 62 | + return nil |
| 63 | + }, |
| 64 | + RunE: func(cmd *cobra.Command, args []string) error { |
| 65 | + // support `-R, --repo` override |
| 66 | + opts.BaseRepo = f.BaseRepo |
| 67 | + |
| 68 | + opts.SecretName = args[0] |
| 69 | + |
| 70 | + if cmd.Flags().Changed("visibility") { |
| 71 | + if opts.OrgName == "" { |
| 72 | + return &cmdutil.FlagError{Err: errors.New( |
| 73 | + "--visibility not supported for repository secrets; did you mean to pass --org?")} |
| 74 | + } |
| 75 | + |
| 76 | + if opts.Visibility != shared.VisAll && opts.Visibility != shared.VisPrivate && opts.Visibility != shared.VisSelected { |
| 77 | + return &cmdutil.FlagError{Err: errors.New( |
| 78 | + "--visibility must be one of `all`, `private`, or `selected`")} |
| 79 | + } |
| 80 | + } |
| 81 | + |
| 82 | + if cmd.Flags().Changed("repos") && opts.Visibility != shared.VisSelected { |
| 83 | + return &cmdutil.FlagError{Err: errors.New( |
| 84 | + "--repos only supported when --visibility='selected'")} |
| 85 | + } |
| 86 | + |
| 87 | + if opts.Visibility == shared.VisSelected && len(opts.RepositoryNames) == 0 { |
| 88 | + return &cmdutil.FlagError{Err: errors.New( |
| 89 | + "--repos flag required when --visibility='selected'")} |
| 90 | + } |
| 91 | + |
| 92 | + if runF != nil { |
| 93 | + return runF(opts) |
| 94 | + } |
| 95 | + |
| 96 | + return createRun(opts) |
| 97 | + }, |
| 98 | + } |
| 99 | + cmd.Flags().StringVar(&opts.OrgName, "org", "", "List secrets for an organization") |
| 100 | + cmd.Flags().Lookup("org").NoOptDefVal = "@owner" |
| 101 | + cmd.Flags().StringVarP(&opts.Visibility, "visibility", "v", "private", "Set visibility for an organization secret: `all`, `private`, or `selected`") |
| 102 | + cmd.Flags().StringSliceVarP(&opts.RepositoryNames, "repos", "r", []string{}, "List of repository names for `selected` visibility") |
| 103 | + cmd.Flags().StringVarP(&opts.Body, "body", "b", "-", "Provide either a literal string or a file path; prepend file paths with an @. Reads from STDIN if not provided.") |
| 104 | + |
| 105 | + return cmd |
| 106 | +} |
| 107 | + |
| 108 | +func createRun(opts *CreateOptions) error { |
| 109 | + body, err := getBody(opts) |
| 110 | + if err != nil { |
| 111 | + return fmt.Errorf("did not understand secret body: %w", err) |
| 112 | + } |
| 113 | + |
| 114 | + c, err := opts.HttpClient() |
| 115 | + if err != nil { |
| 116 | + return fmt.Errorf("could not create http client: %w", err) |
| 117 | + } |
| 118 | + client := api.NewClientFromHTTP(c) |
| 119 | + |
| 120 | + var baseRepo ghrepo.Interface |
| 121 | + if opts.OrgName == "" || opts.OrgName == "@owner" { |
| 122 | + baseRepo, err = opts.BaseRepo() |
| 123 | + if err != nil { |
| 124 | + return fmt.Errorf("could not determine base repo: %w", err) |
| 125 | + } |
| 126 | + } |
| 127 | + |
| 128 | + host := ghinstance.OverridableDefault() |
| 129 | + if opts.OrgName == "@owner" { |
| 130 | + opts.OrgName = baseRepo.RepoOwner() |
| 131 | + host = baseRepo.RepoHost() |
| 132 | + } |
| 133 | + |
| 134 | + var pk *PubKey |
| 135 | + if opts.OrgName != "" { |
| 136 | + pk, err = getOrgPublicKey(client, host, opts.OrgName) |
| 137 | + } else { |
| 138 | + pk, err = getRepoPubKey(client, baseRepo) |
| 139 | + } |
| 140 | + if err != nil { |
| 141 | + return fmt.Errorf("failed to fetch public key: %w", err) |
| 142 | + } |
| 143 | + |
| 144 | + eBody, err := box.SealAnonymous(nil, body, &pk.Raw, opts.RandomOverride) |
| 145 | + if err != nil { |
| 146 | + return fmt.Errorf("failed to encrypt body: %w", err) |
| 147 | + } |
| 148 | + |
| 149 | + encoded := base64.StdEncoding.EncodeToString(eBody) |
| 150 | + |
| 151 | + if opts.OrgName != "" { |
| 152 | + err = putOrgSecret(client, pk, host, *opts, encoded) |
| 153 | + } else { |
| 154 | + err = putRepoSecret(client, pk, baseRepo, opts.SecretName, encoded) |
| 155 | + } |
| 156 | + if err != nil { |
| 157 | + return fmt.Errorf("failed to create secret: %w", err) |
| 158 | + } |
| 159 | + |
| 160 | + return nil |
| 161 | +} |
| 162 | + |
| 163 | +func getBody(opts *CreateOptions) (body []byte, err error) { |
| 164 | + if opts.Body == "-" { |
| 165 | + body, err = ioutil.ReadAll(opts.IO.In) |
| 166 | + if err != nil { |
| 167 | + return nil, fmt.Errorf("failed to read from STDIN: %w", err) |
| 168 | + } |
| 169 | + |
| 170 | + return |
| 171 | + } |
| 172 | + |
| 173 | + if strings.HasPrefix(opts.Body, "@") { |
| 174 | + body, err = opts.IO.ReadUserFile(opts.Body[1:]) |
| 175 | + if err != nil { |
| 176 | + return nil, fmt.Errorf("failed to read file %s: %w", opts.Body[1:], err) |
| 177 | + } |
| 178 | + |
| 179 | + return |
| 180 | + } |
| 181 | + |
| 182 | + return []byte(opts.Body), nil |
| 183 | +} |
0 commit comments