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

     1  package v1_test
     2  
     3  import (
     4  	"context"
     5  	"sort"
     6  	"strings"
     7  	"testing"
     8  
     9  	"github.com/authzed/authzed-go/pkg/requestmeta"
    10  	"github.com/authzed/authzed-go/pkg/responsemeta"
    11  	v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
    12  	"github.com/stretchr/testify/require"
    13  	"google.golang.org/grpc"
    14  	"google.golang.org/grpc/metadata"
    15  	"google.golang.org/protobuf/encoding/prototext"
    16  	"google.golang.org/protobuf/types/known/structpb"
    17  
    18  	"github.com/authzed/spicedb/internal/datastore/memdb"
    19  	tf "github.com/authzed/spicedb/internal/testfixtures"
    20  	"github.com/authzed/spicedb/internal/testserver"
    21  	"github.com/authzed/spicedb/pkg/datastore"
    22  	"github.com/authzed/spicedb/pkg/genutil/mapz"
    23  	core "github.com/authzed/spicedb/pkg/proto/core/v1"
    24  	"github.com/authzed/spicedb/pkg/tuple"
    25  	"github.com/authzed/spicedb/pkg/zedtoken"
    26  )
    27  
    28  type debugCheckRequest struct {
    29  	resource      *v1.ObjectReference
    30  	permission    string
    31  	subject       *v1.SubjectReference
    32  	caveatContext map[string]any
    33  }
    34  
    35  type rda func(req *require.Assertions, debugInfo *v1.DebugInformation)
    36  
    37  type debugCheckInfo struct {
    38  	name                           string
    39  	checkRequest                   debugCheckRequest
    40  	expectedPermission             v1.CheckPermissionResponse_Permissionship
    41  	expectedMinimumSubProblemCount int
    42  	runDebugAssertions             []rda
    43  }
    44  
    45  func expectDebugFrames(permissionNames ...string) rda {
    46  	return func(req *require.Assertions, debugInfo *v1.DebugInformation) {
    47  		found := mapz.NewSet[string]()
    48  		for _, sp := range debugInfo.Check.GetSubProblems().Traces {
    49  			for _, permissionName := range permissionNames {
    50  				if sp.Permission == permissionName {
    51  					found.Insert(permissionName)
    52  				}
    53  			}
    54  		}
    55  
    56  		foundNames := found.AsSlice()
    57  		sort.Strings(permissionNames)
    58  		sort.Strings(foundNames)
    59  
    60  		req.Equal(permissionNames, foundNames, "missing expected subproblem(s)")
    61  	}
    62  }
    63  
    64  func expectCaveat(caveatExpression string) rda {
    65  	return func(req *require.Assertions, debugInfo *v1.DebugInformation) {
    66  		req.Equal(caveatExpression, debugInfo.Check.CaveatEvaluationInfo.Expression)
    67  	}
    68  }
    69  
    70  func expectMissingContext(context ...string) rda {
    71  	sort.Strings(context)
    72  	return func(req *require.Assertions, debugInfo *v1.DebugInformation) {
    73  		missing := debugInfo.Check.CaveatEvaluationInfo.PartialCaveatInfo.MissingRequiredContext
    74  		sort.Strings(missing)
    75  		req.Equal(context, missing)
    76  	}
    77  }
    78  
    79  func findFrame(checkTrace *v1.CheckDebugTrace, resourceType string, permissionName string) *v1.CheckDebugTrace {
    80  	if checkTrace.Resource.ObjectType == resourceType && checkTrace.Permission == permissionName {
    81  		return checkTrace
    82  	}
    83  
    84  	subProblems := checkTrace.GetSubProblems()
    85  	if subProblems != nil {
    86  		for _, sp := range subProblems.Traces {
    87  			found := findFrame(sp, resourceType, permissionName)
    88  			if found != nil {
    89  				return found
    90  			}
    91  		}
    92  	}
    93  	return nil
    94  }
    95  
    96  func TestCheckPermissionWithDebug(t *testing.T) {
    97  	tcs := []struct {
    98  		name          string
    99  		schema        string
   100  		relationships []*core.RelationTuple
   101  		toTest        []debugCheckInfo
   102  	}{
   103  		{
   104  			"basic debug",
   105  			`definition user {}
   106  			
   107  			 definition document {
   108  				relation editor: user
   109  				relation viewer: user
   110  				permission edit = editor
   111  				permission view = viewer + edit
   112  			 }
   113  			`,
   114  			[]*core.RelationTuple{
   115  				tuple.MustParse("document:first#viewer@user:tom"),
   116  				tuple.MustParse("document:first#editor@user:sarah"),
   117  			},
   118  			[]debugCheckInfo{
   119  				{
   120  					"sarah as editor",
   121  					debugCheckRequest{
   122  						obj("document", "first"),
   123  						"view",
   124  						sub("user", "sarah", ""),
   125  						nil,
   126  					},
   127  					v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION,
   128  					1,
   129  					[]rda{expectDebugFrames("editor")},
   130  				},
   131  				{
   132  					"tom as viewer",
   133  					debugCheckRequest{
   134  						obj("document", "first"),
   135  						"view",
   136  						sub("user", "tom", ""),
   137  						nil,
   138  					},
   139  					v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION,
   140  					1,
   141  					[]rda{expectDebugFrames("viewer")},
   142  				},
   143  				{
   144  					"benny as nothing",
   145  					debugCheckRequest{
   146  						obj("document", "first"),
   147  						"view",
   148  						sub("user", "benny", ""),
   149  						nil,
   150  					},
   151  					v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION,
   152  					2,
   153  					[]rda{expectDebugFrames("viewer", "editor")},
   154  				},
   155  			},
   156  		},
   157  		{
   158  			"caveated debug",
   159  			`definition user {}
   160  			
   161  			caveat somecaveat(somecondition int) {
   162  				somecondition == 42
   163  			}
   164  
   165  			 definition document {
   166  				relation another: user
   167  				relation viewer: user with somecaveat
   168  				permission view = viewer + another
   169  			 }
   170  			`,
   171  			[]*core.RelationTuple{
   172  				tuple.MustParse("document:first#viewer@user:tom"),
   173  				tuple.MustParse("document:first#viewer@user:sarah[somecaveat]"),
   174  			},
   175  			[]debugCheckInfo{
   176  				{
   177  					"sarah as viewer",
   178  					debugCheckRequest{
   179  						obj("document", "first"),
   180  						"view",
   181  						sub("user", "sarah", ""),
   182  						map[string]any{
   183  							"somecondition": 42,
   184  						},
   185  					},
   186  					v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION,
   187  					1,
   188  					[]rda{expectDebugFrames("viewer"), expectCaveat("somecondition == 42")},
   189  				},
   190  				{
   191  					"sarah as not viewer",
   192  					debugCheckRequest{
   193  						obj("document", "first"),
   194  						"view",
   195  						sub("user", "sarah", ""),
   196  						map[string]any{
   197  							"somecondition": 31,
   198  						},
   199  					},
   200  					v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION,
   201  					1,
   202  					[]rda{expectDebugFrames("viewer"), expectCaveat("somecondition == 42")},
   203  				},
   204  				{
   205  					"sarah as conditional viewer",
   206  					debugCheckRequest{
   207  						obj("document", "first"),
   208  						"view",
   209  						sub("user", "sarah", ""),
   210  						map[string]any{},
   211  					},
   212  					v1.CheckPermissionResponse_PERMISSIONSHIP_CONDITIONAL_PERMISSION,
   213  					1,
   214  					[]rda{
   215  						expectDebugFrames("viewer"),
   216  						expectCaveat("somecondition == 42"),
   217  						expectMissingContext("somecondition"),
   218  					},
   219  				},
   220  			},
   221  		},
   222  		{
   223  			"batched recursive",
   224  			`definition user {}
   225  
   226  			definition folder {
   227  				relation parent: folder
   228  				relation fviewer: user
   229  				permission fview = fviewer + parent->fview
   230  			}
   231  			
   232  			 definition document {
   233  				relation folder: folder
   234  				relation viewer: user
   235  				permission view = viewer + folder->fview
   236  			 }
   237  			`,
   238  			[]*core.RelationTuple{
   239  				tuple.MustParse("document:first#viewer@user:tom"),
   240  				tuple.MustParse("document:first#folder@folder:f1"),
   241  				tuple.MustParse("document:first#folder@folder:f2"),
   242  				tuple.MustParse("document:first#folder@folder:f3"),
   243  				tuple.MustParse("document:first#folder@folder:f4"),
   244  				tuple.MustParse("document:first#folder@folder:f5"),
   245  				tuple.MustParse("document:first#folder@folder:f6"),
   246  				tuple.MustParse("folder:f1#parent@folder:f1p"),
   247  				tuple.MustParse("folder:f2#parent@folder:f2p"),
   248  				tuple.MustParse("folder:f3#parent@folder:f3p"),
   249  				tuple.MustParse("folder:f4#parent@folder:f4p"),
   250  				tuple.MustParse("folder:f5#parent@folder:f5p"),
   251  				tuple.MustParse("folder:f6#parent@folder:f6p"),
   252  				tuple.MustParse("folder:f6p#fviewer@user:sarah"),
   253  			},
   254  			[]debugCheckInfo{
   255  				{
   256  					"tom as viewer",
   257  					debugCheckRequest{
   258  						obj("document", "first"),
   259  						"view",
   260  						sub("user", "tom", ""),
   261  						nil,
   262  					},
   263  					v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION,
   264  					1,
   265  					[]rda{expectDebugFrames("viewer")},
   266  				},
   267  				{
   268  					"sarah as recursive viewer",
   269  					debugCheckRequest{
   270  						obj("document", "first"),
   271  						"view",
   272  						sub("user", "sarah", ""),
   273  						nil,
   274  					},
   275  					v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION,
   276  					1,
   277  					[]rda{expectDebugFrames("fview")},
   278  				},
   279  				{
   280  					"benny as not viewer",
   281  					debugCheckRequest{
   282  						obj("document", "first"),
   283  						"view",
   284  						sub("user", "benny", ""),
   285  						nil,
   286  					},
   287  					v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION,
   288  					2,
   289  					[]rda{
   290  						expectDebugFrames("viewer"),
   291  						func(req *require.Assertions, debugInfo *v1.DebugInformation) {
   292  							// Ensure that all the resource IDs are batched into a single frame.
   293  							found := findFrame(debugInfo.Check, "folder", "fview")
   294  							req.NotNil(found)
   295  							req.Equal(6, len(strings.Split(found.Resource.ObjectId, ",")))
   296  
   297  							// Ensure there are no more than 2 subframes, to verify we haven't
   298  							// accidentally fanned out.
   299  							req.LessOrEqual(2, len(found.GetSubProblems().Traces))
   300  						},
   301  					},
   302  				},
   303  			},
   304  		},
   305  		{
   306  			"ip address caveat",
   307  			`definition user {}
   308  
   309  			caveat has_valid_ip(user_ip ipaddress, allowed_range string) {
   310  				user_ip.in_cidr(allowed_range)
   311  			}
   312  			
   313  			definition resource {
   314  				relation viewer: user | user with has_valid_ip
   315  			}`,
   316  			[]*core.RelationTuple{
   317  				tuple.MustParse(`resource:first#viewer@user:sarah[has_valid_ip:{"allowed_range":"192.168.0.0/16"}]`),
   318  			},
   319  			[]debugCheckInfo{
   320  				{
   321  					"sarah as viewer",
   322  					debugCheckRequest{
   323  						obj("resource", "first"),
   324  						"viewer",
   325  						sub("user", "sarah", ""),
   326  						map[string]any{
   327  							"user_ip": "192.168.1.100",
   328  						},
   329  					},
   330  					v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION,
   331  					0,
   332  					nil,
   333  				},
   334  			},
   335  		},
   336  		{
   337  			"multiple caveated debug",
   338  			`definition user {}
   339  			
   340  			caveat somecaveat(somecondition int) {
   341  				somecondition == 42
   342  			}
   343  
   344  			caveat anothercaveat(anothercondition string) {
   345  				anothercondition == "hello world"
   346  			}
   347  
   348  			definition org {
   349  				relation member: user with somecaveat
   350  			}
   351  
   352  			 definition document {
   353  				relation parent: org with anothercaveat
   354  				permission view = parent->member
   355  			 }
   356  			`,
   357  			[]*core.RelationTuple{
   358  				tuple.MustParse("document:first#parent@org:someorg[anothercaveat]"),
   359  				tuple.MustParse("org:someorg#member@user:sarah[somecaveat]"),
   360  			},
   361  			[]debugCheckInfo{
   362  				{
   363  					"sarah as viewer",
   364  					debugCheckRequest{
   365  						obj("document", "first"),
   366  						"view",
   367  						sub("user", "sarah", ""),
   368  						map[string]any{
   369  							"anothercondition": "hello world",
   370  							"somecondition":    "42",
   371  						},
   372  					},
   373  					v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION,
   374  					1,
   375  					[]rda{expectDebugFrames("member"), expectCaveat(`anothercondition == "hello world" && somecondition == 42`)},
   376  				},
   377  				{
   378  					"sarah as not viewer due to org",
   379  					debugCheckRequest{
   380  						obj("document", "first"),
   381  						"view",
   382  						sub("user", "sarah", ""),
   383  						map[string]any{
   384  							"anothercondition": "hi there",
   385  							"somecondition":    "42",
   386  						},
   387  					},
   388  					v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION,
   389  					1,
   390  					[]rda{expectDebugFrames("member"), expectCaveat(`anothercondition == "hello world"`)},
   391  				},
   392  				{
   393  					"sarah as not viewer due to viewer",
   394  					debugCheckRequest{
   395  						obj("document", "first"),
   396  						"view",
   397  						sub("user", "sarah", ""),
   398  						map[string]any{
   399  							"anothercondition": "hello world",
   400  							"somecondition":    "41",
   401  						},
   402  					},
   403  					v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION,
   404  					1,
   405  					[]rda{expectDebugFrames("member"), expectCaveat(`anothercondition == "hello world" && somecondition == 42`)},
   406  				},
   407  				{
   408  					"sarah as partially conditional viewer",
   409  					debugCheckRequest{
   410  						obj("document", "first"),
   411  						"view",
   412  						sub("user", "sarah", ""),
   413  						map[string]any{
   414  							"anothercondition": "hello world",
   415  						},
   416  					},
   417  					v1.CheckPermissionResponse_PERMISSIONSHIP_CONDITIONAL_PERMISSION,
   418  					1,
   419  					[]rda{
   420  						expectDebugFrames("member"),
   421  						expectMissingContext("somecondition"),
   422  					},
   423  				},
   424  				{
   425  					"sarah as fully conditional viewer",
   426  					debugCheckRequest{
   427  						obj("document", "first"),
   428  						"view",
   429  						sub("user", "sarah", ""),
   430  						map[string]any{},
   431  					},
   432  					v1.CheckPermissionResponse_PERMISSIONSHIP_CONDITIONAL_PERMISSION,
   433  					1,
   434  					[]rda{
   435  						expectDebugFrames("member"),
   436  						expectMissingContext("anothercondition"),
   437  					},
   438  				},
   439  			},
   440  		},
   441  	}
   442  
   443  	for _, tc := range tcs {
   444  		tc := tc
   445  		t.Run(tc.name, func(t *testing.T) {
   446  			req := require.New(t)
   447  			conn, cleanup, _, revision := testserver.NewTestServer(req, testTimedeltas[0], memdb.DisableGC, true,
   448  				func(ds datastore.Datastore, require *require.Assertions) (datastore.Datastore, datastore.Revision) {
   449  					return tf.DatastoreFromSchemaAndTestRelationships(ds, tc.schema, tc.relationships, req)
   450  				})
   451  
   452  			client := v1.NewPermissionsServiceClient(conn)
   453  			t.Cleanup(cleanup)
   454  
   455  			ctx := context.Background()
   456  			ctx = requestmeta.AddRequestHeaders(ctx, requestmeta.RequestDebugInformation)
   457  
   458  			for _, stc := range tc.toTest {
   459  				stc := stc
   460  				t.Run(stc.name, func(t *testing.T) {
   461  					req := require.New(t)
   462  
   463  					var trailer metadata.MD
   464  					caveatContext, err := structpb.NewStruct(stc.checkRequest.caveatContext)
   465  					req.NoError(err)
   466  
   467  					checkResp, err := client.CheckPermission(ctx, &v1.CheckPermissionRequest{
   468  						Consistency: &v1.Consistency{
   469  							Requirement: &v1.Consistency_AtLeastAsFresh{
   470  								AtLeastAsFresh: zedtoken.MustNewFromRevision(revision),
   471  							},
   472  						},
   473  						Resource:   stc.checkRequest.resource,
   474  						Permission: stc.checkRequest.permission,
   475  						Subject:    stc.checkRequest.subject,
   476  						Context:    caveatContext,
   477  					}, grpc.Trailer(&trailer))
   478  
   479  					req.NoError(err)
   480  					req.Equal(stc.expectedPermission, checkResp.Permissionship)
   481  
   482  					encodedDebugInfo, err := responsemeta.GetResponseTrailerMetadataOrNil(trailer, responsemeta.DebugInformation)
   483  					req.NoError(err)
   484  
   485  					// DebugInfo No longer comes as part of the trailer
   486  					req.Nil(encodedDebugInfo)
   487  
   488  					debugInfo := checkResp.DebugTrace
   489  					req.NotEmpty(debugInfo.SchemaUsed)
   490  
   491  					req.Equal(stc.checkRequest.resource.ObjectType, debugInfo.Check.Resource.ObjectType)
   492  					req.Equal(stc.checkRequest.resource.ObjectId, debugInfo.Check.Resource.ObjectId)
   493  					req.Equal(stc.checkRequest.permission, debugInfo.Check.Permission)
   494  
   495  					if debugInfo.Check.GetSubProblems() != nil {
   496  						req.GreaterOrEqual(len(debugInfo.Check.GetSubProblems().Traces), stc.expectedMinimumSubProblemCount, "found traces: %s", prototext.Format(debugInfo.Check))
   497  					} else {
   498  						req.Equal(0, stc.expectedMinimumSubProblemCount)
   499  					}
   500  
   501  					for _, rda := range stc.runDebugAssertions {
   502  						rda(req, debugInfo)
   503  					}
   504  				})
   505  			}
   506  		})
   507  	}
   508  }