Skip to content

Commit

Permalink
feat(iamcel): add member function on caller object
Browse files Browse the repository at this point in the history
Introduce a member function on the caller object, to extract the first
member value of a member kind, or fail if there are none.
  • Loading branch information
radhus committed Oct 20, 2023
1 parent 3e65b78 commit 0bbd922
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 0 deletions.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
1 change: 1 addition & 0 deletions iamauthz/after.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func NewAfterMethodAuthorization(
iamcel.NewTestAllFunctionImplementation(options, permissionTester),
iamcel.NewTestAnyFunctionImplementation(options, permissionTester),
iamcel.NewAncestorFunctionImplementation(),
iamcel.NewMemberFunctionImplementation(),
),
)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions iamauthz/before.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func NewBeforeMethodAuthorization(
iamcel.NewTestAllFunctionImplementation(options, permissionTester),
iamcel.NewTestAnyFunctionImplementation(options, permissionTester),
iamcel.NewAncestorFunctionImplementation(),
iamcel.NewMemberFunctionImplementation(),
),
)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions iamcel/after.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ func NewAfterEnv(method protoreflect.MethodDescriptor) (*cel.Env, error) {
NewTestAllFunctionDeclaration(),
NewTestAnyFunctionDeclaration(),
NewAncestorFunctionDeclaration(),
NewMemberFunctionDeclaration(),
),
)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions iamcel/before.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ func NewBeforeEnv(method protoreflect.MethodDescriptor) (*cel.Env, error) {
NewTestAllFunctionDeclaration(),
NewTestAnyFunctionDeclaration(),
NewAncestorFunctionDeclaration(),
NewMemberFunctionDeclaration(),
),
)
if err != nil {
Expand Down
61 changes: 61 additions & 0 deletions iamcel/member.go
Original file line number Diff line number Diff line change
@@ -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)
},
}
}
81 changes: 81 additions & 0 deletions iamcel/member_test.go
Original file line number Diff line number Diff line change
@@ -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")
})
}

0 comments on commit 0bbd922

Please sign in to comment.