diff --git a/api/v1beta2/auth_config_types.go b/api/v1beta2/auth_config_types.go index 098ceb05..147f8ae5 100644 --- a/api/v1beta2/auth_config_types.go +++ b/api/v1beta2/auth_config_types.go @@ -51,6 +51,7 @@ const ( PlainAuthResponse JsonAuthResponse WristbandAuthResponse + CelAuthResponse // The following constants are used to identify the different methods of callback functions. UnknownCallbackMethod CallbackMethod = iota @@ -733,6 +734,8 @@ func (s *SuccessResponseSpec) GetMethod() AuthResponseMethod { type AuthResponseMethodSpec struct { // Plain text content Plain *PlainAuthResponseSpec `json:"plain,omitempty"` + // Cel Expression, where the result is outputted as JSON + Expression string `json:"expression,omitempty"` // JSON object // Specify it as the list of properties of the object, whose values can combine static values and values selected from the authorization JSON. Json *JsonAuthResponseSpec `json:"json,omitempty"` diff --git a/controllers/auth_config_controller.go b/controllers/auth_config_controller.go index cca5ff1f..1cae2acf 100644 --- a/controllers/auth_config_controller.go +++ b/controllers/auth_config_controller.go @@ -628,6 +628,13 @@ func injectResponseConfig(ctx context.Context, authConfig *api.AuthConfig, succe translatedResponse.DynamicJSON = response_evaluators.NewDynamicJSONResponse(jsonProperties) + case api.CelAuthResponse: + if exp, err := response_evaluators.NewDynamicCelResponse(string(*successResponse.Expression)); err != nil { + return err + } else { + translatedResponse.DynamicCEL = exp + } + // plain case api.PlainAuthResponse: translatedResponse.Plain = &response_evaluators.Plain{ diff --git a/go.mod b/go.mod index 14024eeb..39680364 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/gogo/googleapis v1.4.0 github.com/golang-jwt/jwt v3.2.2+incompatible github.com/golang/mock v1.6.0 + github.com/google/cel-go v0.21.0 github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 github.com/hashicorp/go-multierror v1.1.1 github.com/open-policy-agent/opa v0.68.0 @@ -43,6 +44,7 @@ require ( require ( cloud.google.com/go/compute/metadata v0.3.0 // indirect + github.com/antlr4-go/antlr/v4 v4.13.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -59,6 +61,7 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect + github.com/stoewer/go-strcase v1.2.0 // indirect github.com/tchap/go-patricia/v2 v2.3.1 // indirect go.opentelemetry.io/otel/metric v1.28.0 // indirect go.opentelemetry.io/proto/otlp v1.3.1 // indirect diff --git a/go.sum b/go.sum index 784a7ad9..d9ef5732 100644 --- a/go.sum +++ b/go.sum @@ -25,6 +25,8 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/allegro/bigcache/v2 v2.2.5 h1:mRc8r6GQjuJsmSKQNPsR5jQVXc8IJ1xsW5YXUYMLfqI= github.com/allegro/bigcache/v2 v2.2.5/go.mod h1:FppZsIO+IZk7gCuj5FiIDHGygD9xvWQcqg1uIPMb6tY= +github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= +github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= @@ -226,6 +228,8 @@ github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/cel-go v0.21.0 h1:cl6uW/gxN+Hy50tNYvI691+sXxioCnstFzLp2WO4GCI= +github.com/google/cel-go v0.21.0/go.mod h1:rHUlWCcBKgyEk+eV03RPdZUekPp6YcJwV0FxuUksYxc= github.com/google/flatbuffers v1.12.1 h1:MVlul7pQNoDzWRLTw5imwYsl+usrS1TXG2H4jg6ImGw= github.com/google/flatbuffers v1.12.1/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I= @@ -506,6 +510,8 @@ github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzu github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 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.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= +github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= @@ -517,6 +523,7 @@ github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRci github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= diff --git a/pkg/evaluators/response.go b/pkg/evaluators/response.go index 6ee4b156..f96dca01 100644 --- a/pkg/evaluators/response.go +++ b/pkg/evaluators/response.go @@ -53,6 +53,7 @@ type ResponseConfig struct { Cache EvaluatorCache Wristband auth.WristbandIssuer `yaml:"wristband,omitempty"` + DynamicCEL *response.DynamicCEL `yaml:"json,omitempty"` DynamicJSON *response.DynamicJSON `yaml:"json,omitempty"` Plain *response.Plain `yaml:"plain,omitempty"` } diff --git a/pkg/evaluators/response/dynamic_cel.go b/pkg/evaluators/response/dynamic_cel.go new file mode 100644 index 00000000..e4be22dc --- /dev/null +++ b/pkg/evaluators/response/dynamic_cel.go @@ -0,0 +1,90 @@ +package response + +import ( + "context" + "reflect" + "strings" + + "github.com/golang/protobuf/jsonpb" + "github.com/google/cel-go/cel" + "github.com/google/cel-go/checker/decls" + "github.com/google/cel-go/common/types/ref" + "github.com/kuadrant/authorino/pkg/auth" + "google.golang.org/protobuf/types/known/structpb" + + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" +) + +const rootBinding = "auth" + +func NewDynamicCelResponse(expression string) (*DynamicCEL, error) { + + cel_exp := DynamicCEL{} + + env, err := cel.NewEnv(cel.Declarations( + decls.NewConst(rootBinding, decls.NewObjectType("google.protobuf.Struct"), nil), + )) + if err != nil { + return nil, err + } + + ast, issues := env.Parse(expression) + if issues.Err() != nil { + return nil, issues.Err() + } + + checked, issues := env.Check(ast) + if issues.Err() != nil { + return nil, issues.Err() + } + + program, err := env.Program(checked) + if err != nil { + return nil, err + } + + cel_exp.program = program + + return &cel_exp, nil +} + +type DynamicCEL struct { + program cel.Program +} + +func (c *DynamicCEL) Call(pipeline auth.AuthPipeline, ctx context.Context) (interface{}, error) { + + auth_json := pipeline.GetAuthorizationJSON() + data := structpb.Struct{} + if err := jsonpb.Unmarshal(strings.NewReader(auth_json), &data); err != nil { + return nil, err + } + + value := data.GetFields()["auth"] + result, _, err := c.program.Eval(map[string]interface{}{ + rootBinding: value, + }) + if err != nil { + return nil, err + } + + if jsonVal, err := valueToJSON(result); err != nil { + return nil, err + } else { + return jsonVal, nil + } +} + +func valueToJSON(val ref.Val) (string, error) { + v, err := val.ConvertToNative(reflect.TypeOf(&structpb.Value{})) + if err != nil { + return "", err + } + marshaller := protojson.MarshalOptions{Multiline: false} + bytes, err := marshaller.Marshal(v.(proto.Message)) + if err != nil { + return "", err + } + return string(bytes), nil +} diff --git a/pkg/evaluators/response/dynamic_cel_test.go b/pkg/evaluators/response/dynamic_cel_test.go new file mode 100644 index 00000000..1089d444 --- /dev/null +++ b/pkg/evaluators/response/dynamic_cel_test.go @@ -0,0 +1,35 @@ +package response + +import ( + "context" + "encoding/json" + "testing" + + mock_auth "github.com/kuadrant/authorino/pkg/auth/mocks" + "gotest.tools/assert" + + "github.com/golang/mock/gomock" +) + +func TestDynamicCELCall(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + celResponseEvaluator, _ := NewDynamicCelResponse(`{"prop1": "value1", "prop2": auth.identity.username}`) + + pipelineMock := mock_auth.NewMockAuthPipeline(ctrl) + pipelineMock.EXPECT().GetAuthorizationJSON().Return(`{"auth":{"identity":{"username":"john","evil": false}}}`) + + response, err := celResponseEvaluator.Call(pipelineMock, context.TODO()) + assert.NilError(t, err) + + // We need to parse this response: https://protobuf.dev/reference/go/faq/#unstable-json + result := struct { + Prop1 string `json:"prop1"` + Prop2 string `json:"prop2"` + }{} + assert.NilError(t, json.Unmarshal([]byte(response.(string)), &result)) + + assert.Equal(t, result.Prop1, "value1") + assert.Equal(t, result.Prop2, "john") +} diff --git a/pkg/evaluators/response/dynamic_json_test.go b/pkg/evaluators/response/dynamic_json_test.go index 47d48b93..417f9adb 100644 --- a/pkg/evaluators/response/dynamic_json_test.go +++ b/pkg/evaluators/response/dynamic_json_test.go @@ -19,6 +19,10 @@ func TestDynamicJSONCall(t *testing.T) { jsonProperties := []json.JSONProperty{ {Name: "prop1", Value: json.JSONValue{Static: "value1"}}, {Name: "prop2", Value: json.JSONValue{Pattern: "auth.identity.username"}}, + {Name: "prop2", Value: json.JSONValue{Pattern: "auth.identity.username"}}, + {Name: "prop2", Value: json.JSONValue{Pattern: "auth.identity.username"}}, + {Name: "prop2", Value: json.JSONValue{Pattern: "auth.identity.username"}}, + {Name: "prop2", Value: json.JSONValue{Pattern: "auth.identity.username"}}, } jsonResponseEvaluator := NewDynamicJSONResponse(jsonProperties)