-
Notifications
You must be signed in to change notification settings - Fork 32
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
CEL Support #495
CEL Support #495
Conversation
fc65fbc
to
dc50681
Compare
00949f1
to
7f17d95
Compare
install/rbac/role.yaml
Outdated
- apiGroups: | ||
- operator.authorino.kuadrant.io | ||
resources: | ||
- authorinos |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's something wrong going on here. It's somehow bringing to the operand permissions that only belong to the operator.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah! Again... remember it happened to me already when I refactored the controller to v1beta2
🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you try and run it on your machine and push the changes to this branch please? I have no idea what's screwed up on my end 🤷
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. 28e7fb9
❯ git st
On branch cel
Your branch is up to date with 'origin/cel'.
nothing to commit, working tree clean
❯ make generate manifests
go mod tidy
go: downloading github.com/envoyproxy/go-control-plane v0.12.1-0.20240621013728-1eb8caab5155
go: downloading github.com/prometheus/client_golang v1.20.2
go: downloading github.com/open-policy-agent/opa v0.68.0
go: downloading google.golang.org/grpc v1.66.0
go: downloading go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0
go: downloading github.com/google/cel-go v0.21.0
go: downloading google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094
go: downloading github.com/klauspost/compress v1.17.9
go: downloading golang.org/x/time v0.6.0
go: downloading github.com/stoewer/go-strcase v1.2.0
go: downloading cloud.google.com/go/compute/metadata v0.3.0
go: downloading github.com/golang/glog v1.2.1
go: downloading golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d
go: downloading cloud.google.com/go/compute v1.23.0
go: downloading cloud.google.com/go v0.34.0
go mod vendor
controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
/Library/Developer/CommandLineTools/usr/bin/make fmt vet
go fmt ./...
go vet ./...
controller-gen crd:crdVersions=v1 rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=install/crd output:rbac:artifacts:config=install/rbac && /Users/guilherme.cassolato/Workspace/go/src/github.com/kuadrant/authorino/bin/kustomize build install > /Users/guilherme.cassolato/Workspace/go/src/github.com/kuadrant/authorino/install/manifests.yaml
/Library/Developer/CommandLineTools/usr/bin/make patch-webhook
envsubst \
< /Users/guilherme.cassolato/Workspace/go/src/github.com/kuadrant/authorino/install/manifests.yaml \
> /Users/guilherme.cassolato/Workspace/go/src/github.com/kuadrant/authorino/install/manifests.yaml.tmp && \
mv /Users/guilherme.cassolato/Workspace/go/src/github.com/kuadrant/authorino/install/manifests.yaml.tmp /Users/guilherme.cassolato/Workspace/go/src/github.com/kuadrant/authorino/install/manifests.yaml
❯ git st
On branch cel
Your branch is up to date with 'origin/cel'.
Changes not staged for commit:
(use "git add <file>..." to update what will be committed)
(use "git restore <file>..." to discard changes in working directory)
modified: install/manifests.yaml
modified: install/rbac/role.yaml
no changes added to commit (use "git add" and/or "git commit -a")
❯ grep "controller-gen@" Makefile
$(call go-get-tool,$(CONTROLLER_GEN),sigs.k8s.io/controller-tools/cmd/[email protected])
❯ ./bin/controller-gen --version
Version: v0.15.0
🤦 This should in
@guicassolato am I missing something else? |
return fmt.Sprintf("%s", value.ResolveFor(authJSON)) | ||
jsonValueToStr := func(value expressions.Value) (string, error) { | ||
if value == nil { | ||
return "", nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ugh... Now that it is a pointer, we could get nil
indeed... I don't know whether all these cases are now covered, hard to tell what can or not be nil
in those specs
901996d
to
067f83d
Compare
api/v1beta1/zz_generated.deepcopy.go
Outdated
@@ -1,5 +1,4 @@ | |||
//go:build !ignore_autogenerated | |||
// +build !ignore_autogenerated |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The // +build
syntax has been deprecated since Go v1.17.
https://go.googlesource.com/proposal/+/master/design/draft-gobuild.md
api/v1beta3/auth_config_types.go
Outdated
// AuthConfig is the schema for Authorino's AuthConfig API | ||
// +kubebuilder:object:root=true | ||
// +kubebuilder:subresource:status | ||
// +kubebuilder:storageversion |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Shall we remove it from v1beta2
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One and only one version must be marked as the storage version.
package v1beta3 | ||
|
||
// Hub marks this version as a conversion hub. | ||
func (a *AuthConfig) Hub() {} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't know for sure how this works, but can we have 2 versions marked as conversion hub? I believe right now v1beta2
is also marked as hub, right?
dc2777a
to
a25343f
Compare
Signed-off-by: Alex Snaps <[email protected]>
Signed-off-by: Alex Snaps <[email protected]>
Signed-off-by: Alex Snaps <[email protected]>
Signed-off-by: Alex Snaps <[email protected]>
Signed-off-by: Alex Snaps <[email protected]>
Signed-off-by: Alex Snaps <[email protected]>
Signed-off-by: Alex Snaps <[email protected]>
Signed-off-by: Alex Snaps <[email protected]>
Signed-off-by: Alex Snaps <[email protected]>
Signed-off-by: Alex Snaps <[email protected]>
Signed-off-by: Alex Snaps <[email protected]>
Signed-off-by: Alex Snaps <[email protected]>
Signed-off-by: Alex Snaps <[email protected]>
Signed-off-by: Alex Snaps <[email protected]>
Signed-off-by: Alex Snaps <[email protected]>
e2e tests now pass as well... 🎉 |
This below would let us get rid of all the remainder selector et al stuff: diff --git a/pkg/expressions/cel/expressions.go b/pkg/expressions/cel/expressions.go
index 18ba5728..40fd95b9 100644
--- a/pkg/expressions/cel/expressions.go
+++ b/pkg/expressions/cel/expressions.go
@@ -9,6 +9,7 @@ import (
"github.com/google/cel-go/cel"
"github.com/google/cel-go/checker/decls"
"github.com/google/cel-go/common/types/ref"
+ "github.com/google/cel-go/ext"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/structpb"
@@ -130,6 +131,7 @@ func Compile(expression string, expectedType *cel.Type, opts ...cel.EnvOption) (
decls.NewConst(RootDestinationBinding, decls.NewObjectType("google.protobuf.Struct"), nil),
decls.NewConst(RootAuthBinding, decls.NewObjectType("google.protobuf.Struct"), nil),
)}, opts...)
+ envOpts = append(envOpts, ext.Strings())
env, env_err := cel.NewEnv(envOpts...)
if env_err != nil {
return nil, env_err
diff --git a/pkg/expressions/cel/expressions_test.go b/pkg/expressions/cel/expressions_test.go
index 587ac4cd..6154d020 100644
--- a/pkg/expressions/cel/expressions_test.go
+++ b/pkg/expressions/cel/expressions_test.go
@@ -42,3 +42,14 @@ func TestPredicate(t *testing.T) {
assert.NilError(t, err)
assert.Equal(t, response, true)
}
+
+func TestExpression(t *testing.T) {
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ expression, err := NewStringExpression(`"hello hello".replace("", "_")`)
+ assert.NilError(t, err)
+ resolveFor, err := expression.ResolveFor("{}")
+ assert.NilError(t, err)
+ assert.Equal(t, resolveFor, "_h_e_l_l_o_ _h_e_l_l_o_")
+}
diff --git a/tests/v1beta3/authconfig.yaml b/tests/v1beta3/authconfig.yaml
index 509b93fe..554f2f62 100644
--- a/tests/v1beta3/authconfig.yaml
+++ b/tests/v1beta3/authconfig.yaml
@@ -77,10 +77,10 @@ spec:
Accept:
value: application/json
method: GET
- url: http://ip-location.authorino.svc.cluster.local:3000/{context.request.http.headers.x-forwarded-for.@extract:{"sep":","}}
+ urlExpression: "http://ip-location.authorino.svc.cluster.local:3000/" + request.headers["x-forwarded-for"].split(",")[0]
cache:
key:
- selector: request.http.headers.x-forwarded-for.@extract:{"sep":","}
+ expresssion: request.headers["x-forwarded-for"].split(",")[0]
user-info:
userInfo:
identitySource: keycloak
@@ -179,7 +179,7 @@ spec:
uri:
expression: request.path
scope:
- selector: request.http.method.@case:lower
+ expression: request.http.method.lowerAscii()
signingKeyRefs:
- name: wristband-signing-key
algorithm: ES256 |
func stringValueFrom(user *api.ValueOrSelector) (expressions.Value, error) { | ||
var strValue expressions.Value | ||
var err error | ||
if user.Expression != "" { | ||
if strValue, err = cel.NewStringExpression(string(user.Expression)); err != nil { | ||
return nil, err | ||
} | ||
} else { | ||
strValue = &json.JSONValue{Static: user.Value, Pattern: user.Selector} | ||
} | ||
return strValue, nil | ||
} | ||
|
||
func valueFrom(user *api.ValueOrSelector) (expressions.Value, error) { | ||
var strValue expressions.Value | ||
var err error | ||
if user.Expression != "" { | ||
if strValue, err = cel.NewStringExpression(string(user.Expression)); err != nil { | ||
return nil, err | ||
} | ||
} else { | ||
strValue = &json.JSONValue{Static: user.Value, Pattern: user.Selector} | ||
} | ||
return strValue, nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any difference between these two funcs?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typo! This the latter should create a NewExpression
! Tho I wonder if we should keep that one altogether... (by the current state of things, I'd say no!)
@@ -272,7 +284,15 @@ func (r *AuthConfigReconciler) translateAuthConfig(ctx context.Context, authConf | |||
} | |||
|
|||
case api.PlainIdentityAuthentication: | |||
translatedIdentity.Plain = &identity_evaluators.Plain{Pattern: identity.Plain.Selector} | |||
if identity.Plain.Expression != "" { | |||
expression, err := cel.NewStringExpression(string(identity.Plain.Expression)) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if understand NewStringExpression
here. Does this mean plain.expression
will always resolve to a string?
With JSONValue
, I think plain.selector
can resolve to any type, including returning complex objects to set auth.identity
, no?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, what would be plain.selector
value's type for auth.identity
if one would be to select that? In Golang's type system that is...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
from gjson
func (t Result) Value() interface{} {
if t.Type == String {
return t.Str
}
switch t.Type {
default:
return nil
case False:
return false
case Number:
return t.Num
case JSON:
r := t.arrayOrMap(0, true)
if r.vc == '{' {
return r.oi
} else if r.vc == '[' {
return r.ai
}
return nil
case True:
return true
}
}
So a map[string]interface{}
, where interface
is one of the above recursively I guess...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One more question tho... what is that good for tho? I realize I took a shortcut here, and even left a note to make this yet even (according to me) better, but... we're just marshaling json back and forth, right? This only ever gets used thru AuthPipeline.GetAuthorizationJSON
, no? i.e. as JSON?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this is where the mismatch happens:
ResolveFor(jsonData string) (interface{}, error)
ResolveFor
is the thing that only ever get to interact with these payloads, is that correct? So we end up spending all our CPU cycles marshaling that data back and forth, are we not? Am I missing something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, what would be
plain.selector
value's type forauth.identity
if one would be to select that? In Golang's type system that is...
Not sure if I follow. plain.selector
is not for selecting from auth.identity
; it selects for setting the value of auth.identity
. What it selects can be of any type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So, what would be
plain.selector
value's type forauth.identity
if one would be to select that? In Golang's type system that is...Not sure if I follow.
plain.selector
is not for selecting fromauth.identity
; it selects for setting the value ofauth.identity
. What it selects can be of any type.
Not going to elaborate on this... as it's orthogonal and I found the answer, as per this answer. But the follow up questions remain. I mean I can go thru the whole process of remapping to golang native types... to only marshal them back into JSON literals later... but it makes no sense, other than something needing to use that in golang. Tho even then I'd argue it might be better to optimize for the most frequent data access, which seems to be JSON based of my scanning of the code base.
I do think that, if we ever 🔥 the gjson stuff, as per my comment in the code, ideally the pipeline stores all that data as protobuf... which would make it much lighter weight to inject it into the CEL interpreter.
So I can have the resulting value of a CEL expression be returned from ResolveFor
, be expressed in golang's type system, but I really think this isn't sensible... as, again afaict! but I could be missing something, nothing actually needs that, other than to re marshal it back into JSON...
Is my reasoning sensible? Am I missing something that renders it inapplicable to the problem space?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I think ResolveFor(jsonData string) (interface{}, error)
should probably not expect json in and golang out, but have a clear contract, i.e. json string
in & out or interface{}
in (tho probably the proper type in, not "any") & out... It's not really symmetrical. But not arguing to fix this as part of this PR or even now neither
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For future us... Not sensible for this PR I think
Thinking out loud further...
ResolveFor
should probably be ResolveFor(pipeline *AuthPipeline) (interface{}, error)
.
That now would let us control what the expression takes in as argument format. gjson stuff can call GetAuthorizationJSON()
, while CEL could bypass it go straight to the protobuf format. I'd still argue for storing things in protobuf tho... So in order to remain "compatible", the result would be... "undefined" which probably isn't good... 🤔
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let me show what I mean with an example...
Requisite:
- Add the
omitempty
suggested at CEL Support #495 (comment)
Run the service:
export LOG_LEVEL=debug; make cluster install run
Create a couple of AuthConfigs expected to behave exactly the same:
kubectl apply -f -<<EOF
apiVersion: authorino.kuadrant.io/v1beta3
kind: AuthConfig
metadata:
name: gjson
spec:
hosts: ["gjson"]
authentication:
"envoy-verified-jwt":
plain:
selector: metadata.filter_metadata.envoy\.filters\.http\.jwt_authn|verified_jwt
authorization:
"must_be_admin":
patternMatching:
patterns:
- selector: auth.identity.roles
operator: incl
value: admin
---
apiVersion: authorino.kuadrant.io/v1beta3
kind: AuthConfig
metadata:
name: cel
spec:
hosts: ["cel"]
authentication:
"envoy-verified-jwt":
plain:
expression: |
metadata.filter_metadata["envoy.filters.http.jwt_authn"].verified_jwt
authorization:
"must_be_admin":
patternMatching:
patterns:
- predicate: |
"admin" in auth.identity.roles
EOF
Send a request to the AuthConfig using plain.selector
:
grpcurl -plaintext -d @ localhost:50051 envoy.service.auth.v3.Authorization.Check <<EOF
{
"attributes": {
"request": {
"http": {
"host": "gjson"
}
},
"metadata_context": {
"filter_metadata": {
"envoy.filters.http.jwt_authn": {
"verified_jwt": {
"aud": "toystore",
"exp": 1730284979,
"iat": 1730371379,
"iss": "auth-server.io",
"sub": "55474041-c8c6-48bb-9398-34c2dbfd88b3",
"roles": ["admin"]
}
}
}
}
}
}
EOF
{
"status": {},
"okResponse": {},
"dynamicMetadata": {}
}
Now send a request to the AuthConfig using plain.expression
:
grpcurl -plaintext -d @ localhost:50051 envoy.service.auth.v3.Authorization.Check <<EOF
{
"attributes": {
"request": {
"http": {
"host": "cel"
}
},
"metadata_context": {
"filter_metadata": {
"envoy.filters.http.jwt_authn": {
"verified_jwt": {
"aud": "toystore",
"exp": 1730284979,
"iat": 1730371379,
"iss": "auth-server.io",
"sub": "55474041-c8c6-48bb-9398-34c2dbfd88b3",
"roles": ["admin"]
}
}
}
}
}
}
EOF
{
"status": {
"code": 7
},
"deniedResponse": {
"status": {
"code": "Forbidden"
},
"headers": [
{
"header": {
"key": "X-Ext-Auth-Reason",
"value": "no such key: roles"
}
}
]
}
}
If you check the logs, both requests succeed in the authentication, however...
The first request evaluates authorisation based on the following Authorization JSON:
{
"auth": {
"identity": {
"aud": "toystore",
"exp": 1730284979,
"iat": 1730371379,
"iss": "auth-server.io",
"roles": [
"admin"
],
"sub": "55474041-c8c6-48bb-9398-34c2dbfd88b3"
}
},
"context": {
"metadata_context": {
"filter_metadata": {
"envoy.filters.http.jwt_authn": {
"verified_jwt": {
"aud": "toystore",
"exp": 1730284979,
"iat": 1730371379,
"iss": "auth-server.io",
"roles": [
"admin"
],
"sub": "55474041-c8c6-48bb-9398-34c2dbfd88b3"
}
}
}
},
"request": {
"http": {
"host": "gjson",
"id": "b59827c5-ec62-422c-972c-b86f1d29ec52"
}
}
},
"destination": {},
"metadata": {
"filter_metadata": {
"envoy.filters.http.jwt_authn": {
"verified_jwt": {
"aud": "toystore",
"exp": 1730284979,
"iat": 1730371379,
"iss": "auth-server.io",
"roles": [
"admin"
],
"sub": "55474041-c8c6-48bb-9398-34c2dbfd88b3"
}
}
}
},
"request": {
"host": "gjson",
"id": "b59827c5-ec62-422c-972c-b86f1d29ec52"
},
"source": {}
}
The second request evaluates authorisation based on the following Authorization JSON:
{
"auth": {
"identity": "{\"aud\":\"toystore\", \"exp\":1730284979, \"iat\":1730371379, \"iss\":\"auth-server.io\", \"roles\":[\"admin\"], \"sub\":\"55474041-c8c6-48bb-9398-34c2dbfd88b3\"}"
},
"context": {
"metadata_context": {
"filter_metadata": {
"envoy.filters.http.jwt_authn": {
"verified_jwt": {
"aud": "toystore",
"exp": 1730284979,
"iat": 1730371379,
"iss": "auth-server.io",
"roles": [
"admin"
],
"sub": "55474041-c8c6-48bb-9398-34c2dbfd88b3"
}
}
}
},
"request": {
"http": {
"host": "cel",
"id": "04eb5f71-ec3c-40f1-a941-a7a5f0b3390b"
}
}
},
"destination": {},
"metadata": {
"filter_metadata": {
"envoy.filters.http.jwt_authn": {
"verified_jwt": {
"aud": "toystore",
"exp": 1730284979,
"iat": 1730371379,
"iss": "auth-server.io",
"roles": [
"admin"
],
"sub": "55474041-c8c6-48bb-9398-34c2dbfd88b3"
}
}
}
},
"request": {
"host": "cel",
"id": "04eb5f71-ec3c-40f1-a941-a7a5f0b3390b"
},
"source": {}
}
Static: property.Value, | ||
Pattern: property.Selector, | ||
}, false)) | ||
if value, err := stringValueFrom(&property); err != nil { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as https://github.com/Kuadrant/authorino/pull/495/files#r1821989655. The value of an extended property can be of any type.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, just to be clear (and we may want to rename that function), this does return a "JSON literal", but for String
returns a plain string... i.e. w/o the quotes.
Signed-off-by: Alex Snaps <[email protected]>
Signed-off-by: Guilherme Cassolato <[email protected]>
Signed-off-by: Alex Snaps <[email protected]>
Signed-off-by: Alex Snaps <[email protected]>
Signed-off-by: Alex Snaps <[email protected]>
Signed-off-by: Alex Snaps <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Amazing work, @alexsnaps!
I've manually tried this against a couple use cases myself and it seems to work like a charm.
Fixes #475