diff --git a/agent/agent_endpoint.go b/agent/agent_endpoint.go index 89bf16b622..cb4d06c59f 100644 --- a/agent/agent_endpoint.go +++ b/agent/agent_endpoint.go @@ -4,6 +4,7 @@ import ( "fmt" "log" "net/http" + "net/url" "strconv" "strings" @@ -885,6 +886,85 @@ func (s *HTTPServer) AgentConnectCALeafCert(resp http.ResponseWriter, req *http. // // POST /v1/agent/connect/authorize func (s *HTTPServer) AgentConnectAuthorize(resp http.ResponseWriter, req *http.Request) (interface{}, error) { - // NOTE(mitchellh): return 200 for now. To be implemented later. - return nil, nil + // Decode the request from the request body + var authReq structs.ConnectAuthorizeRequest + if err := decodeBody(req, &authReq, nil); err != nil { + resp.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(resp, "Request decode failed: %v", err) + return nil, nil + } + + // We need to have a target to check intentions + if authReq.Target == "" { + resp.WriteHeader(http.StatusBadRequest) + fmt.Fprintf(resp, "Target service must be specified") + return nil, nil + } + + // Parse the certificate URI from the client ID + uriRaw, err := url.Parse(authReq.ClientID) + if err != nil { + return &connectAuthorizeResp{ + Authorized: false, + Reason: fmt.Sprintf("Client ID must be a URI: %s", err), + }, nil + } + uri, err := connect.ParseCertURI(uriRaw) + if err != nil { + return &connectAuthorizeResp{ + Authorized: false, + Reason: fmt.Sprintf("Invalid client ID: %s", err), + }, nil + } + + uriService, ok := uri.(*connect.SpiffeIDService) + if !ok { + return &connectAuthorizeResp{ + Authorized: false, + Reason: fmt.Sprintf("Client ID must be a valid SPIFFE service URI"), + }, nil + } + + // Get the intentions for this target service + args := &structs.IntentionQueryRequest{ + Datacenter: s.agent.config.Datacenter, + Match: &structs.IntentionQueryMatch{ + Type: structs.IntentionMatchDestination, + Entries: []structs.IntentionMatchEntry{ + { + Namespace: structs.IntentionDefaultNamespace, + Name: authReq.Target, + }, + }, + }, + } + var reply structs.IndexedIntentionMatches + if err := s.agent.RPC("Intention.Match", args, &reply); err != nil { + return nil, err + } + if len(reply.Matches) != 1 { + return nil, fmt.Errorf("Internal error loading matches") + } + + // Test the authorization for each match + for _, ixn := range reply.Matches[0] { + if auth, ok := uriService.Authorize(ixn); ok { + return &connectAuthorizeResp{ + Authorized: auth, + Reason: fmt.Sprintf("Matched intention %s", ixn.ID), + }, nil + } + } + + // TODO(mitchellh): default behavior here for now is "deny" but we + // should consider how this is determined. + return &connectAuthorizeResp{ + Authorized: false, + Reason: "No matching intention, using default behavior", + }, nil +} + +type connectAuthorizeResp struct { + Authorized bool + Reason string } diff --git a/agent/agent_endpoint_test.go b/agent/agent_endpoint_test.go index 15267107ad..cae7a4cccb 100644 --- a/agent/agent_endpoint_test.go +++ b/agent/agent_endpoint_test.go @@ -2130,3 +2130,165 @@ func TestAgentConnectCALeafCert_good(t *testing.T) { // TODO(mitchellh): verify the private key matches the cert } + +func TestAgentConnectAuthorize_badBody(t *testing.T) { + t.Parallel() + + assert := assert.New(t) + a := NewTestAgent(t.Name(), "") + defer a.Shutdown() + + args := []string{} + req, _ := http.NewRequest("POST", "/v1/agent/connect/authorize", jsonReader(args)) + resp := httptest.NewRecorder() + _, err := a.srv.AgentConnectAuthorize(resp, req) + assert.Nil(err) + assert.Equal(400, resp.Code) + assert.Contains(resp.Body.String(), "decode") +} + +func TestAgentConnectAuthorize_noTarget(t *testing.T) { + t.Parallel() + + assert := assert.New(t) + a := NewTestAgent(t.Name(), "") + defer a.Shutdown() + + args := &structs.ConnectAuthorizeRequest{} + req, _ := http.NewRequest("POST", "/v1/agent/connect/authorize", jsonReader(args)) + resp := httptest.NewRecorder() + _, err := a.srv.AgentConnectAuthorize(resp, req) + assert.Nil(err) + assert.Equal(400, resp.Code) + assert.Contains(resp.Body.String(), "Target service") +} + +// Client ID is not in the valid URI format +func TestAgentConnectAuthorize_idInvalidFormat(t *testing.T) { + t.Parallel() + + assert := assert.New(t) + a := NewTestAgent(t.Name(), "") + defer a.Shutdown() + + args := &structs.ConnectAuthorizeRequest{ + Target: "web", + ClientID: "tubes", + } + req, _ := http.NewRequest("POST", "/v1/agent/connect/authorize", jsonReader(args)) + resp := httptest.NewRecorder() + respRaw, err := a.srv.AgentConnectAuthorize(resp, req) + assert.Nil(err) + assert.Equal(200, resp.Code) + + obj := respRaw.(*connectAuthorizeResp) + assert.False(obj.Authorized) + assert.Contains(obj.Reason, "Invalid client") +} + +// Client ID is a valid URI but its not a service URI +func TestAgentConnectAuthorize_idNotService(t *testing.T) { + t.Parallel() + + assert := assert.New(t) + a := NewTestAgent(t.Name(), "") + defer a.Shutdown() + + args := &structs.ConnectAuthorizeRequest{ + Target: "web", + ClientID: "spiffe://1234.consul", + } + req, _ := http.NewRequest("POST", "/v1/agent/connect/authorize", jsonReader(args)) + resp := httptest.NewRecorder() + respRaw, err := a.srv.AgentConnectAuthorize(resp, req) + assert.Nil(err) + assert.Equal(200, resp.Code) + + obj := respRaw.(*connectAuthorizeResp) + assert.False(obj.Authorized) + assert.Contains(obj.Reason, "must be a valid") +} + +// Test when there is an intention allowing the connection +func TestAgentConnectAuthorize_allow(t *testing.T) { + t.Parallel() + + assert := assert.New(t) + a := NewTestAgent(t.Name(), "") + defer a.Shutdown() + + target := "db" + + // Create some intentions + { + req := structs.IntentionRequest{ + Datacenter: "dc1", + Op: structs.IntentionOpCreate, + Intention: structs.TestIntention(t), + } + req.Intention.SourceNS = structs.IntentionDefaultNamespace + req.Intention.SourceName = "web" + req.Intention.DestinationNS = structs.IntentionDefaultNamespace + req.Intention.DestinationName = target + req.Intention.Action = structs.IntentionActionAllow + + var reply string + assert.Nil(a.RPC("Intention.Apply", &req, &reply)) + } + + args := &structs.ConnectAuthorizeRequest{ + Target: target, + ClientID: connect.TestSpiffeIDService(t, "web").URI().String(), + } + req, _ := http.NewRequest("POST", "/v1/agent/connect/authorize", jsonReader(args)) + resp := httptest.NewRecorder() + respRaw, err := a.srv.AgentConnectAuthorize(resp, req) + assert.Nil(err) + assert.Equal(200, resp.Code) + + obj := respRaw.(*connectAuthorizeResp) + assert.True(obj.Authorized) + assert.Contains(obj.Reason, "Matched") +} + +// Test when there is an intention denying the connection +func TestAgentConnectAuthorize_deny(t *testing.T) { + t.Parallel() + + assert := assert.New(t) + a := NewTestAgent(t.Name(), "") + defer a.Shutdown() + + target := "db" + + // Create some intentions + { + req := structs.IntentionRequest{ + Datacenter: "dc1", + Op: structs.IntentionOpCreate, + Intention: structs.TestIntention(t), + } + req.Intention.SourceNS = structs.IntentionDefaultNamespace + req.Intention.SourceName = "web" + req.Intention.DestinationNS = structs.IntentionDefaultNamespace + req.Intention.DestinationName = target + req.Intention.Action = structs.IntentionActionDeny + + var reply string + assert.Nil(a.RPC("Intention.Apply", &req, &reply)) + } + + args := &structs.ConnectAuthorizeRequest{ + Target: target, + ClientID: connect.TestSpiffeIDService(t, "web").URI().String(), + } + req, _ := http.NewRequest("POST", "/v1/agent/connect/authorize", jsonReader(args)) + resp := httptest.NewRecorder() + respRaw, err := a.srv.AgentConnectAuthorize(resp, req) + assert.Nil(err) + assert.Equal(200, resp.Code) + + obj := respRaw.(*connectAuthorizeResp) + assert.False(obj.Authorized) + assert.Contains(obj.Reason, "Matched") +} diff --git a/agent/structs/connect.go b/agent/structs/connect.go new file mode 100644 index 0000000000..1a2e03da8b --- /dev/null +++ b/agent/structs/connect.go @@ -0,0 +1,17 @@ +package structs + +// ConnectAuthorizeRequest is the structure of a request to authorize +// a connection. +type ConnectAuthorizeRequest struct { + // Target is the name of the service that is being requested. + Target string + + // ClientID is a unique identifier for the requesting client. This + // is currently the URI SAN from the TLS client certificate. + // + // ClientCertSerial is a colon-hex-encoded of the serial number for + // the requesting client cert. This is used to check against revocation + // lists. + ClientID string + ClientCertSerial string +}