github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/graph/check.go (about) 1 package graph 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "time" 8 9 "github.com/prometheus/client_golang/prometheus" 10 "github.com/samber/lo" 11 "go.opentelemetry.io/otel" 12 "go.opentelemetry.io/otel/trace" 13 "google.golang.org/protobuf/types/known/durationpb" 14 15 "github.com/authzed/spicedb/internal/dispatch" 16 log "github.com/authzed/spicedb/internal/logging" 17 datastoremw "github.com/authzed/spicedb/internal/middleware/datastore" 18 "github.com/authzed/spicedb/internal/namespace" 19 "github.com/authzed/spicedb/internal/taskrunner" 20 "github.com/authzed/spicedb/pkg/datastore" 21 "github.com/authzed/spicedb/pkg/genutil/mapz" 22 "github.com/authzed/spicedb/pkg/genutil/slicez" 23 nspkg "github.com/authzed/spicedb/pkg/namespace" 24 core "github.com/authzed/spicedb/pkg/proto/core/v1" 25 v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" 26 iv1 "github.com/authzed/spicedb/pkg/proto/impl/v1" 27 "github.com/authzed/spicedb/pkg/spiceerrors" 28 "github.com/authzed/spicedb/pkg/tuple" 29 ) 30 31 var tracer = otel.Tracer("spicedb/internal/graph/check") 32 33 var dispatchChunkCountHistogram = prometheus.NewHistogram(prometheus.HistogramOpts{ 34 Name: "spicedb_check_dispatch_chunk_count", 35 Help: "number of chunks when dispatching in check", 36 Buckets: []float64{1, 2, 3, 5, 10, 25, 100, 250}, 37 }) 38 39 var directDispatchQueryHistogram = prometheus.NewHistogram(prometheus.HistogramOpts{ 40 Name: "spicedb_check_direct_dispatch_query_count", 41 Help: "number of queries made per direct dispatch", 42 Buckets: []float64{1, 2}, 43 }) 44 45 func init() { 46 prometheus.MustRegister(directDispatchQueryHistogram) 47 prometheus.MustRegister(dispatchChunkCountHistogram) 48 } 49 50 // NewConcurrentChecker creates an instance of ConcurrentChecker. 51 func NewConcurrentChecker(d dispatch.Check, concurrencyLimit uint16) *ConcurrentChecker { 52 return &ConcurrentChecker{d, concurrencyLimit} 53 } 54 55 // ConcurrentChecker exposes a method to perform Check requests, and delegates subproblems to the 56 // provided dispatch.Check instance. 57 type ConcurrentChecker struct { 58 d dispatch.Check 59 concurrencyLimit uint16 60 } 61 62 // ValidatedCheckRequest represents a request after it has been validated and parsed for internal 63 // consumption. 64 type ValidatedCheckRequest struct { 65 *v1.DispatchCheckRequest 66 Revision datastore.Revision 67 } 68 69 // currentRequestContext holds context information for the current request being 70 // processed. 71 type currentRequestContext struct { 72 // parentReq is the parent request being processed. 73 parentReq ValidatedCheckRequest 74 75 // filteredResourceIDs are those resource IDs to be checked after filtering for 76 // any resource IDs found directly matching the incoming subject. 77 // 78 // For example, a check of resources `user:{tom,sarah,fred}` and subject `user:sarah` will 79 // result in this slice containing `tom` and `fred`, but not `sarah`, as she was found as a 80 // match. 81 // 82 // This check and filter occurs via the filterForFoundMemberResource function in the 83 // checkInternal function before the rest of the checking logic is run. This slice should never 84 // be empty. 85 filteredResourceIDs []string 86 87 // resultsSetting is the results setting to use for this request and all subsequent 88 // requests. 89 resultsSetting v1.DispatchCheckRequest_ResultsSetting 90 91 // maxDispatchCount is the maximum number of resource IDs that can be specified in each dispatch. 92 maxDispatchCount uint16 93 } 94 95 // Check performs a check request with the provided request and context 96 func (cc *ConcurrentChecker) Check(ctx context.Context, req ValidatedCheckRequest, relation *core.Relation) (*v1.DispatchCheckResponse, error) { 97 var startTime *time.Time 98 if req.Debug != v1.DispatchCheckRequest_NO_DEBUG { 99 now := time.Now() 100 startTime = &now 101 } 102 103 resolved := cc.checkInternal(ctx, req, relation) 104 resolved.Resp.Metadata = addCallToResponseMetadata(resolved.Resp.Metadata) 105 if req.Debug == v1.DispatchCheckRequest_NO_DEBUG { 106 return resolved.Resp, resolved.Err 107 } 108 109 // Add debug information if requested. 110 debugInfo := resolved.Resp.Metadata.DebugInfo 111 if debugInfo == nil { 112 debugInfo = &v1.DebugInformation{ 113 Check: &v1.CheckDebugTrace{}, 114 } 115 } 116 117 debugInfo.Check.Request = req.DispatchCheckRequest 118 debugInfo.Check.Duration = durationpb.New(time.Since(*startTime)) 119 120 if nspkg.GetRelationKind(relation) == iv1.RelationMetadata_PERMISSION { 121 debugInfo.Check.ResourceRelationType = v1.CheckDebugTrace_PERMISSION 122 } else if nspkg.GetRelationKind(relation) == iv1.RelationMetadata_RELATION { 123 debugInfo.Check.ResourceRelationType = v1.CheckDebugTrace_RELATION 124 } 125 126 // Build the results for the debug trace. 127 results := make(map[string]*v1.ResourceCheckResult, len(req.DispatchCheckRequest.ResourceIds)) 128 for _, resourceID := range req.DispatchCheckRequest.ResourceIds { 129 if found, ok := resolved.Resp.ResultsByResourceId[resourceID]; ok { 130 results[resourceID] = found 131 } 132 } 133 134 debugInfo.Check.Results = results 135 resolved.Resp.Metadata.DebugInfo = debugInfo 136 return resolved.Resp, resolved.Err 137 } 138 139 func (cc *ConcurrentChecker) checkInternal(ctx context.Context, req ValidatedCheckRequest, relation *core.Relation) CheckResult { 140 // Ensure that we have proper type information for running the check. This is now required as of the deprecation and removal 141 // of the v0 API. 142 if relation.GetTypeInformation() == nil && relation.GetUsersetRewrite() == nil { 143 return checkResultError( 144 fmt.Errorf("found relation `%s` without type information; to fix, please re-write your schema", relation.Name), 145 emptyMetadata, 146 ) 147 } 148 149 // Ensure that we have at least one resource ID for which to execute the check. 150 if len(req.ResourceIds) == 0 { 151 return checkResultError( 152 fmt.Errorf("empty resource IDs given to dispatched check"), 153 emptyMetadata, 154 ) 155 } 156 157 // Ensure that we are not performing a check for a wildcard as the subject. 158 if req.Subject.ObjectId == tuple.PublicWildcard { 159 return checkResultError(NewErrInvalidArgument(errors.New("cannot perform check on wildcard")), emptyMetadata) 160 } 161 162 // Deduplicate any incoming resource IDs. 163 resourceIds := lo.Uniq(req.ResourceIds) 164 165 // Filter the incoming resource IDs for any which match the subject directly. For example, if we receive 166 // a check for resource `user:{tom, fred, sarah}#...` and a subject of `user:sarah#...`, then we know 167 // that `user:sarah#...` is a valid "member" of the resource, as it matches exactly. 168 // 169 // If the filtering results in no further resource IDs to check, or a result is found and a single 170 // result is allowed, we terminate early. 171 membershipSet, filteredResourcesIds := filterForFoundMemberResource(req.ResourceRelation, resourceIds, req.Subject) 172 if membershipSet.HasDeterminedMember() && req.DispatchCheckRequest.ResultsSetting == v1.DispatchCheckRequest_ALLOW_SINGLE_RESULT { 173 return checkResultsForMembership(membershipSet, emptyMetadata) 174 } 175 176 if len(filteredResourcesIds) == 0 { 177 return noMembers() 178 } 179 180 // NOTE: We can always allow a single result if we're only trying to find the results for a 181 // single resource ID. This "reset" allows for short circuiting of downstream dispatched calls. 182 resultsSetting := req.ResultsSetting 183 if len(filteredResourcesIds) == 1 { 184 resultsSetting = v1.DispatchCheckRequest_ALLOW_SINGLE_RESULT 185 } 186 187 crc := currentRequestContext{ 188 parentReq: req, 189 filteredResourceIDs: filteredResourcesIds, 190 resultsSetting: resultsSetting, 191 maxDispatchCount: maxDispatchChunkSize, 192 } 193 194 if req.Debug == v1.DispatchCheckRequest_ENABLE_TRACE_DEBUGGING { 195 crc.maxDispatchCount = 1 196 } 197 198 if relation.UsersetRewrite == nil { 199 return combineResultWithFoundResources(cc.checkDirect(ctx, crc, relation), membershipSet) 200 } 201 202 return combineResultWithFoundResources(cc.checkUsersetRewrite(ctx, crc, relation.UsersetRewrite), membershipSet) 203 } 204 205 type directDispatch struct { 206 resourceType *core.RelationReference 207 resourceIds []string 208 } 209 210 func (cc *ConcurrentChecker) checkDirect(ctx context.Context, crc currentRequestContext, relation *core.Relation) CheckResult { 211 ctx, span := tracer.Start(ctx, "checkDirect") 212 defer span.End() 213 214 // Build a filter for finding the direct relationships for the check. There are three 215 // classes of relationships to be found: 216 // 1) the target subject itself, if allowed on this relation 217 // 2) the wildcard form of the target subject, if a wildcard is allowed on this relation 218 // 3) Otherwise, any non-terminal (non-`...`) subjects, if allowed on this relation, to be 219 // redispatched outward 220 hasNonTerminals := false 221 hasDirectSubject := false 222 hasWildcardSubject := false 223 224 defer func() { 225 if hasNonTerminals { 226 span.SetName("non terminal") 227 } else if hasDirectSubject { 228 span.SetName("terminal") 229 } else { 230 span.SetName("wildcard subject") 231 } 232 }() 233 log.Ctx(ctx).Trace().Object("direct", crc.parentReq).Send() 234 ds := datastoremw.MustFromContext(ctx).SnapshotReader(crc.parentReq.Revision) 235 236 for _, allowedDirectRelation := range relation.GetTypeInformation().GetAllowedDirectRelations() { 237 // If the namespace of the allowed direct relation matches the subject type, there are two 238 // cases to optimize: 239 // 1) Finding the target subject itself, as a direct lookup 240 // 2) Finding a wildcard for the subject type+relation 241 if allowedDirectRelation.GetNamespace() == crc.parentReq.Subject.Namespace { 242 if allowedDirectRelation.GetPublicWildcard() != nil { 243 hasWildcardSubject = true 244 } else if allowedDirectRelation.GetRelation() == crc.parentReq.Subject.Relation { 245 hasDirectSubject = true 246 } 247 } 248 249 // If the relation found is not an ellipsis, then this is a nested relation that 250 // might need to be followed, so indicate that such relationships should be returned 251 // 252 // TODO(jschorr): Use type information to *further* optimize this query around which nested 253 // relations can reach the target subject type. 254 if allowedDirectRelation.GetRelation() != tuple.Ellipsis { 255 hasNonTerminals = true 256 } 257 } 258 259 foundResources := NewMembershipSet() 260 261 // If the direct subject or a wildcard form can be found, issue a query for just that 262 // subject. 263 var queryCount float64 264 defer func() { 265 directDispatchQueryHistogram.Observe(queryCount) 266 }() 267 268 if hasDirectSubject || hasWildcardSubject { 269 subjectSelectors := []datastore.SubjectsSelector{} 270 271 if hasDirectSubject { 272 subjectSelectors = append(subjectSelectors, datastore.SubjectsSelector{ 273 OptionalSubjectType: crc.parentReq.Subject.Namespace, 274 OptionalSubjectIds: []string{crc.parentReq.Subject.ObjectId}, 275 RelationFilter: datastore.SubjectRelationFilter{}.WithRelation(crc.parentReq.Subject.Relation), 276 }) 277 } 278 279 if hasWildcardSubject { 280 subjectSelectors = append(subjectSelectors, datastore.SubjectsSelector{ 281 OptionalSubjectType: crc.parentReq.Subject.Namespace, 282 OptionalSubjectIds: []string{tuple.PublicWildcard}, 283 RelationFilter: datastore.SubjectRelationFilter{}.WithEllipsisRelation(), 284 }) 285 } 286 287 filter := datastore.RelationshipsFilter{ 288 OptionalResourceType: crc.parentReq.ResourceRelation.Namespace, 289 OptionalResourceIds: crc.filteredResourceIDs, 290 OptionalResourceRelation: crc.parentReq.ResourceRelation.Relation, 291 OptionalSubjectsSelectors: subjectSelectors, 292 } 293 294 it, err := ds.QueryRelationships(ctx, filter) 295 if err != nil { 296 return checkResultError(NewCheckFailureErr(err), emptyMetadata) 297 } 298 defer it.Close() 299 queryCount += 1.0 300 301 // Find the matching subject(s). 302 for tpl := it.Next(); tpl != nil; tpl = it.Next() { 303 if it.Err() != nil { 304 return checkResultError(NewCheckFailureErr(it.Err()), emptyMetadata) 305 } 306 307 // If the subject of the relationship matches the target subject, then we've found 308 // a result. 309 if !tuple.OnrEqualOrWildcard(tpl.Subject, crc.parentReq.Subject) { 310 tplString, err := tuple.String(tpl) 311 if err != nil { 312 return checkResultError(err, emptyMetadata) 313 } 314 315 return checkResultError( 316 NewCheckFailureErr( 317 fmt.Errorf("somehow got invalid ONR for direct check matching: %s vs %s", tuple.StringONR(crc.parentReq.Subject), tplString), 318 ), 319 emptyMetadata, 320 ) 321 } 322 323 foundResources.AddDirectMember(tpl.ResourceAndRelation.ObjectId, tpl.Caveat) 324 if crc.resultsSetting == v1.DispatchCheckRequest_ALLOW_SINGLE_RESULT && foundResources.HasDeterminedMember() { 325 return checkResultsForMembership(foundResources, emptyMetadata) 326 } 327 } 328 it.Close() 329 } 330 331 // Filter down the resource IDs for further dispatch based on whether they exist as found 332 // subjects in the existing membership set. 333 furtherFilteredResourceIDs := make([]string, 0, len(crc.filteredResourceIDs)-foundResources.Size()) 334 for _, resourceID := range crc.filteredResourceIDs { 335 if foundResources.HasConcreteResourceID(resourceID) { 336 continue 337 } 338 339 furtherFilteredResourceIDs = append(furtherFilteredResourceIDs, resourceID) 340 } 341 342 // If there are no possible non-terminals, then the check is completed. 343 if !hasNonTerminals || len(furtherFilteredResourceIDs) == 0 { 344 return checkResultsForMembership(foundResources, emptyMetadata) 345 } 346 347 // Otherwise, for any remaining resource IDs, query for redispatch. 348 filter := datastore.RelationshipsFilter{ 349 OptionalResourceType: crc.parentReq.ResourceRelation.Namespace, 350 OptionalResourceIds: furtherFilteredResourceIDs, 351 OptionalResourceRelation: crc.parentReq.ResourceRelation.Relation, 352 OptionalSubjectsSelectors: []datastore.SubjectsSelector{ 353 { 354 RelationFilter: datastore.SubjectRelationFilter{}.WithOnlyNonEllipsisRelations(), 355 }, 356 }, 357 } 358 359 it, err := ds.QueryRelationships(ctx, filter) 360 if err != nil { 361 return checkResultError(NewCheckFailureErr(err), emptyMetadata) 362 } 363 defer it.Close() 364 queryCount += 1.0 365 366 // Find the subjects over which to dispatch. 367 subjectsToDispatch := tuple.NewONRByTypeSet() 368 relationshipsBySubjectONR := mapz.NewMultiMap[string, *core.RelationTuple]() 369 370 for tpl := it.Next(); tpl != nil; tpl = it.Next() { 371 if it.Err() != nil { 372 return checkResultError(NewCheckFailureErr(it.Err()), emptyMetadata) 373 } 374 375 // Add the subject as an object over which to dispatch. 376 if tpl.Subject.Relation == Ellipsis { 377 return checkResultError(NewCheckFailureErr(fmt.Errorf("got a terminal for a non-terminal query")), emptyMetadata) 378 } 379 380 subjectsToDispatch.Add(tpl.Subject) 381 relationshipsBySubjectONR.Add(tuple.StringONR(tpl.Subject), tpl) 382 } 383 it.Close() 384 385 // Convert the subjects into batched requests. 386 // To simplify the logic, +1 is added to account for the situation where 387 // the number of elements is less than the chunk size, and spare us some annoying code. 388 expectedNumberOfChunks := subjectsToDispatch.ValueLen()/int(crc.maxDispatchCount) + 1 389 toDispatch := make([]directDispatch, 0, expectedNumberOfChunks) 390 subjectsToDispatch.ForEachType(func(rr *core.RelationReference, resourceIds []string) { 391 chunkCount := 0.0 392 slicez.ForEachChunk(resourceIds, crc.maxDispatchCount, func(resourceIdChunk []string) { 393 chunkCount++ 394 toDispatch = append(toDispatch, directDispatch{ 395 resourceType: rr, 396 resourceIds: resourceIdChunk, 397 }) 398 }) 399 dispatchChunkCountHistogram.Observe(chunkCount) 400 }) 401 402 // Dispatch and map to the associated resource ID(s). 403 result := union(ctx, crc, toDispatch, func(ctx context.Context, crc currentRequestContext, dd directDispatch) CheckResult { 404 childResult := cc.dispatch(ctx, crc, ValidatedCheckRequest{ 405 &v1.DispatchCheckRequest{ 406 ResourceRelation: dd.resourceType, 407 ResourceIds: dd.resourceIds, 408 Subject: crc.parentReq.Subject, 409 ResultsSetting: crc.resultsSetting, 410 411 Metadata: decrementDepth(crc.parentReq.Metadata), 412 Debug: crc.parentReq.Debug, 413 }, 414 crc.parentReq.Revision, 415 }) 416 if childResult.Err != nil { 417 return childResult 418 } 419 420 return mapFoundResources(childResult, dd.resourceType, relationshipsBySubjectONR) 421 }, cc.concurrencyLimit) 422 423 return combineResultWithFoundResources(result, foundResources) 424 } 425 426 func mapFoundResources(result CheckResult, resourceType *core.RelationReference, relationshipsBySubjectONR *mapz.MultiMap[string, *core.RelationTuple]) CheckResult { 427 // Map any resources found to the parent resource IDs. 428 membershipSet := NewMembershipSet() 429 for foundResourceID, result := range result.Resp.ResultsByResourceId { 430 subjectKey := tuple.StringONR(&core.ObjectAndRelation{ 431 Namespace: resourceType.Namespace, 432 ObjectId: foundResourceID, 433 Relation: resourceType.Relation, 434 }) 435 436 tuples, _ := relationshipsBySubjectONR.Get(subjectKey) 437 for _, relationTuple := range tuples { 438 membershipSet.AddMemberViaRelationship(relationTuple.ResourceAndRelation.ObjectId, result.Expression, relationTuple) 439 } 440 } 441 442 if membershipSet.IsEmpty() { 443 return noMembersWithMetadata(result.Resp.Metadata) 444 } 445 446 return checkResultsForMembership(membershipSet, result.Resp.Metadata) 447 } 448 449 func (cc *ConcurrentChecker) checkUsersetRewrite(ctx context.Context, crc currentRequestContext, rewrite *core.UsersetRewrite) CheckResult { 450 switch rw := rewrite.RewriteOperation.(type) { 451 case *core.UsersetRewrite_Union: 452 if len(rw.Union.Child) > 1 { 453 var span trace.Span 454 ctx, span = tracer.Start(ctx, "+") 455 defer span.End() 456 } 457 return union(ctx, crc, rw.Union.Child, cc.runSetOperation, cc.concurrencyLimit) 458 case *core.UsersetRewrite_Intersection: 459 ctx, span := tracer.Start(ctx, "&") 460 defer span.End() 461 return all(ctx, crc, rw.Intersection.Child, cc.runSetOperation, cc.concurrencyLimit) 462 case *core.UsersetRewrite_Exclusion: 463 ctx, span := tracer.Start(ctx, "-") 464 defer span.End() 465 return difference(ctx, crc, rw.Exclusion.Child, cc.runSetOperation, cc.concurrencyLimit) 466 default: 467 return checkResultError(fmt.Errorf("unknown userset rewrite operator"), emptyMetadata) 468 } 469 } 470 471 func (cc *ConcurrentChecker) dispatch(ctx context.Context, _ currentRequestContext, req ValidatedCheckRequest) CheckResult { 472 log.Ctx(ctx).Trace().Object("dispatch", req).Send() 473 result, err := cc.d.DispatchCheck(ctx, req.DispatchCheckRequest) 474 return CheckResult{result, err} 475 } 476 477 func (cc *ConcurrentChecker) runSetOperation(ctx context.Context, crc currentRequestContext, childOneof *core.SetOperation_Child) CheckResult { 478 switch child := childOneof.ChildType.(type) { 479 case *core.SetOperation_Child_XThis: 480 return checkResultError(errors.New("use of _this is unsupported; please rewrite your schema"), emptyMetadata) 481 case *core.SetOperation_Child_ComputedUserset: 482 return cc.checkComputedUserset(ctx, crc, child.ComputedUserset, nil, nil) 483 case *core.SetOperation_Child_UsersetRewrite: 484 return cc.checkUsersetRewrite(ctx, crc, child.UsersetRewrite) 485 case *core.SetOperation_Child_TupleToUserset: 486 return cc.checkTupleToUserset(ctx, crc, child.TupleToUserset) 487 case *core.SetOperation_Child_XNil: 488 return noMembers() 489 default: 490 return checkResultError(fmt.Errorf("unknown set operation child `%T` in check", child), emptyMetadata) 491 } 492 } 493 494 func (cc *ConcurrentChecker) checkComputedUserset(ctx context.Context, crc currentRequestContext, cu *core.ComputedUserset, rr *core.RelationReference, resourceIds []string) CheckResult { 495 ctx, span := tracer.Start(ctx, cu.Relation) 496 defer span.End() 497 498 var startNamespace string 499 var targetResourceIds []string 500 if cu.Object == core.ComputedUserset_TUPLE_USERSET_OBJECT { 501 if rr == nil || len(resourceIds) == 0 { 502 return checkResultError(spiceerrors.MustBugf("computed userset for tupleset without tuples"), emptyMetadata) 503 } 504 505 startNamespace = rr.Namespace 506 targetResourceIds = resourceIds 507 } else if cu.Object == core.ComputedUserset_TUPLE_OBJECT { 508 if rr != nil { 509 return checkResultError(spiceerrors.MustBugf("computed userset for tupleset with wrong object type"), emptyMetadata) 510 } 511 512 startNamespace = crc.parentReq.ResourceRelation.Namespace 513 targetResourceIds = crc.filteredResourceIDs 514 } 515 516 targetRR := &core.RelationReference{ 517 Namespace: startNamespace, 518 Relation: cu.Relation, 519 } 520 521 // If we will be dispatching to the goal's ONR, then we know that the ONR is a member. 522 membershipSet, updatedTargetResourceIds := filterForFoundMemberResource(targetRR, targetResourceIds, crc.parentReq.Subject) 523 if (membershipSet.HasDeterminedMember() && crc.resultsSetting == v1.DispatchCheckRequest_ALLOW_SINGLE_RESULT) || len(updatedTargetResourceIds) == 0 { 524 return checkResultsForMembership(membershipSet, emptyMetadata) 525 } 526 527 // Check if the target relation exists. If not, return nothing. This is only necessary 528 // for TTU-based computed usersets, as directly computed ones reference relations within 529 // the same namespace as the caller, and thus must be fully typed checked. 530 if cu.Object == core.ComputedUserset_TUPLE_USERSET_OBJECT { 531 ds := datastoremw.MustFromContext(ctx).SnapshotReader(crc.parentReq.Revision) 532 err := namespace.CheckNamespaceAndRelation(ctx, targetRR.Namespace, targetRR.Relation, true, ds) 533 if err != nil { 534 if errors.As(err, &namespace.ErrRelationNotFound{}) { 535 return noMembers() 536 } 537 538 return checkResultError(err, emptyMetadata) 539 } 540 } 541 542 result := cc.dispatch(ctx, crc, ValidatedCheckRequest{ 543 &v1.DispatchCheckRequest{ 544 ResourceRelation: targetRR, 545 ResourceIds: updatedTargetResourceIds, 546 Subject: crc.parentReq.Subject, 547 ResultsSetting: crc.resultsSetting, 548 Metadata: decrementDepth(crc.parentReq.Metadata), 549 Debug: crc.parentReq.Debug, 550 }, 551 crc.parentReq.Revision, 552 }) 553 return combineResultWithFoundResources(result, membershipSet) 554 } 555 556 func filterForFoundMemberResource(resourceRelation *core.RelationReference, resourceIds []string, subject *core.ObjectAndRelation) (*MembershipSet, []string) { 557 if resourceRelation.Namespace != subject.Namespace || resourceRelation.Relation != subject.Relation { 558 return nil, resourceIds 559 } 560 561 for index, resourceID := range resourceIds { 562 if subject.ObjectId == resourceID { 563 membershipSet := NewMembershipSet() 564 membershipSet.AddDirectMember(resourceID, nil) 565 return membershipSet, removeIndexFromSlice(resourceIds, index) 566 } 567 } 568 569 return nil, resourceIds 570 } 571 572 func removeIndexFromSlice[T any](s []T, index int) []T { 573 cpy := make([]T, 0, len(s)-1) 574 cpy = append(cpy, s[:index]...) 575 return append(cpy, s[index+1:]...) 576 } 577 578 func (cc *ConcurrentChecker) checkTupleToUserset(ctx context.Context, crc currentRequestContext, ttu *core.TupleToUserset) CheckResult { 579 ctx, span := tracer.Start(ctx, ttu.Tupleset.Relation+"->"+ttu.ComputedUserset.Relation) 580 defer span.End() 581 582 log.Ctx(ctx).Trace().Object("ttu", crc.parentReq).Send() 583 ds := datastoremw.MustFromContext(ctx).SnapshotReader(crc.parentReq.Revision) 584 it, err := ds.QueryRelationships(ctx, datastore.RelationshipsFilter{ 585 OptionalResourceType: crc.parentReq.ResourceRelation.Namespace, 586 OptionalResourceIds: crc.filteredResourceIDs, 587 OptionalResourceRelation: ttu.Tupleset.Relation, 588 }) 589 if err != nil { 590 return checkResultError(NewCheckFailureErr(err), emptyMetadata) 591 } 592 defer it.Close() 593 594 subjectsToDispatch := tuple.NewONRByTypeSet() 595 relationshipsBySubjectONR := mapz.NewMultiMap[string, *core.RelationTuple]() 596 for tpl := it.Next(); tpl != nil; tpl = it.Next() { 597 if it.Err() != nil { 598 return checkResultError(NewCheckFailureErr(it.Err()), emptyMetadata) 599 } 600 601 subjectsToDispatch.Add(tpl.Subject) 602 relationshipsBySubjectONR.Add(tuple.StringONR(tpl.Subject), tpl) 603 } 604 it.Close() 605 606 // Convert the subjects into batched requests. 607 // To simplify the logic, +1 is added to account for the situation where 608 // the number of elements is less than the chunk size, and spare us some annoying code. 609 expectedNumberOfChunks := subjectsToDispatch.ValueLen()/int(crc.maxDispatchCount) + 1 610 toDispatch := make([]directDispatch, 0, expectedNumberOfChunks) 611 subjectsToDispatch.ForEachType(func(rr *core.RelationReference, resourceIds []string) { 612 chunkCount := 0.0 613 slicez.ForEachChunk(resourceIds, crc.maxDispatchCount, func(resourceIdChunk []string) { 614 chunkCount++ 615 toDispatch = append(toDispatch, directDispatch{ 616 resourceType: rr, 617 resourceIds: resourceIdChunk, 618 }) 619 }) 620 dispatchChunkCountHistogram.Observe(chunkCount) 621 }) 622 623 return union( 624 ctx, 625 crc, 626 toDispatch, 627 func(ctx context.Context, crc currentRequestContext, dd directDispatch) CheckResult { 628 childResult := cc.checkComputedUserset(ctx, crc, ttu.ComputedUserset, dd.resourceType, dd.resourceIds) 629 if childResult.Err != nil { 630 return childResult 631 } 632 633 return mapFoundResources(childResult, dd.resourceType, relationshipsBySubjectONR) 634 }, 635 cc.concurrencyLimit, 636 ) 637 } 638 639 func withDistinctMetadata(result CheckResult) CheckResult { 640 // NOTE: This is necessary to ensure unique debug information on the request and that debug 641 // information from the child metadata is *not* copied over. 642 clonedResp := result.Resp.CloneVT() 643 clonedResp.Metadata = combineResponseMetadata(emptyMetadata, clonedResp.Metadata) 644 return CheckResult{ 645 Resp: clonedResp, 646 Err: result.Err, 647 } 648 } 649 650 // union returns whether any one of the lazy checks pass, and is used for union. 651 func union[T any]( 652 ctx context.Context, 653 crc currentRequestContext, 654 children []T, 655 handler func(ctx context.Context, crc currentRequestContext, child T) CheckResult, 656 concurrencyLimit uint16, 657 ) CheckResult { 658 if len(children) == 0 { 659 return noMembers() 660 } 661 662 if len(children) == 1 { 663 return withDistinctMetadata(handler(ctx, crc, children[0])) 664 } 665 666 resultChan := make(chan CheckResult, len(children)) 667 childCtx, cancelFn := context.WithCancel(ctx) 668 dispatchAllAsync(childCtx, crc, children, handler, resultChan, concurrencyLimit) 669 defer cancelFn() 670 671 responseMetadata := emptyMetadata 672 membershipSet := NewMembershipSet() 673 674 for i := 0; i < len(children); i++ { 675 select { 676 case result := <-resultChan: 677 log.Ctx(ctx).Trace().Object("anyResult", result.Resp).Send() 678 responseMetadata = combineResponseMetadata(responseMetadata, result.Resp.Metadata) 679 if result.Err != nil { 680 return checkResultError(result.Err, responseMetadata) 681 } 682 683 membershipSet.UnionWith(result.Resp.ResultsByResourceId) 684 if membershipSet.HasDeterminedMember() && crc.resultsSetting == v1.DispatchCheckRequest_ALLOW_SINGLE_RESULT { 685 return checkResultsForMembership(membershipSet, responseMetadata) 686 } 687 688 case <-ctx.Done(): 689 log.Ctx(ctx).Trace().Msg("anyCanceled") 690 return checkResultError(context.Canceled, responseMetadata) 691 } 692 } 693 694 return checkResultsForMembership(membershipSet, responseMetadata) 695 } 696 697 // all returns whether all of the lazy checks pass, and is used for intersection. 698 func all[T any]( 699 ctx context.Context, 700 crc currentRequestContext, 701 children []T, 702 handler func(ctx context.Context, crc currentRequestContext, child T) CheckResult, 703 concurrencyLimit uint16, 704 ) CheckResult { 705 if len(children) == 0 { 706 return noMembers() 707 } 708 709 if len(children) == 1 { 710 return withDistinctMetadata(handler(ctx, crc, children[0])) 711 } 712 713 responseMetadata := emptyMetadata 714 715 resultChan := make(chan CheckResult, len(children)) 716 childCtx, cancelFn := context.WithCancel(ctx) 717 dispatchAllAsync(childCtx, currentRequestContext{ 718 parentReq: crc.parentReq, 719 filteredResourceIDs: crc.filteredResourceIDs, 720 resultsSetting: v1.DispatchCheckRequest_REQUIRE_ALL_RESULTS, 721 maxDispatchCount: crc.maxDispatchCount, 722 }, children, handler, resultChan, concurrencyLimit) 723 defer cancelFn() 724 725 var membershipSet *MembershipSet 726 for i := 0; i < len(children); i++ { 727 select { 728 case result := <-resultChan: 729 responseMetadata = combineResponseMetadata(responseMetadata, result.Resp.Metadata) 730 if result.Err != nil { 731 return checkResultError(result.Err, responseMetadata) 732 } 733 734 if membershipSet == nil { 735 membershipSet = NewMembershipSet() 736 membershipSet.UnionWith(result.Resp.ResultsByResourceId) 737 } else { 738 membershipSet.IntersectWith(result.Resp.ResultsByResourceId) 739 } 740 741 if membershipSet.IsEmpty() { 742 return noMembersWithMetadata(responseMetadata) 743 } 744 case <-ctx.Done(): 745 return checkResultError(context.Canceled, responseMetadata) 746 } 747 } 748 749 return checkResultsForMembership(membershipSet, responseMetadata) 750 } 751 752 // difference returns whether the first lazy check passes and none of the supsequent checks pass. 753 func difference[T any]( 754 ctx context.Context, 755 crc currentRequestContext, 756 children []T, 757 handler func(ctx context.Context, crc currentRequestContext, child T) CheckResult, 758 concurrencyLimit uint16, 759 ) CheckResult { 760 if len(children) == 0 { 761 return noMembers() 762 } 763 764 if len(children) == 1 { 765 return checkResultError(fmt.Errorf("difference requires more than a single child"), emptyMetadata) 766 } 767 768 childCtx, cancelFn := context.WithCancel(ctx) 769 baseChan := make(chan CheckResult, 1) 770 othersChan := make(chan CheckResult, len(children)-1) 771 772 go func() { 773 result := handler(childCtx, crc, children[0]) 774 baseChan <- result 775 }() 776 777 dispatchAllAsync(childCtx, currentRequestContext{ 778 parentReq: crc.parentReq, 779 filteredResourceIDs: crc.filteredResourceIDs, 780 resultsSetting: v1.DispatchCheckRequest_REQUIRE_ALL_RESULTS, 781 maxDispatchCount: crc.maxDispatchCount, 782 }, children[1:], handler, othersChan, concurrencyLimit-1) 783 defer cancelFn() 784 785 responseMetadata := emptyMetadata 786 membershipSet := NewMembershipSet() 787 788 // Wait for the base set to return. 789 select { 790 case base := <-baseChan: 791 responseMetadata = combineResponseMetadata(responseMetadata, base.Resp.Metadata) 792 793 if base.Err != nil { 794 return checkResultError(base.Err, responseMetadata) 795 } 796 797 membershipSet.UnionWith(base.Resp.ResultsByResourceId) 798 if membershipSet.IsEmpty() { 799 return noMembersWithMetadata(responseMetadata) 800 } 801 802 case <-ctx.Done(): 803 return checkResultError(context.Canceled, responseMetadata) 804 } 805 806 // Subtract the remaining sets. 807 for i := 1; i < len(children); i++ { 808 select { 809 case sub := <-othersChan: 810 responseMetadata = combineResponseMetadata(responseMetadata, sub.Resp.Metadata) 811 812 if sub.Err != nil { 813 return checkResultError(sub.Err, responseMetadata) 814 } 815 816 membershipSet.Subtract(sub.Resp.ResultsByResourceId) 817 if membershipSet.IsEmpty() { 818 return noMembersWithMetadata(responseMetadata) 819 } 820 821 case <-ctx.Done(): 822 return checkResultError(context.Canceled, responseMetadata) 823 } 824 } 825 826 return checkResultsForMembership(membershipSet, responseMetadata) 827 } 828 829 func dispatchAllAsync[T any]( 830 ctx context.Context, 831 crc currentRequestContext, 832 children []T, 833 handler func(ctx context.Context, crc currentRequestContext, child T) CheckResult, 834 resultChan chan<- CheckResult, 835 concurrencyLimit uint16, 836 ) { 837 tr := taskrunner.NewPreloadedTaskRunner(ctx, concurrencyLimit, len(children)) 838 for _, currentChild := range children { 839 currentChild := currentChild 840 tr.Add(func(ctx context.Context) error { 841 result := handler(ctx, crc, currentChild) 842 resultChan <- result 843 return result.Err 844 }) 845 } 846 847 tr.Start() 848 } 849 850 func noMembers() CheckResult { 851 return CheckResult{ 852 &v1.DispatchCheckResponse{ 853 Metadata: emptyMetadata, 854 }, 855 nil, 856 } 857 } 858 859 func noMembersWithMetadata(metadata *v1.ResponseMeta) CheckResult { 860 return CheckResult{ 861 &v1.DispatchCheckResponse{ 862 Metadata: metadata, 863 }, 864 nil, 865 } 866 } 867 868 func checkResultsForMembership(foundMembership *MembershipSet, subProblemMetadata *v1.ResponseMeta) CheckResult { 869 return CheckResult{ 870 &v1.DispatchCheckResponse{ 871 Metadata: ensureMetadata(subProblemMetadata), 872 ResultsByResourceId: foundMembership.AsCheckResultsMap(), 873 }, 874 nil, 875 } 876 } 877 878 func checkResultError(err error, subProblemMetadata *v1.ResponseMeta) CheckResult { 879 return CheckResult{ 880 &v1.DispatchCheckResponse{ 881 Metadata: ensureMetadata(subProblemMetadata), 882 }, 883 err, 884 } 885 } 886 887 func combineResultWithFoundResources(result CheckResult, foundResources *MembershipSet) CheckResult { 888 if result.Err != nil { 889 return result 890 } 891 892 if foundResources.IsEmpty() { 893 return result 894 } 895 896 foundResources.UnionWith(result.Resp.ResultsByResourceId) 897 return CheckResult{ 898 Resp: &v1.DispatchCheckResponse{ 899 ResultsByResourceId: foundResources.AsCheckResultsMap(), 900 Metadata: result.Resp.Metadata, 901 }, 902 Err: result.Err, 903 } 904 } 905 906 func combineResponseMetadata(existing *v1.ResponseMeta, responseMetadata *v1.ResponseMeta) *v1.ResponseMeta { 907 combined := &v1.ResponseMeta{ 908 DispatchCount: existing.DispatchCount + responseMetadata.DispatchCount, 909 DepthRequired: max(existing.DepthRequired, responseMetadata.DepthRequired), 910 CachedDispatchCount: existing.CachedDispatchCount + responseMetadata.CachedDispatchCount, 911 } 912 913 if existing.DebugInfo == nil && responseMetadata.DebugInfo == nil { 914 return combined 915 } 916 917 debugInfo := &v1.DebugInformation{ 918 Check: &v1.CheckDebugTrace{}, 919 } 920 921 if existing.DebugInfo != nil { 922 if existing.DebugInfo.Check.Request != nil { 923 debugInfo.Check.SubProblems = append(debugInfo.Check.SubProblems, existing.DebugInfo.Check) 924 } else { 925 debugInfo.Check.SubProblems = append(debugInfo.Check.SubProblems, existing.DebugInfo.Check.SubProblems...) 926 } 927 } 928 929 if responseMetadata.DebugInfo != nil { 930 if responseMetadata.DebugInfo.Check.Request != nil { 931 debugInfo.Check.SubProblems = append(debugInfo.Check.SubProblems, responseMetadata.DebugInfo.Check) 932 } else { 933 debugInfo.Check.SubProblems = append(debugInfo.Check.SubProblems, responseMetadata.DebugInfo.Check.SubProblems...) 934 } 935 } 936 937 combined.DebugInfo = debugInfo 938 return combined 939 }