agent/structs: Intention validation
This commit is contained in:
parent
d34ee200de
commit
8e2462e301
|
@ -94,6 +94,11 @@ func (s *Intention) Apply(
|
||||||
args.Intention.DestinationNS = structs.IntentionDefaultNamespace
|
args.Intention.DestinationNS = structs.IntentionDefaultNamespace
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate
|
||||||
|
if err := args.Intention.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Commit
|
// Commit
|
||||||
resp, err := s.srv.raftApply(structs.IntentionRequestType, args)
|
resp, err := s.srv.raftApply(structs.IntentionRequestType, args)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -32,6 +32,8 @@ func TestIntentionApply_new(t *testing.T) {
|
||||||
SourceName: "test",
|
SourceName: "test",
|
||||||
DestinationNS: structs.IntentionDefaultNamespace,
|
DestinationNS: structs.IntentionDefaultNamespace,
|
||||||
DestinationName: "test",
|
DestinationName: "test",
|
||||||
|
Action: structs.IntentionActionAllow,
|
||||||
|
Meta: map[string]string{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
var reply string
|
var reply string
|
||||||
|
@ -86,7 +88,7 @@ func TestIntentionApply_new(t *testing.T) {
|
||||||
actual.CreatedAt = ixn.Intention.CreatedAt
|
actual.CreatedAt = ixn.Intention.CreatedAt
|
||||||
actual.UpdatedAt = ixn.Intention.UpdatedAt
|
actual.UpdatedAt = ixn.Intention.UpdatedAt
|
||||||
if !reflect.DeepEqual(actual, ixn.Intention) {
|
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",
|
SourceName: "test",
|
||||||
DestinationNS: structs.IntentionDefaultNamespace,
|
DestinationNS: structs.IntentionDefaultNamespace,
|
||||||
DestinationName: "test",
|
DestinationName: "test",
|
||||||
|
Action: structs.IntentionActionAllow,
|
||||||
|
Meta: map[string]string{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
var reply string
|
var reply string
|
||||||
|
@ -265,7 +269,11 @@ func TestIntentionApply_deleteGood(t *testing.T) {
|
||||||
Datacenter: "dc1",
|
Datacenter: "dc1",
|
||||||
Op: structs.IntentionOpCreate,
|
Op: structs.IntentionOpCreate,
|
||||||
Intention: &structs.Intention{
|
Intention: &structs.Intention{
|
||||||
SourceName: "test",
|
SourceNS: "test",
|
||||||
|
SourceName: "test",
|
||||||
|
DestinationNS: "test",
|
||||||
|
DestinationName: "test",
|
||||||
|
Action: structs.IntentionActionAllow,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
var reply string
|
var reply string
|
||||||
|
@ -358,6 +366,7 @@ func TestIntentionMatch_good(t *testing.T) {
|
||||||
SourceName: "test",
|
SourceName: "test",
|
||||||
DestinationNS: v[0],
|
DestinationNS: v[0],
|
||||||
DestinationName: v[1],
|
DestinationName: v[1],
|
||||||
|
Action: structs.IntentionActionAllow,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
package structs
|
package structs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-multierror"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -61,6 +65,91 @@ type Intention struct {
|
||||||
RaftIndex
|
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
|
// IntentionAction is the action that the intention represents. This
|
||||||
// can be "allow" or "deny" to whitelist or blacklist intentions.
|
// can be "allow" or "deny" to whitelist or blacklist intentions.
|
||||||
type IntentionAction string
|
type IntentionAction string
|
||||||
|
|
|
@ -3,9 +3,116 @@ package structs
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
"testing"
|
"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) {
|
func TestIntentionPrecedenceSorter(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
Name string
|
Name string
|
||||||
|
|
|
@ -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",
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue