Add support for /ssh/bastion method.

This commit is contained in:
Mariano Cano
2019-11-14 18:24:58 -08:00
committed by max furman
parent a6edcd0a3d
commit 8bf3bf701e
8 changed files with 197 additions and 2 deletions

View File

@@ -261,6 +261,7 @@ func (h *caHandler) Route(r Router) {
r.MethodFunc("POST", "/ssh/config/{type}", h.SSHConfig)
r.MethodFunc("POST", "/ssh/check-host", h.SSHCheckHost)
r.MethodFunc("GET", "/ssh/get-hosts", h.SSHGetHosts)
r.MethodFunc("POST", "/ssh/bastion", h.SSHBastion)
// For compatibility with old code:
r.MethodFunc("POST", "/re-sign", h.Renew)

View File

@@ -557,6 +557,7 @@ type mockAuthority struct {
getSSHFederation func() (*authority.SSHKeys, error)
getSSHConfig func(typ string, data map[string]string) ([]templates.Output, error)
checkSSHHost func(principal string) (bool, error)
getSSHBastion func(user string, hostname string) (*authority.Bastion, error)
}
// TODO: remove once Authorize is deprecated.
@@ -711,6 +712,13 @@ func (m *mockAuthority) CheckSSHHost(principal string) (bool, error) {
return m.ret1.(bool), m.err
}
func (m *mockAuthority) GetSSHBastion(user string, hostname string) (*authority.Bastion, error) {
if m.getSSHBastion != nil {
return m.getSSHBastion(user, hostname)
}
return m.ret1.(*authority.Bastion), m.err
}
func Test_caHandler_Route(t *testing.T) {
type fields struct {
Authority Authority

View File

@@ -24,6 +24,7 @@ type SSHAuthority interface {
GetSSHConfig(typ string, data map[string]string) ([]templates.Output, error)
CheckSSHHost(principal string) (bool, error)
GetSSHHosts() ([]string, error)
GetSSHBastion(user string, hostname string) (*authority.Bastion, error)
}
// SSHSignRequest is the request body of an SSH certificate request.
@@ -207,6 +208,28 @@ type SSHCheckPrincipalResponse struct {
Exists bool `json:"exists"`
}
// SSHBastionRequest is the request body used to get the bastion for a given
// host.
type SSHBastionRequest struct {
User string `json:"user"`
Hostname string `json:"hostname"`
}
// Validate checks the values of the SSHBastionRequest.
func (r *SSHBastionRequest) Validate() error {
if r.Hostname == "" {
return errors.New("missing or empty hostname")
}
return nil
}
// SSHBastionResponse is the response body used to return the bastion for a
// given host.
type SSHBastionResponse struct {
Hostname string `json:"hostname"`
Bastion *authority.Bastion `json:"bastion,omitempty"`
}
// SSHSign is an HTTP handler that reads an SignSSHRequest with a one-time-token
// (ott) from the body and creates a new SSH certificate with the information in
// the request.
@@ -392,3 +415,27 @@ func (h *caHandler) SSHGetHosts(w http.ResponseWriter, r *http.Request) {
Hosts: hosts,
})
}
// SSHBastion provides returns the bastion configured if any.
func (h *caHandler) SSHBastion(w http.ResponseWriter, r *http.Request) {
var body SSHBastionRequest
if err := ReadJSON(r.Body, &body); err != nil {
WriteError(w, BadRequest(errors.Wrap(err, "error reading request body")))
return
}
if err := body.Validate(); err != nil {
WriteError(w, BadRequest(err))
return
}
bastion, err := h.Authority.GetSSHBastion(body.User, body.Hostname)
if err != nil {
WriteError(w, InternalServerError(err))
return
}
JSON(w, &SSHBastionResponse{
Hostname: body.Hostname,
Bastion: bastion,
})
}

View File

@@ -537,6 +537,61 @@ func Test_caHandler_SSHCheckHost(t *testing.T) {
}
}
func Test_caHandler_SSHBastion(t *testing.T) {
bastion := &authority.Bastion{
Hostname: "bastion.local",
}
bastionPort := &authority.Bastion{
Hostname: "bastion.local",
Port: "2222",
}
tests := []struct {
name string
bastion *authority.Bastion
bastionErr error
req []byte
body []byte
statusCode int
}{
{"ok", bastion, nil, []byte(`{"hostname":"host.local"}`), []byte(`{"hostname":"host.local","bastion":{"hostname":"bastion.local"}}`), http.StatusOK},
{"ok", bastionPort, nil, []byte(`{"hostname":"host.local","user":"user"}`), []byte(`{"hostname":"host.local","bastion":{"hostname":"bastion.local","port":"2222"}}`), http.StatusOK},
{"empty", nil, nil, []byte(`{"hostname":"host.local"}`), []byte(`{"hostname":"host.local"}`), http.StatusOK},
{"bad json", bastion, nil, []byte(`bad json`), nil, http.StatusBadRequest},
{"bad request", bastion, nil, []byte(`{"hostname": ""}`), nil, http.StatusBadRequest},
{"error", nil, fmt.Errorf("an error"), []byte(`{"hostname":"host.local"}`), nil, http.StatusInternalServerError},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
h := New(&mockAuthority{
getSSHBastion: func(user, hostname string) (*authority.Bastion, error) {
return tt.bastion, tt.bastionErr
},
}).(*caHandler)
req := httptest.NewRequest("POST", "http://example.com/ssh/bastion", bytes.NewReader(tt.req))
w := httptest.NewRecorder()
h.SSHBastion(logging.NewResponseLogger(w), req)
res := w.Result()
if res.StatusCode != tt.statusCode {
t.Errorf("caHandler.SSHBastion StatusCode = %d, wants %d", res.StatusCode, tt.statusCode)
}
body, err := ioutil.ReadAll(res.Body)
res.Body.Close()
if err != nil {
t.Errorf("caHandler.SSHBastion unexpected error = %v", err)
}
if tt.statusCode < http.StatusBadRequest {
if !bytes.Equal(bytes.TrimSpace(body), tt.body) {
t.Errorf("caHandler.SSHBastion Body = %s, wants %s", body, tt.body)
}
}
})
}
}
func TestSSHPublicKey_MarshalJSON(t *testing.T) {
key, err := ssh.NewPublicKey(sshUserKey.Public())
assert.FatalError(t, err)