github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/pkg/caveats/eval.go (about)

     1  package caveats
     2  
     3  import (
     4  	"fmt"
     5  
     6  	"google.golang.org/protobuf/types/known/structpb"
     7  
     8  	"github.com/authzed/cel-go/cel"
     9  	"github.com/authzed/cel-go/common/types"
    10  	"github.com/authzed/cel-go/common/types/ref"
    11  )
    12  
    13  // EvaluationConfig is configuration given to an EvaluateCaveatWithConfig call.
    14  type EvaluationConfig struct {
    15  	// MaxCost is the max cost of the caveat to be executed.
    16  	MaxCost uint64
    17  }
    18  
    19  // CaveatResult holds the result of evaluating a caveat.
    20  type CaveatResult struct {
    21  	val             ref.Val
    22  	details         *cel.EvalDetails
    23  	parentCaveat    *CompiledCaveat
    24  	contextValues   map[string]any
    25  	missingVarNames []string
    26  	isPartial       bool
    27  }
    28  
    29  // Value returns the computed value for the result.
    30  func (cr CaveatResult) Value() bool {
    31  	if cr.isPartial {
    32  		return false
    33  	}
    34  
    35  	return cr.val.Value().(bool)
    36  }
    37  
    38  // IsPartial returns true if the caveat was only partially evaluated.
    39  func (cr CaveatResult) IsPartial() bool {
    40  	return cr.isPartial
    41  }
    42  
    43  // PartialValue returns the partially evaluated caveat. Only applies if IsPartial is true.
    44  func (cr CaveatResult) PartialValue() (*CompiledCaveat, error) {
    45  	if !cr.isPartial {
    46  		return nil, fmt.Errorf("result is fully evaluated")
    47  	}
    48  
    49  	ast, err := cr.parentCaveat.celEnv.ResidualAst(cr.parentCaveat.ast, cr.details)
    50  	if err != nil {
    51  		return nil, err
    52  	}
    53  
    54  	return &CompiledCaveat{cr.parentCaveat.celEnv, ast, cr.parentCaveat.name}, nil
    55  }
    56  
    57  // ContextValues returns the context values used when computing this result.
    58  func (cr CaveatResult) ContextValues() map[string]any {
    59  	return cr.contextValues
    60  }
    61  
    62  // ContextStruct returns the context values used when computing this result as
    63  // a structpb.
    64  func (cr CaveatResult) ContextStruct() (*structpb.Struct, error) {
    65  	return ConvertContextToStruct(cr.contextValues)
    66  }
    67  
    68  // ExpressionString returns the human-readable expression string for the evaluated expression.
    69  func (cr CaveatResult) ExpressionString() (string, error) {
    70  	return cr.parentCaveat.ExprString()
    71  }
    72  
    73  // MissingVarNames returns the name(s) of the missing variables.
    74  func (cr CaveatResult) MissingVarNames() ([]string, error) {
    75  	if !cr.isPartial {
    76  		return nil, fmt.Errorf("result is fully evaluated")
    77  	}
    78  
    79  	return cr.missingVarNames, nil
    80  }
    81  
    82  // EvaluateCaveat evaluates the compiled caveat with the specified values, and returns
    83  // the result or an error.
    84  func EvaluateCaveat(caveat *CompiledCaveat, contextValues map[string]any) (*CaveatResult, error) {
    85  	return EvaluateCaveatWithConfig(caveat, contextValues, nil)
    86  }
    87  
    88  // EvaluateCaveatWithConfig evaluates the compiled caveat with the specified values, and returns
    89  // the result or an error.
    90  func EvaluateCaveatWithConfig(caveat *CompiledCaveat, contextValues map[string]any, config *EvaluationConfig) (*CaveatResult, error) {
    91  	env := caveat.celEnv
    92  	celopts := make([]cel.ProgramOption, 0, 3)
    93  
    94  	// Option: enables partial evaluation and state tracking for partial evaluation.
    95  	celopts = append(celopts, cel.EvalOptions(cel.OptTrackState))
    96  	celopts = append(celopts, cel.EvalOptions(cel.OptPartialEval))
    97  
    98  	// Option: Cost limit on the evaluation.
    99  	if config != nil && config.MaxCost > 0 {
   100  		celopts = append(celopts, cel.CostLimit(config.MaxCost))
   101  	}
   102  
   103  	prg, err := env.Program(caveat.ast, celopts...)
   104  	if err != nil {
   105  		return nil, err
   106  	}
   107  
   108  	// Mark any unspecified variables as unknown, to ensure that partial application
   109  	// will result in producing a type of Unknown.
   110  	activation, err := env.PartialVars(contextValues)
   111  	if err != nil {
   112  		return nil, err
   113  	}
   114  
   115  	val, details, err := prg.Eval(activation)
   116  	if err != nil {
   117  		return nil, EvaluationErr{err}
   118  	}
   119  
   120  	// If the value produced has Unknown type, then it means required context was missing.
   121  	if types.IsUnknown(val) {
   122  		unknownVal := val.(*types.Unknown)
   123  		missingVarNames := make([]string, 0, len(unknownVal.IDs()))
   124  		for _, id := range unknownVal.IDs() {
   125  			trails, ok := unknownVal.GetAttributeTrails(id)
   126  			if ok {
   127  				for _, attributeTrail := range trails {
   128  					missingVarNames = append(missingVarNames, attributeTrail.String())
   129  				}
   130  			}
   131  		}
   132  
   133  		return &CaveatResult{
   134  			val:             val,
   135  			details:         details,
   136  			parentCaveat:    caveat,
   137  			contextValues:   contextValues,
   138  			missingVarNames: missingVarNames,
   139  			isPartial:       true,
   140  		}, nil
   141  	}
   142  
   143  	return &CaveatResult{
   144  		val:             val,
   145  		details:         details,
   146  		parentCaveat:    caveat,
   147  		contextValues:   contextValues,
   148  		missingVarNames: nil,
   149  		isPartial:       false,
   150  	}, nil
   151  }