diff --git a/agent/consul/intention_endpoint.go b/agent/consul/intention_endpoint.go index 6653d55022..0440f17e49 100644 --- a/agent/consul/intention_endpoint.go +++ b/agent/consul/intention_endpoint.go @@ -6,6 +6,7 @@ import ( "time" "github.com/armon/go-metrics" + "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/consul/state" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/go-memdb" @@ -71,6 +72,20 @@ func (s *Intention) Apply( } *reply = args.Intention.ID + // Get the ACL token for the request for the checks below. + rule, err := s.srv.resolveToken(args.Token) + if err != nil { + return err + } + + // Perform the ACL check + if prefix, ok := args.Intention.GetACLPrefix(); ok { + if rule != nil && !rule.IntentionWrite(prefix) { + s.srv.logger.Printf("[WARN] consul.intention: Operation on intention '%s' denied due to ACLs", args.Intention.ID) + return acl.ErrPermissionDenied + } + } + // If this is not a create, then we have to verify the ID. if args.Op != structs.IntentionOpCreate { state := s.srv.fsm.State() diff --git a/agent/consul/intention_endpoint_test.go b/agent/consul/intention_endpoint_test.go index 2ba5b04c38..5edf904d7d 100644 --- a/agent/consul/intention_endpoint_test.go +++ b/agent/consul/intention_endpoint_test.go @@ -5,6 +5,7 @@ import ( "testing" "time" + "github.com/hashicorp/consul/acl" "github.com/hashicorp/consul/agent/structs" "github.com/hashicorp/consul/testrpc" "github.com/hashicorp/net-rpc-msgpackrpc" @@ -303,6 +304,94 @@ func TestIntentionApply_deleteGood(t *testing.T) { } } +// Test apply with a deny ACL +func TestIntentionApply_aclDeny(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 write permissions + var token string + { + var rules = ` +service "foo" { + policy = "deny" + intentions = "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" + + // Create without a token should error since default deny + var reply string + err := msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply) + if !acl.IsErrPermissionDenied(err) { + t.Fatalf("bad: %v", err) + } + + // Now add the token and try again. + ixn.WriteRequest.Token = token + if err = msgpackrpc.CallWithCodec(codec, "Intention.Apply", &ixn, &reply); err != nil { + t.Fatalf("err: %v", err) + } + + // Read + ixn.Intention.ID = reply + { + req := &structs.IntentionQueryRequest{ + Datacenter: "dc1", + IntentionID: ixn.Intention.ID, + } + 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) + } + actual := resp.Intentions[0] + if resp.Index != actual.ModifyIndex { + t.Fatalf("bad index: %d", resp.Index) + } + + actual.CreateIndex, actual.ModifyIndex = 0, 0 + actual.CreatedAt = ixn.Intention.CreatedAt + actual.UpdatedAt = ixn.Intention.UpdatedAt + if !reflect.DeepEqual(actual, ixn.Intention) { + t.Fatalf("bad:\n\n%#v\n\n%#v", actual, ixn.Intention) + } + } +} + func TestIntentionList(t *testing.T) { t.Parallel() diff --git a/agent/structs/intention.go b/agent/structs/intention.go index 579fef6c10..fb83f85dab 100644 --- a/agent/structs/intention.go +++ b/agent/structs/intention.go @@ -157,6 +157,13 @@ func (x *Intention) Validate() error { return result } +// GetACLPrefix returns the prefix to look up the ACL policy for this +// intention, and a boolean noting whether the prefix is valid to check +// or not. You must check the ok value before using the prefix. +func (x *Intention) GetACLPrefix() (string, bool) { + return x.DestinationName, x.DestinationName != "" +} + // IntentionAction is the action that the intention represents. This // can be "allow" or "deny" to whitelist or blacklist intentions. type IntentionAction string