github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/graph/computed/computecheck.go (about) 1 package computed 2 3 import ( 4 "context" 5 6 cexpr "github.com/authzed/spicedb/internal/caveats" 7 "github.com/authzed/spicedb/internal/dispatch" 8 datastoremw "github.com/authzed/spicedb/internal/middleware/datastore" 9 "github.com/authzed/spicedb/pkg/datastore" 10 "github.com/authzed/spicedb/pkg/genutil/slicez" 11 core "github.com/authzed/spicedb/pkg/proto/core/v1" 12 v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" 13 "github.com/authzed/spicedb/pkg/spiceerrors" 14 ) 15 16 // DebugOption defines the various debug level options for Checks. 17 type DebugOption int 18 19 const ( 20 // NoDebugging indicates that debug information should be retained 21 // while performing the Check. 22 NoDebugging DebugOption = 0 23 24 // BasicDebuggingEnabled indicates that basic debug information, such 25 // as which steps were taken, should be retained while performing the 26 // Check and returned to the caller. 27 // 28 // NOTE: This has a minor performance impact. 29 BasicDebuggingEnabled DebugOption = 1 30 31 // TraceDebuggingEnabled indicates that the Check is being issued for 32 // tracing the exact calls made for debugging, which means that not only 33 // should debug information be recorded and returned, but that optimizations 34 // such as batching should be disabled. 35 // 36 // WARNING: This has a fairly significant performance impact and should only 37 // be used in tooling! 38 TraceDebuggingEnabled DebugOption = 2 39 ) 40 41 // CheckParameters are the parameters for the ComputeCheck call. *All* are required. 42 type CheckParameters struct { 43 ResourceType *core.RelationReference 44 Subject *core.ObjectAndRelation 45 CaveatContext map[string]any 46 AtRevision datastore.Revision 47 MaximumDepth uint32 48 DebugOption DebugOption 49 } 50 51 // ComputeCheck computes a check result for the given resource and subject, computing any 52 // caveat expressions found. 53 func ComputeCheck( 54 ctx context.Context, 55 d dispatch.Check, 56 params CheckParameters, 57 resourceID string, 58 ) (*v1.ResourceCheckResult, *v1.ResponseMeta, error) { 59 resultsMap, meta, err := computeCheck(ctx, d, params, []string{resourceID}) 60 if err != nil { 61 return nil, meta, err 62 } 63 return resultsMap[resourceID], meta, err 64 } 65 66 // ComputeBulkCheck computes a check result for the given resources and subject, computing any 67 // caveat expressions found. 68 func ComputeBulkCheck( 69 ctx context.Context, 70 d dispatch.Check, 71 params CheckParameters, 72 resourceIDs []string, 73 ) (map[string]*v1.ResourceCheckResult, *v1.ResponseMeta, error) { 74 return computeCheck(ctx, d, params, resourceIDs) 75 } 76 77 func computeCheck(ctx context.Context, 78 d dispatch.Check, 79 params CheckParameters, 80 resourceIDs []string, 81 ) (map[string]*v1.ResourceCheckResult, *v1.ResponseMeta, error) { 82 debugging := v1.DispatchCheckRequest_NO_DEBUG 83 if params.DebugOption == BasicDebuggingEnabled { 84 debugging = v1.DispatchCheckRequest_ENABLE_BASIC_DEBUGGING 85 if len(resourceIDs) > 1 { 86 return nil, nil, spiceerrors.MustBugf("debugging can only be enabled for a single resource ID") 87 } 88 } else if params.DebugOption == TraceDebuggingEnabled { 89 debugging = v1.DispatchCheckRequest_ENABLE_TRACE_DEBUGGING 90 if len(resourceIDs) > 1 { 91 return nil, nil, spiceerrors.MustBugf("debugging can only be enabled for a single resource ID") 92 } 93 } 94 95 setting := v1.DispatchCheckRequest_REQUIRE_ALL_RESULTS 96 if len(resourceIDs) == 1 { 97 setting = v1.DispatchCheckRequest_ALLOW_SINGLE_RESULT 98 } 99 100 // Ensure that the number of resources IDs given to each dispatch call is not in excess of the maximum. 101 results := make(map[string]*v1.ResourceCheckResult, len(resourceIDs)) 102 metadata := &v1.ResponseMeta{} 103 104 bf, err := v1.NewTraversalBloomFilter(uint(params.MaximumDepth)) 105 if err != nil { 106 return nil, nil, spiceerrors.MustBugf("failed to create new traversal bloom filter") 107 } 108 109 // TODO(jschorr): Should we make this run in parallel via the preloadedTaskRunner? 110 _, err = slicez.ForEachChunkUntil(resourceIDs, datastore.FilterMaximumIDCount, func(resourceIDsToCheck []string) (bool, error) { 111 checkResult, err := d.DispatchCheck(ctx, &v1.DispatchCheckRequest{ 112 ResourceRelation: params.ResourceType, 113 ResourceIds: resourceIDsToCheck, 114 ResultsSetting: setting, 115 Subject: params.Subject, 116 Metadata: &v1.ResolverMeta{ 117 AtRevision: params.AtRevision.String(), 118 DepthRemaining: params.MaximumDepth, 119 TraversalBloom: bf, 120 }, 121 Debug: debugging, 122 }) 123 124 if len(resourceIDs) == 1 { 125 metadata = checkResult.Metadata 126 } else { 127 metadata = &v1.ResponseMeta{ 128 DispatchCount: metadata.DispatchCount + checkResult.Metadata.DispatchCount, 129 DepthRequired: max(metadata.DepthRequired, checkResult.Metadata.DepthRequired), 130 CachedDispatchCount: metadata.CachedDispatchCount + checkResult.Metadata.CachedDispatchCount, 131 } 132 } 133 134 if err != nil { 135 return false, err 136 } 137 138 for _, resourceID := range resourceIDsToCheck { 139 computed, err := computeCaveatedCheckResult(ctx, params, resourceID, checkResult) 140 if err != nil { 141 return false, err 142 } 143 results[resourceID] = computed 144 } 145 146 return true, nil 147 }) 148 return results, metadata, err 149 } 150 151 func computeCaveatedCheckResult(ctx context.Context, params CheckParameters, resourceID string, checkResult *v1.DispatchCheckResponse) (*v1.ResourceCheckResult, error) { 152 result, ok := checkResult.ResultsByResourceId[resourceID] 153 if !ok { 154 return &v1.ResourceCheckResult{ 155 Membership: v1.ResourceCheckResult_NOT_MEMBER, 156 }, nil 157 } 158 159 if result.Membership == v1.ResourceCheckResult_MEMBER { 160 return result, nil 161 } 162 163 ds := datastoremw.MustFromContext(ctx) 164 reader := ds.SnapshotReader(params.AtRevision) 165 166 caveatResult, err := cexpr.RunCaveatExpression(ctx, result.Expression, params.CaveatContext, reader, cexpr.RunCaveatExpressionNoDebugging) 167 if err != nil { 168 return nil, err 169 } 170 171 if caveatResult.IsPartial() { 172 missingFields, _ := caveatResult.MissingVarNames() 173 return &v1.ResourceCheckResult{ 174 Membership: v1.ResourceCheckResult_CAVEATED_MEMBER, 175 MissingExprFields: missingFields, 176 }, nil 177 } 178 179 if caveatResult.Value() { 180 return &v1.ResourceCheckResult{ 181 Membership: v1.ResourceCheckResult_MEMBER, 182 }, nil 183 } 184 185 return &v1.ResourceCheckResult{ 186 Membership: v1.ResourceCheckResult_NOT_MEMBER, 187 }, nil 188 }