From 82d7619acc403a75e2f1816d2d02f555439fbbad Mon Sep 17 00:00:00 2001 From: Alex Snaps Date: Wed, 30 Oct 2024 12:20:53 -0400 Subject: [PATCH] CEL Support (#495) * CEL Support! Signed-off-by: Alex Snaps --------- Signed-off-by: Alex Snaps Signed-off-by: Guilherme Cassolato Signed-off-by: KevFan Co-authored-by: Guilherme Cassolato Co-authored-by: KevFan --- Makefile | 6 +- api/v1beta3/auth_config_types.go | 18 +- api/v1beta3/zz_generated.deepcopy.go | 16 + controllers/auth_config_controller.go | 375 +++++++++++++----- controllers/auth_config_controller_test.go | 8 +- go.mod | 3 + go.sum | 7 + .../authorino.kuadrant.io_authconfigs.yaml | 114 +++++- install/crd/patches/oneof_in_authconfigs.yaml | 21 + install/manifests.yaml | 142 ++++++- pkg/evaluators/authorization.go | 12 +- pkg/evaluators/authorization/authzed.go | 46 ++- pkg/evaluators/authorization/authzed_test.go | 20 +- .../authorization/kubernetes_authz.go | 71 +++- .../authorization/kubernetes_authz_test.go | 15 +- pkg/evaluators/cache.go | 10 +- pkg/evaluators/config.go | 5 +- pkg/evaluators/identity.go | 18 +- pkg/evaluators/identity/plain.go | 8 +- pkg/evaluators/identity/plain_test.go | 11 +- pkg/evaluators/identity_extension.go | 22 +- pkg/evaluators/identity_extension_test.go | 28 +- pkg/evaluators/identity_test.go | 4 +- pkg/evaluators/metadata.go | 12 +- pkg/evaluators/metadata/generic_http.go | 37 +- pkg/evaluators/metadata/generic_http_test.go | 6 +- pkg/evaluators/metadata_test.go | 2 +- pkg/evaluators/response.go | 12 +- pkg/evaluators/response/dynamic_json.go | 6 +- pkg/evaluators/response/dynamic_json_test.go | 4 +- pkg/evaluators/response/plain.go | 6 +- pkg/evaluators/response/plain_test.go | 9 +- pkg/evaluators/response/wristband.go | 6 +- pkg/evaluators/response/wristband_test.go | 4 +- pkg/expressions/cel/expressions.go | 164 ++++++++ pkg/expressions/cel/expressions_test.go | 44 ++ pkg/expressions/types.go | 5 + pkg/json/json.go | 10 +- pkg/json/json_test.go | 18 +- pkg/service/auth_pipeline.go | 9 +- pkg/service/auth_pipeline_test.go | 4 +- pkg/service/auth_test.go | 2 +- tests/e2e-test.sh | 13 +- tests/v1beta3/authconfig.yaml | 81 ++-- 44 files changed, 1108 insertions(+), 326 deletions(-) create mode 100644 pkg/expressions/cel/expressions.go create mode 100644 pkg/expressions/cel/expressions_test.go create mode 100644 pkg/expressions/types.go diff --git a/Makefile b/Makefile index d6a7013f..5efc0a73 100644 --- a/Makefile +++ b/Makefile @@ -54,7 +54,7 @@ controller-gen: ## Installs controller-gen in $PROJECT_DIR/bin KUSTOMIZE = $(PROJECT_DIR)/bin/kustomize kustomize: ## Installs kustomize in $PROJECT_DIR/bin - $(call go-get-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v4@v4.5.5) + $(call go-get-tool,$(KUSTOMIZE),sigs.k8s.io/kustomize/kustomize/v5@v5.5.0) ENVTEST = $(PROJECT_DIR)/bin/setup-envtest envtest: ## Installs setup-envtest in $PROJECT_DIR/bin @@ -118,11 +118,11 @@ vet: ## Runs go vet against code go vet ./... generate: vendor controller-gen ## Generates types deepcopy code - controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..." + $(CONTROLLER_GEN) object:headerFile="hack/boilerplate.go.txt" paths="./..." $(MAKE) fmt vet manifests: controller-gen kustomize ## Generates the manifests in $PROJECT_DIR/install - controller-gen crd:crdVersions=v1 rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=install/crd output:rbac:artifacts:config=install/rbac && $(KUSTOMIZE) build install > $(AUTHORINO_MANIFESTS) + $(CONTROLLER_GEN) crd:crdVersions=v1 rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=install/crd output:rbac:artifacts:config=install/rbac && $(KUSTOMIZE) build install > $(AUTHORINO_MANIFESTS) $(MAKE) patch-webhook run:git_sha=$(shell git rev-parse HEAD) diff --git a/api/v1beta3/auth_config_types.go b/api/v1beta3/auth_config_types.go index 0c3be7ed..53a9eae7 100644 --- a/api/v1beta3/auth_config_types.go +++ b/api/v1beta3/auth_config_types.go @@ -164,13 +164,19 @@ type PatternExpression struct { Value string `json:"value,omitempty"` } +type CelExpression string + +type CelPredicate struct { + Predicate string `json:"predicate,omitempty"` +} + // +kubebuilder:validation:Enum:=eq;neq;incl;excl;matches type PatternExpressionOperator string type PatternExpressionOrRef struct { PatternExpression `json:",omitempty"` PatternRef `json:",omitempty"` - + CelPredicate `json:",omitempty"` // A list of pattern expressions to be evaluated as a logical AND. All []UnstructuredPatternExpressionOrRef `json:"all,omitempty"` // A list of pattern expressions to be evaluated as a logical OR. @@ -199,6 +205,8 @@ type ValueOrSelector struct { // Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson can be used. // The following Authorino custom modifiers are supported: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, @case:upper|lower, @base64:encode|decode and @strip. Selector string `json:"selector,omitempty"` + + Expression CelExpression `json:"expression,omitempty"` } type CommonEvaluatorSpec struct { @@ -401,7 +409,9 @@ type PlainIdentitySpec struct { // Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). // Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson can be used. // The following Authorino custom modifiers are supported: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, @case:upper|lower, @base64:encode|decode and @strip. - Selector string `json:"selector"` + Selector string `json:"selector,omitempty"` + + Expression CelExpression `json:"expression,omitempty"` } type AnonymousAccessSpec struct{} @@ -437,7 +447,9 @@ type HttpEndpointSpec struct { // The value can include variable placeholders in the format "{selector}", where "selector" is any pattern supported // by https://pkg.go.dev/github.com/tidwall/gjson and selects value from the authorization JSON. // E.g. https://ext-auth-server.io/metadata?p={request.path} - Url string `json:"url"` + Url string `json:"url,omitempty"` + + UrlExpression CelExpression `json:"urlExpression,omitempty"` // HTTP verb used in the request to the service. Accepted values: GET (default), POST. // When the request method is POST, the authorization JSON is passed in the body of the request. diff --git a/api/v1beta3/zz_generated.deepcopy.go b/api/v1beta3/zz_generated.deepcopy.go index 5b0414ce..cbc7f560 100644 --- a/api/v1beta3/zz_generated.deepcopy.go +++ b/api/v1beta3/zz_generated.deepcopy.go @@ -481,6 +481,21 @@ func (in *CallbackSpec) DeepCopy() *CallbackSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CelPredicate) DeepCopyInto(out *CelPredicate) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CelPredicate. +func (in *CelPredicate) DeepCopy() *CelPredicate { + if in == nil { + return nil + } + out := new(CelPredicate) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CommonEvaluatorSpec) DeepCopyInto(out *CommonEvaluatorSpec) { *out = *in @@ -998,6 +1013,7 @@ func (in *PatternExpressionOrRef) DeepCopyInto(out *PatternExpressionOrRef) { *out = *in out.PatternExpression = in.PatternExpression out.PatternRef = in.PatternRef + out.CelPredicate = in.CelPredicate if in.All != nil { in, out := &in.All, &out.All *out = make([]UnstructuredPatternExpressionOrRef, len(*in)) diff --git a/controllers/auth_config_controller.go b/controllers/auth_config_controller.go index 6de5191f..5dbce879 100644 --- a/controllers/auth_config_controller.go +++ b/controllers/auth_config_controller.go @@ -29,6 +29,8 @@ import ( identity_evaluators "github.com/kuadrant/authorino/pkg/evaluators/identity" metadata_evaluators "github.com/kuadrant/authorino/pkg/evaluators/metadata" response_evaluators "github.com/kuadrant/authorino/pkg/evaluators/response" + "github.com/kuadrant/authorino/pkg/expressions" + "github.com/kuadrant/authorino/pkg/expressions/cel" "github.com/kuadrant/authorino/pkg/index" "github.com/kuadrant/authorino/pkg/json" "github.com/kuadrant/authorino/pkg/jsonexp" @@ -179,24 +181,30 @@ func (r *AuthConfigReconciler) translateAuthConfig(ctx context.Context, authConf } for identityCfgName, identity := range authConfigIdentityConfigs { - extendedProperties := make([]evaluators.IdentityExtension, len(identity.Defaults)+len(identity.Overrides)) + extendedProperties := make([]evaluators.IdentityExtension, 0) for propertyName, property := range identity.Defaults { - extendedProperties = append(extendedProperties, evaluators.NewIdentityExtension(propertyName, json.JSONValue{ - Static: property.Value, - Pattern: property.Selector, - }, false)) + if value, err := valueFrom(&property); err != nil { + return nil, err + } else { + extendedProperties = append(extendedProperties, evaluators.NewIdentityExtension(propertyName, value, false)) + } } for propertyName, property := range identity.Overrides { - extendedProperties = append(extendedProperties, evaluators.NewIdentityExtension(propertyName, json.JSONValue{ - Static: property.Value, - Pattern: property.Selector, - }, true)) + if value, err := valueFrom(&property); err != nil { + return nil, err + } else { + extendedProperties = append(extendedProperties, evaluators.NewIdentityExtension(propertyName, value, true)) + } } + predicates, err := buildPredicates(authConfig, identity.Conditions, jsonexp.All) + if err != nil { + return nil, err + } translatedIdentity := &evaluators.IdentityConfig{ Name: identityCfgName, Priority: identity.Priority, - Conditions: buildJSONExpression(authConfig, identity.Conditions, jsonexp.All), + Conditions: predicates, ExtendedProperties: extendedProperties, Metrics: identity.Metrics, } @@ -206,8 +214,12 @@ func (r *AuthConfigReconciler) translateAuthConfig(ctx context.Context, authConf if ttl == 0 { ttl = api.EvaluatorDefaultCacheTTL } + key, err := getJsonFromStaticDynamic(&identity.Cache.Key) + if err != nil { + return nil, err + } translatedIdentity.Cache = evaluators.NewEvaluatorCache( - *getJsonFromStaticDynamic(&identity.Cache.Key), + key, ttl, ) } @@ -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.NewExpression(string(identity.Plain.Expression)) + if err != nil { + return nil, err + } + translatedIdentity.Plain = &identity_evaluators.Plain{Value: expression, Pattern: string(identity.Plain.Expression)} + } else { + translatedIdentity.Plain = &identity_evaluators.Plain{Value: &json.JSONValue{Pattern: identity.Plain.Selector}, Pattern: identity.Plain.Selector} + } case api.AnonymousAccessAuthentication: translatedIdentity.Noop = &identity_evaluators.Noop{AuthCredentials: authCred} @@ -288,10 +308,14 @@ func (r *AuthConfigReconciler) translateAuthConfig(ctx context.Context, authConf interfacedMetadataConfigs := make([]auth.AuthConfigEvaluator, 0) for name, metadata := range authConfig.Spec.Metadata { + predicates, err := buildPredicates(authConfig, metadata.Conditions, jsonexp.All) + if err != nil { + return nil, err + } translatedMetadata := &evaluators.MetadataConfig{ Name: name, Priority: metadata.Priority, - Conditions: buildJSONExpression(authConfig, metadata.Conditions, jsonexp.All), + Conditions: predicates, Metrics: metadata.Metrics, } @@ -300,8 +324,12 @@ func (r *AuthConfigReconciler) translateAuthConfig(ctx context.Context, authConf if ttl == 0 { ttl = api.EvaluatorDefaultCacheTTL } + key, err := getJsonFromStaticDynamic(&metadata.Cache.Key) + if err != nil { + return nil, err + } translatedMetadata.Cache = evaluators.NewEvaluatorCache( - *getJsonFromStaticDynamic(&metadata.Cache.Key), + key, ttl, ) } @@ -357,10 +385,14 @@ func (r *AuthConfigReconciler) translateAuthConfig(ctx context.Context, authConf authzIndex := 0 for authzName, authorization := range authConfig.Spec.Authorization { + predicates, err := buildPredicates(authConfig, authorization.Conditions, jsonexp.All) + if err != nil { + return nil, err + } translatedAuthorization := &evaluators.AuthorizationConfig{ Name: authzName, Priority: authorization.Priority, - Conditions: buildJSONExpression(authConfig, authorization.Conditions, jsonexp.All), + Conditions: predicates, Metrics: authorization.Metrics, } @@ -369,8 +401,12 @@ func (r *AuthConfigReconciler) translateAuthConfig(ctx context.Context, authConf if ttl == 0 { ttl = api.EvaluatorDefaultCacheTTL } + key, err := getJsonFromStaticDynamic(&authorization.Cache.Key) + if err != nil { + return nil, err + } translatedAuthorization.Cache = evaluators.NewEvaluatorCache( - *getJsonFromStaticDynamic(&authorization.Cache.Key), + key, ttl, ) } @@ -415,28 +451,58 @@ func (r *AuthConfigReconciler) translateAuthConfig(ctx context.Context, authConf // json case api.PatternMatchingAuthorization: + rules, err := buildPredicates(authConfig, authorization.PatternMatching.Patterns, jsonexp.All) + if err != nil { + return nil, err + } translatedAuthorization.JSON = &authorization_evaluators.JSONPatternMatching{ - Rules: buildJSONExpression(authConfig, authorization.PatternMatching.Patterns, jsonexp.All), + Rules: rules, } case api.KubernetesSubjectAccessReviewAuthorization: user := authorization.KubernetesSubjectAccessReview.User - authorinoUser := json.JSONValue{Static: user.Value, Pattern: user.Selector} + authorinoUser, err := valueFrom(user) + if err != nil { + return nil, err + } var authorinoResourceAttributes *authorization_evaluators.KubernetesAuthzResourceAttributes resourceAttributes := authorization.KubernetesSubjectAccessReview.ResourceAttributes if resourceAttributes != nil { + namespace, err := valueFrom(&resourceAttributes.Namespace) + if err != nil { + return nil, err + } + group, err := valueFrom(&resourceAttributes.Group) + if err != nil { + return nil, err + } + resource, err := valueFrom(&resourceAttributes.Resource) + if err != nil { + return nil, err + } + name, err := valueFrom(&resourceAttributes.Name) + if err != nil { + return nil, err + } + subResource, err := valueFrom(&resourceAttributes.SubResource) + if err != nil { + return nil, err + } + verb, err := valueFrom(&resourceAttributes.Verb) + if err != nil { + return nil, err + } authorinoResourceAttributes = &authorization_evaluators.KubernetesAuthzResourceAttributes{ - Namespace: json.JSONValue{Static: resourceAttributes.Namespace.Value, Pattern: resourceAttributes.Namespace.Selector}, - Group: json.JSONValue{Static: resourceAttributes.Group.Value, Pattern: resourceAttributes.Group.Selector}, - Resource: json.JSONValue{Static: resourceAttributes.Resource.Value, Pattern: resourceAttributes.Resource.Selector}, - Name: json.JSONValue{Static: resourceAttributes.Name.Value, Pattern: resourceAttributes.Name.Selector}, - SubResource: json.JSONValue{Static: resourceAttributes.SubResource.Value, Pattern: resourceAttributes.SubResource.Selector}, - Verb: json.JSONValue{Static: resourceAttributes.Verb.Value, Pattern: resourceAttributes.Verb.Selector}, + Namespace: namespace, + Group: group, + Resource: resource, + Name: name, + SubResource: subResource, + Verb: verb, } } - var err error translatedAuthorization.KubernetesAuthz, err = authorization_evaluators.NewKubernetesAuthz(authorinoUser, authorization.KubernetesSubjectAccessReview.Groups, authorinoResourceAttributes) if err != nil { return nil, err @@ -454,15 +520,24 @@ func (r *AuthConfigReconciler) translateAuthConfig(ctx context.Context, authConf sharedSecret = string(secret.Data[secretRef.Key]) } + permission, err := getJsonFromStaticDynamic(&authzed.Permission) + if err != nil { + return nil, err + } translatedAuthzed := &authorization_evaluators.Authzed{ Endpoint: authzed.Endpoint, Insecure: authzed.Insecure, SharedSecret: sharedSecret, - Permission: *getJsonFromStaticDynamic(&authzed.Permission), + Permission: permission, + } + translatedAuthzed.Subject, translatedAuthzed.SubjectKind, err = spiceDBObjectToJsonValues(authzed.Subject) + if err != nil { + return nil, err + } + translatedAuthzed.Resource, translatedAuthzed.ResourceKind, err = spiceDBObjectToJsonValues(authzed.Resource) + if err != nil { + return nil, err } - translatedAuthzed.Subject, translatedAuthzed.SubjectKind = spiceDBObjectToJsonValues(authzed.Subject) - translatedAuthzed.Resource, translatedAuthzed.ResourceKind = spiceDBObjectToJsonValues(authzed.Resource) - translatedAuthorization.Authzed = translatedAuthzed case api.UnknownAuthorizationMethod: @@ -477,10 +552,14 @@ func (r *AuthConfigReconciler) translateAuthConfig(ctx context.Context, authConf if responseConfig := authConfig.Spec.Response; responseConfig != nil { for responseName, headerSuccessResponse := range responseConfig.Success.Headers { + predicates, err := buildPredicates(authConfig, headerSuccessResponse.Conditions, jsonexp.All) + if err != nil { + return nil, err + } translatedResponse := evaluators.NewResponseConfig( responseName, headerSuccessResponse.Priority, - buildJSONExpression(authConfig, headerSuccessResponse.Conditions, jsonexp.All), + predicates, "httpHeader", headerSuccessResponse.Key, headerSuccessResponse.Metrics, @@ -495,10 +574,14 @@ func (r *AuthConfigReconciler) translateAuthConfig(ctx context.Context, authConf } for responseName, successResponse := range responseConfig.Success.DynamicMetadata { + predicates, err := buildPredicates(authConfig, successResponse.Conditions, jsonexp.All) + if err != nil { + return nil, err + } translatedResponse := evaluators.NewResponseConfig( responseName, successResponse.Priority, - buildJSONExpression(authConfig, successResponse.Conditions, jsonexp.All), + predicates, "envoyDynamicMetadata", successResponse.Key, successResponse.Metrics, @@ -516,10 +599,14 @@ func (r *AuthConfigReconciler) translateAuthConfig(ctx context.Context, authConf interfacedCallbackConfigs := make([]auth.AuthConfigEvaluator, 0) for callbackName, callback := range authConfig.Spec.Callbacks { + predicates, err := buildPredicates(authConfig, callback.Conditions, jsonexp.All) + if err != nil { + return nil, err + } translatedCallback := &evaluators.CallbackConfig{ Name: callbackName, Priority: callback.Priority, - Conditions: buildJSONExpression(authConfig, callback.Conditions, jsonexp.All), + Conditions: predicates, Metrics: callback.Metrics, } @@ -539,8 +626,12 @@ func (r *AuthConfigReconciler) translateAuthConfig(ctx context.Context, authConf interfacedCallbackConfigs = append(interfacedCallbackConfigs, translatedCallback) } + predicates, err := buildPredicates(authConfig, authConfig.Spec.Conditions, jsonexp.All) + if err != nil { + return nil, err + } translatedAuthConfig := &evaluators.AuthConfig{ - Conditions: buildJSONExpression(authConfig, authConfig.Spec.Conditions, jsonexp.All), + Conditions: predicates, IdentityConfigs: interfacedIdentityConfigs, MetadataConfigs: interfacedMetadataConfigs, AuthorizationConfigs: interfacedAuthorizationConfigs, @@ -552,16 +643,37 @@ func (r *AuthConfigReconciler) translateAuthConfig(ctx context.Context, authConf // denyWith if responseConfig := authConfig.Spec.Response; responseConfig != nil { if denyWith := responseConfig.Unauthenticated; denyWith != nil { - translatedAuthConfig.Unauthenticated = buildAuthorinoDenyWithValues(denyWith) + value, err := buildAuthorinoDenyWithValues(denyWith) + if err != nil { + return nil, err + } + translatedAuthConfig.Unauthenticated = value } if denyWith := responseConfig.Unauthorized; denyWith != nil { - translatedAuthConfig.Unauthorized = buildAuthorinoDenyWithValues(denyWith) + value, err := buildAuthorinoDenyWithValues(denyWith) + if err != nil { + return nil, err + } + translatedAuthConfig.Unauthorized = value } } return translatedAuthConfig, nil } +func valueFrom(user *api.ValueOrSelector) (expressions.Value, error) { + var strValue expressions.Value + var err error + if user.Expression != "" { + if strValue, err = cel.NewExpression(string(user.Expression)); err != nil { + return nil, err + } + } else { + strValue = &json.JSONValue{Static: user.Value, Pattern: user.Selector} + } + return strValue, nil +} + func injectResponseConfig(ctx context.Context, authConfig *api.AuthConfig, successResponse api.SuccessResponseSpec, r *AuthConfigReconciler, translatedResponse *evaluators.ResponseConfig) error { switch successResponse.GetMethod() { // wristband @@ -592,13 +704,14 @@ func injectResponseConfig(ctx context.Context, authConfig *api.AuthConfig, succe customClaims := make([]json.JSONProperty, 0) for claimName, claim := range wristband.CustomClaims { - customClaims = append(customClaims, json.JSONProperty{ - Name: claimName, - Value: json.JSONValue{ - Static: claim.Value, - Pattern: claim.Selector, - }, - }) + if value, err := valueFrom(&claim); err != nil { + return err + } else { + customClaims = append(customClaims, json.JSONProperty{ + Name: claimName, + Value: value, + }) + } } if authorinoWristband, err := response_evaluators.NewWristbandConfig( @@ -617,24 +730,27 @@ func injectResponseConfig(ctx context.Context, authConfig *api.AuthConfig, succe jsonProperties := make([]json.JSONProperty, 0) for propertyName, property := range successResponse.Json.Properties { - jsonProperties = append(jsonProperties, json.JSONProperty{ - Name: propertyName, - Value: json.JSONValue{ - Static: property.Value, - Pattern: property.Selector, - }, - }) + if value, err := valueFrom(&property); err != nil { + return err + } else { + jsonProperties = append(jsonProperties, json.JSONProperty{ + Name: propertyName, + Value: value, + }) + + } } translatedResponse.DynamicJSON = response_evaluators.NewDynamicJSONResponse(jsonProperties) // plain case api.PlainAuthResponse: - translatedResponse.Plain = &response_evaluators.Plain{ - JSONValue: json.JSONValue{ - Static: successResponse.Plain.Value, - Pattern: successResponse.Plain.Selector, - }, + if value, err := valueFrom((*api.ValueOrSelector)(successResponse.Plain)); err != nil { + return err + } else { + translatedResponse.Plain = &response_evaluators.Plain{ + Value: value, + } } case api.UnknownAuthResponseMethod: @@ -643,17 +759,22 @@ func injectResponseConfig(ctx context.Context, authConfig *api.AuthConfig, succe return nil } -func injectCache(cache *api.EvaluatorCaching, translatedResponse *evaluators.ResponseConfig) { +func injectCache(cache *api.EvaluatorCaching, translatedResponse *evaluators.ResponseConfig) error { if cache != nil { ttl := cache.TTL if ttl == 0 { ttl = api.EvaluatorDefaultCacheTTL } - translatedResponse.Cache = evaluators.NewEvaluatorCache( - *getJsonFromStaticDynamic(&cache.Key), - ttl, - ) + if key, err := getJsonFromStaticDynamic(&cache.Key); err != nil { + return err + } else { + translatedResponse.Cache = evaluators.NewEvaluatorCache( + key, + ttl, + ) + } } + return nil } func (r *AuthConfigReconciler) addToIndex(ctx context.Context, resourceNamespace, resourceId string, authConfig *evaluators.AuthConfig, hosts []string) (linkedHosts, looseHosts []string, err error) { @@ -796,31 +917,37 @@ func (r *AuthConfigReconciler) buildGenericHttpEvaluator(ctx context.Context, ht oauth2TokenForceFetch = oauth2Config.Cache != nil && !*oauth2Config.Cache } - var body *json.JSONValue + var body expressions.Value if b := http.Body; b != nil { - body = &json.JSONValue{Static: b.Value, Pattern: b.Selector} + if value, err := valueFrom(b); err != nil { + return nil, err + } else { + body = value + } } params := make([]json.JSONProperty, 0, len(http.Parameters)) for name, param := range http.Parameters { - params = append(params, json.JSONProperty{ - Name: name, - Value: json.JSONValue{ - Static: param.Value, - Pattern: param.Selector, - }, - }) + if value, err := valueFrom(¶m); err != nil { + return nil, err + } else { + params = append(params, json.JSONProperty{ + Name: name, + Value: value, + }) + } } headers := make([]json.JSONProperty, 0, len(http.Headers)) for name, header := range http.Headers { - headers = append(headers, json.JSONProperty{ - Name: name, - Value: json.JSONValue{ - Static: header.Value, - Pattern: header.Selector, - }, - }) + if value, err := valueFrom(&header); err != nil { + return nil, err + } else { + headers = append(headers, json.JSONProperty{ + Name: name, + Value: value, + }) + } } method := "GET" @@ -828,8 +955,19 @@ func (r *AuthConfigReconciler) buildGenericHttpEvaluator(ctx context.Context, ht method = string(*m) } + var dynamicEndpoint expressions.Value + if http.UrlExpression != "" { + endpoint, err := cel.NewExpression(string(http.UrlExpression)) + if err != nil { + return nil, err + } else { + dynamicEndpoint = endpoint + } + } + ev := &metadata_evaluators.GenericHttp{ Endpoint: http.Url, + DynamicEndpoint: dynamicEndpoint, Method: method, Body: body, Parameters: params, @@ -875,18 +1013,26 @@ func findIdentityConfigByName(identityConfigs []evaluators.IdentityConfig, name return nil, fmt.Errorf("missing identity config %v", name) } -func buildJSONExpression(authConfig *api.AuthConfig, patterns []api.PatternExpressionOrRef, op func(...jsonexp.Expression) jsonexp.Expression) jsonexp.Expression { +func buildPredicates(authConfig *api.AuthConfig, patterns []api.PatternExpressionOrRef, op func(...jsonexp.Expression) jsonexp.Expression) (jsonexp.Expression, error) { var expression []jsonexp.Expression for _, pattern := range patterns { // patterns or refs - expression = append(expression, buildJSONExpressionPatterns(authConfig, pattern)...) + expressions, err := buildJSONExpressionPatterns(authConfig, pattern) + if err != nil { + return nil, err + } + expression = append(expression, expressions...) // all if len(pattern.All) > 0 { p := make([]api.PatternExpressionOrRef, len(pattern.All)) for i, ptn := range pattern.All { p[i] = ptn.PatternExpressionOrRef } - expression = append(expression, buildJSONExpression(authConfig, p, jsonexp.All)) + predicates, err := buildPredicates(authConfig, p, jsonexp.All) + if err != nil { + return nil, err + } + expression = append(expression, predicates) } // any if len(pattern.Any) > 0 { @@ -894,25 +1040,35 @@ func buildJSONExpression(authConfig *api.AuthConfig, patterns []api.PatternExpre for i, ptn := range pattern.Any { p[i] = ptn.PatternExpressionOrRef } - expression = append(expression, buildJSONExpression(authConfig, p, jsonexp.Any)) + predicates, err := buildPredicates(authConfig, p, jsonexp.Any) + if err != nil { + return nil, err + } + expression = append(expression, predicates) } } - return op(expression...) + return op(expression...), nil } -func buildJSONExpressionPatterns(authConfig *api.AuthConfig, pattern api.PatternExpressionOrRef) []jsonexp.Expression { +func buildJSONExpressionPatterns(authConfig *api.AuthConfig, pattern api.PatternExpressionOrRef) ([]jsonexp.Expression, error) { expressionsToAdd := api.PatternExpressions{} + expressions := make([]jsonexp.Expression, len(expressionsToAdd)) if expressionsByRef, found := authConfig.Spec.NamedPatterns[pattern.PatternRef.Name]; found { expressionsToAdd = append(expressionsToAdd, expressionsByRef...) } else if pattern.PatternExpression.Operator != "" { expressionsToAdd = append(expressionsToAdd, pattern.PatternExpression) + } else if pattern.Predicate != "" { + if predicate, err := cel.NewPredicate(pattern.Predicate); err != nil { + return nil, err + } else { + expressions = append(expressions, predicate) + } } - expressions := make([]jsonexp.Expression, len(expressionsToAdd)) - for i, expression := range expressionsToAdd { - expressions[i] = buildJSONExpressionPattern(expression) + for _, expression := range expressionsToAdd { + expressions = append(expressions, buildJSONExpressionPattern(expression)) } - return expressions + return expressions, nil } func buildJSONExpressionPattern(expression api.PatternExpression) jsonexp.Expression { @@ -923,42 +1079,63 @@ func buildJSONExpressionPattern(expression api.PatternExpression) jsonexp.Expres } } -func buildAuthorinoDenyWithValues(denyWithSpec *api.DenyWithSpec) *evaluators.DenyWithValues { +func buildAuthorinoDenyWithValues(denyWithSpec *api.DenyWithSpec) (*evaluators.DenyWithValues, error) { if denyWithSpec == nil { - return nil + return nil, nil } headers := make([]json.JSONProperty, 0, len(denyWithSpec.Headers)) for name, header := range denyWithSpec.Headers { - headers = append(headers, json.JSONProperty{Name: name, Value: json.JSONValue{Static: header.Value, Pattern: header.Selector}}) + if value, err := valueFrom(&header); err != nil { + return nil, err + } else { + headers = append(headers, json.JSONProperty{Name: name, Value: value}) + } } + message, err := getJsonFromStaticDynamic(denyWithSpec.Message) + if err != nil { + return nil, err + } + body, err := getJsonFromStaticDynamic(denyWithSpec.Body) + if err != nil { + return nil, err + } return &evaluators.DenyWithValues{ Code: int32(denyWithSpec.Code), - Message: getJsonFromStaticDynamic(denyWithSpec.Message), + Message: message, Headers: headers, - Body: getJsonFromStaticDynamic(denyWithSpec.Body), - } + Body: body, + }, nil } -func getJsonFromStaticDynamic(value *api.ValueOrSelector) *json.JSONValue { +func getJsonFromStaticDynamic(value *api.ValueOrSelector) (expressions.Value, error) { if value == nil { - return nil + return nil, nil + } + expression := string(value.Expression) + if expression != "" { + return cel.NewExpression(expression) } return &json.JSONValue{ Static: value.Value, Pattern: value.Selector, - } + }, nil } -func spiceDBObjectToJsonValues(obj *api.SpiceDBObject) (name json.JSONValue, kind json.JSONValue) { +func spiceDBObjectToJsonValues(obj *api.SpiceDBObject) (name expressions.Value, kind expressions.Value, err error) { if obj == nil { return } - name = *getJsonFromStaticDynamic(&obj.Name) - kind = *getJsonFromStaticDynamic(&obj.Kind) - - return name, kind + nameResolved, err := getJsonFromStaticDynamic(&obj.Name) + if err != nil { + return nil, nil, err + } + kindResolved, err := getJsonFromStaticDynamic(&obj.Kind) + if err != nil { + return nil, nil, err + } + return nameResolved, kindResolved, nil } diff --git a/controllers/auth_config_controller_test.go b/controllers/auth_config_controller_test.go index 8169a6d3..c4a990d1 100644 --- a/controllers/auth_config_controller_test.go +++ b/controllers/auth_config_controller_test.go @@ -95,10 +95,8 @@ func newTestAuthConfig(authConfigLabels map[string]string) api.AuthConfig { PatternMatching: &api.PatternMatchingAuthorizationSpec{ Patterns: []api.PatternExpressionOrRef{ { - PatternExpression: api.PatternExpression{ - Selector: "context.identity.role", - Operator: "eq", - Value: "admin", + CelPredicate: api.CelPredicate{ + Predicate: "auth.identity.role == 'admin'", }, }, }, @@ -170,7 +168,7 @@ func TestReconcileAuthConfigOk(t *testing.T) { config := authConfigIndex.Get("echo-api") assert.Check(t, config != nil) idConfig, _ := config.IdentityConfigs[0].(*evaluators.IdentityConfig) - assert.Equal(t, idConfig.ExtendedProperties[1].Name, "source") + assert.Equal(t, idConfig.ExtendedProperties[0].Name, "source") // TODO(@guicassolato): assert other fields of the AuthConfig } diff --git a/go.mod b/go.mod index 3917d46c..80ee0f7a 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/install/crd/authorino.kuadrant.io_authconfigs.yaml b/install/crd/authorino.kuadrant.io_authconfigs.yaml index 7c15cc4b..c658c8f8 100644 --- a/install/crd/authorino.kuadrant.io_authconfigs.yaml +++ b/install/crd/authorino.kuadrant.io_authconfigs.yaml @@ -2452,6 +2452,8 @@ spec: Key used to store the entry in the cache. The resolved key must be unique within the scope of this particular config. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -2505,6 +2507,8 @@ spec: defaults: additionalProperties: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -2585,6 +2589,8 @@ spec: overrides: additionalProperties: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -2606,14 +2612,14 @@ spec: Identity object extracted from the context. Use this method when authentication is performed beforehand by a proxy and the resulting object passed to Authorino as JSON in the auth request. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson can be used. The following Authorino custom modifiers are supported: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, @case:upper|lower, @base64:encode|decode and @strip. type: string - required: - - selector type: object priority: default: 0 @@ -2656,6 +2662,8 @@ spec: patternRef: description: Reference to a named set of pattern expressions type: string + predicate: + type: string selector: description: |- Path selector to fetch content from the authorization JSON (e.g. 'request.method'). @@ -2747,6 +2755,8 @@ spec: Key used to store the entry in the cache. The resolved key must be unique within the scope of this particular config. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -2785,6 +2795,8 @@ spec: API group of the resource. Use '*' for all API groups. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -2800,6 +2812,8 @@ spec: Resource name Omit it to check for authorization on all resources of the specified kind. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -2814,6 +2828,8 @@ spec: description: Namespace where the user must have permissions on the resource. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -2829,6 +2845,8 @@ spec: Resource kind Use '*' for all resource kinds. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -2842,6 +2860,8 @@ spec: subresource: description: Subresource kind properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -2857,6 +2877,8 @@ spec: Verb to check for authorization on the resource. Use '*' for all verbs. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -2873,6 +2895,8 @@ spec: User to check for authorization in the Kubernetes RBAC. Omit it to check for group authorization only. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -2912,6 +2936,8 @@ spec: Supersedes 'bodyParameters'; use either one or the other. Use it with method=POST; for GET requests, set parameters as query string in the 'endpoint' (placeholders can be used). properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -2925,6 +2951,8 @@ spec: bodyParameters: additionalProperties: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -2984,6 +3012,8 @@ spec: headers: additionalProperties: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3091,8 +3121,8 @@ spec: by https://pkg.go.dev/github.com/tidwall/gjson and selects value from the authorization JSON. E.g. https://ext-auth-server.io/metadata?p={request.path} type: string - required: - - url + urlExpression: + type: string type: object rego: description: |- @@ -3135,6 +3165,8 @@ spec: patternRef: description: Reference to a named set of pattern expressions type: string + predicate: + type: string selector: description: |- Path selector to fetch content from the authorization JSON (e.g. 'request.method'). @@ -3173,6 +3205,8 @@ spec: description: The name of the permission (or relation) on which to execute the check. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3189,6 +3223,8 @@ spec: properties: kind: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3201,6 +3237,8 @@ spec: type: object name: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3235,6 +3273,8 @@ spec: properties: kind: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3247,6 +3287,8 @@ spec: type: object name: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3296,6 +3338,8 @@ spec: patternRef: description: Reference to a named set of pattern expressions type: string + predicate: + type: string selector: description: |- Path selector to fetch content from the authorization JSON (e.g. 'request.method'). @@ -3327,6 +3371,8 @@ spec: Key used to store the entry in the cache. The resolved key must be unique within the scope of this particular config. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3354,6 +3400,8 @@ spec: Supersedes 'bodyParameters'; use either one or the other. Use it with method=POST; for GET requests, set parameters as query string in the 'endpoint' (placeholders can be used). properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3367,6 +3415,8 @@ spec: bodyParameters: additionalProperties: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3426,6 +3476,8 @@ spec: headers: additionalProperties: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3529,8 +3581,8 @@ spec: by https://pkg.go.dev/github.com/tidwall/gjson and selects value from the authorization JSON. E.g. https://ext-auth-server.io/metadata?p={request.path} type: string - required: - - url + urlExpression: + type: string type: object metrics: default: false @@ -3578,6 +3630,8 @@ spec: patternRef: description: Reference to a named set of pattern expressions type: string + predicate: + type: string selector: description: |- Path selector to fetch content from the authorization JSON (e.g. 'request.method'). @@ -3618,6 +3672,8 @@ spec: Key used to store the entry in the cache. The resolved key must be unique within the scope of this particular config. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3645,6 +3701,8 @@ spec: Supersedes 'bodyParameters'; use either one or the other. Use it with method=POST; for GET requests, set parameters as query string in the 'endpoint' (placeholders can be used). properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3658,6 +3716,8 @@ spec: bodyParameters: additionalProperties: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3717,6 +3777,8 @@ spec: headers: additionalProperties: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3820,8 +3882,8 @@ spec: by https://pkg.go.dev/github.com/tidwall/gjson and selects value from the authorization JSON. E.g. https://ext-auth-server.io/metadata?p={request.path} type: string - required: - - url + urlExpression: + type: string type: object metrics: default: false @@ -3906,6 +3968,8 @@ spec: patternRef: description: Reference to a named set of pattern expressions type: string + predicate: + type: string selector: description: |- Path selector to fetch content from the authorization JSON (e.g. 'request.method'). @@ -3979,6 +4043,8 @@ spec: Key used to store the entry in the cache. The resolved key must be unique within the scope of this particular config. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -4006,6 +4072,8 @@ spec: properties: additionalProperties: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -4033,6 +4101,8 @@ spec: plain: description: Plain text content properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -4085,6 +4155,8 @@ spec: description: Reference to a named set of pattern expressions type: string + predicate: + type: string selector: description: |- Path selector to fetch content from the authorization JSON (e.g. 'request.method'). @@ -4104,6 +4176,8 @@ spec: customClaims: additionalProperties: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -4178,6 +4252,8 @@ spec: Key used to store the entry in the cache. The resolved key must be unique within the scope of this particular config. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -4205,6 +4281,8 @@ spec: properties: additionalProperties: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -4232,6 +4310,8 @@ spec: plain: description: Plain text content properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -4284,6 +4364,8 @@ spec: description: Reference to a named set of pattern expressions type: string + predicate: + type: string selector: description: |- Path selector to fetch content from the authorization JSON (e.g. 'request.method'). @@ -4303,6 +4385,8 @@ spec: customClaims: additionalProperties: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -4374,6 +4458,8 @@ spec: description: HTTP response body to override the default denial body. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -4394,6 +4480,8 @@ spec: headers: additionalProperties: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -4410,6 +4498,8 @@ spec: message: description: HTTP message to override the default denial message. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -4431,6 +4521,8 @@ spec: description: HTTP response body to override the default denial body. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -4451,6 +4543,8 @@ spec: headers: additionalProperties: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -4467,6 +4561,8 @@ spec: message: description: HTTP message to override the default denial message. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -4514,6 +4610,8 @@ spec: patternRef: description: Reference to a named set of pattern expressions type: string + predicate: + type: string selector: description: |- Path selector to fetch content from the authorization JSON (e.g. 'request.method'). diff --git a/install/crd/patches/oneof_in_authconfigs.yaml b/install/crd/patches/oneof_in_authconfigs.yaml index 919f6e83..f614394b 100644 --- a/install/crd/patches/oneof_in_authconfigs.yaml +++ b/install/crd/patches/oneof_in_authconfigs.yaml @@ -319,6 +319,9 @@ - properties: any: {} required: [any] + - properties: + predicate: {} + required: [predicate] - op: add path: /spec/versions/1/schema/openAPIV3Schema/properties/spec/properties/when/items/oneOf @@ -337,6 +340,9 @@ - properties: any: {} required: [any] + - properties: + predicate: {} + required: [predicate] - op: add path: /spec/versions/1/schema/openAPIV3Schema/properties/spec/properties/authentication/additionalProperties/properties/when/items/oneOf @@ -355,6 +361,9 @@ - properties: any: {} required: [any] + - properties: + predicate: {} + required: [predicate] - op: add path: /spec/versions/1/schema/openAPIV3Schema/properties/spec/properties/metadata/additionalProperties/properties/when/items/oneOf @@ -373,6 +382,9 @@ - properties: any: {} required: [any] + - properties: + predicate: {} + required: [predicate] - op: add path: /spec/versions/1/schema/openAPIV3Schema/properties/spec/properties/authorization/additionalProperties/properties/when/items/oneOf @@ -391,6 +403,9 @@ - properties: any: {} required: [any] + - properties: + predicate: {} + required: [predicate] - op: add path: /spec/versions/1/schema/openAPIV3Schema/properties/spec/properties/response/properties/success/properties/headers/additionalProperties/properties/when/items/oneOf @@ -409,6 +424,9 @@ - properties: any: {} required: [any] + - properties: + predicate: {} + required: [predicate] - op: add path: /spec/versions/1/schema/openAPIV3Schema/properties/spec/properties/response/properties/success/properties/dynamicMetadata/additionalProperties/properties/when/items/oneOf @@ -427,3 +445,6 @@ - properties: any: {} required: [any] + - properties: + predicate: {} + required: [predicate] diff --git a/install/manifests.yaml b/install/manifests.yaml index 3bc0f460..a0ed239b 100644 --- a/install/manifests.yaml +++ b/install/manifests.yaml @@ -2719,6 +2719,8 @@ spec: Key used to store the entry in the cache. The resolved key must be unique within the scope of this particular config. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -2772,6 +2774,8 @@ spec: defaults: additionalProperties: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -2852,6 +2856,8 @@ spec: overrides: additionalProperties: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -2873,14 +2879,14 @@ spec: Identity object extracted from the context. Use this method when authentication is performed beforehand by a proxy and the resulting object passed to Authorino as JSON in the auth request. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). Any pattern supported by https://pkg.go.dev/github.com/tidwall/gjson can be used. The following Authorino custom modifiers are supported: @extract:{sep:" ",pos:0}, @replace{old:"",new:""}, @case:upper|lower, @base64:encode|decode and @strip. type: string - required: - - selector type: object priority: default: 0 @@ -2914,6 +2920,10 @@ spec: any: {} required: - any + - properties: + predicate: {} + required: + - predicate properties: all: description: A list of pattern expressions to be evaluated @@ -2943,6 +2953,8 @@ spec: patternRef: description: Reference to a named set of pattern expressions type: string + predicate: + type: string selector: description: |- Path selector to fetch content from the authorization JSON (e.g. 'request.method'). @@ -3051,6 +3063,8 @@ spec: Key used to store the entry in the cache. The resolved key must be unique within the scope of this particular config. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3089,6 +3103,8 @@ spec: API group of the resource. Use '*' for all API groups. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3104,6 +3120,8 @@ spec: Resource name Omit it to check for authorization on all resources of the specified kind. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3118,6 +3136,8 @@ spec: description: Namespace where the user must have permissions on the resource. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3133,6 +3153,8 @@ spec: Resource kind Use '*' for all resource kinds. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3146,6 +3168,8 @@ spec: subresource: description: Subresource kind properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3161,6 +3185,8 @@ spec: Verb to check for authorization on the resource. Use '*' for all verbs. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3177,6 +3203,8 @@ spec: User to check for authorization in the Kubernetes RBAC. Omit it to check for group authorization only. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3216,6 +3244,8 @@ spec: Supersedes 'bodyParameters'; use either one or the other. Use it with method=POST; for GET requests, set parameters as query string in the 'endpoint' (placeholders can be used). properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3229,6 +3259,8 @@ spec: bodyParameters: additionalProperties: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3288,6 +3320,8 @@ spec: headers: additionalProperties: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3395,8 +3429,8 @@ spec: by https://pkg.go.dev/github.com/tidwall/gjson and selects value from the authorization JSON. E.g. https://ext-auth-server.io/metadata?p={request.path} type: string - required: - - url + urlExpression: + type: string type: object rego: description: |- @@ -3430,6 +3464,10 @@ spec: any: {} required: - any + - properties: + predicate: {} + required: + - predicate properties: all: description: A list of pattern expressions to be evaluated @@ -3459,6 +3497,8 @@ spec: patternRef: description: Reference to a named set of pattern expressions type: string + predicate: + type: string selector: description: |- Path selector to fetch content from the authorization JSON (e.g. 'request.method'). @@ -3497,6 +3537,8 @@ spec: description: The name of the permission (or relation) on which to execute the check. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3513,6 +3555,8 @@ spec: properties: kind: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3525,6 +3569,8 @@ spec: type: object name: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3559,6 +3605,8 @@ spec: properties: kind: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3571,6 +3619,8 @@ spec: type: object name: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3611,6 +3661,10 @@ spec: any: {} required: - any + - properties: + predicate: {} + required: + - predicate properties: all: description: A list of pattern expressions to be evaluated @@ -3640,6 +3694,8 @@ spec: patternRef: description: Reference to a named set of pattern expressions type: string + predicate: + type: string selector: description: |- Path selector to fetch content from the authorization JSON (e.g. 'request.method'). @@ -3671,6 +3727,8 @@ spec: Key used to store the entry in the cache. The resolved key must be unique within the scope of this particular config. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3698,6 +3756,8 @@ spec: Supersedes 'bodyParameters'; use either one or the other. Use it with method=POST; for GET requests, set parameters as query string in the 'endpoint' (placeholders can be used). properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3711,6 +3771,8 @@ spec: bodyParameters: additionalProperties: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3770,6 +3832,8 @@ spec: headers: additionalProperties: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -3873,8 +3937,8 @@ spec: by https://pkg.go.dev/github.com/tidwall/gjson and selects value from the authorization JSON. E.g. https://ext-auth-server.io/metadata?p={request.path} type: string - required: - - url + urlExpression: + type: string type: object metrics: default: false @@ -3922,6 +3986,8 @@ spec: patternRef: description: Reference to a named set of pattern expressions type: string + predicate: + type: string selector: description: |- Path selector to fetch content from the authorization JSON (e.g. 'request.method'). @@ -3975,6 +4041,8 @@ spec: Key used to store the entry in the cache. The resolved key must be unique within the scope of this particular config. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -4002,6 +4070,8 @@ spec: Supersedes 'bodyParameters'; use either one or the other. Use it with method=POST; for GET requests, set parameters as query string in the 'endpoint' (placeholders can be used). properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -4015,6 +4085,8 @@ spec: bodyParameters: additionalProperties: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -4074,6 +4146,8 @@ spec: headers: additionalProperties: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -4177,8 +4251,8 @@ spec: by https://pkg.go.dev/github.com/tidwall/gjson and selects value from the authorization JSON. E.g. https://ext-auth-server.io/metadata?p={request.path} type: string - required: - - url + urlExpression: + type: string type: object metrics: default: false @@ -4254,6 +4328,10 @@ spec: any: {} required: - any + - properties: + predicate: {} + required: + - predicate properties: all: description: A list of pattern expressions to be evaluated @@ -4283,6 +4361,8 @@ spec: patternRef: description: Reference to a named set of pattern expressions type: string + predicate: + type: string selector: description: |- Path selector to fetch content from the authorization JSON (e.g. 'request.method'). @@ -4369,6 +4449,8 @@ spec: Key used to store the entry in the cache. The resolved key must be unique within the scope of this particular config. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -4396,6 +4478,8 @@ spec: properties: additionalProperties: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -4423,6 +4507,8 @@ spec: plain: description: Plain text content properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -4465,6 +4551,10 @@ spec: any: {} required: - any + - properties: + predicate: {} + required: + - predicate properties: all: description: A list of pattern expressions to @@ -4495,6 +4585,8 @@ spec: description: Reference to a named set of pattern expressions type: string + predicate: + type: string selector: description: |- Path selector to fetch content from the authorization JSON (e.g. 'request.method'). @@ -4514,6 +4606,8 @@ spec: customClaims: additionalProperties: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -4601,6 +4695,8 @@ spec: Key used to store the entry in the cache. The resolved key must be unique within the scope of this particular config. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -4628,6 +4724,8 @@ spec: properties: additionalProperties: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -4655,6 +4753,8 @@ spec: plain: description: Plain text content properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -4697,6 +4797,10 @@ spec: any: {} required: - any + - properties: + predicate: {} + required: + - predicate properties: all: description: A list of pattern expressions to @@ -4727,6 +4831,8 @@ spec: description: Reference to a named set of pattern expressions type: string + predicate: + type: string selector: description: |- Path selector to fetch content from the authorization JSON (e.g. 'request.method'). @@ -4746,6 +4852,8 @@ spec: customClaims: additionalProperties: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -4817,6 +4925,8 @@ spec: description: HTTP response body to override the default denial body. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -4837,6 +4947,8 @@ spec: headers: additionalProperties: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -4853,6 +4965,8 @@ spec: message: description: HTTP message to override the default denial message. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -4874,6 +4988,8 @@ spec: description: HTTP response body to override the default denial body. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -4894,6 +5010,8 @@ spec: headers: additionalProperties: properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -4910,6 +5028,8 @@ spec: message: description: HTTP message to override the default denial message. properties: + expression: + type: string selector: description: |- Simple path selector to fetch content from the authorization JSON (e.g. 'request.method') or a string template with variables that resolve to patterns (e.g. "Hello, {auth.identity.name}!"). @@ -4948,6 +5068,10 @@ spec: any: {} required: - any + - properties: + predicate: {} + required: + - predicate properties: all: description: A list of pattern expressions to be evaluated as @@ -4977,6 +5101,8 @@ spec: patternRef: description: Reference to a named set of pattern expressions type: string + predicate: + type: string selector: description: |- Path selector to fetch content from the authorization JSON (e.g. 'request.method'). diff --git a/pkg/evaluators/authorization.go b/pkg/evaluators/authorization.go index 75df06d3..765d0d2c 100644 --- a/pkg/evaluators/authorization.go +++ b/pkg/evaluators/authorization.go @@ -57,11 +57,13 @@ func (config *AuthorizationConfig) Call(pipeline auth.AuthPipeline, ctx context. var cacheKey interface{} if cache != nil { - cacheKey = cache.ResolveKeyFor(pipeline.GetAuthorizationJSON()) - if cachedObj, err := cache.Get(cacheKey); err != nil { - logger.V(1).Error(err, "failed to retrieve data from the cache") - } else if cachedObj != nil { - return cachedObj, nil + cacheKey, _ = cache.ResolveKeyFor(pipeline.GetAuthorizationJSON()) + if cacheKey != nil { + if cachedObj, err := cache.Get(cacheKey); err != nil { + logger.V(1).Error(err, "failed to retrieve data from the cache") + } else if cachedObj != nil { + return cachedObj, nil + } } } diff --git a/pkg/evaluators/authorization/authzed.go b/pkg/evaluators/authorization/authzed.go index cb65acda..5bc00ee5 100644 --- a/pkg/evaluators/authorization/authzed.go +++ b/pkg/evaluators/authorization/authzed.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/kuadrant/authorino/pkg/auth" - "github.com/kuadrant/authorino/pkg/json" + "github.com/kuadrant/authorino/pkg/expressions" "google.golang.org/grpc" insecuregrpc "google.golang.org/grpc/credentials/insecure" @@ -19,11 +19,11 @@ type Authzed struct { Insecure bool SharedSecret string - Subject json.JSONValue - SubjectKind json.JSONValue - Resource json.JSONValue - ResourceKind json.JSONValue - Permission json.JSONValue + Subject expressions.Value + SubjectKind expressions.Value + Resource expressions.Value + ResourceKind expressions.Value + Permission expressions.Value } type permissionResponse struct { @@ -48,10 +48,22 @@ func (a *Authzed) Call(pipeline auth.AuthPipeline, ctx gocontext.Context) (inter authJSON := pipeline.GetAuthorizationJSON() + resource, err := authzedObjectFor(a.Resource, a.ResourceKind, authJSON) + if err != nil { + return nil, err + } + object, err := authzedObjectFor(a.Subject, a.SubjectKind, authJSON) + if err != nil { + return nil, err + } + permission, err := a.Permission.ResolveFor(authJSON) + if err != nil { + return nil, err + } resp, err := client.CheckPermission(ctx, &authzedpb.CheckPermissionRequest{ - Resource: authzedObjectFor(a.Resource, a.ResourceKind, authJSON), - Subject: &authzedpb.SubjectReference{Object: authzedObjectFor(a.Subject, a.SubjectKind, authJSON)}, - Permission: fmt.Sprintf("%s", a.Permission.ResolveFor(authJSON)), + Resource: resource, + Subject: &authzedpb.SubjectReference{Object: object}, + Permission: fmt.Sprintf("%s", permission), }) if err != nil { return nil, err @@ -74,9 +86,17 @@ func (a *Authzed) Call(pipeline auth.AuthPipeline, ctx gocontext.Context) (inter return obj, nil } -func authzedObjectFor(name, kind json.JSONValue, authJSON string) *authzedpb.ObjectReference { - return &authzedpb.ObjectReference{ - ObjectId: fmt.Sprintf("%s", name.ResolveFor(authJSON)), - ObjectType: fmt.Sprintf("%s", kind.ResolveFor(authJSON)), +func authzedObjectFor(name, kind expressions.Value, authJSON string) (*authzedpb.ObjectReference, error) { + objectId, err := name.ResolveFor(authJSON) + if err != nil { + return nil, err } + objectType, err := kind.ResolveFor(authJSON) + if err != nil { + return nil, err + } + return &authzedpb.ObjectReference{ + ObjectId: fmt.Sprintf("%s", objectId), + ObjectType: fmt.Sprintf("%s", objectType), + }, nil } diff --git a/pkg/evaluators/authorization/authzed_test.go b/pkg/evaluators/authorization/authzed_test.go index dd133654..ad29f482 100644 --- a/pkg/evaluators/authorization/authzed_test.go +++ b/pkg/evaluators/authorization/authzed_test.go @@ -52,11 +52,11 @@ func TestAuthzedCallAuthorized(t *testing.T) { Endpoint: testAuthzedServerEndpoint, Insecure: true, SharedSecret: "secret", - Subject: json.JSONValue{Static: "1"}, - SubjectKind: json.JSONValue{Static: "user"}, - Resource: json.JSONValue{Static: "123"}, - ResourceKind: json.JSONValue{Static: "post"}, - Permission: json.JSONValue{Static: "read"}, + Subject: &json.JSONValue{Static: "1"}, + SubjectKind: &json.JSONValue{Static: "user"}, + Resource: &json.JSONValue{Static: "123"}, + ResourceKind: &json.JSONValue{Static: "post"}, + Permission: &json.JSONValue{Static: "read"}, } obj, err := authzed.Call(pipelineMock, ctx) @@ -91,11 +91,11 @@ func TestAuthzedCallForbidden(t *testing.T) { Endpoint: testAuthzedServerEndpoint, Insecure: true, SharedSecret: "secret", - Subject: json.JSONValue{Static: "1"}, - SubjectKind: json.JSONValue{Static: "user"}, - Resource: json.JSONValue{Static: "123"}, - ResourceKind: json.JSONValue{Static: "post"}, - Permission: json.JSONValue{Static: "read"}, + Subject: &json.JSONValue{Static: "1"}, + SubjectKind: &json.JSONValue{Static: "user"}, + Resource: &json.JSONValue{Static: "123"}, + ResourceKind: &json.JSONValue{Static: "post"}, + Permission: &json.JSONValue{Static: "read"}, } obj, err := authzed.Call(pipelineMock, ctx) diff --git a/pkg/evaluators/authorization/kubernetes_authz.go b/pkg/evaluators/authorization/kubernetes_authz.go index e37fa80f..947327ea 100644 --- a/pkg/evaluators/authorization/kubernetes_authz.go +++ b/pkg/evaluators/authorization/kubernetes_authz.go @@ -7,7 +7,7 @@ import ( "github.com/kuadrant/authorino/pkg/auth" "github.com/kuadrant/authorino/pkg/context" - "github.com/kuadrant/authorino/pkg/json" + "github.com/kuadrant/authorino/pkg/expressions" "github.com/kuadrant/authorino/pkg/log" kubeAuthz "k8s.io/api/authorization/v1" @@ -21,7 +21,7 @@ type kubernetesSubjectAccessReviewer interface { SubjectAccessReviews() kubeAuthzClient.SubjectAccessReviewInterface } -func NewKubernetesAuthz(user json.JSONValue, groups []string, resourceAttributes *KubernetesAuthzResourceAttributes) (*KubernetesAuthz, error) { +func NewKubernetesAuthz(user expressions.Value, groups []string, resourceAttributes *KubernetesAuthzResourceAttributes) (*KubernetesAuthz, error) { config, err := rest.InClusterConfig() if err != nil { return nil, err @@ -41,16 +41,16 @@ func NewKubernetesAuthz(user json.JSONValue, groups []string, resourceAttributes } type KubernetesAuthzResourceAttributes struct { - Namespace json.JSONValue - Group json.JSONValue - Resource json.JSONValue - Name json.JSONValue - SubResource json.JSONValue - Verb json.JSONValue + Namespace expressions.Value + Group expressions.Value + Resource expressions.Value + Name expressions.Value + SubResource expressions.Value + Verb expressions.Value } type KubernetesAuthz struct { - User json.JSONValue + User expressions.Value Groups []string ResourceAttributes *KubernetesAuthzResourceAttributes @@ -63,26 +63,61 @@ func (k *KubernetesAuthz) Call(pipeline auth.AuthPipeline, ctx gocontext.Context } authJSON := pipeline.GetAuthorizationJSON() - jsonValueToStr := func(value json.JSONValue) string { - return fmt.Sprintf("%s", value.ResolveFor(authJSON)) + jsonValueToStr := func(value expressions.Value) (string, error) { + if value == nil { + return "", nil + } + resolved, err := value.ResolveFor(authJSON) + if err != nil { + return "", err + } + return fmt.Sprintf("%s", resolved), nil } + user, err := jsonValueToStr(k.User) + if err != nil { + return nil, err + } subjectAccessReview := kubeAuthz.SubjectAccessReview{ Spec: kubeAuthz.SubjectAccessReviewSpec{ - User: jsonValueToStr(k.User), + User: user, }, } if k.ResourceAttributes != nil { resourceAttributes := k.ResourceAttributes + namespace, err := jsonValueToStr(resourceAttributes.Namespace) + if err != nil { + return nil, err + } + group, err := jsonValueToStr(resourceAttributes.Group) + if err != nil { + return nil, err + } + resource, err := jsonValueToStr(resourceAttributes.Resource) + if err != nil { + return nil, err + } + name, err := jsonValueToStr(resourceAttributes.Name) + if err != nil { + return nil, err + } + subresource, err := jsonValueToStr(resourceAttributes.SubResource) + if err != nil { + return nil, err + } + verb, err := jsonValueToStr(resourceAttributes.Verb) + if err != nil { + return nil, err + } subjectAccessReview.Spec.ResourceAttributes = &kubeAuthz.ResourceAttributes{ - Namespace: jsonValueToStr(resourceAttributes.Namespace), - Group: jsonValueToStr(resourceAttributes.Group), - Resource: jsonValueToStr(resourceAttributes.Resource), - Name: jsonValueToStr(resourceAttributes.Name), - Subresource: jsonValueToStr(resourceAttributes.SubResource), - Verb: jsonValueToStr(resourceAttributes.Verb), + Namespace: namespace, + Group: group, + Resource: resource, + Name: name, + Subresource: subresource, + Verb: verb, } } else { request := pipeline.GetHttp() diff --git a/pkg/evaluators/authorization/kubernetes_authz_test.go b/pkg/evaluators/authorization/kubernetes_authz_test.go index f1c29e33..77a54826 100644 --- a/pkg/evaluators/authorization/kubernetes_authz_test.go +++ b/pkg/evaluators/authorization/kubernetes_authz_test.go @@ -5,6 +5,7 @@ import ( "testing" mock_auth "github.com/kuadrant/authorino/pkg/auth/mocks" + "github.com/kuadrant/authorino/pkg/expressions" "github.com/kuadrant/authorino/pkg/json" envoy_auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" @@ -58,7 +59,7 @@ func (client *k8sAuthorizationClientMock) GetRequest() kubeAuthz.SubjectAccessRe return client.request } -func newKubernetesAuthz(user json.JSONValue, groups []string, resourceAttributes *KubernetesAuthzResourceAttributes, subjectAccessReviewResponseStatus kubeAuthz.SubjectAccessReviewStatus) *KubernetesAuthz { +func newKubernetesAuthz(user expressions.Value, groups []string, resourceAttributes *KubernetesAuthzResourceAttributes, subjectAccessReviewResponseStatus kubeAuthz.SubjectAccessReviewStatus) *KubernetesAuthz { return &KubernetesAuthz{ User: user, Groups: groups, @@ -80,7 +81,7 @@ func TestKubernetesAuthzNonResource_Allowed(t *testing.T) { pipelineMock.EXPECT().GetHttp().Return(request) kubernetesAuth := newKubernetesAuthz( - json.JSONValue{Pattern: "auth.identity.username"}, + &json.JSONValue{Pattern: "auth.identity.username"}, []string{}, nil, kubeAuthz.SubjectAccessReviewStatus{Allowed: true, Reason: ""}, @@ -108,7 +109,7 @@ func TestKubernetesAuthzNonResource_Denied(t *testing.T) { pipelineMock.EXPECT().GetHttp().Return(request) kubernetesAuth := newKubernetesAuthz( - json.JSONValue{Pattern: "auth.identity.username"}, + &json.JSONValue{Pattern: "auth.identity.username"}, []string{}, nil, kubeAuthz.SubjectAccessReviewStatus{Allowed: false, Reason: "some-reason"}, @@ -133,9 +134,9 @@ func TestKubernetesAuthzResource_Allowed(t *testing.T) { pipelineMock.EXPECT().GetAuthorizationJSON().Return(`{"context":{"request":{"http":{"method":"GET","path":"/hello"}}},"auth":{"identity":{"username":"john"}}}`) kubernetesAuth := newKubernetesAuthz( - json.JSONValue{Pattern: "auth.identity.username"}, + &json.JSONValue{Pattern: "auth.identity.username"}, []string{}, - &KubernetesAuthzResourceAttributes{Namespace: json.JSONValue{Static: "default"}}, + &KubernetesAuthzResourceAttributes{Namespace: &json.JSONValue{Static: "default"}}, kubeAuthz.SubjectAccessReviewStatus{Allowed: true, Reason: ""}, ) authorized, err := kubernetesAuth.Call(pipelineMock, context.TODO()) @@ -157,9 +158,9 @@ func TestKubernetesAuthzResource_Denied(t *testing.T) { pipelineMock.EXPECT().GetAuthorizationJSON().Return(`{"context":{"request":{"http":{"method":"GET","path":"/hello"}}},"auth":{"identity":{"username":"john"}}}`) kubernetesAuth := newKubernetesAuthz( - json.JSONValue{Pattern: "auth.identity.username"}, + &json.JSONValue{Pattern: "auth.identity.username"}, []string{}, - &KubernetesAuthzResourceAttributes{Namespace: json.JSONValue{Static: "default"}}, + &KubernetesAuthzResourceAttributes{Namespace: &json.JSONValue{Static: "default"}}, kubeAuthz.SubjectAccessReviewStatus{Allowed: false, Reason: "some-reason"}, ) authorized, err := kubernetesAuth.Call(pipelineMock, context.TODO()) diff --git a/pkg/evaluators/cache.go b/pkg/evaluators/cache.go index 05aa4559..1bf03f52 100644 --- a/pkg/evaluators/cache.go +++ b/pkg/evaluators/cache.go @@ -4,7 +4,7 @@ import ( gojson "encoding/json" "time" - "github.com/kuadrant/authorino/pkg/json" + "github.com/kuadrant/authorino/pkg/expressions" "github.com/coocood/freecache" gocache "github.com/eko/gocache/cache" @@ -16,11 +16,11 @@ var EvaluatorCacheSize int // in megabytes type EvaluatorCache interface { Get(key interface{}) (interface{}, error) Set(key, value interface{}) error - ResolveKeyFor(authJSON string) interface{} + ResolveKeyFor(authJSON string) (interface{}, error) Shutdown() error } -func NewEvaluatorCache(keyTemplate json.JSONValue, ttl int) EvaluatorCache { +func NewEvaluatorCache(keyTemplate expressions.Value, ttl int) EvaluatorCache { duration := time.Duration(ttl) * time.Second cacheClient := freecache.NewCache(EvaluatorCacheSize * 1024 * 1024) cacheStore := cache_store.NewFreecache(cacheClient, &cache_store.Options{Expiration: duration}) @@ -33,7 +33,7 @@ func NewEvaluatorCache(keyTemplate json.JSONValue, ttl int) EvaluatorCache { // evaluatorCache caches JSON values (objects, arrays, strings, etc) type evaluatorCache struct { - keyTemplate json.JSONValue + keyTemplate expressions.Value store *gocache.Cache } @@ -58,7 +58,7 @@ func (c *evaluatorCache) Set(key, value interface{}) error { } } -func (c *evaluatorCache) ResolveKeyFor(authJSON string) interface{} { +func (c *evaluatorCache) ResolveKeyFor(authJSON string) (interface{}, error) { return c.keyTemplate.ResolveFor(authJSON) } diff --git a/pkg/evaluators/config.go b/pkg/evaluators/config.go index 3d0f58c4..a2b8c13b 100644 --- a/pkg/evaluators/config.go +++ b/pkg/evaluators/config.go @@ -6,6 +6,7 @@ import ( "sync" "github.com/kuadrant/authorino/pkg/auth" + "github.com/kuadrant/authorino/pkg/expressions" "github.com/kuadrant/authorino/pkg/json" "github.com/kuadrant/authorino/pkg/jsonexp" @@ -74,7 +75,7 @@ type DenyWith struct { type DenyWithValues struct { Code int32 - Message *json.JSONValue + Message expressions.Value Headers []json.JSONProperty - Body *json.JSONValue + Body expressions.Value } diff --git a/pkg/evaluators/identity.go b/pkg/evaluators/identity.go index 1c712f01..0bce9cbf 100644 --- a/pkg/evaluators/identity.go +++ b/pkg/evaluators/identity.go @@ -84,11 +84,13 @@ func (config *IdentityConfig) Call(pipeline auth.AuthPipeline, ctx context.Conte var cacheKey interface{} if cache != nil { - cacheKey = cache.ResolveKeyFor(pipeline.GetAuthorizationJSON()) - if cachedObj, err := cache.Get(cacheKey); err != nil { - logger.V(1).Error(err, "failed to retrieve data from the cache") - } else if cachedObj != nil { - return cachedObj, nil + cacheKey, _ = cache.ResolveKeyFor(pipeline.GetAuthorizationJSON()) + if cacheKey != nil { + if cachedObj, err := cache.Get(cacheKey); err != nil { + logger.V(1).Error(err, "failed to retrieve data from the cache") + } else if cachedObj != nil { + return cachedObj, nil + } } } @@ -197,7 +199,11 @@ func (config *IdentityConfig) ResolveExtendedProperties(pipeline auth.AuthPipeli authJSON := pipeline.GetAuthorizationJSON() for _, extendedProperty := range config.ExtendedProperties { - extendedIdentityObject[extendedProperty.Name] = extendedProperty.ResolveFor(extendedIdentityObject, authJSON) + resolved, err := extendedProperty.ResolveFor(extendedIdentityObject, authJSON) + if err != nil { + return nil, err + } + extendedIdentityObject[extendedProperty.Name] = resolved } return extendedIdentityObject, nil diff --git a/pkg/evaluators/identity/plain.go b/pkg/evaluators/identity/plain.go index d30f0636..b44d7824 100644 --- a/pkg/evaluators/identity/plain.go +++ b/pkg/evaluators/identity/plain.go @@ -7,18 +7,20 @@ import ( "net/http" "github.com/kuadrant/authorino/pkg/auth" - "github.com/kuadrant/authorino/pkg/json" + "github.com/kuadrant/authorino/pkg/expressions" envoy_auth "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3" ) type Plain struct { + Value expressions.Value Pattern string } func (p *Plain) Call(pipeline auth.AuthPipeline, ctx context.Context) (interface{}, error) { - pattern := json.JSONValue{Pattern: p.Pattern} - if object := pattern.ResolveFor(pipeline.GetAuthorizationJSON()); object != nil { + if object, err := p.Value.ResolveFor(pipeline.GetAuthorizationJSON()); err != nil { + return nil, err + } else if object != nil { return object, nil } return nil, fmt.Errorf("could not retrieve identity object or null") diff --git a/pkg/evaluators/identity/plain_test.go b/pkg/evaluators/identity/plain_test.go index 18d8fe21..ed0ecc41 100644 --- a/pkg/evaluators/identity/plain_test.go +++ b/pkg/evaluators/identity/plain_test.go @@ -6,6 +6,7 @@ import ( "testing" mock_auth "github.com/kuadrant/authorino/pkg/auth/mocks" + j "github.com/kuadrant/authorino/pkg/json" "github.com/golang/mock/gomock" "gotest.tools/assert" @@ -18,7 +19,7 @@ func TestPlainCall(t *testing.T) { pipelineMock := mock_auth.NewMockAuthPipeline(ctrl) pipelineMock.EXPECT().GetAuthorizationJSON().Return(`{"context":{"request":{"http":{"body":"{\"username\":\"john\"}"}}}}`) - plain := &Plain{Pattern: "context.request.http.body.@fromstr"} + plain := &Plain{Value: &j.JSONValue{Pattern: "context.request.http.body.@fromstr"}, Pattern: "context.request.http.body.@fromstr"} id, err := plain.Call(pipelineMock, nil) assert.NilError(t, err) j, _ := json.Marshal(id) @@ -32,7 +33,7 @@ func TestPlainCallWithUresolvableObject(t *testing.T) { pipelineMock := mock_auth.NewMockAuthPipeline(ctrl) pipelineMock.EXPECT().GetAuthorizationJSON().Return(`{}`) - plain := &Plain{Pattern: "context.request.http.body.@fromstr"} + plain := &Plain{Value: &j.JSONValue{Pattern: "context.request.http.body.@fromstr"}, Pattern: "context.request.http.body.@fromstr"} id, err := plain.Call(pipelineMock, nil) assert.ErrorContains(t, err, "could not retrieve identity object") assert.Check(t, id == nil) @@ -45,19 +46,19 @@ func TestPlainCallWithInvalidPatttern(t *testing.T) { pipelineMock := mock_auth.NewMockAuthPipeline(ctrl) pipelineMock.EXPECT().GetAuthorizationJSON().Return(`{"context":{"request":{"http":{"body":"{\"username\":\"john\"}"}}}}`) - plain := &Plain{Pattern: "not a valid json path"} + plain := &Plain{Value: &j.JSONValue{Pattern: "not a valid json path"}, Pattern: "not a valid json path"} id, err := plain.Call(pipelineMock, nil) assert.ErrorContains(t, err, "could not retrieve identity object") assert.Check(t, id == nil) } func TestPlainGetCredentialsKeySelector(t *testing.T) { - plain := &Plain{Pattern: "context.request.http.body.@fromstr"} + plain := &Plain{Value: &j.JSONValue{Pattern: "context.request.http.body.@fromstr"}, Pattern: "context.request.http.body.@fromstr"} assert.Equal(t, plain.GetCredentialsKeySelector(), "context.request.http.body.@fromstr") } func TestPlainGetCredentialsIn(t *testing.T) { - plain := &Plain{Pattern: "context.request.http.body.@fromstr"} + plain := &Plain{Value: &j.JSONValue{Pattern: "context.request.http.body.@fromstr"}, Pattern: "context.request.http.body.@fromstr"} assert.Equal(t, plain.GetCredentialsIn(), "context.request.http.body.@fromstr") } diff --git a/pkg/evaluators/identity_extension.go b/pkg/evaluators/identity_extension.go index b945e10e..29c20f14 100644 --- a/pkg/evaluators/identity_extension.go +++ b/pkg/evaluators/identity_extension.go @@ -1,14 +1,18 @@ package evaluators -import "github.com/kuadrant/authorino/pkg/json" +import ( + "github.com/kuadrant/authorino/pkg/expressions" + "github.com/kuadrant/authorino/pkg/json" +) -func NewIdentityExtension(name string, value json.JSONValue, overwrite bool) IdentityExtension { +func NewIdentityExtension(name string, value expressions.Value, overwrite bool) IdentityExtension { + property := json.JSONProperty{ + Name: name, + Value: value, + } return IdentityExtension{ - JSONProperty: json.JSONProperty{ - Name: name, - Value: value, - }, - Overwrite: overwrite, + JSONProperty: property, + Overwrite: overwrite, } } @@ -17,9 +21,9 @@ type IdentityExtension struct { Overwrite bool } -func (i *IdentityExtension) ResolveFor(identityObject map[string]any, authJSON string) interface{} { +func (i *IdentityExtension) ResolveFor(identityObject map[string]any, authJSON string) (interface{}, error) { if value, exists := identityObject[i.Name]; exists && !i.Overwrite { - return value + return value, nil } return i.Value.ResolveFor(authJSON) } diff --git a/pkg/evaluators/identity_extension_test.go b/pkg/evaluators/identity_extension_test.go index edeb3e73..34ee0f2b 100644 --- a/pkg/evaluators/identity_extension_test.go +++ b/pkg/evaluators/identity_extension_test.go @@ -23,68 +23,70 @@ func TestResolveIdentityExtension(t *testing.T) { }{ { name: "static value for existing property without overwrite", - input: NewIdentityExtension("username", json.JSONValue{Static: "foo"}, false), + input: NewIdentityExtension("username", &json.JSONValue{Static: "foo"}, false), expected: "beth", }, { name: "static value for missing property without overwrite", - input: NewIdentityExtension("uid", json.JSONValue{Static: "foo"}, false), + input: NewIdentityExtension("uid", &json.JSONValue{Static: "foo"}, false), expected: "foo", }, { name: "static value for existing property without overwrite", - input: NewIdentityExtension("username", json.JSONValue{Static: "foo"}, true), + input: NewIdentityExtension("username", &json.JSONValue{Static: "foo"}, true), expected: "foo", }, { name: "static value for missing property without overwrite", - input: NewIdentityExtension("uid", json.JSONValue{Static: "foo"}, true), + input: NewIdentityExtension("uid", &json.JSONValue{Static: "foo"}, true), expected: "foo", }, { name: "existing pattern for existing property without overwrite", - input: NewIdentityExtension("username", json.JSONValue{Pattern: "auth.identity.sub"}, false), + input: NewIdentityExtension("username", &json.JSONValue{Pattern: "auth.identity.sub"}, false), expected: "beth", }, { name: "existing pattern for missing property without overwrite", - input: NewIdentityExtension("uid", json.JSONValue{Pattern: "auth.identity.sub"}, false), + input: NewIdentityExtension("uid", &json.JSONValue{Pattern: "auth.identity.sub"}, false), expected: "1234567890", }, { name: "existing pattern for existing property without overwrite", - input: NewIdentityExtension("username", json.JSONValue{Pattern: "auth.identity.sub"}, true), + input: NewIdentityExtension("username", &json.JSONValue{Pattern: "auth.identity.sub"}, true), expected: "1234567890", }, { name: "existing pattern for missing property without overwrite", - input: NewIdentityExtension("uid", json.JSONValue{Pattern: "auth.identity.sub"}, true), + input: NewIdentityExtension("uid", &json.JSONValue{Pattern: "auth.identity.sub"}, true), expected: "1234567890", }, { name: "missing pattern for existing property without overwrite", - input: NewIdentityExtension("username", json.JSONValue{Pattern: "auth.identity.full_name"}, false), + input: NewIdentityExtension("username", &json.JSONValue{Pattern: "auth.identity.full_name"}, false), expected: "beth", }, { name: "missing pattern for missing property without overwrite", - input: NewIdentityExtension("uid", json.JSONValue{Pattern: "auth.identity.full_name"}, false), + input: NewIdentityExtension("uid", &json.JSONValue{Pattern: "auth.identity.full_name"}, false), expected: "", }, { name: "missing pattern for existing property without overwrite", - input: NewIdentityExtension("username", json.JSONValue{Pattern: "auth.identity.full_name"}, true), + input: NewIdentityExtension("username", &json.JSONValue{Pattern: "auth.identity.full_name"}, true), expected: "", }, { name: "missing pattern for missing property without overwrite", - input: NewIdentityExtension("uid", json.JSONValue{Pattern: "auth.identity.full_name"}, true), + input: NewIdentityExtension("uid", &json.JSONValue{Pattern: "auth.identity.full_name"}, true), expected: "", }, } for _, tc := range testCases { - actual, _ := json.StringifyJSON(tc.input.ResolveFor(obj, authJSON)) + resolved, err := tc.input.ResolveFor(obj, authJSON) + assert.NilError(t, err) + actual, _ := json.StringifyJSON(resolved) assert.Equal(t, actual, tc.expected, fmt.Sprintf("%s failed: got '%s', want '%s'", tc.name, string(actual), string(tc.expected))) } } diff --git a/pkg/evaluators/identity_test.go b/pkg/evaluators/identity_test.go index 8e16f6f2..39620adf 100644 --- a/pkg/evaluators/identity_test.go +++ b/pkg/evaluators/identity_test.go @@ -41,8 +41,8 @@ func TestIdentityConfig_ResolveExtendedProperties(t *testing.T) { Name: "test", KubernetesAuth: &identity.KubernetesAuth{}, ExtendedProperties: []IdentityExtension{ - NewIdentityExtension("prop1", json.JSONValue{Static: "value1"}, true), - NewIdentityExtension("prop2", json.JSONValue{Pattern: "auth.identity.sub"}, true), + NewIdentityExtension("prop1", &json.JSONValue{Static: "value1"}, true), + NewIdentityExtension("prop2", &json.JSONValue{Pattern: "auth.identity.sub"}, true), }, } diff --git a/pkg/evaluators/metadata.go b/pkg/evaluators/metadata.go index e32eb01e..02fc2602 100644 --- a/pkg/evaluators/metadata.go +++ b/pkg/evaluators/metadata.go @@ -53,11 +53,13 @@ func (config *MetadataConfig) Call(pipeline auth.AuthPipeline, ctx context.Conte var cacheKey interface{} if cache != nil { - cacheKey = cache.ResolveKeyFor(pipeline.GetAuthorizationJSON()) - if cachedObj, err := cache.Get(cacheKey); err != nil { - logger.V(1).Error(err, "failed to retrieve data from the cache") - } else if cachedObj != nil { - return cachedObj, nil + cacheKey, _ = cache.ResolveKeyFor(pipeline.GetAuthorizationJSON()) + if cacheKey != nil { + if cachedObj, err := cache.Get(cacheKey); err != nil { + logger.V(1).Error(err, "failed to retrieve data from the cache") + } else if cachedObj != nil { + return cachedObj, nil + } } } diff --git a/pkg/evaluators/metadata/generic_http.go b/pkg/evaluators/metadata/generic_http.go index f9a8417f..437f21eb 100644 --- a/pkg/evaluators/metadata/generic_http.go +++ b/pkg/evaluators/metadata/generic_http.go @@ -12,6 +12,7 @@ import ( "github.com/kuadrant/authorino/pkg/auth" "github.com/kuadrant/authorino/pkg/context" + "github.com/kuadrant/authorino/pkg/expressions" "github.com/kuadrant/authorino/pkg/json" "github.com/kuadrant/authorino/pkg/log" "github.com/kuadrant/authorino/pkg/oauth2" @@ -22,8 +23,9 @@ import ( type GenericHttp struct { Endpoint string + DynamicEndpoint expressions.Value Method string - Body *json.JSONValue + Body expressions.Value Parameters []json.JSONProperty Headers []json.JSONProperty ContentType string @@ -39,7 +41,16 @@ func (h *GenericHttp) Call(pipeline auth.AuthPipeline, ctx gocontext.Context) (i } authJSON := pipeline.GetAuthorizationJSON() - endpoint := json.ReplaceJSONPlaceholders(h.Endpoint, authJSON) + var endpoint string + if h.DynamicEndpoint != nil { + if val, err := h.DynamicEndpoint.ResolveFor(authJSON); err != nil { + return nil, err + } else { + endpoint = val.(string) + } + } else { + endpoint = json.ReplaceJSONPlaceholders(h.Endpoint, authJSON) + } req, err := h.buildRequest(ctx, endpoint, authJSON) if err != nil { @@ -127,7 +138,11 @@ func (h *GenericHttp) buildRequest(ctx gocontext.Context, endpoint, authJSON str } for _, header := range h.Headers { - req.Header.Set(header.Name, fmt.Sprintf("%s", header.Value.ResolveFor(authJSON))) + headerValue, err := header.Value.ResolveFor(authJSON) + if err != nil { + return nil, err + } + req.Header.Set(header.Name, fmt.Sprintf("%s", headerValue)) } req.Header.Set("Content-Type", contentType) @@ -152,16 +167,24 @@ func (h *GenericHttp) buildRequest(ctx gocontext.Context, endpoint, authJSON str func (h *GenericHttp) buildRequestBody(authData string) (io.Reader, error) { if h.Body != nil { - if body, err := json.StringifyJSON(h.Body.ResolveFor(authData)); err != nil { - return nil, fmt.Errorf("failed to encode http request") + if resolved, err := h.Body.ResolveFor(authData); err != nil { + return nil, err } else { - return bytes.NewBufferString(body), nil + if body, err := json.StringifyJSON(resolved); err != nil { + return nil, fmt.Errorf("failed to encode http request") + } else { + return bytes.NewBufferString(body), nil + } } } data := make(map[string]interface{}) for _, param := range h.Parameters { - data[param.Name] = param.Value.ResolveFor(authData) + if resolved, err := param.Value.ResolveFor(authData); err != nil { + return nil, err + } else { + data[param.Name] = resolved + } } switch h.ContentType { diff --git a/pkg/evaluators/metadata/generic_http_test.go b/pkg/evaluators/metadata/generic_http_test.go index b00ecbda..3f56a117 100644 --- a/pkg/evaluators/metadata/generic_http_test.go +++ b/pkg/evaluators/metadata/generic_http_test.go @@ -80,7 +80,7 @@ func TestGenericHttpCallWithPOST(t *testing.T) { metadata := &GenericHttp{ Endpoint: endpoint, Method: "POST", - Parameters: []json.JSONProperty{{Name: "user", Value: json.JSONValue{Pattern: "auth.identity.user"}}}, + Parameters: []json.JSONProperty{{Name: "user", Value: &json.JSONValue{Pattern: "auth.identity.user"}}}, ContentType: "application/x-www-form-urlencoded", SharedSecret: "secret", AuthCredentials: sharedCredsMock, @@ -226,8 +226,8 @@ func TestGenericHttpCallWithCustomHeaders(t *testing.T) { Endpoint: endpoint, Method: "GET", Headers: []json.JSONProperty{ - {Name: "X-Requested-By", Value: json.JSONValue{Static: "authorino"}}, - {Name: "Content-Type", Value: json.JSONValue{Static: "to-be-overwritten"}}, + {Name: "X-Requested-By", Value: &json.JSONValue{Static: "authorino"}}, + {Name: "Content-Type", Value: &json.JSONValue{Static: "to-be-overwritten"}}, }, AuthCredentials: sharedCredsMock, } diff --git a/pkg/evaluators/metadata_test.go b/pkg/evaluators/metadata_test.go index c2cf70e0..66caeba3 100644 --- a/pkg/evaluators/metadata_test.go +++ b/pkg/evaluators/metadata_test.go @@ -59,7 +59,7 @@ func TestMetadataCaching(t *testing.T) { assert.NilError(t, err) // With caching of metadata - cache := NewEvaluatorCache(json.JSONValue{Static: "x"}, 2) // 2 seconds ttl + cache := NewEvaluatorCache(&json.JSONValue{Static: "x"}, 2) // 2 seconds ttl metadataConfig.Cache = cache defer metadataConfig.Clean(context.TODO()) diff --git a/pkg/evaluators/response.go b/pkg/evaluators/response.go index 6ee4b156..b3a4af00 100644 --- a/pkg/evaluators/response.go +++ b/pkg/evaluators/response.go @@ -82,11 +82,13 @@ func (config *ResponseConfig) Call(pipeline auth.AuthPipeline, ctx context.Conte var cacheKey interface{} if cache != nil { - cacheKey = cache.ResolveKeyFor(pipeline.GetAuthorizationJSON()) - if cachedObj, err := cache.Get(cacheKey); err != nil { - logger.V(1).Error(err, "failed to retrieve data from the cache") - } else if cachedObj != nil { - return cachedObj, nil + cacheKey, _ = cache.ResolveKeyFor(pipeline.GetAuthorizationJSON()) + if cacheKey != nil { + if cachedObj, err := cache.Get(cacheKey); err != nil { + logger.V(1).Error(err, "failed to retrieve data from the cache") + } else if cachedObj != nil { + return cachedObj, nil + } } } diff --git a/pkg/evaluators/response/dynamic_json.go b/pkg/evaluators/response/dynamic_json.go index 7ab9d2ae..8dc04e00 100644 --- a/pkg/evaluators/response/dynamic_json.go +++ b/pkg/evaluators/response/dynamic_json.go @@ -24,7 +24,11 @@ func (j *DynamicJSON) Call(pipeline auth.AuthPipeline, ctx context.Context) (int for _, property := range j.Properties { value := property.Value - obj[property.Name] = value.ResolveFor(authJSON) + if resolved, err := value.ResolveFor(authJSON); err != nil { + return nil, err + } else { + obj[property.Name] = resolved + } } return obj, nil diff --git a/pkg/evaluators/response/dynamic_json_test.go b/pkg/evaluators/response/dynamic_json_test.go index 47d48b93..1b064e6c 100644 --- a/pkg/evaluators/response/dynamic_json_test.go +++ b/pkg/evaluators/response/dynamic_json_test.go @@ -17,8 +17,8 @@ func TestDynamicJSONCall(t *testing.T) { defer ctrl.Finish() jsonProperties := []json.JSONProperty{ - {Name: "prop1", Value: json.JSONValue{Static: "value1"}}, - {Name: "prop2", Value: json.JSONValue{Pattern: "auth.identity.username"}}, + {Name: "prop1", Value: &json.JSONValue{Static: "value1"}}, + {Name: "prop2", Value: &json.JSONValue{Pattern: "auth.identity.username"}}, } jsonResponseEvaluator := NewDynamicJSONResponse(jsonProperties) diff --git a/pkg/evaluators/response/plain.go b/pkg/evaluators/response/plain.go index c84f5ded..6b3611b6 100644 --- a/pkg/evaluators/response/plain.go +++ b/pkg/evaluators/response/plain.go @@ -4,14 +4,14 @@ import ( "context" "github.com/kuadrant/authorino/pkg/auth" - "github.com/kuadrant/authorino/pkg/json" + "github.com/kuadrant/authorino/pkg/expressions" ) type Plain struct { - json.JSONValue + expressions.Value } func (p *Plain) Call(pipeline auth.AuthPipeline, ctx context.Context) (interface{}, error) { authJSON := pipeline.GetAuthorizationJSON() - return p.ResolveFor(authJSON), nil + return p.ResolveFor(authJSON) } diff --git a/pkg/evaluators/response/plain_test.go b/pkg/evaluators/response/plain_test.go index 76ad5ee1..993e63da 100644 --- a/pkg/evaluators/response/plain_test.go +++ b/pkg/evaluators/response/plain_test.go @@ -6,6 +6,7 @@ import ( "testing" mock_auth "github.com/kuadrant/authorino/pkg/auth/mocks" + "github.com/kuadrant/authorino/pkg/json" "gotest.tools/assert" "github.com/golang/mock/gomock" @@ -16,7 +17,9 @@ func TestPlainCallWithStaticValue(t *testing.T) { defer ctrl.Finish() ev := Plain{} - ev.Static = "value1" + ev.Value = &json.JSONValue{ + Static: "value1", + } pipelineMock := mock_auth.NewMockAuthPipeline(ctrl) pipelineMock.EXPECT().GetAuthorizationJSON().Return(`{"auth":{"identity":{"username":"john"}}}`) @@ -32,7 +35,9 @@ func TestPlainCallWithPattern(t *testing.T) { defer ctrl.Finish() ev := Plain{} - ev.Pattern = "auth.identity.username" + ev.Value = &json.JSONValue{ + Pattern: "auth.identity.username", + } pipelineMock := mock_auth.NewMockAuthPipeline(ctrl) pipelineMock.EXPECT().GetAuthorizationJSON().Return(`{"auth":{"identity":{"username":"john"}}}`) diff --git a/pkg/evaluators/response/wristband.go b/pkg/evaluators/response/wristband.go index db811703..1851d960 100644 --- a/pkg/evaluators/response/wristband.go +++ b/pkg/evaluators/response/wristband.go @@ -120,7 +120,11 @@ func (w *Wristband) Call(pipeline auth.AuthPipeline, ctx context.Context) (inter for _, claim := range w.CustomClaims { value := claim.Value - claims[claim.Name] = value.ResolveFor(authJSON) + if resolved, err := value.ResolveFor(authJSON); err != nil { + return nil, err + } else { + claims[claim.Name] = resolved + } } } diff --git a/pkg/evaluators/response/wristband_test.go b/pkg/evaluators/response/wristband_test.go index 354ee91e..aebd8b7c 100644 --- a/pkg/evaluators/response/wristband_test.go +++ b/pkg/evaluators/response/wristband_test.go @@ -160,11 +160,11 @@ func TestWristbandCall(t *testing.T) { claims := []json.JSONProperty{ { Name: "sta", - Value: json.JSONValue{Static: "foo"}, + Value: &json.JSONValue{Static: "foo"}, }, { Name: "dyn", - Value: json.JSONValue{Pattern: "auth.identity"}, + Value: &json.JSONValue{Pattern: "auth.identity"}, }, } signingKey, _ := NewSigningKey("my-signing-key", "ES256", []byte(ellipticCurveSigningKey)) diff --git a/pkg/expressions/cel/expressions.go b/pkg/expressions/cel/expressions.go new file mode 100644 index 00000000..c75959fe --- /dev/null +++ b/pkg/expressions/cel/expressions.go @@ -0,0 +1,164 @@ +package cel + +import ( + "fmt" + "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/tidwall/gjson" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/structpb" +) + +const RootMetadataBinding = "metadata" +const RootRequestBinding = "request" +const RootSourceBinding = "source" +const RootDestinationBinding = "destination" +const RootAuthBinding = "auth" + +type Predicate struct { + program cel.Program + source string +} + +func NewPredicate(source string) (*Predicate, error) { + program, err := Compile(source, cel.BoolType) + if err != nil { + return nil, err + } + return &Predicate{ + program: program, + source: source, + }, nil +} + +func (p *Predicate) Matches(json string) (bool, error) { + input, err := AuthJsonToCel(json) + if err != nil { + return false, err + } + result, _, err := p.program.Eval(input) + if err != nil { + return false, err + } + return result.Value().(bool), nil +} + +type Expression struct { + program cel.Program + source string +} + +type StringExpression struct { + expression Expression +} + +func NewExpression(source string) (*Expression, error) { + program, err := Compile(source, nil) + if err != nil { + return nil, err + } + return &Expression{ + program: program, + source: source, + }, nil +} + +func (e *Expression) ResolveFor(json string) (interface{}, error) { + result, _, err := e.Evaluate(json) + if err != nil { + return nil, err + } + + // this is for backwards compatibility with JSONValue, these should interoperate seamlessly this way + if jsonLiteral, err := ValueToJSON(result); err != nil { + return nil, err + } else { + return gjson.Parse(jsonLiteral).Value(), nil + } +} + +func (e *Expression) Evaluate(json string) (ref.Val, *cel.EvalDetails, error) { + input, err := AuthJsonToCel(json) + if err != nil { + return nil, nil, err + } + + return e.program.Eval(input) +} + +func Compile(expression string, expectedType *cel.Type, opts ...cel.EnvOption) (cel.Program, error) { + envOpts := append([]cel.EnvOption{cel.Declarations( + decls.NewConst(RootMetadataBinding, decls.NewObjectType("google.protobuf.Struct"), nil), + decls.NewConst(RootRequestBinding, decls.NewObjectType("google.protobuf.Struct"), nil), + decls.NewConst(RootSourceBinding, decls.NewObjectType("google.protobuf.Struct"), nil), + decls.NewConst(RootDestinationBinding, decls.NewObjectType("google.protobuf.Struct"), nil), + decls.NewConst(RootAuthBinding, decls.NewObjectType("google.protobuf.Struct"), nil), + )}, opts...) + env, env_err := cel.NewEnv(envOpts...) + if env_err != nil { + return nil, env_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() + } + + if expectedType != nil { + if !reflect.DeepEqual(checked.OutputType(), expectedType) && !reflect.DeepEqual(checked.OutputType(), cel.DynType) { + return nil, fmt.Errorf("type error: got %v, wanted %v output type", checked.OutputType(), expectedType) + } + } + + program, err := env.Program(checked) + if err != nil { + return nil, err + } + return program, 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 +} + +// todo this should eventually be sourced as proper proto from the pipeline +func AuthJsonToCel(json string) (map[string]interface{}, error) { + data := structpb.Struct{} + if err := jsonpb.Unmarshal(strings.NewReader(json), &data); err != nil { + return nil, err + } + metadata := data.GetFields()[RootMetadataBinding] + request := data.GetFields()[RootRequestBinding] + source := data.GetFields()[RootSourceBinding] + destination := data.GetFields()[RootDestinationBinding] + auth := data.GetFields()[RootAuthBinding] + + input := map[string]interface{}{ + RootMetadataBinding: metadata, + RootRequestBinding: request, + RootSourceBinding: source, + RootDestinationBinding: destination, + RootAuthBinding: auth, + } + return input, nil +} diff --git a/pkg/expressions/cel/expressions_test.go b/pkg/expressions/cel/expressions_test.go new file mode 100644 index 00000000..587ac4cd --- /dev/null +++ b/pkg/expressions/cel/expressions_test.go @@ -0,0 +1,44 @@ +package cel + +import ( + "testing" + + mock_auth "github.com/kuadrant/authorino/pkg/auth/mocks" + "gotest.tools/assert" + + "github.com/golang/mock/gomock" +) + +func TestPredicate(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + predicate, err := NewPredicate(`auth`) + assert.ErrorContains(t, err, "wanted bool output type") + + pipelineMock := mock_auth.NewMockAuthPipeline(ctrl) + + predicate, err = NewPredicate(`false == true`) + assert.NilError(t, err) + + pipelineMock.EXPECT().GetAuthorizationJSON().Return(`{"auth":{"identity":{"username":"john","evil": false}}}`) + response, err := predicate.Matches(pipelineMock.GetAuthorizationJSON()) + assert.NilError(t, err) + assert.Equal(t, response, false) + + predicate, err = NewPredicate(`auth.identity.evil == false`) + assert.NilError(t, err) + + pipelineMock.EXPECT().GetAuthorizationJSON().Return(`{"auth":{"identity":{"username":"john","evil": false}}}`) + response, err = predicate.Matches(pipelineMock.GetAuthorizationJSON()) + assert.NilError(t, err) + assert.Equal(t, response, true) + + predicate, err = NewPredicate(`request.http.method == "GET"`) + assert.NilError(t, err) + + pipelineMock.EXPECT().GetAuthorizationJSON().Return(`{"request":{"http": {"method": "GET"}}}`) + response, err = predicate.Matches(pipelineMock.GetAuthorizationJSON()) + assert.NilError(t, err) + assert.Equal(t, response, true) +} diff --git a/pkg/expressions/types.go b/pkg/expressions/types.go new file mode 100644 index 00000000..d0406fa9 --- /dev/null +++ b/pkg/expressions/types.go @@ -0,0 +1,5 @@ +package expressions + +type Value interface { + ResolveFor(jsonData string) (interface{}, error) +} diff --git a/pkg/json/json.go b/pkg/json/json.go index b7eddcd4..738b102a 100644 --- a/pkg/json/json.go +++ b/pkg/json/json.go @@ -11,6 +11,8 @@ import ( "strings" "unicode" + "github.com/kuadrant/authorino/pkg/expressions" + "github.com/tidwall/gjson" ) @@ -23,7 +25,7 @@ var ( // a pattern for a value fetched dynamically from the authorization JSON type JSONProperty struct { Name string - Value JSONValue + Value expressions.Value } type JSONValue struct { @@ -38,7 +40,11 @@ type JSONValue struct { // simple pattern or as a template that mixes static value with variable placeholders that resolve to patterns. // In case of a template that mixes no variable placeholder, but it contains nothing but a static string value, users // should use `JSONValue.Static` instead of `JSONValue.Pattern`. -func (v *JSONValue) ResolveFor(jsonData string) interface{} { +func (v *JSONValue) ResolveFor(jsonData string) (interface{}, error) { + return v.resolveForSafe(jsonData), nil +} + +func (v *JSONValue) resolveForSafe(jsonData string) interface{} { if v.Pattern != "" { // If all curly braces in the pattern are for passing arguments to modifiers, then it's likely NOT a template. // To be a template, the pattern must contain at least one curly brace delimiting a variable placeholder. diff --git a/pkg/json/json_test.go b/pkg/json/json_test.go index 0205e048..b5510115 100644 --- a/pkg/json/json_test.go +++ b/pkg/json/json_test.go @@ -33,17 +33,17 @@ func TestJSONValueResolveFor(t *testing.T) { var resolvedValueAsJSON []byte value = JSONValue{Static: "foo"} - assert.Equal(t, value.ResolveFor(jsonData), "foo") - assert.Equal(t, value.ResolveFor(""), "foo") + assert.Equal(t, value.resolveForSafe(jsonData), "foo") + assert.Equal(t, value.resolveForSafe(""), "foo") value = JSONValue{Pattern: "auth.identity.username"} - assert.Equal(t, value.ResolveFor(jsonData), "john") + assert.Equal(t, value.resolveForSafe(jsonData), "john") value = JSONValue{Pattern: "auth.identity.email_verified"} - assert.Equal(t, value.ResolveFor(jsonData), true) + assert.Equal(t, value.resolveForSafe(jsonData), true) value = JSONValue{Pattern: "auth.identity.address"} - resolvedValueAsJSON, _ = json.Marshal(value.ResolveFor(jsonData)) + resolvedValueAsJSON, _ = json.Marshal(value.resolveForSafe(jsonData)) type address struct { Line1 string `json:"line_1"` PostalCode int `json:"postal_code"` @@ -54,22 +54,22 @@ func TestJSONValueResolveFor(t *testing.T) { assert.Equal(t, resolvedAddress.PostalCode, 987654) value = JSONValue{Pattern: "auth.identity.roles"} - resolvedValueAsJSON, _ = json.Marshal(value.ResolveFor(jsonData)) + resolvedValueAsJSON, _ = json.Marshal(value.resolveForSafe(jsonData)) var resolvedRoles []string _ = json.Unmarshal(resolvedValueAsJSON, &resolvedRoles) assert.DeepEqual(t, resolvedRoles, []string{"user", "admin"}) // pattern mixing static and variable placeholders ("template") value = JSONValue{Pattern: "Hello, {auth.identity.username}!"} - assert.Equal(t, value.ResolveFor(jsonData), "Hello, john!") + assert.Equal(t, value.resolveForSafe(jsonData), "Hello, john!") // template with inner patterns passing arguments to modifier value = JSONValue{Pattern: `Email domain: {auth.identity.email.@extract:{"sep":"@","pos":1}}`} - assert.Equal(t, value.ResolveFor(jsonData), "Email domain: test") + assert.Equal(t, value.resolveForSafe(jsonData), "Email domain: test") // simple pattern passing arguments to modifier (not a template) value = JSONValue{Pattern: `auth.identity.email.@extract:{"sep":"@","pos":1}`} - assert.Equal(t, value.ResolveFor(jsonData), "test") + assert.Equal(t, value.resolveForSafe(jsonData), "test") } func TestIsTemplate(t *testing.T) { diff --git a/pkg/service/auth_pipeline.go b/pkg/service/auth_pipeline.go index 7e2478ab..22e112ab 100644 --- a/pkg/service/auth_pipeline.go +++ b/pkg/service/auth_pipeline.go @@ -587,17 +587,20 @@ func (pipeline *AuthPipeline) customizeDenyWith(authResult auth.AuthResult, deny authJSON := pipeline.GetAuthorizationJSON() if denyWith.Message != nil { - authResult.Message, _ = json.StringifyJSON(denyWith.Message.ResolveFor(authJSON)) + resolved, _ := denyWith.Message.ResolveFor(authJSON) + authResult.Message, _ = json.StringifyJSON(resolved) } if denyWith.Body != nil { - authResult.Body, _ = json.StringifyJSON(denyWith.Body.ResolveFor(authJSON)) + resolved, _ := denyWith.Body.ResolveFor(authJSON) + authResult.Body, _ = json.StringifyJSON(resolved) } if len(denyWith.Headers) > 0 { headers := make([]map[string]string, 0) for _, header := range denyWith.Headers { - value, _ := json.StringifyJSON(header.Value.ResolveFor(authJSON)) + resolved, _ := header.Value.ResolveFor(authJSON) + value, _ := json.StringifyJSON(resolved) headers = append(headers, map[string]string{header.Name: value}) } authResult.Headers = headers diff --git a/pkg/service/auth_pipeline_test.go b/pkg/service/auth_pipeline_test.go index 446ddad7..d3feadf9 100644 --- a/pkg/service/auth_pipeline_test.go +++ b/pkg/service/auth_pipeline_test.go @@ -340,8 +340,8 @@ func TestEvaluateWithCustomDenyOptions(t *testing.T) { Unauthenticated: &evaluators.DenyWithValues{ Code: 302, Headers: []json.JSONProperty{ - {Name: "X-Static-Header", Value: json.JSONValue{Static: "some-value"}}, - {Name: "Location", Value: json.JSONValue{Pattern: "https://my-app.io/login?redirect_to=https://{context.request.http.host}{context.request.http.path}"}}, + {Name: "X-Static-Header", Value: &json.JSONValue{Static: "some-value"}}, + {Name: "Location", Value: &json.JSONValue{Pattern: "https://my-app.io/login?redirect_to=https://{context.request.http.host}{context.request.http.path}"}}, }, Body: &json.JSONValue{ Static: authConfigStaticResponse, diff --git a/pkg/service/auth_test.go b/pkg/service/auth_test.go index 26366e04..d23ce0f7 100644 --- a/pkg/service/auth_test.go +++ b/pkg/service/auth_test.go @@ -256,7 +256,7 @@ func TestAuthServiceRawHTTPAuthorization_WithHeaders(t *testing.T) { Wrapper: "httpHeader", WrapperKey: "x-auth-data", DynamicJSON: &response.DynamicJSON{ - Properties: []json.JSONProperty{{Name: "headers", Value: json.JSONValue{Pattern: "context.request.http.headers"}}}, + Properties: []json.JSONProperty{{Name: "headers", Value: &json.JSONValue{Pattern: "context.request.http.headers"}}}, }, }} indexMock := mock_index.NewMockIndex(mockController) diff --git a/tests/e2e-test.sh b/tests/e2e-test.sh index 3e502eed..fb071730 100755 --- a/tests/e2e-test.sh +++ b/tests/e2e-test.sh @@ -8,7 +8,7 @@ for cmd in realpath kubectl curl jq base64; do done namespace=${NAMESPACE:-"authorino"} -authconfig_version=${AUTHCONFIG_VERSION:-"v1beta2"} +authconfig_version=${AUTHCONFIG_VERSION:-"v1beta3"} authconfig=${AUTHCONFIG:-"$(dirname $(realpath $0))/${authconfig_version}/authconfig.yaml"} authconfig_invalid=${AUTHCONFIG_INVALID:-"$(dirname $(realpath $0))/${authconfig_version}/authconfig-invalid.yaml"} verbose=${VERBOSE} @@ -204,6 +204,7 @@ send_k8s_sa_requests $IP_IN "app-1-sa" " GET /admin => 200 GET /greetings/1 => 403" +# Test #5 done send_k8s_sa_requests $IP_IN "app-2-sa" " GET / => 200 POST / => 200 @@ -211,6 +212,7 @@ send_k8s_sa_requests $IP_IN "app-2-sa" " GET /admin => 403 GET /greetings/1 => 403" +# Test #10 done send_api_key_requests $IP_IN "ndyBzreUzF4zqDQsqSPMHkRhriEOtcRx" " GET / => 200 POST / => 200 @@ -218,6 +220,7 @@ send_api_key_requests $IP_IN "ndyBzreUzF4zqDQsqSPMHkRhriEOtcRx" " GET /admin => 200 GET /greetings/1 => 403" +# Test #15 done send_api_key_requests $IP_IN "pR2zLorYFIYOE4LLiQAWMPIRei1YgRBy" " GET / => 200 POST / => 200 @@ -225,14 +228,17 @@ send_api_key_requests $IP_IN "pR2zLorYFIYOE4LLiQAWMPIRei1YgRBy" " GET /admin => 403 GET /greetings/1 => 403" +# Test #20 done kubectl -n $namespace delete secret/alice-api-key 2>/dev/null >/dev/null && sleep 1 send_api_key_requests $IP_IN "pR2zLorYFIYOE4LLiQAWMPIRei1YgRBy" " POST / => 401" +# Test #21 done send_api_key_requests $IP_IN "ndyBzreUzF4zqDQsqSPMHkRhriEOtcRx" " POST / => 200" +# Test #22 done send_oidc_requests $IP_IN "john" "p" " GET / => 200 POST / => 200 @@ -240,6 +246,7 @@ send_oidc_requests $IP_IN "john" "p" " GET /admin => 403 GET /greetings/1 => 200" +# Test #27 done send_oidc_requests $IP_IN "jane" "p" " GET / => 200 POST / => 200 @@ -247,6 +254,7 @@ send_oidc_requests $IP_IN "jane" "p" " GET /admin => 200 GET /greetings/1 => 403" +# Test #32 done send_oauth_opaque_requests $IP_IN "peter" "p" " GET / => 200 POST / => 200 @@ -254,6 +262,7 @@ send_oauth_opaque_requests $IP_IN "peter" "p" " GET /admin => 403 GET /greetings/1 => 403" +# Test #37 done send_anonymous_requests $IP_IN " GET / => 200 POST / => 401 @@ -261,9 +270,11 @@ send_anonymous_requests $IP_IN " GET /admin => 401 GET /greetings/1 => 401" +# Test #42 done send_anonymous_requests $IP_OUT " GET / => 403" +# Test #43 done send_requests "https" "authorino-authorino-oidc" "8083" $IP_IN "" " GET /authorino/e2e-test/wristband/.well-known/openid-configuration => 200 GET /authorino/e2e-test/wristband/.well-known/openid-connect/certs => 200 diff --git a/tests/v1beta3/authconfig.yaml b/tests/v1beta3/authconfig.yaml index 6a6c067a..7e783002 100644 --- a/tests/v1beta3/authconfig.yaml +++ b/tests/v1beta3/authconfig.yaml @@ -6,16 +6,6 @@ spec: hosts: - talker-api.127.0.0.1.nip.io - patterns: - admin-path: - - selector: context.request.http.path - operator: matches - value: ^/admin(/.*)?$ - resource-path: - - selector: context.request.http.path - operator: matches - value: ^/greetings/\d+$ - authentication: k8s-auth: kubernetesTokenReview: @@ -25,8 +15,7 @@ spec: kubernetes-rbac: value: true username: - selector: auth.identity.user.username - value: null + expression: auth.identity.user.username api-key: apiKey: selector: @@ -39,7 +28,7 @@ spec: kubernetes-rbac: value: true username: - selector: auth.identity.metadata.annotations.username + expression: auth.identity.metadata.annotations.username keycloak: jwt: issuerUrl: http://keycloak.authorino.svc.cluster.local:8080/realms/kuadrant @@ -48,9 +37,9 @@ spec: jwt-rbac: value: true roles: - selector: auth.identity.realm_access.roles + expression: "has(auth.identity.realm_access) ? auth.identity.realm_access.roles : []" username: - selector: auth.identity.preferred_username + expression: "has(auth.identity.preferred_username) ? auth.identity.preferred_username : 'unknown'" oauth2-introspection: oauth2Introspection: credentialsRef: @@ -64,22 +53,18 @@ spec: jwt-rbac: value: true roles: - selector: auth.identity.realm_access.roles + expression: "has(auth.identity.realm_access) ? auth.identity.realm_access.roles : []" username: - selector: auth.identity.preferred_username + expression: "has(auth.identity.preferred_username) ? auth.identity.preferred_username : 'unknown'" cache: key: - selector: context.request.http.headers.authorization + expression: request.headers["authorization"] anonymous: anonymous: {} priority: 1 when: - - selector: context.request.http.method - operator: eq - value: GET - - selector: context.request.http.path - operator: matches - value: ^/$ + - predicate: request.method == "GET" + - predicate: request.path.matches("^/$") defaults: username: value: global @@ -95,23 +80,23 @@ spec: url: http://ip-location.authorino.svc.cluster.local:3000/{context.request.http.headers.x-forwarded-for.@extract:{"sep":","}} cache: key: - selector: context.request.http.headers.x-forwarded-for.@extract:{"sep":","} + selector: request.http.headers.x-forwarded-for.@extract:{"sep":","} user-info: userInfo: identitySource: keycloak cache: key: - selector: context.request.http.headers.authorization + expression: request.headers["authorization"] resource-info: when: - - patternRef: resource-path + - predicate: request.path.matches("^/greetings/\\d+$") uma: credentialsRef: name: talker-api-uma-credentials endpoint: http://keycloak.authorino.svc.cluster.local:8080/realms/kuadrant cache: key: - selector: context.request.http.path + expression: request.path authorization: allowed-methods: @@ -130,27 +115,21 @@ spec: } admin-kubernetes-rbac: when: - - patternRef: admin-path - - selector: auth.identity.kubernetes-rbac - operator: eq - value: 'true' + - predicate: request.path.matches("^/admin(/.*)?$") + - predicate: auth.identity["kubernetes-rbac"] kubernetesSubjectAccessReview: user: - selector: auth.identity.username + expression: auth.identity.username admin-jwt-rbac: when: - - patternRef: admin-path - - selector: auth.identity.jwt-rbac - operator: eq - value: 'true' + - predicate: request.path.matches("^/admin(/.*)?$") + - predicate: auth.identity["jwt-rbac"] patternMatching: patterns: - - selector: auth.identity.roles - operator: incl - value: admin + - predicate: auth.identity.roles.exists(r, r == "admin") resource-owner: when: - - patternRef: resource-path + - predicate: request.path.matches("^/greetings/\\d+$") opa: rego: | allow { @@ -180,40 +159,38 @@ spec: value: Authorino x-username: plain: - selector: auth.identity.username + expression: auth.identity.username x-auth-data: json: properties: username: - selector: auth.identity.username + expression: auth.identity.username geo: - selector: auth.metadata.geo-info + expression: auth.metadata["geo-info"] timestamp: - selector: auth.authorization.timestamp.now + expression: auth.authorization.timestamp.now wristband: wristband: issuer: https://authorino-authorino-oidc.authorino.svc.cluster.local:8083/authorino/e2e-test/wristband tokenDuration: 300 customClaims: username: - selector: auth.identity.username + expression: auth.identity.username uri: - selector: context.request.http.path + expression: request.path scope: - selector: context.request.http.method.@case:lower + selector: request.http.method.@case:lower signingKeyRefs: - name: wristband-signing-key algorithm: ES256 when: - - selector: auth.identity.anonymous - operator: neq - value: 'true' + - predicate: auth.identity.anonymous == false dynamicMetadata: rate-limit-data: json: properties: username: - selector: auth.identity.username + expression: auth.identity.username key: ext_auth_data --- apiVersion: v1