diff --git a/api/agent.go b/api/agent.go index 359206c54..860483671 100644 --- a/api/agent.go +++ b/api/agent.go @@ -172,6 +172,19 @@ type SampledValue struct { Labels map[string]string } +// AgentAuthorizeParams are the request parameters for authorizing a request. +type AgentAuthorizeParams struct { + Target string + ClientID string + ClientCertSerial string +} + +// AgentAuthorize is the response structure for Connect authorization. +type AgentAuthorize struct { + Authorized bool + Reason string +} + // Agent can be used to query the Agent endpoints type Agent struct { c *Client @@ -505,6 +518,75 @@ func (a *Agent) ForceLeave(node string) error { return nil } +// ConnectAuthorize is used to authorize an incoming connection +// to a natively integrated Connect service. +// +// TODO(mitchellh): we need to test this better once we have a way to +// configure CAs from the API package (when the CA work is done). +func (a *Agent) ConnectAuthorize(auth *AgentAuthorizeParams) (*AgentAuthorize, error) { + r := a.c.newRequest("POST", "/v1/agent/connect/authorize") + r.obj = auth + _, resp, err := requireOK(a.c.doRequest(r)) + if err != nil { + return nil, err + } + resp.Body.Close() + + var out AgentAuthorize + if err := decodeBody(resp, &out); err != nil { + return nil, err + } + return &out, nil +} + +// ConnectCARoots returns the list of roots. +// +// TODO(mitchellh): we need to test this better once we have a way to +// configure CAs from the API package (when the CA work is done). +func (a *Agent) ConnectCARoots(q *QueryOptions) (*CARootList, *QueryMeta, error) { + r := a.c.newRequest("GET", "/v1/agent/connect/ca/roots") + r.setQueryOptions(q) + rtt, resp, err := requireOK(a.c.doRequest(r)) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + qm := &QueryMeta{} + parseQueryMeta(resp, qm) + qm.RequestTime = rtt + + var out CARootList + if err := decodeBody(resp, &out); err != nil { + return nil, nil, err + } + return &out, qm, nil +} + +// ConnectCALeaf gets the leaf certificate for the given service ID. +// +// TODO(mitchellh): we need to test this better once we have a way to +// configure CAs from the API package (when the CA work is done). +func (a *Agent) ConnectCALeaf(serviceID string, q *QueryOptions) (*IssuedCert, *QueryMeta, error) { + r := a.c.newRequest("GET", "/v1/agent/connect/ca/leaf/"+serviceID) + r.setQueryOptions(q) + rtt, resp, err := requireOK(a.c.doRequest(r)) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + qm := &QueryMeta{} + parseQueryMeta(resp, qm) + qm.RequestTime = rtt + + var out IssuedCert + if err := decodeBody(resp, &out); err != nil { + return nil, nil, err + } + return &out, qm, nil +} + // EnableServiceMaintenance toggles service maintenance mode on // for the given service ID. func (a *Agent) EnableServiceMaintenance(serviceID, reason string) error { diff --git a/api/agent_test.go b/api/agent_test.go index d45a9a131..653512be9 100644 --- a/api/agent_test.go +++ b/api/agent_test.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/consul/testutil" "github.com/hashicorp/consul/testutil/retry" "github.com/hashicorp/serf/serf" + "github.com/stretchr/testify/require" ) func TestAPI_AgentSelf(t *testing.T) { @@ -981,3 +982,17 @@ func TestAPI_AgentUpdateToken(t *testing.T) { t.Fatalf("err: %v", err) } } + +func TestAPI_AgentConnectCARoots_empty(t *testing.T) { + t.Parallel() + + require := require.New(t) + c, s := makeClient(t) + defer s.Stop() + + agent := c.Agent() + list, meta, err := agent.ConnectCARoots(nil) + require.Nil(err) + require.Equal(uint64(0), meta.LastIndex) + require.Len(list.Roots, 0) +} diff --git a/api/connect.go b/api/connect.go new file mode 100644 index 000000000..0f75a45fa --- /dev/null +++ b/api/connect.go @@ -0,0 +1,65 @@ +package api + +import ( + "time" +) + +// CARootList is the structure for the results of listing roots. +type CARootList struct { + ActiveRootID string + Roots []*CARoot +} + +// CARoot is a single CA within Connect. +type CARoot struct { + ID string + Name string + RootCert string + Active bool + CreateIndex uint64 + ModifyIndex uint64 +} + +type IssuedCert struct { + SerialNumber string + CertPEM string + PrivateKeyPEM string + Service string + ServiceURI string + ValidAfter time.Time + ValidBefore time.Time + CreateIndex uint64 + ModifyIndex uint64 +} + +// Connect can be used to work with endpoints related to Connect, the +// feature for securely connecting services within Consul. +type Connect struct { + c *Client +} + +// Health returns a handle to the health endpoints +func (c *Client) Connect() *Connect { + return &Connect{c} +} + +// CARoots queries the list of available roots. +func (h *Connect) CARoots(q *QueryOptions) (*CARootList, *QueryMeta, error) { + r := h.c.newRequest("GET", "/v1/connect/ca/roots") + r.setQueryOptions(q) + rtt, resp, err := requireOK(h.c.doRequest(r)) + if err != nil { + return nil, nil, err + } + defer resp.Body.Close() + + qm := &QueryMeta{} + parseQueryMeta(resp, qm) + qm.RequestTime = rtt + + var out CARootList + if err := decodeBody(resp, &out); err != nil { + return nil, nil, err + } + return &out, qm, nil +} diff --git a/api/connect_test.go b/api/connect_test.go new file mode 100644 index 000000000..3ad7cb078 --- /dev/null +++ b/api/connect_test.go @@ -0,0 +1,26 @@ +package api + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +// NOTE(mitchellh): we don't have a way to test CA roots yet since there +// is no API public way to configure the root certs. This wll be resolved +// in the future and we can write tests then. This is tested in agent and +// agent/consul which do have internal access to manually create roots. + +func TestAPI_ConnectCARoots_empty(t *testing.T) { + t.Parallel() + + require := require.New(t) + c, s := makeClient(t) + defer s.Stop() + + connect := c.Connect() + list, meta, err := connect.CARoots(nil) + require.Nil(err) + require.Equal(uint64(0), meta.LastIndex) + require.Len(list.Roots, 0) +}