From dedb1f8f185590a47950eed5ca60c676bc726b0a Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 10 Jul 2024 23:23:08 -0400 Subject: [PATCH] Switch experimental LookupResources2 to request additional chunks of dispatched resources when checking those already received from another dispatch Adds some parallelism back into LR2 --- internal/graph/lookupresources2.go | 274 ++------ internal/graph/lr2streams.go | 286 ++++++++ .../steelthreadtesting/definitions.go | 52 ++ .../services/steelthreadtesting/operations.go | 2 +- ...other-permission-page-size-16-results.yaml | 5 +- ...-other-permission-page-size-5-results.yaml | 11 - ...ookup-resources-for-user-fred-results.yaml | 151 +++++ ...ookup-resources-for-user-fred-results.yaml | 151 +++++ .../document-with-intersect-resources.yaml | 622 ++++++++++++++++++ 9 files changed, 1314 insertions(+), 240 deletions(-) create mode 100644 internal/graph/lr2streams.go create mode 100644 internal/services/steelthreadtesting/steelresults/lookup-resources-with-intersection-cursored-lookup-resources-for-user-fred-results.yaml create mode 100644 internal/services/steelthreadtesting/steelresults/lookup-resources-with-intersection-uncursored-indirect-lookup-resources-for-user-fred-results.yaml create mode 100644 internal/services/steelthreadtesting/testdata/document-with-intersect-resources.yaml diff --git a/internal/graph/lookupresources2.go b/internal/graph/lookupresources2.go index 2e85e89a97..fff4da2b4a 100644 --- a/internal/graph/lookupresources2.go +++ b/internal/graph/lookupresources2.go @@ -2,7 +2,6 @@ package graph import ( "context" - "errors" "slices" "sort" @@ -439,6 +438,11 @@ func hintString(resourceID string, entrypoint typesystem.ReachabilityEntrypoint, return typesystem.CheckHint(resourceKey, terminalSubject), nil } +type possibleResourceAndIndex struct { + resource *v1.PossibleResource + index int +} + // redispatchOrReport checks if further redispatching is necessary for the found resource // type. If not, and the found resource type+relation matches the target resource type+relation, // the resource is reported to the parent stream. @@ -483,7 +487,14 @@ func (crr *CursoredLookupResources2) redispatchOrReport( return nil } - filtered := offsetted + filtered := make([]possibleResourceAndIndex, 0, len(offsetted)) + for index, resource := range offsetted { + filtered = append(filtered, possibleResourceAndIndex{ + resource: resource, + index: index, + }) + } + metadata := emptyMetadata // If the entrypoint is not a direct result, issue a check to further filter the results on the intersection or exclusion. @@ -517,8 +528,8 @@ func (crr *CursoredLookupResources2) redispatchOrReport( metadata = addCallToResponseMetadata(checkMetadata) - filtered = make([]*v1.PossibleResource, 0, len(offsetted)) - for _, resource := range offsetted { + filtered = make([]possibleResourceAndIndex, 0, len(offsetted)) + for index, resource := range offsetted { result, ok := resultsByResourceID[resource.ResourceId] if !ok { continue @@ -526,16 +537,22 @@ func (crr *CursoredLookupResources2) redispatchOrReport( switch result.Membership { case v1.ResourceCheckResult_MEMBER: - filtered = append(filtered, resource) + filtered = append(filtered, possibleResourceAndIndex{ + resource: resource, + index: index, + }) case v1.ResourceCheckResult_CAVEATED_MEMBER: missingContextParams := mapz.NewSet(result.MissingExprFields...) missingContextParams.Extend(resource.MissingContextParams) - filtered = append(filtered, &v1.PossibleResource{ - ResourceId: resource.ResourceId, - ForSubjectIds: resource.ForSubjectIds, - MissingContextParams: missingContextParams.AsSlice(), + filtered = append(filtered, possibleResourceAndIndex{ + resource: &v1.PossibleResource{ + ResourceId: resource.ResourceId, + ForSubjectIds: resource.ForSubjectIds, + MissingContextParams: missingContextParams.AsSlice(), + }, + index: index, }) case v1.ResourceCheckResult_NOT_MEMBER: @@ -547,15 +564,15 @@ func (crr *CursoredLookupResources2) redispatchOrReport( } } - for index, resource := range filtered { + for _, resourceAndIndex := range filtered { if !ci.limits.prepareForPublishing() { return nil } err := parentStream.Publish(&v1.DispatchLookupResources2Response{ - Resource: resource, + Resource: resourceAndIndex.resource, Metadata: metadata, - AfterResponseCursor: nextCursorWith(currentOffset + index + 1), + AfterResponseCursor: nextCursorWith(currentOffset + resourceAndIndex.index + 1), }) if err != nil { return err @@ -581,28 +598,11 @@ func (crr *CursoredLookupResources2) redispatchOrReport( return nil } - // The stream that collects the results of the dispatch will add metadata to the response, - // map the results found based on the mapping data in the results and, if the entrypoint is not - // direct, issue a check to further filter the results. - currentCursor := ci.currentCursor - - // Loop until we've produced enough results to satisfy the limit. This is necessary because - // the dispatch may return a set of results that, after checking, is less than the limit. - for { - stream, completed := lookupResourcesDispatchStreamForEntrypoint(ctx, foundResources, parentStream, entrypoint, ci, parentRequest, crr.dc) - - // NOTE: if the entrypoint is a direct result, then all results returned by the dispatch will, themselves, - // be direct results. In this case, we can request the full limit of results. If the entrypoint is not a - // direct result, then we must request more than the limit in the hope that we get enough results to satisfy the - // limit after filtering. - var limit uint32 = uint32(datastore.FilterMaximumIDCount) - if entrypoint.IsDirectResult() { - limit = parentRequest.OptionalLimit - } - - // Dispatch the found resources as the subjects for the next call, to continue the - // resolution. - err = crr.dl.DispatchLookupResources2(&v1.DispatchLookupResources2Request{ + // If the entrypoint is a direct result then we can simply dispatch directly and map + // all found results, as no further filtering will be needed. + if entrypoint.IsDirectResult() { + stream := unfilteredLookupResourcesDispatchStreamForEntrypoint(ctx, foundResources, parentStream, ci) + return crr.dl.DispatchLookupResources2(&v1.DispatchLookupResources2Request{ ResourceRelation: parentRequest.ResourceRelation, SubjectRelation: newSubjectType, SubjectIds: filteredSubjectIDs, @@ -611,198 +611,24 @@ func (crr *CursoredLookupResources2) redispatchOrReport( AtRevision: parentRequest.Revision.String(), DepthRemaining: parentRequest.Metadata.DepthRemaining - 1, }, - OptionalCursor: currentCursor, - OptionalLimit: limit, // Request more than the limit to hopefully get enough results. + OptionalCursor: ci.currentCursor, + OptionalLimit: parentRequest.OptionalLimit, }, stream) - if err != nil { - // If the dispatch was canceled due to the limit, do not treat it as an error. - if errors.Is(err, errCanceledBecauseLimitReached) { - return err - } - } - - nextCursor, err := completed() - if err != nil { - return err - } - - if nextCursor == nil || ci.limits.hasExhaustedLimit() { - break - } - currentCursor = nextCursor } - return nil + // Otherwise, we need to dispatch and filter results by batch checking along the way. + return runDispatchAndChecker( + ctx, + parentRequest, + foundResources, + ci, + parentStream, + newSubjectType, + filteredSubjectIDs, + entrypoint, + crr.dl, + crr.dc, + crr.concurrencyLimit, + ) }) } - -func lookupResourcesDispatchStreamForEntrypoint( - ctx context.Context, - foundResources dispatchableResourcesSubjectMap2, - parentStream dispatch.LookupResources2Stream, - entrypoint typesystem.ReachabilityEntrypoint, - ci cursorInformation, - parentRequest ValidatedLookupResources2Request, - dc dispatch.Check, -) (dispatch.LookupResources2Stream, func() (*v1.Cursor, error)) { - // Branch the context so that the dispatch can be canceled without canceling the parent - // call. - sctx, cancelDispatch := branchContext(ctx) - - needsCallAddedToMetadata := true - resultsToCheck := make([]*v1.DispatchLookupResources2Response, 0, int(datastore.FilterMaximumIDCount)) - var nextCursor *v1.Cursor - - publishResultToParentStream := func( - result *v1.DispatchLookupResources2Response, - additionalMissingContext []string, - additionalMetadata *v1.ResponseMeta, - ) error { - // Map the found resources via the subject+resources used for dispatching, to determine - // if any need to be made conditional due to caveats. - mappedResource, err := foundResources.mapPossibleResource(result.Resource) - if err != nil { - return err - } - - if !ci.limits.prepareForPublishing() { - cancelDispatch(errCanceledBecauseLimitReached) - return nil - } - - // The cursor for the response is that of the parent response + the cursor from the result itself. - afterResponseCursor, err := combineCursors( - ci.responsePartialCursor(), - result.AfterResponseCursor, - ) - if err != nil { - return err - } - - metadata := combineResponseMetadata(result.Metadata, additionalMetadata) - - // Only the first dispatched result gets the call added to it. This is to prevent overcounting - // of the batched dispatch. - if needsCallAddedToMetadata { - metadata = addCallToResponseMetadata(metadata) - needsCallAddedToMetadata = false - } else { - metadata = addAdditionalDepthRequired(metadata) - } - - missingContextParameters := mapz.NewSet(mappedResource.MissingContextParams...) - missingContextParameters.Extend(result.Resource.MissingContextParams) - missingContextParameters.Extend(additionalMissingContext) - - mappedResource.MissingContextParams = missingContextParameters.AsSlice() - - resp := &v1.DispatchLookupResources2Response{ - Resource: mappedResource, - Metadata: metadata, - AfterResponseCursor: afterResponseCursor, - } - - return parentStream.Publish(resp) - } - - batchCheckAndPublishIfNecessary := func(result *v1.DispatchLookupResources2Response) error { - // Add the result to the list of results to check. If nil, this is the final call to check+publish. - if result != nil { - resultsToCheck = append(resultsToCheck, result) - } - - // If we have not yet reached the maximum number of results to check and this is not the final - // call, return early. - if len(resultsToCheck) < int(datastore.FilterMaximumIDCount) && result != nil { - return nil - } - - // Ensure there are items left to check. - if len(resultsToCheck) == 0 { - return nil - } - - // Build the set of resource IDs to check and the hints to short circuit the check on the current entrypoint. - checkHints := make(map[string]*v1.ResourceCheckResult, len(resultsToCheck)) - resourceIDsToCheck := make([]string, 0, len(resultsToCheck)) - for _, resource := range resultsToCheck { - hintKey, err := hintString(resource.Resource.ResourceId, entrypoint, parentRequest.TerminalSubject) - if err != nil { - return err - } - - resourceIDsToCheck = append(resourceIDsToCheck, resource.Resource.ResourceId) - - checkHints[hintKey] = &v1.ResourceCheckResult{ - Membership: v1.ResourceCheckResult_MEMBER, - } - } - - // Batch check the results to filter to those visible and then publish just the visible resources. - resultsByResourceID, checkMetadata, err := computed.ComputeBulkCheck(ctx, dc, computed.CheckParameters{ - ResourceType: parentRequest.ResourceRelation, - Subject: parentRequest.TerminalSubject, - CaveatContext: parentRequest.Context.AsMap(), - AtRevision: parentRequest.Revision, - MaximumDepth: parentRequest.Metadata.DepthRemaining - 1, - DebugOption: computed.NoDebugging, - CheckHints: checkHints, - }, resourceIDsToCheck) - if err != nil { - return err - } - - metadata := checkMetadata - for _, resource := range resultsToCheck { - result, ok := resultsByResourceID[resource.Resource.ResourceId] - if !ok { - continue - } - - switch result.Membership { - case v1.ResourceCheckResult_MEMBER: - fallthrough - - case v1.ResourceCheckResult_CAVEATED_MEMBER: - if err := publishResultToParentStream(resource, result.MissingExprFields, metadata); err != nil { - return err - } - metadata = emptyMetadata - - case v1.ResourceCheckResult_NOT_MEMBER: - // Skip. - continue - - default: - return spiceerrors.MustBugf("unexpected result from check: %v", result.Membership) - } - } - - resultsToCheck = make([]*v1.DispatchLookupResources2Response, 0, int(datastore.FilterMaximumIDCount)) - return nil - } - - wrappedStream := dispatch.NewHandlingDispatchStream(sctx, func(result *v1.DispatchLookupResources2Response) error { - select { - case <-ctx.Done(): - return ctx.Err() - - default: - } - - nextCursor = result.AfterResponseCursor - - // If the entrypoint is a direct result, simply publish the found resource. - if entrypoint.IsDirectResult() { - return publishResultToParentStream(result, nil, emptyMetadata) - } - - // Otherwise, queue the result for checking and publishing if the check succeeds. - return batchCheckAndPublishIfNecessary(result) - }) - - return wrappedStream, func() (*v1.Cursor, error) { - defer cancelDispatch(nil) - return nextCursor, batchCheckAndPublishIfNecessary(nil) - } -} diff --git a/internal/graph/lr2streams.go b/internal/graph/lr2streams.go new file mode 100644 index 0000000000..65455f4004 --- /dev/null +++ b/internal/graph/lr2streams.go @@ -0,0 +1,286 @@ +package graph + +import ( + "context" + "sync" + + "github.com/authzed/spicedb/internal/dispatch" + "github.com/authzed/spicedb/internal/graph/computed" + "github.com/authzed/spicedb/internal/taskrunner" + "github.com/authzed/spicedb/pkg/datastore" + "github.com/authzed/spicedb/pkg/genutil/mapz" + core "github.com/authzed/spicedb/pkg/proto/core/v1" + v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" + "github.com/authzed/spicedb/pkg/spiceerrors" + "github.com/authzed/spicedb/pkg/typesystem" +) + +// runDispatchAndChecker runs the dispatch and checker for a lookup resources call, and publishes +// the results to the parent stream. This function is responsible for handling the dispatching +// of the lookup resources call, and then checking the results to filter them. +func runDispatchAndChecker( + ctx context.Context, + parentReq ValidatedLookupResources2Request, + foundResources dispatchableResourcesSubjectMap2, + ci cursorInformation, + parentStream dispatch.LookupResources2Stream, + newSubjectType *core.RelationReference, + filteredSubjectIDs []string, + entrypoint typesystem.ReachabilityEntrypoint, + lrDispatcher dispatch.LookupResources2, + checkDispatcher dispatch.Check, + concurrencyLimit uint16, +) error { + // Only allow max one dispatcher and one checker to run concurrently. + concurrencyLimit = min(concurrencyLimit, 2) + + rdc := &rdc{ + parentRequest: parentReq, + foundResources: foundResources, + ci: ci, + parentStream: parentStream, + newSubjectType: newSubjectType, + filteredSubjectIDs: filteredSubjectIDs, + entrypoint: entrypoint, + lrDispatcher: lrDispatcher, + checkDispatcher: checkDispatcher, + taskrunner: taskrunner.NewTaskRunner(ctx, concurrencyLimit), + lock: &sync.Mutex{}, + } + + return rdc.runAndWait() +} + +type rdc struct { + parentRequest ValidatedLookupResources2Request + foundResources dispatchableResourcesSubjectMap2 + ci cursorInformation + parentStream dispatch.LookupResources2Stream + newSubjectType *core.RelationReference + filteredSubjectIDs []string + entrypoint typesystem.ReachabilityEntrypoint + lrDispatcher dispatch.LookupResources2 + checkDispatcher dispatch.Check + + taskrunner *taskrunner.TaskRunner + + lock *sync.Mutex +} + +func (rdc *rdc) dispatchAndCollect(ctx context.Context, cursor *v1.Cursor) ([]*v1.DispatchLookupResources2Response, error) { + collectingStream := dispatch.NewCollectingDispatchStream[*v1.DispatchLookupResources2Response](ctx) + err := rdc.lrDispatcher.DispatchLookupResources2(&v1.DispatchLookupResources2Request{ + ResourceRelation: rdc.parentRequest.ResourceRelation, + SubjectRelation: rdc.newSubjectType, + SubjectIds: rdc.filteredSubjectIDs, + TerminalSubject: rdc.parentRequest.TerminalSubject, + Metadata: &v1.ResolverMeta{ + AtRevision: rdc.parentRequest.Revision.String(), + DepthRemaining: rdc.parentRequest.Metadata.DepthRemaining - 1, + }, + OptionalCursor: cursor, + OptionalLimit: uint32(datastore.FilterMaximumIDCount), + }, collectingStream) + return collectingStream.Results(), err +} + +func (rdc *rdc) runDispatch(ctx context.Context, cursor *v1.Cursor) error { + rdc.lock.Lock() + if rdc.ci.limits.hasExhaustedLimit() { + rdc.lock.Unlock() + return nil + } + rdc.lock.Unlock() + + collected, err := rdc.dispatchAndCollect(ctx, cursor) + if err != nil { + return err + } + + if len(collected) == 0 { + return nil + } + + // Kick off a worker to filter the results via a check and then publish what was found. + rdc.taskrunner.Schedule(func(ctx context.Context) error { + return rdc.runChecker(ctx, collected) + }) + + // Start another dispatch at the cursor of the last response, to run in the background + // and collect more results for filtering while the checker is running. + rdc.taskrunner.Schedule(func(ctx context.Context) error { + return rdc.runDispatch(ctx, collected[len(collected)-1].AfterResponseCursor) + }) + + return nil +} + +func (rdc *rdc) runChecker(ctx context.Context, collected []*v1.DispatchLookupResources2Response) error { + rdc.lock.Lock() + if rdc.ci.limits.hasExhaustedLimit() { + rdc.lock.Unlock() + return nil + } + rdc.lock.Unlock() + + checkHints := make(map[string]*v1.ResourceCheckResult, len(collected)) + resourceIDsToCheck := make([]string, 0, len(collected)) + for _, resource := range collected { + hintKey, err := hintString(resource.Resource.ResourceId, rdc.entrypoint, rdc.parentRequest.TerminalSubject) + if err != nil { + return err + } + + resourceIDsToCheck = append(resourceIDsToCheck, resource.Resource.ResourceId) + + checkHints[hintKey] = &v1.ResourceCheckResult{ + Membership: v1.ResourceCheckResult_MEMBER, + } + } + + // Batch check the results to filter to those visible and then publish just the visible resources. + resultsByResourceID, checkMetadata, err := computed.ComputeBulkCheck(ctx, rdc.checkDispatcher, computed.CheckParameters{ + ResourceType: rdc.parentRequest.ResourceRelation, + Subject: rdc.parentRequest.TerminalSubject, + CaveatContext: rdc.parentRequest.Context.AsMap(), + AtRevision: rdc.parentRequest.Revision, + MaximumDepth: rdc.parentRequest.Metadata.DepthRemaining - 1, + DebugOption: computed.NoDebugging, + CheckHints: checkHints, + }, resourceIDsToCheck) + if err != nil { + return err + } + + // Publish any resources that are visible. + isFirstPublishCall := true + for _, resource := range collected { + result, ok := resultsByResourceID[resource.Resource.ResourceId] + if !ok { + continue + } + + switch result.Membership { + case v1.ResourceCheckResult_MEMBER: + fallthrough + + case v1.ResourceCheckResult_CAVEATED_MEMBER: + rdc.lock.Lock() + if err := publishResultToParentStream(resource, rdc.ci, rdc.foundResources, result.MissingExprFields, isFirstPublishCall, checkMetadata, rdc.parentStream); err != nil { + rdc.lock.Unlock() + return err + } + + isFirstPublishCall = false + + if rdc.ci.limits.hasExhaustedLimit() { + rdc.lock.Unlock() + return nil + } + rdc.lock.Unlock() + + case v1.ResourceCheckResult_NOT_MEMBER: + // Skip. + continue + + default: + return spiceerrors.MustBugf("unexpected result from check: %v", result.Membership) + } + } + + return nil +} + +func (rdc *rdc) runAndWait() error { + currentCursor := rdc.ci.currentCursor + + // Kick off a dispatch at the current cursor. + rdc.taskrunner.Schedule(func(ctx context.Context) error { + return rdc.runDispatch(ctx, currentCursor) + }) + + return rdc.taskrunner.Wait() +} + +// unfilteredLookupResourcesDispatchStreamForEntrypoint creates a new dispatch stream that wraps +// the parent stream, and publishes the results of the lookup resources call to the parent stream, +// mapped via foundResources. +func unfilteredLookupResourcesDispatchStreamForEntrypoint( + ctx context.Context, + foundResources dispatchableResourcesSubjectMap2, + parentStream dispatch.LookupResources2Stream, + ci cursorInformation, +) dispatch.LookupResources2Stream { + isFirstPublishCall := true + + wrappedStream := dispatch.NewHandlingDispatchStream(ctx, func(result *v1.DispatchLookupResources2Response) error { + select { + case <-ctx.Done(): + return ctx.Err() + + default: + } + + if err := publishResultToParentStream(result, ci, foundResources, nil, isFirstPublishCall, emptyMetadata, parentStream); err != nil { + return err + } + isFirstPublishCall = false + return nil + }) + + return wrappedStream +} + +// publishResultToParentStream publishes the result of a lookup resources call to the parent stream, +// mapped via foundResources. +func publishResultToParentStream( + result *v1.DispatchLookupResources2Response, + ci cursorInformation, + foundResources dispatchableResourcesSubjectMap2, + additionalMissingContext []string, + isFirstPublishCall bool, + additionalMetadata *v1.ResponseMeta, + parentStream dispatch.LookupResources2Stream, +) error { + // Map the found resources via the subject+resources used for dispatching, to determine + // if any need to be made conditional due to caveats. + mappedResource, err := foundResources.mapPossibleResource(result.Resource) + if err != nil { + return err + } + + if !ci.limits.prepareForPublishing() { + return nil + } + + // The cursor for the response is that of the parent response + the cursor from the result itself. + afterResponseCursor, err := combineCursors( + ci.responsePartialCursor(), + result.AfterResponseCursor, + ) + if err != nil { + return err + } + + metadata := result.Metadata + if isFirstPublishCall { + metadata = addCallToResponseMetadata(metadata) + metadata = combineResponseMetadata(metadata, additionalMetadata) + } else { + metadata = addAdditionalDepthRequired(metadata) + } + + missingContextParameters := mapz.NewSet(mappedResource.MissingContextParams...) + missingContextParameters.Extend(result.Resource.MissingContextParams) + missingContextParameters.Extend(additionalMissingContext) + + mappedResource.MissingContextParams = missingContextParameters.AsSlice() + + resp := &v1.DispatchLookupResources2Response{ + Resource: mappedResource, + Metadata: metadata, + AfterResponseCursor: afterResponseCursor, + } + + return parentStream.Publish(resp) +} diff --git a/internal/services/steelthreadtesting/definitions.go b/internal/services/steelthreadtesting/definitions.go index f1e280d77f..c95a6db16e 100644 --- a/internal/services/steelthreadtesting/definitions.go +++ b/internal/services/steelthreadtesting/definitions.go @@ -275,4 +275,56 @@ var steelThreadTestCases = []steelThreadTestCase{ }, }, }, + { + name: "lookup resources with intersection", + datafile: "document-with-intersect-resources.yaml", + operations: []steelThreadOperationCase{ + { + name: "uncursored lookup resources for user:fred", + operationName: "lookupResources", + arguments: map[string]any{ + "resource_type": "document", + "permission": "view", + "subject_type": "user", + "subject_object_id": "fred", + }, + resultsFileName: "lookup-resources-with-intersection-uncursored-indirect-lookup-resources-for-user-fred-results.yaml", + }, + { + name: "uncursored indirect lookup resources for user:fred", + operationName: "lookupResources", + arguments: map[string]any{ + "resource_type": "document", + "permission": "indirect_view", + "subject_type": "user", + "subject_object_id": "fred", + }, + resultsFileName: "lookup-resources-with-intersection-uncursored-indirect-lookup-resources-for-user-fred-results.yaml", + }, + { + name: "cursored lookup resources for user:fred", + operationName: "cursoredLookupResources", + arguments: map[string]any{ + "resource_type": "document", + "permission": "view", + "subject_type": "user", + "subject_object_id": "fred", + "page_size": 18, + }, + resultsFileName: "lookup-resources-with-intersection-cursored-lookup-resources-for-user-fred-results.yaml", + }, + { + name: "cursored indirect lookup resources for user:fred", + operationName: "cursoredLookupResources", + arguments: map[string]any{ + "resource_type": "document", + "permission": "indirect_view", + "subject_type": "user", + "subject_object_id": "fred", + "page_size": 18, + }, + resultsFileName: "lookup-resources-with-intersection-cursored-lookup-resources-for-user-fred-results.yaml", + }, + }, + }, } diff --git a/internal/services/steelthreadtesting/operations.go b/internal/services/steelthreadtesting/operations.go index 41c9c833dd..5537b72a4d 100644 --- a/internal/services/steelthreadtesting/operations.go +++ b/internal/services/steelthreadtesting/operations.go @@ -216,7 +216,7 @@ func cursoredLookupResources(parameters map[string]any, client v1.PermissionsSer } if count != parameters["page_size"].(int) { - return nil, fmt.Errorf("expected full page size of %d for page #%d, got %d", parameters["page_size"].(int), index, count) + return nil, fmt.Errorf("expected full page size of %d for page #%d (of %d), got %d\npage sizes: %v", parameters["page_size"].(int), index, len(resultCounts), count, resultCounts) } } diff --git a/internal/services/steelthreadtesting/steelresults/basic-lookup-resources-indirect-without-other-permission-page-size-16-results.yaml b/internal/services/steelthreadtesting/steelresults/basic-lookup-resources-indirect-without-other-permission-page-size-16-results.yaml index fae8d51ba9..0292a6a3a2 100644 --- a/internal/services/steelthreadtesting/steelresults/basic-lookup-resources-indirect-without-other-permission-page-size-16-results.yaml +++ b/internal/services/steelthreadtesting/steelresults/basic-lookup-resources-indirect-without-other-permission-page-size-16-results.yaml @@ -79,9 +79,6 @@ - 'doc-79' - 'doc-8' - 'doc-9' -- - 'doc-9' - - 'public-doc-0' +- - 'public-doc-0' - 'public-doc-1' - 'public-doc-3' -- - 'public-doc-1' - - 'public-doc-3' diff --git a/internal/services/steelthreadtesting/steelresults/basic-lookup-resources-indirect-without-other-permission-page-size-5-results.yaml b/internal/services/steelthreadtesting/steelresults/basic-lookup-resources-indirect-without-other-permission-page-size-5-results.yaml index 6f3175bf97..9378e5b804 100644 --- a/internal/services/steelthreadtesting/steelresults/basic-lookup-resources-indirect-without-other-permission-page-size-5-results.yaml +++ b/internal/services/steelthreadtesting/steelresults/basic-lookup-resources-indirect-without-other-permission-page-size-5-results.yaml @@ -79,17 +79,6 @@ - 'doc-79' - 'doc-8' - 'doc-9' -- - 'doc-9' - - 'public-doc-0' - - 'public-doc-1' - - 'public-doc-3' -- - 'doc-9' - - 'public-doc-0' - - 'public-doc-1' - - 'public-doc-3' -- - 'public-doc-0' - - 'public-doc-1' - - 'public-doc-3' - - 'public-doc-0' - 'public-doc-1' - 'public-doc-3' diff --git a/internal/services/steelthreadtesting/steelresults/lookup-resources-with-intersection-cursored-lookup-resources-for-user-fred-results.yaml b/internal/services/steelthreadtesting/steelresults/lookup-resources-with-intersection-cursored-lookup-resources-for-user-fred-results.yaml new file mode 100644 index 0000000000..c215832620 --- /dev/null +++ b/internal/services/steelthreadtesting/steelresults/lookup-resources-with-intersection-cursored-lookup-resources-for-user-fred-results.yaml @@ -0,0 +1,151 @@ +--- +- - 'doc-150' + - 'doc-151' + - 'doc-152' + - 'doc-153' + - 'doc-154' + - 'doc-155' + - 'doc-156' + - 'doc-157' + - 'doc-158' + - 'doc-159' + - 'doc-160' + - 'doc-161' + - 'doc-162' + - 'doc-163' + - 'doc-164' + - 'doc-165' + - 'doc-166' + - 'doc-167' +- - 'doc-168' + - 'doc-169' + - 'doc-170' + - 'doc-171' + - 'doc-172' + - 'doc-173' + - 'doc-174' + - 'doc-175' + - 'doc-176' + - 'doc-177' + - 'doc-178' + - 'doc-179' + - 'doc-180' + - 'doc-181' + - 'doc-182' + - 'doc-183' + - 'doc-184' + - 'doc-185' +- - 'doc-186' + - 'doc-187' + - 'doc-188' + - 'doc-189' + - 'doc-190' + - 'doc-191' + - 'doc-192' + - 'doc-193' + - 'doc-194' + - 'doc-195' + - 'doc-196' + - 'doc-197' + - 'doc-198' + - 'doc-199' + - 'doc-200' + - 'doc-201' + - 'doc-202' + - 'doc-203' +- - 'doc-204' + - 'doc-205' + - 'doc-206' + - 'doc-207' + - 'doc-208' + - 'doc-209' + - 'doc-210' + - 'doc-211' + - 'doc-212' + - 'doc-213' + - 'doc-214' + - 'doc-215' + - 'doc-216' + - 'doc-217' + - 'doc-218' + - 'doc-219' + - 'doc-220' + - 'doc-221' +- - 'doc-222' + - 'doc-223' + - 'doc-224' + - 'doc-225' + - 'doc-226' + - 'doc-227' + - 'doc-228' + - 'doc-229' + - 'doc-230' + - 'doc-231' + - 'doc-232' + - 'doc-233' + - 'doc-234' + - 'doc-235' + - 'doc-236' + - 'doc-237' + - 'doc-238' + - 'doc-239' +- - 'doc-240' + - 'doc-241' + - 'doc-242' + - 'doc-243' + - 'doc-244' + - 'doc-245' + - 'doc-246' + - 'doc-247' + - 'doc-248' + - 'doc-249' + - 'doc-250' + - 'doc-251' + - 'doc-252' + - 'doc-253' + - 'doc-254' + - 'doc-255' + - 'doc-256' + - 'doc-257' +- - 'doc-258' + - 'doc-259' + - 'doc-260' + - 'doc-261' + - 'doc-262' + - 'doc-263' + - 'doc-264' + - 'doc-265' + - 'doc-266' + - 'doc-267' + - 'doc-268' + - 'doc-269' + - 'doc-270' + - 'doc-271' + - 'doc-272' + - 'doc-273' + - 'doc-274' + - 'doc-275' +- - 'doc-276' + - 'doc-277' + - 'doc-278' + - 'doc-279' + - 'doc-280' + - 'doc-281' + - 'doc-282' + - 'doc-283' + - 'doc-284' + - 'doc-285' + - 'doc-286' + - 'doc-287' + - 'doc-288' + - 'doc-289' + - 'doc-290' + - 'doc-291' + - 'doc-292' + - 'doc-293' +- - 'doc-294' + - 'doc-295' + - 'doc-296' + - 'doc-297' + - 'doc-298' + - 'doc-299' diff --git a/internal/services/steelthreadtesting/steelresults/lookup-resources-with-intersection-uncursored-indirect-lookup-resources-for-user-fred-results.yaml b/internal/services/steelthreadtesting/steelresults/lookup-resources-with-intersection-uncursored-indirect-lookup-resources-for-user-fred-results.yaml new file mode 100644 index 0000000000..f0a775f0fa --- /dev/null +++ b/internal/services/steelthreadtesting/steelresults/lookup-resources-with-intersection-uncursored-indirect-lookup-resources-for-user-fred-results.yaml @@ -0,0 +1,151 @@ +--- +- 'doc-150' +- 'doc-151' +- 'doc-152' +- 'doc-153' +- 'doc-154' +- 'doc-155' +- 'doc-156' +- 'doc-157' +- 'doc-158' +- 'doc-159' +- 'doc-160' +- 'doc-161' +- 'doc-162' +- 'doc-163' +- 'doc-164' +- 'doc-165' +- 'doc-166' +- 'doc-167' +- 'doc-168' +- 'doc-169' +- 'doc-170' +- 'doc-171' +- 'doc-172' +- 'doc-173' +- 'doc-174' +- 'doc-175' +- 'doc-176' +- 'doc-177' +- 'doc-178' +- 'doc-179' +- 'doc-180' +- 'doc-181' +- 'doc-182' +- 'doc-183' +- 'doc-184' +- 'doc-185' +- 'doc-186' +- 'doc-187' +- 'doc-188' +- 'doc-189' +- 'doc-190' +- 'doc-191' +- 'doc-192' +- 'doc-193' +- 'doc-194' +- 'doc-195' +- 'doc-196' +- 'doc-197' +- 'doc-198' +- 'doc-199' +- 'doc-200' +- 'doc-201' +- 'doc-202' +- 'doc-203' +- 'doc-204' +- 'doc-205' +- 'doc-206' +- 'doc-207' +- 'doc-208' +- 'doc-209' +- 'doc-210' +- 'doc-211' +- 'doc-212' +- 'doc-213' +- 'doc-214' +- 'doc-215' +- 'doc-216' +- 'doc-217' +- 'doc-218' +- 'doc-219' +- 'doc-220' +- 'doc-221' +- 'doc-222' +- 'doc-223' +- 'doc-224' +- 'doc-225' +- 'doc-226' +- 'doc-227' +- 'doc-228' +- 'doc-229' +- 'doc-230' +- 'doc-231' +- 'doc-232' +- 'doc-233' +- 'doc-234' +- 'doc-235' +- 'doc-236' +- 'doc-237' +- 'doc-238' +- 'doc-239' +- 'doc-240' +- 'doc-241' +- 'doc-242' +- 'doc-243' +- 'doc-244' +- 'doc-245' +- 'doc-246' +- 'doc-247' +- 'doc-248' +- 'doc-249' +- 'doc-250' +- 'doc-251' +- 'doc-252' +- 'doc-253' +- 'doc-254' +- 'doc-255' +- 'doc-256' +- 'doc-257' +- 'doc-258' +- 'doc-259' +- 'doc-260' +- 'doc-261' +- 'doc-262' +- 'doc-263' +- 'doc-264' +- 'doc-265' +- 'doc-266' +- 'doc-267' +- 'doc-268' +- 'doc-269' +- 'doc-270' +- 'doc-271' +- 'doc-272' +- 'doc-273' +- 'doc-274' +- 'doc-275' +- 'doc-276' +- 'doc-277' +- 'doc-278' +- 'doc-279' +- 'doc-280' +- 'doc-281' +- 'doc-282' +- 'doc-283' +- 'doc-284' +- 'doc-285' +- 'doc-286' +- 'doc-287' +- 'doc-288' +- 'doc-289' +- 'doc-290' +- 'doc-291' +- 'doc-292' +- 'doc-293' +- 'doc-294' +- 'doc-295' +- 'doc-296' +- 'doc-297' +- 'doc-298' +- 'doc-299' diff --git a/internal/services/steelthreadtesting/testdata/document-with-intersect-resources.yaml b/internal/services/steelthreadtesting/testdata/document-with-intersect-resources.yaml new file mode 100644 index 0000000000..57e1932ac7 --- /dev/null +++ b/internal/services/steelthreadtesting/testdata/document-with-intersect-resources.yaml @@ -0,0 +1,622 @@ +--- +schema: |+ + definition user {} + + definition document { + relation viewer1: user + relation viewer2: user + permission view = viewer1 & viewer2 + + permission indirect_viewer1 = viewer1 + permission indirect_viewer2 = viewer2 + permission indirect_view = indirect_viewer1 & indirect_viewer2 + } + +relationships: | + // user:fred as viewer1 for 300 documents + // for docid in range(0, 300): + // document:doc-{docid}#viewer1@user:fred + document:doc-0#viewer1@user:fred + document:doc-1#viewer1@user:fred + document:doc-2#viewer1@user:fred + document:doc-3#viewer1@user:fred + document:doc-4#viewer1@user:fred + document:doc-5#viewer1@user:fred + document:doc-6#viewer1@user:fred + document:doc-7#viewer1@user:fred + document:doc-8#viewer1@user:fred + document:doc-9#viewer1@user:fred + document:doc-10#viewer1@user:fred + document:doc-11#viewer1@user:fred + document:doc-12#viewer1@user:fred + document:doc-13#viewer1@user:fred + document:doc-14#viewer1@user:fred + document:doc-15#viewer1@user:fred + document:doc-16#viewer1@user:fred + document:doc-17#viewer1@user:fred + document:doc-18#viewer1@user:fred + document:doc-19#viewer1@user:fred + document:doc-20#viewer1@user:fred + document:doc-21#viewer1@user:fred + document:doc-22#viewer1@user:fred + document:doc-23#viewer1@user:fred + document:doc-24#viewer1@user:fred + document:doc-25#viewer1@user:fred + document:doc-26#viewer1@user:fred + document:doc-27#viewer1@user:fred + document:doc-28#viewer1@user:fred + document:doc-29#viewer1@user:fred + document:doc-30#viewer1@user:fred + document:doc-31#viewer1@user:fred + document:doc-32#viewer1@user:fred + document:doc-33#viewer1@user:fred + document:doc-34#viewer1@user:fred + document:doc-35#viewer1@user:fred + document:doc-36#viewer1@user:fred + document:doc-37#viewer1@user:fred + document:doc-38#viewer1@user:fred + document:doc-39#viewer1@user:fred + document:doc-40#viewer1@user:fred + document:doc-41#viewer1@user:fred + document:doc-42#viewer1@user:fred + document:doc-43#viewer1@user:fred + document:doc-44#viewer1@user:fred + document:doc-45#viewer1@user:fred + document:doc-46#viewer1@user:fred + document:doc-47#viewer1@user:fred + document:doc-48#viewer1@user:fred + document:doc-49#viewer1@user:fred + document:doc-50#viewer1@user:fred + document:doc-51#viewer1@user:fred + document:doc-52#viewer1@user:fred + document:doc-53#viewer1@user:fred + document:doc-54#viewer1@user:fred + document:doc-55#viewer1@user:fred + document:doc-56#viewer1@user:fred + document:doc-57#viewer1@user:fred + document:doc-58#viewer1@user:fred + document:doc-59#viewer1@user:fred + document:doc-60#viewer1@user:fred + document:doc-61#viewer1@user:fred + document:doc-62#viewer1@user:fred + document:doc-63#viewer1@user:fred + document:doc-64#viewer1@user:fred + document:doc-65#viewer1@user:fred + document:doc-66#viewer1@user:fred + document:doc-67#viewer1@user:fred + document:doc-68#viewer1@user:fred + document:doc-69#viewer1@user:fred + document:doc-70#viewer1@user:fred + document:doc-71#viewer1@user:fred + document:doc-72#viewer1@user:fred + document:doc-73#viewer1@user:fred + document:doc-74#viewer1@user:fred + document:doc-75#viewer1@user:fred + document:doc-76#viewer1@user:fred + document:doc-77#viewer1@user:fred + document:doc-78#viewer1@user:fred + document:doc-79#viewer1@user:fred + document:doc-80#viewer1@user:fred + document:doc-81#viewer1@user:fred + document:doc-82#viewer1@user:fred + document:doc-83#viewer1@user:fred + document:doc-84#viewer1@user:fred + document:doc-85#viewer1@user:fred + document:doc-86#viewer1@user:fred + document:doc-87#viewer1@user:fred + document:doc-88#viewer1@user:fred + document:doc-89#viewer1@user:fred + document:doc-90#viewer1@user:fred + document:doc-91#viewer1@user:fred + document:doc-92#viewer1@user:fred + document:doc-93#viewer1@user:fred + document:doc-94#viewer1@user:fred + document:doc-95#viewer1@user:fred + document:doc-96#viewer1@user:fred + document:doc-97#viewer1@user:fred + document:doc-98#viewer1@user:fred + document:doc-99#viewer1@user:fred + document:doc-100#viewer1@user:fred + document:doc-101#viewer1@user:fred + document:doc-102#viewer1@user:fred + document:doc-103#viewer1@user:fred + document:doc-104#viewer1@user:fred + document:doc-105#viewer1@user:fred + document:doc-106#viewer1@user:fred + document:doc-107#viewer1@user:fred + document:doc-108#viewer1@user:fred + document:doc-109#viewer1@user:fred + document:doc-110#viewer1@user:fred + document:doc-111#viewer1@user:fred + document:doc-112#viewer1@user:fred + document:doc-113#viewer1@user:fred + document:doc-114#viewer1@user:fred + document:doc-115#viewer1@user:fred + document:doc-116#viewer1@user:fred + document:doc-117#viewer1@user:fred + document:doc-118#viewer1@user:fred + document:doc-119#viewer1@user:fred + document:doc-120#viewer1@user:fred + document:doc-121#viewer1@user:fred + document:doc-122#viewer1@user:fred + document:doc-123#viewer1@user:fred + document:doc-124#viewer1@user:fred + document:doc-125#viewer1@user:fred + document:doc-126#viewer1@user:fred + document:doc-127#viewer1@user:fred + document:doc-128#viewer1@user:fred + document:doc-129#viewer1@user:fred + document:doc-130#viewer1@user:fred + document:doc-131#viewer1@user:fred + document:doc-132#viewer1@user:fred + document:doc-133#viewer1@user:fred + document:doc-134#viewer1@user:fred + document:doc-135#viewer1@user:fred + document:doc-136#viewer1@user:fred + document:doc-137#viewer1@user:fred + document:doc-138#viewer1@user:fred + document:doc-139#viewer1@user:fred + document:doc-140#viewer1@user:fred + document:doc-141#viewer1@user:fred + document:doc-142#viewer1@user:fred + document:doc-143#viewer1@user:fred + document:doc-144#viewer1@user:fred + document:doc-145#viewer1@user:fred + document:doc-146#viewer1@user:fred + document:doc-147#viewer1@user:fred + document:doc-148#viewer1@user:fred + document:doc-149#viewer1@user:fred + document:doc-150#viewer1@user:fred + document:doc-151#viewer1@user:fred + document:doc-152#viewer1@user:fred + document:doc-153#viewer1@user:fred + document:doc-154#viewer1@user:fred + document:doc-155#viewer1@user:fred + document:doc-156#viewer1@user:fred + document:doc-157#viewer1@user:fred + document:doc-158#viewer1@user:fred + document:doc-159#viewer1@user:fred + document:doc-160#viewer1@user:fred + document:doc-161#viewer1@user:fred + document:doc-162#viewer1@user:fred + document:doc-163#viewer1@user:fred + document:doc-164#viewer1@user:fred + document:doc-165#viewer1@user:fred + document:doc-166#viewer1@user:fred + document:doc-167#viewer1@user:fred + document:doc-168#viewer1@user:fred + document:doc-169#viewer1@user:fred + document:doc-170#viewer1@user:fred + document:doc-171#viewer1@user:fred + document:doc-172#viewer1@user:fred + document:doc-173#viewer1@user:fred + document:doc-174#viewer1@user:fred + document:doc-175#viewer1@user:fred + document:doc-176#viewer1@user:fred + document:doc-177#viewer1@user:fred + document:doc-178#viewer1@user:fred + document:doc-179#viewer1@user:fred + document:doc-180#viewer1@user:fred + document:doc-181#viewer1@user:fred + document:doc-182#viewer1@user:fred + document:doc-183#viewer1@user:fred + document:doc-184#viewer1@user:fred + document:doc-185#viewer1@user:fred + document:doc-186#viewer1@user:fred + document:doc-187#viewer1@user:fred + document:doc-188#viewer1@user:fred + document:doc-189#viewer1@user:fred + document:doc-190#viewer1@user:fred + document:doc-191#viewer1@user:fred + document:doc-192#viewer1@user:fred + document:doc-193#viewer1@user:fred + document:doc-194#viewer1@user:fred + document:doc-195#viewer1@user:fred + document:doc-196#viewer1@user:fred + document:doc-197#viewer1@user:fred + document:doc-198#viewer1@user:fred + document:doc-199#viewer1@user:fred + document:doc-200#viewer1@user:fred + document:doc-201#viewer1@user:fred + document:doc-202#viewer1@user:fred + document:doc-203#viewer1@user:fred + document:doc-204#viewer1@user:fred + document:doc-205#viewer1@user:fred + document:doc-206#viewer1@user:fred + document:doc-207#viewer1@user:fred + document:doc-208#viewer1@user:fred + document:doc-209#viewer1@user:fred + document:doc-210#viewer1@user:fred + document:doc-211#viewer1@user:fred + document:doc-212#viewer1@user:fred + document:doc-213#viewer1@user:fred + document:doc-214#viewer1@user:fred + document:doc-215#viewer1@user:fred + document:doc-216#viewer1@user:fred + document:doc-217#viewer1@user:fred + document:doc-218#viewer1@user:fred + document:doc-219#viewer1@user:fred + document:doc-220#viewer1@user:fred + document:doc-221#viewer1@user:fred + document:doc-222#viewer1@user:fred + document:doc-223#viewer1@user:fred + document:doc-224#viewer1@user:fred + document:doc-225#viewer1@user:fred + document:doc-226#viewer1@user:fred + document:doc-227#viewer1@user:fred + document:doc-228#viewer1@user:fred + document:doc-229#viewer1@user:fred + document:doc-230#viewer1@user:fred + document:doc-231#viewer1@user:fred + document:doc-232#viewer1@user:fred + document:doc-233#viewer1@user:fred + document:doc-234#viewer1@user:fred + document:doc-235#viewer1@user:fred + document:doc-236#viewer1@user:fred + document:doc-237#viewer1@user:fred + document:doc-238#viewer1@user:fred + document:doc-239#viewer1@user:fred + document:doc-240#viewer1@user:fred + document:doc-241#viewer1@user:fred + document:doc-242#viewer1@user:fred + document:doc-243#viewer1@user:fred + document:doc-244#viewer1@user:fred + document:doc-245#viewer1@user:fred + document:doc-246#viewer1@user:fred + document:doc-247#viewer1@user:fred + document:doc-248#viewer1@user:fred + document:doc-249#viewer1@user:fred + document:doc-250#viewer1@user:fred + document:doc-251#viewer1@user:fred + document:doc-252#viewer1@user:fred + document:doc-253#viewer1@user:fred + document:doc-254#viewer1@user:fred + document:doc-255#viewer1@user:fred + document:doc-256#viewer1@user:fred + document:doc-257#viewer1@user:fred + document:doc-258#viewer1@user:fred + document:doc-259#viewer1@user:fred + document:doc-260#viewer1@user:fred + document:doc-261#viewer1@user:fred + document:doc-262#viewer1@user:fred + document:doc-263#viewer1@user:fred + document:doc-264#viewer1@user:fred + document:doc-265#viewer1@user:fred + document:doc-266#viewer1@user:fred + document:doc-267#viewer1@user:fred + document:doc-268#viewer1@user:fred + document:doc-269#viewer1@user:fred + document:doc-270#viewer1@user:fred + document:doc-271#viewer1@user:fred + document:doc-272#viewer1@user:fred + document:doc-273#viewer1@user:fred + document:doc-274#viewer1@user:fred + document:doc-275#viewer1@user:fred + document:doc-276#viewer1@user:fred + document:doc-277#viewer1@user:fred + document:doc-278#viewer1@user:fred + document:doc-279#viewer1@user:fred + document:doc-280#viewer1@user:fred + document:doc-281#viewer1@user:fred + document:doc-282#viewer1@user:fred + document:doc-283#viewer1@user:fred + document:doc-284#viewer1@user:fred + document:doc-285#viewer1@user:fred + document:doc-286#viewer1@user:fred + document:doc-287#viewer1@user:fred + document:doc-288#viewer1@user:fred + document:doc-289#viewer1@user:fred + document:doc-290#viewer1@user:fred + document:doc-291#viewer1@user:fred + document:doc-292#viewer1@user:fred + document:doc-293#viewer1@user:fred + document:doc-294#viewer1@user:fred + document:doc-295#viewer1@user:fred + document:doc-296#viewer1@user:fred + document:doc-297#viewer1@user:fred + document:doc-298#viewer1@user:fred + document:doc-299#viewer1@user:fred + + // user:fred as viewer2 for 300 documents, starting at doc 150 + // for docid in range(150, 450): + // document:doc-{docid}#viewer2@user:fred + document:doc-150#viewer2@user:fred + document:doc-151#viewer2@user:fred + document:doc-152#viewer2@user:fred + document:doc-153#viewer2@user:fred + document:doc-154#viewer2@user:fred + document:doc-155#viewer2@user:fred + document:doc-156#viewer2@user:fred + document:doc-157#viewer2@user:fred + document:doc-158#viewer2@user:fred + document:doc-159#viewer2@user:fred + document:doc-160#viewer2@user:fred + document:doc-161#viewer2@user:fred + document:doc-162#viewer2@user:fred + document:doc-163#viewer2@user:fred + document:doc-164#viewer2@user:fred + document:doc-165#viewer2@user:fred + document:doc-166#viewer2@user:fred + document:doc-167#viewer2@user:fred + document:doc-168#viewer2@user:fred + document:doc-169#viewer2@user:fred + document:doc-170#viewer2@user:fred + document:doc-171#viewer2@user:fred + document:doc-172#viewer2@user:fred + document:doc-173#viewer2@user:fred + document:doc-174#viewer2@user:fred + document:doc-175#viewer2@user:fred + document:doc-176#viewer2@user:fred + document:doc-177#viewer2@user:fred + document:doc-178#viewer2@user:fred + document:doc-179#viewer2@user:fred + document:doc-180#viewer2@user:fred + document:doc-181#viewer2@user:fred + document:doc-182#viewer2@user:fred + document:doc-183#viewer2@user:fred + document:doc-184#viewer2@user:fred + document:doc-185#viewer2@user:fred + document:doc-186#viewer2@user:fred + document:doc-187#viewer2@user:fred + document:doc-188#viewer2@user:fred + document:doc-189#viewer2@user:fred + document:doc-190#viewer2@user:fred + document:doc-191#viewer2@user:fred + document:doc-192#viewer2@user:fred + document:doc-193#viewer2@user:fred + document:doc-194#viewer2@user:fred + document:doc-195#viewer2@user:fred + document:doc-196#viewer2@user:fred + document:doc-197#viewer2@user:fred + document:doc-198#viewer2@user:fred + document:doc-199#viewer2@user:fred + document:doc-200#viewer2@user:fred + document:doc-201#viewer2@user:fred + document:doc-202#viewer2@user:fred + document:doc-203#viewer2@user:fred + document:doc-204#viewer2@user:fred + document:doc-205#viewer2@user:fred + document:doc-206#viewer2@user:fred + document:doc-207#viewer2@user:fred + document:doc-208#viewer2@user:fred + document:doc-209#viewer2@user:fred + document:doc-210#viewer2@user:fred + document:doc-211#viewer2@user:fred + document:doc-212#viewer2@user:fred + document:doc-213#viewer2@user:fred + document:doc-214#viewer2@user:fred + document:doc-215#viewer2@user:fred + document:doc-216#viewer2@user:fred + document:doc-217#viewer2@user:fred + document:doc-218#viewer2@user:fred + document:doc-219#viewer2@user:fred + document:doc-220#viewer2@user:fred + document:doc-221#viewer2@user:fred + document:doc-222#viewer2@user:fred + document:doc-223#viewer2@user:fred + document:doc-224#viewer2@user:fred + document:doc-225#viewer2@user:fred + document:doc-226#viewer2@user:fred + document:doc-227#viewer2@user:fred + document:doc-228#viewer2@user:fred + document:doc-229#viewer2@user:fred + document:doc-230#viewer2@user:fred + document:doc-231#viewer2@user:fred + document:doc-232#viewer2@user:fred + document:doc-233#viewer2@user:fred + document:doc-234#viewer2@user:fred + document:doc-235#viewer2@user:fred + document:doc-236#viewer2@user:fred + document:doc-237#viewer2@user:fred + document:doc-238#viewer2@user:fred + document:doc-239#viewer2@user:fred + document:doc-240#viewer2@user:fred + document:doc-241#viewer2@user:fred + document:doc-242#viewer2@user:fred + document:doc-243#viewer2@user:fred + document:doc-244#viewer2@user:fred + document:doc-245#viewer2@user:fred + document:doc-246#viewer2@user:fred + document:doc-247#viewer2@user:fred + document:doc-248#viewer2@user:fred + document:doc-249#viewer2@user:fred + document:doc-250#viewer2@user:fred + document:doc-251#viewer2@user:fred + document:doc-252#viewer2@user:fred + document:doc-253#viewer2@user:fred + document:doc-254#viewer2@user:fred + document:doc-255#viewer2@user:fred + document:doc-256#viewer2@user:fred + document:doc-257#viewer2@user:fred + document:doc-258#viewer2@user:fred + document:doc-259#viewer2@user:fred + document:doc-260#viewer2@user:fred + document:doc-261#viewer2@user:fred + document:doc-262#viewer2@user:fred + document:doc-263#viewer2@user:fred + document:doc-264#viewer2@user:fred + document:doc-265#viewer2@user:fred + document:doc-266#viewer2@user:fred + document:doc-267#viewer2@user:fred + document:doc-268#viewer2@user:fred + document:doc-269#viewer2@user:fred + document:doc-270#viewer2@user:fred + document:doc-271#viewer2@user:fred + document:doc-272#viewer2@user:fred + document:doc-273#viewer2@user:fred + document:doc-274#viewer2@user:fred + document:doc-275#viewer2@user:fred + document:doc-276#viewer2@user:fred + document:doc-277#viewer2@user:fred + document:doc-278#viewer2@user:fred + document:doc-279#viewer2@user:fred + document:doc-280#viewer2@user:fred + document:doc-281#viewer2@user:fred + document:doc-282#viewer2@user:fred + document:doc-283#viewer2@user:fred + document:doc-284#viewer2@user:fred + document:doc-285#viewer2@user:fred + document:doc-286#viewer2@user:fred + document:doc-287#viewer2@user:fred + document:doc-288#viewer2@user:fred + document:doc-289#viewer2@user:fred + document:doc-290#viewer2@user:fred + document:doc-291#viewer2@user:fred + document:doc-292#viewer2@user:fred + document:doc-293#viewer2@user:fred + document:doc-294#viewer2@user:fred + document:doc-295#viewer2@user:fred + document:doc-296#viewer2@user:fred + document:doc-297#viewer2@user:fred + document:doc-298#viewer2@user:fred + document:doc-299#viewer2@user:fred + document:doc-300#viewer2@user:fred + document:doc-301#viewer2@user:fred + document:doc-302#viewer2@user:fred + document:doc-303#viewer2@user:fred + document:doc-304#viewer2@user:fred + document:doc-305#viewer2@user:fred + document:doc-306#viewer2@user:fred + document:doc-307#viewer2@user:fred + document:doc-308#viewer2@user:fred + document:doc-309#viewer2@user:fred + document:doc-310#viewer2@user:fred + document:doc-311#viewer2@user:fred + document:doc-312#viewer2@user:fred + document:doc-313#viewer2@user:fred + document:doc-314#viewer2@user:fred + document:doc-315#viewer2@user:fred + document:doc-316#viewer2@user:fred + document:doc-317#viewer2@user:fred + document:doc-318#viewer2@user:fred + document:doc-319#viewer2@user:fred + document:doc-320#viewer2@user:fred + document:doc-321#viewer2@user:fred + document:doc-322#viewer2@user:fred + document:doc-323#viewer2@user:fred + document:doc-324#viewer2@user:fred + document:doc-325#viewer2@user:fred + document:doc-326#viewer2@user:fred + document:doc-327#viewer2@user:fred + document:doc-328#viewer2@user:fred + document:doc-329#viewer2@user:fred + document:doc-330#viewer2@user:fred + document:doc-331#viewer2@user:fred + document:doc-332#viewer2@user:fred + document:doc-333#viewer2@user:fred + document:doc-334#viewer2@user:fred + document:doc-335#viewer2@user:fred + document:doc-336#viewer2@user:fred + document:doc-337#viewer2@user:fred + document:doc-338#viewer2@user:fred + document:doc-339#viewer2@user:fred + document:doc-340#viewer2@user:fred + document:doc-341#viewer2@user:fred + document:doc-342#viewer2@user:fred + document:doc-343#viewer2@user:fred + document:doc-344#viewer2@user:fred + document:doc-345#viewer2@user:fred + document:doc-346#viewer2@user:fred + document:doc-347#viewer2@user:fred + document:doc-348#viewer2@user:fred + document:doc-349#viewer2@user:fred + document:doc-350#viewer2@user:fred + document:doc-351#viewer2@user:fred + document:doc-352#viewer2@user:fred + document:doc-353#viewer2@user:fred + document:doc-354#viewer2@user:fred + document:doc-355#viewer2@user:fred + document:doc-356#viewer2@user:fred + document:doc-357#viewer2@user:fred + document:doc-358#viewer2@user:fred + document:doc-359#viewer2@user:fred + document:doc-360#viewer2@user:fred + document:doc-361#viewer2@user:fred + document:doc-362#viewer2@user:fred + document:doc-363#viewer2@user:fred + document:doc-364#viewer2@user:fred + document:doc-365#viewer2@user:fred + document:doc-366#viewer2@user:fred + document:doc-367#viewer2@user:fred + document:doc-368#viewer2@user:fred + document:doc-369#viewer2@user:fred + document:doc-370#viewer2@user:fred + document:doc-371#viewer2@user:fred + document:doc-372#viewer2@user:fred + document:doc-373#viewer2@user:fred + document:doc-374#viewer2@user:fred + document:doc-375#viewer2@user:fred + document:doc-376#viewer2@user:fred + document:doc-377#viewer2@user:fred + document:doc-378#viewer2@user:fred + document:doc-379#viewer2@user:fred + document:doc-380#viewer2@user:fred + document:doc-381#viewer2@user:fred + document:doc-382#viewer2@user:fred + document:doc-383#viewer2@user:fred + document:doc-384#viewer2@user:fred + document:doc-385#viewer2@user:fred + document:doc-386#viewer2@user:fred + document:doc-387#viewer2@user:fred + document:doc-388#viewer2@user:fred + document:doc-389#viewer2@user:fred + document:doc-390#viewer2@user:fred + document:doc-391#viewer2@user:fred + document:doc-392#viewer2@user:fred + document:doc-393#viewer2@user:fred + document:doc-394#viewer2@user:fred + document:doc-395#viewer2@user:fred + document:doc-396#viewer2@user:fred + document:doc-397#viewer2@user:fred + document:doc-398#viewer2@user:fred + document:doc-399#viewer2@user:fred + document:doc-400#viewer2@user:fred + document:doc-401#viewer2@user:fred + document:doc-402#viewer2@user:fred + document:doc-403#viewer2@user:fred + document:doc-404#viewer2@user:fred + document:doc-405#viewer2@user:fred + document:doc-406#viewer2@user:fred + document:doc-407#viewer2@user:fred + document:doc-408#viewer2@user:fred + document:doc-409#viewer2@user:fred + document:doc-410#viewer2@user:fred + document:doc-411#viewer2@user:fred + document:doc-412#viewer2@user:fred + document:doc-413#viewer2@user:fred + document:doc-414#viewer2@user:fred + document:doc-415#viewer2@user:fred + document:doc-416#viewer2@user:fred + document:doc-417#viewer2@user:fred + document:doc-418#viewer2@user:fred + document:doc-419#viewer2@user:fred + document:doc-420#viewer2@user:fred + document:doc-421#viewer2@user:fred + document:doc-422#viewer2@user:fred + document:doc-423#viewer2@user:fred + document:doc-424#viewer2@user:fred + document:doc-425#viewer2@user:fred + document:doc-426#viewer2@user:fred + document:doc-427#viewer2@user:fred + document:doc-428#viewer2@user:fred + document:doc-429#viewer2@user:fred + document:doc-430#viewer2@user:fred + document:doc-431#viewer2@user:fred + document:doc-432#viewer2@user:fred + document:doc-433#viewer2@user:fred + document:doc-434#viewer2@user:fred + document:doc-435#viewer2@user:fred + document:doc-436#viewer2@user:fred + document:doc-437#viewer2@user:fred + document:doc-438#viewer2@user:fred + document:doc-439#viewer2@user:fred + document:doc-440#viewer2@user:fred + document:doc-441#viewer2@user:fred + document:doc-442#viewer2@user:fred + document:doc-443#viewer2@user:fred + document:doc-444#viewer2@user:fred + document:doc-445#viewer2@user:fred + document:doc-446#viewer2@user:fred + document:doc-447#viewer2@user:fred + document:doc-448#viewer2@user:fred + document:doc-449#viewer2@user:fred