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  }