diff --git a/go.mod b/go.mod index ef5ebeb..7ed9122 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.22.8 require ( github.com/envoyproxy/go-control-plane v0.13.1 + github.com/google/cel-go v0.17.7 github.com/jmespath-community/go-jmespath v1.1.2-0.20240117150817-e430401a2172 github.com/kyverno/kyverno-json v0.0.3-alpha.2 github.com/spf13/cobra v1.8.1 @@ -15,18 +16,15 @@ require ( k8s.io/apimachinery v0.31.2 ) -require ( - github.com/fxamacker/cbor/v2 v2.7.0 // indirect - github.com/x448/float16 v0.8.4 // indirect -) - require ( github.com/IGLOU-EU/go-wildcard v1.0.3 // indirect + github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df // indirect github.com/aquilax/truncate v1.0.0 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cncf/xds/go v0.0.0-20240723142845-024c85f92f20 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/envoyproxy/protoc-gen-validate v1.1.0 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt v3.2.2+incompatible @@ -42,12 +40,15 @@ require ( github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stoewer/go-strcase v1.3.0 // indirect + github.com/x448/float16 v0.8.4 // indirect github.com/zach-klippenstein/goregen v0.0.0-20160303162051-795b5e3961ea // indirect golang.org/x/crypto v0.26.0 // indirect golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect golang.org/x/net v0.28.0 // indirect golang.org/x/sys v0.24.0 // indirect golang.org/x/text v0.17.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 // indirect google.golang.org/protobuf v1.35.1 gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect diff --git a/go.sum b/go.sum index a64ba8a..4ce6a2b 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/IGLOU-EU/go-wildcard v1.0.3 h1:r8T46+8/9V1STciXJomTWRpPEv4nGJATDbJkdU0Nou0= github.com/IGLOU-EU/go-wildcard v1.0.3/go.mod h1:/qeV4QLmydCbwH0UMQJmXDryrFKJknWi/jjO8IiuQfY= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df h1:7RFfzj4SSt6nnvCPbCqijJi1nWCd+TqAT3bYCStRC18= +github.com/antlr/antlr4/runtime/Go/antlr/v4 v4.0.0-20230305170008-8188dc5388df/go.mod h1:pSwJ0fSY5KhvocuWSx4fz3BA8OrA1bQn+K1Eli3BRwM= github.com/aquilax/truncate v1.0.0 h1:UgIGS8U/aZ4JyOJ2h3xcF5cSQ06+gGBnjxH2RUHJe0U= github.com/aquilax/truncate v1.0.0/go.mod h1:BeMESIDMlvlS3bmg4BVvBbbZUNwWtS8uzYPAKXwwhLw= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= @@ -25,6 +27,8 @@ github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= +github.com/google/cel-go v0.17.7 h1:6ebJFzu1xO2n7TLtN+UBqShGBhlD85bhvglh5DpcfqQ= +github.com/google/cel-go v0.17.7/go.mod h1:HXZKzB0LXqer5lHHgfWAnlYwJaQBDKMjxjulNQzhwhY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -78,8 +82,15 @@ github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs= +github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= @@ -127,6 +138,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gomodules.xyz/jsonpatch/v2 v2.4.0 h1:Ci3iUJyx9UeRx7CeFN8ARgGbkESwJK+KB9lLcWxY/Zw= gomodules.xyz/jsonpatch/v2 v2.4.0/go.mod h1:AH3dM2RI6uoBZxn3LVrfvJ3E0/9dG4cSrbuBJT4moAY= +google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142 h1:wKguEg1hsxI2/L3hUYrpo1RVi48K+uTyzKqprwLXsb8= +google.golang.org/genproto/googleapis/api v0.0.0-20240814211410-ddb44dafa142/go.mod h1:d6be+8HhtEtucleCbxpPW9PA9XwISACu8nvpPqF0BVo= google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142 h1:e7S5W7MGGLaSu8j3YjdezkZ+m1/Nm0uRVRMEMGk26Xs= google.golang.org/genproto/googleapis/rpc v0.0.0-20240814211410-ddb44dafa142/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E= @@ -141,6 +154,7 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= diff --git a/pkg/authz/cel.go b/pkg/authz/cel.go new file mode 100644 index 0000000..7803db3 --- /dev/null +++ b/pkg/authz/cel.go @@ -0,0 +1,220 @@ +package authz + +import ( + "reflect" + + corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + authv3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" + typesv3 "github.com/envoyproxy/go-control-plane/envoy/type/v3" + "github.com/google/cel-go/cel" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" + "google.golang.org/protobuf/types/known/structpb" +) + +func celEnv() (*cel.Env, error) { + base, err := cel.NewEnv(cel.Types((*authv3.CheckRequest)(nil), (*authv3.CheckResponse)(nil))) + if err != nil { + return nil, err + } + return base.Extend(cel.Lib(EnvoyLib(base.CELTypeAdapter()))) +} + +func EnvoyLib(adapter types.Adapter) cel.Library { + return &envoylib{ + adapter: adapter, + } +} + +type envoylib struct { + adapter types.Adapter +} + +func (*envoylib) LibraryName() string { + return "kyverno.envoy" +} + +func (c *envoylib) CompileOptions() []cel.EnvOption { + _response := types.NewObjectType("envoy.service.auth.v3.CheckResponse") + _ok := types.NewObjectType("envoy.service.auth.v3.OkHttpResponse") + _denied := types.NewObjectType("envoy.service.auth.v3.DeniedHttpResponse") + _struct := types.NewObjectType("google.protobuf.Struct") + _header := types.NewObjectType("envoy.config.core.v3.HeaderValueOption") + var libraryDecls = map[string][]cel.FunctionOpt{ + "envoy.Allowed": { + cel.Overload("allowed", []*cel.Type{}, _ok, cel.FunctionBinding(func(values ...ref.Val) ref.Val { return c.allowed() })), + }, + "envoy.Denied": { + cel.Overload("denied", []*cel.Type{types.IntType}, _denied, cel.UnaryBinding(c.denied)), + }, + "envoy.Response": { + cel.Overload("response_ok", []*cel.Type{_ok}, _response, cel.UnaryBinding(c.response_ok)), + cel.Overload("response_denied", []*cel.Type{_denied}, _response, cel.UnaryBinding(c.response_denied)), + }, + "envoy.Header": { + cel.Overload("header_key_value", []*cel.Type{types.StringType, types.StringType}, _header, cel.BinaryBinding(c.header_key_value)), + }, + "WithBody": { + cel.MemberOverload("denied_with_body", []*cel.Type{_denied, types.StringType}, _denied, cel.BinaryBinding(c.denied_with_body)), + }, + "WithHeader": { + cel.MemberOverload("ok_with_header", []*cel.Type{_ok, _header}, _ok, cel.BinaryBinding(c.ok_with_header)), + cel.MemberOverload("denied_with_header", []*cel.Type{_denied, _header}, _denied, cel.BinaryBinding(c.denied_with_header)), + }, + "WithoutHeader": { + cel.MemberOverload("ok_without_header", []*cel.Type{_ok, types.StringType}, _ok, cel.BinaryBinding(c.ok_without_header)), + }, + "WithResponseHeader": { + cel.MemberOverload("ok_with_response_header", []*cel.Type{_ok, _header}, _ok, cel.BinaryBinding(c.ok_with_response_header)), + }, + "KeepEmptyValue": { + cel.MemberOverload("header_keep_empty_value", []*cel.Type{_header}, _header, cel.UnaryBinding(c.header_keep_empty_value)), + cel.MemberOverload("header_keep_empty_value_bool", []*cel.Type{_header, types.BoolType}, _header, cel.BinaryBinding(c.header_keep_empty_value_bool)), + }, + "Response": { + cel.MemberOverload("ok_response", []*cel.Type{_ok}, _response, cel.UnaryBinding(c.response_ok)), + cel.MemberOverload("denied_response", []*cel.Type{_denied}, _response, cel.UnaryBinding(c.response_denied)), + }, + "WithMetadata": { + cel.MemberOverload("response_with_metadata", []*cel.Type{_response, _struct}, _ok, cel.BinaryBinding(c.response_with_metadata)), + }, + } + options := []cel.EnvOption{} + for name, overloads := range libraryDecls { + options = append(options, cel.Function(name, overloads...)) + } + return options +} + +func (_ *envoylib) ProgramOptions() []cel.ProgramOption { + return []cel.ProgramOption{} +} + +func (c *envoylib) allowed() ref.Val { + r := &authv3.OkHttpResponse{} + return c.adapter.NativeToValue(r) +} + +func (c *envoylib) ok_with_header(ok ref.Val, header ref.Val) ref.Val { + if ok, err := convertToNative[*authv3.OkHttpResponse](ok); err != nil { + return types.WrapErr(err) + } else if header, err := convertToNative[*corev3.HeaderValueOption](header); err != nil { + return types.WrapErr(err) + } else { + ok.Headers = append(ok.Headers, header) + return c.adapter.NativeToValue(ok) + } +} + +func (c *envoylib) ok_without_header(ok ref.Val, header ref.Val) ref.Val { + if ok, err := convertToNative[*authv3.OkHttpResponse](ok); err != nil { + return types.WrapErr(err) + } else if header, err := convertToNative[string](header); err != nil { + return types.WrapErr(err) + } else { + ok.HeadersToRemove = append(ok.HeadersToRemove, header) + return c.adapter.NativeToValue(ok) + } +} + +func (c *envoylib) ok_with_response_header(ok ref.Val, header ref.Val) ref.Val { + if ok, err := convertToNative[*authv3.OkHttpResponse](ok); err != nil { + return types.WrapErr(err) + } else if header, err := convertToNative[*corev3.HeaderValueOption](header); err != nil { + return types.WrapErr(err) + } else { + ok.ResponseHeadersToAdd = append(ok.ResponseHeadersToAdd, header) + return c.adapter.NativeToValue(ok) + } +} + +func (c *envoylib) denied(code ref.Val) ref.Val { + if code, err := convertToNative[typesv3.StatusCode](code); err != nil { + return types.WrapErr(err) + } else { + return c.adapter.NativeToValue(&authv3.DeniedHttpResponse{Status: &typesv3.HttpStatus{Code: code}}) + } +} + +func (c *envoylib) denied_with_body(denied ref.Val, body ref.Val) ref.Val { + if denied, err := convertToNative[*authv3.DeniedHttpResponse](denied); err != nil { + return types.WrapErr(err) + } else if body, err := convertToNative[string](body); err != nil { + return types.WrapErr(err) + } else { + denied.Body = body + return c.adapter.NativeToValue(denied) + } +} + +func (c *envoylib) denied_with_header(denied ref.Val, header ref.Val) ref.Val { + if denied, err := convertToNative[*authv3.DeniedHttpResponse](denied); err != nil { + return types.WrapErr(err) + } else if header, err := convertToNative[*corev3.HeaderValueOption](header); err != nil { + return types.WrapErr(err) + } else { + denied.Headers = append(denied.Headers, header) + return c.adapter.NativeToValue(denied) + } +} + +func (c *envoylib) header_key_value(key ref.Val, value ref.Val) ref.Val { + if key, err := convertToNative[string](key); err != nil { + return types.WrapErr(err) + } else if value, err := convertToNative[string](value); err != nil { + return types.WrapErr(err) + } else { + return c.adapter.NativeToValue(&corev3.HeaderValueOption{Header: &corev3.HeaderValue{Key: key, Value: value}}) + } +} + +func (c *envoylib) header_keep_empty_value(header ref.Val) ref.Val { + return c.header_keep_empty_value_bool(header, types.True) +} + +func (c *envoylib) header_keep_empty_value_bool(header ref.Val, flag ref.Val) ref.Val { + if header, err := convertToNative[*corev3.HeaderValueOption](header); err != nil { + return types.WrapErr(err) + } else if flag, err := convertToNative[bool](flag); err != nil { + return types.WrapErr(err) + } else { + header.KeepEmptyValue = flag + return c.adapter.NativeToValue(header) + } +} + +func (c *envoylib) response_ok(ok ref.Val) ref.Val { + if ok, err := convertToNative[*authv3.OkHttpResponse](ok); err != nil { + return types.WrapErr(err) + } else { + return c.adapter.NativeToValue(&authv3.CheckResponse{HttpResponse: &authv3.CheckResponse_OkResponse{OkResponse: ok}}) + } +} + +func (c *envoylib) response_denied(denied ref.Val) ref.Val { + if denied, err := convertToNative[*authv3.DeniedHttpResponse](denied); err != nil { + return types.WrapErr(err) + } else { + return c.adapter.NativeToValue(&authv3.CheckResponse{HttpResponse: &authv3.CheckResponse_DeniedResponse{DeniedResponse: denied}}) + } +} + +func (c *envoylib) response_with_metadata(response ref.Val, metadata ref.Val) ref.Val { + if response, err := convertToNative[*authv3.CheckResponse](response); err != nil { + return types.WrapErr(err) + } else if metadata, err := convertToNative[*structpb.Struct](metadata); err != nil { + return types.WrapErr(err) + } else { + response.DynamicMetadata = metadata + return c.adapter.NativeToValue(response) + } +} + +func convertToNative[T any](value ref.Val) (T, error) { + response, err := value.ConvertToNative(reflect.TypeFor[T]()) + if err != nil { + var t T + return t, err + } + return response.(T), nil +} diff --git a/pkg/authz/cel_test.go b/pkg/authz/cel_test.go new file mode 100644 index 0000000..d1b27b3 --- /dev/null +++ b/pkg/authz/cel_test.go @@ -0,0 +1,34 @@ +package authz + +import ( + "reflect" + "testing" + + authv3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" + "github.com/google/cel-go/interpreter" + "github.com/stretchr/testify/assert" +) + +func Test_celEnv(t *testing.T) { + source := ` +envoy + .Denied(401) + .WithBody("Authentication Failed") + .WithHeader(envoy.Header("foo", "bar").KeepEmptyValue()) + .Response() + .WithMetadata({"my-new-metadata": "my-new-value"}) +` + env, err := celEnv() + assert.NoError(t, err) + ast, issues := env.Compile(source) + assert.Nil(t, issues) + prog, err := env.Program(ast) + assert.NoError(t, err) + assert.NotNil(t, prog) + out, _, err := prog.Eval(interpreter.EmptyActivation()) + assert.NoError(t, err) + assert.NotNil(t, out) + a, err := out.ConvertToNative(reflect.TypeFor[*authv3.CheckResponse]()) + assert.NoError(t, err) + assert.NotNil(t, a) +}