mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-12 05:21:58 +00:00
Add IP address CEL extension
This adds new CEL functions to the library for validating if a string is an IP address, and, if it can be parsed as an IP address, adds additional accessors to get properties of the IP address.
This commit is contained in:
parent
d19420f3ec
commit
c6aa360d3e
86
staging/src/k8s.io/apiserver/pkg/cel/ip.go
Normal file
86
staging/src/k8s.io/apiserver/pkg/cel/ip.go
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package cel
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"net/netip"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/google/cel-go/cel"
|
||||||
|
"github.com/google/cel-go/common/types"
|
||||||
|
"github.com/google/cel-go/common/types/ref"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IP provides a CEL representation of an IP address.
|
||||||
|
type IP struct {
|
||||||
|
netip.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
IPType = cel.OpaqueType("net.IP")
|
||||||
|
)
|
||||||
|
|
||||||
|
// ConvertToNative implements ref.Val.ConvertToNative.
|
||||||
|
func (d IP) ConvertToNative(typeDesc reflect.Type) (any, error) {
|
||||||
|
if reflect.TypeOf(d.Addr).AssignableTo(typeDesc) {
|
||||||
|
return d.Addr, nil
|
||||||
|
}
|
||||||
|
if reflect.TypeOf("").AssignableTo(typeDesc) {
|
||||||
|
return d.Addr.String(), nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("type conversion error from 'IP' to '%v'", typeDesc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertToType implements ref.Val.ConvertToType.
|
||||||
|
func (d IP) ConvertToType(typeVal ref.Type) ref.Val {
|
||||||
|
switch typeVal {
|
||||||
|
case IPType:
|
||||||
|
return d
|
||||||
|
case types.TypeType:
|
||||||
|
return IPType
|
||||||
|
case types.StringType:
|
||||||
|
return types.String(d.Addr.String())
|
||||||
|
}
|
||||||
|
return types.NewErr("type conversion error from '%s' to '%s'", IPType, typeVal)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal implements ref.Val.Equal.
|
||||||
|
func (d IP) Equal(other ref.Val) ref.Val {
|
||||||
|
otherD, ok := other.(IP)
|
||||||
|
if !ok {
|
||||||
|
return types.ValOrErr(other, "no such overload")
|
||||||
|
}
|
||||||
|
return types.Bool(d.Addr == otherD.Addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type implements ref.Val.Type.
|
||||||
|
func (d IP) Type() ref.Type {
|
||||||
|
return IPType
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value implements ref.Val.Value.
|
||||||
|
func (d IP) Value() any {
|
||||||
|
return d.Addr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size returns the size of the IP address in bytes.
|
||||||
|
// Used in the size estimation of the runtime cost.
|
||||||
|
func (d IP) Size() ref.Val {
|
||||||
|
return types.Int(int(math.Ceil(float64(d.Addr.BitLen()) / 8)))
|
||||||
|
}
|
229
staging/src/k8s.io/apiserver/pkg/cel/library/ip.go
Normal file
229
staging/src/k8s.io/apiserver/pkg/cel/library/ip.go
Normal file
@ -0,0 +1,229 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 The Kubernetes Authors.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package library
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/netip"
|
||||||
|
|
||||||
|
"github.com/google/cel-go/cel"
|
||||||
|
"github.com/google/cel-go/common/types"
|
||||||
|
"github.com/google/cel-go/common/types/ref"
|
||||||
|
|
||||||
|
apiservercel "k8s.io/apiserver/pkg/cel"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IP provides a CEL function library extension of IP address parsing functions.
|
||||||
|
//
|
||||||
|
// ip
|
||||||
|
//
|
||||||
|
// Converts a string to an IP address or results in an error if the string is not a valid IP address.
|
||||||
|
// The IP address must be an IPv4 or IPv6 address.
|
||||||
|
// IPv4-mapped IPv6 addresses (e.g. ::ffff:1.2.3.4) are not allowed.
|
||||||
|
// IP addresses with zones (e.g. fe80::1%eth0) are not allowed.
|
||||||
|
// Leading zeros in IPv4 address octets are not allowed.
|
||||||
|
//
|
||||||
|
// ip(<string>) <IPAddr>
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
//
|
||||||
|
// ip('127.0.0.1') // returns an IPv4 address
|
||||||
|
// ip('::1') // returns an IPv6 address
|
||||||
|
// ip('127.0.0.256') // error
|
||||||
|
// ip(':::1') // error
|
||||||
|
//
|
||||||
|
// isIP
|
||||||
|
//
|
||||||
|
// Returns true if a string is a valid IP address.
|
||||||
|
// The IP address must be an IPv4 or IPv6 address.
|
||||||
|
// IPv4-mapped IPv6 addresses (e.g. ::ffff:1.2.3.4) are not allowed.
|
||||||
|
// IP addresses with zones (e.g. fe80::1%eth0) are not allowed.
|
||||||
|
// Leading zeros in IPv4 address octets are not allowed.
|
||||||
|
//
|
||||||
|
// isIP(<string>) <bool>
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
//
|
||||||
|
// isIP('127.0.0.1') // returns true
|
||||||
|
// isIP('::1') // returns true
|
||||||
|
// isIP('127.0.0.256') // returns false
|
||||||
|
// isIP(':::1') // returns false
|
||||||
|
//
|
||||||
|
// ip.isCanonical
|
||||||
|
//
|
||||||
|
// Returns true if the IP address is in its canonical form.
|
||||||
|
// There is exactly one canonical form for every IP address, so fields containing
|
||||||
|
// IPs in canonical form can just be treated as strings when checking for equality or uniqueness.
|
||||||
|
//
|
||||||
|
// ip.isCanonical(<string>) <bool>
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
//
|
||||||
|
// ip.isCanonical('127.0.0.1') // returns true; all valid IPv4 addresses are canonical
|
||||||
|
// ip.isCanonical('2001:db8::abcd') // returns true
|
||||||
|
// ip.isCanonical('2001:DB8::ABCD') // returns false
|
||||||
|
// ip.isCanonical('2001:db8::0:0:0:abcd') // returns false
|
||||||
|
//
|
||||||
|
// family
|
||||||
|
//
|
||||||
|
// - family: returns the IP addresses' family (IPv4 or IPv6) as an integer, either '4' or '6'.
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
//
|
||||||
|
// ip('127.0.0.1').family() // returns '4”
|
||||||
|
// ip('::1').family() // returns '6'
|
||||||
|
// ip('127.0.0.1').family() == 4 // returns true
|
||||||
|
// ip('::1').family() == 6 // returns true
|
||||||
|
func IP() cel.EnvOption {
|
||||||
|
return cel.Lib(ipLib)
|
||||||
|
}
|
||||||
|
|
||||||
|
var ipLib = &ip{}
|
||||||
|
|
||||||
|
type ip struct{}
|
||||||
|
|
||||||
|
func (*ip) LibraryName() string {
|
||||||
|
return "net.ip"
|
||||||
|
}
|
||||||
|
|
||||||
|
var ipLibraryDecls = map[string][]cel.FunctionOpt{
|
||||||
|
"ip": {
|
||||||
|
cel.Overload("string_to_ip", []*cel.Type{cel.StringType}, apiservercel.IPType,
|
||||||
|
cel.UnaryBinding(stringToIP)),
|
||||||
|
},
|
||||||
|
"family": {
|
||||||
|
cel.MemberOverload("ip_family", []*cel.Type{apiservercel.IPType}, cel.IntType,
|
||||||
|
cel.UnaryBinding(family)),
|
||||||
|
},
|
||||||
|
"ip.isCanonical": {
|
||||||
|
cel.Overload("ip_is_canonical", []*cel.Type{cel.StringType}, cel.BoolType,
|
||||||
|
cel.UnaryBinding(ipIsCanonical)),
|
||||||
|
},
|
||||||
|
"isIP": {
|
||||||
|
cel.Overload("is_ip", []*cel.Type{cel.StringType}, cel.BoolType,
|
||||||
|
cel.UnaryBinding(isIP)),
|
||||||
|
},
|
||||||
|
"string": {
|
||||||
|
cel.Overload("ip_to_string", []*cel.Type{apiservercel.IPType}, cel.StringType,
|
||||||
|
cel.UnaryBinding(ipToString)),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ip) CompileOptions() []cel.EnvOption {
|
||||||
|
options := []cel.EnvOption{cel.Types(apiservercel.IPType),
|
||||||
|
cel.Variable(apiservercel.IPType.TypeName(), types.NewTypeTypeWithParam(apiservercel.IPType)),
|
||||||
|
}
|
||||||
|
for name, overloads := range ipLibraryDecls {
|
||||||
|
options = append(options, cel.Function(name, overloads...))
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*ip) ProgramOptions() []cel.ProgramOption {
|
||||||
|
return []cel.ProgramOption{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func stringToIP(arg ref.Val) ref.Val {
|
||||||
|
s, ok := arg.Value().(string)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
addr, err := parseIPAddr(s)
|
||||||
|
if err != nil {
|
||||||
|
// Don't add context, we control the error message already.
|
||||||
|
return types.NewErr("%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return apiservercel.IP{
|
||||||
|
Addr: addr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ipToString(arg ref.Val) ref.Val {
|
||||||
|
ip, ok := arg.(apiservercel.IP)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.String(ip.Addr.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func family(arg ref.Val) ref.Val {
|
||||||
|
ip, ok := arg.(apiservercel.IP)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case ip.Addr.Is4():
|
||||||
|
return types.Int(4)
|
||||||
|
case ip.Addr.Is6():
|
||||||
|
return types.Int(6)
|
||||||
|
default:
|
||||||
|
return types.NewErr("IP address %q is not an IPv4 or IPv6 address", ip.Addr.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ipIsCanonical(arg ref.Val) ref.Val {
|
||||||
|
s, ok := arg.Value().(string)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
addr, err := parseIPAddr(s)
|
||||||
|
if err != nil {
|
||||||
|
// Don't add context, we control the error message already.
|
||||||
|
return types.NewErr("%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Addr.String() always returns the canonical form of the IP address.
|
||||||
|
// Therefore comparing this with the original string representation
|
||||||
|
// will tell us if the IP address is in its canonical form.
|
||||||
|
return types.Bool(addr.String() == s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isIP(arg ref.Val) ref.Val {
|
||||||
|
s, ok := arg.Value().(string)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := parseIPAddr(s)
|
||||||
|
return types.Bool(err == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseIPAddr parses a string into an IP address.
|
||||||
|
// We use this function to parse IP addresses in the CEL library
|
||||||
|
// so that we can share the common logic of rejecting IP addresses
|
||||||
|
// that contain zones or are IPv4-mapped IPv6 addresses.
|
||||||
|
func parseIPAddr(raw string) (netip.Addr, error) {
|
||||||
|
addr, err := netip.ParseAddr(raw)
|
||||||
|
if err != nil {
|
||||||
|
return netip.Addr{}, fmt.Errorf("IP Address %q parse error during conversion from string: %v", raw, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if addr.Zone() != "" {
|
||||||
|
return netip.Addr{}, fmt.Errorf("IP address %q with zone value is not allowed", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
if addr.Is4In6() {
|
||||||
|
return netip.Addr{}, fmt.Errorf("IPv4-mapped IPv6 address %q is not allowed", raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
return addr, nil
|
||||||
|
}
|
@ -26,7 +26,7 @@ import (
|
|||||||
|
|
||||||
func TestLibraryCompatibility(t *testing.T) {
|
func TestLibraryCompatibility(t *testing.T) {
|
||||||
var libs []map[string][]cel.FunctionOpt
|
var libs []map[string][]cel.FunctionOpt
|
||||||
libs = append(libs, authzLibraryDecls, listsLibraryDecls, regexLibraryDecls, urlLibraryDecls, quantityLibraryDecls)
|
libs = append(libs, authzLibraryDecls, listsLibraryDecls, regexLibraryDecls, urlLibraryDecls, quantityLibraryDecls, ipLibraryDecls)
|
||||||
functionNames := sets.New[string]()
|
functionNames := sets.New[string]()
|
||||||
for _, lib := range libs {
|
for _, lib := range libs {
|
||||||
for name := range lib {
|
for name := range lib {
|
||||||
@ -47,6 +47,8 @@ func TestLibraryCompatibility(t *testing.T) {
|
|||||||
"errored", "error",
|
"errored", "error",
|
||||||
// Kubernetes <1.29>:
|
// Kubernetes <1.29>:
|
||||||
"add", "asApproximateFloat", "asInteger", "compareTo", "isGreaterThan", "isInteger", "isLessThan", "isQuantity", "quantity", "sign", "sub",
|
"add", "asApproximateFloat", "asInteger", "compareTo", "isGreaterThan", "isInteger", "isLessThan", "isQuantity", "quantity", "sign", "sub",
|
||||||
|
// Kubernetes <1.30>:
|
||||||
|
"ip", "family", "ip.isCanonical", "isIP", "string",
|
||||||
// Kubernetes <1.??>:
|
// Kubernetes <1.??>:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user