Repository navigation
Expand file tree
/
Copy pathtypes.go
More file actions
339 lines (297 loc) · 11.9 KB
/
Copy pathtypes.go
File metadata and controls
339 lines (297 loc) · 11.9 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
package mapper
import (
"fmt"
"time"
"github.com/openfga/mapper/language"
)
// Tuple is an OpenFGA relationship tuple produced by evaluation. It is an alias
// for language.Tuple so SDK callers can name it without a second import; the two
// are the same type.
type Tuple = language.Tuple
// TupleFilter is a rendered tuple filter produced by evaluation. It is an alias
// for language.TupleFilter so SDK callers can name it without a second import; the
// two are the same type.
type TupleFilter = language.TupleFilter
// Position is a source location within a mapping file. It is an alias for
// language.Position so SDK callers handling the mapper error path (EvalError,
// Diagnostic) can name it without importing the language package; the two are the
// same type.
type Position = language.Position
// ValidationError is a structural validation error carrying a source Position. It
// is an alias for language.ValidationError so SDK callers can branch on the mapper
// error surface (alongside EvalError and ConflictError) without a second import;
// the two are the same type.
type ValidationError = language.ValidationError
// TupleFilterOperation groups a rule's rendered filters with its desired-state tuples.
// One per rule that has tuple_filters. The consumer uses these to drive read-diff-write
// against FGA.
type TupleFilterOperation struct {
Filters []TupleFilter `json:"filters"`
Tuples []Tuple `json:"tuples"`
}
// EvalError represents an Expr expression evaluation error.
type EvalError struct {
Expression string
RuleName string // populated during evaluation before wrapping; empty if unknown
Field string // tuple field name ("user", "relation", "object"); set for compile-time interpolation errors
Position language.Position // source location; set for compile-time interpolation errors; zero = unknown
Err error
}
func (e *EvalError) Error() string {
if e.Position.StartLine > 0 {
return fmt.Sprintf("evaluation error at %d:%d in expression %q: %v",
e.Position.StartLine, e.Position.StartColumn, e.Expression, e.Err)
}
return fmt.Sprintf("evaluation error in expression %q: %v", e.Expression, e.Err)
}
func (e *EvalError) Unwrap() error {
return e.Err
}
// Conflict describes an unsatisfiable set of desired states on a single
// (User, Relation, Object) key.
type Conflict struct {
User string
Relation string
Object string
}
// ConflictKind classifies why a set of desired states on one URO cannot be
// satisfied in a single OpenFGA Write batch.
type ConflictKind int
const (
// ConflictWriteDelete is a write and a delete targeting the same URO.
ConflictWriteDelete ConflictKind = iota
// ConflictCompetingWrites is two writes on the same URO whose condition or
// context differ, i.e. two incompatible desired states for one relationship.
ConflictCompetingWrites
)
// ConflictError represents a runtime conflict on a single relationship (URO).
// OpenFGA identifies a relationship by (user, relation, object) only, so any
// URO carrying more than one incompatible desired state produces an invalid
// Write batch and is rejected here instead.
type ConflictError struct {
Conflict
Condition string // write/delete conflicts: the write tuple's condition, if any
Kind ConflictKind
}
func (e *ConflictError) Error() string {
if e.Kind == ConflictCompetingWrites {
return fmt.Sprintf("conflict: tuple (%s, %s, %s) has competing write actions with differing condition or context",
e.User, e.Relation, e.Object)
}
if e.Condition != "" {
return fmt.Sprintf("conflict: tuple (%s, %s, %s) with condition %q has both a write and a delete action",
e.User, e.Relation, e.Object, e.Condition)
}
return fmt.Sprintf("conflict: tuple (%s, %s, %s) has both a write and a delete action",
e.User, e.Relation, e.Object)
}
// Result is the output of Mapping.Evaluate.
type Result struct {
Tuples []Tuple `json:"tuples"`
TupleFilterOperations []TupleFilterOperation `json:"tuple_filter_operations,omitempty"`
Trace *Trace `json:"trace,omitempty"` // nil unless tracing is enabled on the Compiler
}
// PostProcessResult holds metadata from tuple post-processing (dedup + conflict detection).
// Populated on the Trace only when tracing is enabled.
type PostProcessResult struct {
RemovedTuples []Tuple // the duplicate tuples that were removed
Conflicts []Conflict // all write/delete conflicts detected (empty if none)
}
// uroKey returns a key identifying a relationship by (user, relation, object)
// only — OpenFGA's notion of relationship identity. Condition and context are
// payload, not identity.
func uroKey(t language.Tuple) string {
k := language.Tuple{User: t.User, Relation: t.Relation, Object: t.Object}
return k.Key()
}
// collapseTuples deduplicates and conflict-checks tuples in a single pass,
// reusing the tuples slice's backing array (write-pointer pattern). It
// implements the core loop logic shared by postProcess and Collapse.
//
// When collectAll is false (fast path), the function returns immediately on
// the first ConflictError, returning the partially-collapsed prefix and the
// error. It always returns immediately on a ValidationError regardless of
// collectAll. When collectAll is true, all removed duplicates and all
// conflicts are accumulated; a ValidationError still causes an early return.
//
// Nil-interface invariant: firstErr is always a true nil interface or a
// non-nil concrete error value — a (*ConflictError)(nil) is never returned
// inside the interface.
func collapseTuples(tuples []language.Tuple, collectAll bool) (collapsed []language.Tuple, removed []language.Tuple, allConflicts []Conflict, firstErr error) {
type uroState struct {
hasWrite bool
writeTuple language.Tuple
hasDelete bool
}
seenFull := make(map[string]struct{}, len(tuples))
states := make(map[string]*uroState, len(tuples))
var firstConflictErr *ConflictError
recordConflict := func(ce *ConflictError) {
if firstConflictErr == nil {
firstConflictErr = ce
}
allConflicts = append(allConflicts, ce.Conflict)
}
w := 0
for _, t := range tuples {
if t.Action != language.ActionWrite && t.Action != language.ActionDelete {
tuples = tuples[:w]
return tuples, removed, allConflicts, &language.ValidationError{
Field: fmt.Sprintf("(%s, %s, %s)", t.User, t.Relation, t.Object),
Message: fmt.Sprintf("unknown tuple action %q", t.Action),
}
}
fullKey := t.Key()
if _, dup := seenFull[fullKey]; dup {
if collectAll {
removed = append(removed, t)
}
continue
}
uk := uroKey(t)
st := states[uk]
if st == nil {
st = &uroState{}
states[uk] = st
}
conflict := Conflict{User: t.User, Relation: t.Relation, Object: t.Object}
var ce *ConflictError
switch t.Action {
case language.ActionWrite:
switch {
case st.hasDelete:
ce = &ConflictError{Conflict: conflict, Condition: t.Condition, Kind: ConflictWriteDelete}
case st.hasWrite:
// Distinct from the stored write (identical ones caught by seenFull),
// so this is a second, incompatible desired state for the same relationship.
ce = &ConflictError{Conflict: conflict, Kind: ConflictCompetingWrites}
}
case language.ActionDelete:
switch {
case st.hasWrite:
ce = &ConflictError{Conflict: conflict, Condition: st.writeTuple.Condition, Kind: ConflictWriteDelete}
case st.hasDelete:
// A delete targets a relationship by URO regardless of condition, so a
// second delete on the same URO is a duplicate.
if collectAll {
removed = append(removed, t)
}
continue
}
}
if ce != nil {
if !collectAll {
tuples = tuples[:w]
return tuples, nil, nil, ce
}
recordConflict(ce)
}
switch t.Action {
case language.ActionWrite:
if !st.hasWrite {
st.hasWrite = true
st.writeTuple = t
}
case language.ActionDelete:
st.hasDelete = true
}
seenFull[fullKey] = struct{}{}
tuples[w] = t
w++
}
collapsed = tuples[:w]
if firstConflictErr != nil {
firstErr = firstConflictErr
}
return collapsed, removed, allConflicts, firstErr
}
// postProcess deduplicates result tuples in place and detects conflicts in a single pass.
// A relationship is identified by its (user, relation, object) triple, matching how
// OpenFGA stores tuples and validates a Write batch. Two desired states on the same URO
// that cannot both be satisfied in one batch are conflicts:
// - a write and a delete on the same URO (ConflictWriteDelete);
// - two writes on the same URO whose condition or context differ (ConflictCompetingWrites).
//
// Dedup: exact-identity duplicates (including condition and context) collapse to the first,
// as do repeated deletes on the same URO; order is preserved and the backing array reused.
// When tracing is enabled, all metadata is stored on Trace.PostProcess and all conflicts are
// collected. When tracing is disabled, returns immediately on the first conflict (fast path).
// Returns the first conflict as a *ConflictError, or nil.
func (r *Result) postProcess() error {
tracing := r.Trace != nil
collapsed, removed, conflicts, firstErr := collapseTuples(r.Tuples, tracing)
r.Tuples = collapsed
// On a ValidationError (any mode) or a ConflictError in non-tracing mode,
// skip TupleFilterOperations dedup — preserve the original early-exit behaviour.
_, isVal := firstErr.(*language.ValidationError)
if isVal || (firstErr != nil && !tracing) {
return firstErr
}
// Dedup each TupleFilterOperation's desired-state tuples independently.
// Desired state is a set per rule; duplicates from iterators are wasteful.
for i := range r.TupleFilterOperations {
r.TupleFilterOperations[i].Tuples = dedupTuples(r.TupleFilterOperations[i].Tuples)
}
if tracing && (len(removed) > 0 || len(conflicts) > 0) {
r.Trace.PostProcess = &PostProcessResult{
RemovedTuples: removed,
Conflicts: conflicts,
}
}
return firstErr
}
// Compact deduplicates tuples by full identity and detects write/delete and
// competing-write conflicts on the same (user, relation, object) relationship.
// It applies the same post-processing semantics Evaluate runs per-record, but
// over an arbitrary tuple set — for callers reconciling tuples produced across
// multiple Evaluate calls (e.g. a batch of input records). Order is preserved,
// first occurrence wins. Returns the compacted tuples and the first conflict as
// a *ConflictError, or nil.
func Compact(tuples []language.Tuple) ([]language.Tuple, error) {
collapsed, _, _, firstErr := collapseTuples(tuples, false)
return collapsed, firstErr
}
// dedupTuples removes duplicate tuples by their full identity (including condition and context),
// preserving insertion order. First occurrence wins.
func dedupTuples(tuples []language.Tuple) []language.Tuple {
if len(tuples) <= 1 {
return tuples
}
seen := make(map[string]struct{}, len(tuples))
w := 0
for _, t := range tuples {
k := t.Key()
if _, dup := seen[k]; dup {
continue
}
seen[k] = struct{}{}
tuples[w] = t
w++
}
return tuples[:w]
}
// Trace holds execution trace data for debugging rule evaluation.
// Populated only when WithTrace(true) is set on the Compiler.
type Trace struct {
Rules []RuleTrace
Duration time.Duration
PostProcess *PostProcessResult // nil unless dedup or conflicts occurred
}
// RuleStatus represents the outcome of evaluating a single rule.
type RuleStatus string
const (
// RuleMatched indicates the rule's when guard evaluated to true and the rule produced tuples.
RuleMatched RuleStatus = "matched"
// RuleSkipped indicates the rule's when guard evaluated to false and the rule was skipped.
RuleSkipped RuleStatus = "skipped"
// RuleErrored indicates an error occurred while evaluating the rule.
RuleErrored RuleStatus = "error"
)
// RuleTrace records evaluation details for a single rule.
type RuleTrace struct {
Name string
Status RuleStatus
Error error // populated only when Status == RuleErrored
EmittedN int
FilterN int // number of rendered tuple filters (0 if rule has no tuple_filters)
}