github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/services/v1/grouping_test.go (about) 1 package v1 2 3 import ( 4 "context" 5 "math" 6 "sort" 7 "strings" 8 "testing" 9 10 v1 "github.com/authzed/authzed-go/proto/authzed/api/v1" 11 "github.com/stretchr/testify/require" 12 "golang.org/x/exp/maps" 13 14 "github.com/authzed/spicedb/internal/graph/computed" 15 "github.com/authzed/spicedb/pkg/datastore" 16 "github.com/authzed/spicedb/pkg/testutil" 17 "github.com/authzed/spicedb/pkg/tuple" 18 ) 19 20 type expectedGroupedRequest struct { 21 resourceType string 22 resourceRel string 23 subject string 24 resourceIDs []string 25 } 26 27 func TestGroupItems(t *testing.T) { 28 testCases := []struct { 29 name string 30 requests []string 31 groupings []expectedGroupedRequest 32 err string 33 }{ 34 { 35 name: "different subjects cannot be grouped", 36 requests: []string{ 37 "document:1#view@user:1", 38 "document:1#view@user:2", 39 "document:1#view@user:3", 40 }, 41 groupings: []expectedGroupedRequest{ 42 { 43 resourceType: "document", 44 resourceRel: "view", 45 subject: "user:1", 46 resourceIDs: []string{"1"}, 47 }, 48 { 49 resourceType: "document", 50 resourceRel: "view", 51 subject: "user:2", 52 resourceIDs: []string{"1"}, 53 }, 54 { 55 resourceType: "document", 56 resourceRel: "view", 57 subject: "user:3", 58 resourceIDs: []string{"1"}, 59 }, 60 }, 61 }, 62 { 63 name: "different permissions cannot be grouped", 64 requests: []string{ 65 "document:1#view@user:1", 66 "document:1#write@user:1", 67 "document:1#admin@user:1", 68 }, 69 groupings: []expectedGroupedRequest{ 70 { 71 resourceType: "document", 72 resourceRel: "admin", 73 subject: "user:1", 74 resourceIDs: []string{"1"}, 75 }, 76 { 77 resourceType: "document", 78 resourceRel: "view", 79 subject: "user:1", 80 resourceIDs: []string{"1"}, 81 }, 82 { 83 resourceType: "document", 84 resourceRel: "write", 85 subject: "user:1", 86 resourceIDs: []string{"1"}, 87 }, 88 }, 89 }, 90 { 91 name: "different resource types cannot be grouped", 92 requests: []string{ 93 "document:1#view@user:1", 94 "folder:1#view@user:1", 95 "organization:1#view@user:1", 96 }, 97 groupings: []expectedGroupedRequest{ 98 { 99 resourceType: "document", 100 resourceRel: "view", 101 subject: "user:1", 102 resourceIDs: []string{"1"}, 103 }, 104 { 105 resourceType: "folder", 106 resourceRel: "view", 107 subject: "user:1", 108 resourceIDs: []string{"1"}, 109 }, 110 { 111 resourceType: "organization", 112 resourceRel: "view", 113 subject: "user:1", 114 resourceIDs: []string{"1"}, 115 }, 116 }, 117 }, 118 { 119 name: "grouping takes place", 120 requests: []string{ 121 "document:3#view@user:2", 122 "document:1#view@user:1", 123 "document:1#view@user:2", 124 "document:2#view@user:1", 125 "document:5#view@user:2", 126 }, 127 groupings: []expectedGroupedRequest{ 128 { 129 resourceType: "document", 130 resourceRel: "view", 131 subject: "user:1", 132 resourceIDs: []string{"1", "2"}, 133 }, 134 { 135 resourceType: "document", 136 resourceRel: "view", 137 subject: "user:2", 138 resourceIDs: []string{"1", "3", "5"}, 139 }, 140 }, 141 }, 142 { 143 name: "different caveat context cannot be grouped", 144 requests: []string{ 145 `document:1#view@user:1[somecaveat:{"hey": "bud"}]`, 146 `document:2#view@user:1[somecaveat:{"hi": "there"}]`, 147 }, 148 groupings: []expectedGroupedRequest{ 149 { 150 resourceType: "document", 151 resourceRel: "view", 152 subject: "user:1", 153 resourceIDs: []string{"1"}, 154 }, 155 { 156 resourceType: "document", 157 resourceRel: "view", 158 subject: "user:1", 159 resourceIDs: []string{"2"}, 160 }, 161 }, 162 }, 163 { 164 name: "same caveat context can be grouped", 165 requests: []string{ 166 `document:1#view@user:1[somecaveat:{"hey": "bud"}]`, 167 `document:2#view@user:1[somecaveat:{"hey": "bud"}]`, 168 }, 169 groupings: []expectedGroupedRequest{ 170 { 171 resourceType: "document", 172 resourceRel: "view", 173 subject: "user:1", 174 resourceIDs: []string{"1", "2"}, 175 }, 176 }, 177 }, 178 } 179 180 for _, tt := range testCases { 181 t.Run(tt.name, func(t *testing.T) { 182 var items []*v1.CheckBulkPermissionsRequestItem 183 for _, r := range tt.requests { 184 rel := tuple.ParseRel(r) 185 item := &v1.CheckBulkPermissionsRequestItem{ 186 Resource: rel.Resource, 187 Permission: rel.Relation, 188 Subject: rel.Subject, 189 } 190 if rel.OptionalCaveat != nil { 191 item.Context = rel.OptionalCaveat.Context 192 } 193 items = append(items, item) 194 } 195 196 cp := groupingParameters{ 197 atRevision: datastore.NoRevision, 198 maxCaveatContextSize: math.MaxInt, 199 maximumAPIDepth: 1, 200 } 201 202 ccpByHash, err := groupItems(context.Background(), cp, items) 203 if tt.err != "" { 204 require.ErrorContains(t, err, tt.err) 205 } else { 206 ccp := maps.Values(ccpByHash) 207 require.NoError(t, err) 208 require.Equal(t, len(tt.groupings), len(ccp)) 209 210 sort.Slice(tt.groupings, func(first, second int) bool { 211 // NOTE: This sorting is solely for testing, so it does not need to be secure 212 firstParams := tt.groupings[first] 213 secondParams := tt.groupings[second] 214 firstKey := firstParams.resourceType + firstParams.resourceRel + firstParams.subject 215 secondKey := secondParams.resourceType + secondParams.resourceRel + secondParams.subject 216 return firstKey < secondKey 217 }) 218 219 sort.Slice(ccp, func(first, second int) bool { 220 // NOTE: This sorting is solely for testing, so it does not need to be secure 221 firstParams := ccp[first].params 222 secondParams := ccp[second].params 223 224 firstKey := firstParams.ResourceType.Namespace + firstParams.ResourceType.Relation + 225 firstParams.Subject.Namespace + firstParams.Subject.ObjectId + firstParams.Subject.Relation + strings.Join(ccp[first].resourceIDs, ",") 226 secondKey := secondParams.ResourceType.Namespace + secondParams.ResourceType.Relation + 227 secondParams.Subject.Namespace + secondParams.Subject.ObjectId + secondParams.Subject.Relation + strings.Join(ccp[second].resourceIDs, ",") 228 return firstKey < secondKey 229 }) 230 231 for i, expected := range tt.groupings { 232 sort.Strings(expected.resourceIDs) 233 sort.Strings(ccp[i].resourceIDs) 234 235 require.Equal(t, expected.resourceIDs, ccp[i].resourceIDs) 236 237 require.Equal(t, cp.maximumAPIDepth, ccp[i].params.MaximumDepth) 238 require.Equal(t, cp.atRevision, ccp[i].params.AtRevision) 239 require.Equal(t, computed.NoDebugging, ccp[i].params.DebugOption) 240 241 err := testutil.AreProtoEqual(tuple.RelationReference(expected.resourceType, expected.resourceRel), ccp[i].params.ResourceType, "resource type diff") 242 require.NoError(t, err) 243 244 err = testutil.AreProtoEqual(tuple.ParseSubjectONR(expected.subject), ccp[i].params.Subject, "resource type diff") 245 require.NoError(t, err) 246 } 247 } 248 }) 249 } 250 } 251 252 func TestCaveatContextSizeLimitIsEnforced(t *testing.T) { 253 cp := groupingParameters{ 254 atRevision: datastore.NoRevision, 255 maxCaveatContextSize: 1, 256 maximumAPIDepth: 1, 257 } 258 rel := tuple.ParseRel(`document:1#view@user:1[somecaveat:{"hey": "bud"}]`) 259 items := []*v1.CheckBulkPermissionsRequestItem{ 260 { 261 Resource: rel.Resource, 262 Permission: rel.Relation, 263 Subject: rel.Subject, 264 Context: rel.OptionalCaveat.Context, 265 }, 266 } 267 _, err := groupItems(context.Background(), cp, items) 268 require.ErrorContains(t, err, "request caveat context should have less than 1 bytes but had 14") 269 }