diff --git a/agent/http_oss.go b/agent/http_oss.go index 9b9857e40..92c7bf4c0 100644 --- a/agent/http_oss.go +++ b/agent/http_oss.go @@ -48,6 +48,7 @@ func init() { registerEndpoint("/v1/connect/ca/roots", []string{"GET"}, (*HTTPServer).ConnectCARoots) registerEndpoint("/v1/connect/intentions", []string{"GET", "POST"}, (*HTTPServer).IntentionEndpoint) registerEndpoint("/v1/connect/intentions/match", []string{"GET"}, (*HTTPServer).IntentionMatch) + registerEndpoint("/v1/connect/intentions/test", []string{"GET"}, (*HTTPServer).IntentionTest) registerEndpoint("/v1/connect/intentions/", []string{"GET", "PUT", "DELETE"}, (*HTTPServer).IntentionSpecific) registerEndpoint("/v1/coordinate/datacenters", []string{"GET"}, (*HTTPServer).CoordinateDatacenters) registerEndpoint("/v1/coordinate/nodes", []string{"GET"}, (*HTTPServer).CoordinateNodes) diff --git a/agent/intentions_endpoint.go b/agent/intentions_endpoint.go index 5a2e0e809..cb846bc19 100644 --- a/agent/intentions_endpoint.go +++ b/agent/intentions_endpoint.go @@ -122,6 +122,59 @@ func (s *HTTPServer) IntentionMatch(resp http.ResponseWriter, req *http.Request) return response, nil } +// GET /v1/connect/intentions/test +func (s *HTTPServer) IntentionTest(resp http.ResponseWriter, req *http.Request) (interface{}, error) { + // Prepare args + args := &structs.IntentionQueryRequest{Test: &structs.IntentionQueryTest{}} + if done := s.parse(resp, req, &args.Datacenter, &args.QueryOptions); done { + return nil, nil + } + + q := req.URL.Query() + + // Set the source type if set + args.Test.SourceType = structs.IntentionSourceConsul + if sourceType, ok := q["source-type"]; ok && len(sourceType) > 0 { + args.Test.SourceType = structs.IntentionSourceType(sourceType[0]) + } + + // Extract the source/destination + source, ok := q["source"] + if !ok || len(source) != 1 { + return nil, fmt.Errorf("required query parameter 'source' not set") + } + destination, ok := q["destination"] + if !ok || len(destination) != 1 { + return nil, fmt.Errorf("required query parameter 'destination' not set") + } + + // We parse them the same way as matches to extract namespace/name + args.Test.SourceName = source[0] + if args.Test.SourceType == structs.IntentionSourceConsul { + entry, err := parseIntentionMatchEntry(source[0]) + if err != nil { + return nil, fmt.Errorf("source %q is invalid: %s", source[0], err) + } + args.Test.SourceNS = entry.Namespace + args.Test.SourceName = entry.Name + } + + // The destination is always in the Consul format + entry, err := parseIntentionMatchEntry(destination[0]) + if err != nil { + return nil, fmt.Errorf("destination %q is invalid: %s", destination[0], err) + } + args.Test.DestinationNS = entry.Namespace + args.Test.DestinationName = entry.Name + + var reply structs.IntentionQueryTestResponse + if err := s.agent.RPC("Intention.Test", args, &reply); err != nil { + return nil, err + } + + return &reply, nil +} + // IntentionSpecific handles the endpoint for /v1/connection/intentions/:id func (s *HTTPServer) IntentionSpecific(resp http.ResponseWriter, req *http.Request) (interface{}, error) { id := strings.TrimPrefix(req.URL.Path, "/v1/connect/intentions/") diff --git a/agent/intentions_endpoint_test.go b/agent/intentions_endpoint_test.go index d4d68f26c..e669bcf5f 100644 --- a/agent/intentions_endpoint_test.go +++ b/agent/intentions_endpoint_test.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/consul/agent/structs" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestIntentionsList_empty(t *testing.T) { @@ -180,6 +181,96 @@ func TestIntentionsMatch_noName(t *testing.T) { assert.Nil(obj) } +func TestIntentionsTest_basic(t *testing.T) { + t.Parallel() + + require := require.New(t) + a := NewTestAgent(t.Name(), "") + defer a.Shutdown() + + // Create some intentions + { + insert := [][]string{ + {"foo", "*", "foo", "*"}, + {"foo", "*", "foo", "bar"}, + {"bar", "*", "foo", "bar"}, + } + + for _, v := range insert { + ixn := structs.IntentionRequest{ + Datacenter: "dc1", + Op: structs.IntentionOpCreate, + Intention: structs.TestIntention(t), + } + ixn.Intention.SourceNS = v[0] + ixn.Intention.SourceName = v[1] + ixn.Intention.DestinationNS = v[2] + ixn.Intention.DestinationName = v[3] + ixn.Intention.Action = structs.IntentionActionDeny + + // Create + var reply string + require.Nil(a.RPC("Intention.Apply", &ixn, &reply)) + } + } + + // Request matching intention + { + req, _ := http.NewRequest("GET", + "/v1/connect/intentions/test?source=foo/bar&destination=foo/baz", nil) + resp := httptest.NewRecorder() + obj, err := a.srv.IntentionTest(resp, req) + require.Nil(err) + value := obj.(*structs.IntentionQueryTestResponse) + require.False(value.Allowed) + } + + // Request non-matching intention + { + req, _ := http.NewRequest("GET", + "/v1/connect/intentions/test?source=foo/bar&destination=bar/qux", nil) + resp := httptest.NewRecorder() + obj, err := a.srv.IntentionTest(resp, req) + require.Nil(err) + value := obj.(*structs.IntentionQueryTestResponse) + require.True(value.Allowed) + } +} + +func TestIntentionsTest_noSource(t *testing.T) { + t.Parallel() + + require := require.New(t) + a := NewTestAgent(t.Name(), "") + defer a.Shutdown() + + // Request + req, _ := http.NewRequest("GET", + "/v1/connect/intentions/test?destination=B", nil) + resp := httptest.NewRecorder() + obj, err := a.srv.IntentionTest(resp, req) + require.NotNil(err) + require.Contains(err.Error(), "'source' not set") + require.Nil(obj) +} + +func TestIntentionsTest_noDestination(t *testing.T) { + t.Parallel() + + require := require.New(t) + a := NewTestAgent(t.Name(), "") + defer a.Shutdown() + + // Request + req, _ := http.NewRequest("GET", + "/v1/connect/intentions/test?source=B", nil) + resp := httptest.NewRecorder() + obj, err := a.srv.IntentionTest(resp, req) + require.NotNil(err) + require.Contains(err.Error(), "'destination' not set") + require.Nil(obj) +} + func TestIntentionsCreate_good(t *testing.T) { t.Parallel()