VYPR
Moderate severityNVD Advisory· Published Sep 18, 2024· Updated Sep 18, 2024

Multiple caveats on resources of the same type can result in no permission when permission is expected

CVE-2024-46989

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.

PackageAffected versionsPatched versions
github.com/authzed/spicedbGo
< 1.35.31.35.3

Affected products

1

Patches

2
d4ef8e1dbce1

Merge pull request #2027 from josephschorr/caveat-check-fix

https://github.com/authzed/spicedbJoseph SchorrAug 15, 2024via ghsa
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
    
20855de75812

Ensure all resources are returned for relation check when caveats are specified

https://github.com/authzed/spicedbJoseph SchorrAug 15, 2024via ghsa
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

News mentions

0

No linked articles in our index yet.