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
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
|
package caveats
import (
"fmt"
"strings"
"github.com/authzed/cel-go/cel"
"github.com/authzed/cel-go/common"
"google.golang.org/protobuf/proto"
"github.com/authzed/spicedb/pkg/caveats/replacer"
"github.com/authzed/spicedb/pkg/caveats/types"
"github.com/authzed/spicedb/pkg/genutil/mapz"
impl "github.com/authzed/spicedb/pkg/proto/impl/v1"
)
const anonymousCaveat = ""
const maxCaveatExpressionSize = 100_000 // characters
// CompiledCaveat is a compiled form of a caveat.
type CompiledCaveat struct {
// env is the environment under which the CEL program was compiled.
celEnv *cel.Env
// ast is the AST form of the CEL program.
ast *cel.Ast
// name of the caveat
name string
}
// Name represents a user-friendly reference to a caveat
func (cc CompiledCaveat) Name() string {
return cc.name
}
// ExprString returns the string-form of the caveat.
func (cc CompiledCaveat) ExprString() (string, error) {
return cel.AstToString(cc.ast)
}
// RewriteVariable replaces the use of a variable with another variable in the compiled caveat.
func (cc CompiledCaveat) RewriteVariable(oldName, newName string) (CompiledCaveat, error) {
// Find the existing parameter name and get its type.
oldExpr, issues := cc.celEnv.Compile(oldName)
if issues.Err() != nil {
return CompiledCaveat{}, fmt.Errorf("failed to parse old variable name: %w", issues.Err())
}
oldType := oldExpr.OutputType()
// Ensure the new variable name is not used.
_, niss := cc.celEnv.Compile(newName)
if niss.Err() == nil {
return CompiledCaveat{}, fmt.Errorf("variable name '%s' is already used", newName)
}
// Extend the environment with the new variable name.
extended, err := cc.celEnv.Extend(cel.Variable(newName, oldType))
if err != nil {
return CompiledCaveat{}, fmt.Errorf("failed to extend environment: %w", err)
}
// Replace the variable in the AST.
updatedAst, err := replacer.ReplaceVariable(extended, cc.ast, oldName, newName)
if err != nil {
return CompiledCaveat{}, fmt.Errorf("failed to rewrite variable: %w", err)
}
return CompiledCaveat{extended, updatedAst, cc.name}, nil
}
// Serialize serializes the compiled caveat into a byte string for storage.
func (cc CompiledCaveat) Serialize() ([]byte, error) {
cexpr, err := cel.AstToCheckedExpr(cc.ast)
if err != nil {
return nil, err
}
caveat := &impl.DecodedCaveat{
KindOneof: &impl.DecodedCaveat_Cel{
Cel: cexpr,
},
Name: cc.name,
}
// TODO(jschorr): change back to MarshalVT once stable is supported.
// See: https://github.com/planetscale/vtprotobuf/pull/133
return proto.MarshalOptions{Deterministic: true}.Marshal(caveat)
}
// ReferencedParameters returns the names of the parameters referenced in the expression.
func (cc CompiledCaveat) ReferencedParameters(parameters []string) (*mapz.Set[string], error) {
referencedParams := mapz.NewSet[string]()
definedParameters := mapz.NewSet[string]()
definedParameters.Extend(parameters)
checked, err := cel.AstToCheckedExpr(cc.ast)
if err != nil {
return nil, err
}
referencedParameters(definedParameters, checked.Expr, referencedParams)
return referencedParams, nil
}
// CompileCaveatWithName compiles a caveat string into a compiled caveat with a given name,
// or returns the compilation errors.
func CompileCaveatWithName(env *Environment, exprString, name string) (*CompiledCaveat, error) {
c, err := CompileCaveatWithSource(env, name, common.NewStringSource(exprString, name), nil)
if err != nil {
return nil, err
}
c.name = name
return c, nil
}
// CompileCaveatWithSource compiles a caveat source into a compiled caveat, or returns the compilation errors.
func CompileCaveatWithSource(env *Environment, name string, source common.Source, startPosition SourcePosition) (*CompiledCaveat, error) {
celEnv, err := env.asCelEnvironment()
if err != nil {
return nil, err
}
if len(strings.TrimSpace(source.Content())) > maxCaveatExpressionSize {
return nil, fmt.Errorf("caveat expression provided exceeds maximum allowed size of %d characters", maxCaveatExpressionSize)
}
ast, issues := celEnv.CompileSource(source)
if issues != nil && issues.Err() != nil {
if startPosition == nil {
return nil, MultipleCompilationError{issues.Err(), issues}
}
// Construct errors with the source location adjusted based on the starting source position
// in the parent schema (if any). This ensures that the errors coming out of CEL show the correct
// *overall* location information..
line, col, err := startPosition.LineAndColumn()
if err != nil {
return nil, err
}
adjustedErrors := common.NewErrors(source)
for _, existingErr := range issues.Errors() {
location := existingErr.Location
// NOTE: Our locations are zero-indexed while CEL is 1-indexed, so we need to adjust the line/column values accordingly.
if location.Line() == 1 {
location = common.NewLocation(line+location.Line(), col+location.Column())
} else {
location = common.NewLocation(line+location.Line(), location.Column())
}
adjustedError := &common.Error{
Message: existingErr.Message,
ExprID: existingErr.ExprID,
Location: location,
}
adjustedErrors = adjustedErrors.Append([]*common.Error{
adjustedError,
})
}
adjustedIssues := cel.NewIssues(adjustedErrors)
return nil, MultipleCompilationError{adjustedIssues.Err(), adjustedIssues}
}
if ast.OutputType() != cel.BoolType {
return nil, MultipleCompilationError{fmt.Errorf("caveat expression must result in a boolean value: found `%s`", ast.OutputType().String()), nil}
}
compiled := &CompiledCaveat{celEnv, ast, anonymousCaveat}
compiled.name = name
return compiled, nil
}
// compileCaveat compiles a caveat string into a compiled caveat, or returns the compilation errors.
func compileCaveat(env *Environment, exprString string) (*CompiledCaveat, error) {
s := common.NewStringSource(exprString, "caveat")
return CompileCaveatWithSource(env, "caveat", s, nil)
}
// DeserializeCaveat deserializes a byte-serialized caveat back into a CompiledCaveat.
func DeserializeCaveat(serialized []byte, parameterTypes map[string]types.VariableType) (*CompiledCaveat, error) {
env, err := EnvForVariables(parameterTypes)
if err != nil {
return nil, err
}
return DeserializeCaveatWithEnviroment(env, serialized)
}
// DeserializeCaveatWithTypeSet deserializes a byte-serialized caveat back into a CompiledCaveat.
func DeserializeCaveatWithTypeSet(ts *types.TypeSet, serialized []byte, parameterTypes map[string]types.VariableType) (*CompiledCaveat, error) {
env, err := EnvForVariablesWithTypeSet(ts, parameterTypes)
if err != nil {
return nil, err
}
return DeserializeCaveatWithEnviroment(env, serialized)
}
// DeserializeCaveatWithEnviroment deserializes a byte-serialized caveat back into a CompiledCaveat,
// using the provided environment. It is the responsibility of the caller to ensure that the environment
// has the parameters defined as variables.
func DeserializeCaveatWithEnviroment(env *Environment, serialized []byte) (*CompiledCaveat, error) {
if len(serialized) == 0 {
return nil, fmt.Errorf("given empty serialized")
}
caveat := &impl.DecodedCaveat{}
err := caveat.UnmarshalVT(serialized)
if err != nil {
return nil, err
}
celEnv, err := env.asCelEnvironment()
if err != nil {
return nil, err
}
ast := cel.CheckedExprToAst(caveat.GetCel())
return &CompiledCaveat{celEnv, ast, caveat.Name}, nil
}
|