From 0bbd9229dbb7d77c05e24810540af62f6c3cf1e5 Mon Sep 17 00:00:00 2001 From: William Johansson Date: Fri, 13 Oct 2023 10:11:51 +0200 Subject: [PATCH] feat(iamcel): add member function on caller object Introduce a member function on the caller object, to extract the first member value of a member kind, or fail if there are none. --- README.md | 4 +++ iamauthz/after.go | 1 + iamauthz/before.go | 1 + iamcel/after.go | 1 + iamcel/before.go | 1 + iamcel/member.go | 61 ++++++++++++++++++++++++++++++++ iamcel/member_test.go | 81 +++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 150 insertions(+) create mode 100644 iamcel/member.go create mode 100644 iamcel/member_test.go diff --git a/README.md b/README.md index a3ab673b..679dc424 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"`. +#### [`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. + ### 6) Generate authorization middleware Coming soon. diff --git a/iamauthz/after.go b/iamauthz/after.go index 3e15944e..94af3c16 100644 --- a/iamauthz/after.go +++ b/iamauthz/after.go @@ -46,6 +46,7 @@ func NewAfterMethodAuthorization( iamcel.NewTestAllFunctionImplementation(options, permissionTester), iamcel.NewTestAnyFunctionImplementation(options, permissionTester), iamcel.NewAncestorFunctionImplementation(), + iamcel.NewMemberFunctionImplementation(), ), ) if err != nil { diff --git a/iamauthz/before.go b/iamauthz/before.go index da895462..f560b976 100644 --- a/iamauthz/before.go +++ b/iamauthz/before.go @@ -46,6 +46,7 @@ func NewBeforeMethodAuthorization( iamcel.NewTestAllFunctionImplementation(options, permissionTester), iamcel.NewTestAnyFunctionImplementation(options, permissionTester), iamcel.NewAncestorFunctionImplementation(), + iamcel.NewMemberFunctionImplementation(), ), ) if err != nil { diff --git a/iamcel/after.go b/iamcel/after.go index ad8eac71..79727442 100644 --- a/iamcel/after.go +++ b/iamcel/after.go @@ -26,6 +26,7 @@ func NewAfterEnv(method protoreflect.MethodDescriptor) (*cel.Env, error) { NewTestAllFunctionDeclaration(), NewTestAnyFunctionDeclaration(), NewAncestorFunctionDeclaration(), + NewMemberFunctionDeclaration(), ), ) if err != nil { diff --git a/iamcel/before.go b/iamcel/before.go index 1665423c..89a9b94f 100644 --- a/iamcel/before.go +++ b/iamcel/before.go @@ -25,6 +25,7 @@ func NewBeforeEnv(method protoreflect.MethodDescriptor) (*cel.Env, error) { NewTestAllFunctionDeclaration(), NewTestAnyFunctionDeclaration(), NewAncestorFunctionDeclaration(), + NewMemberFunctionDeclaration(), ), ) if err != nil { diff --git a/iamcel/member.go b/iamcel/member.go new file mode 100644 index 00000000..1e0e6074 --- /dev/null +++ b/iamcel/member.go @@ -0,0 +1,61 @@ +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/iam/iammember" + iamv1 "go.einride.tech/iam/proto/gen/einride/iam/v1" + expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1" +) + +// MemberFunction is the name of the CEL member function. +const MemberFunction = "member" + +const memberFunctionOverload = "member_caller_string_string" + +// NewMemberFunctionDeclaration creates a new declaration for the member function. +func NewMemberFunctionDeclaration() *expr.Decl { + return decls.NewFunction( + MemberFunction, + decls.NewInstanceOverload( + memberFunctionOverload, + []*expr.Type{ + decls.NewObjectType(string((&iamv1.Caller{}).ProtoReflect().Descriptor().FullName())), + decls.String, + }, + decls.String, + ), + ) +} + +// NewMemberFunctionImplementation creates a new implementation for the member function. +func NewMemberFunctionImplementation() *functions.Overload { + return &functions.Overload{ + Operator: memberFunctionOverload, + Binary: func(callerVal, kindVal ref.Val) ref.Val { + caller, ok := callerVal.Value().(*iamv1.Caller) + if !ok { + return types.NewErr("test: unexpected type of arg 1, expected %T but got %T", &iamv1.Caller{}, callerVal.Value()) + } + + kind, ok := kindVal.Value().(string) + if !ok { + return types.NewErr("test: unexpected type of arg 2, expected string but got %T", kindVal.Value()) + } + + for _, member := range caller.GetMembers() { + memberKind, memberValue, ok := iammember.Parse(member) + if !ok { + return types.NewErr("member: error parsing caller member '%s'", member) + } + if memberKind == kind { + return types.String(memberValue) + } + } + + return types.NewErr("member: no kind '%s' found in caller", kind) + }, + } +} diff --git a/iamcel/member_test.go b/iamcel/member_test.go new file mode 100644 index 00000000..4c5cf868 --- /dev/null +++ b/iamcel/member_test.go @@ -0,0 +1,81 @@ +package iamcel + +import ( + "testing" + + "github.com/google/cel-go/cel" + iamv1 "go.einride.tech/iam/proto/gen/einride/iam/v1" + "gotest.tools/v3/assert" +) + +func TestMemberFunction(t *testing.T) { + caller := (&iamv1.Caller{}).ProtoReflect().Descriptor() + dependencies, err := collectDependencies(caller) + assert.NilError(t, err) + env, err := cel.NewEnv( + cel.TypeDescs(dependencies), + cel.Variable("caller", cel.ObjectType(string(caller.FullName()))), + cel.Declarations(NewMemberFunctionDeclaration()), + ) + assert.NilError(t, err) + + t.Run("single kind", func(t *testing.T) { + ast, issues := env.Compile(`caller.member('kind1')`) + assert.NilError(t, issues.Err()) + //nolint: staticcheck // TODO: migrate to new top-level API + program, err := env.Program(ast, cel.Functions(NewMemberFunctionImplementation())) + assert.NilError(t, err) + result, _, err := program.Eval( + map[string]interface{}{ + "caller": &iamv1.Caller{ + Members: []string{ + "kind1:value1", + "kind2:value2", + "kind2:value3", + }, + }, + }, + ) + assert.NilError(t, err) + assert.Equal(t, "value1", result.Value().(string)) + }) + t.Run("pick first", func(t *testing.T) { + ast, issues := env.Compile(`caller.member('kind2')`) + assert.NilError(t, issues.Err()) + //nolint: staticcheck // TODO: migrate to new top-level API + program, err := env.Program(ast, cel.Functions(NewMemberFunctionImplementation())) + assert.NilError(t, err) + result, _, err := program.Eval( + map[string]interface{}{ + "caller": &iamv1.Caller{ + Members: []string{ + "kind1:value1", + "kind2:value2", + "kind2:value3", + }, + }, + }, + ) + assert.NilError(t, err) + assert.Equal(t, "value2", result.Value().(string)) + }) + t.Run("no such kind", func(t *testing.T) { + ast, issues := env.Compile(`caller.member('kind3')`) + assert.NilError(t, issues.Err()) + //nolint: staticcheck // TODO: migrate to new top-level API + program, err := env.Program(ast, cel.Functions(NewMemberFunctionImplementation())) + assert.NilError(t, err) + _, _, err = program.Eval( + map[string]interface{}{ + "caller": &iamv1.Caller{ + Members: []string{ + "kind1:value1", + "kind2:value2", + "kind2:value3", + }, + }, + }, + ) + assert.Error(t, err, "member: no kind 'kind3' found in caller") + }) +}