forked from cedar-policy/cedar-go
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbatch.go
More file actions
402 lines (364 loc) · 11.6 KB
/
Copy pathbatch.go
File metadata and controls
402 lines (364 loc) · 11.6 KB
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
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
// Package batch allows for performant batch evaluations of Cedar policy given a set of principals, actions, resources,
// and/or context as variables. The batch evaluation takes advantage of a form of [partial evaluation] to whittle the
// policy set down to just those policies which refer to the set of unknown variables. This allows for queries over a
// policy set, such as "to which resources can user A connect when the request comes from outside the United States?"
// which can run much faster than a brute force trawl through every possible authorization request.
//
// [partial evaluation]: https://en.wikipedia.org/wiki/Partial_evaluation
package batch
import (
"context"
"errors"
"fmt"
"maps"
"slices"
"github.com/cedar-policy/cedar-go"
"github.com/cedar-policy/cedar-go/internal/consts"
"github.com/cedar-policy/cedar-go/internal/eval"
"github.com/cedar-policy/cedar-go/internal/mapset"
"github.com/cedar-policy/cedar-go/types"
"github.com/cedar-policy/cedar-go/x/exp/ast"
)
// Ignore returns a value that should be ignored during batch evaluation.
func Ignore() types.Value { return eval.Ignore() }
// Variable returns a named variable that is populated during batch evaluation.
func Variable(name types.String) types.Value { return eval.Variable(name) }
// Request defines the PARC and map of Variables to batch evaluate.
type Request struct {
Principal types.Value
Action types.Value
Resource types.Value
Context types.Value
Variables Variables
}
// Variables is a map of String to slice of Value.
type Variables map[types.String][]types.Value
// Values is a map of String to Value. This structure is part of the result and
// reveals the current variable substitutions.
type Values map[types.String]types.Value
// Result is the result of a single batched authorization. It includes a
// specific Request, the Values that were substituted, and the resulting
// Decision and Diagnostics.
type Result struct {
Request types.Request
Values Values
Decision types.Decision
Diagnostic types.Diagnostic
}
// Callback is a function that is called for each single batch authorization with
// a Result.
type Callback func(Result) error
type idEvaler struct {
Policy *ast.Policy
Evaler eval.BoolEvaler
}
type batchEvaler struct {
Variables []variableItem
Values Values
policies map[types.PolicyID]*ast.Policy
compiled bool
evalers map[types.PolicyID]*idEvaler
env eval.Env
callback Callback
}
type variableItem struct {
Key types.String
Values []types.Value
}
const unknownEntityType = "__cedar::unknown"
func unknownEntity(v types.String) types.EntityUID {
return types.NewEntityUID(unknownEntityType, v)
}
var errUnboundVariable = fmt.Errorf("unbound variable")
var errUnusedVariable = fmt.Errorf("unused variable")
var errMissingPart = fmt.Errorf("missing part")
var errInvalidPart = fmt.Errorf("invalid part")
// Authorize will run a batch of authorization evaluations.
//
// All the request parts (PARC) must be specified, but you can
// specify [Variable] or [Ignore]. Variables can be enumerated
// using the Variables.
//
// Using [Ignore] you can ask questions like "When ignoring context could this request be allowed?"
//
// 1. When a Permit Policy Condition refers to an ignored value, the Condition is dropped from the Policy.
// 2. When a Forbid Policy Condition refers to an ignored value, the Policy is dropped.
// 3. When a Scope clause refers to an ignored value, that scope clause is set to match any.
//
// Errors may be returned for a variety of reasons:
//
// - It will error in case of a context.Context error (e.g. cancellation).
// - It will error in case any of PARC are an incorrect type at authorization.
// - It will error in case there are unbound variables.
// - It will error in case there are unused variables.
// - It will error in case of a callback error.
//
// The result passed to the callback must be used / cloned immediately and not modified.
func Authorize(ctx context.Context, policies cedar.PolicyIterator, entities types.EntityGetter, request Request, cb Callback) error {
be := &batchEvaler{}
var found mapset.MapSet[types.String]
findVariables(&found, request.Principal)
findVariables(&found, request.Action)
findVariables(&found, request.Resource)
findVariables(&found, request.Context)
for key := range found.All() {
if _, ok := request.Variables[key]; !ok {
return fmt.Errorf("%w: %v", errUnboundVariable, key)
}
}
for k := range request.Variables {
if !found.Contains(k) {
return fmt.Errorf("%w: %v", errUnusedVariable, k)
}
}
for _, vs := range request.Variables {
if len(vs) == 0 {
return nil
}
}
be.policies = map[types.PolicyID]*ast.Policy{}
for k, p := range policies.All() {
be.policies[k] = (*ast.Policy)(p.AST())
}
be.callback = cb
switch {
case request.Principal == nil:
return fmt.Errorf("%w: principal", errMissingPart)
case request.Action == nil:
return fmt.Errorf("%w: action", errMissingPart)
case request.Resource == nil:
return fmt.Errorf("%w: resource", errMissingPart)
case request.Context == nil:
return fmt.Errorf("%w: context", errMissingPart)
}
if entities == nil {
var zero types.EntityMap
entities = zero
}
be.env = eval.Env{
Entities: entities,
Principal: request.Principal,
Action: request.Action,
Resource: request.Resource,
Context: request.Context,
}
be.Values = Values{}
for k, v := range request.Variables {
be.Variables = append(be.Variables, variableItem{Key: k, Values: v})
}
slices.SortFunc(be.Variables, func(a, b variableItem) int {
return len(a.Values) - len(b.Values)
})
// resolve ignores if no variables exist
if len(be.Variables) == 0 {
doPartial(be)
fixIgnores(be)
}
return errors.Join(doBatch(ctx, be), ctx.Err())
}
func doPartial(be *batchEvaler) {
np := map[types.PolicyID]*ast.Policy{}
for k, p := range be.policies {
part, keep := eval.PartialPolicy(be.env, p)
if !keep {
continue
}
np[k] = part
}
be.compiled = false
be.policies = np
be.evalers = nil
}
// fixIgnores replaces the Ignore PAR (which may not be EntityUID's in the future) with
// EntityUID's so that the conversion to Result is successful. An ignored context is
// replaced with a nil Record for the same reason.
func fixIgnores(be *batchEvaler) {
if eval.IsIgnore(be.env.Principal) {
be.env.Principal = unknownEntity(consts.Principal)
}
if eval.IsIgnore(be.env.Action) {
be.env.Action = unknownEntity(consts.Action)
}
if eval.IsIgnore(be.env.Resource) {
be.env.Resource = unknownEntity(consts.Resource)
}
if eval.IsIgnore(be.env.Context) {
var nilRecord types.Record
be.env.Context = nilRecord
}
}
func doBatch(ctx context.Context, be *batchEvaler) error {
if err := ctx.Err(); err != nil {
return err
}
// if no variables, authorize
if len(be.Variables) == 0 {
return diagnosticAuthzWithCallback(be)
}
// save previous state
prevState := *be
// else, partial eval what we have so far
doPartial(be)
// if no more partial evaluation, fill in ignores with defaults
if len(be.Variables) == 1 {
fixIgnores(be)
}
// then loop the current variable
loopEnv := be.env
u := be.Variables[0]
dummyVal := types.True
_, chPrincipal := cloneSub(be.env.Principal, u.Key, dummyVal)
_, chAction := cloneSub(be.env.Action, u.Key, dummyVal)
_, chResource := cloneSub(be.env.Resource, u.Key, dummyVal)
_, chContext := cloneSub(be.env.Context, u.Key, dummyVal)
be.Variables = be.Variables[1:]
be.Values = maps.Clone(be.Values)
for _, v := range u.Values {
be.env = loopEnv
be.Values[u.Key] = v
if chPrincipal {
be.env.Principal, _ = cloneSub(loopEnv.Principal, u.Key, v)
}
if chAction {
be.env.Action, _ = cloneSub(loopEnv.Action, u.Key, v)
}
if chResource {
be.env.Resource, _ = cloneSub(loopEnv.Resource, u.Key, v)
}
if chContext {
be.env.Context, _ = cloneSub(loopEnv.Context, u.Key, v)
}
if err := doBatch(ctx, be); err != nil {
return err
}
}
// restore previous state
*be = prevState
return nil
}
func diagnosticAuthzWithCallback(be *batchEvaler) error {
var res Result
var err error
if res.Request.Principal, err = eval.ValueToEntity(be.env.Principal); err != nil {
return fmt.Errorf("%w: %w", errInvalidPart, err)
}
if res.Request.Action, err = eval.ValueToEntity(be.env.Action); err != nil {
return fmt.Errorf("%w: %w", errInvalidPart, err)
}
if res.Request.Resource, err = eval.ValueToEntity(be.env.Resource); err != nil {
return fmt.Errorf("%w: %w", errInvalidPart, err)
}
if res.Request.Context, err = eval.ValueToRecord(be.env.Context); err != nil {
return fmt.Errorf("%w: %w", errInvalidPart, err)
}
res.Values = be.Values
batchCompile(be)
res.Decision, res.Diagnostic = isAuthorized(be.evalers, be.env)
return be.callback(res)
}
func isAuthorized(ps map[types.PolicyID]*idEvaler, env eval.Env) (types.Decision, types.Diagnostic) {
var diag types.Diagnostic
var forbids []types.DiagnosticReason
var permits []types.DiagnosticReason
// Don't try to short circuit this.
// - Even though single forbid means forbid
// - All policy should be run to collect errors
// - For permit, all permits must be run to collect annotations
// - For forbid, forbids must be run to collect annotations
for pid, po := range ps {
result, err := po.Evaler.Eval(env)
if err != nil {
diag.Errors = append(diag.Errors, types.DiagnosticError{PolicyID: pid, Position: types.Position(po.Policy.Position), Message: err.Error()})
continue
}
if !result {
continue
}
if po.Policy.Effect == ast.EffectPermit {
permits = append(permits, types.DiagnosticReason{PolicyID: pid, Position: types.Position(po.Policy.Position)})
} else {
forbids = append(forbids, types.DiagnosticReason{PolicyID: pid, Position: types.Position(po.Policy.Position)})
}
}
if len(forbids) > 0 {
diag.Reasons = forbids
return types.Deny, diag
}
if len(permits) > 0 {
diag.Reasons = permits
return types.Allow, diag
}
return types.Deny, diag
}
func batchCompile(be *batchEvaler) {
if be.compiled {
return
}
be.evalers = make(map[types.PolicyID]*idEvaler, len(be.policies))
for k, p := range be.policies {
be.evalers[k] = &idEvaler{Policy: p, Evaler: eval.Compile(p)}
}
be.compiled = true
}
// cloneSub will return a new value if any of its children have changed
// and signal the change via the boolean
func cloneSub(r types.Value, k types.String, v types.Value) (types.Value, bool) {
switch t := r.(type) {
case types.EntityUID:
if key, ok := eval.ToVariable(t); ok && key == k {
return v, true
}
case types.Record:
var newMap types.RecordMap
for kk, vv := range t.All() {
if vv, delta := cloneSub(vv, k, v); delta && newMap == nil {
if newMap == nil {
newMap = t.Map()
}
newMap[kk] = vv
}
}
if newMap == nil {
return t, false
}
return types.NewRecord(newMap), true
case types.Set:
hasDeltas := false
// Look for deltas. Unfortunately, due to the indeterminate nature of the set iteration order,
// we can't pull the same trick as we do for Records above
for vv := range t.All() {
if _, delta := cloneSub(vv, k, v); delta {
hasDeltas = true
break
}
}
// If no deltas, just return the input Value
if !hasDeltas {
return t, false
}
// If there were deltas, build a new Set
newSlice := make([]types.Value, 0, t.Len())
for vv := range t.All() {
vv, _ = cloneSub(vv, k, v)
newSlice = append(newSlice, vv)
}
return types.NewSet(newSlice...), true
}
return r, false
}
func findVariables(found *mapset.MapSet[types.String], r types.Value) {
switch t := r.(type) {
case types.EntityUID:
if key, ok := eval.ToVariable(t); ok {
found.Add(key)
}
case types.Record:
for vv := range t.Values() {
findVariables(found, vv)
}
case types.Set:
for vv := range t.All() {
findVariables(found, vv)
}
}
}