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 }