Multiple caveats on resources of the same type can result in no permission when permission is expected
Description
spicedb is an Open Source, Google Zanzibar-inspired permissions database to enable fine-grained authorization for customer applications. Multiple caveats over the same indirect subject type on the same relation can result in no permission being returned when permission is expected. If the resource has multiple groups, and each group is caveated, it is possible for the returned permission to be "no permission" when permission is expected. Permission is returned as NO_PERMISSION when PERMISSION is expected on the CheckPermission API. This issue has been addressed in release version 1.35.3. Users are advised to upgrade. Users unable to upgrade should not use caveats or avoid the use of caveats on an indirect subject type with multiple entries.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/authzed/spicedbGo | < 1.35.3 | 1.35.3 |
Affected products
1Patches
2d4ef8e1dbce1Merge pull request #2027 from josephschorr/caveat-check-fix
5 files changed · +161 −11
internal/dispatch/graph/check_test.go+124 −9 modified@@ -288,13 +288,14 @@ func TestCheckMetadata(t *testing.T) { func TestCheckPermissionOverSchema(t *testing.T) { testCases := []struct { - name string - schema string - relationships []*core.RelationTuple - resource *core.ObjectAndRelation - subject *core.ObjectAndRelation - expectedPermissionship v1.ResourceCheckResult_Membership - expectedCaveat *core.CaveatExpression + name string + schema string + relationships []*core.RelationTuple + resource *core.ObjectAndRelation + subject *core.ObjectAndRelation + expectedPermissionship v1.ResourceCheckResult_Membership + expectedCaveat *core.CaveatExpression + alternativeExpectedCaveat *core.CaveatExpression }{ { "basic union", @@ -312,6 +313,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_MEMBER, nil, + nil, }, { "basic intersection", @@ -330,6 +332,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_MEMBER, nil, + nil, }, { "basic exclusion", @@ -347,6 +350,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_MEMBER, nil, + nil, }, { "basic union, multiple branches", @@ -365,6 +369,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_MEMBER, nil, + nil, }, { "basic union no permission", @@ -380,6 +385,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_NOT_MEMBER, nil, + nil, }, { "basic intersection no permission", @@ -397,6 +403,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_NOT_MEMBER, nil, + nil, }, { "basic exclusion no permission", @@ -415,6 +422,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_NOT_MEMBER, nil, + nil, }, { "exclusion with multiple branches", @@ -441,6 +449,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_MEMBER, nil, + nil, }, { "intersection with multiple branches", @@ -467,6 +476,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_MEMBER, nil, + nil, }, { "exclusion with multiple branches no permission", @@ -494,6 +504,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_NOT_MEMBER, nil, + nil, }, { "intersection with multiple branches no permission", @@ -519,6 +530,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_NOT_MEMBER, nil, + nil, }, { "basic arrow", @@ -541,6 +553,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_MEMBER, nil, + nil, }, { "basic any arrow", @@ -563,6 +576,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_MEMBER, nil, + nil, }, { "basic all arrow negative", @@ -585,6 +599,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_NOT_MEMBER, nil, + nil, }, { "basic all arrow positive", @@ -608,6 +623,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_MEMBER, nil, + nil, }, { "basic all arrow positive with different types", @@ -635,6 +651,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_MEMBER, nil, + nil, }, { "basic all arrow negative over different types", @@ -663,6 +680,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_NOT_MEMBER, nil, + nil, }, { "basic all arrow positive over different types", @@ -692,6 +710,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_MEMBER, nil, + nil, }, { "all arrow for single org", @@ -713,6 +732,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_MEMBER, nil, + nil, }, { "all arrow for no orgs", @@ -733,6 +753,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_NOT_MEMBER, nil, + nil, }, { "view_by_all negative", @@ -766,6 +787,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "fred", "..."), v1.ResourceCheckResult_NOT_MEMBER, nil, + nil, }, { "view_by_any positive", @@ -801,6 +823,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "fred", "..."), v1.ResourceCheckResult_MEMBER, nil, + nil, }, { "view_by_any positive directly", @@ -836,6 +859,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "rachel", "..."), v1.ResourceCheckResult_MEMBER, nil, + nil, }, { "caveated intersection arrow", @@ -862,6 +886,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_CAVEATED_MEMBER, caveatAndCtx("somecaveat", nil), + nil, }, { "intersection arrow with caveated member", @@ -888,6 +913,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_CAVEATED_MEMBER, caveatAndCtx("somecaveat", nil), + nil, }, { "caveated intersection arrow with caveated member", @@ -914,6 +940,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_CAVEATED_MEMBER, caveatAndCtx("somecaveat", nil), + nil, }, { "caveated intersection arrow with caveated member, different context", @@ -947,6 +974,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { caveatAndCtx("anothercaveat", map[string]any{"someparam": int64(43)}), caveatAndCtx("somecaveat", map[string]any{"someparam": int64(42)}), ), + nil, }, { "caveated intersection arrow with multiple caveated branches", @@ -978,8 +1006,8 @@ func TestCheckPermissionOverSchema(t *testing.T) { caveatAndCtx("somecaveat", map[string]any{"someparam": int64(41)}), caveatAndCtx("somecaveat", map[string]any{"someparam": int64(42)}), ), + nil, }, - { "caveated intersection arrow with multiple caveated members", `definition user {} @@ -1010,6 +1038,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { caveatAndCtx("somecaveat", map[string]any{"someparam": int64(41)}), caveatAndCtx("somecaveat", map[string]any{"someparam": int64(42)}), ), + nil, }, { "caveated intersection arrow with one caveated branch", @@ -1038,6 +1067,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_CAVEATED_MEMBER, caveatAndCtx("somecaveat", map[string]any{"someparam": int64(42)}), + nil, }, { "caveated intersection arrow with one caveated member", @@ -1066,6 +1096,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_CAVEATED_MEMBER, caveatAndCtx("somecaveat", map[string]any{"someparam": int64(42)}), + nil, }, { "caveated intersection arrow multiple paths to the same subject", @@ -1093,6 +1124,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_CAVEATED_MEMBER, caveatAndCtx("somecaveat", nil), + nil, }, { "recursive all arrow positive result", @@ -1129,6 +1161,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "fred", "..."), v1.ResourceCheckResult_MEMBER, nil, + nil, }, { "recursive all arrow negative result", @@ -1165,6 +1198,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_NOT_MEMBER, nil, + nil, }, { "recursive all arrow negative result due to recursion missing a folder", @@ -1202,6 +1236,79 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "fred", "..."), v1.ResourceCheckResult_NOT_MEMBER, nil, + nil, + }, + { + "caveated over multiple branches", + ` + caveat somecaveat(somevalue int) { + somevalue == 42 + } + + definition user {} + + definition role { + relation member: user + } + + definition resource { + relation viewer: role#member with somecaveat + permission view = viewer + } + `, + []*core.RelationTuple{ + tuple.MustParse(`role:firstrole#member@user:tom[somecaveat:{"somevalue":40}]`), + tuple.MustParse(`role:secondrole#member@user:tom[somecaveat:{"somevalue":42}]`), + tuple.MustParse(`resource:doc1#viewer@role:firstrole#member`), + tuple.MustParse(`resource:doc1#viewer@role:secondrole#member`), + }, + ONR("resource", "doc1", "view"), + ONR("user", "tom", "..."), + v1.ResourceCheckResult_CAVEATED_MEMBER, + caveatOr( + caveatAndCtx("somecaveat", map[string]any{"somevalue": int64(40)}), + caveatAndCtx("somecaveat", map[string]any{"somevalue": int64(42)}), + ), + caveatOr( + caveatAndCtx("somecaveat", map[string]any{"somevalue": int64(42)}), + caveatAndCtx("somecaveat", map[string]any{"somevalue": int64(40)}), + ), + }, + { + "caveated over multiple branches reversed", + ` + caveat somecaveat(somevalue int) { + somevalue == 42 + } + + definition user {} + + definition role { + relation member: user + } + + definition resource { + relation viewer: role#member with somecaveat + permission view = viewer + } + `, + []*core.RelationTuple{ + tuple.MustParse(`role:secondrole#member@user:tom[somecaveat:{"somevalue":42}]`), + tuple.MustParse(`role:firstrole#member@user:tom[somecaveat:{"somevalue":40}]`), + tuple.MustParse(`resource:doc1#viewer@role:secondrole#member`), + tuple.MustParse(`resource:doc1#viewer@role:firstrole#member`), + }, + ONR("resource", "doc1", "view"), + ONR("user", "tom", "..."), + v1.ResourceCheckResult_CAVEATED_MEMBER, + caveatOr( + caveatAndCtx("somecaveat", map[string]any{"somevalue": int64(40)}), + caveatAndCtx("somecaveat", map[string]any{"somevalue": int64(42)}), + ), + caveatOr( + caveatAndCtx("somecaveat", map[string]any{"somevalue": int64(42)}), + caveatAndCtx("somecaveat", map[string]any{"somevalue": int64(40)}), + ), }, } @@ -1239,10 +1346,18 @@ func TestCheckPermissionOverSchema(t *testing.T) { require.Equal(tc.expectedPermissionship, membership) - if tc.expectedCaveat != nil { + if tc.expectedCaveat != nil && tc.alternativeExpectedCaveat == nil { require.NotEmpty(resp.ResultsByResourceId[tc.resource.ObjectId].Expression) testutil.RequireProtoEqual(t, tc.expectedCaveat, resp.ResultsByResourceId[tc.resource.ObjectId].Expression, "mismatch in caveat") } + + if tc.expectedCaveat != nil && tc.alternativeExpectedCaveat != nil { + require.NotEmpty(resp.ResultsByResourceId[tc.resource.ObjectId].Expression) + + if testutil.AreProtoEqual(tc.expectedCaveat, resp.ResultsByResourceId[tc.resource.ObjectId].Expression, "mismatch in caveat") != nil { + testutil.RequireProtoEqual(t, tc.alternativeExpectedCaveat, resp.ResultsByResourceId[tc.resource.ObjectId].Expression, "mismatch in caveat") + } + } }) } }
internal/dispatch/graph/lookupsubjects_test.go+1 −0 modified@@ -28,6 +28,7 @@ var ( caveatAndCtx = caveats.MustCaveatExprForTestingWithContext caveatAnd = caveats.And caveatInvert = caveats.Invert + caveatOr = caveats.Or ) func TestSimpleLookupSubjects(t *testing.T) {
internal/graph/check.go+12 −1 modified@@ -466,6 +466,7 @@ func (cc *ConcurrentChecker) checkDirect(ctx context.Context, crc currentRequest // Find the subjects over which to dispatch. subjectsToDispatch := tuple.NewONRByTypeSet() relationshipsBySubjectONR := mapz.NewMultiMap[string, *core.RelationTuple]() + hasCaveats := false for tpl := it.Next(); tpl != nil; tpl = it.Next() { if it.Err() != nil { @@ -479,6 +480,9 @@ func (cc *ConcurrentChecker) checkDirect(ctx context.Context, crc currentRequest subjectsToDispatch.Add(tpl.Subject) relationshipsBySubjectONR.Add(tuple.StringONR(tpl.Subject), tpl) + if tpl.Caveat != nil && tpl.Caveat.CaveatName != "" { + hasCaveats = true + } } it.Close() @@ -499,14 +503,21 @@ func (cc *ConcurrentChecker) checkDirect(ctx context.Context, crc currentRequest dispatchChunkCountHistogram.Observe(chunkCount) }) + // If there are caveats on the incoming relationships, then we must require all results to be + // found, as we need to ensure that all caveats are used for building the final expression. + resultsSetting := crc.resultsSetting + if hasCaveats { + resultsSetting = v1.DispatchCheckRequest_REQUIRE_ALL_RESULTS + } + // Dispatch and map to the associated resource ID(s). result := union(ctx, crc, toDispatch, func(ctx context.Context, crc currentRequestContext, dd directDispatch) CheckResult { childResult := cc.dispatch(ctx, crc, ValidatedCheckRequest{ &v1.DispatchCheckRequest{ ResourceRelation: dd.resourceType, ResourceIds: dd.resourceIds, Subject: crc.parentReq.Subject, - ResultsSetting: crc.resultsSetting, + ResultsSetting: resultsSetting, Metadata: decrementDepth(crc.parentReq.Metadata), Debug: crc.parentReq.Debug,
internal/services/integrationtesting/testconfigs/caveatmultiplebranchessamerel.yaml+23 −0 added@@ -0,0 +1,23 @@ +schema: >- + definition user {} + + caveat write_limit(limit uint, count uint) { + count < limit + } + + definition role { + relation member: user + } + + definition database { + relation writer: role#member with write_limit + permission write = writer + } +relationships: |- + database:listings#writer@role:default#member[write_limit:{"limit":2}] + database:listings#writer@role:premium#member[write_limit:{"limit":4}] + role:default#member@user:bob + role:premium#member@user:bob +assertions: + assertTrue: + - 'database:listings#write@user:bob with {"count":3}'
pkg/tuple/tuple.go+1 −1 modified@@ -175,7 +175,7 @@ func MustParse(tpl string) *core.RelationTuple { if parsed := Parse(tpl); parsed != nil { return parsed } - panic("failed to parse tuple") + panic("failed to parse tuple: " + tpl) } // Parse unmarshals the string form of a Tuple and returns nil if there is a
20855de75812Ensure all resources are returned for relation check when caveats are specified
5 files changed · +161 −11
internal/dispatch/graph/check_test.go+124 −9 modified@@ -288,13 +288,14 @@ func TestCheckMetadata(t *testing.T) { func TestCheckPermissionOverSchema(t *testing.T) { testCases := []struct { - name string - schema string - relationships []*core.RelationTuple - resource *core.ObjectAndRelation - subject *core.ObjectAndRelation - expectedPermissionship v1.ResourceCheckResult_Membership - expectedCaveat *core.CaveatExpression + name string + schema string + relationships []*core.RelationTuple + resource *core.ObjectAndRelation + subject *core.ObjectAndRelation + expectedPermissionship v1.ResourceCheckResult_Membership + expectedCaveat *core.CaveatExpression + alternativeExpectedCaveat *core.CaveatExpression }{ { "basic union", @@ -312,6 +313,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_MEMBER, nil, + nil, }, { "basic intersection", @@ -330,6 +332,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_MEMBER, nil, + nil, }, { "basic exclusion", @@ -347,6 +350,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_MEMBER, nil, + nil, }, { "basic union, multiple branches", @@ -365,6 +369,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_MEMBER, nil, + nil, }, { "basic union no permission", @@ -380,6 +385,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_NOT_MEMBER, nil, + nil, }, { "basic intersection no permission", @@ -397,6 +403,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_NOT_MEMBER, nil, + nil, }, { "basic exclusion no permission", @@ -415,6 +422,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_NOT_MEMBER, nil, + nil, }, { "exclusion with multiple branches", @@ -441,6 +449,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_MEMBER, nil, + nil, }, { "intersection with multiple branches", @@ -467,6 +476,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_MEMBER, nil, + nil, }, { "exclusion with multiple branches no permission", @@ -494,6 +504,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_NOT_MEMBER, nil, + nil, }, { "intersection with multiple branches no permission", @@ -519,6 +530,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_NOT_MEMBER, nil, + nil, }, { "basic arrow", @@ -541,6 +553,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_MEMBER, nil, + nil, }, { "basic any arrow", @@ -563,6 +576,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_MEMBER, nil, + nil, }, { "basic all arrow negative", @@ -585,6 +599,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_NOT_MEMBER, nil, + nil, }, { "basic all arrow positive", @@ -608,6 +623,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_MEMBER, nil, + nil, }, { "basic all arrow positive with different types", @@ -635,6 +651,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_MEMBER, nil, + nil, }, { "basic all arrow negative over different types", @@ -663,6 +680,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_NOT_MEMBER, nil, + nil, }, { "basic all arrow positive over different types", @@ -692,6 +710,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_MEMBER, nil, + nil, }, { "all arrow for single org", @@ -713,6 +732,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_MEMBER, nil, + nil, }, { "all arrow for no orgs", @@ -733,6 +753,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_NOT_MEMBER, nil, + nil, }, { "view_by_all negative", @@ -766,6 +787,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "fred", "..."), v1.ResourceCheckResult_NOT_MEMBER, nil, + nil, }, { "view_by_any positive", @@ -801,6 +823,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "fred", "..."), v1.ResourceCheckResult_MEMBER, nil, + nil, }, { "view_by_any positive directly", @@ -836,6 +859,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "rachel", "..."), v1.ResourceCheckResult_MEMBER, nil, + nil, }, { "caveated intersection arrow", @@ -862,6 +886,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_CAVEATED_MEMBER, caveatAndCtx("somecaveat", nil), + nil, }, { "intersection arrow with caveated member", @@ -888,6 +913,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_CAVEATED_MEMBER, caveatAndCtx("somecaveat", nil), + nil, }, { "caveated intersection arrow with caveated member", @@ -914,6 +940,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_CAVEATED_MEMBER, caveatAndCtx("somecaveat", nil), + nil, }, { "caveated intersection arrow with caveated member, different context", @@ -947,6 +974,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { caveatAndCtx("anothercaveat", map[string]any{"someparam": int64(43)}), caveatAndCtx("somecaveat", map[string]any{"someparam": int64(42)}), ), + nil, }, { "caveated intersection arrow with multiple caveated branches", @@ -978,8 +1006,8 @@ func TestCheckPermissionOverSchema(t *testing.T) { caveatAndCtx("somecaveat", map[string]any{"someparam": int64(41)}), caveatAndCtx("somecaveat", map[string]any{"someparam": int64(42)}), ), + nil, }, - { "caveated intersection arrow with multiple caveated members", `definition user {} @@ -1010,6 +1038,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { caveatAndCtx("somecaveat", map[string]any{"someparam": int64(41)}), caveatAndCtx("somecaveat", map[string]any{"someparam": int64(42)}), ), + nil, }, { "caveated intersection arrow with one caveated branch", @@ -1038,6 +1067,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_CAVEATED_MEMBER, caveatAndCtx("somecaveat", map[string]any{"someparam": int64(42)}), + nil, }, { "caveated intersection arrow with one caveated member", @@ -1066,6 +1096,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_CAVEATED_MEMBER, caveatAndCtx("somecaveat", map[string]any{"someparam": int64(42)}), + nil, }, { "caveated intersection arrow multiple paths to the same subject", @@ -1093,6 +1124,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_CAVEATED_MEMBER, caveatAndCtx("somecaveat", nil), + nil, }, { "recursive all arrow positive result", @@ -1129,6 +1161,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "fred", "..."), v1.ResourceCheckResult_MEMBER, nil, + nil, }, { "recursive all arrow negative result", @@ -1165,6 +1198,7 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "tom", "..."), v1.ResourceCheckResult_NOT_MEMBER, nil, + nil, }, { "recursive all arrow negative result due to recursion missing a folder", @@ -1202,6 +1236,79 @@ func TestCheckPermissionOverSchema(t *testing.T) { ONR("user", "fred", "..."), v1.ResourceCheckResult_NOT_MEMBER, nil, + nil, + }, + { + "caveated over multiple branches", + ` + caveat somecaveat(somevalue int) { + somevalue == 42 + } + + definition user {} + + definition role { + relation member: user + } + + definition resource { + relation viewer: role#member with somecaveat + permission view = viewer + } + `, + []*core.RelationTuple{ + tuple.MustParse(`role:firstrole#member@user:tom[somecaveat:{"somevalue":40}]`), + tuple.MustParse(`role:secondrole#member@user:tom[somecaveat:{"somevalue":42}]`), + tuple.MustParse(`resource:doc1#viewer@role:firstrole#member`), + tuple.MustParse(`resource:doc1#viewer@role:secondrole#member`), + }, + ONR("resource", "doc1", "view"), + ONR("user", "tom", "..."), + v1.ResourceCheckResult_CAVEATED_MEMBER, + caveatOr( + caveatAndCtx("somecaveat", map[string]any{"somevalue": int64(40)}), + caveatAndCtx("somecaveat", map[string]any{"somevalue": int64(42)}), + ), + caveatOr( + caveatAndCtx("somecaveat", map[string]any{"somevalue": int64(42)}), + caveatAndCtx("somecaveat", map[string]any{"somevalue": int64(40)}), + ), + }, + { + "caveated over multiple branches reversed", + ` + caveat somecaveat(somevalue int) { + somevalue == 42 + } + + definition user {} + + definition role { + relation member: user + } + + definition resource { + relation viewer: role#member with somecaveat + permission view = viewer + } + `, + []*core.RelationTuple{ + tuple.MustParse(`role:secondrole#member@user:tom[somecaveat:{"somevalue":42}]`), + tuple.MustParse(`role:firstrole#member@user:tom[somecaveat:{"somevalue":40}]`), + tuple.MustParse(`resource:doc1#viewer@role:secondrole#member`), + tuple.MustParse(`resource:doc1#viewer@role:firstrole#member`), + }, + ONR("resource", "doc1", "view"), + ONR("user", "tom", "..."), + v1.ResourceCheckResult_CAVEATED_MEMBER, + caveatOr( + caveatAndCtx("somecaveat", map[string]any{"somevalue": int64(40)}), + caveatAndCtx("somecaveat", map[string]any{"somevalue": int64(42)}), + ), + caveatOr( + caveatAndCtx("somecaveat", map[string]any{"somevalue": int64(42)}), + caveatAndCtx("somecaveat", map[string]any{"somevalue": int64(40)}), + ), }, } @@ -1239,10 +1346,18 @@ func TestCheckPermissionOverSchema(t *testing.T) { require.Equal(tc.expectedPermissionship, membership) - if tc.expectedCaveat != nil { + if tc.expectedCaveat != nil && tc.alternativeExpectedCaveat == nil { require.NotEmpty(resp.ResultsByResourceId[tc.resource.ObjectId].Expression) testutil.RequireProtoEqual(t, tc.expectedCaveat, resp.ResultsByResourceId[tc.resource.ObjectId].Expression, "mismatch in caveat") } + + if tc.expectedCaveat != nil && tc.alternativeExpectedCaveat != nil { + require.NotEmpty(resp.ResultsByResourceId[tc.resource.ObjectId].Expression) + + if testutil.AreProtoEqual(tc.expectedCaveat, resp.ResultsByResourceId[tc.resource.ObjectId].Expression, "mismatch in caveat") != nil { + testutil.RequireProtoEqual(t, tc.alternativeExpectedCaveat, resp.ResultsByResourceId[tc.resource.ObjectId].Expression, "mismatch in caveat") + } + } }) } }
internal/dispatch/graph/lookupsubjects_test.go+1 −0 modified@@ -28,6 +28,7 @@ var ( caveatAndCtx = caveats.MustCaveatExprForTestingWithContext caveatAnd = caveats.And caveatInvert = caveats.Invert + caveatOr = caveats.Or ) func TestSimpleLookupSubjects(t *testing.T) {
internal/graph/check.go+12 −1 modified@@ -466,6 +466,7 @@ func (cc *ConcurrentChecker) checkDirect(ctx context.Context, crc currentRequest // Find the subjects over which to dispatch. subjectsToDispatch := tuple.NewONRByTypeSet() relationshipsBySubjectONR := mapz.NewMultiMap[string, *core.RelationTuple]() + hasCaveats := false for tpl := it.Next(); tpl != nil; tpl = it.Next() { if it.Err() != nil { @@ -479,6 +480,9 @@ func (cc *ConcurrentChecker) checkDirect(ctx context.Context, crc currentRequest subjectsToDispatch.Add(tpl.Subject) relationshipsBySubjectONR.Add(tuple.StringONR(tpl.Subject), tpl) + if tpl.Caveat != nil && tpl.Caveat.CaveatName != "" { + hasCaveats = true + } } it.Close() @@ -499,14 +503,21 @@ func (cc *ConcurrentChecker) checkDirect(ctx context.Context, crc currentRequest dispatchChunkCountHistogram.Observe(chunkCount) }) + // If there are caveats on the incoming relationships, then we must require all results to be + // found, as we need to ensure that all caveats are used for building the final expression. + resultsSetting := crc.resultsSetting + if hasCaveats { + resultsSetting = v1.DispatchCheckRequest_REQUIRE_ALL_RESULTS + } + // Dispatch and map to the associated resource ID(s). result := union(ctx, crc, toDispatch, func(ctx context.Context, crc currentRequestContext, dd directDispatch) CheckResult { childResult := cc.dispatch(ctx, crc, ValidatedCheckRequest{ &v1.DispatchCheckRequest{ ResourceRelation: dd.resourceType, ResourceIds: dd.resourceIds, Subject: crc.parentReq.Subject, - ResultsSetting: crc.resultsSetting, + ResultsSetting: resultsSetting, Metadata: decrementDepth(crc.parentReq.Metadata), Debug: crc.parentReq.Debug,
internal/services/integrationtesting/testconfigs/caveatmultiplebranchessamerel.yaml+23 −0 added@@ -0,0 +1,23 @@ +schema: >- + definition user {} + + caveat write_limit(limit uint, count uint) { + count < limit + } + + definition role { + relation member: user + } + + definition database { + relation writer: role#member with write_limit + permission write = writer + } +relationships: |- + database:listings#writer@role:default#member[write_limit:{"limit":2}] + database:listings#writer@role:premium#member[write_limit:{"limit":4}] + role:default#member@user:bob + role:premium#member@user:bob +assertions: + assertTrue: + - 'database:listings#write@user:bob with {"count":3}'
pkg/tuple/tuple.go+1 −1 modified@@ -175,7 +175,7 @@ func MustParse(tpl string) *core.RelationTuple { if parsed := Parse(tpl); parsed != nil { return parsed } - panic("failed to parse tuple") + panic("failed to parse tuple: " + tpl) } // Parse unmarshals the string form of a Tuple and returns nil if there is a
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-jhg6-6qrx-38mrghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-46989ghsaADVISORY
- github.com/authzed/spicedb/commit/20855de75812bcbc975efebe7f76abf47c0f3edbghsaWEB
- github.com/authzed/spicedb/commit/d4ef8e1dbce1eafaf25847f4c0f09738820f5bf2ghsax_refsource_MISCWEB
- github.com/authzed/spicedb/security/advisories/GHSA-jhg6-6qrx-38mrghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.