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

     1  package computed
     2  
     3  import (
     4  	"context"
     5  
     6  	cexpr "github.com/authzed/spicedb/internal/caveats"
     7  	"github.com/authzed/spicedb/internal/dispatch"
     8  	datastoremw "github.com/authzed/spicedb/internal/middleware/datastore"
     9  	"github.com/authzed/spicedb/pkg/datastore"
    10  	"github.com/authzed/spicedb/pkg/genutil/slicez"
    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/spiceerrors"
    14  )
    15  
    16  // DebugOption defines the various debug level options for Checks.
    17  type DebugOption int
    18  
    19  const (
    20  	// NoDebugging indicates that debug information should be retained
    21  	// while performing the Check.
    22  	NoDebugging DebugOption = 0
    23  
    24  	// BasicDebuggingEnabled indicates that basic debug information, such
    25  	// as which steps were taken, should be retained while performing the
    26  	// Check and returned to the caller.
    27  	//
    28  	// NOTE: This has a minor performance impact.
    29  	BasicDebuggingEnabled DebugOption = 1
    30  
    31  	// TraceDebuggingEnabled indicates that the Check is being issued for
    32  	// tracing the exact calls made for debugging, which means that not only
    33  	// should debug information be recorded and returned, but that optimizations
    34  	// such as batching should be disabled.
    35  	//
    36  	// WARNING: This has a fairly significant performance impact and should only
    37  	// be used in tooling!
    38  	TraceDebuggingEnabled DebugOption = 2
    39  )
    40  
    41  // CheckParameters are the parameters for the ComputeCheck call. *All* are required.
    42  type CheckParameters struct {
    43  	ResourceType  *core.RelationReference
    44  	Subject       *core.ObjectAndRelation
    45  	CaveatContext map[string]any
    46  	AtRevision    datastore.Revision
    47  	MaximumDepth  uint32
    48  	DebugOption   DebugOption
    49  }
    50  
    51  // ComputeCheck computes a check result for the given resource and subject, computing any
    52  // caveat expressions found.
    53  func ComputeCheck(
    54  	ctx context.Context,
    55  	d dispatch.Check,
    56  	params CheckParameters,
    57  	resourceID string,
    58  ) (*v1.ResourceCheckResult, *v1.ResponseMeta, error) {
    59  	resultsMap, meta, err := computeCheck(ctx, d, params, []string{resourceID})
    60  	if err != nil {
    61  		return nil, meta, err
    62  	}
    63  	return resultsMap[resourceID], meta, err
    64  }
    65  
    66  // ComputeBulkCheck computes a check result for the given resources and subject, computing any
    67  // caveat expressions found.
    68  func ComputeBulkCheck(
    69  	ctx context.Context,
    70  	d dispatch.Check,
    71  	params CheckParameters,
    72  	resourceIDs []string,
    73  ) (map[string]*v1.ResourceCheckResult, *v1.ResponseMeta, error) {
    74  	return computeCheck(ctx, d, params, resourceIDs)
    75  }
    76  
    77  func computeCheck(ctx context.Context,
    78  	d dispatch.Check,
    79  	params CheckParameters,
    80  	resourceIDs []string,
    81  ) (map[string]*v1.ResourceCheckResult, *v1.ResponseMeta, error) {
    82  	debugging := v1.DispatchCheckRequest_NO_DEBUG
    83  	if params.DebugOption == BasicDebuggingEnabled {
    84  		debugging = v1.DispatchCheckRequest_ENABLE_BASIC_DEBUGGING
    85  		if len(resourceIDs) > 1 {
    86  			return nil, nil, spiceerrors.MustBugf("debugging can only be enabled for a single resource ID")
    87  		}
    88  	} else if params.DebugOption == TraceDebuggingEnabled {
    89  		debugging = v1.DispatchCheckRequest_ENABLE_TRACE_DEBUGGING
    90  		if len(resourceIDs) > 1 {
    91  			return nil, nil, spiceerrors.MustBugf("debugging can only be enabled for a single resource ID")
    92  		}
    93  	}
    94  
    95  	setting := v1.DispatchCheckRequest_REQUIRE_ALL_RESULTS
    96  	if len(resourceIDs) == 1 {
    97  		setting = v1.DispatchCheckRequest_ALLOW_SINGLE_RESULT
    98  	}
    99  
   100  	// Ensure that the number of resources IDs given to each dispatch call is not in excess of the maximum.
   101  	results := make(map[string]*v1.ResourceCheckResult, len(resourceIDs))
   102  	metadata := &v1.ResponseMeta{}
   103  
   104  	bf, err := v1.NewTraversalBloomFilter(uint(params.MaximumDepth))
   105  	if err != nil {
   106  		return nil, nil, spiceerrors.MustBugf("failed to create new traversal bloom filter")
   107  	}
   108  
   109  	// TODO(jschorr): Should we make this run in parallel via the preloadedTaskRunner?
   110  	_, err = slicez.ForEachChunkUntil(resourceIDs, datastore.FilterMaximumIDCount, func(resourceIDsToCheck []string) (bool, error) {
   111  		checkResult, err := d.DispatchCheck(ctx, &v1.DispatchCheckRequest{
   112  			ResourceRelation: params.ResourceType,
   113  			ResourceIds:      resourceIDsToCheck,
   114  			ResultsSetting:   setting,
   115  			Subject:          params.Subject,
   116  			Metadata: &v1.ResolverMeta{
   117  				AtRevision:     params.AtRevision.String(),
   118  				DepthRemaining: params.MaximumDepth,
   119  				TraversalBloom: bf,
   120  			},
   121  			Debug: debugging,
   122  		})
   123  
   124  		if len(resourceIDs) == 1 {
   125  			metadata = checkResult.Metadata
   126  		} else {
   127  			metadata = &v1.ResponseMeta{
   128  				DispatchCount:       metadata.DispatchCount + checkResult.Metadata.DispatchCount,
   129  				DepthRequired:       max(metadata.DepthRequired, checkResult.Metadata.DepthRequired),
   130  				CachedDispatchCount: metadata.CachedDispatchCount + checkResult.Metadata.CachedDispatchCount,
   131  			}
   132  		}
   133  
   134  		if err != nil {
   135  			return false, err
   136  		}
   137  
   138  		for _, resourceID := range resourceIDsToCheck {
   139  			computed, err := computeCaveatedCheckResult(ctx, params, resourceID, checkResult)
   140  			if err != nil {
   141  				return false, err
   142  			}
   143  			results[resourceID] = computed
   144  		}
   145  
   146  		return true, nil
   147  	})
   148  	return results, metadata, err
   149  }
   150  
   151  func computeCaveatedCheckResult(ctx context.Context, params CheckParameters, resourceID string, checkResult *v1.DispatchCheckResponse) (*v1.ResourceCheckResult, error) {
   152  	result, ok := checkResult.ResultsByResourceId[resourceID]
   153  	if !ok {
   154  		return &v1.ResourceCheckResult{
   155  			Membership: v1.ResourceCheckResult_NOT_MEMBER,
   156  		}, nil
   157  	}
   158  
   159  	if result.Membership == v1.ResourceCheckResult_MEMBER {
   160  		return result, nil
   161  	}
   162  
   163  	ds := datastoremw.MustFromContext(ctx)
   164  	reader := ds.SnapshotReader(params.AtRevision)
   165  
   166  	caveatResult, err := cexpr.RunCaveatExpression(ctx, result.Expression, params.CaveatContext, reader, cexpr.RunCaveatExpressionNoDebugging)
   167  	if err != nil {
   168  		return nil, err
   169  	}
   170  
   171  	if caveatResult.IsPartial() {
   172  		missingFields, _ := caveatResult.MissingVarNames()
   173  		return &v1.ResourceCheckResult{
   174  			Membership:        v1.ResourceCheckResult_CAVEATED_MEMBER,
   175  			MissingExprFields: missingFields,
   176  		}, nil
   177  	}
   178  
   179  	if caveatResult.Value() {
   180  		return &v1.ResourceCheckResult{
   181  			Membership: v1.ResourceCheckResult_MEMBER,
   182  		}, nil
   183  	}
   184  
   185  	return &v1.ResourceCheckResult{
   186  		Membership: v1.ResourceCheckResult_NOT_MEMBER,
   187  	}, nil
   188  }