diff --git a/agent/consul/intention_endpoint.go b/agent/consul/intention_endpoint.go index 434c90628..56f310e18 100644 --- a/agent/consul/intention_endpoint.go +++ b/agent/consul/intention_endpoint.go @@ -94,6 +94,11 @@ func (s *Intention) Apply( args.Intention.DestinationNS = structs.IntentionDefaultNamespace } + // Validate + if err := args.Intention.Validate(); err != nil { + return err + } + // Commit resp, err := s.srv.raftApply(structs.IntentionRequestType, args) if err != nil { diff --git a/agent/consul/intention_endpoint_test.go b/agent/consul/intention_endpoint_test.go index 21f112f48..2ee49f207 100644 --- a/agent/consul/intention_endpoint_test.go +++ b/agent/consul/intention_endpoint_test.go @@ -32,6 +32,8 @@ func TestIntentionApply_new(t *testing.T) { SourceName: "test", DestinationNS: structs.IntentionDefaultNamespace, DestinationName: "test", + Action: structs.IntentionActionAllow, + Meta: map[string]string{}, }, } var reply string @@ -86,7 +88,7 @@ func TestIntentionApply_new(t *testing.T) { actual.CreatedAt = ixn.Intention.CreatedAt actual.UpdatedAt = ixn.Intention.UpdatedAt if !reflect.DeepEqual(actual, ixn.Intention) { - t.Fatalf("bad: %v", actual) + t.Fatalf("bad:\n\n%#v\n\n%#v", actual, ixn.Intention) } } } @@ -140,6 +142,8 @@ func TestIntentionApply_updateGood(t *testing.T) { SourceName: "test", DestinationNS: structs.IntentionDefaultNamespace, DestinationName: "test", + Action: structs.IntentionActionAllow, + Meta: map[string]string{}, }, } var reply string @@ -265,7 +269,11 @@ func TestIntentionApply_deleteGood(t *testing.T) { Datacenter: "dc1", Op: structs.IntentionOpCreate, Intention: &structs.Intention{ - SourceName: "test", + SourceNS: "test", + SourceName: "test", + DestinationNS: "test", + DestinationName: "test", + Action: structs.IntentionActionAllow, }, } var reply string @@ -358,6 +366,7 @@ func TestIntentionMatch_good(t *testing.T) { SourceName: "test", DestinationNS: v[0], DestinationName: v[1], + Action: structs.IntentionActionAllow, }, } diff --git a/agent/structs/intention.go b/agent/structs/intention.go index 0a7d8c5d4..a8da939f7 100644 --- a/agent/structs/intention.go +++ b/agent/structs/intention.go @@ -1,7 +1,11 @@ package structs import ( + "fmt" + "strings" "time" + + "github.com/hashicorp/go-multierror" ) const ( @@ -61,6 +65,91 @@ type Intention struct { RaftIndex } +// Validate returns an error if the intention is invalid for inserting +// or updating. +func (x *Intention) Validate() error { + var result error + + // Empty values + if x.SourceNS == "" { + result = multierror.Append(result, fmt.Errorf("SourceNS must be set")) + } + if x.SourceName == "" { + result = multierror.Append(result, fmt.Errorf("SourceName must be set")) + } + if x.DestinationNS == "" { + result = multierror.Append(result, fmt.Errorf("DestinationNS must be set")) + } + if x.DestinationName == "" { + result = multierror.Append(result, fmt.Errorf("DestinationName must be set")) + } + + // Wildcard usage verification + if x.SourceNS != IntentionWildcard { + if strings.Contains(x.SourceNS, IntentionWildcard) { + result = multierror.Append(result, fmt.Errorf( + "SourceNS: wildcard character '*' cannot be used with partial values")) + } + } + if x.SourceName != IntentionWildcard { + if strings.Contains(x.SourceName, IntentionWildcard) { + result = multierror.Append(result, fmt.Errorf( + "SourceName: wildcard character '*' cannot be used with partial values")) + } + + if x.SourceNS == IntentionWildcard { + result = multierror.Append(result, fmt.Errorf( + "SourceName: exact value cannot follow wildcard namespace")) + } + } + if x.DestinationNS != IntentionWildcard { + if strings.Contains(x.DestinationNS, IntentionWildcard) { + result = multierror.Append(result, fmt.Errorf( + "DestinationNS: wildcard character '*' cannot be used with partial values")) + } + } + if x.DestinationName != IntentionWildcard { + if strings.Contains(x.DestinationName, IntentionWildcard) { + result = multierror.Append(result, fmt.Errorf( + "DestinationName: wildcard character '*' cannot be used with partial values")) + } + + if x.DestinationNS == IntentionWildcard { + result = multierror.Append(result, fmt.Errorf( + "DestinationName: exact value cannot follow wildcard namespace")) + } + } + + // Length of opaque values + if len(x.Description) > metaValueMaxLength { + result = multierror.Append(result, fmt.Errorf( + "Description exceeds maximum length %d", metaValueMaxLength)) + } + if len(x.Meta) > metaMaxKeyPairs { + result = multierror.Append(result, fmt.Errorf( + "Meta exceeds maximum element count %d", metaMaxKeyPairs)) + } + for k, v := range x.Meta { + if len(k) > metaKeyMaxLength { + result = multierror.Append(result, fmt.Errorf( + "Meta key %q exceeds maximum length %d", k, metaKeyMaxLength)) + } + if len(v) > metaValueMaxLength { + result = multierror.Append(result, fmt.Errorf( + "Meta value for key %q exceeds maximum length %d", k, metaValueMaxLength)) + } + } + + switch x.Action { + case IntentionActionAllow, IntentionActionDeny: + default: + result = multierror.Append(result, fmt.Errorf( + "Action must be set to 'allow' or 'deny'")) + } + + return result +} + // IntentionAction is the action that the intention represents. This // can be "allow" or "deny" to whitelist or blacklist intentions. type IntentionAction string diff --git a/agent/structs/intention_test.go b/agent/structs/intention_test.go index 19ac5811a..500b24d5a 100644 --- a/agent/structs/intention_test.go +++ b/agent/structs/intention_test.go @@ -3,9 +3,116 @@ package structs import ( "reflect" "sort" + "strings" "testing" ) +func TestIntentionValidate(t *testing.T) { + cases := []struct { + Name string + Modify func(*Intention) + Err string + }{ + { + "long description", + func(x *Intention) { + x.Description = strings.Repeat("x", metaValueMaxLength+1) + }, + "description exceeds", + }, + + { + "no action set", + func(x *Intention) { x.Action = "" }, + "action must be set", + }, + + { + "no SourceNS", + func(x *Intention) { x.SourceNS = "" }, + "SourceNS must be set", + }, + + { + "no SourceName", + func(x *Intention) { x.SourceName = "" }, + "SourceName must be set", + }, + + { + "no DestinationNS", + func(x *Intention) { x.DestinationNS = "" }, + "DestinationNS must be set", + }, + + { + "no DestinationName", + func(x *Intention) { x.DestinationName = "" }, + "DestinationName must be set", + }, + + { + "SourceNS partial wildcard", + func(x *Intention) { x.SourceNS = "foo*" }, + "partial value", + }, + + { + "SourceName partial wildcard", + func(x *Intention) { x.SourceName = "foo*" }, + "partial value", + }, + + { + "SourceName exact following wildcard", + func(x *Intention) { + x.SourceNS = "*" + x.SourceName = "foo" + }, + "follow wildcard", + }, + + { + "DestinationNS partial wildcard", + func(x *Intention) { x.DestinationNS = "foo*" }, + "partial value", + }, + + { + "DestinationName partial wildcard", + func(x *Intention) { x.DestinationName = "foo*" }, + "partial value", + }, + + { + "DestinationName exact following wildcard", + func(x *Intention) { + x.DestinationNS = "*" + x.DestinationName = "foo" + }, + "follow wildcard", + }, + } + + for _, tc := range cases { + t.Run(tc.Name, func(t *testing.T) { + ixn := TestIntention(t) + tc.Modify(ixn) + + err := ixn.Validate() + if (err != nil) != (tc.Err != "") { + t.Fatalf("err: %s", err) + } + if err == nil { + return + } + if !strings.Contains(strings.ToLower(err.Error()), strings.ToLower(tc.Err)) { + t.Fatalf("err: %s", err) + } + }) + } +} + func TestIntentionPrecedenceSorter(t *testing.T) { cases := []struct { Name string diff --git a/agent/structs/testing_intention.go b/agent/structs/testing_intention.go new file mode 100644 index 000000000..b653c1174 --- /dev/null +++ b/agent/structs/testing_intention.go @@ -0,0 +1,15 @@ +package structs + +import ( + "github.com/mitchellh/go-testing-interface" +) + +// TestIntention returns a valid, uninserted (no ID set) intention. +func TestIntention(t testing.T) *Intention { + return &Intention{ + SourceNS: "eng", + SourceName: "api", + DestinationNS: "eng", + DestinationName: "db", + } +}