Add models from cel-policy-templates

This commit is contained in:
Joe Betz 2021-11-09 16:09:52 -05:00
parent d73403dc12
commit a37dfa7f0e
14 changed files with 3908 additions and 0 deletions

View File

@ -0,0 +1,303 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package model
import (
"fmt"
"strings"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
)
// NewDecision returns an empty Decision instance.
func NewDecision() *Decision {
return &Decision{}
}
// Decision contains a decision name, or reference to a decision name, and an output expression.
type Decision struct {
Name string
Reference *cel.Ast
Output *cel.Ast
}
// DecisionValue represents a named decision and value.
type DecisionValue interface {
fmt.Stringer
// Name returns the decision name.
Name() string
// IsFinal returns whether the decision value will change with additional rule evaluations.
//
// When a decision is final, additional productions and rules which may also trigger the same
// decision may be skipped.
IsFinal() bool
}
// SingleDecisionValue extends the DecisionValue which contains a single decision value as well
// as some metadata about the evaluation details and the rule that spawned the value.
type SingleDecisionValue interface {
DecisionValue
// Value returns the single value for the decision.
Value() ref.Val
// Details returns the evaluation details, if present, that produced the value.
Details() *cel.EvalDetails
// RuleID indicate which policy rule id within an instance that produced the decision.
RuleID() int64
}
// MultiDecisionValue extends the DecisionValue which contains a set of decision values as well as
// the corresponding metadata about how each value was produced.
type MultiDecisionValue interface {
DecisionValue
// Values returns the collection of values produced for the decision.
Values() []ref.Val
// Details returns the evaluation details for each value in the decision.
// The value index correponds to the details index. The details may be nil.
Details() []*cel.EvalDetails
// RulesIDs returns the rule id within an instance which produce the decision values.
// The value index corresponds to the rule id index.
RuleIDs() []int64
}
// DecisionSelector determines whether the given decision is the decision set requested by the
// caller.
type DecisionSelector func(decision string) bool
// NewBoolDecisionValue returns a boolean decision with an initial value.
func NewBoolDecisionValue(name string, value types.Bool) *BoolDecisionValue {
return &BoolDecisionValue{
name: name,
value: value,
}
}
// BoolDecisionValue represents the decision value type associated with a decision.
type BoolDecisionValue struct {
name string
value ref.Val
isFinal bool
details *cel.EvalDetails
ruleID int64
}
// And logically ANDs the current decision value with the incoming CEL value.
//
// And follows CEL semantics with respect to errors and unknown values where errors may be
// absorbed or short-circuited away by subsequent 'false' values. When unkonwns are encountered
// the unknown values combine and aggregate within the decision. Unknowns may also be absorbed
// per CEL semantics.
func (dv *BoolDecisionValue) And(other ref.Val) *BoolDecisionValue {
v, vBool := dv.value.(types.Bool)
if vBool && v == types.False {
return dv
}
o, oBool := other.(types.Bool)
if oBool && o == types.False {
dv.value = types.False
return dv
}
if vBool && oBool {
return dv
}
dv.value = logicallyMergeUnkErr(dv.value, other)
return dv
}
// Details implements the SingleDecisionValue interface method.
func (dv *BoolDecisionValue) Details() *cel.EvalDetails {
return dv.details
}
// Finalize marks the decision as immutable with additional input and indicates the rule and
// evaluation details which triggered the finalization.
func (dv *BoolDecisionValue) Finalize(details *cel.EvalDetails, rule Rule) DecisionValue {
dv.details = details
if rule != nil {
dv.ruleID = rule.GetID()
}
dv.isFinal = true
return dv
}
// IsFinal returns whether the decision is final.
func (dv *BoolDecisionValue) IsFinal() bool {
return dv.isFinal
}
// Or logically ORs the decision value with the incoming CEL value.
//
// The ORing logic follows CEL semantics with respect to errors and unknown values.
// Errors may be absorbed or short-circuited away by subsequent 'true' values. When unkonwns are
// encountered the unknown values combine and aggregate within the decision. Unknowns may also be
// absorbed per CEL semantics.
func (dv *BoolDecisionValue) Or(other ref.Val) *BoolDecisionValue {
v, vBool := dv.value.(types.Bool)
if vBool && v == types.True {
return dv
}
o, oBool := other.(types.Bool)
if oBool && o == types.True {
dv.value = types.True
return dv
}
if vBool && oBool {
return dv
}
dv.value = logicallyMergeUnkErr(dv.value, other)
return dv
}
// Name implements the DecisionValue interface method.
func (dv *BoolDecisionValue) Name() string {
return dv.name
}
// RuleID implements the SingleDecisionValue interface method.
func (dv *BoolDecisionValue) RuleID() int64 {
return dv.ruleID
}
// String renders the decision value to a string for debug purposes.
func (dv *BoolDecisionValue) String() string {
var buf strings.Builder
buf.WriteString(dv.name)
buf.WriteString(": ")
buf.WriteString(fmt.Sprintf("rule[%d] -> ", dv.ruleID))
buf.WriteString(fmt.Sprintf("%v", dv.value))
return buf.String()
}
// Value implements the SingleDecisionValue interface method.
func (dv *BoolDecisionValue) Value() ref.Val {
return dv.value
}
// NewListDecisionValue returns a named decision value which contains a list of CEL values produced
// by one or more policy instances and / or production rules.
func NewListDecisionValue(name string) *ListDecisionValue {
return &ListDecisionValue{
name: name,
values: []ref.Val{},
details: []*cel.EvalDetails{},
ruleIDs: []int64{},
}
}
// ListDecisionValue represents a named decision which collects into a list of values.
type ListDecisionValue struct {
name string
values []ref.Val
isFinal bool
details []*cel.EvalDetails
ruleIDs []int64
}
// Append accumulates the incoming CEL value into the decision's value list.
func (dv *ListDecisionValue) Append(val ref.Val, det *cel.EvalDetails, rule Rule) {
dv.values = append(dv.values, val)
dv.details = append(dv.details, det)
// Rule ids may be null if the policy is a singleton.
ruleID := int64(0)
if rule != nil {
ruleID = rule.GetID()
}
dv.ruleIDs = append(dv.ruleIDs, ruleID)
}
// Details returns the list of evaluation details observed in computing the values in the decision.
// The details indices correlate 1:1 with the value indices.
func (dv *ListDecisionValue) Details() []*cel.EvalDetails {
return dv.details
}
// Finalize marks the list decision complete.
func (dv *ListDecisionValue) Finalize() DecisionValue {
dv.isFinal = true
return dv
}
// IsFinal implements the DecisionValue interface method.
func (dv *ListDecisionValue) IsFinal() bool {
return dv.isFinal
}
// Name implements the DecisionValue interface method.
func (dv *ListDecisionValue) Name() string {
return dv.name
}
// RuleIDs returns the list of rule ids which produced the evaluation results.
// The indices of the ruleIDs correlate 1:1 with the value indices.
func (dv *ListDecisionValue) RuleIDs() []int64 {
return dv.ruleIDs
}
func (dv *ListDecisionValue) String() string {
var buf strings.Builder
buf.WriteString(dv.name)
buf.WriteString(": ")
for i, v := range dv.values {
if len(dv.ruleIDs) == len(dv.values) {
buf.WriteString(fmt.Sprintf("rule[%d] -> ", dv.ruleIDs[i]))
}
buf.WriteString(fmt.Sprintf("%v", v))
buf.WriteString("\n")
if i < len(dv.values)-1 {
buf.WriteString("\t")
}
}
return buf.String()
}
// Values implements the MultiDecisionValue interface method.
func (dv *ListDecisionValue) Values() []ref.Val {
return dv.values
}
func logicallyMergeUnkErr(value, other ref.Val) ref.Val {
vUnk := types.IsUnknown(value)
oUnk := types.IsUnknown(other)
if vUnk && oUnk {
merged := types.Unknown{}
merged = append(merged, value.(types.Unknown)...)
merged = append(merged, other.(types.Unknown)...)
return merged
}
if vUnk {
return value
}
if oUnk {
return other
}
if types.IsError(value) {
return value
}
if types.IsError(other) {
return other
}
return types.NewErr(
"got values (%v, %v), wanted boolean values",
value, other)
}

View File

@ -0,0 +1,121 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package model
import (
"reflect"
"testing"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
)
func TestBoolDecisionValue_And(t *testing.T) {
tests := []struct {
name string
value types.Bool
ands []ref.Val
result ref.Val
}{
{
name: "init_false_end_false",
value: types.False,
ands: []ref.Val{types.NewErr("err"), types.True},
result: types.False,
},
{
name: "init_true_end_false",
value: types.True,
ands: []ref.Val{types.NewErr("err"), types.False},
result: types.False,
},
{
name: "init_true_end_err",
value: types.True,
ands: []ref.Val{types.True, types.NewErr("err")},
result: types.NewErr("err"),
},
{
name: "init_true_end_unk",
value: types.True,
ands: []ref.Val{types.True, types.Unknown{1}, types.NewErr("err"), types.Unknown{2}},
result: types.Unknown{1, 2},
},
}
for _, tst := range tests {
tc := tst
t.Run(tc.name, func(tt *testing.T) {
v := NewBoolDecisionValue(tc.name, tc.value)
for _, av := range tc.ands {
v = v.And(av)
}
v.Finalize(nil, nil)
if !reflect.DeepEqual(v.Value(), tc.result) {
tt.Errorf("decision AND failed. got %v, wanted %v", v.Value(), tc.result)
}
})
}
}
func TestBoolDecisionValue_Or(t *testing.T) {
tests := []struct {
name string
value types.Bool
ors []ref.Val
result ref.Val
}{
{
name: "init_false_end_true",
value: types.False,
ors: []ref.Val{types.NewErr("err"), types.Unknown{1}, types.True},
result: types.True,
},
{
name: "init_true_end_true",
value: types.True,
ors: []ref.Val{types.NewErr("err"), types.False},
result: types.True,
},
{
name: "init_false_end_err",
value: types.False,
ors: []ref.Val{types.False, types.NewErr("err1"), types.NewErr("err2")},
result: types.NewErr("err1"),
},
{
name: "init_false_end_unk",
value: types.False,
ors: []ref.Val{types.False, types.Unknown{1}, types.NewErr("err"), types.Unknown{2}},
result: types.Unknown{1, 2},
},
}
for _, tst := range tests {
tc := tst
t.Run(tc.name, func(tt *testing.T) {
v := NewBoolDecisionValue(tc.name, tc.value)
for _, av := range tc.ors {
v = v.Or(av)
}
// Test finalization
v.Finalize(nil, nil)
// Ensure that calling string on the value doesn't error.
_ = v.String()
// Compare the output result
if !reflect.DeepEqual(v.Value(), tc.result) {
tt.Errorf("decision OR failed. got %v, wanted %v", v.Value(), tc.result)
}
})
}
}

View File

@ -0,0 +1,209 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package model
import (
"github.com/google/cel-go/cel"
"github.com/google/cel-go/checker/decls"
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
)
// NewEnv creates an empty Env instance with a fully qualified name that may be referenced
// within templates.
func NewEnv(name string) *Env {
return &Env{
Name: name,
Functions: []*Function{},
Vars: []*Var{},
Types: map[string]*DeclType{},
}
}
// Env declares a set of variables, functions, and types available to a given set of CEL
// expressions.
//
// The Env name must be fully qualified as it will be referenced within template evaluators,
// validators, and possibly within the metadata of the instance rule schema.
//
// Note, the Types values currently only holds type definitions associated with a variable
// declaration. Any type mentioned in the environment which does not have a definition is
// treated as a reference to a type which must be supplied in the base CEL environment provided
// by the policy engine.
type Env struct {
Name string
Container string
Functions []*Function
Vars []*Var
Types map[string]*DeclType
}
// ExprEnvOptions returns a set of CEL environment options to be used when extending the base
// policy engine CEL environment.
func (e *Env) ExprEnvOptions() []cel.EnvOption {
opts := []cel.EnvOption{}
if e.Container != "" {
opts = append(opts, cel.Container(e.Container))
}
if len(e.Vars) > 0 {
vars := make([]*exprpb.Decl, len(e.Vars))
for i, v := range e.Vars {
vars[i] = v.ExprDecl()
}
opts = append(opts, cel.Declarations(vars...))
}
if len(e.Functions) > 0 {
funcs := make([]*exprpb.Decl, len(e.Functions))
for i, f := range e.Functions {
funcs[i] = f.ExprDecl()
}
opts = append(opts, cel.Declarations(funcs...))
}
return opts
}
// NewVar creates a new variable with a name and a type.
func NewVar(name string, dt *DeclType) *Var {
return &Var{
Name: name,
Type: dt,
}
}
// Var represents a named instanced of a type.
type Var struct {
Name string
Type *DeclType
}
// ExprDecl produces a CEL proto declaration for the variable.
func (v *Var) ExprDecl() *exprpb.Decl {
return decls.NewVar(v.Name, v.Type.ExprType())
}
// NewFunction creates a Function instance with a simple function name and a set of overload
// signatures.
func NewFunction(name string, overloads ...*Overload) *Function {
return &Function{
Name: name,
Overloads: overloads,
}
}
// Function represents a simple name and a set of overload signatures.
type Function struct {
Name string
Overloads []*Overload
}
// ExprDecl produces a CEL proto declaration for the function and its overloads.
func (f *Function) ExprDecl() *exprpb.Decl {
overloadDecls := make([]*exprpb.Decl_FunctionDecl_Overload, len(f.Overloads))
for i, o := range f.Overloads {
overloadDecls[i] = o.overloadDecl()
}
return decls.NewFunction(f.Name, overloadDecls...)
}
// NewOverload returns a receiver-style overload declaration for a given function.
//
// The overload name must follow the conventions laid out within the CEL overloads.go file.
//
// // Receiver-style overload name:
// <receiver_type>_<func>_<arg_type0>_<arg_typeN>
//
// Within this function, the first type supplied is the receiver type, and the last type supplied
// is used as the return type. At least two types must be specified for a zero-arity receiver
// function.
func NewOverload(name string, first *DeclType, rest ...*DeclType) *Overload {
argTypes := make([]*DeclType, 1+len(rest))
argTypes[0] = first
for i := 1; i < len(rest)+1; i++ {
argTypes[i] = rest[i-1]
}
returnType := argTypes[len(argTypes)-1]
argTypes = argTypes[0 : len(argTypes)-1]
return newOverload(name, false, argTypes, returnType)
}
// NewFreeFunctionOverload returns a free function overload for a given function name.
//
// The overload name must follow the conventions laid out within the CEL overloads.go file:
//
// // Free function style overload name:
// <func>_<arg_type0>_<arg_typeN>
//
// When the function name is global, <func> will refer to the simple function name. When the
// function has a qualified name, replace the '.' characters in the fully-qualified name with
// underscores.
//
// Within this function, the last type supplied is used as the return type. At least one type must
// be specified for a zero-arity free function.
func NewFreeFunctionOverload(name string, first *DeclType, rest ...*DeclType) *Overload {
argTypes := make([]*DeclType, 1+len(rest))
argTypes[0] = first
for i := 1; i < len(rest)+1; i++ {
argTypes[i] = rest[i-1]
}
returnType := argTypes[len(argTypes)-1]
argTypes = argTypes[0 : len(argTypes)-1]
return newOverload(name, true, argTypes, returnType)
}
func newOverload(name string,
freeFunction bool,
argTypes []*DeclType,
returnType *DeclType) *Overload {
return &Overload{
Name: name,
FreeFunction: freeFunction,
Args: argTypes,
ReturnType: returnType,
}
}
// Overload represents a single function overload signature.
type Overload struct {
Name string
FreeFunction bool
Args []*DeclType
ReturnType *DeclType
}
func (o *Overload) overloadDecl() *exprpb.Decl_FunctionDecl_Overload {
typeParams := map[string]struct{}{}
argExprTypes := make([]*exprpb.Type, len(o.Args))
for i, a := range o.Args {
if a.TypeParam {
typeParams[a.TypeName()] = struct{}{}
}
argExprTypes[i] = a.ExprType()
}
returnType := o.ReturnType.ExprType()
if len(typeParams) == 0 {
if o.FreeFunction {
return decls.NewOverload(o.Name, argExprTypes, returnType)
}
return decls.NewInstanceOverload(o.Name, argExprTypes, returnType)
}
typeParamNames := make([]string, 0, len(typeParams))
for param := range typeParams {
typeParamNames = append(typeParamNames, param)
}
if o.FreeFunction {
return decls.NewParameterizedOverload(o.Name, argExprTypes, returnType, typeParamNames)
}
return decls.NewParameterizedInstanceOverload(o.Name, argExprTypes, returnType, typeParamNames)
}

View File

@ -0,0 +1,73 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package model
import (
"testing"
"github.com/google/cel-go/cel"
)
func TestEnv_Vars(t *testing.T) {
env := NewEnv("test.v1.Environment")
env.Container = "test.v1"
env.Vars = []*Var{
NewVar("greeting", StringType),
NewVar("replies", NewListType(StringType)),
}
expr := `greeting == 'hello' && replies.size() > 0`
stdEnv, _ := cel.NewEnv()
ast, iss := stdEnv.Compile(expr)
if iss.Err() == nil {
t.Errorf("got ast %v, expected error", ast)
}
custEnv, err := stdEnv.Extend(env.ExprEnvOptions()...)
if err != nil {
t.Fatal(err)
}
_, iss = custEnv.Compile(expr)
if iss.Err() != nil {
t.Errorf("got error %v, wanted ast", iss)
}
}
func TestEnv_Funcs(t *testing.T) {
env := NewEnv("test.v1.Environment")
env.Container = "test.v1"
env.Functions = []*Function{
NewFunction("greeting",
NewOverload("string_greeting_string", StringType, StringType, BoolType),
NewFreeFunctionOverload("greeting_string", StringType, BoolType)),
NewFunction("getOrDefault",
NewOverload("map_get_or_default_param",
NewMapType(NewTypeParam("K"), NewTypeParam("V")),
NewTypeParam("K"), NewTypeParam("V"),
NewTypeParam("V"))),
}
expr := `greeting('hello') && 'jim'.greeting('hello') && {'a': 0}.getOrDefault('b', 1) == 1`
stdEnv, _ := cel.NewEnv()
ast, iss := stdEnv.Compile(expr)
if iss.Err() == nil {
t.Errorf("got ast %v, expected error", ast)
}
custEnv, err := stdEnv.Extend(env.ExprEnvOptions()...)
if err != nil {
t.Fatal(err)
}
_, iss = custEnv.Compile(expr)
if iss.Err() != nil {
t.Errorf("got error %v, wanted ast", iss)
}
}

View File

@ -0,0 +1,148 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package model contains abstract representations of policy template and instance config objects.
package model
import (
"strings"
)
// NewInstance returns an empty policy instance.
func NewInstance(info SourceMetadata) *Instance {
return &Instance{
Metadata: &InstanceMetadata{},
Selectors: []Selector{},
Rules: []Rule{},
Meta: info,
}
}
// Instance represents the compiled, type-checked, and validated policy instance.
type Instance struct {
APIVersion string
Kind string
Metadata *InstanceMetadata
Description string
// Selectors determine whether the instance applies to the current evaluation context.
// All Selector values must return true for the policy instance to be included in policy
// evaluation step.
Selectors []Selector
// Rules represent reference data to be used in evaluation policy decisions.
// Depending on the nature of the decisions being emitted, some or all Rules may be evaluated
// and the results aggregated according to the decision types being emitted.
Rules []Rule
// Meta represents the source metadata from the input instance.
Meta SourceMetadata
}
// MetadataMap returns the metadata name to value map, which can be used in evaluation.
// Only "name" field is supported for now.
func (i *Instance) MetadataMap() map[string]interface{} {
return map[string]interface{}{
"name": i.Metadata.Name,
}
}
// InstanceMetadata contains standard metadata which may be associated with an instance.
type InstanceMetadata struct {
UID string
Name string
Namespace string
}
// Selector interface indicates a pre-formatted instance selection condition.
//
// The implementations of such conditions are expected to be platform specific.
//
// Note, if there is a clear need to tailor selection more heavily, then the schema definition
// for a selector should be moved into the Template schema.
type Selector interface {
isSelector()
}
// LabelSelector matches key, value pairs of labels associated with the evaluation context.
//
// In Kubernetes, the such labels are provided as 'resource.labels'.
type LabelSelector struct {
// LabelValues provides a map of the string keys and values expected.
LabelValues map[string]string
}
func (*LabelSelector) isSelector() {}
// ExpressionSelector matches a label against an existence condition.
type ExpressionSelector struct {
// Label name being matched.
Label string
// Operator determines the evaluation behavior. Must be one of Exists, NotExists, In, or NotIn.
Operator string
// Values set, optional, to be used in the NotIn, In set membership tests.
Values []interface{}
}
func (*ExpressionSelector) isSelector() {}
// Rule interface indicates the value types that may be used as Rule instances.
//
// Note, the code within the main repo deals exclusively with custom, yaml-based rules, but it
// is entirely possible to use a protobuf message as the rule container.
type Rule interface {
isRule()
GetID() int64
GetFieldID(field string) int64
}
// CustomRule embeds the DynValue and represents rules whose type definition is provided in the
// policy template.
type CustomRule struct {
*DynValue
}
func (*CustomRule) isRule() {}
// GetID returns the parse-time generated ID of the rule node.
func (c *CustomRule) GetID() int64 {
return c.ID
}
// GetFieldID returns the parse-time generated ID pointing to the rule field. If field is not
// specified or is not found, falls back to the ID of the rule node.
func (c *CustomRule) GetFieldID(field string) int64 {
if field == "" {
return c.GetID()
}
paths := strings.Split(field, ".")
val := c.DynValue
for _, path := range paths {
var f *Field
var ok bool
switch v := val.Value().(type) {
case *ObjectValue:
f, ok = v.GetField(path)
case *MapValue:
f, ok = v.GetField(path)
}
if !ok {
return c.GetID()
}
val = f.Ref
}
return val.ID
}

View File

@ -0,0 +1,189 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package model
import (
"fmt"
"sync"
"github.com/google/cel-go/cel"
)
// Resolver declares methods to find policy templates and related configuration objects.
type Resolver interface {
// FindEnv returns an Env object by its fully-qualified name, if present.
FindEnv(name string) (*Env, bool)
// FindExprEnv returns a CEL expression environment by its fully-qualified name, if present.
//
// Note, the CEL expression environment name corresponds with the model Environment name;
// however, the expression environment may inherit configuration via the CEL env.Extend method.
FindExprEnv(name string) (*cel.Env, bool)
// FindSchema returns an Open API Schema instance by name, if present.
//
// Schema names start with a `#` sign as this method is only used to resolve references to
// relative schema elements within `$ref` schema nodes.
FindSchema(name string) (*OpenAPISchema, bool)
// FindTemplate returns a Template by its fully-qualified name, if present.
FindTemplate(name string) (*Template, bool)
// FindType returns a DeclType instance corresponding to the given fully-qualified name, if
// present.
FindType(name string) (*DeclType, bool)
}
// NewRegistry create a registry for keeping track of environments, schemas, templates, and more
// from a base cel.Env expression environment.
func NewRegistry(stdExprEnv *cel.Env) *Registry {
return &Registry{
envs: map[string]*Env{},
exprEnvs: map[string]*cel.Env{"": stdExprEnv},
schemas: map[string]*OpenAPISchema{
"#anySchema": AnySchema,
"#envSchema": envSchema,
"#instanceSchema": instanceSchema,
"#openAPISchema": schemaDef,
"#templateSchema": templateSchema,
},
templates: map[string]*Template{},
types: map[string]*DeclType{
AnyType.TypeName(): AnyType,
BoolType.TypeName(): BoolType,
BytesType.TypeName(): BytesType,
DoubleType.TypeName(): DoubleType,
DurationType.TypeName(): DurationType,
IntType.TypeName(): IntType,
NullType.TypeName(): NullType,
PlainTextType.TypeName(): PlainTextType,
StringType.TypeName(): StringType,
TimestampType.TypeName(): TimestampType,
UintType.TypeName(): UintType,
ListType.TypeName(): ListType,
MapType.TypeName(): MapType,
},
}
}
// Registry defines a repository of environment, schema, template, and type definitions.
//
// Registry instances are concurrency-safe.
type Registry struct {
rwMux sync.RWMutex
envs map[string]*Env
exprEnvs map[string]*cel.Env
schemas map[string]*OpenAPISchema
templates map[string]*Template
types map[string]*DeclType
}
// FindEnv implements the Resolver interface method.
func (r *Registry) FindEnv(name string) (*Env, bool) {
r.rwMux.RLock()
defer r.rwMux.RUnlock()
env, found := r.envs[name]
return env, found
}
// FindExprEnv implements the Resolver interface method.
func (r *Registry) FindExprEnv(name string) (*cel.Env, bool) {
r.rwMux.RLock()
defer r.rwMux.RUnlock()
exprEnv, found := r.exprEnvs[name]
return exprEnv, found
}
// FindSchema implements the Resolver interface method.
func (r *Registry) FindSchema(name string) (*OpenAPISchema, bool) {
r.rwMux.RLock()
defer r.rwMux.RUnlock()
schema, found := r.schemas[name]
return schema, found
}
// FindTemplate implements the Resolver interface method.
func (r *Registry) FindTemplate(name string) (*Template, bool) {
r.rwMux.RLock()
defer r.rwMux.RUnlock()
tmpl, found := r.templates[name]
return tmpl, found
}
// FindType implements the Resolver interface method.
func (r *Registry) FindType(name string) (*DeclType, bool) {
r.rwMux.RLock()
defer r.rwMux.RUnlock()
typ, found := r.types[name]
if found {
return typ, true
}
return typ, found
}
// SetEnv registers an environment description by fully qualified name.
func (r *Registry) SetEnv(name string, env *Env) error {
r.rwMux.Lock()
defer r.rwMux.Unlock()
// Cleanup environment related artifacts when the env is reset.
priorEnv, found := r.envs[name]
if found {
for typeName := range priorEnv.Types {
delete(r.types, typeName)
}
}
// Configure the new environment.
baseExprEnv, found := r.exprEnvs[""]
if !found {
return fmt.Errorf("missing default expression environment")
}
exprEnv, err := baseExprEnv.Extend(env.ExprEnvOptions()...)
if err != nil {
return err
}
r.exprEnvs[name] = exprEnv
r.envs[name] = env
for typeName, typ := range env.Types {
r.types[typeName] = typ
}
return nil
}
// SetSchema registers an OpenAPISchema fragment by its relative name so that it may be referenced
// as a reusable schema unit within other OpenAPISchema instances.
//
// Name format: '#<simpleName>'.
func (r *Registry) SetSchema(name string, schema *OpenAPISchema) error {
r.rwMux.Lock()
defer r.rwMux.Unlock()
r.schemas[name] = schema
return nil
}
// SetTemplate registers a template by its fully qualified name.
func (r *Registry) SetTemplate(name string, tmpl *Template) error {
r.rwMux.Lock()
defer r.rwMux.Unlock()
r.templates[name] = tmpl
return nil
}
// SetType registers a DeclType descriptor by its fully qualified name.
func (r *Registry) SetType(name string, declType *DeclType) error {
r.rwMux.Lock()
defer r.rwMux.Unlock()
r.types[name] = declType
return nil
}

View File

@ -0,0 +1,451 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package model
import (
"strings"
"gopkg.in/yaml.v3"
)
// NewOpenAPISchema returns an empty instance of an OpenAPISchema object.
func NewOpenAPISchema() *OpenAPISchema {
return &OpenAPISchema{
Enum: []interface{}{},
Metadata: map[string]string{},
Properties: map[string]*OpenAPISchema{},
Required: []string{},
}
}
// OpenAPISchema declares a struct capable of representing a subset of Open API Schemas
// supported by Kubernetes which can also be specified within Protocol Buffers.
//
// There are a handful of notable differences:
// - The validating constructs `allOf`, `anyOf`, `oneOf`, `not`, and type-related restrictsion are
// not supported as they can be better validated in the template 'validator' block.
// - The $ref field supports references to other schema definitions, but such aliases
// should be removed before being serialized.
// - The `additionalProperties` and `properties` fields are not currently mutually exclusive as is
// the case for Kubernetes.
//
// See: https://kubernetes.io/docs/tasks/access-kubernetes-api/custom-resources/custom-resource-definitions/#validation
type OpenAPISchema struct {
Title string `yaml:"title,omitempty"`
Description string `yaml:"description,omitempty"`
Type string `yaml:"type,omitempty"`
TypeParam string `yaml:"type_param,omitempty"`
TypeRef string `yaml:"$ref,omitempty"`
DefaultValue interface{} `yaml:"default,omitempty"`
Enum []interface{} `yaml:"enum,omitempty"`
Format string `yaml:"format,omitempty"`
Items *OpenAPISchema `yaml:"items,omitempty"`
Metadata map[string]string `yaml:"metadata,omitempty"`
Required []string `yaml:"required,omitempty"`
Properties map[string]*OpenAPISchema `yaml:"properties,omitempty"`
AdditionalProperties *OpenAPISchema `yaml:"additionalProperties,omitempty"`
}
// DeclTypes constructs a top-down set of DeclType instances whose name is derived from the root
// type name provided on the call, if not set to a custom type.
func (s *OpenAPISchema) DeclTypes(maybeRootType string) (*DeclType, map[string]*DeclType) {
root := s.DeclType().MaybeAssignTypeName(maybeRootType)
types := FieldTypeMap(maybeRootType, root)
return root, types
}
// DeclType returns the CEL Policy Templates type name associated with the schema element.
func (s *OpenAPISchema) DeclType() *DeclType {
if s.TypeParam != "" {
return NewTypeParam(s.TypeParam)
}
declType, found := openAPISchemaTypes[s.Type]
if !found {
return NewObjectTypeRef("*error*")
}
switch declType.TypeName() {
case ListType.TypeName():
return NewListType(s.Items.DeclType())
case MapType.TypeName():
if s.AdditionalProperties != nil {
return NewMapType(StringType, s.AdditionalProperties.DeclType())
}
fields := make(map[string]*DeclField, len(s.Properties))
required := make(map[string]struct{}, len(s.Required))
for _, name := range s.Required {
required[name] = struct{}{}
}
for name, prop := range s.Properties {
_, isReq := required[name]
fields[name] = &DeclField{
Name: name,
Required: isReq,
Type: prop.DeclType(),
defaultValue: prop.DefaultValue,
enumValues: prop.Enum,
}
}
customType, hasCustomType := s.Metadata["custom_type"]
if !hasCustomType {
return NewObjectType("object", fields)
}
return NewObjectType(customType, fields)
case StringType.TypeName():
switch s.Format {
case "byte", "binary":
return BytesType
case "google-duration":
return DurationType
case "date", "date-time", "google-datetime":
return TimestampType
case "int64":
return IntType
case "uint64":
return UintType
}
}
return declType
}
// FindProperty returns the Open API Schema type for the given property name.
//
// A property may either be explicitly defined in a `properties` map or implicitly defined in an
// `additionalProperties` block.
func (s *OpenAPISchema) FindProperty(name string) (*OpenAPISchema, bool) {
if s.DeclType() == AnyType {
return s, true
}
if s.Properties != nil {
prop, found := s.Properties[name]
if found {
return prop, true
}
}
if s.AdditionalProperties != nil {
return s.AdditionalProperties, true
}
return nil, false
}
var (
// SchemaDef defines an Open API Schema definition in terms of an Open API Schema.
schemaDef *OpenAPISchema
// AnySchema indicates that the value may be of any type.
AnySchema *OpenAPISchema
// EnvSchema defines the schema for CEL environments referenced within Policy Templates.
envSchema *OpenAPISchema
// InstanceSchema defines a basic schema for defining Policy Instances where the instance rule
// references a TemplateSchema derived from the Instance's template kind.
instanceSchema *OpenAPISchema
// TemplateSchema defines a schema for defining Policy Templates.
templateSchema *OpenAPISchema
openAPISchemaTypes map[string]*DeclType = map[string]*DeclType{
"boolean": BoolType,
"number": DoubleType,
"integer": IntType,
"null": NullType,
"string": StringType,
"google-duration": DurationType,
"google-datetime": TimestampType,
"date": TimestampType,
"date-time": TimestampType,
"array": ListType,
"object": MapType,
"": AnyType,
}
)
const (
schemaDefYaml = `
type: object
properties:
$ref:
type: string
type:
type: string
type_param: # prohibited unless used within an environment.
type: string
format:
type: string
description:
type: string
required:
type: array
items:
type: string
enum:
type: array
items:
type: string
enumDescriptions:
type: array
items:
type: string
default: {}
items:
$ref: "#openAPISchema"
properties:
type: object
additionalProperties:
$ref: "#openAPISchema"
additionalProperties:
$ref: "#openAPISchema"
metadata:
type: object
additionalProperties:
type: string
`
templateSchemaYaml = `
type: object
required:
- apiVersion
- kind
- metadata
- evaluator
properties:
apiVersion:
type: string
kind:
type: string
metadata:
type: object
required:
- name
properties:
uid:
type: string
name:
type: string
namespace:
type: string
default: "default"
etag:
type: string
labels:
type: object
additionalProperties:
type: string
pluralName:
type: string
description:
type: string
schema:
$ref: "#openAPISchema"
validator:
type: object
required:
- productions
properties:
description:
type: string
environment:
type: string
terms:
type: object
additionalProperties: {}
productions:
type: array
items:
type: object
required:
- message
properties:
match:
type: string
default: true
field:
type: string
message:
type: string
details: {}
evaluator:
type: object
required:
- productions
properties:
description:
type: string
environment:
type: string
ranges:
type: array
items:
type: object
required:
- in
properties:
in:
type: string
key:
type: string
index:
type: string
value:
type: string
terms:
type: object
additionalProperties:
type: string
productions:
type: array
items:
type: object
properties:
match:
type: string
default: "true"
decision:
type: string
decisionRef:
type: string
output: {}
decisions:
type: array
items:
type: object
required:
- output
properties:
decision:
type: string
decisionRef:
type: string
output: {}
`
instanceSchemaYaml = `
type: object
required:
- apiVersion
- kind
- metadata
properties:
apiVersion:
type: string
kind:
type: string
metadata:
type: object
additionalProperties:
type: string
description:
type: string
selector:
type: object
properties:
matchLabels:
type: object
additionalProperties:
type: string
matchExpressions:
type: array
items:
type: object
required:
- key
- operator
properties:
key:
type: string
operator:
type: string
enum: ["DoesNotExist", "Exists", "In", "NotIn"]
values:
type: array
items: {}
default: []
rule:
$ref: "#templateRuleSchema"
rules:
type: array
items:
$ref: "#templateRuleSchema"
`
// TODO: support subsetting of built-in functions and macros
// TODO: support naming anonymous types within rule schema and making them accessible to
// declarations.
// TODO: consider supporting custom macros
envSchemaYaml = `
type: object
required:
- name
properties:
name:
type: string
container:
type: string
variables:
type: object
additionalProperties:
$ref: "#openAPISchema"
functions:
type: object
properties:
extensions:
type: object
additionalProperties:
type: object # function name
additionalProperties:
type: object # overload name
required:
- return
properties:
free_function:
type: boolean
args:
type: array
items:
$ref: "#openAPISchema"
return:
$ref: "#openAPISchema"
`
)
func init() {
AnySchema = NewOpenAPISchema()
instanceSchema = NewOpenAPISchema()
in := strings.ReplaceAll(instanceSchemaYaml, "\t", " ")
err := yaml.Unmarshal([]byte(in), instanceSchema)
if err != nil {
panic(err)
}
envSchema = NewOpenAPISchema()
in = strings.ReplaceAll(envSchemaYaml, "\t", " ")
err = yaml.Unmarshal([]byte(in), envSchema)
if err != nil {
panic(err)
}
schemaDef = NewOpenAPISchema()
in = strings.ReplaceAll(schemaDefYaml, "\t", " ")
err = yaml.Unmarshal([]byte(in), schemaDef)
if err != nil {
panic(err)
}
templateSchema = NewOpenAPISchema()
in = strings.ReplaceAll(templateSchemaYaml, "\t", " ")
err = yaml.Unmarshal([]byte(in), templateSchema)
if err != nil {
panic(err)
}
}

View File

@ -0,0 +1,203 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package model
import (
"reflect"
"testing"
"github.com/google/cel-go/checker/decls"
"github.com/google/cel-go/common/types"
"google.golang.org/protobuf/proto"
)
func TestSchemaDeclType(t *testing.T) {
ts := testSchema()
cust := ts.DeclType()
if cust.TypeName() != "CustomObject" {
t.Errorf("incorrect type name, got %v, wanted CustomObject", cust.TypeName())
}
if len(cust.Fields) != 4 {
t.Errorf("incorrect number of fields, got %d, wanted 4", len(cust.Fields))
}
for _, f := range cust.Fields {
prop, found := ts.FindProperty(f.Name)
if !found {
t.Errorf("type field not found in schema, field: %s", f.Name)
}
fdv := f.DefaultValue()
if prop.DefaultValue != nil {
pdv := types.DefaultTypeAdapter.NativeToValue(prop.DefaultValue)
if !reflect.DeepEqual(fdv, pdv) {
t.Errorf("field and schema do not agree on default value, field: %s", f.Name)
}
}
if prop.Enum == nil && len(f.EnumValues()) != 0 {
t.Errorf("field had more enum values than the property. field: %s", f.Name)
}
if prop.Enum != nil {
fevs := f.EnumValues()
for _, fev := range fevs {
found := false
for _, pev := range prop.Enum {
pev = types.DefaultTypeAdapter.NativeToValue(pev)
if reflect.DeepEqual(fev, pev) {
found = true
break
}
}
if !found {
t.Errorf(
"could not find field enum value in property definition. field: %s, enum: %v",
f.Name, fev)
}
}
}
}
for _, name := range ts.Required {
df, found := cust.FindField(name)
if !found {
t.Errorf("custom type missing required field. field=%s", name)
}
if !df.Required {
t.Errorf("field marked as required in schema, but optional in type. field=%s", df.Name)
}
}
}
func TestSchemaDeclTypes(t *testing.T) {
ts := testSchema()
cust, typeMap := ts.DeclTypes("mock_template")
nested, _ := cust.FindField("nested")
metadata, _ := cust.FindField("metadata")
metadataElem := metadata.Type.ElemType
expectedObjTypeMap := map[string]*DeclType{
"CustomObject": cust,
"CustomObject.nested": nested.Type,
"CustomObject.metadata.@elem": metadataElem,
}
objTypeMap := map[string]*DeclType{}
for name, t := range typeMap {
if t.IsObject() {
objTypeMap[name] = t
}
}
if len(objTypeMap) != len(expectedObjTypeMap) {
t.Errorf("got different type set. got=%v, wanted=%v", typeMap, expectedObjTypeMap)
}
for exp, expType := range expectedObjTypeMap {
actType, found := objTypeMap[exp]
if !found {
t.Errorf("missing type in rule types: %s", exp)
continue
}
if !proto.Equal(expType.ExprType(), actType.ExprType()) {
t.Errorf("incompatible CEL types. got=%v, wanted=%v", actType.ExprType(), expType.ExprType())
}
}
metaExprType := metadata.Type.ExprType()
expectedMetaExprType := decls.NewMapType(
decls.String,
decls.NewObjectType("CustomObject.metadata.@elem"))
if !proto.Equal(expectedMetaExprType, metaExprType) {
t.Errorf("got metadata CEL type %v, wanted %v", metaExprType, expectedMetaExprType)
}
}
func testSchema() *OpenAPISchema {
// Manual construction of a schema with the following definition:
//
// schema:
// type: object
// metadata:
// custom_type: "CustomObject"
// required:
// - name
// - value
// properties:
// name:
// type: string
// nested:
// type: object
// properties:
// subname:
// type: string
// flags:
// type: object
// additionalProperties:
// type: boolean
// dates:
// type: array
// items:
// type: string
// format: date-time
// metadata:
// type: object
// additionalProperties:
// type: object
// properties:
// key:
// type: string
// values:
// type: array
// items: string
// value:
// type: integer
// format: int64
// default: 1
// enum: [1,2,3]
nameField := NewOpenAPISchema()
nameField.Type = "string"
valueField := NewOpenAPISchema()
valueField.Type = "integer"
valueField.Format = "int64"
valueField.DefaultValue = int64(1)
valueField.Enum = []interface{}{int64(1), int64(2), int64(3)}
nestedObjField := NewOpenAPISchema()
nestedObjField.Type = "object"
nestedObjField.Properties["subname"] = NewOpenAPISchema()
nestedObjField.Properties["subname"].Type = "string"
nestedObjField.Properties["flags"] = NewOpenAPISchema()
nestedObjField.Properties["flags"].Type = "object"
nestedObjField.Properties["flags"].AdditionalProperties = NewOpenAPISchema()
nestedObjField.Properties["flags"].AdditionalProperties.Type = "boolean"
nestedObjField.Properties["dates"] = NewOpenAPISchema()
nestedObjField.Properties["dates"].Type = "array"
nestedObjField.Properties["dates"].Items = NewOpenAPISchema()
nestedObjField.Properties["dates"].Items.Type = "string"
nestedObjField.Properties["dates"].Items.Format = "date-time"
metadataKeyValue := NewOpenAPISchema()
metadataKeyValue.Type = "object"
metadataKeyValue.Properties["key"] = NewOpenAPISchema()
metadataKeyValue.Properties["key"].Type = "string"
metadataKeyValue.Properties["values"] = NewOpenAPISchema()
metadataKeyValue.Properties["values"].Type = "array"
metadataKeyValue.Properties["values"].Items = NewOpenAPISchema()
metadataKeyValue.Properties["values"].Items.Type = "string"
metadataObjField := NewOpenAPISchema()
metadataObjField.Type = "object"
metadataObjField.AdditionalProperties = metadataKeyValue
ts := NewOpenAPISchema()
ts.Type = "object"
ts.Metadata["custom_type"] = "CustomObject"
ts.Required = []string{"name", "value"}
ts.Properties["name"] = nameField
ts.Properties["value"] = valueField
ts.Properties["nested"] = nestedObjField
ts.Properties["metadata"] = metadataObjField
return ts
}

View File

@ -0,0 +1,190 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package model
import (
"github.com/google/cel-go/common"
)
// ByteSource converts a byte sequence and location description to a model.Source.
func ByteSource(contents []byte, location string) *Source {
return StringSource(string(contents), location)
}
// StringSource converts a string and location description to a model.Source.
func StringSource(contents, location string) *Source {
return &Source{
Source: common.NewStringSource(contents, location),
}
}
// Source represents the contents of a single source file.
type Source struct {
common.Source
}
// Relative produces a RelativeSource object for the content provided at the absolute location
// within the parent Source as indicated by the line and column.
func (src *Source) Relative(content string, line, col int) *RelativeSource {
return &RelativeSource{
Source: src.Source,
localSrc: common.NewStringSource(content, src.Description()),
absLoc: common.NewLocation(line, col),
}
}
// RelativeSource represents an embedded source element within a larger source.
type RelativeSource struct {
common.Source
localSrc common.Source
absLoc common.Location
}
// AbsoluteLocation returns the location within the parent Source where the RelativeSource starts.
func (rel *RelativeSource) AbsoluteLocation() common.Location {
return rel.absLoc
}
// Content returns the embedded source snippet.
func (rel *RelativeSource) Content() string {
return rel.localSrc.Content()
}
// OffsetLocation returns the absolute location given the relative offset, if found.
func (rel *RelativeSource) OffsetLocation(offset int32) (common.Location, bool) {
absOffset, found := rel.Source.LocationOffset(rel.absLoc)
if !found {
return common.NoLocation, false
}
return rel.Source.OffsetLocation(absOffset + offset)
}
// NewLocation creates an absolute common.Location based on a local line, column
// position from a relative source.
func (rel *RelativeSource) NewLocation(line, col int) common.Location {
localLoc := common.NewLocation(line, col)
relOffset, found := rel.localSrc.LocationOffset(localLoc)
if !found {
return common.NoLocation
}
offset, _ := rel.Source.LocationOffset(rel.absLoc)
absLoc, _ := rel.Source.OffsetLocation(offset + relOffset)
return absLoc
}
// NewSourceInfo creates SourceInfo metadata from a Source object.
func NewSourceInfo(src common.Source) *SourceInfo {
return &SourceInfo{
Comments: make(map[int64][]*Comment),
LineOffsets: src.LineOffsets(),
Description: src.Description(),
Offsets: make(map[int64]int32),
}
}
// SourceInfo contains metadata about the Source such as comments, line positions, and source
// element offsets.
type SourceInfo struct {
// Comments mapped by source element id to a comment set.
Comments map[int64][]*Comment
// LineOffsets contains the list of character offsets where newlines occur in the source.
LineOffsets []int32
// Description indicates something about the source, such as its file name.
Description string
// Offsets map from source element id to the character offset where the source element starts.
Offsets map[int64]int32
}
// SourceMetadata enables the lookup for expression source metadata by expression id.
type SourceMetadata interface {
// CommentsByID returns the set of comments associated with the expression id, if present.
CommentsByID(int64) ([]*Comment, bool)
// LocationByID returns the CEL common.Location of the expression id, if present.
LocationByID(int64) (common.Location, bool)
}
// CommentsByID returns the set of comments by expression id, if present.
func (info *SourceInfo) CommentsByID(id int64) ([]*Comment, bool) {
comments, found := info.Comments[id]
return comments, found
}
// LocationByID returns the line and column location of source node by its id.
func (info *SourceInfo) LocationByID(id int64) (common.Location, bool) {
charOff, found := info.Offsets[id]
if !found {
return common.NoLocation, false
}
ln, lnOff := info.findLine(charOff)
return common.NewLocation(int(ln), int(charOff-lnOff)), true
}
func (info *SourceInfo) findLine(characterOffset int32) (int32, int32) {
var line int32 = 1
for _, lineOffset := range info.LineOffsets {
if lineOffset > characterOffset {
break
} else {
line++
}
}
if line == 1 {
return line, 0
}
return line, info.LineOffsets[line-2]
}
// CommentStyle type used to indicate where a comment occurs.
type CommentStyle int
const (
// HeadComment indicates that the comment is defined in the lines preceding the source element.
HeadComment CommentStyle = iota + 1
// LineComment indicates that the comment occurs on the same line after the source element.
LineComment
// FootComment indicates that the comment occurs after the source element with at least one
// blank line before the next source element.
FootComment
)
// NewHeadComment creates a new HeadComment from the text.
func NewHeadComment(txt string) *Comment {
return &Comment{Text: txt, Style: HeadComment}
}
// NewLineComment creates a new LineComment from the text.
func NewLineComment(txt string) *Comment {
return &Comment{Text: txt, Style: LineComment}
}
// NewFootComment creates a new FootComment from the text.
func NewFootComment(txt string) *Comment {
return &Comment{Text: txt, Style: FootComment}
}
// Comment represents a comment within source.
type Comment struct {
// Text contains the comment text.
Text string
// Style indicates where the comment appears relative to a source element.
Style CommentStyle
}

View File

@ -0,0 +1,159 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package model
import (
"github.com/google/cel-go/cel"
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
)
// NewTemplate produces an empty policy Template instance.
func NewTemplate(info SourceMetadata) *Template {
return &Template{
Metadata: NewTemplateMetadata(),
Evaluator: NewEvaluator(),
Meta: info,
}
}
// Template represents the compiled and type-checked policy template.
type Template struct {
APIVersion string
Kind string
Metadata *TemplateMetadata
Description string
RuleTypes *RuleTypes
Validator *Evaluator
Evaluator *Evaluator
Meta SourceMetadata
}
// EvaluatorDecisionCount returns the number of decisions which can be produced by the template
// evaluator production rules.
func (t *Template) EvaluatorDecisionCount() int {
return t.Evaluator.DecisionCount()
}
// MetadataMap returns the metadata name to value map, which can be used in evaluation.
// Only "name" field is supported for now.
func (t *Template) MetadataMap() map[string]interface{} {
return map[string]interface{}{
"name": t.Metadata.Name,
}
}
// NewTemplateMetadata returns an empty *TemplateMetadata instance.
func NewTemplateMetadata() *TemplateMetadata {
return &TemplateMetadata{
Properties: make(map[string]string),
}
}
// TemplateMetadata contains the top-level information about the Template, including its name and
// namespace.
type TemplateMetadata struct {
UID string
Name string
Namespace string
// PluralMame is the plural form of the template name to use when managing a collection of
// template instances.
PluralName string
// Properties contains an optional set of key-value information which external applications
// might find useful.
Properties map[string]string
}
// NewEvaluator returns an empty instance of a Template Evaluator.
func NewEvaluator() *Evaluator {
return &Evaluator{
Terms: []*Term{},
Productions: []*Production{},
}
}
// Evaluator contains a set of production rules used to validate policy templates or
// evaluate template instances.
//
// The evaluator may optionally specify a named and versioned Environment as the basis for the
// variables and functions exposed to the CEL expressions within the Evaluator, and an optional
// set of terms.
//
// Terms are like template-local variables. Terms may rely on other terms which precede them.
// Term order matters, and no cycles are permitted among terms by design and convention.
type Evaluator struct {
Environment string
Ranges []*Range
Terms []*Term
Productions []*Production
}
// DecisionCount returns the number of possible decisions which could be emitted by this evaluator.
func (e *Evaluator) DecisionCount() int {
decMap := map[string]struct{}{}
for _, p := range e.Productions {
for _, d := range p.Decisions {
decMap[d.Name] = struct{}{}
}
}
return len(decMap)
}
// Range expresses a looping condition where the key (or index) and value can be extracted from the
// range CEL expression.
type Range struct {
ID int64
Key *exprpb.Decl
Value *exprpb.Decl
Expr *cel.Ast
}
// NewTerm produces a named Term instance associated with a CEL Ast and a list of the input
// terms needed to evaluate the Ast successfully.
func NewTerm(id int64, name string, expr *cel.Ast) *Term {
return &Term{
ID: id,
Name: name,
Expr: expr,
}
}
// Term is a template-local variable whose name may shadow names in the Template environment and
// which may depend on preceding terms as input.
type Term struct {
ID int64
Name string
Expr *cel.Ast
}
// NewProduction returns an empty instance of a Production rule which minimally contains a single
// Decision.
func NewProduction(id int64, match *cel.Ast) *Production {
return &Production{
ID: id,
Match: match,
Decisions: []*Decision{},
}
}
// Production describes an match-decision pair where the match, if set, indicates whether the
// Decision is applicable, and the decision indicates its name and output value.
type Production struct {
ID int64
Match *cel.Ast
Decisions []*Decision
}

View File

@ -0,0 +1,562 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package model
import (
"fmt"
"time"
"github.com/google/cel-go/cel"
"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/common/types/traits"
"google.golang.org/protobuf/proto"
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
)
// NewListType returns a parameterized list type with a specified element type.
func NewListType(elem *DeclType) *DeclType {
return &DeclType{
name: "list",
ElemType: elem,
exprType: decls.NewListType(elem.ExprType()),
defaultValue: NewListValue(),
}
}
// NewMapType returns a parameterized map type with the given key and element types.
func NewMapType(key, elem *DeclType) *DeclType {
return &DeclType{
name: "map",
KeyType: key,
ElemType: elem,
exprType: decls.NewMapType(key.ExprType(), elem.ExprType()),
defaultValue: NewMapValue(),
}
}
// NewObjectType creates an object type with a qualified name and a set of field declarations.
func NewObjectType(name string, fields map[string]*DeclField) *DeclType {
t := &DeclType{
name: name,
Fields: fields,
exprType: decls.NewObjectType(name),
traitMask: traits.FieldTesterType | traits.IndexerType,
}
t.defaultValue = NewObjectValue(t)
return t
}
// NewObjectTypeRef returns a reference to an object type by name
func NewObjectTypeRef(name string) *DeclType {
t := &DeclType{
name: name,
exprType: decls.NewObjectType(name),
traitMask: traits.FieldTesterType | traits.IndexerType,
}
return t
}
// NewTypeParam creates a type parameter type with a simple name.
//
// Type parameters are resolved at compilation time to concrete types, or CEL 'dyn' type if no
// type assignment can be inferred.
func NewTypeParam(name string) *DeclType {
return &DeclType{
name: name,
TypeParam: true,
exprType: decls.NewTypeParamType(name),
}
}
func newSimpleType(name string, exprType *exprpb.Type, zeroVal ref.Val) *DeclType {
return &DeclType{
name: name,
exprType: exprType,
defaultValue: zeroVal,
}
}
// DeclType represents the universal type descriptor for Policy Templates.
type DeclType struct {
fmt.Stringer
name string
Fields map[string]*DeclField
KeyType *DeclType
ElemType *DeclType
TypeParam bool
Metadata map[string]string
exprType *exprpb.Type
traitMask int
defaultValue ref.Val
}
// MaybeAssignTypeName attempts to set the DeclType name to a fully qualified name, if the type
// is of `object` type.
//
// The DeclType must return true for `IsObject` or this assignment will error.
func (t *DeclType) MaybeAssignTypeName(name string) *DeclType {
if t.IsObject() {
objUpdated := false
if t.name != "object" {
name = t.name
} else {
objUpdated = true
}
fieldMap := make(map[string]*DeclField, len(t.Fields))
for fieldName, field := range t.Fields {
fieldType := field.Type
fieldTypeName := fmt.Sprintf("%s.%s", name, fieldName)
updated := fieldType.MaybeAssignTypeName(fieldTypeName)
if updated == fieldType {
fieldMap[fieldName] = field
continue
}
objUpdated = true
fieldMap[fieldName] = &DeclField{
Name: fieldName,
Type: updated,
Required: field.Required,
enumValues: field.enumValues,
defaultValue: field.defaultValue,
}
}
if !objUpdated {
return t
}
return &DeclType{
name: name,
Fields: fieldMap,
KeyType: t.KeyType,
ElemType: t.ElemType,
TypeParam: t.TypeParam,
Metadata: t.Metadata,
exprType: decls.NewObjectType(name),
traitMask: t.traitMask,
defaultValue: t.defaultValue,
}
}
if t.IsMap() {
elemTypeName := fmt.Sprintf("%s.@elem", name)
updated := t.ElemType.MaybeAssignTypeName(elemTypeName)
if updated == t.ElemType {
return t
}
return NewMapType(t.KeyType, updated)
}
if t.IsList() {
elemTypeName := fmt.Sprintf("%s.@idx", name)
updated := t.ElemType.MaybeAssignTypeName(elemTypeName)
if updated == t.ElemType {
return t
}
return NewListType(updated)
}
return t
}
// ExprType returns the CEL expression type of this declaration.
func (t *DeclType) ExprType() *exprpb.Type {
return t.exprType
}
// FindField returns the DeclField with the given name if present.
func (t *DeclType) FindField(name string) (*DeclField, bool) {
f, found := t.Fields[name]
return f, found
}
// HasTrait implements the CEL ref.Type interface making this type declaration suitable for use
// within the CEL evaluator.
func (t *DeclType) HasTrait(trait int) bool {
if t.traitMask&trait == trait {
return true
}
if t.defaultValue == nil {
return false
}
_, isDecl := t.defaultValue.Type().(*DeclType)
if isDecl {
return false
}
return t.defaultValue.Type().HasTrait(trait)
}
// IsList returns whether the declaration is a `list` type which defines a parameterized element
// type, but not a parameterized key type or fields.
func (t *DeclType) IsList() bool {
return t.KeyType == nil && t.ElemType != nil && t.Fields == nil
}
// IsMap returns whether the declaration is a 'map' type which defines parameterized key and
// element types, but not fields.
func (t *DeclType) IsMap() bool {
return t.KeyType != nil && t.ElemType != nil && t.Fields == nil
}
// IsObject returns whether the declartion is an 'object' type which defined a set of typed fields.
func (t *DeclType) IsObject() bool {
return t.KeyType == nil && t.ElemType == nil && t.Fields != nil
}
// String implements the fmt.Stringer interface method.
func (t *DeclType) String() string {
return t.name
}
// TypeName returns the fully qualified type name for the DeclType.
func (t *DeclType) TypeName() string {
return t.name
}
// DefaultValue returns the CEL ref.Val representing the default value for this object type,
// if one exists.
func (t *DeclType) DefaultValue() ref.Val {
return t.defaultValue
}
// FieldTypeMap constructs a map of the field and object types nested within a given type.
func FieldTypeMap(path string, t *DeclType) map[string]*DeclType {
if t.IsObject() && t.TypeName() != "object" {
path = t.TypeName()
}
types := make(map[string]*DeclType)
buildDeclTypes(path, t, types)
return types
}
func buildDeclTypes(path string, t *DeclType, types map[string]*DeclType) {
// Ensure object types are properly named according to where they appear in the schema.
if t.IsObject() {
// Hack to ensure that names are uniquely qualified and work well with the type
// resolution steps which require fully qualified type names for field resolution
// to function properly.
types[t.TypeName()] = t
for name, field := range t.Fields {
fieldPath := fmt.Sprintf("%s.%s", path, name)
buildDeclTypes(fieldPath, field.Type, types)
}
}
// Map element properties to type names if needed.
if t.IsMap() {
mapElemPath := fmt.Sprintf("%s.@elem", path)
buildDeclTypes(mapElemPath, t.ElemType, types)
types[path] = t
}
// List element properties.
if t.IsList() {
listIdxPath := fmt.Sprintf("%s.@idx", path)
buildDeclTypes(listIdxPath, t.ElemType, types)
types[path] = t
}
}
// DeclField describes the name, ordinal, and optionality of a field declaration within a type.
type DeclField struct {
Name string
Type *DeclType
Required bool
enumValues []interface{}
defaultValue interface{}
}
// TypeName returns the string type name of the field.
func (f *DeclField) TypeName() string {
return f.Type.TypeName()
}
// DefaultValue returns the zero value associated with the field.
func (f *DeclField) DefaultValue() ref.Val {
if f.defaultValue != nil {
return types.DefaultTypeAdapter.NativeToValue(f.defaultValue)
}
return f.Type.DefaultValue()
}
// EnumValues returns the set of values that this field may take.
func (f *DeclField) EnumValues() []ref.Val {
if f.enumValues == nil || len(f.enumValues) == 0 {
return []ref.Val{}
}
ev := make([]ref.Val, len(f.enumValues))
for i, e := range f.enumValues {
ev[i] = types.DefaultTypeAdapter.NativeToValue(e)
}
return ev
}
// NewRuleTypes returns an Open API Schema-based type-system which is CEL compatible.
func NewRuleTypes(kind string,
schema *OpenAPISchema,
res Resolver) (*RuleTypes, error) {
// Note, if the schema indicates that it's actually based on another proto
// then prefer the proto definition. For expressions in the proto, a new field
// annotation will be needed to indicate the expected environment and type of
// the expression.
schemaTypes, err := newSchemaTypeProvider(kind, schema)
if err != nil {
return nil, err
}
return &RuleTypes{
Schema: schema,
ruleSchemaDeclTypes: schemaTypes,
resolver: res,
}, nil
}
// RuleTypes extends the CEL ref.TypeProvider interface and provides an Open API Schema-based
// type-system.
type RuleTypes struct {
ref.TypeProvider
Schema *OpenAPISchema
ruleSchemaDeclTypes *schemaTypeProvider
typeAdapter ref.TypeAdapter
resolver Resolver
}
// EnvOptions returns a set of cel.EnvOption values which includes the Template's declaration set
// as well as a custom ref.TypeProvider.
//
// Note, the standard declaration set includes 'rule' which is defined as the top-level rule-schema
// type if one is configured.
//
// If the RuleTypes value is nil, an empty []cel.EnvOption set is returned.
func (rt *RuleTypes) EnvOptions(tp ref.TypeProvider) ([]cel.EnvOption, error) {
if rt == nil {
return []cel.EnvOption{}, nil
}
var ta ref.TypeAdapter = types.DefaultTypeAdapter
tpa, ok := tp.(ref.TypeAdapter)
if ok {
ta = tpa
}
rtWithTypes := &RuleTypes{
TypeProvider: tp,
typeAdapter: ta,
Schema: rt.Schema,
ruleSchemaDeclTypes: rt.ruleSchemaDeclTypes,
resolver: rt.resolver,
}
for name, declType := range rt.ruleSchemaDeclTypes.types {
tpType, found := tp.FindType(name)
if found && !proto.Equal(tpType, declType.ExprType()) {
return nil, fmt.Errorf(
"type %s definition differs between CEL environment and template", name)
}
}
return []cel.EnvOption{
cel.CustomTypeProvider(rtWithTypes),
cel.CustomTypeAdapter(rtWithTypes),
cel.Declarations(
decls.NewVar("rule", rt.ruleSchemaDeclTypes.root.ExprType()),
),
}, nil
}
// FindType attempts to resolve the typeName provided from the template's rule-schema, or if not
// from the embedded ref.TypeProvider.
//
// FindType overrides the default type-finding behavior of the embedded TypeProvider.
//
// Note, when the type name is based on the Open API Schema, the name will reflect the object path
// where the type definition appears.
func (rt *RuleTypes) FindType(typeName string) (*exprpb.Type, bool) {
if rt == nil {
return nil, false
}
declType, found := rt.findDeclType(typeName)
if found {
return declType.ExprType(), found
}
return rt.TypeProvider.FindType(typeName)
}
// FindDeclType returns the CPT type description which can be mapped to a CEL type.
func (rt *RuleTypes) FindDeclType(typeName string) (*DeclType, bool) {
if rt == nil {
return nil, false
}
return rt.findDeclType(typeName)
}
// FindFieldType returns a field type given a type name and field name, if found.
//
// Note, the type name for an Open API Schema type is likely to be its qualified object path.
// If, in the future an object instance rather than a type name were provided, the field
// resolution might more accurately reflect the expected type model. However, in this case
// concessions were made to align with the existing CEL interfaces.
func (rt *RuleTypes) FindFieldType(typeName, fieldName string) (*ref.FieldType, bool) {
st, found := rt.findDeclType(typeName)
if !found {
return rt.TypeProvider.FindFieldType(typeName, fieldName)
}
f, found := st.Fields[fieldName]
if found {
ft := f.Type
return &ref.FieldType{
Type: ft.ExprType(),
}, true
}
// This could be a dynamic map.
if st.IsMap() {
et := st.ElemType
return &ref.FieldType{
Type: et.ExprType(),
}, true
}
return nil, false
}
// ConvertToRule transforms an untyped DynValue into a typed object.
//
// Conversion is done deeply and will traverse the object graph represented by the dyn value.
func (rt *RuleTypes) ConvertToRule(dyn *DynValue) Rule {
ruleSchemaType := rt.ruleSchemaDeclTypes.root
// TODO: handle conversions to protobuf types.
dyn = rt.convertToCustomType(dyn, ruleSchemaType)
return &CustomRule{DynValue: dyn}
}
// NativeToValue is an implementation of the ref.TypeAdapater interface which supports conversion
// of policy template values to CEL ref.Val instances.
func (rt *RuleTypes) NativeToValue(val interface{}) ref.Val {
switch v := val.(type) {
case *CustomRule:
return v.ExprValue()
default:
return rt.typeAdapter.NativeToValue(val)
}
}
// TypeNames returns the list of type names declared within the RuleTypes object.
func (rt *RuleTypes) TypeNames() []string {
typeNames := make([]string, len(rt.ruleSchemaDeclTypes.types))
i := 0
for name := range rt.ruleSchemaDeclTypes.types {
typeNames[i] = name
i++
}
return typeNames
}
func (rt *RuleTypes) findDeclType(typeName string) (*DeclType, bool) {
declType, found := rt.ruleSchemaDeclTypes.types[typeName]
if found {
return declType, true
}
declType, found = rt.resolver.FindType(typeName)
if found {
return declType, true
}
return nil, false
}
func (rt *RuleTypes) convertToCustomType(dyn *DynValue, declType *DeclType) *DynValue {
switch v := dyn.Value().(type) {
case *MapValue:
if declType.IsObject() {
obj := v.ConvertToObject(declType)
for name, f := range obj.fieldMap {
field := declType.Fields[name]
f.Ref = rt.convertToCustomType(f.Ref, field.Type)
}
dyn.SetValue(obj)
return dyn
}
// TODO: handle complex map types which have non-string keys.
fieldType := declType.ElemType
for _, f := range v.fieldMap {
f.Ref = rt.convertToCustomType(f.Ref, fieldType)
}
return dyn
case *ListValue:
for i := 0; i < len(v.Entries); i++ {
elem := v.Entries[i]
elem = rt.convertToCustomType(elem, declType.ElemType)
v.Entries[i] = elem
}
return dyn
default:
return dyn
}
}
func newSchemaTypeProvider(kind string, schema *OpenAPISchema) (*schemaTypeProvider, error) {
root := schema.DeclType().MaybeAssignTypeName(kind)
types := FieldTypeMap(kind, root)
return &schemaTypeProvider{
root: root,
types: types,
}, nil
}
type schemaTypeProvider struct {
root *DeclType
types map[string]*DeclType
}
var (
// AnyType is equivalent to the CEL 'protobuf.Any' type in that the value may have any of the
// types supported by CEL Policy Templates.
AnyType = newSimpleType("any", decls.Any, nil)
// BoolType is equivalent to the CEL 'bool' type.
BoolType = newSimpleType("bool", decls.Bool, types.False)
// BytesType is equivalent to the CEL 'bytes' type.
BytesType = newSimpleType("bytes", decls.Bytes, types.Bytes([]byte{}))
// DoubleType is equivalent to the CEL 'double' type which is a 64-bit floating point value.
DoubleType = newSimpleType("double", decls.Double, types.Double(0))
// DurationType is equivalent to the CEL 'duration' type.
DurationType = newSimpleType("duration", decls.Duration, types.Duration{Duration: time.Duration(0)})
// DynType is the equivalent of the CEL 'dyn' concept which indicates that the type will be
// determined at runtime rather than compile time.
DynType = newSimpleType("dyn", decls.Dyn, nil)
// IntType is equivalent to the CEL 'int' type which is a 64-bit signed int.
IntType = newSimpleType("int", decls.Int, types.IntZero)
// NullType is equivalent to the CEL 'null_type'.
NullType = newSimpleType("null_type", decls.Null, types.NullValue)
// StringType is equivalent to the CEL 'string' type which is expected to be a UTF-8 string.
// StringType values may either be string literals or expression strings.
StringType = newSimpleType("string", decls.String, types.String(""))
// PlainTextType is equivalent to the CEL 'string' type, but which has been specifically
// designated as a string literal.
PlainTextType = newSimpleType("string_lit", decls.String, types.String(""))
// TimestampType corresponds to the well-known protobuf.Timestamp type supported within CEL.
TimestampType = newSimpleType("timestamp", decls.Timestamp, types.Timestamp{Time: time.Time{}})
// UintType is equivalent to the CEL 'uint' type.
UintType = newSimpleType("uint", decls.Uint, types.Uint(0))
// ListType is equivalent to the CEL 'list' type.
ListType = NewListType(AnyType)
// MapType is equivalent to the CEL 'map' type.
MapType = NewMapType(AnyType, AnyType)
)

View File

@ -0,0 +1,155 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package model
import (
"testing"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types"
exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
)
func TestTypes_ListType(t *testing.T) {
list := NewListType(StringType)
if !list.IsList() {
t.Error("list type not identifiable as list")
}
if list.TypeName() != "list" {
t.Errorf("got %s, wanted list", list.TypeName())
}
if list.DefaultValue() == nil {
t.Error("got nil zero value for list type")
}
if list.ElemType.TypeName() != "string" {
t.Errorf("got %s, wanted elem type of string", list.ElemType.TypeName())
}
if list.ExprType().GetListType() == nil {
t.Errorf("got %v, wanted CEL list type", list.ExprType())
}
}
func TestTypes_MapType(t *testing.T) {
mp := NewMapType(StringType, IntType)
if !mp.IsMap() {
t.Error("map type not identifiable as map")
}
if mp.TypeName() != "map" {
t.Errorf("got %s, wanted map", mp.TypeName())
}
if mp.DefaultValue() == nil {
t.Error("got nil zero value for map type")
}
if mp.KeyType.TypeName() != "string" {
t.Errorf("got %s, wanted key type of string", mp.KeyType.TypeName())
}
if mp.ElemType.TypeName() != "int" {
t.Errorf("got %s, wanted elem type of int", mp.ElemType.TypeName())
}
if mp.ExprType().GetMapType() == nil {
t.Errorf("got %v, wanted CEL map type", mp.ExprType())
}
}
func TestTypes_RuleTypesFieldMapping(t *testing.T) {
stdEnv, _ := cel.NewEnv()
reg := NewRegistry(stdEnv)
rt, err := NewRuleTypes("mock_template", testSchema(), reg)
if err != nil {
t.Fatal(err)
}
nestedFieldType, found := rt.FindFieldType("CustomObject", "nested")
if !found {
t.Fatal("got field not found for 'CustomObject.nested', wanted found")
}
if nestedFieldType.Type.GetMessageType() != "CustomObject.nested" {
t.Errorf("got field type %v, wanted mock_template.nested", nestedFieldType.Type)
}
subnameFieldType, found := rt.FindFieldType("CustomObject.nested", "subname")
if !found {
t.Fatal("got field not found for 'CustomObject.nested.subname', wanted found")
}
if subnameFieldType.Type.GetPrimitive() != exprpb.Type_STRING {
t.Errorf("got field type %v, wanted string", subnameFieldType.Type)
}
flagsFieldType, found := rt.FindFieldType("CustomObject.nested", "flags")
if !found {
t.Fatal("got field not found for 'CustomObject.nested.flags', wanted found")
}
if flagsFieldType.Type.GetMapType() == nil {
t.Errorf("got field type %v, wanted map", flagsFieldType.Type)
}
flagFieldType, found := rt.FindFieldType("CustomObject.nested.flags", "my_flag")
if !found {
t.Fatal("got field not found for 'CustomObject.nested.flags.my_flag', wanted found")
}
if flagFieldType.Type.GetPrimitive() != exprpb.Type_BOOL {
t.Errorf("got field type %v, wanted bool", flagFieldType.Type)
}
// Manually constructed instance of the schema.
name := NewField(1, "name")
name.Ref = testValue(t, 2, "test-instance")
nestedVal := NewMapValue()
flags := NewField(5, "flags")
flagsVal := NewMapValue()
myFlag := NewField(6, "my_flag")
myFlag.Ref = testValue(t, 7, true)
flagsVal.AddField(myFlag)
flags.Ref = testValue(t, 8, flagsVal)
dates := NewField(9, "dates")
dates.Ref = testValue(t, 10, NewListValue())
nestedVal.AddField(flags)
nestedVal.AddField(dates)
nested := NewField(3, "nested")
nested.Ref = testValue(t, 4, nestedVal)
mapVal := NewMapValue()
mapVal.AddField(name)
mapVal.AddField(nested)
rule := rt.ConvertToRule(testValue(t, 11, mapVal))
if rule == nil {
t.Error("map could not be converted to rule")
}
if rule.GetID() != 11 {
t.Errorf("got %d as the rule id, wanted 11", rule.GetID())
}
ruleVal := rt.NativeToValue(rule)
if ruleVal == nil {
t.Error("got CEL rule value of nil, wanted non-nil")
}
opts, err := rt.EnvOptions(stdEnv.TypeProvider())
if err != nil {
t.Fatal(err)
}
ruleEnv, err := stdEnv.Extend(opts...)
if err != nil {
t.Fatal(err)
}
helloVal := ruleEnv.TypeAdapter().NativeToValue("hello")
if helloVal.Equal(types.String("hello")) != types.True {
t.Errorf("got %v, wanted types.String('hello')", helloVal)
}
}
func testValue(t *testing.T, id int64, val interface{}) *DynValue {
t.Helper()
dv, err := NewDynValue(id, val)
if err != nil {
t.Fatalf("model.NewDynValue(%d, %v) failed: %v", id, val, err)
}
return dv
}

View File

@ -0,0 +1,782 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package model
import (
"fmt"
"reflect"
"sync"
"time"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/common/types/traits"
)
// EncodeStyle is a hint for string encoding of parsed values.
type EncodeStyle int
const (
// BlockValueStyle is the default string encoding which preserves whitespace and newlines.
BlockValueStyle EncodeStyle = iota
// FlowValueStyle indicates that the string is an inline representation of complex types.
FlowValueStyle
// FoldedValueStyle is a multiline string with whitespace and newlines trimmed to a single
// a whitespace. Repeated newlines are replaced with a single newline rather than a single
// whitespace.
FoldedValueStyle
// LiteralStyle is a multiline string that preserves newlines, but trims all other whitespace
// to a single character.
LiteralStyle
)
// ParsedValue represents a top-level object representing either a template or instance value.
type ParsedValue struct {
ID int64
Value *MapValue
Meta SourceMetadata
}
// NewEmptyDynValue returns the zero-valued DynValue.
func NewEmptyDynValue() *DynValue {
// note: 0 is not a valid parse node identifier.
dv, _ := NewDynValue(0, nil)
return dv
}
// NewDynValue returns a DynValue that corresponds to a parse node id and value.
func NewDynValue(id int64, val interface{}) (*DynValue, error) {
dv := &DynValue{ID: id}
err := dv.SetValue(val)
return dv, err
}
// DynValue is a dynamically typed value used to describe unstructured content.
// Whether the value has the desired type is determined by where it is used within the Instance or
// Template, and whether there are schemas which might enforce a more rigid type definition.
type DynValue struct {
ID int64
EncodeStyle EncodeStyle
value interface{}
exprValue ref.Val
declType *DeclType
}
// DeclType returns the policy model type of the dyn value.
func (dv *DynValue) DeclType() *DeclType {
return dv.declType
}
// ConvertToNative is an implementation of the CEL ref.Val method used to adapt between CEL types
// and Go-native types.
//
// The default behavior of this method is to first convert to a CEL type which has a well-defined
// set of conversion behaviors and proxy to the CEL ConvertToNative method for the type.
func (dv *DynValue) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
ev := dv.ExprValue()
if types.IsError(ev) {
return nil, ev.(*types.Err)
}
return ev.ConvertToNative(typeDesc)
}
// Equal returns whether the dyn value is equal to a given CEL value.
func (dv *DynValue) Equal(other ref.Val) ref.Val {
dvType := dv.Type()
otherType := other.Type()
// Preserve CEL's homogeneous equality constraint.
if dvType.TypeName() != otherType.TypeName() {
return types.MaybeNoSuchOverloadErr(other)
}
switch v := dv.value.(type) {
case ref.Val:
return v.Equal(other)
case PlainTextValue:
return celBool(string(v) == other.Value().(string))
case *MultilineStringValue:
return celBool(v.Value == other.Value().(string))
case time.Duration:
otherDuration := other.Value().(time.Duration)
return celBool(v == otherDuration)
case time.Time:
otherTimestamp := other.Value().(time.Time)
return celBool(v.Equal(otherTimestamp))
default:
return celBool(reflect.DeepEqual(v, other.Value()))
}
}
// ExprValue converts the DynValue into a CEL value.
func (dv *DynValue) ExprValue() ref.Val {
return dv.exprValue
}
// Value returns the underlying value held by this reference.
func (dv *DynValue) Value() interface{} {
return dv.value
}
// SetValue updates the underlying value held by this reference.
func (dv *DynValue) SetValue(value interface{}) error {
dv.value = value
var err error
dv.exprValue, dv.declType, err = exprValue(value)
return err
}
// Type returns the CEL type for the given value.
func (dv *DynValue) Type() ref.Type {
return dv.ExprValue().Type()
}
func exprValue(value interface{}) (ref.Val, *DeclType, error) {
switch v := value.(type) {
case bool:
return types.Bool(v), BoolType, nil
case []byte:
return types.Bytes(v), BytesType, nil
case float64:
return types.Double(v), DoubleType, nil
case int64:
return types.Int(v), IntType, nil
case string:
return types.String(v), StringType, nil
case uint64:
return types.Uint(v), UintType, nil
case PlainTextValue:
return types.String(string(v)), PlainTextType, nil
case *MultilineStringValue:
return types.String(v.Value), StringType, nil
case time.Duration:
return types.Duration{Duration: v}, DurationType, nil
case time.Time:
return types.Timestamp{Time: v}, TimestampType, nil
case types.Null:
return v, NullType, nil
case *ListValue:
return v, ListType, nil
case *MapValue:
return v, MapType, nil
case *ObjectValue:
return v, v.objectType, nil
default:
return nil, unknownType, fmt.Errorf("unsupported type: (%T)%v", v, v)
}
}
// PlainTextValue is a text string literal which must not be treated as an expression.
type PlainTextValue string
// MultilineStringValue is a multiline string value which has been parsed in a way which omits
// whitespace as well as a raw form which preserves whitespace.
type MultilineStringValue struct {
Value string
Raw string
}
func newStructValue() *structValue {
return &structValue{
Fields: []*Field{},
fieldMap: map[string]*Field{},
}
}
type structValue struct {
Fields []*Field
fieldMap map[string]*Field
}
// AddField appends a MapField to the MapValue and indexes the field by name.
func (sv *structValue) AddField(field *Field) {
sv.Fields = append(sv.Fields, field)
sv.fieldMap[field.Name] = field
}
// ConvertToNative converts the MapValue type to a native go types.
func (sv *structValue) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
if typeDesc.Kind() != reflect.Map &&
typeDesc.Kind() != reflect.Struct &&
typeDesc.Kind() != reflect.Ptr &&
typeDesc.Kind() != reflect.Interface {
return nil, fmt.Errorf("type conversion error from object to '%v'", typeDesc)
}
// TODO: Special case handling for protobuf Struct and Any if needed
// Unwrap pointers, but track their use.
isPtr := false
if typeDesc.Kind() == reflect.Ptr {
tk := typeDesc
typeDesc = typeDesc.Elem()
if typeDesc.Kind() == reflect.Ptr {
return nil, fmt.Errorf("unsupported type conversion to '%v'", tk)
}
isPtr = true
}
if typeDesc.Kind() == reflect.Map {
keyType := typeDesc.Key()
if keyType.Kind() != reflect.String && keyType.Kind() != reflect.Interface {
return nil, fmt.Errorf("object fields cannot be converted to type '%v'", keyType)
}
elemType := typeDesc.Elem()
sz := len(sv.fieldMap)
ntvMap := reflect.MakeMapWithSize(typeDesc, sz)
for name, val := range sv.fieldMap {
refVal, err := val.Ref.ConvertToNative(elemType)
if err != nil {
return nil, err
}
ntvMap.SetMapIndex(reflect.ValueOf(name), reflect.ValueOf(refVal))
}
return ntvMap.Interface(), nil
}
if typeDesc.Kind() == reflect.Struct {
ntvObjPtr := reflect.New(typeDesc)
ntvObj := ntvObjPtr.Elem()
for name, val := range sv.fieldMap {
f := ntvObj.FieldByName(name)
if !f.IsValid() {
return nil, fmt.Errorf("type conversion error, no such field %s in type %v",
name, typeDesc)
}
fv, err := val.Ref.ConvertToNative(f.Type())
if err != nil {
return nil, err
}
f.Set(reflect.ValueOf(fv))
}
if isPtr {
return ntvObjPtr.Interface(), nil
}
return ntvObj.Interface(), nil
}
return nil, fmt.Errorf("type conversion error from object to '%v'", typeDesc)
}
// GetField returns a MapField by name if one exists.
func (sv *structValue) GetField(name string) (*Field, bool) {
field, found := sv.fieldMap[name]
return field, found
}
// IsSet returns whether the given field, which is defined, has also been set.
func (sv *structValue) IsSet(key ref.Val) ref.Val {
k, ok := key.(types.String)
if !ok {
return types.MaybeNoSuchOverloadErr(key)
}
name := string(k)
_, found := sv.fieldMap[name]
return celBool(found)
}
// NewObjectValue creates a struct value with a schema type and returns the empty ObjectValue.
func NewObjectValue(sType *DeclType) *ObjectValue {
return &ObjectValue{
structValue: newStructValue(),
objectType: sType,
}
}
// ObjectValue is a struct with a custom schema type which indicates the fields and types
// associated with the structure.
type ObjectValue struct {
*structValue
objectType *DeclType
}
// ConvertToType is an implementation of the CEL ref.Val interface method.
func (o *ObjectValue) ConvertToType(t ref.Type) ref.Val {
if t == types.TypeType {
return types.NewObjectTypeValue(o.objectType.TypeName())
}
if t.TypeName() == o.objectType.TypeName() {
return o
}
return types.NewErr("type conversion error from '%s' to '%s'", o.Type(), t)
}
// Equal returns true if the two object types are equal and their field values are equal.
func (o *ObjectValue) Equal(other ref.Val) ref.Val {
// Preserve CEL's homogeneous equality semantics.
if o.objectType.TypeName() != other.Type().TypeName() {
return types.MaybeNoSuchOverloadErr(other)
}
o2 := other.(traits.Indexer)
for name := range o.objectType.Fields {
k := types.String(name)
v := o.Get(k)
ov := o2.Get(k)
vEq := v.Equal(ov)
if vEq != types.True {
return vEq
}
}
return types.True
}
// Get returns the value of the specified field.
//
// If the field is set, its value is returned. If the field is not set, the default value for the
// field is returned thus allowing for safe-traversal and preserving proto-like field traversal
// semantics for Open API Schema backed types.
func (o *ObjectValue) Get(name ref.Val) ref.Val {
n, ok := name.(types.String)
if !ok {
return types.MaybeNoSuchOverloadErr(n)
}
nameStr := string(n)
field, found := o.fieldMap[nameStr]
if found {
return field.Ref.ExprValue()
}
fieldDef, found := o.objectType.Fields[nameStr]
if !found {
return types.NewErr("no such field: %s", nameStr)
}
defValue := fieldDef.DefaultValue()
if defValue != nil {
return defValue
}
return types.NewErr("no default for type: %s", fieldDef.TypeName())
}
// Type returns the CEL type value of the object.
func (o *ObjectValue) Type() ref.Type {
return o.objectType
}
// Value returns the Go-native representation of the object.
func (o *ObjectValue) Value() interface{} {
return o
}
// NewMapValue returns an empty MapValue.
func NewMapValue() *MapValue {
return &MapValue{
structValue: newStructValue(),
}
}
// MapValue declares an object with a set of named fields whose values are dynamically typed.
type MapValue struct {
*structValue
}
// ConvertToObject produces an ObjectValue from the MapValue with the associated schema type.
//
// The conversion is shallow and the memory shared between the Object and Map as all references
// to the map are expected to be replaced with the Object reference.
func (m *MapValue) ConvertToObject(declType *DeclType) *ObjectValue {
return &ObjectValue{
structValue: m.structValue,
objectType: declType,
}
}
// Contains returns whether the given key is contained in the MapValue.
func (m *MapValue) Contains(key ref.Val) ref.Val {
v, found := m.Find(key)
if v != nil && types.IsUnknownOrError(v) {
return v
}
return celBool(found)
}
// ConvertToType converts the MapValue to another CEL type, if possible.
func (m *MapValue) ConvertToType(t ref.Type) ref.Val {
switch t {
case types.MapType:
return m
case types.TypeType:
return types.MapType
}
return types.NewErr("type conversion error from '%s' to '%s'", m.Type(), t)
}
// Equal returns true if the maps are of the same size, have the same keys, and the key-values
// from each map are equal.
func (m *MapValue) Equal(other ref.Val) ref.Val {
oMap, isMap := other.(traits.Mapper)
if !isMap {
return types.MaybeNoSuchOverloadErr(other)
}
if m.Size() != oMap.Size() {
return types.False
}
for name, field := range m.fieldMap {
k := types.String(name)
ov, found := oMap.Find(k)
if !found {
return types.False
}
v := field.Ref.ExprValue()
vEq := v.Equal(ov)
if vEq != types.True {
return vEq
}
}
return types.True
}
// Find returns the value for the key in the map, if found.
func (m *MapValue) Find(name ref.Val) (ref.Val, bool) {
// Currently only maps with string keys are supported as this is best aligned with JSON,
// and also much simpler to support.
n, ok := name.(types.String)
if !ok {
return types.MaybeNoSuchOverloadErr(n), true
}
nameStr := string(n)
field, found := m.fieldMap[nameStr]
if found {
return field.Ref.ExprValue(), true
}
return nil, false
}
// Get returns the value for the key in the map, or error if not found.
func (m *MapValue) Get(key ref.Val) ref.Val {
v, found := m.Find(key)
if found {
return v
}
return types.ValOrErr(key, "no such key: %v", key)
}
// Iterator produces a traits.Iterator which walks over the map keys.
//
// The Iterator is frequently used within comprehensions.
func (m *MapValue) Iterator() traits.Iterator {
keys := make([]ref.Val, len(m.fieldMap))
i := 0
for k := range m.fieldMap {
keys[i] = types.String(k)
i++
}
return &baseMapIterator{
baseVal: &baseVal{},
keys: keys,
}
}
// Size returns the number of keys in the map.
func (m *MapValue) Size() ref.Val {
return types.Int(len(m.Fields))
}
// Type returns the CEL ref.Type for the map.
func (m *MapValue) Type() ref.Type {
return types.MapType
}
// Value returns the Go-native representation of the MapValue.
func (m *MapValue) Value() interface{} {
return m
}
type baseMapIterator struct {
*baseVal
keys []ref.Val
idx int
}
// HasNext implements the traits.Iterator interface method.
func (it *baseMapIterator) HasNext() ref.Val {
if it.idx < len(it.keys) {
return types.True
}
return types.False
}
// Next implements the traits.Iterator interface method.
func (it *baseMapIterator) Next() ref.Val {
key := it.keys[it.idx]
it.idx++
return key
}
// Type implements the CEL ref.Val interface metohd.
func (it *baseMapIterator) Type() ref.Type {
return types.IteratorType
}
// NewField returns a MapField instance with an empty DynValue that refers to the
// specified parse node id and field name.
func NewField(id int64, name string) *Field {
return &Field{
ID: id,
Name: name,
Ref: NewEmptyDynValue(),
}
}
// Field specifies a field name and a reference to a dynamic value.
type Field struct {
ID int64
Name string
Ref *DynValue
}
// NewListValue returns an empty ListValue instance.
func NewListValue() *ListValue {
return &ListValue{
Entries: []*DynValue{},
}
}
// ListValue contains a list of dynamically typed entries.
type ListValue struct {
Entries []*DynValue
initValueSet sync.Once
valueSet map[ref.Val]struct{}
}
// Add concatenates two lists together to produce a new CEL list value.
func (lv *ListValue) Add(other ref.Val) ref.Val {
oArr, isArr := other.(traits.Lister)
if !isArr {
return types.MaybeNoSuchOverloadErr(other)
}
szRight := len(lv.Entries)
szLeft := int(oArr.Size().(types.Int))
sz := szRight + szLeft
combo := make([]ref.Val, sz)
for i := 0; i < szRight; i++ {
combo[i] = lv.Entries[i].ExprValue()
}
for i := 0; i < szLeft; i++ {
combo[i+szRight] = oArr.Get(types.Int(i))
}
return types.DefaultTypeAdapter.NativeToValue(combo)
}
// Append adds another entry into the ListValue.
func (lv *ListValue) Append(entry *DynValue) {
lv.Entries = append(lv.Entries, entry)
// The append resets all previously built indices.
lv.initValueSet = sync.Once{}
}
// Contains returns whether the input `val` is equal to an element in the list.
//
// If any pair-wise comparison between the input value and the list element is an error, the
// operation will return an error.
func (lv *ListValue) Contains(val ref.Val) ref.Val {
if types.IsUnknownOrError(val) {
return val
}
lv.initValueSet.Do(lv.finalizeValueSet)
if lv.valueSet != nil {
_, found := lv.valueSet[val]
if found {
return types.True
}
// Instead of returning false, ensure that CEL's heterogeneous equality constraint
// is satisfied by allowing pair-wise equality behavior to determine the outcome.
}
var err ref.Val
sz := len(lv.Entries)
for i := 0; i < sz; i++ {
elem := lv.Entries[i]
cmp := elem.Equal(val)
b, ok := cmp.(types.Bool)
if !ok && err == nil {
err = types.MaybeNoSuchOverloadErr(cmp)
}
if b == types.True {
return types.True
}
}
if err != nil {
return err
}
return types.False
}
// ConvertToNative is an implementation of the CEL ref.Val method used to adapt between CEL types
// and Go-native array-like types.
func (lv *ListValue) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
// Non-list conversion.
if typeDesc.Kind() != reflect.Slice &&
typeDesc.Kind() != reflect.Array &&
typeDesc.Kind() != reflect.Interface {
return nil, fmt.Errorf("type conversion error from list to '%v'", typeDesc)
}
// If the list is already assignable to the desired type return it.
if reflect.TypeOf(lv).AssignableTo(typeDesc) {
return lv, nil
}
// List conversion.
otherElem := typeDesc.Elem()
// Allow the element ConvertToNative() function to determine whether conversion is possible.
sz := len(lv.Entries)
nativeList := reflect.MakeSlice(typeDesc, int(sz), int(sz))
for i := 0; i < sz; i++ {
elem := lv.Entries[i]
nativeElemVal, err := elem.ConvertToNative(otherElem)
if err != nil {
return nil, err
}
nativeList.Index(int(i)).Set(reflect.ValueOf(nativeElemVal))
}
return nativeList.Interface(), nil
}
// ConvertToType converts the ListValue to another CEL type.
func (lv *ListValue) ConvertToType(t ref.Type) ref.Val {
switch t {
case types.ListType:
return lv
case types.TypeType:
return types.ListType
}
return types.NewErr("type conversion error from '%s' to '%s'", ListType, t)
}
// Equal returns true if two lists are of the same size, and the values at each index are also
// equal.
func (lv *ListValue) Equal(other ref.Val) ref.Val {
oArr, isArr := other.(traits.Lister)
if !isArr {
return types.MaybeNoSuchOverloadErr(other)
}
sz := types.Int(len(lv.Entries))
if sz != oArr.Size() {
return types.False
}
for i := types.Int(0); i < sz; i++ {
cmp := lv.Get(i).Equal(oArr.Get(i))
if cmp != types.True {
return cmp
}
}
return types.True
}
// Get returns the value at the given index.
//
// If the index is negative or greater than the size of the list, an error is returned.
func (lv *ListValue) Get(idx ref.Val) ref.Val {
iv, isInt := idx.(types.Int)
if !isInt {
return types.ValOrErr(idx, "unsupported index: %v", idx)
}
i := int(iv)
if i < 0 || i >= len(lv.Entries) {
return types.NewErr("index out of bounds: %v", idx)
}
return lv.Entries[i].ExprValue()
}
// Iterator produces a traits.Iterator suitable for use in CEL comprehension macros.
func (lv *ListValue) Iterator() traits.Iterator {
return &baseListIterator{
getter: lv.Get,
sz: len(lv.Entries),
}
}
// Size returns the number of elements in the list.
func (lv *ListValue) Size() ref.Val {
return types.Int(len(lv.Entries))
}
// Type returns the CEL ref.Type for the list.
func (lv *ListValue) Type() ref.Type {
return types.ListType
}
// Value returns the Go-native value.
func (lv *ListValue) Value() interface{} {
return lv
}
// finalizeValueSet inspects the ListValue entries in order to make internal optimizations once all list
// entries are known.
func (lv *ListValue) finalizeValueSet() {
valueSet := make(map[ref.Val]struct{})
for _, e := range lv.Entries {
switch e.value.(type) {
case bool, float64, int64, string, uint64, types.Null, PlainTextValue:
if valueSet != nil {
valueSet[e.ExprValue()] = struct{}{}
}
default:
lv.valueSet = nil
return
}
}
lv.valueSet = valueSet
}
type baseVal struct{}
func (*baseVal) ConvertToNative(typeDesc reflect.Type) (interface{}, error) {
return nil, fmt.Errorf("unsupported native conversion to: %v", typeDesc)
}
func (*baseVal) ConvertToType(t ref.Type) ref.Val {
return types.NewErr("unsupported type conversion to: %v", t)
}
func (*baseVal) Equal(other ref.Val) ref.Val {
return types.NewErr("unsupported equality test between instances")
}
func (v *baseVal) Value() interface{} {
return nil
}
type baseListIterator struct {
*baseVal
getter func(idx ref.Val) ref.Val
sz int
idx int
}
func (it *baseListIterator) HasNext() ref.Val {
if it.idx < it.sz {
return types.True
}
return types.False
}
func (it *baseListIterator) Next() ref.Val {
v := it.getter(types.Int(it.idx))
it.idx++
return v
}
func (it *baseListIterator) Type() ref.Type {
return types.IteratorType
}
func celBool(pred bool) ref.Val {
if pred {
return types.True
}
return types.False
}
var unknownType = &DeclType{name: "unknown"}

View File

@ -0,0 +1,363 @@
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package model
import (
"fmt"
"reflect"
"testing"
"time"
"github.com/google/cel-go/common/types"
"github.com/google/cel-go/common/types/ref"
"github.com/google/cel-go/common/types/traits"
)
func TestConvertToType(t *testing.T) {
objType := NewObjectType("TestObject", map[string]*DeclField{})
tests := []struct {
val interface{}
typ ref.Type
}{
{true, types.BoolType},
{float64(1.2), types.DoubleType},
{int64(-42), types.IntType},
{uint64(63), types.UintType},
{PlainTextValue("plain text"), types.StringType},
{&MultilineStringValue{Value: "multiline", Raw: "multi\nline"}, types.StringType},
{time.Duration(300), types.DurationType},
{time.Now().UTC(), types.TimestampType},
{types.NullValue, types.NullType},
{NewListValue(), types.ListType},
{NewMapValue(), types.MapType},
{[]byte("bytes"), types.BytesType},
{NewObjectValue(objType), objType},
}
for i, tc := range tests {
idx := i
tst := tc
t.Run(fmt.Sprintf("[%d]", i), func(t *testing.T) {
dv := testValue(t, int64(idx), tst.val)
ev := dv.ExprValue()
if ev.ConvertToType(types.TypeType).(ref.Type).TypeName() != tst.typ.TypeName() {
t.Errorf("got %v, wanted %v type", ev.ConvertToType(types.TypeType), tst.typ)
}
if ev.ConvertToType(tst.typ).Equal(ev) != types.True {
t.Errorf("got %v, wanted input value %v", ev.ConvertToType(tst.typ), ev)
}
})
}
}
func TestEqual(t *testing.T) {
vals := []interface{}{
true, []byte("bytes"), float64(1.2), int64(-42), uint64(63), PlainTextValue("plain text"),
&MultilineStringValue{Value: "multiline", Raw: "multi\nline"}, time.Duration(300),
time.Now().UTC(), types.NullValue, NewListValue(), NewMapValue(),
NewObjectValue(NewObjectType("TestObject", map[string]*DeclField{})),
}
for i, v := range vals {
dv := testValue(t, int64(i), v)
if dv.Equal(dv.ExprValue()) != types.True {
t.Errorf("got %v, wanted dyn value %v equal to itself", dv.Equal(dv.ExprValue()), dv.ExprValue())
}
}
}
func TestListValueAdd(t *testing.T) {
lv := NewListValue()
lv.Append(testValue(t, 1, "first"))
ov := NewListValue()
ov.Append(testValue(t, 2, "second"))
ov.Append(testValue(t, 3, "third"))
llv := NewListValue()
llv.Append(testValue(t, 4, lv))
lov := NewListValue()
lov.Append(testValue(t, 5, ov))
var v traits.Lister = llv.Add(lov).(traits.Lister)
if v.Size() != types.Int(2) {
t.Errorf("got list size %d, wanted 2", v.Size())
}
complex, err := v.ConvertToNative(reflect.TypeOf([][]string{}))
complexList := complex.([][]string)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(complexList, [][]string{{"first"}, {"second", "third"}}) {
t.Errorf("got %v, wanted [['first'], ['second', 'third']]", complexList)
}
}
func TestListValueContains(t *testing.T) {
lv := NewListValue()
lv.Append(testValue(t, 1, "first"))
lv.Append(testValue(t, 2, "second"))
lv.Append(testValue(t, 3, "third"))
for i := types.Int(0); i < lv.Size().(types.Int); i++ {
e := lv.Get(i)
contained := lv.Contains(e)
if contained != types.True {
t.Errorf("got %v, wanted list contains elem[%v] %v == true", contained, i, e)
}
}
if lv.Contains(types.String("fourth")) != types.False {
t.Errorf("got %v, wanted false 'fourth'", lv.Contains(types.String("fourth")))
}
if !types.IsError(lv.Contains(types.Int(-1))) {
t.Errorf("got %v, wanted error for invalid type", lv.Contains(types.Int(-1)))
}
}
func TestListValueContainsNestedList(t *testing.T) {
lvA := NewListValue()
lvA.Append(testValue(t, 1, int64(1)))
lvA.Append(testValue(t, 2, int64(2)))
lvB := NewListValue()
lvB.Append(testValue(t, 3, int64(3)))
elemA, elemB := testValue(t, 4, lvA), testValue(t, 5, lvB)
lv := NewListValue()
lv.Append(elemA)
lv.Append(elemB)
contained := lv.Contains(elemA.ExprValue())
if contained != types.True {
t.Errorf("got %v, wanted elemA contained in list value", contained)
}
contained = lv.Contains(elemB.ExprValue())
if contained != types.True {
t.Errorf("got %v, wanted elemB contained in list value", contained)
}
contained = lv.Contains(types.DefaultTypeAdapter.NativeToValue([]int32{4}))
if contained != types.False {
t.Errorf("got %v, wanted empty list not contained", contained)
}
}
func TestListValueConvertToNative(t *testing.T) {
lv := NewListValue()
none, err := lv.ConvertToNative(reflect.TypeOf([]interface{}{}))
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(none, []interface{}{}) {
t.Errorf("got %v, wanted empty list", none)
}
lv.Append(testValue(t, 1, "first"))
one, err := lv.ConvertToNative(reflect.TypeOf([]string{}))
oneList := one.([]string)
if err != nil {
t.Fatal(err)
}
if len(oneList) != 1 {
t.Errorf("got len(one) == %d, wanted 1", len(oneList))
}
if !reflect.DeepEqual(oneList, []string{"first"}) {
t.Errorf("got %v, wanted string list", oneList)
}
ov := NewListValue()
ov.Append(testValue(t, 2, "second"))
ov.Append(testValue(t, 3, "third"))
if ov.Size() != types.Int(2) {
t.Errorf("got list size %d, wanted 2", ov.Size())
}
llv := NewListValue()
llv.Append(testValue(t, 4, lv))
llv.Append(testValue(t, 5, ov))
if llv.Size() != types.Int(2) {
t.Errorf("got list size %d, wanted 2", llv.Size())
}
complex, err := llv.ConvertToNative(reflect.TypeOf([][]string{}))
complexList := complex.([][]string)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(complexList, [][]string{{"first"}, {"second", "third"}}) {
t.Errorf("got %v, wanted [['first'], ['second', 'third']]", complexList)
}
}
func TestListValueIterator(t *testing.T) {
lv := NewListValue()
lv.Append(testValue(t, 1, "first"))
lv.Append(testValue(t, 2, "second"))
lv.Append(testValue(t, 3, "third"))
it := lv.Iterator()
if it.Type() != types.IteratorType {
t.Errorf("got type %v for iterator, wanted IteratorType", it.Type())
}
i := types.Int(0)
for it.HasNext() == types.True {
v := it.Next()
if v.Equal(lv.Get(i)) != types.True {
t.Errorf("iterator value %v and value %v at index %d not equal", v, lv.Get(i), i)
}
i++
}
}
func TestMapValueConvertToNative(t *testing.T) {
mv := NewMapValue()
none, err := mv.ConvertToNative(reflect.TypeOf(map[string]interface{}{}))
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(none, map[string]interface{}{}) {
t.Errorf("got %v, wanted empty map", none)
}
none, err = mv.ConvertToNative(reflect.TypeOf(map[interface{}]interface{}{}))
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(none, map[interface{}]interface{}{}) {
t.Errorf("got %v, wanted empty map", none)
}
mv.AddField(NewField(1, "Test"))
tst, _ := mv.GetField("Test")
tst.Ref = testValue(t, 2, uint64(12))
mv.AddField(NewField(3, "Check"))
chk, _ := mv.GetField("Check")
chk.Ref = testValue(t, 4, uint64(34))
if mv.Size() != types.Int(2) {
t.Errorf("got size %d, wanted 2", mv.Size())
}
if mv.Contains(types.String("Test")) != types.True {
t.Error("key 'Test' not found")
}
if mv.Contains(types.String("Check")) != types.True {
t.Error("key 'Check' not found")
}
if mv.Contains(types.String("Checked")) != types.False {
t.Error("key 'Checked' found, wanted not found")
}
it := mv.Iterator()
for it.HasNext() == types.True {
k := it.Next()
v := mv.Get(k)
if k == types.String("Test") && v != types.Uint(12) {
t.Errorf("key 'Test' not equal to 12u")
}
if k == types.String("Check") && v != types.Uint(34) {
t.Errorf("key 'Check' not equal to 34u")
}
}
mpStrUint, err := mv.ConvertToNative(reflect.TypeOf(map[string]uint64{}))
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(mpStrUint, map[string]uint64{
"Test": uint64(12),
"Check": uint64(34),
}) {
t.Errorf("got %v, wanted {'Test': 12u, 'Check': 34u}", mpStrUint)
}
tstStr, err := mv.ConvertToNative(reflect.TypeOf(&tstStruct{}))
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(tstStr, &tstStruct{
Test: uint64(12),
Check: uint64(34),
}) {
t.Errorf("got %v, wanted tstStruct{Test: 12u, Check: 34u}", tstStr)
}
}
func TestMapValueEqual(t *testing.T) {
mv := NewMapValue()
name := NewField(1, "name")
name.Ref = testValue(t, 2, "alert")
priority := NewField(3, "priority")
priority.Ref = testValue(t, 4, int64(4))
mv.AddField(name)
mv.AddField(priority)
if mv.Equal(mv) != types.True {
t.Fatalf("map.Equal(map) failed: %v", mv.Equal(mv))
}
}
func TestMapValueNotEqual(t *testing.T) {
mv := NewMapValue()
name := NewField(1, "name")
name.Ref = testValue(t, 2, "alert")
priority := NewField(3, "priority")
priority.Ref = testValue(t, 4, int64(4))
mv.AddField(name)
mv.AddField(priority)
mv2 := NewMapValue()
mv2.AddField(name)
if mv.Equal(mv2) != types.False {
t.Fatalf("mv.Equal(mv2) failed: %v", mv.Equal(mv2))
}
priority2 := NewField(5, "priority")
priority2.Ref = testValue(t, 6, int64(3))
mv2.AddField(priority2)
if mv.Equal(mv2) != types.False {
t.Fatalf("mv.Equal(mv2) failed: %v", mv.Equal(mv2))
}
}
func TestMapValueIsSet(t *testing.T) {
mv := NewMapValue()
if mv.IsSet(types.String("name")) != types.False {
t.Error("map.IsSet('name') returned true for unset key")
}
mv.AddField(NewField(1, "name"))
if mv.IsSet(types.String("name")) != types.True {
t.Error("map.IsSet('name') returned false for a set key")
}
}
func TestObjectValueEqual(t *testing.T) {
objType := NewObjectType("Notice", map[string]*DeclField{
"name": &DeclField{Name: "name", Type: StringType},
"priority": &DeclField{Name: "priority", Type: IntType},
"message": &DeclField{Name: "message", Type: PlainTextType, defaultValue: "<eom>"},
})
name := NewField(1, "name")
name.Ref = testValue(t, 2, "alert")
priority := NewField(3, "priority")
priority.Ref = testValue(t, 4, int64(4))
message := NewField(5, "message")
message.Ref = testValue(t, 6, "call immediately")
mv1 := NewMapValue()
mv1.AddField(name)
mv1.AddField(priority)
obj1 := mv1.ConvertToObject(objType)
if obj1.Equal(obj1) != types.True {
t.Errorf("obj1.Equal(obj1) failed, got: %v", obj1.Equal(obj1))
}
mv2 := NewMapValue()
mv2.AddField(name)
mv2.AddField(priority)
mv2.AddField(message)
obj2 := mv2.ConvertToObject(objType)
if obj1.Equal(obj2) == types.True {
t.Error("obj1.Equal(obj2) returned true, wanted false")
}
if obj2.Equal(obj1) == types.True {
t.Error("obj2.Equal(obj1) returned true, wanted false")
}
}
type tstStruct struct {
Test uint64
Check uint64
}