Permissions processing error in spacedb
Description
Spicedb is an Open Source, Google Zanzibar-inspired permissions database to enable fine-grained authorization for customer applications. Use of an exclusion under an arrow that has multiple resources may resolve to NO_PERMISSION when permission is expected. If the resource exists under *multiple* folders and the user has access to view more than a single folder, SpiceDB may report the user does not have access due to a failure in the exclusion dispatcher to request that *all* the folders in which the user is a member be returned. Permission is returned as NO_PERMISSION when PERMISSION is expected on the CheckPermission API. This issue has been addressed in version 1.33.1. All users are advised to upgrade. There are no known workarounds for this issue.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
github.com/authzed/spicedbGo | < 1.33.1 | 1.33.1 |
Affected products
1Patches
1ecef31d2b266Merge pull request from GHSA-grjv-gjgr-66g2
4 files changed · +327 −1
internal/dispatch/graph/check_test.go+261 −0 modified@@ -298,6 +298,267 @@ func addFrame(trace *v1.CheckDebugTrace, foundFrames *mapz.Set[string]) { } } +func TestCheckPermissionOverSchema(t *testing.T) { + testCases := []struct { + name string + schema string + relationships []*core.RelationTuple + resource *core.ObjectAndRelation + subject *core.ObjectAndRelation + expectedPermissionship v1.ResourceCheckResult_Membership + }{ + { + "basic union", + `definition user {} + + definition document { + relation editor: user + relation viewer: user + permission view = viewer + editor + }`, + []*core.RelationTuple{ + tuple.MustParse("document:first#viewer@user:tom"), + }, + ONR("document", "first", "view"), + ONR("user", "tom", "..."), + v1.ResourceCheckResult_MEMBER, + }, + { + "basic intersection", + `definition user {} + + definition document { + relation editor: user + relation viewer: user + permission view = viewer & editor + }`, + []*core.RelationTuple{ + tuple.MustParse("document:first#viewer@user:tom"), + tuple.MustParse("document:first#editor@user:tom"), + }, + ONR("document", "first", "view"), + ONR("user", "tom", "..."), + v1.ResourceCheckResult_MEMBER, + }, + { + "basic exclusion", + `definition user {} + + definition document { + relation editor: user + relation viewer: user + permission view = viewer - editor + }`, + []*core.RelationTuple{ + tuple.MustParse("document:first#viewer@user:tom"), + }, + ONR("document", "first", "view"), + ONR("user", "tom", "..."), + v1.ResourceCheckResult_MEMBER, + }, + { + "basic union, multiple branches", + `definition user {} + + definition document { + relation editor: user + relation viewer: user + permission view = viewer + editor + }`, + []*core.RelationTuple{ + tuple.MustParse("document:first#viewer@user:tom"), + tuple.MustParse("document:first#editor@user:tom"), + }, + ONR("document", "first", "view"), + ONR("user", "tom", "..."), + v1.ResourceCheckResult_MEMBER, + }, + { + "basic union no permission", + `definition user {} + + definition document { + relation editor: user + relation viewer: user + permission view = viewer + editor + }`, + []*core.RelationTuple{}, + ONR("document", "first", "view"), + ONR("user", "tom", "..."), + v1.ResourceCheckResult_NOT_MEMBER, + }, + { + "basic intersection no permission", + `definition user {} + + definition document { + relation editor: user + relation viewer: user + permission view = viewer & editor + }`, + []*core.RelationTuple{ + tuple.MustParse("document:first#viewer@user:tom"), + }, + ONR("document", "first", "view"), + ONR("user", "tom", "..."), + v1.ResourceCheckResult_NOT_MEMBER, + }, + { + "basic exclusion no permission", + `definition user {} + + definition document { + relation banned: user + relation viewer: user + permission view = viewer - banned + }`, + []*core.RelationTuple{ + tuple.MustParse("document:first#viewer@user:tom"), + tuple.MustParse("document:first#banned@user:tom"), + }, + ONR("document", "first", "view"), + ONR("user", "tom", "..."), + v1.ResourceCheckResult_NOT_MEMBER, + }, + { + "exclusion with multiple branches", + `definition user {} + + definition group { + relation member: user + relation banned: user + permission view = member - banned + } + + definition document { + relation group: group + permission view = group->view + }`, + []*core.RelationTuple{ + tuple.MustParse("document:first#group@group:first"), + tuple.MustParse("document:first#group@group:second"), + tuple.MustParse("group:first#member@user:tom"), + tuple.MustParse("group:first#banned@user:tom"), + tuple.MustParse("group:second#member@user:tom"), + }, + ONR("document", "first", "view"), + ONR("user", "tom", "..."), + v1.ResourceCheckResult_MEMBER, + }, + { + "intersection with multiple branches", + `definition user {} + + definition group { + relation member: user + relation other: user + permission view = member & other + } + + definition document { + relation group: group + permission view = group->view + }`, + []*core.RelationTuple{ + tuple.MustParse("document:first#group@group:first"), + tuple.MustParse("document:first#group@group:second"), + tuple.MustParse("group:first#member@user:tom"), + tuple.MustParse("group:first#other@user:tom"), + tuple.MustParse("group:second#member@user:tom"), + }, + ONR("document", "first", "view"), + ONR("user", "tom", "..."), + v1.ResourceCheckResult_MEMBER, + }, + { + "exclusion with multiple branches no permission", + `definition user {} + + definition group { + relation member: user + relation banned: user + permission view = member - banned + } + + definition document { + relation group: group + permission view = group->view + }`, + []*core.RelationTuple{ + tuple.MustParse("document:first#group@group:first"), + tuple.MustParse("document:first#group@group:second"), + tuple.MustParse("group:first#member@user:tom"), + tuple.MustParse("group:first#banned@user:tom"), + tuple.MustParse("group:second#member@user:tom"), + tuple.MustParse("group:second#banned@user:tom"), + }, + ONR("document", "first", "view"), + ONR("user", "tom", "..."), + v1.ResourceCheckResult_NOT_MEMBER, + }, + { + "intersection with multiple branches no permission", + `definition user {} + + definition group { + relation member: user + relation other: user + permission view = member & other + } + + definition document { + relation group: group + permission view = group->view + }`, + []*core.RelationTuple{ + tuple.MustParse("document:first#group@group:first"), + tuple.MustParse("document:first#group@group:second"), + tuple.MustParse("group:first#member@user:tom"), + tuple.MustParse("group:second#member@user:tom"), + }, + ONR("document", "first", "view"), + ONR("user", "tom", "..."), + v1.ResourceCheckResult_NOT_MEMBER, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + require := require.New(t) + + dispatcher := NewLocalOnlyDispatcher(10) + + ds, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC) + require.NoError(err) + + ds, revision := testfixtures.DatastoreFromSchemaAndTestRelationships(ds, tc.schema, tc.relationships, require) + + ctx := datastoremw.ContextWithHandle(context.Background()) + require.NoError(datastoremw.SetInContext(ctx, ds)) + + resp, err := dispatcher.DispatchCheck(ctx, &v1.DispatchCheckRequest{ + ResourceRelation: RR(tc.resource.Namespace, tc.resource.Relation), + ResourceIds: []string{tc.resource.ObjectId}, + Subject: tc.subject, + Metadata: &v1.ResolverMeta{ + AtRevision: revision.String(), + DepthRemaining: 50, + }, + ResultsSetting: v1.DispatchCheckRequest_ALLOW_SINGLE_RESULT, + }) + require.NoError(err) + + membership := v1.ResourceCheckResult_NOT_MEMBER + if r, ok := resp.ResultsByResourceId[tc.resource.ObjectId]; ok { + membership = r.Membership + } + + require.Equal(tc.expectedPermissionship, membership) + }) + } +} + func TestCheckDebugging(t *testing.T) { type expectedFrame struct { resourceType *core.RelationReference
internal/graph/check.go+6 −1 modified@@ -771,7 +771,12 @@ func difference[T any]( othersChan := make(chan CheckResult, len(children)-1) go func() { - result := handler(childCtx, crc, children[0]) + result := handler(childCtx, currentRequestContext{ + parentReq: crc.parentReq, + filteredResourceIDs: crc.filteredResourceIDs, + resultsSetting: v1.DispatchCheckRequest_REQUIRE_ALL_RESULTS, + maxDispatchCount: crc.maxDispatchCount, + }, children[0]) baseChan <- result }()
internal/graph/membershipset_test.go+28 −0 modified@@ -705,6 +705,34 @@ func TestMembershipSetSubtract(t *testing.T) { false, false, }, + { + "non overlapping", + map[string]*core.CaveatExpression{ + "resource1": nil, + "resource2": nil, + }, + map[string]*core.CaveatExpression{ + "resource2": nil, + }, + map[string]*core.CaveatExpression{ + "resource1": nil, + }, + true, + false, + }, + { + "non overlapping reversed", + map[string]*core.CaveatExpression{ + "resource2": nil, + }, + map[string]*core.CaveatExpression{ + "resource1": nil, + "resource2": nil, + }, + map[string]*core.CaveatExpression{}, + false, + true, + }, } for _, tc := range tcs {
internal/services/integrationtesting/arrowovermultiexclusion.yaml+32 −0 added@@ -0,0 +1,32 @@ +--- +schema: |+ + definition user {} + + definition group { + relation direct_member: user + relation excluded: user:* + permission view = direct_member - excluded + } + + definition resource { + relation group: group + permission view = group->view + } + +relationships: >- + resource:one#group@group:group1 + + resource:one#group@group:group2 + + group:group1#direct_member@user:fred + + group:group1#excluded@user:* + + group:group2#direct_member@user:fred +assertions: + assertTrue: + - "group:group1#direct_member@user:fred" + - "group:group2#direct_member@user:fred" + - "group:group2#view@user:fred" + assertFalse: + - "group:group1#view@user:fred"
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
4- github.com/advisories/GHSA-grjv-gjgr-66g2ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2024-38361ghsaADVISORY
- github.com/authzed/spicedb/commit/ecef31d2b266fde17eb2c3415e2ec4ceff96fbebghsax_refsource_MISCWEB
- github.com/authzed/spicedb/security/advisories/GHSA-grjv-gjgr-66g2ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.