github.com/openfga/openfga@v1.5.4-rc1/pkg/server/commands/listusers/list_users_rpc.go (about) 1 package listusers 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "sync" 8 "sync/atomic" 9 "time" 10 11 openfgav1 "github.com/openfga/api/proto/openfga/v1" 12 "github.com/sourcegraph/conc/pool" 13 "go.opentelemetry.io/otel" 14 "go.opentelemetry.io/otel/attribute" 15 16 serverconfig "github.com/openfga/openfga/internal/server/config" 17 18 "github.com/openfga/openfga/pkg/telemetry" 19 20 "github.com/openfga/openfga/pkg/logger" 21 22 "github.com/openfga/openfga/pkg/storage/storagewrappers" 23 24 "github.com/openfga/openfga/internal/condition" 25 "github.com/openfga/openfga/internal/condition/eval" 26 "github.com/openfga/openfga/internal/graph" 27 "github.com/openfga/openfga/internal/validation" 28 "github.com/openfga/openfga/pkg/storage" 29 "github.com/openfga/openfga/pkg/tuple" 30 "github.com/openfga/openfga/pkg/typesystem" 31 ) 32 33 var tracer = otel.Tracer("openfga/pkg/server/commands/list_users") 34 35 type listUsersQuery struct { 36 logger logger.Logger 37 ds storage.RelationshipTupleReader 38 typesystemResolver typesystem.TypesystemResolverFunc 39 resolveNodeBreadthLimit uint32 40 resolveNodeLimit uint32 41 maxResults uint32 42 maxConcurrentReads uint32 43 deadline time.Duration 44 } 45 type expandResponse struct { 46 hasCycle bool 47 err error 48 } 49 50 type ListUsersQueryOption func(l *listUsersQuery) 51 52 func WithListUsersQueryLogger(l logger.Logger) ListUsersQueryOption { 53 return func(d *listUsersQuery) { 54 d.logger = l 55 } 56 } 57 58 // WithListUsersMaxResults see server.WithListUsersMaxResults. 59 func WithListUsersMaxResults(max uint32) ListUsersQueryOption { 60 return func(d *listUsersQuery) { 61 d.maxResults = max 62 } 63 } 64 65 // WithListUsersDeadline see server.WithListUsersDeadline. 66 func WithListUsersDeadline(t time.Duration) ListUsersQueryOption { 67 return func(d *listUsersQuery) { 68 d.deadline = t 69 } 70 } 71 72 // WithResolveNodeLimit see server.WithResolveNodeLimit. 73 func WithResolveNodeLimit(limit uint32) ListUsersQueryOption { 74 return func(d *listUsersQuery) { 75 d.resolveNodeLimit = limit 76 } 77 } 78 79 // WithResolveNodeBreadthLimit see server.WithResolveNodeBreadthLimit. 80 func WithResolveNodeBreadthLimit(limit uint32) ListUsersQueryOption { 81 return func(d *listUsersQuery) { 82 d.resolveNodeBreadthLimit = limit 83 } 84 } 85 86 // WithListUsersMaxConcurrentReads see server.WithMaxConcurrentReadsForListUsers. 87 func WithListUsersMaxConcurrentReads(limit uint32) ListUsersQueryOption { 88 return func(d *listUsersQuery) { 89 d.maxConcurrentReads = limit 90 } 91 } 92 93 // NewListUsersQuery is not meant to be shared. 94 func NewListUsersQuery(ds storage.RelationshipTupleReader, opts ...ListUsersQueryOption) *listUsersQuery { 95 l := &listUsersQuery{ 96 logger: logger.NewNoopLogger(), 97 ds: ds, 98 typesystemResolver: func(ctx context.Context, storeID, modelID string) (*typesystem.TypeSystem, error) { 99 typesys, exists := typesystem.TypesystemFromContext(ctx) 100 if !exists { 101 return nil, fmt.Errorf("typesystem not provided in context") 102 } 103 104 return typesys, nil 105 }, 106 resolveNodeBreadthLimit: serverconfig.DefaultResolveNodeBreadthLimit, 107 resolveNodeLimit: serverconfig.DefaultResolveNodeLimit, 108 deadline: serverconfig.DefaultListUsersDeadline, 109 maxResults: serverconfig.DefaultListUsersMaxResults, 110 maxConcurrentReads: serverconfig.DefaultMaxConcurrentReadsForListUsers, 111 } 112 113 for _, opt := range opts { 114 opt(l) 115 } 116 117 return l 118 } 119 120 // ListUsers assumes that the typesystem is in the context. 121 func (l *listUsersQuery) ListUsers( 122 ctx context.Context, 123 req *openfgav1.ListUsersRequest, 124 ) (*listUsersResponse, error) { 125 ctx, span := tracer.Start(ctx, "ListUsers") 126 defer span.End() 127 128 cancellableCtx, cancelCtx := context.WithCancel(ctx) 129 defer cancelCtx() 130 if l.deadline != 0 { 131 cancellableCtx, cancelCtx = context.WithTimeout(cancellableCtx, l.deadline) 132 defer cancelCtx() 133 } 134 l.ds = storagewrappers.NewCombinedTupleReader( 135 storagewrappers.NewBoundedConcurrencyTupleReader(l.ds, l.maxConcurrentReads), 136 req.GetContextualTuples(), 137 ) 138 typesys, ok := typesystem.TypesystemFromContext(cancellableCtx) 139 if !ok { 140 return nil, fmt.Errorf("typesystem missing in context") 141 } 142 143 userFilter := req.GetUserFilters()[0] 144 isReflexiveUserset := userFilter.GetType() == req.GetObject().GetType() && userFilter.GetRelation() == req.GetRelation() 145 146 if !isReflexiveUserset { 147 hasPossibleEdges, err := doesHavePossibleEdges(typesys, req) 148 if err != nil { 149 return nil, err 150 } 151 if !hasPossibleEdges { 152 span.SetAttributes(attribute.Bool("no_possible_edges", true)) 153 return &listUsersResponse{ 154 Users: []*openfgav1.User{}, 155 Metadata: listUsersResponseMetadata{ 156 DatastoreQueryCount: 0, 157 }, 158 }, nil 159 } 160 } 161 162 datastoreQueryCount := atomic.Uint32{} 163 164 foundUsersCh := l.buildResultsChannel() 165 expandErrCh := make(chan error, 1) 166 167 foundUsersUnique := make(map[tuple.UserString]struct{}, 1000) 168 doneWithFoundUsersCh := make(chan struct{}, 1) 169 go func() { 170 for foundObject := range foundUsersCh { 171 foundUsersUnique[tuple.UserProtoToString(foundObject)] = struct{}{} 172 if l.maxResults > 0 { 173 if uint32(len(foundUsersUnique)) >= l.maxResults { 174 span.SetAttributes(attribute.Bool("max_results_found", true)) 175 break 176 } 177 } 178 } 179 180 doneWithFoundUsersCh <- struct{}{} 181 }() 182 183 go func() { 184 internalRequest := fromListUsersRequest(req, &datastoreQueryCount) 185 resp := l.expand(cancellableCtx, internalRequest, foundUsersCh) 186 close(foundUsersCh) 187 188 if resp.err != nil { 189 expandErrCh <- resp.err 190 } 191 }() 192 193 select { 194 case err := <-expandErrCh: 195 telemetry.TraceError(span, err) 196 return nil, err 197 case <-doneWithFoundUsersCh: 198 break 199 case <-cancellableCtx.Done(): 200 // to avoid a race on the 'foundUsersUnique' map below, wait for the range over the channel to close 201 <-doneWithFoundUsersCh 202 break 203 } 204 205 cancelCtx() 206 207 foundUsers := make([]*openfgav1.User, 0, len(foundUsersUnique)) 208 for foundUser := range foundUsersUnique { 209 foundUsers = append(foundUsers, tuple.StringToUserProto(foundUser)) 210 } 211 span.SetAttributes(attribute.Int("result_count", len(foundUsers))) 212 213 return &listUsersResponse{ 214 Users: foundUsers, 215 Metadata: listUsersResponseMetadata{ 216 DatastoreQueryCount: datastoreQueryCount.Load(), 217 }, 218 }, nil 219 } 220 221 func doesHavePossibleEdges(typesys *typesystem.TypeSystem, req *openfgav1.ListUsersRequest) (bool, error) { 222 g := graph.New(typesys) 223 224 userFilters := req.GetUserFilters() 225 226 source := typesystem.DirectRelationReference(userFilters[0].GetType(), userFilters[0].GetRelation()) 227 target := typesystem.DirectRelationReference(req.GetObject().GetType(), req.GetRelation()) 228 229 edges, err := g.GetPrunedRelationshipEdges(target, source) 230 if err != nil { 231 return false, err 232 } 233 234 return len(edges) > 0, err 235 } 236 237 func (l *listUsersQuery) expand( 238 ctx context.Context, 239 req *internalListUsersRequest, 240 foundUsersChan chan<- *openfgav1.User, 241 ) expandResponse { 242 ctx, span := tracer.Start(ctx, "expand") 243 defer span.End() 244 span.SetAttributes(attribute.Int("depth", int(req.depth))) 245 if req.depth >= l.resolveNodeLimit { 246 return expandResponse{ 247 err: graph.ErrResolutionDepthExceeded, 248 } 249 } 250 req.depth++ 251 252 if enteredCycle(req) { 253 span.SetAttributes(attribute.Bool("cycle_detected", true)) 254 return expandResponse{ 255 hasCycle: true, 256 } 257 } 258 259 reqObjectType := req.GetObject().GetType() 260 reqObjectID := req.GetObject().GetId() 261 reqRelation := req.GetRelation() 262 263 for _, userFilter := range req.GetUserFilters() { 264 if reqObjectType == userFilter.GetType() && reqRelation == userFilter.GetRelation() { 265 user := &openfgav1.User{ 266 User: &openfgav1.User_Userset{ 267 Userset: &openfgav1.UsersetUser{ 268 Type: reqObjectType, 269 Id: reqObjectID, 270 Relation: reqRelation, 271 }, 272 }, 273 } 274 trySendResult(ctx, user, foundUsersChan) 275 } 276 } 277 278 typesys, err := l.typesystemResolver(ctx, req.GetStoreId(), req.GetAuthorizationModelId()) 279 if err != nil { 280 return expandResponse{ 281 err: err, 282 } 283 } 284 285 targetObjectType := req.GetObject().GetType() 286 targetRelation := req.GetRelation() 287 288 relation, err := typesys.GetRelation(targetObjectType, targetRelation) 289 if err != nil { 290 return expandResponse{ 291 err: err, 292 } 293 } 294 295 relationRewrite := relation.GetRewrite() 296 resp := l.expandRewrite(ctx, req, relationRewrite, foundUsersChan) 297 if resp.err != nil { 298 telemetry.TraceError(span, resp.err) 299 } 300 return resp 301 } 302 303 func (l *listUsersQuery) expandRewrite( 304 ctx context.Context, 305 req *internalListUsersRequest, 306 rewrite *openfgav1.Userset, 307 foundUsersChan chan<- *openfgav1.User, 308 ) expandResponse { 309 ctx, span := tracer.Start(ctx, "expandRewrite") 310 defer span.End() 311 312 var resp expandResponse 313 switch rewrite := rewrite.GetUserset().(type) { 314 case *openfgav1.Userset_This: 315 resp = l.expandDirect(ctx, req, foundUsersChan) 316 case *openfgav1.Userset_ComputedUserset: 317 rewrittenReq := req.clone() 318 rewrittenReq.Relation = rewrite.ComputedUserset.GetRelation() 319 resp = l.expand(ctx, rewrittenReq, foundUsersChan) 320 case *openfgav1.Userset_TupleToUserset: 321 resp = l.expandTTU(ctx, req, rewrite, foundUsersChan) 322 case *openfgav1.Userset_Intersection: 323 resp = l.expandIntersection(ctx, req, rewrite, foundUsersChan) 324 case *openfgav1.Userset_Difference: 325 resp = l.expandExclusion(ctx, req, rewrite, foundUsersChan) 326 case *openfgav1.Userset_Union: 327 pool := pool.New().WithContext(ctx) 328 pool.WithCancelOnError() 329 pool.WithMaxGoroutines(int(l.resolveNodeBreadthLimit)) 330 331 children := rewrite.Union.GetChild() 332 for _, childRewrite := range children { 333 childRewriteCopy := childRewrite 334 pool.Go(func(ctx context.Context) error { 335 resp := l.expandRewrite(ctx, req, childRewriteCopy, foundUsersChan) 336 return resp.err 337 }) 338 } 339 340 resp.err = pool.Wait() 341 default: 342 panic("unexpected userset rewrite encountered") 343 } 344 345 if resp.err != nil { 346 telemetry.TraceError(span, resp.err) 347 } 348 return resp 349 } 350 351 func (l *listUsersQuery) expandDirect( 352 ctx context.Context, 353 req *internalListUsersRequest, 354 foundUsersChan chan<- *openfgav1.User, 355 ) expandResponse { 356 ctx, span := tracer.Start(ctx, "expandDirect") 357 defer span.End() 358 typesys, err := l.typesystemResolver(ctx, req.GetStoreId(), req.GetAuthorizationModelId()) 359 if err != nil { 360 return expandResponse{ 361 err: err, 362 } 363 } 364 365 iter, err := l.ds.Read(ctx, req.GetStoreId(), &openfgav1.TupleKey{ 366 Object: tuple.ObjectKey(req.GetObject()), 367 Relation: req.GetRelation(), 368 }) 369 if err != nil { 370 telemetry.TraceError(span, err) 371 return expandResponse{ 372 err: err, 373 } 374 } 375 defer iter.Stop() 376 req.datastoreQueryCount.Add(1) 377 378 filteredIter := storage.NewFilteredTupleKeyIterator( 379 storage.NewTupleKeyIteratorFromTupleIterator(iter), 380 validation.FilterInvalidTuples(typesys), 381 ) 382 defer filteredIter.Stop() 383 384 pool := pool.New().WithContext(ctx) 385 pool.WithCancelOnError() 386 pool.WithMaxGoroutines(int(l.resolveNodeBreadthLimit)) 387 388 var errs error 389 var hasCycle atomic.Bool 390 LoopOnIterator: 391 for { 392 tupleKey, err := filteredIter.Next(ctx) 393 if err != nil { 394 if !errors.Is(err, storage.ErrIteratorDone) { 395 errs = errors.Join(errs, err) 396 } 397 398 break LoopOnIterator 399 } 400 401 condEvalResult, err := eval.EvaluateTupleCondition(ctx, tupleKey, typesys, req.GetContext()) 402 if err != nil { 403 errs = errors.Join(errs, err) 404 break LoopOnIterator 405 } 406 407 if len(condEvalResult.MissingParameters) > 0 { 408 err := condition.NewEvaluationError( 409 tupleKey.GetCondition().GetName(), 410 fmt.Errorf("context is missing parameters '%v'", condEvalResult.MissingParameters), 411 ) 412 if err != nil { 413 telemetry.TraceError(span, err) 414 errs = errors.Join(errs, err) 415 } 416 } 417 418 if !condEvalResult.ConditionMet { 419 continue 420 } 421 422 tupleKeyUser := tupleKey.GetUser() 423 userObject, userRelation := tuple.SplitObjectRelation(tupleKeyUser) 424 userObjectType, userObjectID := tuple.SplitObject(userObject) 425 426 if userRelation == "" { 427 for _, f := range req.GetUserFilters() { 428 if f.GetType() == userObjectType { 429 user := tuple.StringToUserProto(tuple.BuildObject(userObjectType, userObjectID)) 430 trySendResult(ctx, user, foundUsersChan) 431 } 432 } 433 continue 434 } 435 436 pool.Go(func(ctx context.Context) error { 437 rewrittenReq := req.clone() 438 rewrittenReq.Object = &openfgav1.Object{Type: userObjectType, Id: userObjectID} 439 rewrittenReq.Relation = userRelation 440 resp := l.expand(ctx, rewrittenReq, foundUsersChan) 441 if resp.hasCycle { 442 hasCycle.Store(true) 443 } 444 return resp.err 445 }) 446 } 447 448 errs = errors.Join(errs, pool.Wait()) 449 if errs != nil { 450 telemetry.TraceError(span, errs) 451 } 452 return expandResponse{ 453 err: errs, 454 hasCycle: hasCycle.Load(), 455 } 456 } 457 458 func (l *listUsersQuery) expandIntersection( 459 ctx context.Context, 460 req *internalListUsersRequest, 461 rewrite *openfgav1.Userset_Intersection, 462 foundUsersChan chan<- *openfgav1.User, 463 ) expandResponse { 464 ctx, span := tracer.Start(ctx, "expandIntersection") 465 defer span.End() 466 pool := pool.New().WithContext(ctx) 467 pool.WithCancelOnError() 468 pool.WithMaxGoroutines(int(l.resolveNodeBreadthLimit)) 469 470 childOperands := rewrite.Intersection.GetChild() 471 intersectionFoundUsersChans := make([]chan *openfgav1.User, len(childOperands)) 472 for i, rewrite := range childOperands { 473 i := i 474 rewrite := rewrite 475 intersectionFoundUsersChans[i] = make(chan *openfgav1.User, 1) 476 pool.Go(func(ctx context.Context) error { 477 resp := l.expandRewrite(ctx, req, rewrite, intersectionFoundUsersChans[i]) 478 return resp.err 479 }) 480 } 481 482 errChan := make(chan error, 1) 483 484 go func() { 485 err := pool.Wait() 486 for i := range intersectionFoundUsersChans { 487 close(intersectionFoundUsersChans[i]) 488 } 489 errChan <- err 490 close(errChan) 491 }() 492 493 var mu sync.Mutex 494 495 var wg sync.WaitGroup 496 wg.Add(len(childOperands)) 497 498 wildcardCount := atomic.Uint32{} 499 wildcardKey := tuple.TypedPublicWildcard(req.GetUserFilters()[0].GetType()) 500 foundUsersCountMap := make(map[string]uint32, 0) 501 for _, foundUsersChan := range intersectionFoundUsersChans { 502 go func(foundUsersChan chan *openfgav1.User) { 503 defer wg.Done() 504 foundUsersMap := make(map[string]uint32, 0) 505 for foundUser := range foundUsersChan { 506 key := tuple.UserProtoToString(foundUser) 507 foundUsersMap[key]++ 508 } 509 510 _, wildcardExists := foundUsersMap[wildcardKey] 511 if wildcardExists { 512 wildcardCount.Add(1) 513 } 514 for userKey := range foundUsersMap { 515 mu.Lock() 516 // Increment the count for a user but decrement if a wildcard 517 // also exists to prevent double counting. This ensures accurate 518 // tracking for intersection criteria, avoiding inflated counts 519 // when both a user and a wildcard are present. 520 foundUsersCountMap[userKey]++ 521 if wildcardExists { 522 foundUsersCountMap[userKey]-- 523 } 524 mu.Unlock() 525 } 526 }(foundUsersChan) 527 } 528 wg.Wait() 529 530 for key, count := range foundUsersCountMap { 531 // Compare the number of times the specific user was returned for 532 // all intersection operands plus the number of wildcards. 533 // If this summed value equals the number of operands, the user satisfies 534 // the intersection expression and can be sent on `foundUsersChan` 535 if (count + wildcardCount.Load()) == uint32(len(childOperands)) { 536 trySendResult(ctx, tuple.StringToUserProto(key), foundUsersChan) 537 } 538 } 539 540 return expandResponse{ 541 err: <-errChan, 542 } 543 } 544 545 func (l *listUsersQuery) expandExclusion( 546 ctx context.Context, 547 req *internalListUsersRequest, 548 rewrite *openfgav1.Userset_Difference, 549 foundUsersChan chan<- *openfgav1.User, 550 ) expandResponse { 551 ctx, span := tracer.Start(ctx, "expandExclusion") 552 defer span.End() 553 baseFoundUsersCh := make(chan *openfgav1.User, 1) 554 subtractFoundUsersCh := make(chan *openfgav1.User, 1) 555 556 var baseError error 557 go func() { 558 resp := l.expandRewrite(ctx, req, rewrite.Difference.GetBase(), baseFoundUsersCh) 559 baseError = resp.err 560 close(baseFoundUsersCh) 561 }() 562 563 var substractError error 564 var subtractHasCycle bool 565 go func() { 566 resp := l.expandRewrite(ctx, req, rewrite.Difference.GetSubtract(), subtractFoundUsersCh) 567 substractError = resp.err 568 subtractHasCycle = resp.hasCycle 569 close(subtractFoundUsersCh) 570 }() 571 572 baseFoundUsersMap := make(map[string]struct{}, 0) 573 for fu := range baseFoundUsersCh { 574 key := tuple.UserProtoToString(fu) 575 baseFoundUsersMap[key] = struct{}{} 576 } 577 subtractFoundUsersMap := make(map[string]struct{}, len(baseFoundUsersMap)) 578 for fu := range subtractFoundUsersCh { 579 key := tuple.UserProtoToString(fu) 580 subtractFoundUsersMap[key] = struct{}{} 581 } 582 583 if subtractHasCycle { 584 // Because exclusion contains the only bespoke treatment of 585 // cycle, everywhere else we consider it a falsey outcome. 586 // Once we make a determination within the exclusion handler, we're 587 // able to properly handle the case and do not need to propagate 588 // the existence of a cycle to an upstream handler. 589 return expandResponse{ 590 err: nil, 591 } 592 } 593 594 wildcardKey := tuple.TypedPublicWildcard(req.GetUserFilters()[0].GetType()) 595 _, subtractWildcardExists := subtractFoundUsersMap[wildcardKey] 596 for key := range baseFoundUsersMap { 597 if _, isSubtracted := subtractFoundUsersMap[key]; !isSubtracted && !subtractWildcardExists { 598 // Iterate over base users because at minimum they need to pass 599 // but then they are further compared to the subtracted users map. 600 // If users exist in both maps, they are excluded. Only users that exist 601 // solely in the base map will be returned. 602 trySendResult(ctx, tuple.StringToUserProto(key), foundUsersChan) 603 } 604 } 605 606 errs := errors.Join(baseError, substractError) 607 if errs != nil { 608 telemetry.TraceError(span, errs) 609 } 610 return expandResponse{ 611 err: errs, 612 } 613 } 614 615 func (l *listUsersQuery) expandTTU( 616 ctx context.Context, 617 req *internalListUsersRequest, 618 rewrite *openfgav1.Userset_TupleToUserset, 619 foundUsersChan chan<- *openfgav1.User, 620 ) expandResponse { 621 ctx, span := tracer.Start(ctx, "expandTTU") 622 defer span.End() 623 tuplesetRelation := rewrite.TupleToUserset.GetTupleset().GetRelation() 624 computedRelation := rewrite.TupleToUserset.GetComputedUserset().GetRelation() 625 626 typesys, err := l.typesystemResolver(ctx, req.GetStoreId(), req.GetAuthorizationModelId()) 627 if err != nil { 628 return expandResponse{ 629 err: err, 630 } 631 } 632 633 iter, err := l.ds.Read(ctx, req.GetStoreId(), &openfgav1.TupleKey{ 634 Object: tuple.ObjectKey(req.GetObject()), 635 Relation: tuplesetRelation, 636 }) 637 if err != nil { 638 telemetry.TraceError(span, err) 639 return expandResponse{ 640 err: err, 641 } 642 } 643 defer iter.Stop() 644 req.datastoreQueryCount.Add(1) 645 646 filteredIter := storage.NewFilteredTupleKeyIterator( 647 storage.NewTupleKeyIteratorFromTupleIterator(iter), 648 validation.FilterInvalidTuples(typesys), 649 ) 650 defer filteredIter.Stop() 651 652 pool := pool.New().WithContext(ctx) 653 pool.WithCancelOnError() 654 pool.WithMaxGoroutines(int(l.resolveNodeBreadthLimit)) 655 656 var errs error 657 658 LoopOnIterator: 659 for { 660 tupleKey, err := filteredIter.Next(ctx) 661 if err != nil { 662 if !errors.Is(err, storage.ErrIteratorDone) { 663 errs = errors.Join(errs, err) 664 } 665 666 break LoopOnIterator 667 } 668 669 condEvalResult, err := eval.EvaluateTupleCondition(ctx, tupleKey, typesys, req.GetContext()) 670 if err != nil { 671 errs = errors.Join(errs, err) 672 break LoopOnIterator 673 } 674 675 if len(condEvalResult.MissingParameters) > 0 { 676 err := condition.NewEvaluationError( 677 tupleKey.GetCondition().GetName(), 678 fmt.Errorf("context is missing parameters '%v'", condEvalResult.MissingParameters), 679 ) 680 if err != nil { 681 telemetry.TraceError(span, err) 682 errs = errors.Join(errs, err) 683 } 684 } 685 686 if !condEvalResult.ConditionMet { 687 continue 688 } 689 690 userObject := tupleKey.GetUser() 691 userObjectType, userObjectID := tuple.SplitObject(userObject) 692 693 pool.Go(func(ctx context.Context) error { 694 rewrittenReq := req.clone() 695 rewrittenReq.Object = &openfgav1.Object{Type: userObjectType, Id: userObjectID} 696 rewrittenReq.Relation = computedRelation 697 resp := l.expand(ctx, rewrittenReq, foundUsersChan) 698 return resp.err 699 }) 700 } 701 702 errs = errors.Join(pool.Wait(), errs) 703 if errs != nil { 704 telemetry.TraceError(span, errs) 705 } 706 return expandResponse{ 707 err: errs, 708 } 709 } 710 711 func enteredCycle(req *internalListUsersRequest) bool { 712 key := fmt.Sprintf("%s#%s", tuple.ObjectKey(req.GetObject()), req.Relation) 713 if _, loaded := req.visitedUsersetsMap[key]; loaded { 714 return true 715 } 716 req.visitedUsersetsMap[key] = struct{}{} 717 return false 718 } 719 720 func (l *listUsersQuery) buildResultsChannel() chan *openfgav1.User { 721 foundUsersCh := make(chan *openfgav1.User, serverconfig.DefaultListUsersMaxResults) 722 maxResults := l.maxResults 723 if maxResults > 0 { 724 foundUsersCh = make(chan *openfgav1.User, maxResults) 725 } 726 return foundUsersCh 727 } 728 729 func trySendResult(ctx context.Context, user *openfgav1.User, foundUsersCh chan<- *openfgav1.User) { 730 select { 731 case <-ctx.Done(): 732 return 733 case foundUsersCh <- user: 734 return 735 } 736 }