github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/graph/resourcesubjectsmap_test.go (about)

     1  package graph
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  	"strings"
     7  	"testing"
     8  
     9  	"github.com/stretchr/testify/require"
    10  
    11  	core "github.com/authzed/spicedb/pkg/proto/core/v1"
    12  	v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1"
    13  	"github.com/authzed/spicedb/pkg/testutil"
    14  	"github.com/authzed/spicedb/pkg/tuple"
    15  )
    16  
    17  func TestResourcesSubjectsMapBasic(t *testing.T) {
    18  	rsm := newResourcesSubjectMap(&core.RelationReference{
    19  		Namespace: "document",
    20  		Relation:  "view",
    21  	})
    22  
    23  	require.Equal(t, rsm.resourceType.Namespace, "document")
    24  	require.Equal(t, rsm.resourceType.Relation, "view")
    25  	require.Equal(t, 0, rsm.len())
    26  
    27  	rsm.addSubjectIDAsFoundResourceID("first")
    28  	require.Equal(t, 1, rsm.len())
    29  
    30  	rsm.addSubjectIDAsFoundResourceID("second")
    31  	require.Equal(t, 2, rsm.len())
    32  
    33  	err := rsm.addRelationship(tuple.MustParse("document:third#view@user:tom"))
    34  	require.NoError(t, err)
    35  	require.Equal(t, 3, rsm.len())
    36  
    37  	err = rsm.addRelationship(tuple.MustParse("document:fourth#view@user:sarah[somecaveat]"))
    38  	require.NoError(t, err)
    39  	require.Equal(t, 4, rsm.len())
    40  
    41  	locked := rsm.asReadOnly()
    42  	require.False(t, locked.isEmpty())
    43  
    44  	directAsResources := locked.asReachableResources(true)
    45  	testutil.RequireProtoSlicesEqual(t, []*v1.ReachableResource{
    46  		{
    47  			ResourceId:    "first",
    48  			ResultStatus:  v1.ReachableResource_HAS_PERMISSION,
    49  			ForSubjectIds: []string{"first"},
    50  		},
    51  		{
    52  			ResourceId:    "fourth",
    53  			ResultStatus:  v1.ReachableResource_REQUIRES_CHECK,
    54  			ForSubjectIds: []string{"sarah"},
    55  		},
    56  		{
    57  			ResourceId:    "second",
    58  			ResultStatus:  v1.ReachableResource_HAS_PERMISSION,
    59  			ForSubjectIds: []string{"second"},
    60  		},
    61  		{
    62  			ResourceId:    "third",
    63  			ResultStatus:  v1.ReachableResource_HAS_PERMISSION,
    64  			ForSubjectIds: []string{"tom"},
    65  		},
    66  	}, directAsResources, nil, "different resources")
    67  
    68  	notDirectAsResources := locked.asReachableResources(false)
    69  	testutil.RequireProtoSlicesEqual(t, []*v1.ReachableResource{
    70  		{
    71  			ResourceId:    "first",
    72  			ResultStatus:  v1.ReachableResource_REQUIRES_CHECK,
    73  			ForSubjectIds: []string{"first"},
    74  		},
    75  		{
    76  			ResourceId:    "fourth",
    77  			ResultStatus:  v1.ReachableResource_REQUIRES_CHECK,
    78  			ForSubjectIds: []string{"sarah"},
    79  		},
    80  		{
    81  			ResourceId:    "second",
    82  			ResultStatus:  v1.ReachableResource_REQUIRES_CHECK,
    83  			ForSubjectIds: []string{"second"},
    84  		},
    85  		{
    86  			ResourceId:    "third",
    87  			ResultStatus:  v1.ReachableResource_REQUIRES_CHECK,
    88  			ForSubjectIds: []string{"tom"},
    89  		},
    90  	}, notDirectAsResources, nil, "different resources")
    91  }
    92  
    93  func TestResourcesSubjectsMapAsReachableResources(t *testing.T) {
    94  	tcs := []struct {
    95  		name     string
    96  		rels     []*core.RelationTuple
    97  		expected []*v1.ReachableResource
    98  	}{
    99  		{
   100  			"empty",
   101  			[]*core.RelationTuple{},
   102  			[]*v1.ReachableResource{},
   103  		},
   104  		{
   105  			"basic",
   106  			[]*core.RelationTuple{
   107  				tuple.MustParse("document:first#view@user:tom"),
   108  				tuple.MustParse("document:second#view@user:sarah"),
   109  			},
   110  			[]*v1.ReachableResource{
   111  				{
   112  					ResourceId:    "first",
   113  					ResultStatus:  v1.ReachableResource_HAS_PERMISSION,
   114  					ForSubjectIds: []string{"tom"},
   115  				},
   116  				{
   117  					ResourceId:    "second",
   118  					ResultStatus:  v1.ReachableResource_HAS_PERMISSION,
   119  					ForSubjectIds: []string{"sarah"},
   120  				},
   121  			},
   122  		},
   123  		{
   124  			"caveated and non-caveated",
   125  			[]*core.RelationTuple{
   126  				tuple.MustParse("document:first#view@user:tom"),
   127  				tuple.MustParse("document:first#view@user:sarah[somecaveat]"),
   128  			},
   129  			[]*v1.ReachableResource{
   130  				{
   131  					ResourceId:    "first",
   132  					ResultStatus:  v1.ReachableResource_HAS_PERMISSION,
   133  					ForSubjectIds: []string{"tom"},
   134  				},
   135  			},
   136  		},
   137  		{
   138  			"all caveated",
   139  			[]*core.RelationTuple{
   140  				tuple.MustParse("document:first#view@user:tom[anothercaveat]"),
   141  				tuple.MustParse("document:first#view@user:sarah[somecaveat]"),
   142  			},
   143  			[]*v1.ReachableResource{
   144  				{
   145  					ResourceId:    "first",
   146  					ResultStatus:  v1.ReachableResource_REQUIRES_CHECK,
   147  					ForSubjectIds: []string{"sarah", "tom"},
   148  				},
   149  			},
   150  		},
   151  		{
   152  			"full",
   153  			[]*core.RelationTuple{
   154  				tuple.MustParse("document:first#view@user:tom[anothercaveat]"),
   155  				tuple.MustParse("document:first#view@user:sarah[somecaveat]"),
   156  				tuple.MustParse("document:second#view@user:tom"),
   157  				tuple.MustParse("document:second#view@user:sarah[somecaveat]"),
   158  				tuple.MustParse("document:third#view@user:tom"),
   159  				tuple.MustParse("document:third#view@user:sarah"),
   160  			},
   161  			[]*v1.ReachableResource{
   162  				{
   163  					ResourceId:    "first",
   164  					ResultStatus:  v1.ReachableResource_REQUIRES_CHECK,
   165  					ForSubjectIds: []string{"sarah", "tom"},
   166  				},
   167  				{
   168  					ResourceId:    "second",
   169  					ResultStatus:  v1.ReachableResource_HAS_PERMISSION,
   170  					ForSubjectIds: []string{"tom"},
   171  				},
   172  				{
   173  					ResourceId:    "third",
   174  					ResultStatus:  v1.ReachableResource_HAS_PERMISSION,
   175  					ForSubjectIds: []string{"tom", "sarah"},
   176  				},
   177  			},
   178  		},
   179  	}
   180  
   181  	for _, tc := range tcs {
   182  		tc := tc
   183  		t.Run(tc.name, func(t *testing.T) {
   184  			for _, isDirectEntrypoint := range []bool{true, false} {
   185  				isDirectEntrypoint := isDirectEntrypoint
   186  				t.Run(fmt.Sprintf("%v", isDirectEntrypoint), func(t *testing.T) {
   187  					rsm := newResourcesSubjectMap(&core.RelationReference{
   188  						Namespace: "document",
   189  						Relation:  "view",
   190  					})
   191  
   192  					for _, rel := range tc.rels {
   193  						err := rsm.addRelationship(rel)
   194  						require.NoError(t, err)
   195  					}
   196  
   197  					expected := make([]*v1.ReachableResource, 0, len(tc.expected))
   198  					for _, expectedResource := range tc.expected {
   199  						cloned := expectedResource.CloneVT()
   200  						if !isDirectEntrypoint {
   201  							cloned.ResultStatus = v1.ReachableResource_REQUIRES_CHECK
   202  						}
   203  						expected = append(expected, cloned)
   204  					}
   205  
   206  					locked := rsm.asReadOnly()
   207  					resources := locked.asReachableResources(isDirectEntrypoint)
   208  					testutil.RequireProtoSlicesEqual(t, expected, resources, sortByResource, "different resources")
   209  				})
   210  			}
   211  		})
   212  	}
   213  }
   214  
   215  func TestResourcesSubjectsMapMapFoundResources(t *testing.T) {
   216  	tcs := []struct {
   217  		name           string
   218  		rels           []*core.RelationTuple
   219  		foundResources []*v1.ReachableResource
   220  		expected       []*v1.ReachableResource
   221  	}{
   222  		{
   223  			"empty",
   224  			[]*core.RelationTuple{},
   225  			[]*v1.ReachableResource{},
   226  			[]*v1.ReachableResource{},
   227  		},
   228  		{
   229  			"basic no caveats",
   230  			[]*core.RelationTuple{
   231  				tuple.MustParse("group:firstgroup#member@organization:foo"),
   232  				tuple.MustParse("group:firstgroup#member@organization:bar"),
   233  			},
   234  			[]*v1.ReachableResource{
   235  				{
   236  					ResourceId:    "first",
   237  					ForSubjectIds: []string{"firstgroup"},
   238  					ResultStatus:  v1.ReachableResource_HAS_PERMISSION,
   239  				},
   240  			},
   241  			[]*v1.ReachableResource{
   242  				{
   243  					ResourceId:    "first",
   244  					ForSubjectIds: []string{"foo", "bar"},
   245  					ResultStatus:  v1.ReachableResource_HAS_PERMISSION,
   246  				},
   247  			},
   248  		},
   249  		{
   250  			"caveated all found",
   251  			[]*core.RelationTuple{
   252  				tuple.MustParse("group:firstgroup#member@organization:foo[somecaveat]"),
   253  				tuple.MustParse("group:firstgroup#member@organization:bar[somecvaeat]"),
   254  			},
   255  			[]*v1.ReachableResource{
   256  				{
   257  					ResourceId:    "first",
   258  					ForSubjectIds: []string{"firstgroup"},
   259  					ResultStatus:  v1.ReachableResource_HAS_PERMISSION,
   260  				},
   261  			},
   262  			[]*v1.ReachableResource{
   263  				{
   264  					ResourceId:    "first",
   265  					ForSubjectIds: []string{"bar", "foo"},
   266  					ResultStatus:  v1.ReachableResource_REQUIRES_CHECK,
   267  				},
   268  			},
   269  		},
   270  		{
   271  			"simple short circuit",
   272  			[]*core.RelationTuple{
   273  				tuple.MustParse("group:firstgroup#member@organization:foo[somecaveat]"),
   274  				tuple.MustParse("group:firstgroup#member@organization:bar"),
   275  			},
   276  			[]*v1.ReachableResource{
   277  				{
   278  					ResourceId:    "first",
   279  					ForSubjectIds: []string{"firstgroup"},
   280  					ResultStatus:  v1.ReachableResource_HAS_PERMISSION,
   281  				},
   282  			},
   283  			[]*v1.ReachableResource{
   284  				{
   285  					ResourceId:    "first",
   286  					ForSubjectIds: []string{"bar"},
   287  					ResultStatus:  v1.ReachableResource_HAS_PERMISSION,
   288  				},
   289  			},
   290  		},
   291  		{
   292  			"check requires on incoming subject",
   293  			[]*core.RelationTuple{
   294  				tuple.MustParse("group:firstgroup#member@organization:foo"),
   295  				tuple.MustParse("group:firstgroup#member@organization:bar"),
   296  			},
   297  			[]*v1.ReachableResource{
   298  				{
   299  					ResourceId:    "first",
   300  					ForSubjectIds: []string{"firstgroup"},
   301  					ResultStatus:  v1.ReachableResource_REQUIRES_CHECK,
   302  				},
   303  			},
   304  			[]*v1.ReachableResource{
   305  				{
   306  					ResourceId:    "first",
   307  					ForSubjectIds: []string{"foo", "bar"},
   308  					ResultStatus:  v1.ReachableResource_REQUIRES_CHECK,
   309  				},
   310  			},
   311  		},
   312  		{
   313  			"multi-input short circuit",
   314  			[]*core.RelationTuple{
   315  				tuple.MustParse("group:firstgroup#member@organization:foo"),
   316  				tuple.MustParse("group:firstgroup#member@organization:bar"),
   317  				tuple.MustParse("group:secondgroup#member@organization:foo[somecaveat]"),
   318  			},
   319  			[]*v1.ReachableResource{
   320  				{
   321  					ResourceId:    "somedoc",
   322  					ForSubjectIds: []string{"firstgroup", "secondgroup"},
   323  					ResultStatus:  v1.ReachableResource_HAS_PERMISSION,
   324  				},
   325  			},
   326  			[]*v1.ReachableResource{
   327  				{
   328  					ResourceId:    "somedoc",
   329  					ForSubjectIds: []string{"foo", "bar"},
   330  					ResultStatus:  v1.ReachableResource_HAS_PERMISSION,
   331  				},
   332  			},
   333  		},
   334  		{
   335  			"multi-input short circuit from single input",
   336  			[]*core.RelationTuple{
   337  				tuple.MustParse("group:firstgroup#member@organization:bar"),
   338  				tuple.MustParse("group:secondgroup#member@organization:foo[somecaveat]"),
   339  			},
   340  			[]*v1.ReachableResource{
   341  				{
   342  					ResourceId:    "somedoc",
   343  					ForSubjectIds: []string{"firstgroup", "secondgroup"},
   344  					ResultStatus:  v1.ReachableResource_HAS_PERMISSION,
   345  				},
   346  			},
   347  			[]*v1.ReachableResource{
   348  				{
   349  					ResourceId:    "somedoc",
   350  					ForSubjectIds: []string{"bar"},
   351  					ResultStatus:  v1.ReachableResource_HAS_PERMISSION,
   352  				},
   353  			},
   354  		},
   355  		{
   356  			"multi-input short circuit from single input with check required on parent",
   357  			[]*core.RelationTuple{
   358  				tuple.MustParse("group:firstgroup#member@organization:bar"),
   359  				tuple.MustParse("group:secondgroup#member@organization:foo[somecaveat]"),
   360  			},
   361  			[]*v1.ReachableResource{
   362  				{
   363  					ResourceId:    "somedoc",
   364  					ForSubjectIds: []string{"firstgroup", "secondgroup"},
   365  					ResultStatus:  v1.ReachableResource_REQUIRES_CHECK,
   366  				},
   367  			},
   368  			[]*v1.ReachableResource{
   369  				{
   370  					ResourceId:    "somedoc",
   371  					ForSubjectIds: []string{"bar"},
   372  					ResultStatus:  v1.ReachableResource_REQUIRES_CHECK,
   373  				},
   374  			},
   375  		},
   376  		{
   377  			"multi-input all caveated",
   378  			[]*core.RelationTuple{
   379  				tuple.MustParse("group:firstgroup#member@organization:bar[anothercaveat]"),
   380  				tuple.MustParse("group:secondgroup#member@organization:foo[somecaveat]"),
   381  			},
   382  			[]*v1.ReachableResource{
   383  				{
   384  					ResourceId:    "somedoc",
   385  					ForSubjectIds: []string{"firstgroup", "secondgroup"},
   386  					ResultStatus:  v1.ReachableResource_HAS_PERMISSION,
   387  				},
   388  			},
   389  			[]*v1.ReachableResource{
   390  				{
   391  					ResourceId:    "somedoc",
   392  					ForSubjectIds: []string{"bar", "foo"},
   393  					ResultStatus:  v1.ReachableResource_REQUIRES_CHECK,
   394  				},
   395  			},
   396  		},
   397  	}
   398  
   399  	for _, tc := range tcs {
   400  		tc := tc
   401  		t.Run(tc.name, func(t *testing.T) {
   402  			for _, isDirectEntrypoint := range []bool{true, false} {
   403  				isDirectEntrypoint := isDirectEntrypoint
   404  				t.Run(fmt.Sprintf("%v", isDirectEntrypoint), func(t *testing.T) {
   405  					rsm := newResourcesSubjectMap(&core.RelationReference{
   406  						Namespace: "group",
   407  						Relation:  "member",
   408  					})
   409  
   410  					for _, rel := range tc.rels {
   411  						err := rsm.addRelationship(rel)
   412  						require.NoError(t, err)
   413  					}
   414  
   415  					expected := make([]*v1.ReachableResource, 0, len(tc.expected))
   416  					for _, expectedResource := range tc.expected {
   417  						cloned := expectedResource.CloneVT()
   418  						if !isDirectEntrypoint {
   419  							cloned.ResultStatus = v1.ReachableResource_REQUIRES_CHECK
   420  						}
   421  						sort.Strings(cloned.ForSubjectIds)
   422  						expected = append(expected, cloned)
   423  					}
   424  
   425  					locked := rsm.asReadOnly()
   426  
   427  					resources := make([]*v1.ReachableResource, 0, len(tc.foundResources))
   428  					for _, resource := range tc.foundResources {
   429  						r, err := locked.mapFoundResource(resource, isDirectEntrypoint)
   430  						require.NoError(t, err)
   431  						resources = append(resources, r)
   432  					}
   433  
   434  					for _, r := range resources {
   435  						sort.Strings(r.ForSubjectIds)
   436  					}
   437  
   438  					testutil.RequireProtoSlicesEqual(t, expected, resources, sortByResource, "different resources")
   439  				})
   440  			}
   441  		})
   442  	}
   443  }
   444  
   445  func sortByResource(first *v1.ReachableResource, second *v1.ReachableResource) int {
   446  	return strings.Compare(first.ResourceId, second.ResourceId)
   447  }
   448  
   449  func TestSubjectIDsToResourcesMap(t *testing.T) {
   450  	rsm := subjectIDsToResourcesMap(&core.RelationReference{
   451  		Namespace: "document",
   452  		Relation:  "view",
   453  	}, []string{"first", "second", "third"})
   454  
   455  	require.Equal(t, 3, rsm.len())
   456  }