Skip to content

Commit

Permalink
feat: add cel envoy lib
Browse files Browse the repository at this point in the history
Signed-off-by: Charles-Edouard Brétéché <charles.edouard@nirmata.com>
  • Loading branch information
eddycharly committed Oct 28, 2024
1 parent a993b5f commit 0eeb747
Show file tree
Hide file tree
Showing 4 changed files with 274 additions and 5 deletions.
11 changes: 6 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
14 changes: 14 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -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=
Expand All @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand All @@ -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=
Expand Down
220 changes: 220 additions & 0 deletions pkg/authz/cel.go
Original file line number Diff line number Diff line change
@@ -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
}
34 changes: 34 additions & 0 deletions pkg/authz/cel_test.go
Original file line number Diff line number Diff line change
@@ -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)
}

0 comments on commit 0eeb747

Please sign in to comment.