summaryrefslogtreecommitdiff
path: root/vendor/github.com/authzed/spicedb/pkg/caveats/eval.go
blob: cef9a4b52e9c9707597e92ae47bc5a141123682d (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
package caveats

import (
	"fmt"

	"google.golang.org/protobuf/types/known/structpb"

	"github.com/authzed/cel-go/cel"
	"github.com/authzed/cel-go/common/types"
	"github.com/authzed/cel-go/common/types/ref"
)

// EvaluationConfig is configuration given to an EvaluateCaveatWithConfig call.
type EvaluationConfig struct {
	// MaxCost is the max cost of the caveat to be executed.
	MaxCost uint64
}

// CaveatResult holds the result of evaluating a caveat.
type CaveatResult struct {
	val             ref.Val
	details         *cel.EvalDetails
	parentCaveat    *CompiledCaveat
	contextValues   map[string]any
	missingVarNames []string
	isPartial       bool
}

// Value returns the computed value for the result.
func (cr CaveatResult) Value() bool {
	if cr.isPartial {
		return false
	}

	return cr.val.Value().(bool)
}

// IsPartial returns true if the caveat was only partially evaluated.
func (cr CaveatResult) IsPartial() bool {
	return cr.isPartial
}

// PartialValue returns the partially evaluated caveat. Only applies if IsPartial is true.
func (cr CaveatResult) PartialValue() (*CompiledCaveat, error) {
	if !cr.isPartial {
		return nil, fmt.Errorf("result is fully evaluated")
	}

	ast, err := cr.parentCaveat.celEnv.ResidualAst(cr.parentCaveat.ast, cr.details)
	if err != nil {
		return nil, err
	}

	return &CompiledCaveat{cr.parentCaveat.celEnv, ast, cr.parentCaveat.name}, nil
}

// ContextValues returns the context values used when computing this result.
func (cr CaveatResult) ContextValues() map[string]any {
	return cr.contextValues
}

// ContextStruct returns the context values used when computing this result as
// a structpb.
func (cr CaveatResult) ContextStruct() (*structpb.Struct, error) {
	return ConvertContextToStruct(cr.contextValues)
}

// ExpressionString returns the human-readable expression string for the evaluated expression.
func (cr CaveatResult) ExpressionString() (string, error) {
	return cr.parentCaveat.ExprString()
}

// ParentCaveat returns the caveat that was evaluated to produce this result.
func (cr CaveatResult) ParentCaveat() *CompiledCaveat {
	return cr.parentCaveat
}

// MissingVarNames returns the name(s) of the missing variables.
func (cr CaveatResult) MissingVarNames() ([]string, error) {
	if !cr.isPartial {
		return nil, fmt.Errorf("result is fully evaluated")
	}

	return cr.missingVarNames, nil
}

// EvaluateCaveat evaluates the compiled caveat with the specified values, and returns
// the result or an error.
func EvaluateCaveat(caveat *CompiledCaveat, contextValues map[string]any) (*CaveatResult, error) {
	return EvaluateCaveatWithConfig(caveat, contextValues, nil)
}

// EvaluateCaveatWithConfig evaluates the compiled caveat with the specified values, and returns
// the result or an error.
func EvaluateCaveatWithConfig(caveat *CompiledCaveat, contextValues map[string]any, config *EvaluationConfig) (*CaveatResult, error) {
	env := caveat.celEnv
	celopts := make([]cel.ProgramOption, 0, 3)

	// Option: enables partial evaluation and state tracking for partial evaluation.
	celopts = append(celopts, cel.EvalOptions(cel.OptTrackState))
	celopts = append(celopts, cel.EvalOptions(cel.OptPartialEval))

	// Option: Cost limit on the evaluation.
	if config != nil && config.MaxCost > 0 {
		celopts = append(celopts, cel.CostLimit(config.MaxCost))
	}

	prg, err := env.Program(caveat.ast, celopts...)
	if err != nil {
		return nil, err
	}

	// Mark any unspecified variables as unknown, to ensure that partial application
	// will result in producing a type of Unknown.
	activation, err := env.PartialVars(contextValues)
	if err != nil {
		return nil, err
	}

	val, details, err := prg.Eval(activation)
	if err != nil {
		return nil, EvaluationError{err}
	}

	// If the value produced has Unknown type, then it means required context was missing.
	if types.IsUnknown(val) {
		unknownVal := val.(*types.Unknown)
		missingVarNames := make([]string, 0, len(unknownVal.IDs()))
		for _, id := range unknownVal.IDs() {
			trails, ok := unknownVal.GetAttributeTrails(id)
			if ok {
				for _, attributeTrail := range trails {
					missingVarNames = append(missingVarNames, attributeTrail.String())
				}
			}
		}

		return &CaveatResult{
			val:             val,
			details:         details,
			parentCaveat:    caveat,
			contextValues:   contextValues,
			missingVarNames: missingVarNames,
			isPartial:       true,
		}, nil
	}

	return &CaveatResult{
		val:             val,
		details:         details,
		parentCaveat:    caveat,
		contextValues:   contextValues,
		missingVarNames: nil,
		isPartial:       false,
	}, nil
}