diff --git a/internal/ent/schema/validator/errors.go b/internal/ent/schema/validator/errors.go new file mode 100644 index 00000000..5f27c2fb --- /dev/null +++ b/internal/ent/schema/validator/errors.go @@ -0,0 +1,30 @@ +package validator + +import ( + "errors" + "fmt" +) + +// ErrInvalidIPAddr is an error raised when provided IP Address is invalid +var ErrInvalidIPAddr = errors.New("ip Address is invalid") + +// InvalidIPAddrError returns Error Invalid IP Address +func InvalidIPAddrError(ip string) error { + return fmt.Errorf("%w: %s", ErrInvalidIPAddr, ip) +} + +// ErrInvalidIPPref is an error raised when provided IP Block Prefix is invalid +var ErrInvalidIPPref = errors.New("ip block prefix is invalid") + +// InvalidIPPrefError returns Error Invalid IP Block Prefix +func InvalidIPPrefError(prefix string) error { + return fmt.Errorf("%w: %s", ErrInvalidIPPref, prefix) +} + +// ErrIPAddrOutsideBlock is an error raised when provided IP Address is not part of the IP Block +var ErrIPAddrOutsideBlock = errors.New("ip address is invalid for IP Block Prefix") + +// IPAddrOutsideBlockError returns Error IP Address doesn't belong to the IP Block +func IPAddrOutsideBlockError(block string, ip string) error { + return fmt.Errorf("%w: ip address: %s, ip block %s", ErrIPAddrOutsideBlock, ip, block) +} diff --git a/internal/ent/schema/validator/validate.go b/internal/ent/schema/validator/validate.go index 09138512..562f6917 100644 --- a/internal/ent/schema/validator/validate.go +++ b/internal/ent/schema/validator/validate.go @@ -1,10 +1,11 @@ package validator import ( - "errors" - "fmt" "net" + "net/netip" + + "github.com/3th1nk/cidr" ) // IPAddr returns error if IP address is NOT valid @@ -16,14 +17,6 @@ func IPAddr(ip string) error { return InvalidIPAddrError(ip) } -// ErrInvalidIPAddr is an error raised when provided IP Address is invalid -var ErrInvalidIPAddr = errors.New("provided IP Address is invalid") - -// InvalidIPAddrError returns Error Invalid IP Address -func InvalidIPAddrError(ip string) error { - return fmt.Errorf("error %w: %s", ErrInvalidIPAddr, ip) -} - // IPBlockPref returns error if IP Block Prefix is NOT valid func IPBlockPref(prefix string) error { _, err := netip.ParsePrefix(prefix) @@ -34,10 +27,22 @@ func IPBlockPref(prefix string) error { return nil } -// ErrInvalidIPPref is an error raised when provided IP Block Prefix is invalid -var ErrInvalidIPPref = errors.New("provided IP Block Prefix is invalid") +// PartOfBlock returns error if IP address is NOT part of the block given block's prefix +func PartOfBlock(ipBlockPref string, ipAdrr string) error { + c, _ := cidr.Parse(ipBlockPref) + belongsToBlock := false + + c.Each(func(ip string) bool { + if ip == ipAdrr { + belongsToBlock = true + } + + return true + }) + + if belongsToBlock { + return nil + } -// InvalidIPPrefError returns Error Invalid IP Block Prefix -func InvalidIPPrefError(prefix string) error { - return fmt.Errorf("error %w: %s", ErrInvalidIPPref, prefix) + return IPAddrOutsideBlockError(ipBlockPref, ipAdrr) } diff --git a/internal/ent/schema/validator/validate_test.go b/internal/ent/schema/validator/validate_test.go new file mode 100644 index 00000000..0f421955 --- /dev/null +++ b/internal/ent/schema/validator/validate_test.go @@ -0,0 +1,55 @@ +package validator + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPartOfBlock(t *testing.T) { + type args struct { + ipBl string + ipAdrr string + } + + tests := []struct { + name string + args args + wantErr bool + }{ + { + + name: "happy path", + args: args{ + ipBl: "192.168.1.0/28", + ipAdrr: "192.168.1.13"}, + wantErr: false, + }, + { + name: "outside block", + args: args{ + ipBl: "192.168.1.0/28", + ipAdrr: "192.168.1.25"}, + wantErr: true, + }, + { + name: "far from block", + args: args{ + ipBl: "108.1.80.128/30", + ipAdrr: "192.168.10.12"}, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := PartOfBlock(tt.args.ipBl, tt.args.ipAdrr) + if tt.wantErr { + assert.Error(t, err) + assert.ErrorContainsf(t, err, tt.args.ipAdrr, tt.args.ipBl) + } else { + assert.Nil(t, err) + } + }) + } +} diff --git a/internal/graphapi/ipaddress.resolvers.go b/internal/graphapi/ipaddress.resolvers.go index 85055c7a..4e4060e6 100644 --- a/internal/graphapi/ipaddress.resolvers.go +++ b/internal/graphapi/ipaddress.resolvers.go @@ -9,6 +9,7 @@ import ( "go.infratographer.com/ipam-api/internal/ent/generated" "go.infratographer.com/ipam-api/internal/ent/generated/ipaddress" + "go.infratographer.com/ipam-api/internal/ent/schema/validator" "go.infratographer.com/permissions-api/pkg/permissions" "go.infratographer.com/x/gidx" ) @@ -34,6 +35,10 @@ func (r *mutationResolver) CreateIPAddress(ctx context.Context, input generated. return nil, err } + // if err := validator.PartOfBlock(input.IP, input.NodeID.Prefix()); err != nil { + // return nil, err + // } + t, err := r.client.IPAddress.Create().SetInput(input).Save(ctx) if err != nil { return nil, err @@ -53,6 +58,15 @@ func (r *mutationResolver) UpdateIPAddress(ctx context.Context, id gidx.Prefixed return nil, err } + bl, err := r.client.IPBlock.Get(ctx, t.BlockID) + if err != nil { + return nil, err + } + + if err := validator.PartOfBlock(t.IP, bl.Prefix); err != nil { + return nil, err + } + t, err = t.Update().SetInput(input).Save(ctx) if err != nil { return nil, err diff --git a/internal/graphapi/ipaddress_test.go b/internal/graphapi/ipaddress_test.go index 81faded0..572b0f24 100644 --- a/internal/graphapi/ipaddress_test.go +++ b/internal/graphapi/ipaddress_test.go @@ -68,11 +68,11 @@ func Test_IPAddress_Lifecycle(t *testing.T) { ctx = context.WithValue(ctx, permissions.CheckerCtxKey, permissions.DefaultAllowChecker) ipbt := (&IPBlockTypeBuilder{}).MustNew(ctx) - ipb := (&IPBlockBuilder{IPBlockTypeID: ipbt.ID}).MustNew(ctx) + ipb := (&IPBlockBuilder{IPBlockTypeID: ipbt.ID, Prefix: "192.168.1.0/28"}).MustNew(ctx) t.Run("Create", func(t *testing.T) { ipa, err := client.CreateIPAddress(ctx, testclient.CreateIPAddressInput{ - IP: gofakeit.IPv4Address(), + IP: "192.168.1.13", NodeID: gidx.MustNewID(nodePrefix), NodeOwnerID: gidx.MustNewID(ownerPrefix), Reserved: newBool(true), @@ -163,3 +163,65 @@ func Test_IPAddressable(t *testing.T) { require.NoError(t, err) assert.Len(t, addrs.Entities[0].IPAddresses, 0) } + +func Test_IPAddress_PartOfBlock_Succeess(t *testing.T) { + client := graphTestClient() + ctx := context.Background() + + // Permit request + ctx = context.WithValue(ctx, permissions.CheckerCtxKey, permissions.DefaultAllowChecker) + + ipbt := (&IPBlockTypeBuilder{}).MustNew(ctx) + ipb := (&IPBlockBuilder{IPBlockTypeID: ipbt.ID, Prefix: "192.168.1.0/28"}).MustNew(ctx) + + t.Run("Create", func(t *testing.T) { + ipa, err := client.CreateIPAddress(ctx, testclient.CreateIPAddressInput{ + IP: "192.168.1.13", + NodeID: gidx.MustNewID(nodePrefix), + NodeOwnerID: gidx.MustNewID(ownerPrefix), + Reserved: newBool(true), + IPBlockID: ipb.ID, + }) + + require.NoError(t, err) + require.NotNil(t, ipa) + }) +} + +func Test_IPAddress_PartOfBlock_Failure(t *testing.T) { + client := graphTestClient() + ctx := context.Background() + + // Permit request + ctx = context.WithValue(ctx, permissions.CheckerCtxKey, permissions.DefaultAllowChecker) + + ipbt := (&IPBlockTypeBuilder{}).MustNew(ctx) + ipb := (&IPBlockBuilder{IPBlockTypeID: ipbt.ID, Prefix: "192.168.1.0/28"}).MustNew(ctx) + ipb2 := (&IPBlockBuilder{IPBlockTypeID: ipbt.ID, Prefix: "108.1.80.128/30"}).MustNew(ctx) + + t.Run("Create", func(t *testing.T) { + ipa, err := client.CreateIPAddress(ctx, testclient.CreateIPAddressInput{ + IP: "192.168.1.25", + NodeID: gidx.MustNewID(nodePrefix), + NodeOwnerID: gidx.MustNewID(ownerPrefix), + Reserved: newBool(true), + IPBlockID: ipb.ID, + }) + + require.Error(t, err) + require.Nil(t, ipa) + }) + + t.Run("Create", func(t *testing.T) { + ipa, err := client.CreateIPAddress(ctx, testclient.CreateIPAddressInput{ + IP: "192.168.10.12", + NodeID: gidx.MustNewID(nodePrefix), + NodeOwnerID: gidx.MustNewID(ownerPrefix), + Reserved: newBool(true), + IPBlockID: ipb2.ID, + }) + + require.Error(t, err) + require.Nil(t, ipa) + }) +}