OpenFGA Improper Policy Enforcement
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.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/openfga/openfgaGo | >= 1.8.5, < 1.11.3 | 1.11.3 |
Affected products
1Patches
11bb5eddf4a3dfix: order iterator to advance tuples correctly (#2898)
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- github.com/advisories/GHSA-jq9f-gm9w-rwm9ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-24851ghsaADVISORY
- github.com/openfga/openfga/commit/1bb5eddf4a3d2fc718aab7914b8f9a1200d2f7eeghsaWEB
- github.com/openfga/openfga/releases/tag/v1.11.3ghsax_refsource_MISCWEB
- github.com/openfga/openfga/security/advisories/GHSA-jq9f-gm9w-rwm9ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.