Skip to content

Commit

Permalink
Add identity transformation packages idtransform and celformer
Browse files Browse the repository at this point in the history
Implements Supervisor identity transformations helpers using CEL.
  • Loading branch information
cfryanr committed Feb 6, 2023
1 parent aa57a51 commit 65a55d1
Show file tree
Hide file tree
Showing 5 changed files with 1,064 additions and 8 deletions.
6 changes: 3 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ require (
github.com/go-logr/zapr v1.2.3
github.com/gofrs/flock v0.8.1
github.com/golang/mock v1.6.0
github.com/google/cel-go v0.13.0
github.com/google/go-cmp v0.5.9
github.com/google/gofuzz v1.2.0
github.com/google/uuid v1.3.0
Expand Down Expand Up @@ -85,7 +86,6 @@ require (
github.com/golang/glog v1.0.0 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/cel-go v0.12.6 // indirect
github.com/google/gnostic v0.6.9 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect
Expand Down Expand Up @@ -146,8 +146,8 @@ require (
golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect
golang.org/x/tools v0.4.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90 // indirect
google.golang.org/grpc v1.49.0 // indirect
google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c // indirect
google.golang.org/grpc v1.50.1 // indirect
google.golang.org/protobuf v1.28.1 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/ini.v1 v1.66.6 // indirect
Expand Down
11 changes: 6 additions & 5 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -260,8 +260,8 @@ github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEW
github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
github.com/google/cel-go v0.12.6 h1:kjeKudqV0OygrAqA9fX6J55S8gj+Jre2tckIm5RoG4M=
github.com/google/cel-go v0.12.6/go.mod h1:Jk7ljRzLBhkmiAwBoUxB1sZSCVBAzkqPF25olK/iRDw=
github.com/google/cel-go v0.13.0 h1:z+8OBOcmh7IeKyqwT/6IlnMvy621fYUqnTVPEdegGlU=
github.com/google/cel-go v0.13.0/go.mod h1:K2hpQgEjDp18J76a2DKFRlPBPpgRZgi6EbnpDgIhJ8s=
github.com/google/gnostic v0.6.9 h1:ZK/5VhkoX835RikCHpSUJV9a+S3e1zLh59YnyWeBW+0=
github.com/google/gnostic v0.6.9/go.mod h1:Nm8234We1lq6iB9OmlgNv3nH91XLLVZHCDayfA3xq+E=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
Expand Down Expand Up @@ -1079,8 +1079,9 @@ google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP
google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4=
google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90 h1:4SPz2GL2CXJt28MTF8V6Ap/9ZiVbQlJeGSd9qtA7DLs=
google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA=
google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c h1:QgY/XxIAIeccR+Ca/rDdKubLIU9rcJ3xfy1DC/Wd2Oo=
google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
Expand Down Expand Up @@ -1115,8 +1116,8 @@ google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11
google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk=
google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw=
google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
google.golang.org/grpc v1.50.1 h1:DS/BukOZWp8s6p4Dt/tOaJaTQyPyOoCcrjroHuCeLzY=
google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI=
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
Expand Down
278 changes: 278 additions & 0 deletions internal/celtransformer/celformer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
// Copyright 2023 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0

// Package celtransformer is an implementation of upstream-to-downstream identity transformations
// and policies using CEL scripts.
//
// The CEL language is documented in https://github.com/google/cel-spec/blob/master/doc/langdef.md
// with optional extensions documented in https://github.com/google/cel-go/tree/master/ext.
package celtransformer

import (
"context"
"fmt"
"reflect"
"strings"
"time"

"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/ext"

"go.pinniped.dev/internal/idtransform"
)

const (
usernameVariableName = "username"
groupsVariableName = "groups"

defaultPolicyRejectedAuthMessage = "Authentication was rejected by a configured policy"
)

// CELTransformer can compile any number of transformation expression pipelines.
// Each compiled pipeline can be cached in memory for later thread-safe evaluation.
type CELTransformer struct {
compiler *cel.Env
maxExpressionRuntime time.Duration
}

// NewCELTransformer returns a CELTransformer.
// A running process should only need one instance of a CELTransformer.
func NewCELTransformer(maxExpressionRuntime time.Duration) (*CELTransformer, error) {
env, err := newEnv()
if err != nil {
return nil, err
}
return &CELTransformer{compiler: env, maxExpressionRuntime: maxExpressionRuntime}, nil
}

// CompileTransformation compiles a CEL-based identity transformation expression.
// The compiled transform can be cached in memory and executed repeatedly and in a thread-safe way.
func (c *CELTransformer) CompileTransformation(t CELTransformation) (idtransform.IdentityTransformation, error) {
return t.compile(c)
}

// CELTransformation can be compiled into an IdentityTransformation.
type CELTransformation interface {
compile(transformer *CELTransformer) (idtransform.IdentityTransformation, error)
}

// UsernameTransformation is a CEL expression that can transform a username (or leave it unchanged).
// It implements CELTransformation.
type UsernameTransformation struct {
Expression string
}

// GroupsTransformation is a CEL expression that can transform a list of group names (or leave it unchanged).
// It implements CELTransformation.
type GroupsTransformation struct {
Expression string
}

// AllowAuthenticationPolicy is a CEL expression that can allow the authentication to proceed by returning true.
// It implements CELTransformation. When the CEL expression returns false, the authentication is rejected and the
// RejectedAuthenticationMessage is used. When RejectedAuthenticationMessage is empty, a default message will be
// used for rejected authentications.
type AllowAuthenticationPolicy struct {
Expression string
RejectedAuthenticationMessage string
}

func compileProgram(transformer *CELTransformer, expectedExpressionType *cel.Type, expr string) (cel.Program, error) {
if strings.TrimSpace(expr) == "" {
return nil, fmt.Errorf("cannot compile empty CEL expression")
}

// compile does both parsing and type checking. The parsing phase indicates whether the expression is
// syntactically valid and expands any macros present within the environment. Parsing and checking are
// more computationally expensive than evaluation, so parsing and checking are done in advance.
ast, issues := transformer.compiler.Compile(expr)
if issues != nil {
return nil, fmt.Errorf("CEL expression compile error: %s", issues.String())
}

// The compiler's type checker has determined the type of the expression's result.
// Check that it matches the type that we expect.
if ast.OutputType().String() != expectedExpressionType.String() {
return nil, fmt.Errorf("CEL expression should return type %q but returns type %q", expectedExpressionType, ast.OutputType())
}

// The cel.Program is stateless, thread-safe, and cachable.
program, err := transformer.compiler.Program(ast,
cel.InterruptCheckFrequency(100), // Kubernetes uses 100 here, so we'll copy that setting.
cel.EvalOptions(cel.OptOptimize), // Optimize certain things now rather than at evaluation time.
)
if err != nil {
return nil, fmt.Errorf("CEL expression program construction error: %w", err)
}
return program, nil
}

func (t *UsernameTransformation) compile(transformer *CELTransformer) (idtransform.IdentityTransformation, error) {
program, err := compileProgram(transformer, cel.StringType, t.Expression)
if err != nil {
return nil, err
}
return &compiledUsernameTransformation{
program: program,
maxExpressionRuntime: transformer.maxExpressionRuntime,
}, nil
}

func (t *GroupsTransformation) compile(transformer *CELTransformer) (idtransform.IdentityTransformation, error) {
program, err := compileProgram(transformer, cel.ListType(cel.StringType), t.Expression)
if err != nil {
return nil, err
}
return &compiledGroupsTransformation{
program: program,
maxExpressionRuntime: transformer.maxExpressionRuntime,
}, nil
}

func (t *AllowAuthenticationPolicy) compile(transformer *CELTransformer) (idtransform.IdentityTransformation, error) {
program, err := compileProgram(transformer, cel.BoolType, t.Expression)
if err != nil {
return nil, err
}
return &compiledAllowAuthenticationPolicy{
program: program,
maxExpressionRuntime: transformer.maxExpressionRuntime,
rejectedAuthenticationMessage: t.RejectedAuthenticationMessage,
}, nil
}

// Implements idtransform.IdentityTransformation.
type compiledUsernameTransformation struct {
program cel.Program
maxExpressionRuntime time.Duration
}

// Implements idtransform.IdentityTransformation.
type compiledGroupsTransformation struct {
program cel.Program
maxExpressionRuntime time.Duration
}

// Implements idtransform.IdentityTransformation.
type compiledAllowAuthenticationPolicy struct {
program cel.Program
maxExpressionRuntime time.Duration
rejectedAuthenticationMessage string
}

func evalProgram(ctx context.Context, program cel.Program, maxExpressionRuntime time.Duration, username string, groups []string) (ref.Val, error) {
// Limit the runtime of a CEL expression to avoid accidental very expensive expressions.
timeoutCtx, cancel := context.WithTimeout(ctx, maxExpressionRuntime)
defer cancel()

// Evaluation is thread-safe and side effect free. Many inputs can be sent to the same cel.Program
// and if fields are present in the input, but not referenced in the expression, they are ignored.
// The argument to Eval may either be an `interpreter.Activation` or a `map[string]interface{}`.
val, _, err := program.ContextEval(timeoutCtx, map[string]interface{}{
usernameVariableName: username,
groupsVariableName: groups,
})
return val, err
}

func (c *compiledUsernameTransformation) Evaluate(ctx context.Context, username string, groups []string) (*idtransform.TransformationResult, error) {
val, err := evalProgram(ctx, c.program, c.maxExpressionRuntime, username, groups)
if err != nil {
return nil, err
}
nativeValue, err := val.ConvertToNative(reflect.TypeOf(""))
if err != nil {
return nil, fmt.Errorf("could not convert expression result to string: %w", err)
}
stringValue, ok := nativeValue.(string)
if !ok {
return nil, fmt.Errorf("could not convert expression result to string")
}
return &idtransform.TransformationResult{
Username: stringValue,
Groups: groups, // groups are not modified by username transformations
AuthenticationAllowed: true,
}, nil
}

func (c *compiledGroupsTransformation) Evaluate(ctx context.Context, username string, groups []string) (*idtransform.TransformationResult, error) {
val, err := evalProgram(ctx, c.program, c.maxExpressionRuntime, username, groups)
if err != nil {
return nil, err
}
nativeValue, err := val.ConvertToNative(reflect.TypeOf([]string{}))
if err != nil {
return nil, fmt.Errorf("could not convert expression result to []string: %w", err)
}
stringSliceValue, ok := nativeValue.([]string)
if !ok {
return nil, fmt.Errorf("could not convert expression result to []string")
}
return &idtransform.TransformationResult{
Username: username, // username is not modified by groups transformations
Groups: stringSliceValue,
AuthenticationAllowed: true,
}, nil
}

func (c *compiledAllowAuthenticationPolicy) Evaluate(ctx context.Context, username string, groups []string) (*idtransform.TransformationResult, error) {
val, err := evalProgram(ctx, c.program, c.maxExpressionRuntime, username, groups)
if err != nil {
return nil, err
}
nativeValue, err := val.ConvertToNative(reflect.TypeOf(true))
if err != nil {
return nil, fmt.Errorf("could not convert expression result to bool: %w", err)
}
boolValue, ok := nativeValue.(bool)
if !ok {
return nil, fmt.Errorf("could not convert expression result to bool")
}
result := &idtransform.TransformationResult{
Username: username, // username is not modified by policies
Groups: groups, // groups are not modified by policies
AuthenticationAllowed: boolValue,
}
if !boolValue {
if len(c.rejectedAuthenticationMessage) == 0 {
result.RejectedAuthenticationMessage = defaultPolicyRejectedAuthMessage
} else {
result.RejectedAuthenticationMessage = c.rejectedAuthenticationMessage
}
}
return result, nil
}

func newEnv() (*cel.Env, error) {
// Note that Kubernetes uses CEL in several places, which are helpful to see as an example of
// how to configure the CEL compiler for production usage. Examples:
// https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/compiler.go
// https://github.com/kubernetes/kubernetes/blob/master/staging/src/k8s.io/apiextensions-apiserver/pkg/apiserver/schema/cel/compilation.go
return cel.NewEnv(
// Declare our variable without giving them values yet. By declaring them here, the type is known during
// the parsing/checking phase.
cel.Variable(usernameVariableName, cel.StringType),
cel.Variable(groupsVariableName, cel.ListType(cel.StringType)),

// Enable the strings extensions.
// See https://github.com/google/cel-go/tree/master/ext#strings
// CEL also has other extensions for bas64 encoding/decoding and for math that we could choose to enable.
// See https://github.com/google/cel-go/tree/master/ext
// Kubernetes adds more extensions for extra regexp helpers, URLs, and extra list helpers that we could also
// consider enabling. Note that if we added their regexp extension, then we would also need to add
// cel.OptimizeRegex(library.ExtensionLibRegexOptimizations...) as an option when we call cel.Program.
// See https://github.com/kubernetes/kubernetes/tree/master/staging/src/k8s.io/apiserver/pkg/cel/library
ext.Strings(),

// Just in case someone converts a string to a timestamp, make any time operations which do not include
// an explicit timezone argument default to UTC.
cel.DefaultUTCTimeZone(true),

// Check list and map literal entry types during type-checking.
cel.HomogeneousAggregateLiterals(),

// Check for collisions in declarations now instead of later.
cel.EagerlyValidateDeclarations(true),
)
}
Loading

0 comments on commit 65a55d1

Please sign in to comment.