Skip to content

Commit 32ce9c5

Browse files
committed
Fix debug traces when caveats use the same param name
This is done by renaming the parameters in the shared context map and rewriting the expression in the debug trace
1 parent f4bbdf3 commit 32ce9c5

File tree

13 files changed

+983
-228
lines changed

13 files changed

+983
-228
lines changed

internal/caveats/debug.go

+164
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
package caveats
2+
3+
import (
4+
"fmt"
5+
"maps"
6+
"strconv"
7+
"strings"
8+
9+
"google.golang.org/protobuf/types/known/structpb"
10+
11+
"github.com/authzed/spicedb/pkg/caveats"
12+
"github.com/authzed/spicedb/pkg/genutil/mapz"
13+
corev1 "github.com/authzed/spicedb/pkg/proto/core/v1"
14+
"github.com/authzed/spicedb/pkg/spiceerrors"
15+
)
16+
17+
// BuildDebugInformation returns a human-readable string representation of the given
18+
// ExpressionResult and a Struct representation of the context values used in the expression.
19+
func BuildDebugInformation(exprResult ExpressionResult) (string, *structpb.Struct, error) {
20+
// If a concrete result, return its information directly.
21+
if concrete, ok := exprResult.(*caveats.CaveatResult); ok {
22+
exprString, err := concrete.ParentCaveat().ExprString()
23+
if err != nil {
24+
return "", nil, err
25+
}
26+
27+
contextStruct, err := concrete.ContextStruct()
28+
if err != nil {
29+
return "", nil, err
30+
}
31+
32+
return exprString, contextStruct, nil
33+
}
34+
35+
// Collect parameters which are shared across expressions.
36+
syntheticResult, ok := exprResult.(syntheticResult)
37+
if !ok {
38+
return "", nil, spiceerrors.MustBugf("unknown ExpressionResult type: %T", exprResult)
39+
}
40+
41+
resultsByParam := mapz.NewMultiMap[string, *caveats.CaveatResult]()
42+
if err := collectParameterUsage(syntheticResult, resultsByParam); err != nil {
43+
return "", nil, err
44+
}
45+
46+
// Build the synthetic debug information.
47+
exprString, contextMap, err := buildDebugInformation(syntheticResult, resultsByParam)
48+
if err != nil {
49+
return "", nil, err
50+
}
51+
52+
// Convert the context map to a struct.
53+
contextStruct, err := structpb.NewStruct(contextMap)
54+
if err != nil {
55+
return "", nil, err
56+
}
57+
58+
return exprString, contextStruct, nil
59+
}
60+
61+
func buildDebugInformation(sr syntheticResult, resultsByParam *mapz.MultiMap[string, *caveats.CaveatResult]) (string, map[string]any, error) {
62+
childExprStrings := make([]string, 0, len(sr.exprResultsForDebug))
63+
combinedContext := map[string]any{}
64+
65+
for _, child := range sr.exprResultsForDebug {
66+
if _, ok := child.(*caveats.CaveatResult); ok {
67+
childExprString, contextMap, err := buildDebugInformationForConcrete(child.(*caveats.CaveatResult), resultsByParam)
68+
if err != nil {
69+
return "", nil, err
70+
}
71+
72+
childExprStrings = append(childExprStrings, "("+childExprString+")")
73+
maps.Copy(combinedContext, contextMap)
74+
continue
75+
}
76+
77+
childExprString, contextMap, err := buildDebugInformation(child.(syntheticResult), resultsByParam)
78+
if err != nil {
79+
return "", nil, err
80+
}
81+
82+
childExprStrings = append(childExprStrings, "("+childExprString+")")
83+
maps.Copy(combinedContext, contextMap)
84+
}
85+
86+
var combinedExprString string
87+
switch sr.op {
88+
case corev1.CaveatOperation_AND:
89+
combinedExprString = strings.Join(childExprStrings, " && ")
90+
91+
case corev1.CaveatOperation_OR:
92+
combinedExprString = strings.Join(childExprStrings, " || ")
93+
94+
case corev1.CaveatOperation_NOT:
95+
if len(childExprStrings) != 1 {
96+
return "", nil, spiceerrors.MustBugf("NOT operator must have exactly one child")
97+
}
98+
99+
combinedExprString = "!" + childExprStrings[0]
100+
101+
default:
102+
return "", nil, fmt.Errorf("unknown operator: %v", sr.op)
103+
}
104+
105+
return combinedExprString, combinedContext, nil
106+
}
107+
108+
func buildDebugInformationForConcrete(cr *caveats.CaveatResult, resultsByParam *mapz.MultiMap[string, *caveats.CaveatResult]) (string, map[string]any, error) {
109+
// For each paramter used in the context of the caveat, check if it is shared across multiple
110+
// caveats. If so, rewrite the parameter to a unique name.
111+
existingContextMap := cr.ContextValues()
112+
contextMap := make(map[string]any, len(existingContextMap))
113+
114+
caveat := *cr.ParentCaveat()
115+
116+
for paramName, paramValue := range existingContextMap {
117+
index := mapz.IndexOfValueInMultimap(resultsByParam, paramName, cr)
118+
if resultsByParam.CountOf(paramName) > 1 {
119+
newName := paramName + "__" + strconv.Itoa(index)
120+
if resultsByParam.Has(newName) {
121+
return "", nil, fmt.Errorf("failed to generate unique name for parameter: %s", newName)
122+
}
123+
124+
rewritten, err := caveat.RewriteVariable(paramName, newName)
125+
if err != nil {
126+
return "", nil, err
127+
}
128+
129+
caveat = rewritten
130+
contextMap[newName] = paramValue
131+
continue
132+
}
133+
134+
contextMap[paramName] = paramValue
135+
}
136+
137+
exprString, err := caveat.ExprString()
138+
if err != nil {
139+
return "", nil, err
140+
}
141+
142+
return exprString, contextMap, nil
143+
}
144+
145+
func collectParameterUsage(sr syntheticResult, resultsByParam *mapz.MultiMap[string, *caveats.CaveatResult]) error {
146+
for _, exprResult := range sr.exprResultsForDebug {
147+
if concrete, ok := exprResult.(*caveats.CaveatResult); ok {
148+
for paramName := range concrete.ContextValues() {
149+
resultsByParam.Add(paramName, concrete)
150+
}
151+
} else {
152+
cast, ok := exprResult.(syntheticResult)
153+
if !ok {
154+
return spiceerrors.MustBugf("unknown ExpressionResult type: %T", exprResult)
155+
}
156+
157+
if err := collectParameterUsage(cast, resultsByParam); err != nil {
158+
return err
159+
}
160+
}
161+
}
162+
163+
return nil
164+
}

0 commit comments

Comments
 (0)