diff --git a/docs/resources/named_location.md b/docs/resources/named_location.md index 9812a0d38f..8822d7e25a 100644 --- a/docs/resources/named_location.md +++ b/docs/resources/named_location.md @@ -61,7 +61,7 @@ The following arguments are supported: `ip` block supports the following: -* `ip_ranges` - (Required) List of IP address ranges in IPv4 CIDR format (e.g. `1.2.3.4/32`) or any allowable IPv6 format from IETF RFC596. +* `ip_ranges` - (Required) List of IP address ranges in IPv4 CIDR format (e.g. `1.2.3.4/32`) or any allowable IPv6 format from IETF RFC596. Each CIDR prefix must be `/8` or larger. * `trusted` - (Optional) Whether the named location is trusted. Defaults to `false`. --- diff --git a/internal/services/conditionalaccess/named_location_resource.go b/internal/services/conditionalaccess/named_location_resource.go index 1de19bf9e0..d8048c458a 100644 --- a/internal/services/conditionalaccess/named_location_resource.go +++ b/internal/services/conditionalaccess/named_location_resource.go @@ -64,7 +64,8 @@ func namedLocationResource() *pluginsdk.Resource { Type: pluginsdk.TypeList, Required: true, Elem: &pluginsdk.Schema{ - Type: pluginsdk.TypeString, + Type: pluginsdk.TypeString, + ValidateFunc: validation.PrefixLengthAtLeast(8), }, }, @@ -88,7 +89,8 @@ func namedLocationResource() *pluginsdk.Resource { Type: pluginsdk.TypeList, Required: true, Elem: &pluginsdk.Schema{ - Type: pluginsdk.TypeString, + Type: pluginsdk.TypeString, + ValidateFunc: validation.StringIsNotEmpty, }, }, diff --git a/internal/tf/validation/net.go b/internal/tf/validation/net.go new file mode 100644 index 0000000000..ad722c9566 --- /dev/null +++ b/internal/tf/validation/net.go @@ -0,0 +1,79 @@ +package validation + +import ( + "fmt" + "net/netip" +) + +func StringIsIpPrefix(i interface{}, k string) (warnings []string, errors []error) { + if warnings, errors = StringIsNotEmpty(i, k); len(errors) > 0 { + return warnings, errors + } + + if _, err := netip.ParsePrefix(i.(string)); err != nil { + return nil, []error{fmt.Errorf("expected %q to be a valid IPv4 or IPv6 prefix", k)} + } + + return +} + +func PrefixLengthAtLeast(minLength int) func(interface{}, string) ([]string, []error) { + return func(i interface{}, k string) (warnings []string, errors []error) { + if warnings, errors = StringIsNotEmpty(i, k); len(errors) > 0 { + return warnings, errors + } + + prefix, err := netip.ParsePrefix(i.(string)) + if err != nil { + return nil, []error{fmt.Errorf("expected %q to be a valid IPv4 or IPv6 prefix", k)} + } + + if prefixLength := prefix.Bits(); prefixLength < minLength { + return nil, []error{fmt.Errorf("expected %q to have a prefix length at least %d, got %d", k, minLength, prefixLength)} + } + + return + } +} + +func PrefixLengthAtMost(maxLength int) func(interface{}, string) ([]string, []error) { + return func(i interface{}, k string) (warnings []string, errors []error) { + if warnings, errors = StringIsNotEmpty(i, k); len(errors) > 0 { + return warnings, errors + } + + prefix, err := netip.ParsePrefix(i.(string)) + if err != nil { + return nil, []error{fmt.Errorf("expected %q to be a valid IPv4 or IPv6 prefix", k)} + } + + if prefixLength := prefix.Bits(); prefixLength > maxLength { + return nil, []error{fmt.Errorf("expected %q to have a prefix length at most %d, got %d", k, maxLength, prefixLength)} + } + + return + } +} + +func PrefixLengthBetween(minLength, maxLength int) func(interface{}, string) ([]string, []error) { + return func(i interface{}, k string) (warnings []string, errors []error) { + if warnings, errors = StringIsNotEmpty(i, k); len(errors) > 0 { + return warnings, errors + } + + prefix, err := netip.ParsePrefix(i.(string)) + if err != nil { + return nil, []error{fmt.Errorf("expected %q to be a valid IPv4 or IPv6 prefix", k)} + } + + if prefixLength := prefix.Bits(); prefixLength < minLength { + return nil, []error{fmt.Errorf("expected %q to have a prefix length at least %d, got %d", k, minLength, prefixLength)} + } + + if prefixLength := prefix.Bits(); prefixLength > maxLength { + return nil, []error{fmt.Errorf("expected %q to have a prefix length at most %d, got %d", k, maxLength, prefixLength)} + } + + return + } +} diff --git a/internal/tf/validation/net_test.go b/internal/tf/validation/net_test.go new file mode 100644 index 0000000000..a69838bc20 --- /dev/null +++ b/internal/tf/validation/net_test.go @@ -0,0 +1,193 @@ +package validation + +import ( + "testing" +) + +func TestStringIsIpPrefix(t *testing.T) { + cases := []struct { + Value string + TestName string + ErrCount int + }{ + { + Value: "10.0.0.0/8", + TestName: "Valid_NonRoutable1", + ErrCount: 0, + }, + { + Value: "192.168.0.0/16", + TestName: "Valid_NonRoutable2", + ErrCount: 0, + }, + { + Value: "172.16.20.5", + TestName: "Invalid_SingleAddress", + ErrCount: 1, + }, + { + Value: "224.0.50.8", + TestName: "Invalid_MulticastAddress", + ErrCount: 1, + }, + { + Value: "100.64.10.0", + TestName: "Invalid_Network", + ErrCount: 1, + }, + } + + for _, tc := range cases { + t.Run(tc.TestName, func(t *testing.T) { + warnings, errors := StringIsIpPrefix(tc.Value, "test") + + if len(warnings) > 0 { + t.Fatalf("Expected StringIsIpPrefix to have %d not %d warnings for %q", 0, len(warnings), tc.TestName) + } + if len(errors) != tc.ErrCount { + t.Fatalf("Expected StringIsIpPrefix to have %d not %d errors for %q", tc.ErrCount, len(errors), tc.TestName) + } + }) + } +} + +func TestPrefixLengthAtLeast(t *testing.T) { + cases := []struct { + MinLength int + Value string + TestName string + ErrCount int + }{ + { + MinLength: 8, + Value: "10.0.0.0/8", + TestName: "Valid_Exact", + ErrCount: 0, + }, + { + MinLength: 16, + Value: "192.168.0.0/24", + TestName: "Valid_Larger", + ErrCount: 0, + }, + { + MinLength: 8, + Value: "10.0.0.0/4", + TestName: "Invalid_Smaller", + ErrCount: 1, + }, + } + + for _, tc := range cases { + t.Run(tc.TestName, func(t *testing.T) { + warnings, errors := PrefixLengthAtLeast(tc.MinLength)(tc.Value, "test") + + if len(warnings) > 0 { + t.Fatalf("Expected PrefixLengthAtLeast to have %d not %d warnings for %q", 0, len(warnings), tc.TestName) + } + if len(errors) != tc.ErrCount { + t.Fatalf("Expected PrefixLengthAtLeast to have %d not %d errors for %q", tc.ErrCount, len(errors), tc.TestName) + } + }) + } +} + +func TestPrefixLengthAtMost(t *testing.T) { + cases := []struct { + MaxLength int + Value string + TestName string + ErrCount int + }{ + { + MaxLength: 24, + Value: "192.168.0.0/24", + TestName: "Valid_Exact", + ErrCount: 0, + }, + { + MaxLength: 16, + Value: "10.0.0.0/8", + TestName: "Valid_Smaller", + ErrCount: 0, + }, + { + MaxLength: 8, + Value: "10.0.0.0/12", + TestName: "Invalid_Larger", + ErrCount: 1, + }, + } + + for _, tc := range cases { + t.Run(tc.TestName, func(t *testing.T) { + warnings, errors := PrefixLengthAtMost(tc.MaxLength)(tc.Value, "test") + + if len(warnings) > 0 { + t.Fatalf("Expected PrefixLengthAtMost to have %d not %d warnings for %q", 0, len(warnings), tc.TestName) + } + if len(errors) != tc.ErrCount { + t.Fatalf("Expected PrefixLengthAtMost to have %d not %d errors for %q", tc.ErrCount, len(errors), tc.TestName) + } + }) + } +} + +func TestPrefixLengthBetween(t *testing.T) { + cases := []struct { + MinLength int + MaxLength int + Value string + TestName string + ErrCount int + }{ + { + MinLength: 16, + MaxLength: 24, + Value: "192.168.0.0/24", + TestName: "Valid_ExactUpper", + ErrCount: 0, + }, + { + MinLength: 16, + MaxLength: 24, + Value: "172.16.0.0/16", + TestName: "Valid_ExactLower", + ErrCount: 0, + }, + { + MinLength: 8, + MaxLength: 16, + Value: "10.50.0.0/12", + TestName: "Valid_InRange", + ErrCount: 0, + }, + { + MinLength: 24, + MaxLength: 28, + Value: "10.0.0.0/12", + TestName: "Invalid_Smaller", + ErrCount: 1, + }, + { + MinLength: 24, + MaxLength: 28, + Value: "192.168.100.0/30", + TestName: "Invalid_Larger", + ErrCount: 1, + }, + } + + for _, tc := range cases { + t.Run(tc.TestName, func(t *testing.T) { + warnings, errors := PrefixLengthBetween(tc.MinLength, tc.MaxLength)(tc.Value, "test") + + if len(warnings) > 0 { + t.Fatalf("Expected PrefixLengthBetween to have %d not %d warnings for %q", 0, len(warnings), tc.TestName) + } + if len(errors) != tc.ErrCount { + t.Fatalf("Expected PrefixLengthBetween to have %d not %d errors for %q", tc.ErrCount, len(errors), tc.TestName) + } + }) + } +}