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

     1  package computed_test
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"testing"
     7  
     8  	"google.golang.org/protobuf/types/known/structpb"
     9  
    10  	"github.com/authzed/spicedb/internal/datastore/memdb"
    11  	"github.com/authzed/spicedb/internal/dispatch/graph"
    12  	"github.com/authzed/spicedb/internal/graph/computed"
    13  	log "github.com/authzed/spicedb/internal/logging"
    14  	datastoremw "github.com/authzed/spicedb/internal/middleware/datastore"
    15  	"github.com/authzed/spicedb/pkg/caveats/types"
    16  	"github.com/authzed/spicedb/pkg/datastore"
    17  	core "github.com/authzed/spicedb/pkg/proto/core/v1"
    18  	v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1"
    19  	"github.com/authzed/spicedb/pkg/schemadsl/compiler"
    20  	"github.com/authzed/spicedb/pkg/tuple"
    21  
    22  	"github.com/stretchr/testify/require"
    23  )
    24  
    25  type caveatedUpdate struct {
    26  	Operation  core.RelationTupleUpdate_Operation
    27  	tuple      string
    28  	caveatName string
    29  	context    map[string]any
    30  }
    31  
    32  func TestComputeCheckWithCaveats(t *testing.T) {
    33  	type check struct {
    34  		check                 string
    35  		context               map[string]any
    36  		member                v1.ResourceCheckResult_Membership
    37  		expectedMissingFields []string
    38  		error                 string
    39  	}
    40  
    41  	testCases := []struct {
    42  		name    string
    43  		schema  string
    44  		updates []caveatedUpdate
    45  		checks  []check
    46  	}{
    47  		{
    48  			"simple test",
    49  			`definition user {}
    50  
    51  			definition organization {
    52  				relation admin: user | user with testcaveat
    53  			}
    54  					
    55  			definition document {
    56  				relation org: organization | organization with anothercaveat
    57  				relation viewer: user | user with testcaveat
    58  				relation editor: user | user with testcaveat
    59  
    60  				permission edit = editor + org->admin
    61  				permission view = viewer + edit
    62  			}
    63  			
    64  			caveat testcaveat(somecondition int, somebool bool) {
    65  				somecondition == 42 && somebool
    66  			}
    67  
    68  			caveat anothercaveat(anothercondition uint) {
    69  				anothercondition == 15
    70  			}
    71  			`,
    72  			[]caveatedUpdate{
    73  				{core.RelationTupleUpdate_CREATE, "organization:someorg#admin@user:sarah", "testcaveat", nil},
    74  				{core.RelationTupleUpdate_CREATE, "organization:someorg#admin@user:john", "testcaveat", map[string]any{"somecondition": "42", "somebool": true}},
    75  				{core.RelationTupleUpdate_CREATE, "organization:someorg#admin@user:jane", "", nil},
    76  				{core.RelationTupleUpdate_CREATE, "document:foo#org@organization:someorg", "anothercaveat", nil},
    77  				{core.RelationTupleUpdate_CREATE, "document:bar#org@organization:someorg", "", nil},
    78  				{core.RelationTupleUpdate_CREATE, "document:foo#editor@user:vic", "testcaveat", map[string]any{"somecondition": "42", "somebool": true}},
    79  				{core.RelationTupleUpdate_CREATE, "document:foo#viewer@user:vic", "testcaveat", map[string]any{"somecondition": "42", "somebool": true}},
    80  				{core.RelationTupleUpdate_CREATE, "document:foo#viewer@user:blippy", "testcaveat", map[string]any{"somecondition": "42", "somebool": false}},
    81  				{core.RelationTupleUpdate_CREATE, "document:foo#viewer@user:noa", "testcaveat", map[string]any{"somecondition": "42", "somebool": false}},
    82  				{core.RelationTupleUpdate_CREATE, "document:foo#editor@user:noa", "testcaveat", map[string]any{"somecondition": "42", "somebool": false}},
    83  				{core.RelationTupleUpdate_CREATE, "document:foo#editor@user:wayne", "invalid", nil},
    84  			},
    85  			[]check{
    86  				{
    87  					"document:foo#view@user:sarah",
    88  					nil,
    89  					v1.ResourceCheckResult_CAVEATED_MEMBER,
    90  					[]string{"anothercondition"},
    91  					"",
    92  				},
    93  				{
    94  					"document:foo#view@user:sarah",
    95  					map[string]any{
    96  						"somecondition": "42",
    97  					},
    98  					v1.ResourceCheckResult_CAVEATED_MEMBER,
    99  					[]string{"anothercondition"},
   100  					"",
   101  				},
   102  				{
   103  					"document:foo#view@user:sarah",
   104  					map[string]any{
   105  						"anothercondition": "15",
   106  					},
   107  					v1.ResourceCheckResult_CAVEATED_MEMBER,
   108  					[]string{"somecondition", "somebool"},
   109  					"",
   110  				},
   111  				{
   112  					"document:foo#view@user:sarah",
   113  					map[string]any{
   114  						"somecondition":    "42",
   115  						"anothercondition": "15",
   116  						"somebool":         true,
   117  					},
   118  					v1.ResourceCheckResult_MEMBER,
   119  					nil,
   120  					"",
   121  				},
   122  				{
   123  					"document:foo#view@user:john",
   124  					map[string]any{
   125  						"anothercondition": "14",
   126  					},
   127  					v1.ResourceCheckResult_NOT_MEMBER,
   128  					nil,
   129  					"",
   130  				},
   131  				{
   132  					"document:foo#view@user:john",
   133  					map[string]any{
   134  						"anothercondition": "15",
   135  					},
   136  					v1.ResourceCheckResult_MEMBER,
   137  					nil,
   138  					"",
   139  				},
   140  				{
   141  					"document:bar#view@user:jane", nil, v1.ResourceCheckResult_MEMBER,
   142  					nil,
   143  					"",
   144  				},
   145  				{
   146  					"document:foo#view@user:peter",
   147  					nil,
   148  					v1.ResourceCheckResult_NOT_MEMBER,
   149  					nil,
   150  					"",
   151  				},
   152  				{
   153  					"document:foo#view@user:vic",
   154  					nil,
   155  					v1.ResourceCheckResult_MEMBER,
   156  					nil,
   157  					"",
   158  				},
   159  				{
   160  					"document:foo#view@user:blippy",
   161  					nil,
   162  					v1.ResourceCheckResult_NOT_MEMBER,
   163  					nil,
   164  					"",
   165  				},
   166  				{
   167  					"document:foo#view@user:noa",
   168  					nil,
   169  					v1.ResourceCheckResult_NOT_MEMBER,
   170  					nil,
   171  					"",
   172  				},
   173  				{
   174  					"document:foo#view@user:wayne",
   175  					nil,
   176  					v1.ResourceCheckResult_MEMBER,
   177  					nil,
   178  					"caveat with name `invalid` not found",
   179  				},
   180  			},
   181  		},
   182  		{
   183  			"overridden context test",
   184  			`definition user {}
   185  
   186  			definition document {
   187  				relation viewer: user | user with testcaveat
   188  				permission view = viewer
   189  			}
   190  			
   191  			caveat testcaveat(somecondition int) {
   192  				somecondition == 42
   193  			}
   194  			`,
   195  			[]caveatedUpdate{
   196  				{core.RelationTupleUpdate_CREATE, "document:foo#viewer@user:tom", "testcaveat", map[string]any{
   197  					"somecondition": 41, // not allowed
   198  				}},
   199  			},
   200  			[]check{
   201  				{
   202  					"document:foo#view@user:tom",
   203  					nil,
   204  					v1.ResourceCheckResult_NOT_MEMBER,
   205  					nil,
   206  					"",
   207  				},
   208  				{
   209  					"document:foo#view@user:tom",
   210  					map[string]any{
   211  						"somecondition": 42, // still not a member, because the written value takes precedence
   212  					},
   213  					v1.ResourceCheckResult_NOT_MEMBER,
   214  					nil,
   215  					"",
   216  				},
   217  			},
   218  		},
   219  		{
   220  			"intersection test",
   221  			`definition user {}
   222  
   223  			definition document {
   224  				relation viewer: user | user with viewcaveat
   225  				relation editor: user | user with editcaveat
   226  				permission view_and_edit = viewer & editor
   227  			}
   228  			
   229  			caveat viewcaveat(somecondition int) {
   230  				somecondition == 42
   231  			}
   232  
   233  			caveat editcaveat(today string) {
   234  				today == 'tuesday'
   235  			}
   236  			`,
   237  			[]caveatedUpdate{
   238  				{
   239  					core.RelationTupleUpdate_CREATE,
   240  					"document:foo#viewer@user:tom",
   241  					"viewcaveat",
   242  					nil,
   243  				},
   244  				{
   245  					core.RelationTupleUpdate_CREATE,
   246  					"document:foo#editor@user:tom",
   247  					"editcaveat",
   248  					nil,
   249  				},
   250  			},
   251  			[]check{
   252  				{
   253  					"document:foo#view_and_edit@user:tom",
   254  					map[string]any{
   255  						"somecondition": "42",
   256  						"today":         "wednesday",
   257  					},
   258  					v1.ResourceCheckResult_NOT_MEMBER,
   259  					nil,
   260  					"",
   261  				},
   262  				{
   263  					"document:foo#view_and_edit@user:tom",
   264  					map[string]any{
   265  						"somecondition": "41",
   266  						"today":         "tuesday",
   267  					},
   268  					v1.ResourceCheckResult_NOT_MEMBER,
   269  					nil,
   270  					"",
   271  				},
   272  				{
   273  					"document:foo#view_and_edit@user:tom",
   274  					map[string]any{
   275  						"somecondition": "42",
   276  						"today":         "tuesday",
   277  					},
   278  					v1.ResourceCheckResult_MEMBER,
   279  					nil,
   280  					"",
   281  				},
   282  			},
   283  		},
   284  		{
   285  			"exclusion test",
   286  			`definition user {}
   287  
   288  			definition document {
   289  				relation viewer: user | user with viewcaveat
   290  				relation banned: user | user with bannedcaveat
   291  				permission view_not_banned = viewer - banned
   292  			}
   293  			
   294  			caveat viewcaveat(somecondition int) {
   295  				somecondition == 42
   296  			}
   297  			
   298  			caveat bannedcaveat(region string) {
   299  				region == 'bad'
   300  			}
   301  			`,
   302  			[]caveatedUpdate{
   303  				{
   304  					core.RelationTupleUpdate_CREATE,
   305  					"document:foo#viewer@user:tom",
   306  					"viewcaveat",
   307  					nil,
   308  				},
   309  				{
   310  					core.RelationTupleUpdate_CREATE,
   311  					"document:foo#banned@user:tom",
   312  					"bannedcaveat",
   313  					nil,
   314  				},
   315  			},
   316  			[]check{
   317  				{
   318  					"document:foo#view_not_banned@user:tom",
   319  					map[string]any{
   320  						"somecondition": "42",
   321  						"region":        "bad",
   322  					},
   323  					v1.ResourceCheckResult_NOT_MEMBER,
   324  					nil,
   325  					"",
   326  				},
   327  				{
   328  					"document:foo#view_not_banned@user:tom",
   329  					map[string]any{
   330  						"somecondition": "41",
   331  						"region":        "good",
   332  					},
   333  					v1.ResourceCheckResult_NOT_MEMBER,
   334  					nil,
   335  					"",
   336  				},
   337  				{
   338  					"document:foo#view_not_banned@user:tom",
   339  					map[string]any{
   340  						"somecondition": "42",
   341  						"region":        "good",
   342  					},
   343  					v1.ResourceCheckResult_MEMBER,
   344  					nil,
   345  					"",
   346  				},
   347  			},
   348  		},
   349  		{
   350  			"IP Allowlists example",
   351  			`definition user {}
   352  
   353  			definition organization {
   354  				relation members: user
   355  				relation ip_allowlist_policy:  organization#members | organization#members with ip_allowlist
   356  			
   357  				permission policy = ip_allowlist_policy
   358  			}
   359  			
   360  			definition repository {
   361  				relation owner: organization
   362  				relation reader: user
   363  			
   364  				permission read = reader & owner->policy
   365  			}
   366  			
   367  			caveat ip_allowlist(user_ip ipaddress, cidr string) {
   368  				user_ip.in_cidr(cidr)
   369  			}
   370  			`,
   371  			[]caveatedUpdate{
   372  				{
   373  					core.RelationTupleUpdate_CREATE,
   374  					"repository:foobar#owner@organization:myorg",
   375  					"",
   376  					nil,
   377  				},
   378  				{
   379  					core.RelationTupleUpdate_CREATE,
   380  					"organization:myorg#members@user:johndoe",
   381  					"",
   382  					nil,
   383  				},
   384  				{
   385  					core.RelationTupleUpdate_CREATE,
   386  					"repository:foobar#reader@user:johndoe",
   387  					"",
   388  					nil,
   389  				},
   390  				{
   391  					core.RelationTupleUpdate_CREATE,
   392  					"organization:myorg#ip_allowlist_policy@organization:myorg#members",
   393  					"ip_allowlist",
   394  					map[string]any{
   395  						"cidr": "192.168.0.0/16",
   396  					},
   397  				},
   398  			},
   399  			[]check{
   400  				{
   401  					"repository:foobar#read@user:johndoe",
   402  					nil,
   403  					v1.ResourceCheckResult_CAVEATED_MEMBER,
   404  					[]string{"user_ip"},
   405  					"",
   406  				},
   407  				{
   408  					"repository:foobar#read@user:johndoe",
   409  					map[string]any{
   410  						"user_ip": types.MustParseIPAddress("192.168.0.1"),
   411  					},
   412  					v1.ResourceCheckResult_MEMBER,
   413  					nil,
   414  					"",
   415  				},
   416  				{
   417  					"repository:foobar#read@user:johndoe",
   418  					map[string]any{
   419  						"user_ip": types.MustParseIPAddress("9.2.3.1"),
   420  					},
   421  					v1.ResourceCheckResult_NOT_MEMBER,
   422  					nil,
   423  					"",
   424  				},
   425  				{
   426  					"repository:foobar#read@user:johndoe",
   427  					map[string]any{
   428  						"user_ip": "192.168.0.1",
   429  					},
   430  					v1.ResourceCheckResult_MEMBER,
   431  					nil,
   432  					"",
   433  				},
   434  			},
   435  		},
   436  		{
   437  			"App attributes example",
   438  			`definition application {}
   439  			definition group {
   440  				relation member: application | application with attributes_match
   441  				permission allowed = member
   442  			}
   443  			
   444  			caveat attributes_match(expected map<any>, provided map<any>) {
   445  				expected.isSubtreeOf(provided)
   446  			}
   447  			`,
   448  			[]caveatedUpdate{
   449  				{
   450  					core.RelationTupleUpdate_CREATE,
   451  					"group:ui_apps#member@application:frontend_app",
   452  					"attributes_match",
   453  					map[string]any{
   454  						"expected": map[string]any{"type": "frontend", "region": "eu"},
   455  					},
   456  				},
   457  				{
   458  					core.RelationTupleUpdate_CREATE,
   459  					"group:backend_apps#member@application:backend_app",
   460  					"attributes_match",
   461  					map[string]any{
   462  						"expected": map[string]any{
   463  							"type": "backend", "region": "us",
   464  							"additional_attrs": map[string]any{
   465  								"tag1": 100,
   466  								"tag2": false,
   467  							},
   468  						},
   469  					},
   470  				},
   471  			},
   472  			[]check{
   473  				{
   474  					"group:ui_apps#allowed@application:frontend_app",
   475  					map[string]any{
   476  						"provided": map[string]any{"type": "frontend", "region": "eu", "team": "shop"},
   477  					},
   478  					v1.ResourceCheckResult_MEMBER,
   479  					nil,
   480  					"",
   481  				},
   482  				{
   483  					"group:ui_apps#allowed@application:frontend_app",
   484  					map[string]any{
   485  						"provided": map[string]any{"type": "frontend", "region": "us"},
   486  					},
   487  					v1.ResourceCheckResult_NOT_MEMBER,
   488  					nil,
   489  					"",
   490  				},
   491  				{
   492  					"group:backend_apps#allowed@application:backend_app",
   493  					map[string]any{
   494  						"provided": map[string]any{
   495  							"type": "backend", "region": "us", "team": "shop",
   496  							"additional_attrs": map[string]any{
   497  								"tag1": 100.0,
   498  								"tag2": false,
   499  								"tag3": "hi",
   500  							},
   501  						},
   502  					},
   503  					v1.ResourceCheckResult_MEMBER,
   504  					nil,
   505  					"",
   506  				},
   507  				{
   508  					"group:backend_apps#allowed@application:backend_app",
   509  					map[string]any{
   510  						"provided": map[string]any{
   511  							"type": "backend", "region": "us", "team": "shop",
   512  							"additional_attrs": map[string]any{
   513  								"tag1": 200.0,
   514  								"tag2": false,
   515  							},
   516  						},
   517  					},
   518  					v1.ResourceCheckResult_NOT_MEMBER,
   519  					nil,
   520  					"",
   521  				},
   522  			},
   523  		},
   524  		{
   525  			"authorize if resource was created before subject",
   526  			`definition root {
   527  				relation actors: actor
   528  			}
   529  			definition resource {
   530  				relation creation_policy: root#actors | root#actors with created_before
   531  				permission tag = creation_policy
   532  			}
   533  			
   534  			definition actor {}
   535  			
   536  			caveat created_before(actor_created_at string, created_at string) {
   537  				timestamp(actor_created_at) > timestamp(created_at)
   538  			}
   539  			`,
   540  			[]caveatedUpdate{
   541  				{
   542  					core.RelationTupleUpdate_CREATE,
   543  					"resource:foo#creation_policy@root:root#actors",
   544  					"created_before",
   545  					map[string]any{
   546  						"created_at": "2022-01-01T10:00:00.021Z",
   547  					},
   548  				},
   549  				{
   550  					core.RelationTupleUpdate_CREATE,
   551  					"root:root#actors@actor:johndoe",
   552  					"",
   553  					nil,
   554  				},
   555  			},
   556  			[]check{
   557  				{
   558  					"resource:foo#tag@actor:johndoe",
   559  					map[string]any{
   560  						"actor_created_at": "2022-01-01T11:00:00.021Z",
   561  					},
   562  					v1.ResourceCheckResult_MEMBER,
   563  					nil,
   564  					"",
   565  				},
   566  				{
   567  					"resource:foo#tag@actor:johndoe",
   568  					map[string]any{
   569  						"actor_created_at": "2022-01-01T09:00:00.021Z",
   570  					},
   571  					v1.ResourceCheckResult_NOT_MEMBER,
   572  					nil,
   573  					"",
   574  				},
   575  			},
   576  		},
   577  		{
   578  			"time-bound permission",
   579  			`definition resource {
   580  				relation reader: user | user with not_expired
   581  				permission view = reader
   582  			}
   583  			
   584  			caveat not_expired(expiration string, now string) {
   585  				timestamp(now) < timestamp(expiration)
   586  			}
   587  
   588  			definition user {}`,
   589  			[]caveatedUpdate{
   590  				{
   591  					core.RelationTupleUpdate_CREATE,
   592  					"resource:foo#reader@user:sarah",
   593  					"not_expired",
   594  					map[string]any{
   595  						"expiration": "2030-01-01T10:00:00.021Z",
   596  						"now":        "2020-01-01T10:00:00.021Z",
   597  					},
   598  				},
   599  				{
   600  					core.RelationTupleUpdate_CREATE,
   601  					"resource:foo#reader@user:john",
   602  					"not_expired",
   603  					map[string]any{
   604  						"expiration": "2020-01-01T10:00:00.021Z",
   605  						"now":        "2020-01-01T10:00:00.021Z",
   606  					},
   607  				},
   608  			},
   609  			[]check{
   610  				{
   611  					"resource:foo#view@user:sarah",
   612  					nil,
   613  					v1.ResourceCheckResult_MEMBER,
   614  					nil,
   615  					"",
   616  				},
   617  				{
   618  					"resource:foo#view@user:john",
   619  					nil,
   620  					v1.ResourceCheckResult_NOT_MEMBER,
   621  					nil,
   622  					"",
   623  				},
   624  			},
   625  		},
   626  		{
   627  			"legal-guardian example",
   628  			`definition claim {
   629  				relation claimer: user
   630  				relation dependent_of: user#dependent_of | user#dependent_of with legal_guardian
   631  			  
   632  				permission view = claimer + dependent_of
   633  			}
   634  
   635  			caveat legal_guardian(age int, class string) {
   636  				age < 12 || (class != "sensitive" && age > 12 && age < 18)
   637  			}
   638  			
   639  			definition user {
   640  				relation dependent_of: user
   641  			}`,
   642  			[]caveatedUpdate{
   643  				{
   644  					core.RelationTupleUpdate_CREATE,
   645  					"user:son#dependent_of@user:father",
   646  					"",
   647  					nil,
   648  				},
   649  				{
   650  					core.RelationTupleUpdate_CREATE,
   651  					"claim:broken_leg#dependent_of@user:son#dependent_of",
   652  					"legal_guardian",
   653  					map[string]any{
   654  						"age":   10,
   655  						"class": "non-sensitive",
   656  					},
   657  				},
   658  				{
   659  					core.RelationTupleUpdate_CREATE,
   660  					"user:daughter#dependent_of@user:father",
   661  					"",
   662  					nil,
   663  				},
   664  				{
   665  					core.RelationTupleUpdate_CREATE,
   666  					"claim:broken_arm#dependent_of@user:daughter#dependent_of",
   667  					"legal_guardian",
   668  					map[string]any{
   669  						"age":   14,
   670  						"class": "non-sensitive",
   671  					},
   672  				},
   673  				{
   674  					core.RelationTupleUpdate_CREATE,
   675  					"claim:sensitive_matter#dependent_of@user:daughter#dependent_of",
   676  					"legal_guardian",
   677  					map[string]any{
   678  						"age":   14,
   679  						"class": "sensitive",
   680  					},
   681  				},
   682  			},
   683  			[]check{
   684  				{
   685  					"claim:broken_leg#view@user:father",
   686  					nil,
   687  					v1.ResourceCheckResult_MEMBER,
   688  					nil,
   689  					"",
   690  				},
   691  				{
   692  					"claim:broken_arm#view@user:father",
   693  					nil,
   694  					v1.ResourceCheckResult_MEMBER,
   695  					nil,
   696  					"",
   697  				},
   698  				{
   699  					"claim:sensitive_matter#view@user:father",
   700  					nil,
   701  					v1.ResourceCheckResult_NOT_MEMBER,
   702  					nil,
   703  					"",
   704  				},
   705  			},
   706  		},
   707  		{
   708  			"context type error test",
   709  			`definition user {}
   710  
   711  			definition document {
   712  				relation viewer: user | user with testcaveat
   713  
   714  				permission view = viewer
   715  			}
   716  			
   717  			caveat testcaveat(somecondition uint) {
   718  				somecondition == 42
   719  			}
   720  			`,
   721  			[]caveatedUpdate{
   722  				{core.RelationTupleUpdate_CREATE, "document:foo#viewer@user:sarah", "testcaveat", nil},
   723  			},
   724  			[]check{
   725  				{
   726  					"document:foo#view@user:sarah",
   727  					map[string]any{
   728  						"somecondition": "43a",
   729  					},
   730  					v1.ResourceCheckResult_NOT_MEMBER,
   731  					[]string{},
   732  					"type error for parameters for caveat `testcaveat`: could not convert context parameter `somecondition`: for uint: a uint64 value is required, but found invalid string value `43a`",
   733  				},
   734  				{
   735  					"document:foo#view@user:sarah",
   736  					map[string]any{
   737  						"somecondition": "-43",
   738  					},
   739  					v1.ResourceCheckResult_NOT_MEMBER,
   740  					[]string{},
   741  					"type error for parameters for caveat `testcaveat`: could not convert context parameter `somecondition`: for uint: a uint value is required, but found int64 value `-43`",
   742  				},
   743  			},
   744  		},
   745  		{
   746  			"schema caveat test",
   747  			`
   748  			caveat testcaveat(somecondition uint) {
   749  				somecondition == 42
   750  			}
   751  
   752  			definition user {}
   753  
   754  			definition document {
   755  				relation viewer: user with testcaveat
   756  
   757  				permission view = viewer
   758  			}`,
   759  			[]caveatedUpdate{
   760  				{core.RelationTupleUpdate_CREATE, "document:foo#viewer@user:sarah", "testcaveat", nil},
   761  			},
   762  			[]check{
   763  				{
   764  					"document:foo#view@user:sarah",
   765  					map[string]any{
   766  						"somecondition": "43a",
   767  					},
   768  					v1.ResourceCheckResult_NOT_MEMBER,
   769  					[]string{},
   770  					"type error for parameters for caveat `testcaveat`: could not convert context parameter `somecondition`: for uint: a uint64 value is required, but found invalid string value `43a`",
   771  				},
   772  				{
   773  					"document:foo#view@user:sarah",
   774  					map[string]any{
   775  						"somecondition": "-43",
   776  					},
   777  					v1.ResourceCheckResult_NOT_MEMBER,
   778  					[]string{},
   779  					"type error for parameters for caveat `testcaveat`: could not convert context parameter `somecondition`: for uint: a uint value is required, but found int64 value `-43`",
   780  				},
   781  				{
   782  					"document:foo#view@user:sarah",
   783  					map[string]any{
   784  						"somecondition": "41",
   785  					},
   786  					v1.ResourceCheckResult_NOT_MEMBER,
   787  					[]string{},
   788  					"",
   789  				},
   790  				{
   791  					"document:foo#view@user:sarah",
   792  					map[string]any{
   793  						"somecondition": "42",
   794  					},
   795  					v1.ResourceCheckResult_MEMBER,
   796  					[]string{},
   797  					"",
   798  				},
   799  			},
   800  		},
   801  	}
   802  
   803  	for _, tt := range testCases {
   804  		tt := tt
   805  		t.Run(tt.name, func(t *testing.T) {
   806  			ds, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC)
   807  			require.NoError(t, err)
   808  
   809  			dispatch := graph.NewLocalOnlyDispatcher(10)
   810  			ctx := log.Logger.WithContext(datastoremw.ContextWithHandle(context.Background()))
   811  			require.NoError(t, datastoremw.SetInContext(ctx, ds))
   812  
   813  			revision, err := writeCaveatedTuples(ctx, t, ds, tt.schema, tt.updates)
   814  			require.NoError(t, err)
   815  
   816  			for _, r := range tt.checks {
   817  				r := r
   818  				t.Run(fmt.Sprintf("%s::%v", r.check, r.context), func(t *testing.T) {
   819  					rel := tuple.MustParse(r.check)
   820  
   821  					result, _, err := computed.ComputeCheck(ctx, dispatch,
   822  						computed.CheckParameters{
   823  							ResourceType: &core.RelationReference{
   824  								Namespace: rel.ResourceAndRelation.Namespace,
   825  								Relation:  rel.ResourceAndRelation.Relation,
   826  							},
   827  							Subject:       rel.Subject,
   828  							CaveatContext: r.context,
   829  							AtRevision:    revision,
   830  							MaximumDepth:  50,
   831  							DebugOption:   computed.BasicDebuggingEnabled,
   832  						},
   833  						rel.ResourceAndRelation.ObjectId,
   834  					)
   835  
   836  					if r.error != "" {
   837  						require.NotNil(t, err, "missing required error: %s", r.error)
   838  						require.Equal(t, err.Error(), r.error)
   839  					} else {
   840  						require.NoError(t, err)
   841  						require.Equal(t, v1.ResourceCheckResult_Membership_name[int32(r.member)], v1.ResourceCheckResult_Membership_name[int32(result.Membership)], "mismatch for %s with context %v", r.check, r.context)
   842  
   843  						if result.Membership == v1.ResourceCheckResult_CAVEATED_MEMBER {
   844  							require.Equal(t, r.expectedMissingFields, result.MissingExprFields)
   845  						}
   846  					}
   847  				})
   848  			}
   849  		})
   850  	}
   851  }
   852  
   853  func TestComputeCheckError(t *testing.T) {
   854  	ds, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC)
   855  	require.NoError(t, err)
   856  
   857  	dispatch := graph.NewLocalOnlyDispatcher(10)
   858  	ctx := log.Logger.WithContext(datastoremw.ContextWithHandle(context.Background()))
   859  	require.NoError(t, datastoremw.SetInContext(ctx, ds))
   860  
   861  	_, _, err = computed.ComputeCheck(ctx, dispatch,
   862  		computed.CheckParameters{
   863  			ResourceType: &core.RelationReference{
   864  				Namespace: "a",
   865  				Relation:  "b",
   866  			},
   867  			Subject:       &core.ObjectAndRelation{},
   868  			CaveatContext: nil,
   869  			AtRevision:    datastore.NoRevision,
   870  			MaximumDepth:  50,
   871  			DebugOption:   computed.BasicDebuggingEnabled,
   872  		},
   873  		"id",
   874  	)
   875  	require.Error(t, err)
   876  }
   877  
   878  func TestComputeBulkCheck(t *testing.T) {
   879  	ds, err := memdb.NewMemdbDatastore(0, 0, memdb.DisableGC)
   880  	require.NoError(t, err)
   881  
   882  	dispatch := graph.NewLocalOnlyDispatcher(10)
   883  	ctx := log.Logger.WithContext(datastoremw.ContextWithHandle(context.Background()))
   884  	require.NoError(t, datastoremw.SetInContext(ctx, ds))
   885  
   886  	revision, err := writeCaveatedTuples(ctx, t, ds, `
   887  	definition user {}
   888  
   889  	caveat somecaveat(somecondition int) {
   890  		somecondition == 42
   891  	}
   892  
   893  	definition document {
   894  		relation viewer: user | user with somecaveat
   895  		permission view = viewer
   896  	}
   897  	`, []caveatedUpdate{
   898  		{core.RelationTupleUpdate_CREATE, "document:direct#viewer@user:tom", "", nil},
   899  		{core.RelationTupleUpdate_CREATE, "document:first#viewer@user:tom", "somecaveat", map[string]any{
   900  			"somecondition": 42,
   901  		}},
   902  		{core.RelationTupleUpdate_CREATE, "document:second#viewer@user:tom", "somecaveat", map[string]any{}},
   903  		{core.RelationTupleUpdate_CREATE, "document:third#viewer@user:tom", "somecaveat", map[string]any{
   904  			"somecondition": 32,
   905  		}},
   906  	})
   907  	require.NoError(t, err)
   908  
   909  	resp, _, err := computed.ComputeBulkCheck(ctx, dispatch,
   910  		computed.CheckParameters{
   911  			ResourceType: &core.RelationReference{
   912  				Namespace: "document",
   913  				Relation:  "view",
   914  			},
   915  			Subject: &core.ObjectAndRelation{
   916  				Namespace: "user",
   917  				ObjectId:  "tom",
   918  				Relation:  "...",
   919  			},
   920  			CaveatContext: nil,
   921  			AtRevision:    revision,
   922  			MaximumDepth:  50,
   923  			DebugOption:   computed.NoDebugging,
   924  		},
   925  		[]string{"direct", "first", "second", "third"},
   926  	)
   927  	require.NoError(t, err)
   928  
   929  	require.Equal(t, resp["direct"].Membership, v1.ResourceCheckResult_MEMBER)
   930  	require.Equal(t, resp["first"].Membership, v1.ResourceCheckResult_MEMBER)
   931  	require.Equal(t, resp["second"].Membership, v1.ResourceCheckResult_CAVEATED_MEMBER)
   932  	require.Equal(t, resp["third"].Membership, v1.ResourceCheckResult_NOT_MEMBER)
   933  }
   934  
   935  func writeCaveatedTuples(ctx context.Context, _ *testing.T, ds datastore.Datastore, schema string, updates []caveatedUpdate) (datastore.Revision, error) {
   936  	compiled, err := compiler.Compile(compiler.InputSchema{
   937  		Source:       "schema",
   938  		SchemaString: schema,
   939  	}, compiler.AllowUnprefixedObjectType())
   940  	if err != nil {
   941  		return datastore.NoRevision, err
   942  	}
   943  
   944  	return ds.ReadWriteTx(ctx, func(ctx context.Context, rwt datastore.ReadWriteTransaction) error {
   945  		if err := rwt.WriteNamespaces(ctx, compiled.ObjectDefinitions...); err != nil {
   946  			return err
   947  		}
   948  
   949  		if err := rwt.WriteCaveats(ctx, compiled.CaveatDefinitions); err != nil {
   950  			return err
   951  		}
   952  
   953  		var rtu []*core.RelationTupleUpdate
   954  		for _, updt := range updates {
   955  			rtu = append(rtu, &core.RelationTupleUpdate{
   956  				Operation: updt.Operation,
   957  				Tuple:     caveatedRelationTuple(updt.tuple, updt.caveatName, updt.context),
   958  			})
   959  		}
   960  
   961  		return rwt.WriteRelationships(ctx, rtu)
   962  	})
   963  }
   964  
   965  func caveatedRelationTuple(relationTuple string, caveatName string, context map[string]any) *core.RelationTuple {
   966  	c := tuple.MustParse(relationTuple)
   967  	strct, err := structpb.NewStruct(context)
   968  	if err != nil {
   969  		panic(err)
   970  	}
   971  	if caveatName != "" {
   972  		c.Caveat = &core.ContextualizedCaveat{
   973  			CaveatName: caveatName,
   974  			Context:    strct,
   975  		}
   976  	}
   977  	return c
   978  }