From 99c2e1132867ed6c3bf492069cefaed18fde6570 Mon Sep 17 00:00:00 2001 From: acpana <8968914+acpana@users.noreply.github.com> Date: Wed, 22 Jun 2022 16:25:09 -0700 Subject: [PATCH] oss: peering, http: get peer service intentions (#2098) Signed-off-by: acpana <8968914+acpana@users.noreply.github.com> --- agent/intentions_endpoint.go | 97 ++++++++++++++++++++----------- agent/intentions_endpoint_test.go | 88 ++++++++++++++++++++++++++-- 2 files changed, 146 insertions(+), 39 deletions(-) diff --git a/agent/intentions_endpoint.go b/agent/intentions_endpoint.go index d21833a8f5..f43dc3ecf4 100644 --- a/agent/intentions_endpoint.go +++ b/agent/intentions_endpoint.go @@ -145,15 +145,15 @@ func (s *HTTPHandlers) IntentionMatch(resp http.ResponseWriter, req *http.Reques // order of the returned responses. args.Match.Entries = make([]structs.IntentionMatchEntry, len(names)) for i, n := range names { - ap, ns, name, err := parseIntentionStringComponent(n, &entMeta) + parsed, err := parseIntentionStringComponent(n, &entMeta, false) if err != nil { return nil, fmt.Errorf("name %q is invalid: %s", n, err) } args.Match.Entries[i] = structs.IntentionMatchEntry{ - Partition: ap, - Namespace: ns, - Name: name, + Partition: parsed.ap, + Namespace: parsed.ns, + Name: parsed.name, } } @@ -235,23 +235,23 @@ func (s *HTTPHandlers) IntentionCheck(resp http.ResponseWriter, req *http.Reques // We parse them the same way as matches to extract partition/namespace/name args.Check.SourceName = source[0] if args.Check.SourceType == structs.IntentionSourceConsul { - ap, ns, name, err := parseIntentionStringComponent(source[0], &entMeta) + parsed, err := parseIntentionStringComponent(source[0], &entMeta, false) if err != nil { return nil, fmt.Errorf("source %q is invalid: %s", source[0], err) } - args.Check.SourcePartition = ap - args.Check.SourceNS = ns - args.Check.SourceName = name + args.Check.SourcePartition = parsed.ap + args.Check.SourceNS = parsed.ns + args.Check.SourceName = parsed.name } // The destination is always in the Consul format - ap, ns, name, err := parseIntentionStringComponent(destination[0], &entMeta) + parsed, err := parseIntentionStringComponent(destination[0], &entMeta, false) if err != nil { return nil, fmt.Errorf("destination %q is invalid: %s", destination[0], err) } - args.Check.DestinationPartition = ap - args.Check.DestinationNS = ns - args.Check.DestinationName = name + args.Check.DestinationPartition = parsed.ap + args.Check.DestinationNS = parsed.ns + args.Check.DestinationName = parsed.name var reply structs.IntentionQueryCheckResponse if err := s.agent.RPC("Intention.Check", args, &reply); err != nil { @@ -302,23 +302,25 @@ func (s *HTTPHandlers) IntentionGetExact(resp http.ResponseWriter, req *http.Req } { - ap, ns, name, err := parseIntentionStringComponent(source[0], &entMeta) + parsed, err := parseIntentionStringComponent(source[0], &entMeta, true) if err != nil { return nil, fmt.Errorf("source %q is invalid: %s", source[0], err) } - args.Exact.SourcePartition = ap - args.Exact.SourceNS = ns - args.Exact.SourceName = name + + args.Exact.SourcePeer = parsed.peer + args.Exact.SourcePartition = parsed.ap + args.Exact.SourceNS = parsed.ns + args.Exact.SourceName = parsed.name } { - ap, ns, name, err := parseIntentionStringComponent(destination[0], &entMeta) + parsed, err := parseIntentionStringComponent(destination[0], &entMeta, false) if err != nil { return nil, fmt.Errorf("destination %q is invalid: %s", destination[0], err) } - args.Exact.DestinationPartition = ap - args.Exact.DestinationNS = ns - args.Exact.DestinationName = name + args.Exact.DestinationPartition = parsed.ap + args.Exact.DestinationNS = parsed.ns + args.Exact.DestinationName = parsed.name } var reply structs.IndexedIntentions @@ -444,42 +446,67 @@ func parseIntentionQueryExact(req *http.Request, entMeta *acl.EnterpriseMeta) (* var exact structs.IntentionQueryExact { - ap, ns, name, err := parseIntentionStringComponent(source[0], entMeta) + parsed, err := parseIntentionStringComponent(source[0], entMeta, false) if err != nil { return nil, fmt.Errorf("source %q is invalid: %s", source[0], err) } - exact.SourcePartition = ap - exact.SourceNS = ns - exact.SourceName = name + exact.SourcePartition = parsed.ap + exact.SourceNS = parsed.ns + exact.SourceName = parsed.name } { - ap, ns, name, err := parseIntentionStringComponent(destination[0], entMeta) + parsed, err := parseIntentionStringComponent(destination[0], entMeta, false) if err != nil { return nil, fmt.Errorf("destination %q is invalid: %s", destination[0], err) } - exact.DestinationPartition = ap - exact.DestinationNS = ns - exact.DestinationName = name + exact.DestinationPartition = parsed.ap + exact.DestinationNS = parsed.ns + exact.DestinationName = parsed.name } return &exact, nil } -func parseIntentionStringComponent(input string, entMeta *acl.EnterpriseMeta) (string, string, string, error) { +type parsedIntentionInput struct { + peer, ap, ns, name string +} + +func parseIntentionStringComponent(input string, entMeta *acl.EnterpriseMeta, allowPeerKeyword bool) (*parsedIntentionInput, error) { + if strings.HasPrefix(input, "peer:") && !allowPeerKeyword { + return nil, fmt.Errorf("cannot specify a peer here") + } + ss := strings.Split(input, "/") switch len(ss) { case 1: // Name only + // need to specify at least the service name too + if strings.HasPrefix(ss[0], "peer:") { + return nil, fmt.Errorf("need to specify the service name as well") + } + ns := entMeta.NamespaceOrEmpty() ap := entMeta.PartitionOrEmpty() - return ap, ns, ss[0], nil - case 2: // namespace/name + return &parsedIntentionInput{ap: ap, ns: ns, name: ss[0]}, nil + case 2: // peer:peer/name OR namespace/name + if strings.HasPrefix(ss[0], "peer:") { + peerName := strings.TrimPrefix(ss[0], "peer:") + ns := entMeta.NamespaceOrEmpty() + + return &parsedIntentionInput{peer: peerName, ns: ns, name: ss[1]}, nil + } + ap := entMeta.PartitionOrEmpty() - return ap, ss[0], ss[1], nil - case 3: // partition/namespace/name - return ss[0], ss[1], ss[2], nil + return &parsedIntentionInput{ap: ap, ns: ss[0], name: ss[1]}, nil + case 3: // peer:peer/namespace/name OR partition/namespace/name + if strings.HasPrefix(ss[0], "peer:") { + peerName := strings.TrimPrefix(ss[0], "peer:") + return &parsedIntentionInput{peer: peerName, ns: ss[1], name: ss[2]}, nil + } else { + return &parsedIntentionInput{ap: ss[0], ns: ss[1], name: ss[2]}, nil + } default: - return "", "", "", fmt.Errorf("input can contain at most two '/'") + return nil, fmt.Errorf("input can contain at most two '/'") } } diff --git a/agent/intentions_endpoint_test.go b/agent/intentions_endpoint_test.go index ef54ccce3e..148e48f8fb 100644 --- a/agent/intentions_endpoint_test.go +++ b/agent/intentions_endpoint_test.go @@ -349,6 +349,57 @@ func TestIntentionCheck(t *testing.T) { }) } +func TestIntentionGetExact_PeerIntentions(t *testing.T) { + if testing.Short() { + t.Skip("too slow for testing.Short") + } + + t.Parallel() + + a := NewTestAgent(t, "") + testrpc.WaitForTestAgent(t, a.RPC, "dc1") + + testutil.RunStep(t, "create a peer intentions", func(t *testing.T) { + configEntryIntention := structs.ServiceIntentionsConfigEntry{ + Kind: structs.ServiceIntentions, + Name: "bar", + Sources: []*structs.SourceIntention{ + { + Name: "foo", + Peer: "peer1", + Action: structs.IntentionActionAllow, + }, + }, + } + + req, err := http.NewRequest("PUT", "/v1/config", jsonReader(configEntryIntention)) + require.NoError(t, err) + resp := httptest.NewRecorder() + + obj, err := a.srv.ConfigApply(resp, req) + require.NoError(t, err) + + applied, ok := obj.(bool) + require.True(t, ok) + require.True(t, applied) + }) + + t.Run("get peer intention", func(t *testing.T) { + req, err := http.NewRequest("GET", "/v1/connect/intentions/exact?source=peer:peer1/foo&destination=bar", nil) + require.NoError(t, err) + + resp := httptest.NewRecorder() + obj, err := a.srv.IntentionExact(resp, req) + require.NoError(t, err) + require.NotNil(t, obj) + + value, ok := obj.(*structs.Intention) + require.True(t, ok) + require.Equal(t, "peer1", value.SourcePeer) + require.Equal(t, "foo", value.SourceName) + require.Equal(t, "bar", value.DestinationName) + }) +} func TestIntentionGetExact(t *testing.T) { if testing.Short() { t.Skip("too slow for testing.Short") @@ -828,6 +879,8 @@ func TestParseIntentionStringComponent(t *testing.T) { cases := []struct { TestName string Input string + AllowsPeers bool + ExpectedPeer string ExpectedAP string ExpectedNS string ExpectedName string @@ -866,20 +919,47 @@ func TestParseIntentionStringComponent(t *testing.T) { Input: "uhoh/blah/foo/bar", Err: true, }, + { + TestName: "peered without namespace", + Input: "peer:peer1/service_name", + AllowsPeers: true, + ExpectedPeer: "peer1", + ExpectedAP: "", + ExpectedNS: "", + ExpectedName: "service_name", + }, + { + TestName: "need to specify at least a service", + Input: "peer:peer1", + Err: true, + }, + { + TestName: "peered not allowed error", + Input: "peer:peer1/service_name", + AllowsPeers: false, + Err: true, + }, } for _, tc := range cases { t.Run(tc.TestName, func(t *testing.T) { var entMeta acl.EnterpriseMeta - ap, ns, name, err := parseIntentionStringComponent(tc.Input, &entMeta) + parsed, err := parseIntentionStringComponent(tc.Input, &entMeta, tc.AllowsPeers) if tc.Err { require.Error(t, err) } else { require.NoError(t, err) - assert.Equal(t, tc.ExpectedAP, ap) - assert.Equal(t, tc.ExpectedNS, ns) - assert.Equal(t, tc.ExpectedName, name) + if tc.AllowsPeers { + assert.Equal(t, tc.ExpectedPeer, parsed.peer) + assert.Equal(t, "", parsed.ap) + } else { + assert.Equal(t, tc.ExpectedAP, parsed.ap) + assert.Equal(t, "", parsed.peer) + } + + assert.Equal(t, tc.ExpectedNS, parsed.ns) + assert.Equal(t, tc.ExpectedName, parsed.name) } }) }