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

     1  package namespace
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"testing"
     7  
     8  	"github.com/stretchr/testify/require"
     9  
    10  	core "github.com/authzed/spicedb/pkg/proto/core/v1"
    11  	"github.com/authzed/spicedb/pkg/typesystem"
    12  
    13  	"github.com/authzed/spicedb/internal/datastore/memdb"
    14  	ns "github.com/authzed/spicedb/pkg/namespace"
    15  	"github.com/authzed/spicedb/pkg/schemadsl/compiler"
    16  	"github.com/authzed/spicedb/pkg/schemadsl/input"
    17  )
    18  
    19  func TestCanonicalization(t *testing.T) {
    20  	testCases := []struct {
    21  		name             string
    22  		toCheck          *core.NamespaceDefinition
    23  		expectedError    string
    24  		expectedCacheMap map[string]string
    25  	}{
    26  		{
    27  			"empty canonicalization",
    28  			ns.Namespace(
    29  				"document",
    30  			),
    31  			"",
    32  			map[string]string{},
    33  		},
    34  		{
    35  			"basic canonicalization",
    36  			ns.Namespace(
    37  				"document",
    38  				ns.MustRelation("owner", nil),
    39  				ns.MustRelation("viewer", nil),
    40  				ns.MustRelation("edit", ns.Union(
    41  					ns.ComputedUserset("owner"),
    42  				)),
    43  				ns.MustRelation("view", ns.Union(
    44  					ns.ComputedUserset("viewer"),
    45  					ns.ComputedUserset("edit"),
    46  				)),
    47  			),
    48  			"",
    49  			map[string]string{
    50  				"owner":  "owner",
    51  				"viewer": "viewer",
    52  				"edit":   computedKeyPrefix + "596a8660f9a0c085",
    53  				"view":   computedKeyPrefix + "0cb51da20fc9f20f",
    54  			},
    55  		},
    56  		{
    57  			"canonicalization with aliases",
    58  			ns.Namespace(
    59  				"document",
    60  				ns.MustRelation("owner", nil),
    61  				ns.MustRelation("viewer", nil),
    62  				ns.MustRelation("edit", ns.Union(
    63  					ns.ComputedUserset("owner"),
    64  				)),
    65  				ns.MustRelation("other_edit", ns.Union(
    66  					ns.ComputedUserset("owner"),
    67  				)),
    68  			),
    69  			"",
    70  			map[string]string{
    71  				"owner":      "owner",
    72  				"viewer":     "viewer",
    73  				"edit":       computedKeyPrefix + "596a8660f9a0c085",
    74  				"other_edit": computedKeyPrefix + "596a8660f9a0c085",
    75  			},
    76  		},
    77  		{
    78  			"canonicalization with nested aliases",
    79  			ns.Namespace(
    80  				"document",
    81  				ns.MustRelation("owner", nil),
    82  				ns.MustRelation("viewer", nil),
    83  				ns.MustRelation("edit", ns.Union(
    84  					ns.ComputedUserset("owner"),
    85  				)),
    86  				ns.MustRelation("other_edit", ns.Union(
    87  					ns.ComputedUserset("edit"),
    88  				)),
    89  			),
    90  			"",
    91  			map[string]string{
    92  				"owner":      "owner",
    93  				"viewer":     "viewer",
    94  				"edit":       computedKeyPrefix + "596a8660f9a0c085",
    95  				"other_edit": computedKeyPrefix + "596a8660f9a0c085",
    96  			},
    97  		},
    98  		{
    99  			"canonicalization with same union expressions",
   100  			ns.Namespace(
   101  				"document",
   102  				ns.MustRelation("owner", nil),
   103  				ns.MustRelation("viewer", nil),
   104  				ns.MustRelation("first", ns.Union(
   105  					ns.ComputedUserset("owner"),
   106  					ns.ComputedUserset("viewer"),
   107  				)),
   108  				ns.MustRelation("second", ns.Union(
   109  					ns.ComputedUserset("viewer"),
   110  					ns.ComputedUserset("owner"),
   111  				)),
   112  			),
   113  			"",
   114  			map[string]string{
   115  				"owner":  "owner",
   116  				"viewer": "viewer",
   117  				"first":  computedKeyPrefix + "62152badef526205",
   118  				"second": computedKeyPrefix + "62152badef526205",
   119  			},
   120  		},
   121  		{
   122  			"canonicalization with same union expressions due to aliasing",
   123  			ns.Namespace(
   124  				"document",
   125  				ns.MustRelation("owner", nil),
   126  				ns.MustRelation("viewer", nil),
   127  				ns.MustRelation("edit", ns.Union(
   128  					ns.ComputedUserset("owner"),
   129  				)),
   130  				ns.MustRelation("first", ns.Union(
   131  					ns.ComputedUserset("edit"),
   132  					ns.ComputedUserset("viewer"),
   133  				)),
   134  				ns.MustRelation("second", ns.Union(
   135  					ns.ComputedUserset("viewer"),
   136  					ns.ComputedUserset("edit"),
   137  				)),
   138  			),
   139  			"",
   140  			map[string]string{
   141  				"owner":  "owner",
   142  				"viewer": "viewer",
   143  				"edit":   computedKeyPrefix + "596a8660f9a0c085",
   144  				"first":  computedKeyPrefix + "62152badef526205",
   145  				"second": computedKeyPrefix + "62152badef526205",
   146  			},
   147  		},
   148  		{
   149  			"canonicalization with same intersection expressions",
   150  			ns.Namespace(
   151  				"document",
   152  				ns.MustRelation("owner", nil),
   153  				ns.MustRelation("viewer", nil),
   154  				ns.MustRelation("first", ns.Intersection(
   155  					ns.ComputedUserset("owner"),
   156  					ns.ComputedUserset("viewer"),
   157  				)),
   158  				ns.MustRelation("second", ns.Intersection(
   159  					ns.ComputedUserset("viewer"),
   160  					ns.ComputedUserset("owner"),
   161  				)),
   162  			),
   163  			"",
   164  			map[string]string{
   165  				"owner":  "owner",
   166  				"viewer": "viewer",
   167  				"first":  computedKeyPrefix + "18cf8af8ff02bad0",
   168  				"second": computedKeyPrefix + "18cf8af8ff02bad0",
   169  			},
   170  		},
   171  		{
   172  			"canonicalization with different expressions",
   173  			ns.Namespace(
   174  				"document",
   175  				ns.MustRelation("owner", nil),
   176  				ns.MustRelation("viewer", nil),
   177  				ns.MustRelation("first", ns.Exclusion(
   178  					ns.ComputedUserset("owner"),
   179  					ns.ComputedUserset("viewer"),
   180  				)),
   181  				ns.MustRelation("second", ns.Exclusion(
   182  					ns.ComputedUserset("viewer"),
   183  					ns.ComputedUserset("owner"),
   184  				)),
   185  			),
   186  			"",
   187  			map[string]string{
   188  				"owner":  "owner",
   189  				"viewer": "viewer",
   190  				"first":  computedKeyPrefix + "2cd554a00f7f2d94",
   191  				"second": computedKeyPrefix + "69d4722141f74043",
   192  			},
   193  		},
   194  		{
   195  			"canonicalization with arrow expressions",
   196  			ns.Namespace(
   197  				"document",
   198  				ns.MustRelation("owner", nil),
   199  				ns.MustRelation("viewer", nil),
   200  				ns.MustRelation("first", ns.Union(
   201  					ns.TupleToUserset("owner", "something"),
   202  				)),
   203  				ns.MustRelation("second", ns.Union(
   204  					ns.TupleToUserset("owner", "something"),
   205  				)),
   206  				ns.MustRelation("difftuple", ns.Union(
   207  					ns.TupleToUserset("viewer", "something"),
   208  				)),
   209  				ns.MustRelation("diffrel", ns.Union(
   210  					ns.TupleToUserset("owner", "somethingelse"),
   211  				)),
   212  			),
   213  			"",
   214  			map[string]string{
   215  				"owner":     "owner",
   216  				"viewer":    "viewer",
   217  				"first":     computedKeyPrefix + "9fd2b03cabeb2e42",
   218  				"second":    computedKeyPrefix + "9fd2b03cabeb2e42",
   219  				"diffrel":   computedKeyPrefix + "ab86f3a255f31908",
   220  				"difftuple": computedKeyPrefix + "dddc650e89a7bf1a",
   221  			},
   222  		},
   223  		{
   224  			"canonicalization with same nested union expressions",
   225  			ns.Namespace(
   226  				"document",
   227  				ns.MustRelation("owner", nil),
   228  				ns.MustRelation("editor", nil),
   229  				ns.MustRelation("viewer", nil),
   230  				ns.MustRelation("first", ns.Union(
   231  					ns.ComputedUserset("owner"),
   232  					ns.Rewrite(
   233  						ns.Union(
   234  							ns.ComputedUserset("editor"),
   235  							ns.ComputedUserset("viewer"),
   236  						),
   237  					),
   238  				)),
   239  				ns.MustRelation("second", ns.Union(
   240  					ns.ComputedUserset("viewer"),
   241  					ns.Rewrite(
   242  						ns.Union(
   243  							ns.ComputedUserset("editor"),
   244  							ns.ComputedUserset("owner"),
   245  						),
   246  					),
   247  				)),
   248  			),
   249  			"",
   250  			map[string]string{
   251  				"owner":  "owner",
   252  				"editor": "editor",
   253  				"viewer": "viewer",
   254  				"first":  computedKeyPrefix + "4c49627fbdbaf248",
   255  				"second": computedKeyPrefix + "4c49627fbdbaf248",
   256  			},
   257  		},
   258  		{
   259  			"canonicalization with same nested intersection expressions",
   260  			ns.Namespace(
   261  				"document",
   262  				ns.MustRelation("owner", nil),
   263  				ns.MustRelation("editor", nil),
   264  				ns.MustRelation("viewer", nil),
   265  				ns.MustRelation("first", ns.Intersection(
   266  					ns.ComputedUserset("owner"),
   267  					ns.Rewrite(
   268  						ns.Intersection(
   269  							ns.ComputedUserset("editor"),
   270  							ns.ComputedUserset("viewer"),
   271  						),
   272  					),
   273  				)),
   274  				ns.MustRelation("second", ns.Intersection(
   275  					ns.ComputedUserset("viewer"),
   276  					ns.Rewrite(
   277  						ns.Intersection(
   278  							ns.ComputedUserset("editor"),
   279  							ns.ComputedUserset("owner"),
   280  						),
   281  					),
   282  				)),
   283  			),
   284  			"",
   285  			map[string]string{
   286  				"owner":  "owner",
   287  				"editor": "editor",
   288  				"viewer": "viewer",
   289  				"first":  computedKeyPrefix + "7c52666bb7593f0a",
   290  				"second": computedKeyPrefix + "7c52666bb7593f0a",
   291  			},
   292  		},
   293  		{
   294  			"canonicalization with different nested exclusion expressions",
   295  			ns.Namespace(
   296  				"document",
   297  				ns.MustRelation("owner", nil),
   298  				ns.MustRelation("editor", nil),
   299  				ns.MustRelation("viewer", nil),
   300  				ns.MustRelation("first", ns.Exclusion(
   301  					ns.ComputedUserset("owner"),
   302  					ns.Rewrite(
   303  						ns.Exclusion(
   304  							ns.ComputedUserset("editor"),
   305  							ns.ComputedUserset("viewer"),
   306  						),
   307  					),
   308  				)),
   309  				ns.MustRelation("second", ns.Exclusion(
   310  					ns.ComputedUserset("viewer"),
   311  					ns.Rewrite(
   312  						ns.Exclusion(
   313  							ns.ComputedUserset("editor"),
   314  							ns.ComputedUserset("owner"),
   315  						),
   316  					),
   317  				)),
   318  			),
   319  			"",
   320  			map[string]string{
   321  				"owner":  "owner",
   322  				"editor": "editor",
   323  				"viewer": "viewer",
   324  				"first":  computedKeyPrefix + "bb955307170373ae",
   325  				"second": computedKeyPrefix + "6ccf7bece2e540a1",
   326  			},
   327  		},
   328  		{
   329  			"canonicalization with nil expressions",
   330  			ns.Namespace(
   331  				"document",
   332  				ns.MustRelation("owner", nil),
   333  				ns.MustRelation("editor", nil),
   334  				ns.MustRelation("viewer", nil),
   335  				ns.MustRelation("first", ns.Union(
   336  					ns.ComputedUserset("owner"),
   337  					ns.Nil(),
   338  				)),
   339  				ns.MustRelation("second", ns.Union(
   340  					ns.ComputedUserset("viewer"),
   341  					ns.Nil(),
   342  				)),
   343  			),
   344  			"",
   345  			map[string]string{
   346  				"owner":  "owner",
   347  				"editor": "editor",
   348  				"viewer": "viewer",
   349  				"first":  computedKeyPrefix + "95f5633117d42867",
   350  				"second": computedKeyPrefix + "f786018d066f37b4",
   351  			},
   352  		},
   353  		{
   354  			"canonicalization with same expressions with nil expressions",
   355  			ns.Namespace(
   356  				"document",
   357  				ns.MustRelation("owner", nil),
   358  				ns.MustRelation("editor", nil),
   359  				ns.MustRelation("viewer", nil),
   360  				ns.MustRelation("first", ns.Union(
   361  					ns.ComputedUserset("viewer"),
   362  					ns.Nil(),
   363  				)),
   364  				ns.MustRelation("second", ns.Union(
   365  					ns.ComputedUserset("viewer"),
   366  					ns.Nil(),
   367  				)),
   368  			),
   369  			"",
   370  			map[string]string{
   371  				"owner":  "owner",
   372  				"editor": "editor",
   373  				"viewer": "viewer",
   374  				"first":  computedKeyPrefix + "bfc8d945d7030961",
   375  				"second": computedKeyPrefix + "bfc8d945d7030961",
   376  			},
   377  		},
   378  	}
   379  
   380  	for _, tc := range testCases {
   381  		tc := tc
   382  		t.Run(tc.name, func(t *testing.T) {
   383  			require := require.New(t)
   384  
   385  			ds, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC)
   386  			require.NoError(err)
   387  
   388  			ctx := context.Background()
   389  
   390  			lastRevision, err := ds.HeadRevision(context.Background())
   391  			require.NoError(err)
   392  
   393  			ts, err := typesystem.NewNamespaceTypeSystem(tc.toCheck, typesystem.ResolverForDatastoreReader(ds.SnapshotReader(lastRevision)))
   394  			require.NoError(err)
   395  
   396  			vts, terr := ts.Validate(ctx)
   397  			require.NoError(terr)
   398  
   399  			aliases, aerr := computePermissionAliases(vts)
   400  			require.NoError(aerr)
   401  
   402  			cacheKeys, cerr := computeCanonicalCacheKeys(vts, aliases)
   403  			require.NoError(cerr)
   404  			require.Equal(tc.expectedCacheMap, cacheKeys)
   405  		})
   406  	}
   407  }
   408  
   409  const comparisonSchemaTemplate = `
   410  definition document {
   411  	relation viewer: document
   412  	relation editor: document
   413  	relation owner: document
   414  
   415  	permission first = %s
   416  	permission second = %s
   417  }
   418  `
   419  
   420  func TestCanonicalizationComparison(t *testing.T) {
   421  	testCases := []struct {
   422  		name         string
   423  		first        string
   424  		second       string
   425  		expectedSame bool
   426  	}{
   427  		{
   428  			"same relation",
   429  			"viewer",
   430  			"viewer",
   431  			true,
   432  		},
   433  		{
   434  			"different relation",
   435  			"viewer",
   436  			"owner",
   437  			false,
   438  		},
   439  		{
   440  			"union associativity",
   441  			"viewer + owner",
   442  			"owner + viewer",
   443  			true,
   444  		},
   445  		{
   446  			"intersection associativity",
   447  			"viewer & owner",
   448  			"owner & viewer",
   449  			true,
   450  		},
   451  		{
   452  			"exclusion non-associativity",
   453  			"viewer - owner",
   454  			"owner - viewer",
   455  			false,
   456  		},
   457  		{
   458  			"nested union associativity",
   459  			"viewer + (owner + editor)",
   460  			"owner + (viewer + editor)",
   461  			true,
   462  		},
   463  		{
   464  			"nested intersection associativity",
   465  			"viewer & (owner & editor)",
   466  			"owner & (viewer & editor)",
   467  			true,
   468  		},
   469  		{
   470  			"nested union associativity 2",
   471  			"(viewer + owner) + editor",
   472  			"(owner + viewer) + editor",
   473  			true,
   474  		},
   475  		{
   476  			"nested intersection associativity 2",
   477  			"(viewer & owner) & editor",
   478  			"(owner & viewer) & editor",
   479  			true,
   480  		},
   481  		{
   482  			"nested exclusion non-associativity",
   483  			"viewer - (owner - editor)",
   484  			"viewer - owner - editor",
   485  			false,
   486  		},
   487  		{
   488  			"nested exclusion non-associativity with nil",
   489  			"viewer - (owner - nil)",
   490  			"viewer - owner - nil",
   491  			false,
   492  		},
   493  		{
   494  			"nested intersection associativity with nil",
   495  			"(viewer & owner) & nil",
   496  			"(owner & viewer) & nil",
   497  			true,
   498  		},
   499  		{
   500  			"nested intersection associativity with nil 2",
   501  			"(nil & owner) & editor",
   502  			"(owner & nil) & editor",
   503  			true,
   504  		},
   505  	}
   506  
   507  	for _, tc := range testCases {
   508  		tc := tc
   509  		t.Run(tc.name, func(t *testing.T) {
   510  			require := require.New(t)
   511  
   512  			ds, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC)
   513  			require.NoError(err)
   514  
   515  			ctx := context.Background()
   516  
   517  			schemaText := fmt.Sprintf(comparisonSchemaTemplate, tc.first, tc.second)
   518  			compiled, err := compiler.Compile(compiler.InputSchema{
   519  				Source:       input.Source("schema"),
   520  				SchemaString: schemaText,
   521  			}, compiler.AllowUnprefixedObjectType())
   522  			require.NoError(err)
   523  
   524  			lastRevision, err := ds.HeadRevision(context.Background())
   525  			require.NoError(err)
   526  
   527  			ts, err := typesystem.NewNamespaceTypeSystem(compiled.ObjectDefinitions[0], typesystem.ResolverForDatastoreReader(ds.SnapshotReader(lastRevision)))
   528  			require.NoError(err)
   529  
   530  			vts, terr := ts.Validate(ctx)
   531  			require.NoError(terr)
   532  
   533  			aliases, aerr := computePermissionAliases(vts)
   534  			require.NoError(aerr)
   535  
   536  			cacheKeys, cerr := computeCanonicalCacheKeys(vts, aliases)
   537  			require.NoError(cerr)
   538  			require.True((cacheKeys["first"] == cacheKeys["second"]) == tc.expectedSame)
   539  		})
   540  	}
   541  }