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
|
package printers
import (
"encoding/json"
"fmt"
"strings"
"github.com/gookit/color"
v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
"github.com/authzed/spicedb/pkg/tuple"
)
// DisplayCheckTrace prints out the check trace found in the given debug message.
func DisplayCheckTrace(checkTrace *v1.CheckDebugTrace, tp *TreePrinter, hasError bool) {
displayCheckTrace(checkTrace, tp, hasError, map[string]struct{}{})
}
func displayCheckTrace(checkTrace *v1.CheckDebugTrace, tp *TreePrinter, hasError bool, encountered map[string]struct{}) {
red := color.FgRed.Render
green := color.FgGreen.Render
cyan := color.FgCyan.Render
white := color.FgWhite.Render
faint := color.FgGray.Render
magenta := color.FgMagenta.Render
yellow := color.FgYellow.Render
orange := color.C256(166).Sprint
purple := color.C256(99).Sprint
lightgreen := color.C256(35).Sprint
caveatColor := color.C256(198).Sprint
hasPermission := green("✓")
resourceColor := white
permissionColor := color.FgWhite.Render
switch checkTrace.PermissionType {
case v1.CheckDebugTrace_PERMISSION_TYPE_PERMISSION:
permissionColor = lightgreen
case v1.CheckDebugTrace_PERMISSION_TYPE_RELATION:
permissionColor = orange
}
switch checkTrace.Result {
case v1.CheckDebugTrace_PERMISSIONSHIP_CONDITIONAL_PERMISSION:
switch checkTrace.CaveatEvaluationInfo.Result {
case v1.CaveatEvalInfo_RESULT_FALSE:
hasPermission = red("⨉")
resourceColor = faint
permissionColor = faint
case v1.CaveatEvalInfo_RESULT_MISSING_SOME_CONTEXT:
hasPermission = magenta("?")
resourceColor = faint
permissionColor = faint
}
case v1.CheckDebugTrace_PERMISSIONSHIP_NO_PERMISSION:
hasPermission = red("⨉")
resourceColor = faint
permissionColor = faint
case v1.CheckDebugTrace_PERMISSIONSHIP_UNSPECIFIED:
hasPermission = yellow("∵")
}
additional := ""
if checkTrace.GetWasCachedResult() {
sourceKind := ""
source := checkTrace.Source
if source != "" {
parts := strings.Split(source, ":")
if len(parts) > 0 {
sourceKind = parts[0]
}
}
switch sourceKind {
case "":
additional = cyan(" (cached)")
case "spicedb":
additional = cyan(" (cached by spicedb)")
case "materialize":
additional = purple(" (cached by materialize)")
default:
additional = cyan(fmt.Sprintf(" (cached by %s)", sourceKind))
}
} else if hasError && isPartOfCycle(checkTrace, map[string]struct{}{}) {
hasPermission = orange("!")
resourceColor = white
}
isEndOfCycle := false
if hasError {
key := cycleKey(checkTrace)
_, isEndOfCycle = encountered[key]
if isEndOfCycle {
additional = color.C256(166).Sprint(" (cycle)")
}
encountered[key] = struct{}{}
}
timing := ""
if checkTrace.Duration != nil {
timing = fmt.Sprintf(" (%s)", checkTrace.Duration.AsDuration().String())
}
tp = tp.Child(
fmt.Sprintf(
"%s %s:%s %s%s%s",
hasPermission,
resourceColor(checkTrace.Resource.ObjectType),
resourceColor(checkTrace.Resource.ObjectId),
permissionColor(checkTrace.Permission),
additional,
timing,
),
)
if isEndOfCycle {
return
}
if checkTrace.GetCaveatEvaluationInfo() != nil {
indicator := ""
exprColor := color.FgWhite.Render
switch checkTrace.CaveatEvaluationInfo.Result {
case v1.CaveatEvalInfo_RESULT_FALSE:
indicator = red("⨉")
exprColor = faint
case v1.CaveatEvalInfo_RESULT_TRUE:
indicator = green("✓")
case v1.CaveatEvalInfo_RESULT_MISSING_SOME_CONTEXT:
indicator = magenta("?")
}
white := color.HEXStyle("fff")
white.SetOpts(color.Opts{color.OpItalic})
contextMap := checkTrace.CaveatEvaluationInfo.Context.AsMap()
caveatName := checkTrace.CaveatEvaluationInfo.CaveatName
c := tp.Child(fmt.Sprintf("%s %s %s", indicator, exprColor(checkTrace.CaveatEvaluationInfo.Expression), caveatColor(caveatName)))
if len(contextMap) > 0 {
contextJSON, _ := json.MarshalIndent(contextMap, "", " ")
c.Child(string(contextJSON))
} else {
if checkTrace.CaveatEvaluationInfo.Result != v1.CaveatEvalInfo_RESULT_MISSING_SOME_CONTEXT {
c.Child(faint("(no matching context found)"))
}
}
if checkTrace.CaveatEvaluationInfo.Result == v1.CaveatEvalInfo_RESULT_MISSING_SOME_CONTEXT {
c.Child(fmt.Sprintf("missing context: %s", strings.Join(checkTrace.CaveatEvaluationInfo.PartialCaveatInfo.MissingRequiredContext, ", ")))
}
}
if checkTrace.GetSubProblems() != nil {
for _, subProblem := range checkTrace.GetSubProblems().Traces {
displayCheckTrace(subProblem, tp, hasError, encountered)
}
} else if checkTrace.Result == v1.CheckDebugTrace_PERMISSIONSHIP_HAS_PERMISSION {
tp.Child(purple(fmt.Sprintf("%s:%s %s", checkTrace.Subject.Object.ObjectType, checkTrace.Subject.Object.ObjectId, checkTrace.Subject.OptionalRelation)))
}
}
func cycleKey(checkTrace *v1.CheckDebugTrace) string {
return fmt.Sprintf("%s#%s", tuple.V1StringObjectRef(checkTrace.Resource), checkTrace.Permission)
}
func isPartOfCycle(checkTrace *v1.CheckDebugTrace, encountered map[string]struct{}) bool {
if checkTrace.GetSubProblems() == nil {
return false
}
encounteredCopy := make(map[string]struct{}, len(encountered))
for k, v := range encountered {
encounteredCopy[k] = v
}
key := cycleKey(checkTrace)
if _, ok := encounteredCopy[key]; ok {
return true
}
encounteredCopy[key] = struct{}{}
for _, subProblem := range checkTrace.GetSubProblems().Traces {
if isPartOfCycle(subProblem, encounteredCopy) {
return true
}
}
return false
}
|