agent/structs: Intention validation

This commit is contained in:
Mitchell Hashimoto 2018-03-03 09:43:37 -08:00
parent d34ee200de
commit 8e2462e301
No known key found for this signature in database
GPG key ID: 744E147AA52F5B0A
5 changed files with 227 additions and 2 deletions

View file

@ -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 {

View file

@ -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,
},
}

View file

@ -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

View file

@ -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

View file

@ -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",
}
}