VYPR
Moderate severityNVD Advisory· Published Feb 6, 2026· Updated Feb 6, 2026

OpenFGA Improper Policy Enforcement

CVE-2026-24851

Description

OpenFGA is a high-performance and flexible authorization/permission engine built for developers and inspired by Google Zanzibar. OpenFGA v1.8.5 to v1.11.2 ( openfga-0.2.22<= Helm chart <= openfga-0.2.51, v.1.8.5 <= docker <= v.1.11.2) are vulnerable to improper policy enforcement when certain Check calls are executed. The vulnerability requires a model that has a a relation directly assignable by a type bound public access and assignable by type bound non-public access, a tuple assigned for the relation that is a type bound public access, a tuple assigned for the same object with the same relation that is not type bound public access, and a tuple assigned for a different object that has an object ID lexicographically larger with the same user and relation which is not type bound public access. This vulnerability is fixed in v1.11.3.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/openfga/openfgaGo
>= 1.8.5, < 1.11.31.11.3

Affected products

1

Patches

1
1bb5eddf4a3d

fix: order iterator to advance tuples correctly (#2898)

https://github.com/openfga/openfgaAdrian TamJan 28, 2026via ghsa
5 files changed · +469 137
  • CHANGELOG.md+1 0 modified
    @@ -22,6 +22,7 @@ Try to keep listed changes to a concise bulleted list of simple explanations of
     - ListUsers will now properly get datastore throttled if enabled. [#2846](https://github.com/openfga/openfga/pull/2846)
     - Cache controller now uses the logger provided to the server instead of always using a no-op logger. [#2847](https://github.com/openfga/openfga/pull/2847)
     - Typesystem invalidate model with empty intersection and union. [#2865](https://github.com/openfga/openfga/pull/2865)
    +- Ordered iterator to iterate tuples correctly. [#2898](https://github.com/openfga/openfga/pull/2898)
     
     ## [1.11.2] - 2025-12-04
     ### Fixed
    
  • pkg/storage/tuple_iterators.go+10 1 modified
    @@ -497,7 +497,16 @@ IterateOverPending:
     			// If on every call to Head() we discarded, we would need to iterate twice over pending:
     			// one time to find the minIdx, and one time to move the corresponding iterators.
     			for c.mapper(head) == c.mapper(c.lastYielded) {
    -				head, err = iter.Next(ctx)
    +				// We want to advance to the new tuple. However, notice the "Next"'s returned
    +				// tuple is still "old".  Therefore, we will need to load it with "Head" in the
    +				// next step.
    +				_, err = iter.Next(ctx)
    +				if err == nil {
    +					// We need to fetch the iter's Head so that head is updated properly.
    +					// Otherwise, we will be looking at outdated data
    +					head, err = iter.Head(ctx)
    +				}
    +
     				if err != nil {
     					if errors.Is(err, ErrIteratorDone) {
     						iter.Stop()
    
  • pkg/storage/tuple_iterators_test.go+388 136 modified
    @@ -318,6 +318,284 @@ func TestCombinedIterator(t *testing.T) {
     	})
     }
     
    +type combinedIterTestCasesStruct = map[string]struct {
    +	iter     [][]*openfgav1.Tuple
    +	expected []string
    +}
    +
    +var combinedIterUserMapperTestCases = combinedIterTestCasesStruct{
    +	`removes_duplicates_within_iterator`: {
    +		iter: [][]*openfgav1.Tuple{
    +			{
    +				{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    +				{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    +			},
    +			{
    +				{Key: tuple.NewTupleKey("document:1", "2", "user:b")},
    +				{Key: tuple.NewTupleKey("document:1", "2", "user:c")},
    +			},
    +		},
    +		expected: []string{
    +			"user:a", "user:b", "user:c",
    +		},
    +	},
    +	`removes_duplicates_across_iterators_first_entry`: {
    +		iter: [][]*openfgav1.Tuple{
    +			{
    +				{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    +			},
    +			{
    +				{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    +			},
    +		},
    +		expected: []string{"user:a"},
    +	},
    +	`removes_duplicates_across_iterators_last_entry`: {
    +		iter: [][]*openfgav1.Tuple{
    +			{
    +				{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    +				{Key: tuple.NewTupleKey("document:1", "2", "user:b")},
    +			},
    +			{
    +				{Key: tuple.NewTupleKey("document:1", "2", "user:b")},
    +			},
    +		},
    +		expected: []string{
    +			"user:a", "user:b",
    +		},
    +	},
    +	`non_overlapping_elements_returns_all`: {
    +		iter: [][]*openfgav1.Tuple{
    +			{
    +				{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    +				{Key: tuple.NewTupleKey("document:1", "2", "user:c")},
    +				{Key: tuple.NewTupleKey("document:1", "2", "user:e")},
    +			},
    +			{
    +				{Key: tuple.NewTupleKey("document:1", "2", "user:b")},
    +				{Key: tuple.NewTupleKey("document:1", "2", "user:d")},
    +				{Key: tuple.NewTupleKey("document:1", "2", "user:f")},
    +			},
    +		},
    +		expected: []string{
    +			"user:a", "user:b", "user:c", "user:d", "user:e", "user:f",
    +		},
    +	},
    +	`overlapping_elements`: {
    +		iter: [][]*openfgav1.Tuple{
    +			{},
    +			{
    +				{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    +				{Key: tuple.NewTupleKey("document:2", "2", "user:a")},
    +				{Key: tuple.NewTupleKey("document:2", "2", "user:b")},
    +			},
    +		},
    +		expected: []string{"user:a", "user:b"},
    +	},
    +	`overlapping_elements_more_than_once`: {
    +		iter: [][]*openfgav1.Tuple{
    +			{},
    +			{
    +				{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    +				{Key: tuple.NewTupleKey("document:2", "2", "user:a")},
    +				{Key: tuple.NewTupleKey("document:3", "2", "user:a")},
    +				{Key: tuple.NewTupleKey("document:2", "2", "user:b")},
    +			},
    +		},
    +		expected: []string{"user:a", "user:b"},
    +	},
    +	`overlapping_elements_last_items_across_iter`: {
    +		iter: [][]*openfgav1.Tuple{
    +			{
    +				{Key: tuple.NewTupleKey("document:2", "2", "user:a")},
    +			},
    +			{
    +				{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    +				{Key: tuple.NewTupleKey("document:2", "2", "user:b")},
    +			},
    +		},
    +		expected: []string{"user:a", "user:b"},
    +	},
    +	`many_overlapping_elements`: {
    +		iter: [][]*openfgav1.Tuple{
    +			{
    +				{Key: tuple.NewTupleKey("document:2", "2", "user:c")},
    +				{Key: tuple.NewTupleKey("document:3", "2", "user:c")},
    +				{Key: tuple.NewTupleKey("document:4", "2", "user:c")},
    +			},
    +			{
    +				{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    +				{Key: tuple.NewTupleKey("document:2", "2", "user:a")},
    +				{Key: tuple.NewTupleKey("document:2", "2", "user:b")},
    +				{Key: tuple.NewTupleKey("document:3", "2", "user:b")},
    +				{Key: tuple.NewTupleKey("document:4", "2", "user:b")},
    +				{Key: tuple.NewTupleKey("document:5", "2", "user:b")},
    +				{Key: tuple.NewTupleKey("document:6", "2", "user:b")},
    +			},
    +		},
    +		expected: []string{"user:a", "user:b", "user:c"},
    +	},
    +
    +	`all_empty_iterators`: {
    +		iter: [][]*openfgav1.Tuple{
    +			{},
    +			{},
    +		},
    +		expected: []string{},
    +	},
    +	`one_empty_iterator`: {
    +		iter: [][]*openfgav1.Tuple{
    +			{
    +				{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    +			},
    +			{},
    +		},
    +		expected: []string{"user:a"},
    +	},
    +	`single_iterator`: {
    +		iter: [][]*openfgav1.Tuple{
    +			{
    +				{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    +				{Key: tuple.NewTupleKey("document:2", "2", "user:b")},
    +			},
    +		},
    +		expected: []string{"user:a", "user:b"},
    +	},
    +	`single_iterator_duplicate_user`: {
    +		iter: [][]*openfgav1.Tuple{
    +			{
    +				{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    +				{Key: tuple.NewTupleKey("document:2", "2", "user:b")},
    +				{Key: tuple.NewTupleKey("document:3", "2", "user:b")},
    +				{Key: tuple.NewTupleKey("document:4", "2", "user:b")},
    +				{Key: tuple.NewTupleKey("document:2", "2", "user:c")},
    +			},
    +		},
    +		expected: []string{"user:a", "user:b", "user:c"},
    +	},
    +	`single_iterator_duplicate_user_first_item`: {
    +		iter: [][]*openfgav1.Tuple{
    +			{
    +				{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    +				{Key: tuple.NewTupleKey("document:2", "2", "user:a")},
    +				{Key: tuple.NewTupleKey("document:3", "2", "user:b")},
    +				{Key: tuple.NewTupleKey("document:4", "2", "user:b")},
    +				{Key: tuple.NewTupleKey("document:2", "2", "user:c")},
    +			},
    +		},
    +		expected: []string{"user:a", "user:b", "user:c"},
    +	},
    +	`single_iterator_duplicate_user_last_item`: {
    +		iter: [][]*openfgav1.Tuple{
    +			{
    +				{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    +				{Key: tuple.NewTupleKey("document:2", "2", "user:a")},
    +				{Key: tuple.NewTupleKey("document:3", "2", "user:b")},
    +				{Key: tuple.NewTupleKey("document:4", "2", "user:b")},
    +				{Key: tuple.NewTupleKey("document:5", "2", "user:c")},
    +				{Key: tuple.NewTupleKey("document:6", "2", "user:c")},
    +			},
    +		},
    +		expected: []string{"user:a", "user:b", "user:c"},
    +	},
    +}
    +
    +var combinedIterObjectMapperTestCases = combinedIterTestCasesStruct{
    +	`iter_map_to_object`: {
    +		iter: [][]*openfgav1.Tuple{
    +			{
    +				{Key: tuple.NewTupleKey("document:1", "2", "user:*")},
    +				{Key: tuple.NewTupleKey("document:2", "2", "user:*")},
    +			},
    +			{
    +				{Key: tuple.NewTupleKey("document:2", "2", "user:a")},
    +				{Key: tuple.NewTupleKey("document:4", "2", "user:a")},
    +			},
    +		},
    +		expected: []string{
    +			"document:1", "document:2", "document:4",
    +		},
    +	},
    +	`same_object_different_rel`: {
    +		iter: [][]*openfgav1.Tuple{
    +			{
    +				{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    +				{Key: tuple.NewTupleKey("document:2", "2", "user:a")},
    +				{Key: tuple.NewTupleKey("document:2", "3", "user:a")},
    +				{Key: tuple.NewTupleKey("document:3", "2", "user:a")},
    +			},
    +			{
    +				{Key: tuple.NewTupleKey("document:1", "2", "user:*")},
    +				{Key: tuple.NewTupleKey("document:2", "2", "user:*")},
    +				{Key: tuple.NewTupleKey("document:4", "2", "user:*")},
    +			},
    +		},
    +		expected: []string{
    +			"document:1", "document:2", "document:3", "document:4",
    +		},
    +	},
    +	`single_iterator`: {
    +		iter: [][]*openfgav1.Tuple{
    +			{
    +				{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    +				{Key: tuple.NewTupleKey("document:2", "2", "user:b")},
    +			},
    +		},
    +		expected: []string{"document:1", "document:2"},
    +	},
    +	`single_iterator_duplicate_obj`: {
    +		iter: [][]*openfgav1.Tuple{
    +			{
    +				{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    +				{Key: tuple.NewTupleKey("document:2", "2", "user:b")},
    +				{Key: tuple.NewTupleKey("document:2", "2", "user:c")},
    +				{Key: tuple.NewTupleKey("document:2", "2", "user:d")},
    +				{Key: tuple.NewTupleKey("document:3", "2", "user:c")},
    +			},
    +		},
    +		expected: []string{"document:1", "document:2", "document:3"},
    +	},
    +	`single_iterator_duplicate_obj_first_item`: {
    +		iter: [][]*openfgav1.Tuple{
    +			{
    +				{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    +				{Key: tuple.NewTupleKey("document:1", "2", "user:b")},
    +				{Key: tuple.NewTupleKey("document:2", "2", "user:c")},
    +				{Key: tuple.NewTupleKey("document:2", "2", "user:d")},
    +				{Key: tuple.NewTupleKey("document:3", "2", "user:e")},
    +			},
    +		},
    +		expected: []string{"document:1", "document:2", "document:3"},
    +	},
    +	`single_iterator_duplicate_obj_last_item`: {
    +		iter: [][]*openfgav1.Tuple{
    +			{
    +				{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    +				{Key: tuple.NewTupleKey("document:1", "2", "user:b")},
    +				{Key: tuple.NewTupleKey("document:2", "2", "user:c")},
    +				{Key: tuple.NewTupleKey("document:2", "2", "user:d")},
    +				{Key: tuple.NewTupleKey("document:3", "2", "user:e")},
    +				{Key: tuple.NewTupleKey("document:3", "2", "user:*")},
    +			},
    +		},
    +		expected: []string{"document:1", "document:2", "document:3"},
    +	},
    +}
    +
    +var combinedTestCases = map[string]struct {
    +	mapper    TupleMapperFunc
    +	testcases combinedIterTestCasesStruct
    +}{
    +	"userMapper": {
    +		mapper:    UserMapper(),
    +		testcases: combinedIterUserMapperTestCases,
    +	},
    +	"objectMapper": {
    +		mapper:    ObjectMapper(),
    +		testcases: combinedIterObjectMapperTestCases,
    +	},
    +}
    +
     func TestOrderedCombinedIterator(t *testing.T) {
     	t.Run("Stop", func(t *testing.T) {
     		iter1 := NewStaticTupleIterator([]*openfgav1.Tuple{
    @@ -331,109 +609,37 @@ func TestOrderedCombinedIterator(t *testing.T) {
     	})
     
     	t.Run("Next", func(t *testing.T) {
    -		var testcases = map[string]struct {
    -			iter1    TupleIterator
    -			iter2    TupleIterator
    -			expected []*openfgav1.Tuple
    -		}{
    -			`removes_duplicates_within_iterator`: {
    -				iter1: NewStaticTupleIterator([]*openfgav1.Tuple{
    -					{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    -					{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    -				}),
    -				iter2: NewStaticTupleIterator([]*openfgav1.Tuple{
    -					{Key: tuple.NewTupleKey("document:1", "2", "user:b")},
    -					{Key: tuple.NewTupleKey("document:1", "2", "user:c")},
    -				}),
    -				expected: []*openfgav1.Tuple{
    -					{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    -					{Key: tuple.NewTupleKey("document:1", "2", "user:b")},
    -					{Key: tuple.NewTupleKey("document:1", "2", "user:c")},
    -				},
    -			},
    -			`removes_duplicates_across_iterators_first_entry`: {
    -				iter1: NewStaticTupleIterator([]*openfgav1.Tuple{
    -					{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    -				}),
    -				iter2: NewStaticTupleIterator([]*openfgav1.Tuple{
    -					{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    -				}),
    -				expected: []*openfgav1.Tuple{
    -					{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    -				},
    -			},
    -			`removes_duplicates_across_iterators_last_entry`: {
    -				iter1: NewStaticTupleIterator([]*openfgav1.Tuple{
    -					{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    -					{Key: tuple.NewTupleKey("document:1", "2", "user:b")},
    -				}),
    -				iter2: NewStaticTupleIterator([]*openfgav1.Tuple{
    -					{Key: tuple.NewTupleKey("document:1", "2", "user:b")},
    -				}),
    -				expected: []*openfgav1.Tuple{
    -					{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    -					{Key: tuple.NewTupleKey("document:1", "2", "user:b")},
    -				},
    -			},
    -			`non_overlapping_elements_returns_all`: {
    -				iter1: NewStaticTupleIterator([]*openfgav1.Tuple{
    -					{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    -					{Key: tuple.NewTupleKey("document:1", "2", "user:c")},
    -					{Key: tuple.NewTupleKey("document:1", "2", "user:e")},
    -				}),
    -				iter2: NewStaticTupleIterator([]*openfgav1.Tuple{
    -					{Key: tuple.NewTupleKey("document:1", "2", "user:b")},
    -					{Key: tuple.NewTupleKey("document:1", "2", "user:d")},
    -					{Key: tuple.NewTupleKey("document:1", "2", "user:f")},
    -				}),
    -				expected: []*openfgav1.Tuple{
    -					{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    -					{Key: tuple.NewTupleKey("document:1", "2", "user:b")},
    -					{Key: tuple.NewTupleKey("document:1", "2", "user:c")},
    -					{Key: tuple.NewTupleKey("document:1", "2", "user:d")},
    -					{Key: tuple.NewTupleKey("document:1", "2", "user:e")},
    -					{Key: tuple.NewTupleKey("document:1", "2", "user:f")},
    -				},
    -			},
    -			`all_empty_iterators`: {
    -				iter1:    NewStaticTupleIterator([]*openfgav1.Tuple{}),
    -				iter2:    NewStaticTupleIterator([]*openfgav1.Tuple{}),
    -				expected: []*openfgav1.Tuple{},
    -			},
    -			`one_empty_iterator`: {
    -				iter1: NewStaticTupleIterator([]*openfgav1.Tuple{
    -					{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    -				}),
    -				iter2: NewStaticTupleIterator([]*openfgav1.Tuple{}),
    -				expected: []*openfgav1.Tuple{
    -					{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    -				},
    -			},
    -		}
    -
    -		for name, tc := range testcases {
    +		for name, combinedTest := range combinedTestCases {
     			t.Run(name, func(t *testing.T) {
    -				iter := NewOrderedCombinedIterator(UserMapper(), tc.iter1, tc.iter2)
    -				t.Cleanup(func() {
    -					iter.Stop()
    -					require.Empty(t, iter.pending)
    -				})
    +				for name, tc := range combinedTest.testcases {
    +					t.Run(name, func(t *testing.T) {
    +						mapper := combinedTest.mapper
     
    -				gotItems := make([]*openfgav1.Tuple, 0)
    -				for {
    -					got, err := iter.Next(context.Background())
    -					if err != nil {
    -						if errors.Is(err, ErrIteratorDone) {
    -							break
    +						var iters []TupleIterator
    +						for _, curIter := range tc.iter {
    +							iters = append(iters, NewStaticTupleIterator(curIter))
    +						}
    +						iter := NewOrderedCombinedIterator(mapper, iters...)
    +						t.Cleanup(func() {
    +							iter.Stop()
    +							require.Empty(t, iter.pending)
    +						})
    +
    +						gotItems := make([]string, 0)
    +						for {
    +							got, err := iter.Next(context.Background())
    +							if err != nil {
    +								if errors.Is(err, ErrIteratorDone) {
    +									break
    +								}
    +								require.Fail(t, "no error was expected")
    +							}
    +							require.NotNil(t, got)
    +							gotItems = append(gotItems, mapper(got))
     						}
    -						require.Fail(t, "no error was expected")
    -					}
    -					require.NotNil(t, got)
    -					gotItems = append(gotItems, got)
    -				}
     
    -				if diff := cmp.Diff(tc.expected, gotItems, protocmp.Transform()); diff != "" {
    -					t.Errorf("mismatch (-want +got):\n%s", diff)
    +						require.Equal(t, tc.expected, gotItems)
    +					})
     				}
     			})
     		}
    @@ -484,46 +690,92 @@ func TestOrderedCombinedIterator(t *testing.T) {
     			}
     		})
     		t.Run("head_and_next_interleaved", func(t *testing.T) {
    -			iter1 := NewStaticTupleIterator([]*openfgav1.Tuple{
    -				{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    -				{Key: tuple.NewTupleKey("document:1", "2", "user:b")},
    -			})
    -			iter2 := NewStaticTupleIterator([]*openfgav1.Tuple{
    -				{Key: tuple.NewTupleKey("document:1", "2", "user:c")},
    -				{Key: tuple.NewTupleKey("document:1", "2", "user:d")},
    -			})
    -			expected := []*openfgav1.Tuple{
    -				{Key: tuple.NewTupleKey("document:1", "2", "user:a")},
    -				{Key: tuple.NewTupleKey("document:1", "2", "user:b")},
    -				{Key: tuple.NewTupleKey("document:1", "2", "user:c")},
    -				{Key: tuple.NewTupleKey("document:1", "2", "user:d")},
    -			}
    -
    -			iter := NewOrderedCombinedIterator(UserMapper(), iter1, iter2)
    -			t.Cleanup(iter.Stop)
    +			for name, combinedTest := range combinedTestCases {
    +				t.Run(name, func(t *testing.T) {
    +					for name, tc := range combinedTest.testcases {
    +						t.Run(name, func(t *testing.T) {
    +							mapper := combinedTest.mapper
    +
    +							var iters []TupleIterator
    +							for _, curIter := range tc.iter {
    +								iters = append(iters, NewStaticTupleIterator(curIter))
    +							}
    +							iter := NewOrderedCombinedIterator(mapper, iters...)
    +							t.Cleanup(iter.Stop)
    +
    +							var errorFromHead error
    +							gotItems := make([]string, 0)
    +							for {
    +								gotHead, err := iter.Head(context.Background())
    +								if err != nil {
    +									require.ErrorIs(t, err, ErrIteratorDone)
    +									errorFromHead = err
    +								}
    +								gotNext, err := iter.Next(context.Background())
    +								if err != nil {
    +									require.Equal(t, errorFromHead, err)
    +									break
    +								}
    +								require.NotNil(t, gotNext)
    +								if diff := cmp.Diff(gotHead, gotNext, protocmp.Transform()); diff != "" {
    +									t.Errorf("mismatch in result of Next compared to Head (-want +got):\n%s", diff)
    +								}
    +								gotItems = append(gotItems, mapper(gotNext))
    +							}
     
    -			var errorFromHead error
    -			gotItems := make([]*openfgav1.Tuple, 0)
    -			for {
    -				gotHead, err := iter.Head(context.Background())
    -				if err != nil {
    -					require.ErrorIs(t, err, ErrIteratorDone)
    -					errorFromHead = err
    -				}
    -				gotNext, err := iter.Next(context.Background())
    -				if err != nil {
    -					require.Equal(t, errorFromHead, err)
    -					break
    -				}
    -				require.NotNil(t, gotNext)
    -				if diff := cmp.Diff(gotHead, gotNext, protocmp.Transform()); diff != "" {
    -					t.Errorf("mismatch in result of Next compared to Head (-want +got):\n%s", diff)
    -				}
    -				gotItems = append(gotItems, gotNext)
    +							require.Equal(t, tc.expected, gotItems)
    +						})
    +					}
    +				})
     			}
    +		})
    +		t.Run("head_head_and_next_interleaved", func(t *testing.T) {
    +			for name, combinedTest := range combinedTestCases {
    +				t.Run(name, func(t *testing.T) {
    +					for name, tc := range combinedTest.testcases {
    +						t.Run(name, func(t *testing.T) {
    +							mapper := combinedTest.mapper
    +
    +							var iters []TupleIterator
    +							for _, curIter := range tc.iter {
    +								iters = append(iters, NewStaticTupleIterator(curIter))
    +							}
    +							iter := NewOrderedCombinedIterator(mapper, iters...)
    +							t.Cleanup(iter.Stop)
    +
    +							var errorFromHead error
    +							gotItems := make([]string, 0)
    +							for {
    +								gotHead, err := iter.Head(context.Background())
    +								if err != nil {
    +									require.ErrorIs(t, err, ErrIteratorDone)
    +									errorFromHead = err
    +								}
    +								var newHead *openfgav1.Tuple
    +								newHead, err = iter.Head(context.Background())
    +								if err != nil {
    +									require.Equal(t, errorFromHead, err)
    +									require.Nil(t, newHead)
    +								} else {
    +									require.Equal(t, gotHead, newHead)
    +								}
    +
    +								gotNext, err := iter.Next(context.Background())
    +								if err != nil {
    +									require.Equal(t, errorFromHead, err)
    +									break
    +								}
    +								require.NotNil(t, gotNext)
    +								if diff := cmp.Diff(gotHead, gotNext, protocmp.Transform()); diff != "" {
    +									t.Errorf("mismatch in result of Next compared to Head (-want +got):\n%s", diff)
    +								}
    +								gotItems = append(gotItems, mapper(gotNext))
    +							}
     
    -			if diff := cmp.Diff(expected, gotItems, protocmp.Transform()); diff != "" {
    -				t.Errorf("mismatch in result of Next (-want +got):\n%s", diff)
    +							require.Equal(t, tc.expected, gotItems)
    +						})
    +					}
    +				})
     			}
     		})
     	})
    
  • tests/check/check.go+1 0 modified
    @@ -1108,6 +1108,7 @@ type usersets-user
         define userset_cond_to_computed_cond: [directs-user#computed_cond with xcond]
         define userset_cond_to_computed_wild: [directs-user#computed_wild with xcond]
         define userset_cond_to_computed_wild_cond: [directs-user#computed_wild_cond with xcond]
    +    define userset_direct_and_direct_wild: [directs-user#direct_and_direct_wild]
         define userset_to_or_computed: [directs-user#or_computed]
         define userset_to_or_computed_no_cond: [directs-user#or_computed_no_cond]
         define userset_to_butnot_computed: [directs-user#butnot_computed]
    
  • tests/check/check_userset.go+69 0 modified
    @@ -392,6 +392,75 @@ var usersetCompleteTestingModelTest = []*stage{
     			},
     		},
     	},
    +	{
    +		Name: "usersets_userset_direct_and_direct_wild",
    +		Tuples: []*openfgav1.TupleKey{
    +			{Object: "directs-user:uuudadw_1", Relation: "direct_and_direct_wild", User: "user:uuudadw_1"},
    +			{Object: "usersets-user:uuudadw_1", Relation: "userset_direct_and_direct_wild", User: "directs-user:uuudadw_1#direct_and_direct_wild"},
    +
    +			{Object: "directs-user:uuudadw_2", Relation: "direct_and_direct_wild", User: "user:uuudadw_2"},
    +			{Object: "directs-user:uuudadw_2", Relation: "direct_and_direct_wild", User: "user:*"},
    +			{Object: "directs-user:uuudadw_2a", Relation: "direct_and_direct_wild", User: "user:*"},
    +
    +			// Need different order because datastore processes order differently (whether wildcard user is first or last)
    +			{Object: "directs-user:uuudadw_3", Relation: "direct_and_direct_wild", User: "user:uuudadw_3"},
    +			{Object: "directs-user:uuudadw_3", Relation: "direct_and_direct_wild", User: "user:*"},
    +			{Object: "directs-user:uuudadw_3a", Relation: "direct_and_direct_wild", User: "user:uuudadw_3"},
    +
    +			{Object: "usersets-user:uuudadw_2", Relation: "userset_direct_and_direct_wild", User: "directs-user:uuudadw_2#direct_and_direct_wild"},
    +			{Object: "usersets-user:uuudadw_2a", Relation: "userset_direct_and_direct_wild", User: "directs-user:uuudadw_2a#direct_and_direct_wild"},
    +
    +			{Object: "usersets-user:uuudadw_3", Relation: "userset_direct_and_direct_wild", User: "directs-user:uuudadw_3#direct_and_direct_wild"},
    +			{Object: "usersets-user:uuudadw_3a", Relation: "userset_direct_and_direct_wild", User: "directs-user:uuudadw_3a#direct_and_direct_wild"},
    +		},
    +		CheckAssertions: []*checktest.Assertion{
    +			{
    +				Name:        "valid_user",
    +				Tuple:       &openfgav1.TupleKey{Object: "usersets-user:uuudadw_1", Relation: "userset_direct_and_direct_wild", User: "user:uuudadw_1"},
    +				Expectation: true,
    +			},
    +			{
    +				Name:        "valid_user_with_wildcard",
    +				Tuple:       &openfgav1.TupleKey{Object: "usersets-user:uuudadw_2", Relation: "userset_direct_and_direct_wild", User: "user:uuudadw_2"},
    +				Expectation: true,
    +			},
    +			{
    +				Name:        "wildcard_user_with_wildcard",
    +				Tuple:       &openfgav1.TupleKey{Object: "usersets-user:uuudadw_2", Relation: "userset_direct_and_direct_wild", User: "user:uuudadw_wildcard"},
    +				Expectation: true,
    +			},
    +			{
    +				Name:        "same_user_different_group",
    +				Tuple:       &openfgav1.TupleKey{Object: "usersets-user:uuudadw_2a", Relation: "userset_direct_and_direct_wild", User: "user:uuudadw_2"},
    +				Expectation: true,
    +			},
    +			{
    +				Name:        "wildcard_non_wildcard_group",
    +				Tuple:       &openfgav1.TupleKey{Object: "usersets-user:uuudadw_2a", Relation: "userset_direct_and_direct_wild", User: "user:uuudadw_wildcard"},
    +				Expectation: true,
    +			},
    +			{
    +				Name:        "order_3_valid_user_with_wildcard",
    +				Tuple:       &openfgav1.TupleKey{Object: "usersets-user:uuudadw_3", Relation: "userset_direct_and_direct_wild", User: "user:uuudadw_2"},
    +				Expectation: true,
    +			},
    +			{
    +				Name:        "order_3_wildcard_user_with_wildcard",
    +				Tuple:       &openfgav1.TupleKey{Object: "usersets-user:uuudadw_3", Relation: "userset_direct_and_direct_wild", User: "user:uuudadw_wildcard"},
    +				Expectation: true,
    +			},
    +			{
    +				Name:        "order_3_same_user_different_group",
    +				Tuple:       &openfgav1.TupleKey{Object: "usersets-user:uuudadw_3a", Relation: "userset_direct_and_direct_wild", User: "user:uuudadw_3"},
    +				Expectation: true,
    +			},
    +			{
    +				Name:        "order_3_wildcard_non_wildcard_group",
    +				Tuple:       &openfgav1.TupleKey{Object: "usersets-user:uuudadw_3a", Relation: "userset_direct_and_direct_wild", User: "user:uuudadw_wildcard"},
    +				Expectation: false,
    +			},
    +		},
    +	},
     	{
     		Name: "usersets_userset_to_or_computed",
     		Tuples: []*openfgav1.TupleKey{
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.