Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Non-interactive mode #252

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
363 changes: 197 additions & 166 deletions cli/auth.go
Original file line number Diff line number Diff line change
@@ -1,166 +1,197 @@
package cli

import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/exec"
"strings"
"syscall"

"golang.org/x/term"
)

// AuthParam describes an auth input parameter for an AuthHandler.
type AuthParam struct {
Name string
Help string
Required bool
}

// AuthHandler is used to register new authentication handlers that will apply
// auth to an outgoing request as needed.
type AuthHandler interface {
// Parameters returns an ordered list of required and optional input
// parameters for this auth handler. Used when configuring an API.
Parameters() []AuthParam

// OnRequest applies auth to an outgoing request before it hits the wire.
OnRequest(req *http.Request, key string, params map[string]string) error
}

var authHandlers map[string]AuthHandler = map[string]AuthHandler{}

// AddAuth registers a new named auth handler.
func AddAuth(name string, h AuthHandler) {
authHandlers[name] = h
}

// BasicAuth implements HTTP Basic authentication.
type BasicAuth struct{}

// Parameters define the HTTP Basic Auth parameter names.
func (a *BasicAuth) Parameters() []AuthParam {
return []AuthParam{
{Name: "username", Required: true},
{Name: "password", Required: true},
}
}

// OnRequest gets run before the request goes out on the wire.
func (a *BasicAuth) OnRequest(req *http.Request, key string, params map[string]string) error {
_, usernamePresent := params["username"]
_, passwordPresent := params["password"]

if usernamePresent && !passwordPresent {
fmt.Print("password: ")
inputPassword, err := term.ReadPassword(int(syscall.Stdin))
if err == nil {
params["password"] = string(inputPassword)
}
fmt.Println()
}

req.SetBasicAuth(params["username"], params["password"])
return nil
}

// ExternalToolAuth defers authentication to a third party tool.
// This avoids baking all possible authentication implementations
// inside restish itself.
type ExternalToolAuth struct{}

// Request is used to exchange requests with the external tool.
type Request struct {
Method string `json:"method"`
URI string `json:"uri"`
Header http.Header `json:"headers"`
Body string `json:"body"`
}

// Parameters defines the ExternalToolAuth parameter names.
// A single parameter is supported and required: `commandline` which
// points to the tool to call to authenticate a request.
func (a *ExternalToolAuth) Parameters() []AuthParam {
return []AuthParam{
{Name: "commandline", Required: true},
{Name: "omitbody", Required: false},
}
}

// OnRequest gets run before the request goes out on the wire.
// The supplied commandline argument is ran with a JSON input
// and expects a JSON output on stdout
func (a *ExternalToolAuth) OnRequest(req *http.Request, key string, params map[string]string) error {
commandLine := params["commandline"]
omitBodyStr, omitBodyPresent := params["omitbody"]
omitBody := false
if omitBodyPresent && strings.EqualFold(omitBodyStr, "true") {
omitBody = true
}
shell, shellPresent := os.LookupEnv("SHELL")
if !shellPresent {
shell = "/bin/sh"
}
cmd := exec.Command(shell, "-c", commandLine)
stdin, err := cmd.StdinPipe()
if err != nil {
return err
}

bodyStr := ""
if req.Body != nil && !omitBody {
bodyBytes, err := io.ReadAll(req.Body)
if err != nil {
return err
}
bodyStr = string(bodyBytes)
req.Body = io.NopCloser(strings.NewReader(bodyStr))
}

textRequest := Request{
Method: req.Method,
URI: req.URL.String(),
Header: req.Header,
Body: bodyStr,
}
requestBytes, err := json.Marshal(textRequest)
if err != nil {
return err
}
_, err = stdin.Write(requestBytes)
if err != nil {
return err
}
stdin.Close()
outBytes, err := cmd.Output()
if err != nil {
return err
}
if len(outBytes) <= 0 {
return nil
}
var requestUpdates Request
err = json.Unmarshal(outBytes, &requestUpdates)
if err != nil {
return err
}

if len(requestUpdates.URI) > 0 {
req.URL, err = url.Parse(requestUpdates.URI)
if err != nil {
return err
}
}

for k, vs := range requestUpdates.Header {
for _, v := range vs {
// A single value is supported for each header
req.Header.Set(k, v)
}
}
return nil
}
package cli

import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"os/exec"
"strings"
"syscall"

"golang.org/x/term"
"github.com/spf13/viper"
)

// AuthParam describes an auth input parameter for an AuthHandler.
type AuthParam struct {
Name string
Help string
Required bool
}

// AuthHandler is used to register new authentication handlers that will apply
// auth to an outgoing request as needed.
type AuthHandler interface {
// Parameters returns an ordered list of required and optional input
// parameters for this auth handler. Used when configuring an API.
Parameters() []AuthParam

// OnRequest applies auth to an outgoing request before it hits the wire.
OnRequest(req *http.Request, key string, params map[string]string) error
}

var authHandlers map[string]AuthHandler = map[string]AuthHandler{}

// AddAuth registers a new named auth handler.
func AddAuth(name string, h AuthHandler) {
authHandlers[name] = h
}

// BasicAuth implements HTTP Basic authentication.
type BasicAuth struct{}

// Parameters define the HTTP Basic Auth parameter names.
func (a *BasicAuth) Parameters() []AuthParam {
return []AuthParam{
{Name: "username", Required: true},
{Name: "password", Required: true},
}
}

// OnRequest gets run before the request goes out on the wire.
func (a *BasicAuth) OnRequest(req *http.Request, key string, params map[string]string) error {
_, usernamePresent := params["username"]
_, passwordPresent := params["password"]

if usernamePresent && !passwordPresent {
fmt.Print("password: ")
inputPassword, err := term.ReadPassword(int(syscall.Stdin))
if err == nil {
params["password"] = string(inputPassword)
}
fmt.Println()
}

req.SetBasicAuth(params["username"], params["password"])
return nil
}

// ExternalToolAuth defers authentication to a third party tool.
// This avoids baking all possible authentication implementations
// inside restish itself.
type ExternalToolAuth struct{}

// Request is used to exchange requests with the external tool.
type Request struct {
Method string `json:"method"`
URI string `json:"uri"`
Header http.Header `json:"headers"`
Body string `json:"body"`
}

// Parameters defines the ExternalToolAuth parameter names.
// A single parameter is supported and required: `commandline` which
// points to the tool to call to authenticate a request.
func (a *ExternalToolAuth) Parameters() []AuthParam {
return []AuthParam{
{Name: "commandline", Required: true},
{Name: "omitbody", Required: false},
}
}

// OnRequest gets run before the request goes out on the wire.
// The supplied commandline argument is ran with a JSON input
// and expects a JSON output on stdout
func (a *ExternalToolAuth) OnRequest(req *http.Request, key string, params map[string]string) error {
commandLine := params["commandline"]
omitBodyStr, omitBodyPresent := params["omitbody"]
omitBody := false
if omitBodyPresent && strings.EqualFold(omitBodyStr, "true") {
omitBody = true
}
shell, shellPresent := os.LookupEnv("SHELL")
if !shellPresent {
shell = "/bin/sh"
}
cmd := exec.Command(shell, "-c", commandLine)
stdin, err := cmd.StdinPipe()
if err != nil {
return err
}

bodyStr := ""
if req.Body != nil && !omitBody {
bodyBytes, err := io.ReadAll(req.Body)
if err != nil {
return err
}
bodyStr = string(bodyBytes)
req.Body = io.NopCloser(strings.NewReader(bodyStr))
}

textRequest := Request{
Method: req.Method,
URI: req.URL.String(),
Header: req.Header,
Body: bodyStr,
}
requestBytes, err := json.Marshal(textRequest)
if err != nil {
return err
}
_, err = stdin.Write(requestBytes)
if err != nil {
return err
}
stdin.Close()
outBytes, err := cmd.Output()
if err != nil {
return err
}
if len(outBytes) <= 0 {
return nil
}
var requestUpdates Request
err = json.Unmarshal(outBytes, &requestUpdates)
if err != nil {
return err
}

if len(requestUpdates.URI) > 0 {
req.URL, err = url.Parse(requestUpdates.URI)
if err != nil {
return err
}
}

for k, vs := range requestUpdates.Header {
for _, v := range vs {
// A single value is supported for each header
req.Header.Set(k, v)
}
}
return nil
}

// ExternalOverrideAuth implements External Override Auth where
// an HTTP Authorization token is passed in as an argument in non-interactive
// mode.
type ExternalOverrideAuth struct{}

// Parameters define the External Override Auth parameter names.
func (a *ExternalOverrideAuth) Parameters() []AuthParam {
return []AuthParam{
{Name: "prefix", Required: true},
{Name: "token", Required: true},
}
}

// OnRequest gets run before the request goes out on the wire.
func (a *ExternalOverrideAuth) OnRequest(req *http.Request, key string, params map[string]string) error {
prefix := viper.GetString("ni-override-auth-prefix")
token := viper.GetString("ni-override-auth-token")

if token == "" {
return fmt.Errorf("no token provided")
}
switch len(prefix) > 0 {
case true:
req.Header.Add("Authorization", fmt.Sprintf("%s %s", prefix, token))
default:
req.Header.Add("Authorization", token)
}
return nil
}
Loading