Add basic-auth / header addition to OTS-CLI

closes #134

Signed-off-by: Knut Ahlers <knut@ahlers.me>
This commit is contained in:
Knut Ahlers 2023-10-18 12:34:02 +02:00
parent 5c615fbfa5
commit a83ae9ceb3
Signed by: luzifer
GPG key ID: D91C3E91E4CAD6F5
2 changed files with 74 additions and 1 deletions

View file

@ -64,6 +64,10 @@ Both commands can be used in scripts:
- `fetch` prints the secret to `STDOUT` and stores files to the given directory - `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 - 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!) ### 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: 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:

View file

@ -4,14 +4,25 @@ import (
"fmt" "fmt"
"io" "io"
"mime" "mime"
"net/http"
"os" "os"
"path" "path"
"strings"
"github.com/Luzifer/ots/pkg/client" "github.com/Luzifer/ots/pkg/client"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
type (
authRoundTripper struct {
http.RoundTripper
headers http.Header
user, pass string
}
)
var createCmd = &cobra.Command{ var createCmd = &cobra.Command{
Use: "create [-f file]... [--instance url] [--secret-from file]", Use: "create [-f file]... [--instance url] [--secret-from file]",
Short: "Create a new encrypted secret in the given OTS instance", Short: "Create a new encrypted secret in the given OTS instance",
@ -23,15 +34,21 @@ var createCmd = &cobra.Command{
func init() { func init() {
createCmd.Flags().Duration("expire", 0, "When to expire the secret (0 to use server-default)") 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().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().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().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) rootCmd.AddCommand(createCmd)
} }
func createRunE(cmd *cobra.Command, _ []string) error { func createRunE(cmd *cobra.Command, _ []string) (err error) {
var secret client.Secret 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 // Read the secret content
logrus.Info("reading secret content...") logrus.Info("reading secret content...")
secretSourceName, err := cmd.Flags().GetString("secret-from") secretSourceName, err := cmd.Flags().GetString("secret-from")
@ -103,3 +120,55 @@ func createRunE(cmd *cobra.Command, _ []string) error {
return nil 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
}