From 5ac649af7f0a3d01d20c9a623e9137e6323290e7 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sun, 4 Mar 2018 18:32:28 -0800 Subject: [PATCH] agent/consul: Intention.Match ACLs --- agent/consul/intention_endpoint.go | 23 ++- agent/consul/intention_endpoint_test.go | 233 ++++++++++++++++++++++++ 2 files changed, 250 insertions(+), 6 deletions(-) diff --git a/agent/consul/intention_endpoint.go b/agent/consul/intention_endpoint.go index 568446d733..2458a8ee97 100644 --- a/agent/consul/intention_endpoint.go +++ b/agent/consul/intention_endpoint.go @@ -206,8 +206,7 @@ func (s *Intention) List( reply.Intentions = make(structs.Intentions, 0) } - // filterACL - return nil + return s.srv.filterACL(args.Token, reply) }, ) } @@ -221,7 +220,22 @@ func (s *Intention) Match( return err } - // TODO(mitchellh): validate + // Get the ACL token for the request for the checks below. + rule, err := s.srv.resolveToken(args.Token) + if err != nil { + return err + } + + if rule != nil { + // We go through each entry and test the destination to check if it + // matches. + for _, entry := range args.Match.Entries { + if prefix := entry.Name; prefix != "" && !rule.IntentionRead(prefix) { + s.srv.logger.Printf("[WARN] consul.intention: Operation on intention prefix '%s' denied due to ACLs", prefix) + return acl.ErrPermissionDenied + } + } + } return s.srv.blockingQuery( &args.QueryOptions, @@ -234,9 +248,6 @@ func (s *Intention) Match( reply.Index = index reply.Matches = matches - - // TODO(mitchellh): acl filtering - return nil }, ) diff --git a/agent/consul/intention_endpoint_test.go b/agent/consul/intention_endpoint_test.go index 67c2a07d0e..5a0a8a7237 100644 --- a/agent/consul/intention_endpoint_test.go +++ b/agent/consul/intention_endpoint_test.go @@ -768,6 +768,110 @@ func TestIntentionList(t *testing.T) { } } +// Test listing with ACLs +func TestIntentionList_acl(t *testing.T) { + t.Parallel() + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" + c.ACLMasterToken = "root" + c.ACLDefaultPolicy = "deny" + }) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + codec := rpcClient(t, s1) + defer codec.Close() + + testrpc.WaitForLeader(t, s1.RPC, "dc1") + + // Create an ACL with service write permissions. This will grant + // intentions read. + var token string + { + var rules = ` +service "foo" { + policy = "write" +}` + + req := structs.ACLRequest{ + Datacenter: "dc1", + Op: structs.ACLSet, + ACL: structs.ACL{ + Name: "User token", + Type: structs.ACLTypeClient, + Rules: rules, + }, + WriteRequest: structs.WriteRequest{Token: "root"}, + } + if err := msgpackrpc.CallWithCodec(codec, "ACL.Apply", &req, &token); err != nil { + t.Fatalf("err: %v", err) + } + } + + // Create a few records + for _, name := range []string{"foobar", "bar", "baz"} { + ixn := structs.IntentionRequest{ + Datacenter: "dc1", + Op: structs.IntentionOpCreate, + Intention: structs.TestIntention(t), + } + ixn.Intention.DestinationName = name + ixn.WriteRequest.Token = "root" + + // Create + var reply string + if err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply); err != nil { + t.Fatalf("err: %v", err) + } + } + + // Test with no token + { + req := &structs.DCSpecificRequest{ + Datacenter: "dc1", + } + var resp structs.IndexedIntentions + if err := msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp); err != nil { + t.Fatalf("err: %v", err) + } + + if len(resp.Intentions) != 0 { + t.Fatalf("bad: %v", resp) + } + } + + // Test with management token + { + req := &structs.DCSpecificRequest{ + Datacenter: "dc1", + QueryOptions: structs.QueryOptions{Token: "root"}, + } + var resp structs.IndexedIntentions + if err := msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp); err != nil { + t.Fatalf("err: %v", err) + } + + if len(resp.Intentions) != 3 { + t.Fatalf("bad: %v", resp) + } + } + + // Test with user token + { + req := &structs.DCSpecificRequest{ + Datacenter: "dc1", + QueryOptions: structs.QueryOptions{Token: token}, + } + var resp structs.IndexedIntentions + if err := msgpackrpc.CallWithCodec(codec, "Intention.List", req, &resp); err != nil { + t.Fatalf("err: %v", err) + } + + if len(resp.Intentions) != 1 { + t.Fatalf("bad: %v", resp) + } + } +} + // Test basic matching. We don't need to exhaustively test inputs since this // is tested in the agent/consul/state package. func TestIntentionMatch_good(t *testing.T) { @@ -836,3 +940,132 @@ func TestIntentionMatch_good(t *testing.T) { } assert.Equal(expected, actual) } + +// Test matching with ACLs +func TestIntentionMatch_acl(t *testing.T) { + t.Parallel() + dir1, s1 := testServerWithConfig(t, func(c *Config) { + c.ACLDatacenter = "dc1" + c.ACLMasterToken = "root" + c.ACLDefaultPolicy = "deny" + }) + defer os.RemoveAll(dir1) + defer s1.Shutdown() + codec := rpcClient(t, s1) + defer codec.Close() + + testrpc.WaitForLeader(t, s1.RPC, "dc1") + + // Create an ACL with service write permissions. This will grant + // intentions read. + var token string + { + var rules = ` +service "bar" { + policy = "write" +}` + + req := structs.ACLRequest{ + Datacenter: "dc1", + Op: structs.ACLSet, + ACL: structs.ACL{ + Name: "User token", + Type: structs.ACLTypeClient, + Rules: rules, + }, + WriteRequest: structs.WriteRequest{Token: "root"}, + } + if err := msgpackrpc.CallWithCodec(codec, "ACL.Apply", &req, &token); err != nil { + t.Fatalf("err: %v", err) + } + } + + // Create some records + { + insert := [][]string{ + {"foo", "*"}, + {"foo", "bar"}, + {"foo", "baz"}, // shouldn't match + {"bar", "bar"}, // shouldn't match + {"bar", "*"}, // shouldn't match + {"*", "*"}, + } + + for _, v := range insert { + ixn := structs.IntentionRequest{ + Datacenter: "dc1", + Op: structs.IntentionOpCreate, + Intention: structs.TestIntention(t), + } + ixn.Intention.DestinationNS = v[0] + ixn.Intention.DestinationName = v[1] + ixn.WriteRequest.Token = "root" + + // Create + var reply string + if err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply); err != nil { + t.Fatalf("err: %v", err) + } + } + } + + // Test with no token + { + req := &structs.IntentionQueryRequest{ + Datacenter: "dc1", + Match: &structs.IntentionQueryMatch{ + Type: structs.IntentionMatchDestination, + Entries: []structs.IntentionMatchEntry{ + { + Namespace: "foo", + Name: "bar", + }, + }, + }, + } + var resp structs.IndexedIntentionMatches + err := msgpackrpc.CallWithCodec(codec, "Intention.Match", req, &resp) + if !acl.IsErrPermissionDenied(err) { + t.Fatalf("err: %v", err) + } + + if len(resp.Matches) != 0 { + t.Fatalf("bad: %#v", resp.Matches) + } + } + + // Test with proper token + { + req := &structs.IntentionQueryRequest{ + Datacenter: "dc1", + Match: &structs.IntentionQueryMatch{ + Type: structs.IntentionMatchDestination, + Entries: []structs.IntentionMatchEntry{ + { + Namespace: "foo", + Name: "bar", + }, + }, + }, + QueryOptions: structs.QueryOptions{Token: token}, + } + var resp structs.IndexedIntentionMatches + if err := msgpackrpc.CallWithCodec(codec, "Intention.Match", req, &resp); err != nil { + t.Fatalf("err: %v", err) + } + + if len(resp.Matches) != 1 { + t.Fatalf("bad: %#v", resp.Matches) + } + + expected := [][]string{{"foo", "bar"}, {"foo", "*"}, {"*", "*"}} + var actual [][]string + for _, ixn := range resp.Matches[0] { + actual = append(actual, []string{ixn.DestinationNS, ixn.DestinationName}) + } + + if !reflect.DeepEqual(actual, expected) { + t.Fatalf("bad (got, wanted):\n\n%#v\n\n%#v", actual, expected) + } + } +}