From 54b3c4c0c313d528b2c36192b8318a7f28e6cce3 Mon Sep 17 00:00:00 2001 From: William Johansson Date: Mon, 23 Oct 2023 10:34:10 +0200 Subject: [PATCH] feat(iamcel): add join function Join function combines two resource names, using [resourcename.Join] from aip-go. [resourcename.Join]: https://pkg.go.dev/go.einride.tech/aip/resourcename#Join --- README.md | 4 ++ go.mod | 2 +- go.sum | 4 +- iamauthz/after.go | 1 + iamauthz/before.go | 1 + iamcel/after.go | 1 + iamcel/before.go | 1 + iamcel/join.go | 46 ++++++++++++++++++ iamcel/join_test.go | 113 ++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 170 insertions(+), 3 deletions(-) create mode 100644 iamcel/join.go create mode 100644 iamcel/join_test.go diff --git a/README.md b/README.md index 679dc424..871796b9 100644 --- a/README.md +++ b/README.md @@ -134,6 +134,10 @@ Tests `caller`s permissions against any `resources`. This test asserts that the Resolves an ancestor of `resource` using `pattern`. An input of `ancestor("foo/1/bar/2", "foo/{foo}")` will yield the result `"foo/1"`. +#### [`join(parent string, resource string) string`](./iamcel/join.go) + +Joins a `resource` name with a `parent` resource name. An input of `join("foo/1", "bar/2")` will yield the result `"foo/1/bar/2"`. + #### [`caller.member(kind string) string`](./iamcel/member.go) Returns the first IAM member value from the caller's member list which matches the member kind, or fails if there are no such kind. diff --git a/go.mod b/go.mod index 25fec8f9..db155667 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( cloud.google.com/go/longrunning v0.5.1 cloud.google.com/go/spanner v1.51.0 github.com/google/cel-go v0.18.1 - go.einride.tech/aip v0.62.0 + go.einride.tech/aip v0.63.0 go.einride.tech/spanner-aip v0.53.0 google.golang.org/api v0.146.0 google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb diff --git a/go.sum b/go.sum index af01f4de..12c67f49 100644 --- a/go.sum +++ b/go.sum @@ -87,8 +87,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= -go.einride.tech/aip v0.62.0 h1:DVHT0kgIhHfEqcbTUZ/tKTc+YButvOuTVT4JQFWDGo0= -go.einride.tech/aip v0.62.0/go.mod h1:YVrCQRL7SCB5Mv7i2ZF1R6vkLPh844RQBCLrrLcefaU= +go.einride.tech/aip v0.63.0 h1:1u2ppyKS9hj++bp+q4rOEma7dAbG7ZPl6tSBLf8t+wo= +go.einride.tech/aip v0.63.0/go.mod h1:kK5nO4xh3JoniXp64dxgT474Egmr5L7SlsGD6xvP6fU= go.einride.tech/spanner-aip v0.53.0 h1:t9thNf7sAA5nDGmRdAXIv9ekerVboUASPSXm67BnC8I= go.einride.tech/spanner-aip v0.53.0/go.mod h1:ckdDj396+YDxDWjnrifR4pDBnvV7gfgjUGCOznqdq1Q= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= diff --git a/iamauthz/after.go b/iamauthz/after.go index 94af3c16..ad066453 100644 --- a/iamauthz/after.go +++ b/iamauthz/after.go @@ -47,6 +47,7 @@ func NewAfterMethodAuthorization( iamcel.NewTestAnyFunctionImplementation(options, permissionTester), iamcel.NewAncestorFunctionImplementation(), iamcel.NewMemberFunctionImplementation(), + iamcel.NewJoinFunctionImplementation(), ), ) if err != nil { diff --git a/iamauthz/before.go b/iamauthz/before.go index f560b976..da825f64 100644 --- a/iamauthz/before.go +++ b/iamauthz/before.go @@ -47,6 +47,7 @@ func NewBeforeMethodAuthorization( iamcel.NewTestAnyFunctionImplementation(options, permissionTester), iamcel.NewAncestorFunctionImplementation(), iamcel.NewMemberFunctionImplementation(), + iamcel.NewJoinFunctionImplementation(), ), ) if err != nil { diff --git a/iamcel/after.go b/iamcel/after.go index 79727442..80a3437b 100644 --- a/iamcel/after.go +++ b/iamcel/after.go @@ -27,6 +27,7 @@ func NewAfterEnv(method protoreflect.MethodDescriptor) (*cel.Env, error) { NewTestAnyFunctionDeclaration(), NewAncestorFunctionDeclaration(), NewMemberFunctionDeclaration(), + NewJoinFunctionDeclaration(), ), ) if err != nil { diff --git a/iamcel/before.go b/iamcel/before.go index 89a9b94f..15d1f3e4 100644 --- a/iamcel/before.go +++ b/iamcel/before.go @@ -26,6 +26,7 @@ func NewBeforeEnv(method protoreflect.MethodDescriptor) (*cel.Env, error) { NewTestAnyFunctionDeclaration(), NewAncestorFunctionDeclaration(), NewMemberFunctionDeclaration(), + NewJoinFunctionDeclaration(), ), ) if err != nil { diff --git a/iamcel/join.go b/iamcel/join.go new file mode 100644 index 00000000..a262b07b --- /dev/null +++ b/iamcel/join.go @@ -0,0 +1,46 @@ +package iamcel + +import ( + "github.com/google/cel-go/checker/decls" + "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" + "github.com/google/cel-go/interpreter/functions" + "go.einride.tech/aip/resourcename" + expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1" +) + +// JoinFunction is the name of the CEL descendant function. +const JoinFunction = "join" + +const joinFunctionOverload = "join_string_string" + +// NewJoinFunctionDeclaration creates a new declaration for the descendant function. +func NewJoinFunctionDeclaration() *expr.Decl { + return decls.NewFunction( + JoinFunction, + // TODO: if ever possible in CEL-go, declare this as a variadic function. + decls.NewOverload( + joinFunctionOverload, + []*expr.Type{decls.String, decls.String}, + decls.String, + ), + ) +} + +// NewJoinFunctionImplementation creates a new implementation for the descendant function. +func NewJoinFunctionImplementation() *functions.Overload { + return &functions.Overload{ + Operator: joinFunctionOverload, + Binary: func(parentVal, childVal ref.Val) ref.Val { + parent, ok := parentVal.Value().(string) + if !ok { + return types.NewErr("parent: unexpected type of arg 1, expected string but got %T", parentVal.Value()) + } + child, ok := childVal.Value().(string) + if !ok { + return types.NewErr("child: unexpected type of arg 2, expected string but got %T", childVal.Value()) + } + return types.String(resourcename.Join(parent, child)) + }, + } +} diff --git a/iamcel/join_test.go b/iamcel/join_test.go new file mode 100644 index 00000000..11decd29 --- /dev/null +++ b/iamcel/join_test.go @@ -0,0 +1,113 @@ +package iamcel + +import ( + "testing" + + "github.com/google/cel-go/cel" + "gotest.tools/v3/assert" +) + +func TestJoinFunction(t *testing.T) { + env, err := cel.NewEnv(cel.Declarations(NewJoinFunctionDeclaration())) + assert.NilError(t, err) + t.Run("ok", func(t *testing.T) { + ast, issues := env.Compile(`join('parent/1', 'child/2')`) + assert.NilError(t, issues.Err()) + //nolint: staticcheck // TODO: migrate to new top-level API + program, err := env.Program(ast, cel.Functions(NewJoinFunctionImplementation())) + assert.NilError(t, err) + result, _, err := program.Eval(map[string]interface{}(nil)) + assert.NilError(t, err) + assert.Equal(t, "parent/1/child/2", result.Value().(string)) + }) + t.Run("root parent", func(t *testing.T) { + ast, issues := env.Compile(`join('/', 'child/2')`) + assert.NilError(t, issues.Err()) + //nolint: staticcheck // TODO: migrate to new top-level API + program, err := env.Program(ast, cel.Functions(NewJoinFunctionImplementation())) + assert.NilError(t, err) + result, _, err := program.Eval(map[string]interface{}(nil)) + assert.NilError(t, err) + assert.Equal(t, "child/2", result.Value().(string)) + }) + t.Run("root child", func(t *testing.T) { + ast, issues := env.Compile(`join('parent/1', '/')`) + assert.NilError(t, issues.Err()) + //nolint: staticcheck // TODO: migrate to new top-level API + program, err := env.Program(ast, cel.Functions(NewJoinFunctionImplementation())) + assert.NilError(t, err) + result, _, err := program.Eval(map[string]interface{}(nil)) + assert.NilError(t, err) + assert.Equal(t, "parent/1", result.Value().(string)) + }) + t.Run("root parent and child", func(t *testing.T) { + ast, issues := env.Compile(`join('/', '/')`) + assert.NilError(t, issues.Err()) + //nolint: staticcheck // TODO: migrate to new top-level API + program, err := env.Program(ast, cel.Functions(NewJoinFunctionImplementation())) + assert.NilError(t, err) + result, _, err := program.Eval(map[string]interface{}(nil)) + assert.NilError(t, err) + assert.Equal(t, "/", result.Value().(string)) + }) + t.Run("empty parent", func(t *testing.T) { + ast, issues := env.Compile(`join('', 'child/2')`) + assert.NilError(t, issues.Err()) + //nolint: staticcheck // TODO: migrate to new top-level API + program, err := env.Program(ast, cel.Functions(NewJoinFunctionImplementation())) + assert.NilError(t, err) + result, _, err := program.Eval(map[string]interface{}(nil)) + assert.NilError(t, err) + assert.Equal(t, "child/2", result.Value().(string)) + }) + t.Run("empty child", func(t *testing.T) { + ast, issues := env.Compile(`join('parent/1', '')`) + assert.NilError(t, issues.Err()) + //nolint: staticcheck // TODO: migrate to new top-level API + program, err := env.Program(ast, cel.Functions(NewJoinFunctionImplementation())) + assert.NilError(t, err) + result, _, err := program.Eval(map[string]interface{}(nil)) + assert.NilError(t, err) + assert.Equal(t, "parent/1", result.Value().(string)) + }) + t.Run("parent slash suffix", func(t *testing.T) { + ast, issues := env.Compile(`join('parent/1/', 'child/2')`) + assert.NilError(t, issues.Err()) + //nolint: staticcheck // TODO: migrate to new top-level API + program, err := env.Program(ast, cel.Functions(NewJoinFunctionImplementation())) + assert.NilError(t, err) + result, _, err := program.Eval(map[string]interface{}(nil)) + assert.NilError(t, err) + assert.Equal(t, "parent/1/child/2", result.Value().(string)) + }) + t.Run("child slash suffix", func(t *testing.T) { + ast, issues := env.Compile(`join('parent/1', 'child/2/')`) + assert.NilError(t, issues.Err()) + //nolint: staticcheck // TODO: migrate to new top-level API + program, err := env.Program(ast, cel.Functions(NewJoinFunctionImplementation())) + assert.NilError(t, err) + result, _, err := program.Eval(map[string]interface{}(nil)) + assert.NilError(t, err) + assert.Equal(t, "parent/1/child/2", result.Value().(string)) + }) + t.Run("parent slash prefix", func(t *testing.T) { + ast, issues := env.Compile(`join('/parent/1', 'child/2')`) + assert.NilError(t, issues.Err()) + //nolint: staticcheck // TODO: migrate to new top-level API + program, err := env.Program(ast, cel.Functions(NewJoinFunctionImplementation())) + assert.NilError(t, err) + result, _, err := program.Eval(map[string]interface{}(nil)) + assert.NilError(t, err) + assert.Equal(t, "parent/1/child/2", result.Value().(string)) + }) + t.Run("child slash prefix", func(t *testing.T) { + ast, issues := env.Compile(`join('parent/1', '/child/2')`) + assert.NilError(t, issues.Err()) + //nolint: staticcheck // TODO: migrate to new top-level API + program, err := env.Program(ast, cel.Functions(NewJoinFunctionImplementation())) + assert.NilError(t, err) + result, _, err := program.Eval(map[string]interface{}(nil)) + assert.NilError(t, err) + assert.Equal(t, "parent/1/child/2", result.Value().(string)) + }) +}