From a83ae9ceb31d4991f30584abb4685ad0f93f37fb Mon Sep 17 00:00:00 2001 From: Knut Ahlers Date: Wed, 18 Oct 2023 12:34:02 +0200 Subject: [PATCH] Add basic-auth / header addition to OTS-CLI closes #134 Signed-off-by: Knut Ahlers --- README.md | 4 +++ cmd/ots-cli/cmd_create.go | 71 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f187694..1db58f8 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,10 @@ Both commands can be used in scripts: - `fetch` prints the secret to `STDOUT` and stores files to the given directory - both sends logs to `STDERR` which you can disable (`--log-level=fatal`) or ignore in your script +In case your instance needs credentials to use the `/api/create` endpoint you can pass them to OTS-CLI like you would do with curl: +- `ots-cli create --instance ... -u myuser:mypass` for basic-auth +- `ots-cli create --instance ... -H 'Authorization: Token abcde'` for token-auth (you can set any header you need, just repeat `-H ...`) + ### Bash: Sharing an encrypted secret (strongly recommended!) This is slightly more complex as you first need to encrypt your secret before sending it to the API but in this case you can be sure the server will in no case be able to access the secret. Especially if you are using ots.fyi (my public hosted instance) you should not trust me with your secret but use an encrypted secret: diff --git a/cmd/ots-cli/cmd_create.go b/cmd/ots-cli/cmd_create.go index e0f3018..bc7e5d9 100644 --- a/cmd/ots-cli/cmd_create.go +++ b/cmd/ots-cli/cmd_create.go @@ -4,14 +4,25 @@ import ( "fmt" "io" "mime" + "net/http" "os" "path" + "strings" "github.com/Luzifer/ots/pkg/client" "github.com/sirupsen/logrus" "github.com/spf13/cobra" ) +type ( + authRoundTripper struct { + http.RoundTripper + + headers http.Header + user, pass string + } +) + var createCmd = &cobra.Command{ Use: "create [-f file]... [--instance url] [--secret-from file]", Short: "Create a new encrypted secret in the given OTS instance", @@ -23,15 +34,21 @@ var createCmd = &cobra.Command{ func init() { createCmd.Flags().Duration("expire", 0, "When to expire the secret (0 to use server-default)") + createCmd.Flags().StringSliceP("header", "H", nil, "Headers to include in the request (i.e. 'Authorization: Token ...')") createCmd.Flags().String("instance", "https://ots.fyi/", "Instance to create the secret with") createCmd.Flags().StringSliceP("file", "f", nil, "File(s) to attach to the secret") createCmd.Flags().String("secret-from", "-", `File to read the secret content from ("-" for STDIN)`) + createCmd.Flags().StringP("user", "u", "", "Username / Password for basic auth, specified as 'user:pass'") rootCmd.AddCommand(createCmd) } -func createRunE(cmd *cobra.Command, _ []string) error { +func createRunE(cmd *cobra.Command, _ []string) (err error) { var secret client.Secret + if client.HTTPClient, err = constructHTTPClient(cmd); err != nil { + return fmt.Errorf("constructing authorized HTTP client: %w", err) + } + // Read the secret content logrus.Info("reading secret content...") secretSourceName, err := cmd.Flags().GetString("secret-from") @@ -103,3 +120,55 @@ func createRunE(cmd *cobra.Command, _ []string) error { return nil } + +func constructHTTPClient(cmd *cobra.Command) (*http.Client, error) { + basic, _ := cmd.Flags().GetString("user") + headers, _ := cmd.Flags().GetStringSlice("header") + + if basic == "" && headers == nil { + // No authorization needed + return http.DefaultClient, nil + } + + t := authRoundTripper{RoundTripper: http.DefaultTransport, headers: http.Header{}} + + // Set basic auth if available + user, pass, ok := strings.Cut(basic, ":") + if ok { + t.user = user + t.pass = pass + } + + // Parse and set headers if available + for _, hdr := range headers { + key, value, ok := strings.Cut(hdr, ":") + if !ok { + logrus.WithField("header", hdr).Warn("invalid header format, skipping") + continue + } + t.headers.Add(key, strings.TrimSpace(value)) + } + + return &http.Client{Transport: t}, nil +} + +func (a authRoundTripper) RoundTrip(r *http.Request) (*http.Response, error) { + if a.user != "" { + r.SetBasicAuth(a.user, a.pass) + } + + for key, values := range a.headers { + if r.Header == nil { + r.Header = http.Header{} + } + for _, value := range values { + r.Header.Add(key, value) + } + } + + resp, err := a.RoundTripper.RoundTrip(r) + if err != nil { + return nil, fmt.Errorf("executing round-trip: %w", err) + } + return resp, nil +}