diff --git a/agent/consul/acl.go b/agent/consul/acl.go index 1e95e62e4..ce3282b40 100644 --- a/agent/consul/acl.go +++ b/agent/consul/acl.go @@ -454,6 +454,33 @@ func (f *aclFilter) filterCoordinates(coords *structs.Coordinates) { *coords = c } +// filterIntentions is used to filter intentions based on ACL rules. +// We prune entries the user doesn't have access to, and we redact any tokens +// if the user doesn't have a management token. +func (f *aclFilter) filterIntentions(ixns *structs.Intentions) { + // Management tokens can see everything with no filtering. + if f.acl.ACLList() { + return + } + + // Otherwise, we need to see what the token has access to. + ret := make(structs.Intentions, 0, len(*ixns)) + for _, ixn := range *ixns { + // If no prefix ACL applies to this then filter it, since + // we know at this point the user doesn't have a management + // token, otherwise see what the policy says. + prefix, ok := ixn.GetACLPrefix() + if !ok || !f.acl.IntentionRead(prefix) { + f.logger.Printf("[DEBUG] consul: dropping intention %q from result due to ACLs", ixn.ID) + continue + } + + ret = append(ret, ixn) + } + + *ixns = ret +} + // filterNodeDump is used to filter through all parts of a node dump and // remove elements the provided ACL token cannot access. func (f *aclFilter) filterNodeDump(dump *structs.NodeDump) { @@ -598,6 +625,9 @@ func (s *Server) filterACL(token string, subj interface{}) error { case *structs.IndexedHealthChecks: filt.filterHealthChecks(&v.HealthChecks) + case *structs.IndexedIntentions: + filt.filterIntentions(&v.Intentions) + case *structs.IndexedNodeDump: filt.filterNodeDump(&v.Dump) diff --git a/agent/consul/intention_endpoint.go b/agent/consul/intention_endpoint.go index 2a409dcbe..568446d73 100644 --- a/agent/consul/intention_endpoint.go +++ b/agent/consul/intention_endpoint.go @@ -168,7 +168,16 @@ func (s *Intention) Get( reply.Index = index reply.Intentions = structs.Intentions{ixn} - // TODO: acl filtering + // Filter + if err := s.srv.filterACL(args.Token, reply); err != nil { + return err + } + + // If ACLs prevented any responses, error + if len(reply.Intentions) == 0 { + s.srv.logger.Printf("[WARN] consul.intention: Request to get intention '%s' denied due to ACLs", args.IntentionID) + return acl.ErrPermissionDenied + } return nil }, diff --git a/agent/consul/intention_endpoint_test.go b/agent/consul/intention_endpoint_test.go index fd76bbb78..67c2a07d0 100644 --- a/agent/consul/intention_endpoint_test.go +++ b/agent/consul/intention_endpoint_test.go @@ -654,6 +654,96 @@ service "foo" { } } +// Test reading with ACLs +func TestIntentionGet_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) + } + } + + // Setup a basic record to create + ixn := structs.IntentionRequest{ + Datacenter: "dc1", + Op: structs.IntentionOpCreate, + Intention: structs.TestIntention(t), + } + ixn.Intention.DestinationName = "foobar" + ixn.WriteRequest.Token = "root" + + // Create + var reply string + if err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply); err != nil { + t.Fatalf("err: %v", err) + } + ixn.Intention.ID = reply + + // Read without token should be error + { + req := &structs.IntentionQueryRequest{ + Datacenter: "dc1", + IntentionID: ixn.Intention.ID, + } + + var resp structs.IndexedIntentions + err := msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp) + if !acl.IsErrPermissionDenied(err) { + t.Fatalf("bad: %v", err) + } + if len(resp.Intentions) != 0 { + t.Fatalf("bad: %v", resp) + } + } + + // Read with token should work + { + req := &structs.IntentionQueryRequest{ + Datacenter: "dc1", + IntentionID: ixn.Intention.ID, + QueryOptions: structs.QueryOptions{Token: token}, + } + + var resp structs.IndexedIntentions + if err := msgpackrpc.CallWithCodec(codec, "Intention.Get", req, &resp); err != nil { + t.Fatalf("err: %v", err) + } + if len(resp.Intentions) != 1 { + t.Fatalf("bad: %v", resp) + } + } +} + func TestIntentionList(t *testing.T) { t.Parallel()