diff --git a/pkg/evaluators/authorization.go b/pkg/evaluators/authorization.go index 75df06d3..a23f5059 100644 --- a/pkg/evaluators/authorization.go +++ b/pkg/evaluators/authorization.go @@ -57,7 +57,7 @@ func (config *AuthorizationConfig) Call(pipeline auth.AuthPipeline, ctx context. var cacheKey interface{} if cache != nil { - cacheKey = cache.ResolveKeyFor(pipeline.GetAuthorizationJSON()) + 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 { diff --git a/pkg/evaluators/authorization/authzed.go b/pkg/evaluators/authorization/authzed.go index cb65acda..1759b17b 100644 --- a/pkg/evaluators/authorization/authzed.go +++ b/pkg/evaluators/authorization/authzed.go @@ -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 json.JSONValue, 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/kubernetes_authz.go b/pkg/evaluators/authorization/kubernetes_authz.go index e37fa80f..a382248b 100644 --- a/pkg/evaluators/authorization/kubernetes_authz.go +++ b/pkg/evaluators/authorization/kubernetes_authz.go @@ -63,26 +63,58 @@ 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 json.JSONValue) (string, error) { + 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/cache.go b/pkg/evaluators/cache.go index 05aa4559..b5bd3a75 100644 --- a/pkg/evaluators/cache.go +++ b/pkg/evaluators/cache.go @@ -16,7 +16,7 @@ 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 } @@ -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/identity.go b/pkg/evaluators/identity.go index 1c712f01..887b8a41 100644 --- a/pkg/evaluators/identity.go +++ b/pkg/evaluators/identity.go @@ -84,7 +84,7 @@ func (config *IdentityConfig) Call(pipeline auth.AuthPipeline, ctx context.Conte var cacheKey interface{} if cache != nil { - cacheKey = cache.ResolveKeyFor(pipeline.GetAuthorizationJSON()) + 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 { @@ -197,7 +197,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..d996d774 100644 --- a/pkg/evaluators/identity/plain.go +++ b/pkg/evaluators/identity/plain.go @@ -18,8 +18,10 @@ type Plain struct { 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 := pattern.ResolveFor(pipeline.GetAuthorizationJSON()); object != nil { return object, nil + } else if err != nil { + return nil, err } return nil, fmt.Errorf("could not retrieve identity object or null") } diff --git a/pkg/evaluators/identity_extension.go b/pkg/evaluators/identity_extension.go index b945e10e..f57e1ed9 100644 --- a/pkg/evaluators/identity_extension.go +++ b/pkg/evaluators/identity_extension.go @@ -17,9 +17,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..922c1765 100644 --- a/pkg/evaluators/identity_extension_test.go +++ b/pkg/evaluators/identity_extension_test.go @@ -84,7 +84,9 @@ func TestResolveIdentityExtension(t *testing.T) { } 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/metadata.go b/pkg/evaluators/metadata.go index e32eb01e..f04d5695 100644 --- a/pkg/evaluators/metadata.go +++ b/pkg/evaluators/metadata.go @@ -53,7 +53,7 @@ func (config *MetadataConfig) Call(pipeline auth.AuthPipeline, ctx context.Conte var cacheKey interface{} if cache != nil { - cacheKey = cache.ResolveKeyFor(pipeline.GetAuthorizationJSON()) + 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 { diff --git a/pkg/evaluators/metadata/generic_http.go b/pkg/evaluators/metadata/generic_http.go index f9a8417f..80adec26 100644 --- a/pkg/evaluators/metadata/generic_http.go +++ b/pkg/evaluators/metadata/generic_http.go @@ -127,7 +127,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 +156,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/response.go b/pkg/evaluators/response.go index 6ee4b156..f116ad7e 100644 --- a/pkg/evaluators/response.go +++ b/pkg/evaluators/response.go @@ -82,7 +82,7 @@ func (config *ResponseConfig) Call(pipeline auth.AuthPipeline, ctx context.Conte var cacheKey interface{} if cache != nil { - cacheKey = cache.ResolveKeyFor(pipeline.GetAuthorizationJSON()) + 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 { 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/plain.go b/pkg/evaluators/response/plain.go index c84f5ded..6e1e61d6 100644 --- a/pkg/evaluators/response/plain.go +++ b/pkg/evaluators/response/plain.go @@ -13,5 +13,5 @@ type Plain struct { 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/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/expressions/cel/expressions.go b/pkg/expressions/cel/expressions.go index 9d7be3f6..1663e83a 100644 --- a/pkg/expressions/cel/expressions.go +++ b/pkg/expressions/cel/expressions.go @@ -45,6 +45,29 @@ func (p *Predicate) Matches(json string) (bool, error) { return result.Value().(bool), nil } +type Expression struct { + program cel.Program + source string +} + +func (e *Expression) ResolveFor(json string) (interface{}, error) { + input, err := AuthJsonToCel(json) + if err != nil { + return nil, err + } + + result, _, err := e.program.Eval(input) + if err != nil { + return nil, err + } + + if jsonVal, err := ValueToJSON(result); err != nil { + return nil, err + } else { + return jsonVal, nil + } +} + func Compile(expression string, predicate bool, opts ...cel.EnvOption) (cel.Program, error) { envOpts := append([]cel.EnvOption{cel.Declarations( decls.NewConst(RootAuthBinding, decls.NewObjectType("google.protobuf.Struct"), nil), 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..4fe110d6 100644 --- a/pkg/json/json.go +++ b/pkg/json/json.go @@ -38,7 +38,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