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

     1  package v1
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"io"
     7  	"slices"
     8  	"sort"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/authzed/spicedb/internal/dispatch"
    13  	log "github.com/authzed/spicedb/internal/logging"
    14  	"github.com/authzed/spicedb/internal/middleware"
    15  	"github.com/authzed/spicedb/internal/middleware/consistency"
    16  	datastoremw "github.com/authzed/spicedb/internal/middleware/datastore"
    17  	"github.com/authzed/spicedb/internal/middleware/handwrittenvalidation"
    18  	"github.com/authzed/spicedb/internal/middleware/streamtimeout"
    19  	"github.com/authzed/spicedb/internal/middleware/usagemetrics"
    20  	"github.com/authzed/spicedb/internal/relationships"
    21  	"github.com/authzed/spicedb/internal/services/shared"
    22  	"github.com/authzed/spicedb/internal/services/v1/options"
    23  	"github.com/authzed/spicedb/pkg/cursor"
    24  	"github.com/authzed/spicedb/pkg/datastore"
    25  	dsoptions "github.com/authzed/spicedb/pkg/datastore/options"
    26  	core "github.com/authzed/spicedb/pkg/proto/core/v1"
    27  	dispatchv1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1"
    28  	implv1 "github.com/authzed/spicedb/pkg/proto/impl/v1"
    29  	"github.com/authzed/spicedb/pkg/tuple"
    30  	"github.com/authzed/spicedb/pkg/typesystem"
    31  	"github.com/authzed/spicedb/pkg/zedtoken"
    32  
    33  	v1 "github.com/authzed/authzed-go/proto/authzed/api/v1"
    34  	grpcvalidate "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/validator"
    35  	"github.com/samber/lo"
    36  )
    37  
    38  const (
    39  	defaultExportBatchSizeFallback   = 1_000
    40  	maxExportBatchSizeFallback       = 10_000
    41  	streamReadTimeoutFallbackSeconds = 600
    42  )
    43  
    44  // NewExperimentalServer creates a ExperimentalServiceServer instance.
    45  func NewExperimentalServer(dispatch dispatch.Dispatcher, permServerConfig PermissionsServerConfig, opts ...options.ExperimentalServerOptionsOption) v1.ExperimentalServiceServer {
    46  	config := options.NewExperimentalServerOptionsWithOptionsAndDefaults(opts...)
    47  
    48  	if config.DefaultExportBatchSize == 0 {
    49  		log.
    50  			Warn().
    51  			Uint32("specified", config.DefaultExportBatchSize).
    52  			Uint32("fallback", defaultExportBatchSizeFallback).
    53  			Msg("experimental server config specified invalid DefaultExportBatchSize, setting to fallback")
    54  		config.DefaultExportBatchSize = defaultExportBatchSizeFallback
    55  	}
    56  	if config.MaxExportBatchSize == 0 {
    57  		fallback := permServerConfig.MaxBulkExportRelationshipsLimit
    58  		if fallback == 0 {
    59  			fallback = maxExportBatchSizeFallback
    60  		}
    61  
    62  		log.
    63  			Warn().
    64  			Uint32("specified", config.MaxExportBatchSize).
    65  			Uint32("fallback", fallback).
    66  			Msg("experimental server config specified invalid MaxExportBatchSize, setting to fallback")
    67  		config.MaxExportBatchSize = fallback
    68  	}
    69  	if config.StreamReadTimeout == 0 {
    70  		log.
    71  			Warn().
    72  			Stringer("specified", config.StreamReadTimeout).
    73  			Stringer("fallback", streamReadTimeoutFallbackSeconds*time.Second).
    74  			Msg("experimental server config specified invalid StreamReadTimeout, setting to fallback")
    75  		config.StreamReadTimeout = streamReadTimeoutFallbackSeconds * time.Second
    76  	}
    77  
    78  	return &experimentalServer{
    79  		WithServiceSpecificInterceptors: shared.WithServiceSpecificInterceptors{
    80  			Unary: middleware.ChainUnaryServer(
    81  				grpcvalidate.UnaryServerInterceptor(),
    82  				handwrittenvalidation.UnaryServerInterceptor,
    83  				usagemetrics.UnaryServerInterceptor(),
    84  			),
    85  			Stream: middleware.ChainStreamServer(
    86  				grpcvalidate.StreamServerInterceptor(),
    87  				handwrittenvalidation.StreamServerInterceptor,
    88  				usagemetrics.StreamServerInterceptor(),
    89  				streamtimeout.MustStreamServerInterceptor(config.StreamReadTimeout),
    90  			),
    91  		},
    92  		defaultBatchSize: uint64(config.DefaultExportBatchSize),
    93  		maxBatchSize:     uint64(config.MaxExportBatchSize),
    94  		bulkChecker: &bulkChecker{
    95  			maxAPIDepth:          permServerConfig.MaximumAPIDepth,
    96  			maxCaveatContextSize: permServerConfig.MaxCaveatContextSize,
    97  			maxConcurrency:       config.BulkCheckMaxConcurrency,
    98  			dispatch:             dispatch,
    99  		},
   100  	}
   101  }
   102  
   103  type experimentalServer struct {
   104  	v1.UnimplementedExperimentalServiceServer
   105  	shared.WithServiceSpecificInterceptors
   106  
   107  	defaultBatchSize uint64
   108  	maxBatchSize     uint64
   109  
   110  	bulkChecker *bulkChecker
   111  }
   112  
   113  type bulkLoadAdapter struct {
   114  	stream                 v1.ExperimentalService_BulkImportRelationshipsServer
   115  	referencedNamespaceMap map[string]*typesystem.TypeSystem
   116  	referencedCaveatMap    map[string]*core.CaveatDefinition
   117  	current                core.RelationTuple
   118  	caveat                 core.ContextualizedCaveat
   119  
   120  	awaitingNamespaces []string
   121  	awaitingCaveats    []string
   122  
   123  	currentBatch []*v1.Relationship
   124  	numSent      int
   125  	err          error
   126  }
   127  
   128  func (a *bulkLoadAdapter) Next(_ context.Context) (*core.RelationTuple, error) {
   129  	for a.err == nil && a.numSent == len(a.currentBatch) {
   130  		// Load a new batch
   131  		batch, err := a.stream.Recv()
   132  		if err != nil {
   133  			a.err = err
   134  			if errors.Is(a.err, io.EOF) {
   135  				return nil, nil
   136  			}
   137  			return nil, a.err
   138  		}
   139  
   140  		a.currentBatch = batch.Relationships
   141  		a.numSent = 0
   142  
   143  		a.awaitingNamespaces, a.awaitingCaveats = extractBatchNewReferencedNamespacesAndCaveats(
   144  			a.currentBatch,
   145  			a.referencedNamespaceMap,
   146  			a.referencedCaveatMap,
   147  		)
   148  	}
   149  
   150  	if len(a.awaitingNamespaces) > 0 || len(a.awaitingCaveats) > 0 {
   151  		// Shut down the stream to give our caller a chance to fill in this information
   152  		return nil, nil
   153  	}
   154  
   155  	a.current.Caveat = &a.caveat
   156  	tuple.CopyRelationshipToRelationTuple(a.currentBatch[a.numSent], &a.current)
   157  
   158  	if err := relationships.ValidateOneRelationship(
   159  		a.referencedNamespaceMap,
   160  		a.referencedCaveatMap,
   161  		&a.current,
   162  		relationships.ValidateRelationshipForCreateOrTouch,
   163  	); err != nil {
   164  		return nil, err
   165  	}
   166  
   167  	a.numSent++
   168  	return &a.current, nil
   169  }
   170  
   171  func extractBatchNewReferencedNamespacesAndCaveats(
   172  	batch []*v1.Relationship,
   173  	existingNamespaces map[string]*typesystem.TypeSystem,
   174  	existingCaveats map[string]*core.CaveatDefinition,
   175  ) ([]string, []string) {
   176  	newNamespaces := make(map[string]struct{}, 2)
   177  	newCaveats := make(map[string]struct{}, 0)
   178  	for _, rel := range batch {
   179  		if _, ok := existingNamespaces[rel.Resource.ObjectType]; !ok {
   180  			newNamespaces[rel.Resource.ObjectType] = struct{}{}
   181  		}
   182  		if _, ok := existingNamespaces[rel.Subject.Object.ObjectType]; !ok {
   183  			newNamespaces[rel.Subject.Object.ObjectType] = struct{}{}
   184  		}
   185  		if rel.OptionalCaveat != nil {
   186  			if _, ok := existingCaveats[rel.OptionalCaveat.CaveatName]; !ok {
   187  				newCaveats[rel.OptionalCaveat.CaveatName] = struct{}{}
   188  			}
   189  		}
   190  	}
   191  
   192  	return lo.Keys(newNamespaces), lo.Keys(newCaveats)
   193  }
   194  
   195  func (es *experimentalServer) BulkImportRelationships(stream v1.ExperimentalService_BulkImportRelationshipsServer) error {
   196  	ds := datastoremw.MustFromContext(stream.Context())
   197  
   198  	var numWritten uint64
   199  	if _, err := ds.ReadWriteTx(stream.Context(), func(ctx context.Context, rwt datastore.ReadWriteTransaction) error {
   200  		loadedNamespaces := make(map[string]*typesystem.TypeSystem, 2)
   201  		loadedCaveats := make(map[string]*core.CaveatDefinition, 0)
   202  
   203  		adapter := &bulkLoadAdapter{
   204  			stream:                 stream,
   205  			referencedNamespaceMap: loadedNamespaces,
   206  			referencedCaveatMap:    loadedCaveats,
   207  			current: core.RelationTuple{
   208  				ResourceAndRelation: &core.ObjectAndRelation{},
   209  				Subject:             &core.ObjectAndRelation{},
   210  			},
   211  			caveat: core.ContextualizedCaveat{},
   212  		}
   213  		resolver := typesystem.ResolverForDatastoreReader(rwt)
   214  
   215  		var streamWritten uint64
   216  		var err error
   217  		for ; adapter.err == nil && err == nil; streamWritten, err = rwt.BulkLoad(stream.Context(), adapter) {
   218  			numWritten += streamWritten
   219  
   220  			// The stream has terminated because we're awaiting namespace and/or caveat information
   221  			if len(adapter.awaitingNamespaces) > 0 {
   222  				nsDefs, err := rwt.LookupNamespacesWithNames(stream.Context(), adapter.awaitingNamespaces)
   223  				if err != nil {
   224  					return err
   225  				}
   226  
   227  				for _, nsDef := range nsDefs {
   228  					nts, err := typesystem.NewNamespaceTypeSystem(nsDef.Definition, resolver)
   229  					if err != nil {
   230  						return err
   231  					}
   232  
   233  					loadedNamespaces[nsDef.Definition.Name] = nts
   234  				}
   235  				adapter.awaitingNamespaces = nil
   236  			}
   237  
   238  			if len(adapter.awaitingCaveats) > 0 {
   239  				caveats, err := rwt.LookupCaveatsWithNames(stream.Context(), adapter.awaitingCaveats)
   240  				if err != nil {
   241  					return err
   242  				}
   243  
   244  				for _, caveat := range caveats {
   245  					loadedCaveats[caveat.Definition.Name] = caveat.Definition
   246  				}
   247  				adapter.awaitingCaveats = nil
   248  			}
   249  		}
   250  		numWritten += streamWritten
   251  
   252  		return err
   253  	}, dsoptions.WithDisableRetries(true)); err != nil {
   254  		return shared.RewriteErrorWithoutConfig(stream.Context(), err)
   255  	}
   256  
   257  	usagemetrics.SetInContext(stream.Context(), &dispatchv1.ResponseMeta{
   258  		// One request for the whole load
   259  		DispatchCount: 1,
   260  	})
   261  
   262  	return stream.SendAndClose(&v1.BulkImportRelationshipsResponse{
   263  		NumLoaded: numWritten,
   264  	})
   265  }
   266  
   267  func (es *experimentalServer) BulkExportRelationships(
   268  	req *v1.BulkExportRelationshipsRequest,
   269  	resp v1.ExperimentalService_BulkExportRelationshipsServer,
   270  ) error {
   271  	ctx := resp.Context()
   272  	atRevision, _, err := consistency.RevisionFromContext(ctx)
   273  	if err != nil {
   274  		return shared.RewriteErrorWithoutConfig(ctx, err)
   275  	}
   276  
   277  	return BulkExport(ctx, datastoremw.MustFromContext(ctx), es.maxBatchSize, req, atRevision, resp.Send)
   278  }
   279  
   280  // BulkExport implements the BulkExportRelationships API functionality. Given a datastore.Datastore, it will
   281  // export stream via the sender all relationships matched by the incoming request.
   282  // If no cursor is provided, it will fallback to the provided revision.
   283  func BulkExport(ctx context.Context, ds datastore.Datastore, batchSize uint64, req *v1.BulkExportRelationshipsRequest, fallbackRevision datastore.Revision, sender func(response *v1.BulkExportRelationshipsResponse) error) error {
   284  	if req.OptionalLimit > 0 && uint64(req.OptionalLimit) > batchSize {
   285  		return shared.RewriteErrorWithoutConfig(ctx, NewExceedsMaximumLimitErr(uint64(req.OptionalLimit), batchSize))
   286  	}
   287  
   288  	atRevision := fallbackRevision
   289  	var curNamespace string
   290  	var cur dsoptions.Cursor
   291  	if req.OptionalCursor != nil {
   292  		var err error
   293  		atRevision, curNamespace, cur, err = decodeCursor(ds, req.OptionalCursor)
   294  		if err != nil {
   295  			return shared.RewriteErrorWithoutConfig(ctx, err)
   296  		}
   297  	}
   298  
   299  	reader := ds.SnapshotReader(atRevision)
   300  
   301  	namespaces, err := reader.ListAllNamespaces(ctx)
   302  	if err != nil {
   303  		return shared.RewriteErrorWithoutConfig(ctx, err)
   304  	}
   305  
   306  	// Make sure the namespaces are always in a stable order
   307  	slices.SortFunc(namespaces, func(
   308  		lhs datastore.RevisionedDefinition[*core.NamespaceDefinition],
   309  		rhs datastore.RevisionedDefinition[*core.NamespaceDefinition],
   310  	) int {
   311  		return strings.Compare(lhs.Definition.Name, rhs.Definition.Name)
   312  	})
   313  
   314  	// Skip the namespaces that are already fully returned
   315  	for cur != nil && len(namespaces) > 0 && namespaces[0].Definition.Name < curNamespace {
   316  		namespaces = namespaces[1:]
   317  	}
   318  
   319  	limit := batchSize
   320  	if req.OptionalLimit > 0 {
   321  		limit = uint64(req.OptionalLimit)
   322  	}
   323  
   324  	// Pre-allocate all of the relationships that we might need in order to
   325  	// make export easier and faster for the garbage collector.
   326  	relsArray := make([]v1.Relationship, limit)
   327  	objArray := make([]v1.ObjectReference, limit)
   328  	subArray := make([]v1.SubjectReference, limit)
   329  	subObjArray := make([]v1.ObjectReference, limit)
   330  	caveatArray := make([]v1.ContextualizedCaveat, limit)
   331  	for i := range relsArray {
   332  		relsArray[i].Resource = &objArray[i]
   333  		relsArray[i].Subject = &subArray[i]
   334  		relsArray[i].Subject.Object = &subObjArray[i]
   335  	}
   336  
   337  	emptyRels := make([]*v1.Relationship, limit)
   338  	for _, ns := range namespaces {
   339  		rels := emptyRels
   340  
   341  		// Reset the cursor between namespaces.
   342  		if ns.Definition.Name != curNamespace {
   343  			cur = nil
   344  		}
   345  
   346  		// Skip this namespace if a resource type filter was specified.
   347  		if req.OptionalRelationshipFilter != nil && req.OptionalRelationshipFilter.ResourceType != "" {
   348  			if ns.Definition.Name != req.OptionalRelationshipFilter.ResourceType {
   349  				continue
   350  			}
   351  		}
   352  
   353  		// Setup the filter to use for the relationships.
   354  		relationshipFilter := datastore.RelationshipsFilter{OptionalResourceType: ns.Definition.Name}
   355  		if req.OptionalRelationshipFilter != nil {
   356  			rf, err := datastore.RelationshipsFilterFromPublicFilter(req.OptionalRelationshipFilter)
   357  			if err != nil {
   358  				return shared.RewriteErrorWithoutConfig(ctx, err)
   359  			}
   360  
   361  			// Overload the namespace name with the one from the request, because each iteration is for a different namespace.
   362  			rf.OptionalResourceType = ns.Definition.Name
   363  			relationshipFilter = rf
   364  		}
   365  
   366  		// We want to keep iterating as long as we're sending full batches.
   367  		// To bootstrap this loop, we enter the first time with a full rels
   368  		// slice of dummy rels that were never sent.
   369  		for uint64(len(rels)) == limit {
   370  			// Lop off any rels we've already sent
   371  			rels = rels[:0]
   372  
   373  			tplFn := func(tpl *core.RelationTuple) {
   374  				offset := len(rels)
   375  				rels = append(rels, &relsArray[offset]) // nozero
   376  				tuple.CopyRelationTupleToRelationship(tpl, &relsArray[offset], &caveatArray[offset])
   377  			}
   378  
   379  			cur, err = queryForEach(
   380  				ctx,
   381  				reader,
   382  				relationshipFilter,
   383  				tplFn,
   384  				dsoptions.WithLimit(&limit),
   385  				dsoptions.WithAfter(cur),
   386  				dsoptions.WithSort(dsoptions.ByResource),
   387  			)
   388  			if err != nil {
   389  				return shared.RewriteErrorWithoutConfig(ctx, err)
   390  			}
   391  
   392  			if len(rels) == 0 {
   393  				continue
   394  			}
   395  
   396  			encoded, err := cursor.Encode(&implv1.DecodedCursor{
   397  				VersionOneof: &implv1.DecodedCursor_V1{
   398  					V1: &implv1.V1Cursor{
   399  						Revision: atRevision.String(),
   400  						Sections: []string{
   401  							ns.Definition.Name,
   402  							tuple.MustString(cur),
   403  						},
   404  					},
   405  				},
   406  			})
   407  			if err != nil {
   408  				return shared.RewriteErrorWithoutConfig(ctx, err)
   409  			}
   410  
   411  			if err := sender(&v1.BulkExportRelationshipsResponse{
   412  				AfterResultCursor: encoded,
   413  				Relationships:     rels,
   414  			}); err != nil {
   415  				return shared.RewriteErrorWithoutConfig(ctx, err)
   416  			}
   417  		}
   418  	}
   419  	return nil
   420  }
   421  
   422  const maxBulkCheckCount = 10000
   423  
   424  func (es *experimentalServer) BulkCheckPermission(ctx context.Context, req *v1.BulkCheckPermissionRequest) (*v1.BulkCheckPermissionResponse, error) {
   425  	convertedReq := toCheckBulkPermissionsRequest(req)
   426  	res, err := es.bulkChecker.checkBulkPermissions(ctx, convertedReq)
   427  	if err != nil {
   428  		return nil, shared.RewriteErrorWithoutConfig(ctx, err)
   429  	}
   430  
   431  	return toBulkCheckPermissionResponse(res), nil
   432  }
   433  
   434  func (es *experimentalServer) ExperimentalReflectSchema(ctx context.Context, req *v1.ExperimentalReflectSchemaRequest) (*v1.ExperimentalReflectSchemaResponse, error) {
   435  	// Get the current schema.
   436  	schema, atRevision, err := loadCurrentSchema(ctx)
   437  	if err != nil {
   438  		return nil, shared.RewriteErrorWithoutConfig(ctx, err)
   439  	}
   440  
   441  	filters, err := newSchemaFilters(req.OptionalFilters)
   442  	if err != nil {
   443  		return nil, shared.RewriteErrorWithoutConfig(ctx, err)
   444  	}
   445  
   446  	definitions := make([]*v1.ExpDefinition, 0, len(schema.ObjectDefinitions))
   447  	if filters.HasNamespaces() {
   448  		for _, ns := range schema.ObjectDefinitions {
   449  			def, err := namespaceAPIRepr(ns, filters)
   450  			if err != nil {
   451  				return nil, shared.RewriteErrorWithoutConfig(ctx, err)
   452  			}
   453  
   454  			if def != nil {
   455  				definitions = append(definitions, def)
   456  			}
   457  		}
   458  	}
   459  
   460  	caveats := make([]*v1.ExpCaveat, 0, len(schema.CaveatDefinitions))
   461  	if filters.HasCaveats() {
   462  		for _, cd := range schema.CaveatDefinitions {
   463  			caveat, err := caveatAPIRepr(cd, filters)
   464  			if err != nil {
   465  				return nil, shared.RewriteErrorWithoutConfig(ctx, err)
   466  			}
   467  
   468  			if caveat != nil {
   469  				caveats = append(caveats, caveat)
   470  			}
   471  		}
   472  	}
   473  
   474  	return &v1.ExperimentalReflectSchemaResponse{
   475  		Definitions: definitions,
   476  		Caveats:     caveats,
   477  		ReadAt:      zedtoken.MustNewFromRevision(atRevision),
   478  	}, nil
   479  }
   480  
   481  func (es *experimentalServer) ExperimentalDiffSchema(ctx context.Context, req *v1.ExperimentalDiffSchemaRequest) (*v1.ExperimentalDiffSchemaResponse, error) {
   482  	atRevision, _, err := consistency.RevisionFromContext(ctx)
   483  	if err != nil {
   484  		return nil, err
   485  	}
   486  
   487  	diff, existingSchema, comparisonSchema, err := schemaDiff(ctx, req.ComparisonSchema)
   488  	if err != nil {
   489  		return nil, shared.RewriteErrorWithoutConfig(ctx, err)
   490  	}
   491  
   492  	resp, err := convertDiff(diff, existingSchema, comparisonSchema, atRevision)
   493  	if err != nil {
   494  		return nil, shared.RewriteErrorWithoutConfig(ctx, err)
   495  	}
   496  
   497  	return resp, nil
   498  }
   499  
   500  func (es *experimentalServer) ExperimentalComputablePermissions(ctx context.Context, req *v1.ExperimentalComputablePermissionsRequest) (*v1.ExperimentalComputablePermissionsResponse, error) {
   501  	atRevision, revisionReadAt, err := consistency.RevisionFromContext(ctx)
   502  	if err != nil {
   503  		return nil, shared.RewriteErrorWithoutConfig(ctx, err)
   504  	}
   505  
   506  	ds := datastoremw.MustFromContext(ctx).SnapshotReader(atRevision)
   507  	_, vts, err := typesystem.ReadNamespaceAndTypes(ctx, req.DefinitionName, ds)
   508  	if err != nil {
   509  		return nil, shared.RewriteErrorWithoutConfig(ctx, err)
   510  	}
   511  
   512  	relationName := req.RelationName
   513  	if relationName == "" {
   514  		relationName = tuple.Ellipsis
   515  	} else {
   516  		if _, ok := vts.GetRelation(relationName); !ok {
   517  			return nil, shared.RewriteErrorWithoutConfig(ctx, typesystem.NewRelationNotFoundErr(req.DefinitionName, relationName))
   518  		}
   519  	}
   520  
   521  	allNamespaces, err := ds.ListAllNamespaces(ctx)
   522  	if err != nil {
   523  		return nil, shared.RewriteErrorWithoutConfig(ctx, err)
   524  	}
   525  
   526  	allDefinitions := make([]*core.NamespaceDefinition, 0, len(allNamespaces))
   527  	for _, ns := range allNamespaces {
   528  		allDefinitions = append(allDefinitions, ns.Definition)
   529  	}
   530  
   531  	rg := typesystem.ReachabilityGraphFor(vts)
   532  	rr, err := rg.RelationsEncounteredForSubject(ctx, allDefinitions, &core.RelationReference{
   533  		Namespace: req.DefinitionName,
   534  		Relation:  relationName,
   535  	})
   536  	if err != nil {
   537  		return nil, shared.RewriteErrorWithoutConfig(ctx, err)
   538  	}
   539  
   540  	relations := make([]*v1.ExpRelationReference, 0, len(rr))
   541  	for _, r := range rr {
   542  		if r.Namespace == req.DefinitionName && r.Relation == req.RelationName {
   543  			continue
   544  		}
   545  
   546  		if req.OptionalDefinitionNameFilter != "" && !strings.HasPrefix(r.Namespace, req.OptionalDefinitionNameFilter) {
   547  			continue
   548  		}
   549  
   550  		ts, err := vts.TypeSystemForNamespace(ctx, r.Namespace)
   551  		if err != nil {
   552  			return nil, shared.RewriteErrorWithoutConfig(ctx, err)
   553  		}
   554  
   555  		relations = append(relations, &v1.ExpRelationReference{
   556  			DefinitionName: r.Namespace,
   557  			RelationName:   r.Relation,
   558  			IsPermission:   ts.IsPermission(r.Relation),
   559  		})
   560  	}
   561  
   562  	sort.Slice(relations, func(i, j int) bool {
   563  		if relations[i].DefinitionName == relations[j].DefinitionName {
   564  			return relations[i].RelationName < relations[j].RelationName
   565  		}
   566  		return relations[i].DefinitionName < relations[j].DefinitionName
   567  	})
   568  
   569  	return &v1.ExperimentalComputablePermissionsResponse{
   570  		Permissions: relations,
   571  		ReadAt:      revisionReadAt,
   572  	}, nil
   573  }
   574  
   575  func (es *experimentalServer) ExperimentalDependentRelations(ctx context.Context, req *v1.ExperimentalDependentRelationsRequest) (*v1.ExperimentalDependentRelationsResponse, error) {
   576  	atRevision, revisionReadAt, err := consistency.RevisionFromContext(ctx)
   577  	if err != nil {
   578  		return nil, shared.RewriteErrorWithoutConfig(ctx, err)
   579  	}
   580  
   581  	ds := datastoremw.MustFromContext(ctx).SnapshotReader(atRevision)
   582  	_, vts, err := typesystem.ReadNamespaceAndTypes(ctx, req.DefinitionName, ds)
   583  	if err != nil {
   584  		return nil, shared.RewriteErrorWithoutConfig(ctx, err)
   585  	}
   586  
   587  	_, ok := vts.GetRelation(req.PermissionName)
   588  	if !ok {
   589  		return nil, shared.RewriteErrorWithoutConfig(ctx, typesystem.NewRelationNotFoundErr(req.DefinitionName, req.PermissionName))
   590  	}
   591  
   592  	if !vts.IsPermission(req.PermissionName) {
   593  		return nil, shared.RewriteErrorWithoutConfig(ctx, NewNotAPermissionError(req.PermissionName))
   594  	}
   595  
   596  	rg := typesystem.ReachabilityGraphFor(vts)
   597  	rr, err := rg.RelationsEncounteredForResource(ctx, &core.RelationReference{
   598  		Namespace: req.DefinitionName,
   599  		Relation:  req.PermissionName,
   600  	})
   601  	if err != nil {
   602  		return nil, shared.RewriteErrorWithoutConfig(ctx, err)
   603  	}
   604  
   605  	relations := make([]*v1.ExpRelationReference, 0, len(rr))
   606  	for _, r := range rr {
   607  		if r.Namespace == req.DefinitionName && r.Relation == req.PermissionName {
   608  			continue
   609  		}
   610  
   611  		ts, err := vts.TypeSystemForNamespace(ctx, r.Namespace)
   612  		if err != nil {
   613  			return nil, shared.RewriteErrorWithoutConfig(ctx, err)
   614  		}
   615  
   616  		relations = append(relations, &v1.ExpRelationReference{
   617  			DefinitionName: r.Namespace,
   618  			RelationName:   r.Relation,
   619  			IsPermission:   ts.IsPermission(r.Relation),
   620  		})
   621  	}
   622  
   623  	sort.Slice(relations, func(i, j int) bool {
   624  		if relations[i].DefinitionName == relations[j].DefinitionName {
   625  			return relations[i].RelationName < relations[j].RelationName
   626  		}
   627  
   628  		return relations[i].DefinitionName < relations[j].DefinitionName
   629  	})
   630  
   631  	return &v1.ExperimentalDependentRelationsResponse{
   632  		Relations: relations,
   633  		ReadAt:    revisionReadAt,
   634  	}, nil
   635  }
   636  
   637  func queryForEach(
   638  	ctx context.Context,
   639  	reader datastore.Reader,
   640  	filter datastore.RelationshipsFilter,
   641  	fn func(tpl *core.RelationTuple),
   642  	opts ...dsoptions.QueryOptionsOption,
   643  ) (*core.RelationTuple, error) {
   644  	iter, err := reader.QueryRelationships(ctx, filter, opts...)
   645  	if err != nil {
   646  		return nil, err
   647  	}
   648  	defer iter.Close()
   649  
   650  	var hadTuples bool
   651  	for tpl := iter.Next(); tpl != nil; tpl = iter.Next() {
   652  		fn(tpl)
   653  		hadTuples = true
   654  	}
   655  	if iter.Err() != nil {
   656  		return nil, err
   657  	}
   658  
   659  	var cur *core.RelationTuple
   660  	if hadTuples {
   661  		cur, err = iter.Cursor()
   662  		iter.Close()
   663  		if err != nil {
   664  			return nil, err
   665  		}
   666  	}
   667  
   668  	return cur, nil
   669  }
   670  
   671  func decodeCursor(ds datastore.Datastore, encoded *v1.Cursor) (datastore.Revision, string, *core.RelationTuple, error) {
   672  	decoded, err := cursor.Decode(encoded)
   673  	if err != nil {
   674  		return datastore.NoRevision, "", nil, err
   675  	}
   676  
   677  	if decoded.GetV1() == nil {
   678  		return datastore.NoRevision, "", nil, errors.New("malformed cursor: no V1 in OneOf")
   679  	}
   680  
   681  	if len(decoded.GetV1().Sections) != 2 {
   682  		return datastore.NoRevision, "", nil, errors.New("malformed cursor: wrong number of components")
   683  	}
   684  
   685  	atRevision, err := ds.RevisionFromString(decoded.GetV1().Revision)
   686  	if err != nil {
   687  		return datastore.NoRevision, "", nil, err
   688  	}
   689  
   690  	cur := tuple.Parse(decoded.GetV1().GetSections()[1])
   691  	if cur == nil {
   692  		return datastore.NoRevision, "", nil, errors.New("malformed cursor: invalid encoded relation tuple")
   693  	}
   694  
   695  	// Returns the current namespace and the cursor.
   696  	return atRevision, decoded.GetV1().GetSections()[0], cur, nil
   697  }