Skip to content

Commit

Permalink
Cel Expressions
Browse files Browse the repository at this point in the history
Signed-off-by: Alex Snaps <[email protected]>
  • Loading branch information
alexsnaps committed Oct 15, 2024
1 parent bb0e460 commit b1c4cc2
Show file tree
Hide file tree
Showing 19 changed files with 163 additions and 48 deletions.
2 changes: 1 addition & 1 deletion pkg/evaluators/authorization.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
34 changes: 27 additions & 7 deletions pkg/evaluators/authorization/authzed.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
}
50 changes: 41 additions & 9 deletions pkg/evaluators/authorization/kubernetes_authz.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions pkg/evaluators/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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)
}

Expand Down
8 changes: 6 additions & 2 deletions pkg/evaluators/identity.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
4 changes: 3 additions & 1 deletion pkg/evaluators/identity/plain.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down
4 changes: 2 additions & 2 deletions pkg/evaluators/identity_extension.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
4 changes: 3 additions & 1 deletion pkg/evaluators/identity_extension_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
}
}
2 changes: 1 addition & 1 deletion pkg/evaluators/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
22 changes: 17 additions & 5 deletions pkg/evaluators/metadata/generic_http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion pkg/evaluators/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 5 additions & 1 deletion pkg/evaluators/response/dynamic_json.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion pkg/evaluators/response/plain.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
6 changes: 5 additions & 1 deletion pkg/evaluators/response/wristband.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}

Expand Down
23 changes: 23 additions & 0 deletions pkg/expressions/cel/expressions.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
5 changes: 5 additions & 0 deletions pkg/expressions/types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package expressions

type Value interface {
ResolveFor(jsonData string) (interface{}, error)
}
6 changes: 5 additions & 1 deletion pkg/json/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
18 changes: 9 additions & 9 deletions pkg/json/json_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand All @@ -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) {
Expand Down
Loading

0 comments on commit b1c4cc2

Please sign in to comment.