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  }